Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,11 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-tcnative-classes</artifactId>
<version>2.0.76.Final</version>
</dependency>
<dependency>
<groupId>com.aayushatharva.brotli4j</groupId>
<artifactId>brotli4j</artifactId>
Expand Down
1 change: 1 addition & 0 deletions extensions/tls-registry/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
</dependency>

<!-- To register the let's encrypt routes -->

<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ public synchronized SSLOptions getSSLOptions() {
options.setKeyCertOptions(getKeyStoreOptions());
options.setTrustOptions(getTrustStoreOptions());
options.setUseAlpn(config().alpn());
options.setUseHybrid(config().hybrid());
options.setSslHandshakeTimeoutUnit(TimeUnit.SECONDS);
options.setSslHandshakeTimeout(config().handshakeTimeout().toSeconds());
options.setEnabledSecureTransportProtocols(config().protocols());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ public interface TlsBucketConfig {
@WithDefault("true")
boolean alpn();

/**
* Enables the hybrid key exchange protocol.
*/
@WithDefault("false")
boolean hybrid();

/**
* Sets the list of revoked certificates (paths to files).
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ public boolean alpn() {
return false;
}

@Override
public boolean hybrid() {
return true;
}

@Override
public Duration handshakeTimeout() {
return Duration.ofSeconds(10);
Expand All @@ -83,6 +88,11 @@ void testDefault() {
assertFalse(holder.warnIfOldProtocols(Set.of(TlsBucketConfig.DEFAULT_TLS_PROTOCOLS.toLowerCase()), "test"));
}

@Test
void testHybrid() {
assertTrue(holder.getSSLOptions().isUseHybrid());
}

@Test
void testWarnIfOldProtocols_withSSL() {
assertTrue(holder.warnIfOldProtocols(Set.of("SSLv3"), "test"));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package io.quarkus.vertx.http.tls;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.File;
import java.net.URL;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.quarkus.test.QuarkusExtensionTest;
import io.quarkus.test.common.http.TestHTTPResource;
import io.smallrye.certs.Format;
import io.smallrye.certs.junit5.Certificate;
import io.smallrye.certs.junit5.Certificates;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.JksOptions;
import io.vertx.core.net.OpenSSLEngineOptions;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.client.HttpResponse;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.client.WebClientOptions;

@EnabledIf("isOpenSslAvailable")
@Certificates(baseDir = "target/certs", certificates = @Certificate(name = "mtls-hybrid-test", password = "secret", formats = {
Format.JKS, Format.PKCS12, Format.PEM }, client = true))
public class HybridKeyExchangeMtlsTest {

@TestHTTPResource(value = "/hybrid-mtls", tls = true)
URL url;

@RegisterExtension
static final QuarkusExtensionTest config = new QuarkusExtensionTest()
.withApplicationRoot((jar) -> jar
.addClasses(MyBean.class)
.addAsResource(new File("target/certs/mtls-hybrid-test-keystore.jks"), "server-keystore.jks")
.addAsResource(new File("target/certs/mtls-hybrid-test-server-truststore.jks"),
"server-truststore.jks"))
.overrideConfigKey("quarkus.tls.key-store.jks.path", "server-keystore.jks")
.overrideConfigKey("quarkus.tls.key-store.jks.password", "secret")
.overrideConfigKey("quarkus.tls.trust-store.jks.path", "server-truststore.jks")
.overrideConfigKey("quarkus.tls.trust-store.jks.password", "secret")
.overrideConfigKey("quarkus.tls.hybrid", "true")
.overrideConfigKey("quarkus.http.ssl.client-auth", "REQUIRED")
.overrideConfigKey("quarkus.http.insecure-requests", "disabled");

@Inject
Vertx vertx;

@Test
void testHybridKeyExchangeWithMtls() {
WebClientOptions options = new WebClientOptions();
options.setSsl(true);
options.setSslEngineOptions(new OpenSSLEngineOptions());
options.setUseHybrid(true);
options.setTrustAll(true);
options.setKeyCertOptions(new JksOptions()
.setPath("target/certs/mtls-hybrid-test-client-keystore.jks")
.setPassword("secret"));

WebClient client = WebClient.create(vertx, options);
HttpResponse<Buffer> response = client.getAbs(url.toExternalForm())
.send().toCompletionStage().toCompletableFuture().join();
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.bodyAsString()).isEqualTo("mtls-hybrid-ok");
}

static boolean isOpenSslAvailable() {
try {
SslContext ctx = SslContextBuilder.forClient()
.sslProvider(SslProvider.OPENSSL)
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.build();
return true;
} catch (Throwable e) {
return false;
}
}

@ApplicationScoped
static class MyBean {

public void register(@Observes Router router) {
router.get("/hybrid-mtls").handler(rc -> {
assertThat(rc.request().connection().isSsl()).isTrue();
assertThat(rc.request().isSSL()).isTrue();
assertThat(rc.request().connection().sslSession()).isNotNull();
assertThat(rc.request().connection().sslSession().getProtocol()).isEqualTo("TLSv1.3");
try {
assertThat(rc.request().connection().sslSession().getPeerCertificates()).isNotEmpty();
} catch (javax.net.ssl.SSLPeerUnverifiedException e) {
throw new RuntimeException(e);
}
rc.response().end("mtls-hybrid-ok");
});
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package io.quarkus.vertx.http.tls;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.io.File;
import java.net.URL;

import javax.net.ssl.SSLException;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.quarkus.test.QuarkusExtensionTest;
import io.quarkus.test.common.http.TestHTTPResource;
import io.smallrye.certs.Format;
import io.smallrye.certs.junit5.Certificate;
import io.smallrye.certs.junit5.Certificates;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.OpenSSLEngineOptions;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.client.HttpResponse;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.client.WebClientOptions;

@EnabledIf("isOpenSslAvailable")
@Certificates(baseDir = "target/certs", certificates = @Certificate(name = "ssl-hybrid-test", password = "secret", formats = {
Format.JKS, Format.PKCS12, Format.PEM }))
public class HybridKeyExchangeTest {

@TestHTTPResource(value = "/hybrid", tls = true)
URL url;

@RegisterExtension
static final QuarkusExtensionTest config = new QuarkusExtensionTest()
.withApplicationRoot((jar) -> jar
.addClasses(MyBean.class)
.addAsResource(new File("target/certs/ssl-hybrid-test.key"), "server-key.pem")
.addAsResource(new File("target/certs/ssl-hybrid-test.crt"), "server-cert.pem"))
.overrideConfigKey("quarkus.tls.key-store.pem.0.cert", "server-cert.pem")
.overrideConfigKey("quarkus.tls.key-store.pem.0.key", "server-key.pem")
.overrideConfigKey("quarkus.tls.hybrid", "true")
.overrideConfigKey("quarkus.http.insecure-requests", "disabled");

@Inject
Vertx vertx;

@Test
void testHybridKeyExchangeHandshake() {
WebClientOptions options = new WebClientOptions();
options.setSsl(true);
options.setSslEngineOptions(new OpenSSLEngineOptions());
options.setUseHybrid(true);
options.setTrustAll(true);

WebClient client = WebClient.create(vertx, options);
HttpResponse<Buffer> response = client.getAbs(url.toExternalForm())
.send().toCompletionStage().toCompletableFuture().join();
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.bodyAsString()).isEqualTo("hybrid-ok");
}

@Test
void testNonHybridClientRejected() {
WebClientOptions options = new WebClientOptions();
options.setSsl(true);
options.setTrustAll(true);

WebClient client = WebClient.create(vertx, options);
assertThatThrownBy(() -> client.getAbs(url.toExternalForm())
.send().toCompletionStage().toCompletableFuture().join())
.hasRootCauseInstanceOf(SSLException.class);
}

static boolean isOpenSslAvailable() {
try {
SslContext ctx = SslContextBuilder.forClient()
.sslProvider(SslProvider.OPENSSL)
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.build();
return true;
} catch (Throwable e) {
return false;
}
}

@ApplicationScoped
static class MyBean {

public void register(@Observes Router router) {
router.get("/hybrid").handler(rc -> {
assertThat(rc.request().connection().isSsl()).isTrue();
assertThat(rc.request().isSSL()).isTrue();
assertThat(rc.request().connection().sslSession()).isNotNull();
assertThat(rc.request().connection().sslSession().getProtocol()).isEqualTo("TLSv1.3");
rc.response().end("hybrid-ok");
});
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import io.vertx.core.http.HttpVersion;
import io.vertx.core.net.JdkSSLEngineOptions;
import io.vertx.core.net.KeyCertOptions;
import io.vertx.core.net.OpenSSLEngineOptions;
import io.vertx.core.net.TCPSSLOptions;
import io.vertx.core.net.TrafficShapingOptions;
import io.vertx.core.net.TrustOptions;
Expand Down Expand Up @@ -253,6 +254,10 @@ public static void applyTlsConfigurationToHttpServerOptions(TlsConfiguration buc
if (!other.isUseAlpn()) {
serverOptions.setUseAlpn(false);
}
if (other.isUseHybrid()) {
serverOptions.setSslEngineOptions(new OpenSSLEngineOptions());
serverOptions.setUseHybrid(true);
}
serverOptions.setEnabledSecureTransportProtocols(other.getEnabledSecureTransportProtocols());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import io.vertx.core.http.HttpClientRequest;
import io.vertx.core.http.HttpVersion;
import io.vertx.core.net.JksOptions;
import io.vertx.core.net.OpenSSLEngineOptions;
import io.vertx.core.net.ProxyOptions;
import io.vertx.core.net.ProxyType;
import io.vertx.core.net.SSLOptions;
Expand Down Expand Up @@ -395,6 +396,10 @@ private void populateSecurityOptionsFromTlsConfig(HttpClientOptions options) {
}
options.setEnabledSecureTransportProtocols(sslOptions.getEnabledSecureTransportProtocols());
options.setUseAlpn(sslOptions.isUseAlpn());
if (sslOptions.isUseHybrid()) {
options.setSslEngineOptions(new OpenSSLEngineOptions());
options.setUseHybrid(true);
}
}
}

Expand Down