From 272119dcbec1b7a0fbff96c08729b86223bce258 Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Thu, 6 May 2021 08:17:36 +0200 Subject: [PATCH] Add support for SNI (Server Name Indication) for the HTTP server. With SNI enabled, the server can handle multiple certificates. During the TLS handshake, the client indicates the service name allowing the service to looks for the correct certificate and completes the handshake. The JKS and PKCS12 format allow the key stores to contain multiple certificates. However, the PEM format does not allow that. For this reason, the configuration of the PEM certificate and key are now accepting lists of paths. The previous (singular) form are deprecated but still supported. SNI must be enabled explicitly as the server cannot verify if the key stores contain multiple certificates (JKS / PKCS12). This commit does not enable SNI on gRPC, as the gRPC server does not handle it. See https://github.com/vert-x3/vertx-grpc/issues/70 for details. Fix #16851 --- .../src/test/resources/conf/ssl-pem.conf | 4 +- .../vertx/http/runtime/CertificateConfig.java | 25 ++++++++++ .../vertx/http/runtime/ServerSslConfig.java | 7 +++ .../vertx/http/runtime/VertxHttpRecorder.java | 50 ++++++++++++++++--- 4 files changed, 76 insertions(+), 10 deletions(-) diff --git a/extensions/vertx-http/deployment/src/test/resources/conf/ssl-pem.conf b/extensions/vertx-http/deployment/src/test/resources/conf/ssl-pem.conf index 939d00cde91b0..4cbb70dee720e 100644 --- a/extensions/vertx-http/deployment/src/test/resources/conf/ssl-pem.conf +++ b/extensions/vertx-http/deployment/src/test/resources/conf/ssl-pem.conf @@ -1,6 +1,6 @@ # Enable SSL, configure the key store -quarkus.http.ssl.certificate.file=server-cert.pem -quarkus.http.ssl.certificate.key-file=server-key.pem +quarkus.http.ssl.certificate.files=server-cert.pem +quarkus.http.ssl.certificate.key-files=server-key.pem # Test that server starts with this option # See https://github.com/quarkusio/quarkus/issues/8336 quarkus.http.insecure-requests=disabled \ No newline at end of file diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CertificateConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CertificateConfig.java index d56d4b28d6405..40a0206a4f504 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CertificateConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CertificateConfig.java @@ -1,6 +1,7 @@ package io.quarkus.vertx.http.runtime; import java.nio.file.Path; +import java.util.List; import java.util.Optional; import io.quarkus.runtime.annotations.ConfigGroup; @@ -10,19 +11,43 @@ * A certificate configuration. Either the certificate and key files must be given, or a key store must be given. */ @ConfigGroup +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") public class CertificateConfig { + /** * The file path to a server certificate or certificate chain in PEM format. + * + * @deprecated Use {@link #files} instead. */ @ConfigItem + @Deprecated public Optional file; + /** + * The list of path to server certificates using the PEM format. + * Specifying multiple files require SNI to be enabled. + */ + @ConfigItem + public Optional> files; + /** * The file path to the corresponding certificate private key file in PEM format. + * + * @deprecated Use {@link #keyFiles} instead. */ @ConfigItem + @Deprecated public Optional keyFile; + /** + * The list of path to server certificates private key file using the PEM format. + * Specifying multiple files require SNI to be enabled. + * + * The order of the key files must match the order of the certificates. + */ + @ConfigItem + public Optional> keyFiles; + /** * An optional key store which holds the certificate information instead of specifying separate files. */ diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ServerSslConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ServerSslConfig.java index 406d88c1bd634..f57a3f27b4d29 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ServerSslConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ServerSslConfig.java @@ -30,4 +30,11 @@ public class ServerSslConfig { @ConfigItem(defaultValue = "TLSv1.3,TLSv1.2") public List protocols; + /** + * Enables Server Name Indication (SNI), an TLS extension allowing the server to use multiple certificates. + * The client indicate the server name during the TLS handshake, allowing the server to select the right certificate. + */ + @ConfigItem(defaultValue = "false") + public boolean sni; + } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java index 34325801a3d4e..971871c9e06f8 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java @@ -53,6 +53,7 @@ import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.runtime.configuration.ConfigInstantiator; +import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.runtime.configuration.MemorySize; import io.quarkus.runtime.shutdown.ShutdownConfig; import io.quarkus.vertx.core.runtime.VertxCoreRecorder; @@ -578,9 +579,24 @@ private static HttpServerOptions createSslOptions(HttpBuildTimeConfig buildTimeC } ServerSslConfig sslConfig = httpConfiguration.ssl; - //TODO: static fields break config + final Optional certFile = sslConfig.certificate.file; final Optional keyFile = sslConfig.certificate.keyFile; + final List keys = new ArrayList<>(); + final List certificates = new ArrayList<>(); + if (sslConfig.certificate.keyFiles.isPresent()) { + keys.addAll(sslConfig.certificate.keyFiles.get()); + } + if (sslConfig.certificate.files.isPresent()) { + certificates.addAll(sslConfig.certificate.files.get()); + } + if (keyFile.isPresent()) { + keys.add(keyFile.get()); + } + if (certFile.isPresent()) { + certificates.add(certFile.get()); + } + final Optional keyStoreFile = sslConfig.certificate.keyStoreFile; final String keystorePassword = sslConfig.certificate.keyStorePassword; final Optional trustStoreFile = sslConfig.certificate.trustStoreFile; @@ -599,8 +615,8 @@ private static HttpServerOptions createSslOptions(HttpBuildTimeConfig buildTimeC serverOptions.setMaxFormAttributeSize(httpConfiguration.limits.maxFormAttributeSize.asBigInteger().intValueExact()); setIdleTimeout(httpConfiguration, serverOptions); - if (certFile.isPresent() && keyFile.isPresent()) { - createPemKeyCertOptions(certFile.get(), keyFile.get(), serverOptions); + if (!certificates.isEmpty() && !keys.isEmpty()) { + createPemKeyCertOptions(certificates, keys, serverOptions); } else if (keyStoreFile.isPresent()) { final Path keyStorePath = keyStoreFile.get(); final Optional keyStoreFileType = sslConfig.certificate.keyStoreFileType; @@ -649,6 +665,7 @@ private static HttpServerOptions createSslOptions(HttpBuildTimeConfig buildTimeC } } serverOptions.setSsl(true); + serverOptions.setSni(sslConfig.sni); serverOptions.setHost(httpConfiguration.host); serverOptions.setPort(httpConfiguration.determineSslPort(launchMode)); serverOptions.setClientAuth(buildTimeConfig.tlsClientAuth); @@ -676,13 +693,30 @@ private static byte[] getFileContent(Path path) throws IOException { return data; } - private static void createPemKeyCertOptions(Path certFile, Path keyFile, + private static void createPemKeyCertOptions(List certFile, List keyFile, HttpServerOptions serverOptions) throws IOException { - final byte[] cert = getFileContent(certFile); - final byte[] key = getFileContent(keyFile); + + if (certFile.size() != keyFile.size()) { + throw new ConfigurationException("Invalid certificate configuration - `files` and `keyFiles` must have the " + + "same number of elements"); + } + + List certificates = new ArrayList<>(); + List keys = new ArrayList<>(); + + for (Path p : certFile) { + final byte[] cert = getFileContent(p); + certificates.add(Buffer.buffer(cert)); + } + + for (Path p : keyFile) { + final byte[] key = getFileContent(p); + keys.add(Buffer.buffer(key)); + } + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() - .setCertValue(Buffer.buffer(cert)) - .setKeyValue(Buffer.buffer(key)); + .setCertValues(certificates) + .setKeyValues(keys); serverOptions.setPemKeyCertOptions(pemKeyCertOptions); }