Skip to content

Commit

Permalink
Add support for encrypted PKCS#8 (#19)
Browse files Browse the repository at this point in the history
Also added some utilities to handle PKCS#1 and plain PKCS#8
  • Loading branch information
cescoffier authored Nov 14, 2024
1 parent 3bdd579 commit e31b363
Show file tree
Hide file tree
Showing 22 changed files with 1,649 additions and 21 deletions.
6 changes: 6 additions & 0 deletions certificate-generator/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
<artifactId>smallrye-common-os</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>io.smallrye.certs</groupId>
<artifactId>private-key-pem-parser</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<!-- TODO Should we drop this dependency to use our own implementation? -->
<groupId>com.googlecode.plist</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ private CertificateFiles writePem(String name, CertificateHolder holder, Path ro
CertificateUtils.writeCertificateToPEM(serverCert, certFile);
}
if (replaceIfExists || !keyFile.isFile()) {
CertificateUtils.writePrivateKeyToPem(serverKey.getPrivate(), keyFile);
CertificateUtils.writePrivateKeyToPem(serverKey.getPrivate(), request.getPassword(), keyFile);
}
if (replaceIfExists || !clientTrustFile.isFile()) {
writeTruststoreToPem(List.of(serverCert), clientTrustFile);
Expand All @@ -178,7 +178,7 @@ private CertificateFiles writePem(String name, CertificateHolder holder, Path ro
CertificateUtils.writeCertificateToPEM(clientCert, clientCertFile);
}
if (replaceIfExists || !clientKeyFile.isFile()) {
CertificateUtils.writePrivateKeyToPem(clientKey.getPrivate(), clientKeyFile);
CertificateUtils.writePrivateKeyToPem(clientKey.getPrivate(), request.getPassword(), clientKeyFile);
}
if (replaceIfExists || !serverTrustfile.isFile()) {
writeTruststoreToPem(List.of(clientCert), serverTrustfile);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
Expand All @@ -27,6 +29,12 @@
import java.util.List;
import java.util.Map;

import javax.crypto.Cipher;
import javax.crypto.EncryptedPrivateKeyInfo;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import javax.security.auth.x500.X500Principal;

import org.bouncycastle.asn1.ASN1Encodable;
Expand Down Expand Up @@ -188,12 +196,46 @@ public static void writeCertificateToPEM(X509Certificate certificate, File outpu
}
}

public static void writePrivateKeyToPem(PrivateKey privateKey, File output) throws Exception {
// Define PBE parameters
private static final String PBE_ALGORITHM = "PBEWithSHA1AndDESede";
private static final int ITERATION_COUNT = 2048;
private static final int SALT_SIZE = 8; // 8 bytes of salt

private static byte[] generateSalt() {
byte[] salt = new byte[SALT_SIZE];
SecureRandom random = new SecureRandom();
random.nextBytes(salt);
return salt;
}

public static void writePrivateKeyToPem(PrivateKey privateKey, String password, File output) throws Exception {

byte[] content = privateKey.getEncoded();
;
if (password != null) {
byte[] salt = generateSalt();
PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray());
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(PBE_ALGORITHM);
SecretKey secretKey = keyFactory.generateSecret(pbeKeySpec);
Cipher cipher = Cipher.getInstance(PBE_ALGORITHM);
PBEParameterSpec pbeParamSpec = new PBEParameterSpec(salt, ITERATION_COUNT);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, pbeParamSpec);

// Encode the private key in PKCS#8 format
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKey.getEncoded());
byte[] encryptedKeyBytes = cipher.doFinal(pkcs8EncodedKeySpec.getEncoded());

// Wrap encrypted data in EncryptedPrivateKeyInfo
EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(cipher.getParameters(),
encryptedKeyBytes);
content = encryptedPrivateKeyInfo.getEncoded();
}

try (FileWriter fileWriter = new FileWriter(output);
BufferedWriter pemWriter = new BufferedWriter(fileWriter)) {
pemWriter.write("-----BEGIN PRIVATE KEY-----\n");
pemWriter.write(Base64.getEncoder().encodeToString(privateKey.getEncoded()));
pemWriter.write("\n-----END PRIVATE KEY-----\n\n");
pemWriter.write("-----BEGIN " + (password != null ? "ENCRYPTED " : "") + "PRIVATE KEY-----\n");
pemWriter.write(Base64.getEncoder().encodeToString(content));
pemWriter.write("\n-----END " + (password != null ? "ENCRYPTED " : "") + "PRIVATE KEY-----\n\n");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,13 @@ public void generate() throws Exception {
// Write the certificates to files
// root.crt, root.key, intermediary.crt, intermediary.key, cn.crt, cn.key
CertificateUtils.writeCertificateToPEM(rootCertificate, new File(baseDir, "root.crt"));
CertificateUtils.writePrivateKeyToPem(rootKeyPair.getPrivate(), new File(baseDir, "root.key"));
CertificateUtils.writePrivateKeyToPem(rootKeyPair.getPrivate(), null, new File(baseDir, "root.key"));

CertificateUtils.writeCertificateToPEM(intermediaryCertificate, new File(baseDir, "intermediate.crt"));
CertificateUtils.writePrivateKeyToPem(intermediaryKeyPair.getPrivate(), new File(baseDir, "intermediate.key"));
CertificateUtils.writePrivateKeyToPem(intermediaryKeyPair.getPrivate(), null, new File(baseDir, "intermediate.key"));

CertificateUtils.writeCertificateToPEM(leafCertificate, new File(baseDir, cn + ".crt"), intermediaryCertificate);
CertificateUtils.writePrivateKeyToPem(leafKeyPair.getPrivate(), new File(baseDir, cn + ".key"));
CertificateUtils.writePrivateKeyToPem(leafKeyPair.getPrivate(), null, new File(baseDir, cn + ".key"));
}

private KeyPair generateKeyPair() throws NoSuchAlgorithmException, NoSuchProviderException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.util.Collection;

import io.smallrye.certs.pem.parsers.EncryptedPKCS8Parser;
import io.vertx.core.buffer.Buffer;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
Expand Down Expand Up @@ -69,6 +73,36 @@ void PEMGeneration(@Dir Path tempDir) throws Exception {
assertThat(response.statusCode()).isEqualTo(200);
}

@Test
void PEMGenerationWithEncryptedPrivateKey(@Dir Path tempDir) throws Exception {
CertificateRequest request = new CertificateRequest()
.withName("test")
.withFormat(Format.PEM)
.withPassword("secret");
Collection<CertificateFiles> files = new CertificateGenerator(tempDir, true).generate(request);
Assertions.assertThat(files).hasSize(1);
assertThat(files.stream().findFirst().get()).isInstanceOf(PemCertificateFiles.class);

// We need to translate the PEM file.
Buffer buffer = decrypt(new File(tempDir.toFile(), "test.key"), "secret");

KeyCertOptions serverOptions = new PemKeyCertOptions()
.addKeyValue(buffer)
.addCertPath(new File(tempDir.toFile(), "test.crt").getAbsolutePath());
TrustOptions clientOptions = new PemTrustOptions()
.addCertPath(new File(tempDir.toFile(), "test-ca.crt").getAbsolutePath());
var server = VertxHttpHelper.createHttpServer(vertx, serverOptions);
var response = VertxHttpHelper.createHttpClientAndInvoke(vertx, server, clientOptions);

assertThat(response.statusCode()).isEqualTo(200);
}

private static Buffer decrypt(File pem, String password) throws IOException {
var content = Files.readString(pem.toPath());
var parser = new EncryptedPKCS8Parser();
return parser.decryptKey(content, password);
}

@Test
void PCKS12Generation(@Dir Path tempDir) throws Exception {
CertificateRequest request = new CertificateRequest()
Expand Down Expand Up @@ -240,8 +274,11 @@ void mTLSWithJKSAndPemGeneration(@Dir Path tempDir) throws Exception {
assertThat(clientKey).isFile();
File clientCert = new File(tempDir.toFile(), "test-client.crt");
assertThat(clientCert).isFile();

Buffer buffer = decrypt(new File(tempDir.toFile(), "test-client.key"), "secret");

KeyCertOptions clientOptions = new PemKeyCertOptions()
.addKeyPath(clientKey.getAbsolutePath())
.addKeyValue(buffer)
.addCertPath(clientCert.getAbsolutePath());
File clientTrustStore = new File(tempDir.toFile(), "test-client-ca.crt");
assertThat(clientTrustStore).isFile();
Expand Down Expand Up @@ -277,8 +314,11 @@ void mTLSWithP12AndPemGeneration(@Dir Path tempDir) throws Exception {
assertThat(clientKey).isFile();
File clientCert = new File(tempDir.toFile(), "test-client.crt");
assertThat(clientCert).isFile();

Buffer buffer = decrypt(new File(tempDir.toFile(), "test-client.key"), "secret");

KeyCertOptions clientOptions = new PemKeyCertOptions()
.addKeyPath(clientKey.getAbsolutePath())
.addKeyValue(buffer)
.addCertPath(clientCert.getAbsolutePath());
File clientTrustStore = new File(tempDir.toFile(), "test-client-ca.crt");
assertThat(clientTrustStore).isFile();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.smallrye.certs;

import io.smallrye.certs.pem.parsers.EncryptedPKCS8Parser;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.*;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
Expand All @@ -9,6 +11,8 @@
import org.junit.jupiter.params.provider.MethodSource;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
Expand Down Expand Up @@ -41,16 +45,31 @@ private static Stream<Arguments> testMixingKeystoreAndTruststoreFormat() {
return list.stream();
}

private static Buffer decrypt(File pem, String password) throws IOException {
var content = Files.readString(pem.toPath());
var parser = new EncryptedPKCS8Parser();
return parser.decryptKey(content, password);
}

@ParameterizedTest
@MethodSource
public void testMixingKeystoreAndTruststoreFormat(Format serverKeystoreFormat, Format serverTruststoreFormat,
Format clientKeystoreFormat, Format clientTrustoreFormat) throws Exception {
generate();

KeyCertOptions serverKS = switch (serverKeystoreFormat) {
case PEM -> new PemKeyCertOptions()
.addKeyPath("target/certs/test-mixed-mtls.key")
.addCertPath("target/certs/test-mixed-mtls.crt");
case PEM -> {
Buffer buffer = decrypt(new File("target/certs/test-mixed-mtls.key"), "password");
if (buffer != null) {
yield new PemKeyCertOptions()
.addKeyValue(buffer)
.addCertPath("target/certs/test-mixed-mtls.crt");
} else {
yield new PemKeyCertOptions()
.addKeyPath("target/certs/test-mixed-mtls.key")
.addCertPath("target/certs/test-mixed-mtls.crt");
}
}
case JKS -> new JksOptions()
.setPath("target/certs/test-mixed-mtls-keystore.jks")
.setPassword("password");
Expand All @@ -60,9 +79,18 @@ public void testMixingKeystoreAndTruststoreFormat(Format serverKeystoreFormat, F
};

KeyCertOptions clientKS = switch (clientKeystoreFormat) {
case PEM -> new PemKeyCertOptions()
.addKeyPath("target/certs/test-mixed-mtls-client.key")
.addCertPath("target/certs/test-mixed-mtls-client.crt");
case PEM -> {
Buffer buffer = decrypt(new File("target/certs/test-mixed-mtls-client.key"), "password");
if (buffer != null) {
yield new PemKeyCertOptions()
.addKeyValue(buffer)
.addCertPath("target/certs/test-mixed-mtls-client.crt");
} else {
yield new PemKeyCertOptions()
.addKeyPath("target/certs/test-mixed-mtls-client.key")
.addCertPath("target/certs/test-mixed-mtls-client.crt");
}
}
case JKS -> new JksOptions()
.setPath("target/certs/test-mixed-mtls-client-keystore.jks")
.setPassword("password");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.smallrye.certs;

import io.smallrye.certs.pem.parsers.EncryptedPKCS8Parser;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpClientResponse;
import io.vertx.core.http.HttpServer;
import io.vertx.core.net.*;
Expand All @@ -11,6 +13,8 @@
import org.junit.jupiter.params.provider.MethodSource;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.List;
import java.util.stream.Stream;

Expand Down Expand Up @@ -44,17 +48,31 @@ private static Stream<Arguments> testMixingKeystoreAndTruststoreFormat() {
Arguments.of(Format.PEM, Format.PKCS12));
}

private static Buffer decrypt(File pem, String password) throws IOException {
var content = Files.readString(pem.toPath());
var parser = new EncryptedPKCS8Parser();
return parser.decryptKey(content, password);
}

@ParameterizedTest
@MethodSource
public void testMixingKeystoreAndTruststoreFormat(Format keystoreFormat, Format truststoreFormat) throws Exception {
generate();

HttpServer server = switch (keystoreFormat) {
case PEM -> {
KeyCertOptions options = new PemKeyCertOptions()
.addKeyPath("target/certs/test-mixed.key")
.addCertPath("target/certs/test-mixed.crt");
yield VertxHttpHelper.createHttpServer(vertx, options);
var buffer = decrypt(new File("target/certs/test-mixed.key"), "password");
if (buffer != null) {
KeyCertOptions options = new PemKeyCertOptions()
.addKeyValue(buffer)
.addCertPath("target/certs/test-mixed.crt");
yield VertxHttpHelper.createHttpServer(vertx, options);
} else {
KeyCertOptions options = new PemKeyCertOptions()
.addKeyPath("target/certs/test-mixed.key")
.addCertPath("target/certs/test-mixed.crt");
yield VertxHttpHelper.createHttpServer(vertx, options);
}
}
case JKS -> {
KeyCertOptions options = new JksOptions()
Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
<module>certificate-generator</module>
<module>certificate-generator-maven-plugin</module>
<module>certificate-generator-junit5</module>
<module>private-key-pem-parser</module>
</modules>

<dependencyManagement>
Expand Down
Loading

0 comments on commit e31b363

Please sign in to comment.