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