Skip to content

Commit

Permalink
Merge pull request #12 from cescoffier/root-intermediate-leaf-generation
Browse files Browse the repository at this point in the history
Generate root - intermediate - leaf certificates
  • Loading branch information
cescoffier authored Apr 9, 2024
2 parents bd6deab + a2da472 commit 08c0f64
Show file tree
Hide file tree
Showing 5 changed files with 328 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ public CertificateHolder(String cn, List<String> sans, Duration duration, boolea
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);


keys = keyPairGenerator.generateKeyPair();
certificate = CertificateUtils.generateCertificate(keys, cn, sans, duration);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import java.util.List;
import java.util.Map;

class CertificateUtils {
public class CertificateUtils {

static {
Security.addProvider(new BouncyCastleProvider());
Expand Down Expand Up @@ -89,12 +89,17 @@ public static X509Certificate generateCertificate(KeyPair keyPair, String cn, Li
return certGen.generate(keyPair.getPrivate());
}

public static void writeCertificateToPEM(X509Certificate certificate, File output) throws IOException, CertificateEncodingException {
public static void writeCertificateToPEM(X509Certificate certificate, File output, X509Certificate... chain) throws IOException, CertificateEncodingException {
try (FileWriter fileWriter = new FileWriter(output);
BufferedWriter pemWriter = new BufferedWriter(fileWriter)) {
pemWriter.write("-----BEGIN CERTIFICATE-----\n");
pemWriter.write(Base64.getEncoder().encodeToString(certificate.getEncoded()));
pemWriter.write("\n-----END CERTIFICATE-----\n\n");
for (X509Certificate cert : chain) {
pemWriter.write("-----BEGIN CERTIFICATE-----\n");
pemWriter.write(Base64.getEncoder().encodeToString(cert.getEncoded()));
pemWriter.write("\n-----END CERTIFICATE-----\n\n");
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package me.escoffier.certs.chain;

import me.escoffier.certs.CertificateUtils;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.*;
import org.bouncycastle.cert.CertIOException;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;

import java.io.File;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.List;

public class CertificateChainGenerator {

static {
Security.addProvider(new BouncyCastleProvider());
}

private String cn = "localhost";

private List<String> sans = List.of("DNS:localhost");

private File baseDir; // Mandatory

public CertificateChainGenerator(File baseDir) {
this.baseDir = baseDir;
if (!baseDir.isDirectory()) {
baseDir.mkdirs();
}
}

/**
* Configure the common name of the "leaf" certificate.
*
* @param cn the common name, by default `localhost`
* @return the current generator instance
*/
public CertificateChainGenerator withCN(String cn) {
this.cn = cn;
return this;
}

/**
* Configure the Subject Alternative Names of the "leaf" certificate.
*
* @param san the list of SAN, by default `DNS:localhost`
* @return the current generator instance
*/
public CertificateChainGenerator withSAN(List<String> san) {
this.sans = san;
return this;
}

public void generate() throws Exception {

// Generate root certificate
var rootKeyPair = generateKeyPair();
var rootCertificate = generateRootCertificate(rootKeyPair);

// Generate intermediary certificate
var intermediaryKeyPair = generateKeyPair();
var intermediaryCertificate = generateIntermediaryCertificate(intermediaryKeyPair, rootKeyPair, rootCertificate);

// Generate leaf certificate
var leafKeyPair = generateKeyPair();
var leafCertificate = generateLeafCertificate(leafKeyPair, intermediaryKeyPair, intermediaryCertificate);

// 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.writeCertificateToPEM(intermediaryCertificate, new File(baseDir, "intermediate.crt"));
CertificateUtils.writePrivateKeyToPem(intermediaryKeyPair.getPrivate(), new File(baseDir, "intermediate.key"));

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

private KeyPair generateKeyPair() throws NoSuchAlgorithmException, NoSuchProviderException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
keyPairGenerator.initialize(2048, new SecureRandom());
return keyPairGenerator.generateKeyPair();
}

private X509Certificate generateRootCertificate(KeyPair rootKeyPair) throws CertIOException, NoSuchAlgorithmException, OperatorCreationException, CertificateException {
var keyInfo = SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(rootKeyPair.getPublic().getEncoded()));
var issuer = new X500Name("CN=quarkus-root,O=Quarkus Development");
var subject = new X500Name("CN=root");
var yesterday = new Date(System.currentTimeMillis() - 86400000);
var oneYear = new Date(System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000); // 1 year
X509v3CertificateBuilder certGen = new X509v3CertificateBuilder(
issuer,
BigInteger.valueOf(System.currentTimeMillis()),
yesterday,
oneYear,
subject,
keyInfo);

certGen.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign));
certGen.addExtension(Extension.basicConstraints, true, new BasicConstraints(true));
certGen.addExtension(Extension.subjectKeyIdentifier, false, new JcaX509ExtensionUtils().createSubjectKeyIdentifier(rootKeyPair.getPublic()));

JcaContentSignerBuilder contentSignerBuilder = new JcaContentSignerBuilder("SHA256WithRSAEncryption");
ContentSigner signer = contentSignerBuilder.build(rootKeyPair.getPrivate());
X509CertificateHolder holder = certGen.build(signer);
return new JcaX509CertificateConverter().getCertificate(holder);
}

private X509Certificate generateIntermediaryCertificate(KeyPair intermediaryKeyPair, KeyPair rootKeyPair, X509Certificate rootCertificate) throws NoSuchAlgorithmException, CertIOException, OperatorCreationException, CertificateException {
var keyInfo = SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(intermediaryKeyPair.getPublic().getEncoded()));
var yesterday = new Date(System.currentTimeMillis() - 86400000);
var oneYear = new Date(System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000); // 1 year
X509v3CertificateBuilder certGen = new X509v3CertificateBuilder(
new X500Name(rootCertificate.getSubjectX500Principal().getName()),
BigInteger.valueOf(System.currentTimeMillis()),
yesterday,
oneYear,
new X500Name("CN=intermediary"),
keyInfo
);

certGen.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign | KeyUsage.digitalSignature));
certGen.addExtension(Extension.basicConstraints, true, new BasicConstraints(true));
certGen.addExtension(Extension.subjectKeyIdentifier, false, new JcaX509ExtensionUtils().createSubjectKeyIdentifier(intermediaryKeyPair.getPublic()));

JcaContentSignerBuilder contentSignerBuilder = new JcaContentSignerBuilder("SHA256WithRSAEncryption");
ContentSigner contentSigner = contentSignerBuilder.build(rootKeyPair.getPrivate());
return new JcaX509CertificateConverter().getCertificate(certGen.build(contentSigner));
}

private X509Certificate generateLeafCertificate(KeyPair leafKeyPair, KeyPair intermediaryKeyPair, X509Certificate intermediaryCertificate) throws NoSuchAlgorithmException, CertIOException, OperatorCreationException, CertificateException {
var keyInfo = SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(leafKeyPair.getPublic().getEncoded()));
var before = Instant.now().minus(2, ChronoUnit.DAYS);
var after = Instant.now().plus(2, ChronoUnit.DAYS);

X509v3CertificateBuilder certGen = new X509v3CertificateBuilder(
new X500Name(intermediaryCertificate.getSubjectX500Principal().getName()),
BigInteger.valueOf(System.currentTimeMillis()),
new java.util.Date(before.toEpochMilli()),
new java.util.Date(after.toEpochMilli()),
new X500Name("CN=" + cn),
keyInfo
);

certGen.addExtension(Extension.keyUsage, true,
new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment | KeyUsage.dataEncipherment | KeyUsage.keyAgreement | KeyUsage.nonRepudiation));
certGen.addExtension(Extension.subjectKeyIdentifier, false, new JcaX509ExtensionUtils().createSubjectKeyIdentifier(leafKeyPair.getPublic()));

DERSequence subjectAlternativeNames =
new DERSequence(sans.stream().map(s -> {
if (s.startsWith("DNS:")) {
return new GeneralName(GeneralName.dNSName, s.substring(4));
} else if (s.startsWith("IP:")) {
return new GeneralName(GeneralName.iPAddress, s.substring(3));
} else {
return new GeneralName(GeneralName.dNSName, s);
}
}).toArray(ASN1Encodable[]::new));
certGen.addExtension(Extension.subjectAlternativeName, false, subjectAlternativeNames);

JcaContentSignerBuilder contentSignerBuilder = new JcaContentSignerBuilder("SHA256WithRSAEncryption");
ContentSigner contentSigner = contentSignerBuilder.build(intermediaryKeyPair.getPrivate());
return new JcaX509CertificateConverter().getCertificate(certGen.build(contentSigner));
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,30 @@ private VertxHttpHelper() {
// Avoid direct instantiation
}

static HttpServer createHttpServer(Vertx vertx, KeyCertOptions options) {
public static HttpServer createHttpServer(Vertx vertx, KeyCertOptions options) {
return vertx.createHttpServer(new HttpServerOptions().setSsl(true).setKeyCertOptions(options))
.requestHandler(req -> req.response().end("OK"))
.listen(0)
.toCompletionStage().toCompletableFuture().join();
}

static HttpClientResponse createHttpClientAndInvoke(Vertx vertx, HttpServer server, TrustOptions options) {
public static HttpClientResponse createHttpClientAndInvoke(Vertx vertx, HttpServer server, TrustOptions options) {
return createHttpClientAndInvoke(vertx, server, options, true);
}

public static HttpClientResponse createHttpClientAndInvoke(Vertx vertx, HttpServer server, TrustOptions options, boolean verifyHost) {

if (options == null) {
return vertx.createHttpClient(new HttpClientOptions()
.setSsl(true).setDefaultHost("localhost").setDefaultPort(server.actualPort()).setVerifyHost(verifyHost)
)
.request(HttpMethod.GET, "/").flatMap(HttpClientRequest::send).toCompletionStage().toCompletableFuture().join();
}

return vertx.createHttpClient(new HttpClientOptions()
.setSsl(true).setDefaultHost("localhost").setDefaultPort(server.actualPort())
.setTrustOptions(options))
.setTrustOptions(options)
.setVerifyHost(verifyHost))
.request(HttpMethod.GET, "/").flatMap(HttpClientRequest::send).toCompletionStage().toCompletableFuture().join();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package me.escoffier.certs.chain;

import io.vertx.core.Vertx;
import io.vertx.core.http.HttpClientResponse;
import io.vertx.core.http.HttpServer;
import io.vertx.core.net.PemKeyCertOptions;
import io.vertx.core.net.PemTrustOptions;
import io.vertx.core.net.TrustOptions;
import me.escoffier.certs.VertxHttpHelper;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import javax.net.ssl.SSLHandshakeException;
import java.io.File;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class CertificateChainGeneratorTest {

private static Vertx vertx;

@BeforeAll
static void initVertx() {
vertx = Vertx.vertx();
}

@AfterAll
static void closeVertx() {
vertx.close().toCompletionStage().toCompletableFuture().join();
}


@Test
void testGenerateCertificateChainGeneration() throws Exception {
File dir = new File("target/chain");
CertificateChainGenerator generator = new CertificateChainGenerator(dir)
.withCN("my-app");
generator.generate();

// Verify files
File rootCertificate = new File(dir, "root.crt");
File rootKey = new File(dir, "root.key");
File intermediateCertificate = new File(dir, "intermediate.crt");
File intermediateKey = new File(dir, "intermediate.key");
File leafCertificate = new File(dir, "my-app.crt");
File leafKey = new File(dir, "my-app.key");

assertThat(rootCertificate).isFile();
assertThat(rootKey).isFile();
assertThat(intermediateCertificate).isFile();
assertThat(intermediateKey).isFile();
assertThat(leafCertificate).isFile();
assertThat(leafKey).isFile();

// Verify interactions
PemKeyCertOptions serverKS = new PemKeyCertOptions()
.setKeyPath(leafKey.getAbsolutePath())
.setCertPath(leafCertificate.getAbsolutePath());

TrustOptions clientTS = new PemTrustOptions()
.addCertPath(rootCertificate.getAbsolutePath());

HttpServer server = VertxHttpHelper.createHttpServer(vertx, serverKS);
HttpClientResponse response = VertxHttpHelper.createHttpClientAndInvoke(vertx, server, clientTS);
assertThat(response.statusCode()).isEqualTo(200);

}

@Test
void testWithIntermediateInTS() throws Exception {
File dir = new File("target/chain");
CertificateChainGenerator generator = new CertificateChainGenerator(dir)
.withCN("my-app");
generator.generate();

File intermediateCertificate = new File(dir, "intermediate.crt");
File leafCertificate = new File(dir, "my-app.crt");
File leafKey = new File(dir, "my-app.key");

PemKeyCertOptions serverKS = new PemKeyCertOptions()
.setKeyPath(leafKey.getAbsolutePath())
.setCertPath(leafCertificate.getAbsolutePath());

TrustOptions clientTS = new PemTrustOptions()
.addCertPath(intermediateCertificate.getAbsolutePath());

HttpServer server = VertxHttpHelper.createHttpServer(vertx, serverKS);
HttpClientResponse response = VertxHttpHelper.createHttpClientAndInvoke(vertx, server, clientTS);
assertThat(response.statusCode()).isEqualTo(200);
}

@Test
void testWithExposingTheIntermediate() throws Exception {
File dir = new File("target/chain");
CertificateChainGenerator generator = new CertificateChainGenerator(dir)
.withCN("my-app");
generator.generate();

File intermediateCertificate = new File(dir, "intermediate.crt");
File intermediateKey = new File(dir, "intermediate.key");
File rootCertificate = new File(dir, "root.crt");


PemKeyCertOptions serverKS = new PemKeyCertOptions()
.setKeyPath(intermediateKey.getAbsolutePath())
.setCertPath(intermediateCertificate.getAbsolutePath());

TrustOptions clientTS = new PemTrustOptions()
.addCertPath(rootCertificate.getAbsolutePath());

HttpServer server = VertxHttpHelper.createHttpServer(vertx, serverKS);
assertThatThrownBy(() -> VertxHttpHelper.createHttpClientAndInvoke(vertx, server, clientTS))
.hasCauseInstanceOf(SSLHandshakeException.class); // The intermediate is trusted BUT the cn does not match

var response = VertxHttpHelper.createHttpClientAndInvoke(vertx, server, clientTS, false);
assertThat(response.statusCode()).isEqualTo(200);
}

}

0 comments on commit 08c0f64

Please sign in to comment.