Skip to content

Commit

Permalink
Add support for encrypted PKCS#8
Browse files Browse the repository at this point in the history
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 quarkusio#44262
  • Loading branch information
cescoffier committed Nov 17, 2024
1 parent 19ee291 commit 3c764a4
Show file tree
Hide file tree
Showing 10 changed files with 247 additions and 4 deletions.
6 changes: 6 additions & 0 deletions bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,12 @@
<version>${smallrye-certificate-generator.version}</version>
</dependency>

<dependency>
<groupId>io.smallrye.certs</groupId>
<artifactId>smallrye-private-key-pem-parser</artifactId>
<version>${smallrye-certificate-generator.version}</version>
</dependency>

<!-- Jackson dependencies, imported as a BOM -->
<dependency>
<groupId>com.fasterxml.jackson</groupId>
Expand Down
15 changes: 15 additions & 0 deletions docs/src/main/asciidoc/tls-registry-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.<name>.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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
});
}
}
Original file line number Diff line number Diff line change
@@ -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.");
}
}
Original file line number Diff line number Diff line change
@@ -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.");
}
}
Original file line number Diff line number Diff line change
@@ -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");
});
}
}
4 changes: 4 additions & 0 deletions extensions/tls-registry/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-credentials</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye.certs</groupId>
<artifactId>smallrye-private-key-pem-parser</artifactId>
</dependency>

<!-- To register the let's encrypt routes -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -62,22 +64,37 @@ 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;
}

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();

/**
* 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<String> password();
}

}
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
<proto-google-common-protos.version>2.46.0</proto-google-common-protos.version>

<!-- Used in the build parent and test BOM (for the junit 5 plugin) and in the BOM (for the API) -->
<smallrye-certificate-generator.version>0.8.1</smallrye-certificate-generator.version>
<smallrye-certificate-generator.version>0.9.2</smallrye-certificate-generator.version>

<!-- TestNG version: we don't enforce it in the BOM as it is mostly used in the MP TCKs and we need to use the version from the TCKs -->
<testng.version>7.8.0</testng.version>
Expand Down

0 comments on commit 3c764a4

Please sign in to comment.