From 3c764a4b35eb99465e3a7cdd60041440da88c536 Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Fri, 15 Nov 2024 10:11:00 +0100 Subject: [PATCH] Add support for encrypted PKCS#8 Add support for encrypted PEM file (encrypted PKCS#8) to the TLS registry. This is invisible for the extensions, the key being decrypted and passed unencrypted to the extensions (the unencrypted key is never written on disk). Fix https://github.com/quarkusio/quarkus/issues/44262 --- bom/application/pom.xml | 6 ++ .../main/asciidoc/tls-registry-reference.adoc | 15 +++++ .../cli/letsencrypt/LetsEncryptHelpers.java | 2 +- .../tls/DefaultEncryptedPemKeyStoreTest.java | 58 ++++++++++++++++++ .../tls/EncryptedPemWithNoPasswordTest.java | 41 +++++++++++++ .../EncryptedPemWithWrongPasswordTest.java | 42 +++++++++++++ .../tls/NamedEncryptedPemKeyStoreTest.java | 60 +++++++++++++++++++ extensions/tls-registry/runtime/pom.xml | 4 ++ .../tls/runtime/config/PemKeyCertConfig.java | 21 ++++++- pom.xml | 2 +- 10 files changed, 247 insertions(+), 4 deletions(-) create mode 100644 extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/DefaultEncryptedPemKeyStoreTest.java create mode 100644 extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/EncryptedPemWithNoPasswordTest.java create mode 100644 extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/EncryptedPemWithWrongPasswordTest.java create mode 100644 extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/NamedEncryptedPemKeyStoreTest.java diff --git a/bom/application/pom.xml b/bom/application/pom.xml index f68f45192e685..bf1df9f0ba64d 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -337,6 +337,12 @@ ${smallrye-certificate-generator.version} + + io.smallrye.certs + smallrye-private-key-pem-parser + ${smallrye-certificate-generator.version} + + com.fasterxml.jackson diff --git a/docs/src/main/asciidoc/tls-registry-reference.adoc b/docs/src/main/asciidoc/tls-registry-reference.adoc index 2e7322e56b72f..d9f9fe505f84d 100644 --- a/docs/src/main/asciidoc/tls-registry-reference.adoc +++ b/docs/src/main/asciidoc/tls-registry-reference.adoc @@ -228,6 +228,21 @@ For example, `quarkus.tls.key-store.pem.order=b,c,a`. This setting is important when using SNI, because it uses the first specified pair as the default. ==== +When using PEM keystore, the following formats are supported: + +- PKCS#8 private key (unencrypted) +- PKCS#1 RSA private key (unencrypted) +- Encrypted PKCS#8 private key (encrypted with AES-128-CBC) + +In the later case, the `quarkus.tls.key-store.pem.password` (or `quarkus.tls.key-store.pem..password`) property must be set to the password used to decrypt the private key: + +[source,properties] +---- +quarkus.tls.http.key-store.pem.cert=certificate.crt +quarkus.tls.http.key-store.pem.key=key.key +quarkus.tls.http.key-store.pem.password=password +---- + ==== PKCS12 keystores PKCS12 keystores are single files that contain the certificate and the private key. diff --git a/extensions/tls-registry/cli/src/main/java/io/quarkus/tls/cli/letsencrypt/LetsEncryptHelpers.java b/extensions/tls-registry/cli/src/main/java/io/quarkus/tls/cli/letsencrypt/LetsEncryptHelpers.java index be91516fa9916..43d7188717b00 100644 --- a/extensions/tls-registry/cli/src/main/java/io/quarkus/tls/cli/letsencrypt/LetsEncryptHelpers.java +++ b/extensions/tls-registry/cli/src/main/java/io/quarkus/tls/cli/letsencrypt/LetsEncryptHelpers.java @@ -44,7 +44,7 @@ public static void writePrivateKeyAndCertificateChainsAsPem(PrivateKey pk, X509C throw new IllegalArgumentException("The certificate chain cannot be null or empty"); } - CertificateUtils.writePrivateKeyToPem(pk, privateKeyFile); + CertificateUtils.writePrivateKeyToPem(pk, null, privateKeyFile); if (chain.length == 1) { CertificateUtils.writeCertificateToPEM(chain[0], certificateChainFile); diff --git a/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/DefaultEncryptedPemKeyStoreTest.java b/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/DefaultEncryptedPemKeyStoreTest.java new file mode 100644 index 0000000000000..c73679a177791 --- /dev/null +++ b/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/DefaultEncryptedPemKeyStoreTest.java @@ -0,0 +1,58 @@ +package io.quarkus.tls; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.security.KeyStoreException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; + +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.certs.Format; +import io.smallrye.certs.junit5.Certificate; +import io.smallrye.certs.junit5.Certificates; + +@Certificates(baseDir = "target/certs", certificates = { + @Certificate(name = "test-formats-encrypted-pem", password = "password", formats = { Format.JKS, Format.ENCRYPTED_PEM, + Format.PKCS12 }) +}) +public class DefaultEncryptedPemKeyStoreTest { + + private static final String configuration = """ + quarkus.tls.key-store.pem.foo.cert=target/certs/test-formats-encrypted-pem.crt + quarkus.tls.key-store.pem.foo.key=target/certs/test-formats-encrypted-pem.key + quarkus.tls.key-store.pem.foo.password=password + """; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .add(new StringAsset(configuration), "application.properties")); + + @Inject + TlsConfigurationRegistry certificates; + + @Test + void test() throws KeyStoreException, CertificateParsingException { + TlsConfiguration def = certificates.getDefault().orElseThrow(); + + assertThat(def.getKeyStoreOptions()).isNotNull(); + assertThat(def.getKeyStore()).isNotNull(); + + // dummy-entry-x is the alias of the certificate in the keystore generated by Vert.x. + + X509Certificate certificate = (X509Certificate) def.getKeyStore().getCertificate("dummy-entry-0"); + assertThat(certificate).isNotNull(); + assertThat(certificate.getSubjectAlternativeNames()).anySatisfy(l -> { + assertThat(l.get(0)).isEqualTo(2); + assertThat(l.get(1)).isEqualTo("localhost"); + }); + } +} diff --git a/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/EncryptedPemWithNoPasswordTest.java b/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/EncryptedPemWithNoPasswordTest.java new file mode 100644 index 0000000000000..b254644cfd78a --- /dev/null +++ b/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/EncryptedPemWithNoPasswordTest.java @@ -0,0 +1,41 @@ +package io.quarkus.tls; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import java.security.KeyStoreException; +import java.security.cert.CertificateParsingException; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.certs.Format; +import io.smallrye.certs.junit5.Certificate; +import io.smallrye.certs.junit5.Certificates; + +@Certificates(baseDir = "target/certs", certificates = { + @Certificate(name = "test-formats-encrypted-pem", password = "password", formats = { Format.JKS, Format.ENCRYPTED_PEM, + Format.PKCS12 }) +}) +public class EncryptedPemWithNoPasswordTest { + + private static final String configuration = """ + quarkus.tls.key-store.pem.foo.cert=target/certs/test-formats-encrypted-pem.crt + quarkus.tls.key-store.pem.foo.key=target/certs/test-formats-encrypted-pem.key + """; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .add(new StringAsset(configuration), "application.properties")) + .assertException(t -> assertThat(t.getMessage()).contains("key/certificate pair", "default")); + + @Test + void test() throws KeyStoreException, CertificateParsingException { + fail("Should not be called as the extension should fail before."); + } +} diff --git a/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/EncryptedPemWithWrongPasswordTest.java b/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/EncryptedPemWithWrongPasswordTest.java new file mode 100644 index 0000000000000..34e027fb030ce --- /dev/null +++ b/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/EncryptedPemWithWrongPasswordTest.java @@ -0,0 +1,42 @@ +package io.quarkus.tls; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import java.security.KeyStoreException; +import java.security.cert.CertificateParsingException; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.certs.Format; +import io.smallrye.certs.junit5.Certificate; +import io.smallrye.certs.junit5.Certificates; + +@Certificates(baseDir = "target/certs", certificates = { + @Certificate(name = "test-formats-encrypted-pem", password = "password", formats = { Format.JKS, Format.ENCRYPTED_PEM, + Format.PKCS12 }) +}) +public class EncryptedPemWithWrongPasswordTest { + + private static final String configuration = """ + quarkus.tls.key-store.pem.foo.cert=target/certs/test-formats-encrypted-pem.crt + quarkus.tls.key-store.pem.foo.key=target/certs/test-formats-encrypted-pem.key + quarkus.tls.key-store.pem.foo.password=wrong + """; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .add(new StringAsset(configuration), "application.properties")) + .assertException(t -> assertThat(t.getMessage()).contains("key/certificate pair", "default")); + + @Test + void test() throws KeyStoreException, CertificateParsingException { + fail("Should not be called as the extension should fail before."); + } +} diff --git a/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/NamedEncryptedPemKeyStoreTest.java b/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/NamedEncryptedPemKeyStoreTest.java new file mode 100644 index 0000000000000..d3c199bac8c8e --- /dev/null +++ b/extensions/tls-registry/deployment/src/test/java/io/quarkus/tls/NamedEncryptedPemKeyStoreTest.java @@ -0,0 +1,60 @@ +package io.quarkus.tls; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.security.KeyStoreException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; + +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.certs.Format; +import io.smallrye.certs.junit5.Certificate; +import io.smallrye.certs.junit5.Certificates; + +@Certificates(baseDir = "target/certs", certificates = { + @Certificate(name = "test-formats-encrypted-pem", password = "password", formats = { Format.JKS, Format.ENCRYPTED_PEM, + Format.PKCS12 }) +}) +public class NamedEncryptedPemKeyStoreTest { + + private static final String configuration = """ + quarkus.tls.http.key-store.pem.foo.cert=target/certs/test-formats-encrypted-pem.crt + quarkus.tls.http.key-store.pem.foo.key=target/certs/test-formats-encrypted-pem.key + quarkus.tls.http.key-store.pem.foo.password=password + """; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .add(new StringAsset(configuration), "application.properties")); + + @Inject + TlsConfigurationRegistry certificates; + + @Test + void test() throws KeyStoreException, CertificateParsingException { + TlsConfiguration def = certificates.getDefault().orElseThrow(); + TlsConfiguration named = certificates.get("http").orElseThrow(); + + assertThat(def.getKeyStoreOptions()).isNull(); + assertThat(def.getKeyStore()).isNull(); + + assertThat(named.getKeyStoreOptions()).isNotNull(); + assertThat(named.getKeyStore()).isNotNull(); + + X509Certificate certificate = (X509Certificate) named.getKeyStore().getCertificate("dummy-entry-0"); + assertThat(certificate).isNotNull(); + assertThat(certificate.getSubjectAlternativeNames()).anySatisfy(l -> { + assertThat(l.get(0)).isEqualTo(2); + assertThat(l.get(1)).isEqualTo("localhost"); + }); + } +} diff --git a/extensions/tls-registry/runtime/pom.xml b/extensions/tls-registry/runtime/pom.xml index 930ce841182e1..b28c0f94577a3 100644 --- a/extensions/tls-registry/runtime/pom.xml +++ b/extensions/tls-registry/runtime/pom.xml @@ -26,6 +26,10 @@ io.quarkus quarkus-credentials + + io.smallrye.certs + smallrye-private-key-pem-parser + diff --git a/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/config/PemKeyCertConfig.java b/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/config/PemKeyCertConfig.java index 73d2e4608305b..6288e6b233a70 100644 --- a/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/config/PemKeyCertConfig.java +++ b/extensions/tls-registry/runtime/src/main/java/io/quarkus/tls/runtime/config/PemKeyCertConfig.java @@ -2,6 +2,7 @@ import static io.quarkus.tls.runtime.config.TlsConfigUtils.read; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; @@ -10,6 +11,7 @@ import java.util.TreeMap; import io.quarkus.runtime.annotations.ConfigGroup; +import io.smallrye.certs.pem.parsers.EncryptedPKCS8Parser; import io.smallrye.config.WithParentName; import io.vertx.core.buffer.Buffer; import io.vertx.core.net.PemKeyCertOptions; @@ -62,7 +64,17 @@ default PemKeyCertOptions toOptions() { for (KeyCertConfig config : orderedListOfPair) { options.addCertValue(Buffer.buffer(read(config.cert()))); - options.addKeyValue(Buffer.buffer(read(config.key()))); + if (config.password().isPresent()) { + byte[] content = read(config.key()); + String contentAsString = new String(content, StandardCharsets.UTF_8); + Buffer decrypted = new EncryptedPKCS8Parser().decryptKey(contentAsString, config.password().get()); + if (decrypted == null) { + throw new IllegalArgumentException("Unable to decrypt the key file: " + config.key()); + } + options.addKeyValue(decrypted); + } else { + options.addKeyValue(Buffer.buffer(read(config.key()))); + } } return options; } @@ -70,7 +82,7 @@ default PemKeyCertOptions toOptions() { interface KeyCertConfig { /** - * The path to the key file (in PEM format). + * The path to the key file (in PEM format: PKCS#8, PKCS#1 or encrypted PKCS#8). */ Path key(); @@ -78,6 +90,11 @@ interface KeyCertConfig { * The path to the certificate file (in PEM format). */ Path cert(); + + /** + * When the key is encrypted (encrypted PKCS#8), the password to decrypt it. + */ + Optional password(); } } diff --git a/pom.xml b/pom.xml index 94c31a88da386..21e15f2a56d1a 100644 --- a/pom.xml +++ b/pom.xml @@ -87,7 +87,7 @@ 2.46.0 - 0.8.1 + 0.9.2 7.8.0