Skip to content

Commit

Permalink
Merge pull request #13 from cescoffier/ca-generation
Browse files Browse the repository at this point in the history
Generate root certificate
  • Loading branch information
cescoffier authored Jun 25, 2024
2 parents 7fc6d13 + 18a6ea2 commit 8d44255
Show file tree
Hide file tree
Showing 11 changed files with 682 additions and 33 deletions.
11 changes: 11 additions & 0 deletions certificate-generator/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye.common</groupId>
<artifactId>smallrye-common-os</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<!-- TODO Should we drop this dependency to use our own implementation? -->
<groupId>com.googlecode.plist</groupId>
<artifactId>dd-plist</artifactId>
<version>1.28</version>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,22 @@ public class CertificateHolder {
private final X509Certificate clientCertificate;

private final String password;
private final CertificateRequest.Issuer issuer;

/**
* Generates a new instance of {@link CertificateHolder}, with a new random key pair and a certificate.
*/
public CertificateHolder(String cn, List<String> sans, Duration duration, boolean generateClient, String password) throws Exception {
public CertificateHolder(String cn, List<String> sans, Duration duration, boolean generateClient, String password, CertificateRequest.Issuer issuer) throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);

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

if (generateClient) {
clientKeys = keyPairGenerator.generateKeyPair();
clientCertificate = CertificateUtils.generateCertificate(clientKeys, cn, sans, duration);
clientCertificate = CertificateUtils.generateCertificate(clientKeys, cn, sans, duration, issuer);
} else {
clientKeys = null;
clientCertificate = null;
Expand Down Expand Up @@ -59,6 +61,10 @@ public boolean hasClient() {
return clientKeys != null;
}

public CertificateRequest.Issuer issuer() {
return issuer;
}

public char[] password() {
if (password == null) {
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package me.escoffier.certs;

import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
Expand All @@ -18,6 +20,9 @@ public final class CertificateRequest {
private final Map<String, AliasRequest> aliases = new HashMap<>();
private final List<String> sans = new ArrayList<>();

private boolean signed;
private Issuer issuer;

public CertificateRequest withName(String name) {
this.name = name;
return this;
Expand Down Expand Up @@ -71,6 +76,15 @@ public CertificateRequest withAlias(String alias, AliasRequest request) {
return this;
}

public record Issuer(X509Certificate issuer, PrivateKey issuerPrivateKey) {
}

public CertificateRequest signedWith(X509Certificate issuer, PrivateKey issuerPrivateKey) {
this.signed = true;
this.issuer = new Issuer(issuer, issuerPrivateKey);
return this;
}

void validate() {
if (cn == null || cn.isEmpty()) {
cn = "localhost";
Expand Down Expand Up @@ -106,8 +120,15 @@ public List<String> getSubjectAlternativeNames() {
return sans;
}


public Map<String, AliasRequest> aliases() {
return aliases;
}

public boolean isSelfSigned() {
return ! signed;
}

public Issuer issuer() {
return issuer;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public class CertificateRequestManager {
public CertificateRequestManager(CertificateRequest request) throws Exception {
this.request = request;
this.name = request.name();
holders.put(request.name(), new CertificateHolder(request.getCN(), request.getSubjectAlternativeNames(), request.getDuration(), request.hasClient(), request.getPassword()));
holders.put(request.name(),
new CertificateHolder(request.getCN(), request.getSubjectAlternativeNames(), request.getDuration(), request.hasClient(), request.getPassword(), request.issuer()));

for (String alias : request.aliases().keySet()) {
AliasRequest nested = request.aliases().get(alias);
Expand All @@ -38,7 +39,8 @@ public CertificateRequestManager(CertificateRequest request) throws Exception {
if (cn == null) {
cn = request.getCN();
}
holders.put(alias, new CertificateHolder(cn, nested.getSubjectAlternativeNames(), request.getDuration(), nested.hasClient(), nested.getPassword()));
holders.put(alias,
new CertificateHolder(cn, nested.getSubjectAlternativeNames(), request.getDuration(), nested.hasClient(), nested.getPassword(), request.issuer()));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,51 @@
package me.escoffier.certs;

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.BasicConstraints;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
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.X509Principal;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.x509.X509V3CertificateGenerator;

import java.io.*;
import javax.security.auth.x500.X500Principal;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.*;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
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.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.Date;
import java.util.List;
import java.util.Map;

Expand All @@ -34,26 +55,42 @@ public class CertificateUtils {
Security.addProvider(new BouncyCastleProvider());
}

public static X509Certificate generateCertificate(KeyPair keyPair, String cn, List<String> sans, Duration duration) throws Exception {
// Generate self-signed X509 Certificate
X509V3CertificateGenerator certGen = new X509V3CertificateGenerator();
certGen.setSerialNumber(BigInteger.valueOf(System.nanoTime()));
certGen.setSubjectDN(new X509Principal("CN=" + cn));
certGen.setIssuerDN(new X509Principal("CN=" + cn));

public static X509Certificate generateCertificate(KeyPair keyPair, String cn, List<String> sans, Duration duration, CertificateRequest.Issuer issuerHolder) throws Exception {
if (issuerHolder != null) {
return generateSignedCertificate(keyPair, cn, sans, duration, issuerHolder);
}
var issuer = new X500Name("CN=" + cn);
X509v3CertificateBuilder builder = getCertificateBuilder(keyPair, cn, sans, duration, issuer);
JcaContentSignerBuilder contentSignerBuilder = new JcaContentSignerBuilder("SHA256WithRSAEncryption");
return new JcaX509CertificateConverter().getCertificate(builder.build(contentSignerBuilder.build(keyPair.getPrivate())));
}

certGen.addExtension(Extension.subjectKeyIdentifier, false, new JcaX509ExtensionUtils().createSubjectKeyIdentifier(keyPair.getPublic()));
certGen.addExtension(Extension.authorityKeyIdentifier, false, new JcaX509ExtensionUtils().createAuthorityKeyIdentifier(keyPair.getPublic()));
private static X509v3CertificateBuilder getCertificateBuilder(KeyPair keyPair, String cn, List<String> sans, Duration duration, X500Name issuer) throws CertIOException, NoSuchAlgorithmException {
var subject = new X500Name("CN=" + cn);
var before = Instant.now().minus(2, ChronoUnit.DAYS);
var after = Instant.now().plus(duration.toDays(), ChronoUnit.DAYS);
var keyInfo = SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(keyPair.getPublic().getEncoded()));
X509v3CertificateBuilder builder = new X509v3CertificateBuilder(
issuer,
BigInteger.valueOf(System.nanoTime()),
new Date(before.toEpochMilli()),
new Date(after.toEpochMilli()),
subject,
keyInfo
);

builder.addExtension(Extension.subjectKeyIdentifier, false, new JcaX509ExtensionUtils().createSubjectKeyIdentifier(keyPair.getPublic()));
builder.addExtension(Extension.authorityKeyIdentifier, false, new JcaX509ExtensionUtils().createAuthorityKeyIdentifier(keyPair.getPublic()));

// Set certificate extensions
// (1) digitalSignature extension
certGen.addExtension(Extension.keyUsage, true,
builder.addExtension(Extension.keyUsage, true,
new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment | KeyUsage.dataEncipherment | KeyUsage.keyAgreement | KeyUsage.nonRepudiation));

certGen.addExtension(Extension.basicConstraints, false, new BasicConstraints(false));
builder.addExtension(Extension.basicConstraints, false, new BasicConstraints(false));

// (2) extendedKeyUsage extension
certGen.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_serverAuth}));
builder.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_serverAuth}));

// (3) subjectAlternativeName
if (sans.isEmpty()) {
Expand All @@ -62,7 +99,7 @@ public static X509Certificate generateCertificate(KeyPair keyPair, String cn, Li
new GeneralName(GeneralName.iPAddress, "127.0.0.1"),
new GeneralName(GeneralName.iPAddress, "0.0.0.0")
});
certGen.addExtension(Extension.subjectAlternativeName, false, subjectAlternativeNames);
builder.addExtension(Extension.subjectAlternativeName, false, subjectAlternativeNames);
} else {
DERSequence subjectAlternativeNames =
new DERSequence(sans.stream().map(s -> {
Expand All @@ -74,19 +111,57 @@ public static X509Certificate generateCertificate(KeyPair keyPair, String cn, Li
return new GeneralName(GeneralName.dNSName, s);
}
}).toArray(ASN1Encodable[]::new));
certGen.addExtension(Extension.subjectAlternativeName, false, subjectAlternativeNames);
builder.addExtension(Extension.subjectAlternativeName, false, subjectAlternativeNames);
}
return builder;
}


public static X509Certificate generateSignedCertificate(KeyPair keyPair, String cn, List<String> sans, Duration duration, CertificateRequest.Issuer issuerHolder) throws Exception {
var before = Instant.now().minus(2, ChronoUnit.DAYS);
var after = Instant.now().plus(duration.toDays(), ChronoUnit.DAYS);

certGen.setNotBefore(new java.util.Date(before.toEpochMilli()));
certGen.setNotAfter(new java.util.Date(after.toEpochMilli()));
X509V3CertificateGenerator certGen = new X509V3CertificateGenerator();
certGen.setSerialNumber(new java.math.BigInteger("2"));
certGen.setIssuerDN(issuerHolder.issuer().getSubjectX500Principal());
certGen.setSubjectDN(new X500Principal("CN=" + cn));
certGen.setPublicKey(keyPair.getPublic());
certGen.setNotBefore(new Date(before.toEpochMilli())); // Yesterday
certGen.setNotAfter(new Date(after.toEpochMilli())); // 1 year
certGen.setSignatureAlgorithm("SHA256WithRSAEncryption");

return certGen.generate(keyPair.getPrivate());
if (sans.isEmpty()) {
DERSequence subjectAlternativeNames = new DERSequence(new ASN1Encodable[]{
new GeneralName(GeneralName.dNSName, cn),
new GeneralName(GeneralName.iPAddress, "127.0.0.1"),
new GeneralName(GeneralName.iPAddress, "0.0.0.0")
});
certGen.addExtension(Extension.subjectAlternativeName, false, subjectAlternativeNames);
} else {
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);
}
certGen.addExtension(Extension.subjectKeyIdentifier, false, new JcaX509ExtensionUtils().createSubjectKeyIdentifier(keyPair.getPublic()));
// Do not add authority when using a CA-signed certificate

// Set certificate extensions
// // (1) digitalSignature extension
certGen.addExtension(Extension.keyUsage, true,
new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment | KeyUsage.dataEncipherment | KeyUsage.keyAgreement | KeyUsage.nonRepudiation));

certGen.addExtension(Extension.basicConstraints, false, new BasicConstraints(false));

// (2) extendedKeyUsage extension
certGen.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_serverAuth}));

return certGen.generate(issuerHolder.issuerPrivateKey(), "BC");
}

public static void writeCertificateToPEM(X509Certificate certificate, File output, X509Certificate... chain) throws IOException, CertificateEncodingException {
Expand Down Expand Up @@ -202,13 +277,18 @@ public static void writePrivateKeyAndCertificateToPKCS12(Map<String, Certificate
keyStore.load(null, null);

for (Map.Entry<String, CertificateHolder> entry : certificates.entrySet()) {
if (entry.getValue().issuer() != null) {
keyStore.setCertificateEntry("issuer-" + entry.getKey(), entry.getValue().issuer().issuer());
}
keyStore.setKeyEntry(
entry.getKey(),
entry.getValue().keys().getPrivate(),
entry.getValue().password(),
new Certificate[]{entry.getValue().certificate()});
}



FileOutputStream keyStoreFos = new FileOutputStream(output);
keyStore.store(keyStoreFos, password);
keyStoreFos.close();
Expand Down Expand Up @@ -295,8 +375,9 @@ public static X509Certificate loadCertificate(File certificateFile) throws Excep
Object obj = pemParser.readObject();
if (obj instanceof X509Certificate) {
return (X509Certificate) obj;
} if (obj instanceof X509CertificateHolder) {
return new JcaX509CertificateConverter().getCertificate((X509CertificateHolder) obj);
}
if (obj instanceof X509CertificateHolder) {
return new JcaX509CertificateConverter().getCertificate((X509CertificateHolder) obj);
} else {
throw new IllegalArgumentException("Invalid PEM file format: " + obj);
}
Expand Down
Loading

0 comments on commit 8d44255

Please sign in to comment.