package com.otterca.repository.util;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.annotation.ParametersAreNonnullByDefault;
import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.X509Extensions;
import org.bouncycastle.jce.X509Principal;
import org.bouncycastle.x509.X509V3CertificateGenerator;
import org.bouncycastle.x509.extension.AuthorityKeyIdentifierStructure;
import org.bouncycastle.x509.extension.SubjectKeyIdentifierStructure;
/**
* Convenience class that builds X509 certificates.
*
* @author bgiles@otterca.com
*/
@ParametersAreNonnullByDefault
@SuppressWarnings("deprecation")
public class X509CertificateBuilder {
public static final String SIGNATURE_ALGORITHM = "SHA256WITHRSA";
private KeyStore keyStore;
private X509Certificate issuer;
private BigInteger serial;
private X509Principal subjectDN;
private X509Principal issuerDN;
private Date notBefore;
private Date notAfter;
private PublicKey pubkey;
private final List<GeneralName> subjectNames = new ArrayList<>();
private final List<GeneralName> issuerNames = new ArrayList<>();
private KeyUsage keyUsage;
private ExtendedKeyUsage extendedKeyUsage;
private boolean basicConstraint;
private int pathLengthConstraint = -1;
/**
* Default constructor that can be used to create self-signed certificates
* but not certificate chains.
*
* @param keyStore
*/
public X509CertificateBuilder() {
}
/**
* Constructor that requires keystore to validate certificate chain for
* issuer.
*
* @param keyStore
*/
public X509CertificateBuilder(KeyStore keyStore) {
this.keyStore = keyStore;
}
/**
* Reset builder to default state.
*/
public void reset() {
issuer = null;
serial = null;
subjectDN = null;
issuerDN = null;
notBefore = null;
notAfter = null;
pubkey = null;
subjectNames.clear();
issuerNames.clear();
keyUsage = null;
basicConstraint = false;
pathLengthConstraint = -1;
}
/**
* Set serial number.
*
* @param serial
* @return
*/
public X509CertificateBuilder setSerialNumber(BigInteger serial) {
this.serial = serial;
return this;
}
/**
* Set subject name (as X509Principal).
*
* @param dirName
* @return
*/
public X509CertificateBuilder setSubject(String dirName) {
this.subjectDN = new X509Principal(dirName);
return this;
}
/**
* Set issuer name (as X509Principal).
*
* @param dirName
* @return
*/
public X509CertificateBuilder setIssuer(String dirName) {
this.issuerDN = new X509Principal(dirName);
return this;
}
/**
* Set 'notBefore' date.
*
* @param notBefore
* @return
*/
public X509CertificateBuilder setNotBefore(Date notBefore) {
this.notBefore = notBefore;
return this;
}
/**
* Set 'notAfter' date.
*
* @param notAfter
* @return
*/
public X509CertificateBuilder setNotAfter(Date notAfter) {
this.notAfter = notAfter;
return this;
}
/**
* Set public key.
*
* @param pubkey
* @return
*/
public X509CertificateBuilder setPublicKey(PublicKey pubkey) {
this.pubkey = pubkey;
return this;
}
/**
* Set issuer's X509 certificate. This provides issuer's DN and alternate
* names and public key.
*
* @param issuer
* @param pubkey
* @return
*/
public X509CertificateBuilder setIssuer(X509Certificate issuer) {
this.issuer = issuer;
return this;
}
/**
* Set subject's email addresses (individual, server or CA).
*
* @param emailAddresses
* @return
*/
public X509CertificateBuilder setEmailAddresses(String... emailAddresses) {
for (String address : emailAddresses) {
subjectNames.add(new GeneralName(GeneralName.rfc822Name, address));
}
return this;
}
/**
* Set subject's DNS names (server or CA?). Note: servers should use their
* canonical hostname as the X.500 CommonName used as their subjectDN. (See
* above.) This provides more flexibility but many clients won't look for
* these extensions.
*
* @param dnsNames
* @return
*/
public X509CertificateBuilder setDnsNames(String... dnsNames) {
for (String name : dnsNames) {
subjectNames.add(new GeneralName(GeneralName.dNSName, name));
}
return this;
}
/**
* Set subject's IP Address (server).
*
* @param ipAddresses
* @return
*/
public X509CertificateBuilder setIpAddresses(String... ipAddresses) {
for (String address : ipAddresses) {
subjectNames.add(new GeneralName(GeneralName.iPAddress, address));
}
return this;
}
/**
* Set subject's directory names. I think this refers to alternate X.500
* principal names, not filesystem directories.
*
* @param dirNames
* @return
*/
public X509CertificateBuilder setDirectoryNames(String... dirNames) {
for (String name : dirNames) {
subjectNames.add(new GeneralName(GeneralName.directoryName, name));
}
return this;
}
/**
* Set issuer's email addresses (individual, server or CA).
*
* @param emailAddresses
* @return
*/
public X509CertificateBuilder setIssuerEmailAddresses(String... emailAddresses) {
for (String address : emailAddresses) {
issuerNames.add(new GeneralName(GeneralName.rfc822Name, address));
}
return this;
}
/**
* Set issuer's DNS names (server or CA?). Note: servers should use their
* canonical hostname as the X.500 CommonName used as their subjectDN. (See
* above.) This provides more flexibility but many clients won't look for
* these extensions.
*
* @param dnsNames
* @return
*/
public X509CertificateBuilder setIssuerDnsNames(String... dnsNames) {
for (String name : dnsNames) {
issuerNames.add(new GeneralName(GeneralName.dNSName, name));
}
return this;
}
/**
* Set issuer's IP Address (server).
*
* @param ipAddresses
* @return
*/
public X509CertificateBuilder setIssuerIpAddresses(String... ipAddresses) {
for (String address : ipAddresses) {
issuerNames.add(new GeneralName(GeneralName.iPAddress, address));
}
return this;
}
/**
* Set issuer's directory names. I think this refers to alternate X.500
* principal names, not filesystem directories.
*
* @param dirNames
* @return
*/
public X509CertificateBuilder setIssuerDirectoryNames(String... dirNames) {
for (String name : dirNames) {
issuerNames.add(new GeneralName(GeneralName.directoryName, name));
}
return this;
}
/**
* Set key usage. TODO: provide parameters that don't expose implementation
* details.
*
* @param usage
* @return
*/
public X509CertificateBuilder setKeyUsage(KeyUsage usage) {
this.keyUsage = usage;
return this;
}
/**
* Set extended key usage. TODO: provide parameters that don't expose
* implementation details.
*
* @param usage
* @return
*/
public X509CertificateBuilder setExtendedKeyUsage(ExtendedKeyUsage extendedKeyUsage) {
this.extendedKeyUsage = extendedKeyUsage;
return this;
}
/**
* Set the certificate's Basic Constraint (can this certificate be used to
* sign other certificates?). There are no restrictions on certification
* path length.
*
* @param basicConstraint
* @return
*/
public X509CertificateBuilder setBasicConstraints(boolean basicConstraint) {
this.basicConstraint = basicConstraint;
return this;
}
/**
* Set the certificate's Basic Constraint (can this certificate be used to
* sign other certificates?) and maximum certification path length. A value
* of 0 means that this certificate can be used to sign leafs but cannot be
* used to create other signing certs.
*
* @param basicConstraint
* @param pathLengthConstraint
* @return
*/
public X509CertificateBuilder setBasicConstraints(boolean basicConstraint,
int pathLengthConstraint) {
this.basicConstraint = basicConstraint;
this.pathLengthConstraint = pathLengthConstraint;
return this;
}
/**
* Perform any other validation checks on issuer. This method can be
* replaced by an injected strategy.
*
* @param ex
*/
public void checkIssuer(X509CertificateBuilderIllegalArgumentException ex)
throws CertificateParsingException, KeyStoreException {
if (issuer == null) {
return;
}
if (keyStore == null) {
ex.setIssuerCannotSignCertificates(true);
return;
}
// look up issuer's certificate chain in our keystore using
// the issuer's subject DN as the alias.
String alias = issuer.getSubjectDN().getName();
if (!keyStore.containsAlias(alias)) {
ex.setUnknownIssuer(true);
return;
}
// validate the certificate chain.
X509Certificate last = null;
X509Certificate[] chain = (X509Certificate[]) keyStore.getCertificateChain(alias);
for (X509Certificate cert : chain) {
if (cert.getBasicConstraints() < 0) {
ex.setIssuerCannotSignCertificates(true);
}
try {
cert.checkValidity();
} catch (CertificateExpiredException e) {
ex.setIssuerCannotSignCertificates(true);
} catch (CertificateNotYetValidException e) {
ex.setIssuerCannotSignCertificates(true);
}
if (last != null) {
try {
last.verify(cert.getPublicKey());
} catch (CertificateException e) {
ex.setIssuerCannotSignCertificates(true);
} catch (SignatureException e) {
ex.setIssuerCannotSignCertificates(true);
} catch (InvalidKeyException e) {
ex.setIssuerCannotSignCertificates(true);
} catch (NoSuchProviderException e) {
ex.setIssuerCannotSignCertificates(true);
} catch (NoSuchAlgorithmException e) {
ex.setIssuerCannotSignCertificates(true);
}
}
last = cert;
}
// at this point we have a valid certificate chain and
// have to trust that the keystore only contains trusted
// certificates.
}
/**
* Perform any other policy check. This method can be replaced by an
* injected strategy.
*
* @param ex
*/
public void checkPolicy(X509CertificateBuilderIllegalArgumentException ex) {
}
/**
* Establish any other policy. For instance this might add (or remove)
* alternative names, add or remove key usage constraints, etc. This method
* can be replaced by an injected strategy.
*
* @param ex
*/
public void establishPolicy() {
// ensure keyCertSign is (only) available on signing keys.
if (basicConstraint) {
if (keyUsage == null) {
keyUsage = new KeyUsage(KeyUsage.keyCertSign);
} else {
keyUsage = new KeyUsage(keyUsage.intValue() | KeyUsage.keyCertSign);
}
} else {
if (keyUsage != null) {
keyUsage = new KeyUsage(keyUsage.intValue()
& (Integer.MAX_VALUE ^ KeyUsage.keyCertSign));
}
}
// other policies..
}
/**
* Build the X509 certificate.
*
* Implementation note: this uses the deprecated methods until I can find
* documentation on using the newer classes.
*
* @param pkey
* @return
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
* @throws SignatureException
* @throws CertificateEncodingException
* @throws CertificateParsingException
*/
public X509Certificate build(PrivateKey pkey) throws InvalidKeyException,
NoSuchAlgorithmException, SignatureException, CertificateEncodingException,
CertificateParsingException, KeyStoreException {
// verify we have everything we need....
X509CertificateBuilderIllegalArgumentException ex = new X509CertificateBuilderIllegalArgumentException();
if (issuer == null) {
// require issuer cert for everything other than self-signed certs.
if ((issuerDN != null) && (issuerDN.equals(subjectDN))) {
ex.setMissingIssuerCertificate(true);
}
} else {
// verify issuer's cert is active.
Date now = new Date();
if (!(issuer.getNotBefore().before(now) && now.before(issuer.getNotAfter()))) {
ex.setInvalidIssuer(true);
}
// verify our 'notBefore' is within range of issuer's certificate.
if ((notBefore == null) || notBefore.before(issuer.getNotBefore())) {
setNotBefore(issuer.getNotBefore());
}
if (notBefore.after(issuer.getNotAfter())) {
ex.setUnacceptableDateRange(true);
}
// verify our 'notAfter' is within range of issuer's certificate.
if ((notAfter == null) || notAfter.after(issuer.getNotAfter())) {
setNotAfter(issuer.getNotAfter());
}
if (notAfter.before(issuer.getNotBefore())) {
ex.setUnacceptableDateRange(true);
}
// verify issuer can sign certificates
int pathLenConstraint = issuer.getBasicConstraints();
if (pathLenConstraint 0) {
throw ex;
}
X509V3CertificateGenerator generator = new X509V3CertificateGenerator();
// set the mandatory properties
generator.setSerialNumber(serial);
generator.setIssuerDN(issuerDN);
generator.setSubjectDN(subjectDN);
generator.setNotBefore(notBefore);
generator.setNotAfter(notAfter);
generator.setPublicKey(pubkey);
generator.setSignatureAlgorithm(SIGNATURE_ALGORITHM);
// can this certificate be used to sign more certificates?
// make sure pathlenthconstraint is always lower than issuer's.
if (basicConstraint) {
if ((issuer != null) && (pathLengthConstraint > issuer.getBasicConstraints() - 1)) {
pathLengthConstraint = issuer.getBasicConstraints() - 1;
}
if (pathLengthConstraint >= 0) {
generator.addExtension(X509Extensions.BasicConstraints, true, new BasicConstraints(
pathLengthConstraint));
} else {
generator.addExtension(X509Extensions.BasicConstraints, true, new BasicConstraints(
true));
}
} else {
generator.addExtension(X509Extensions.BasicConstraints, true, new BasicConstraints(
false));
}
// set the X509 optional stuff.
// we always add the subject key identifier.
SubjectKeyIdentifierStructure skis = new SubjectKeyIdentifierStructure(pubkey);
generator.addExtension(X509Extensions.SubjectKeyIdentifier, false, skis);
// we always add the authority key identifier if possible.
if (issuer != null) {
// signed certificates....
AuthorityKeyIdentifierStructure akis = new AuthorityKeyIdentifierStructure(issuer);
generator.addExtension(X509Extensions.AuthorityKeyIdentifier, false, akis);
} else if (subjectDN.equals(issuerDN)) {
// self-signed certificates....
GeneralNames issuerName = new GeneralNames(new GeneralName(GeneralName.directoryName,
issuerDN));
AuthorityKeyIdentifier akis = new AuthorityKeyIdentifierStructure(pubkey);
akis = new AuthorityKeyIdentifier(akis.getKeyIdentifier(), issuerName, serial);
generator.addExtension(X509Extensions.AuthorityKeyIdentifier, false, akis);
}
// establish any other policy.
establishPolicy();
// add subject alternative names if available - this is email, dns
// names, etc.
if (!subjectNames.isEmpty()) {
generator.addExtension(X509Extensions.SubjectAlternativeName, false, new GeneralNames(
subjectNames.toArray(new GeneralName[0])));
}
// add issuer alternative names if available - this is email, dns
// names, etc.
if (!issuerNames.isEmpty()) {
generator.addExtension(X509Extensions.IssuerAlternativeName, false, new GeneralNames(
issuerNames.toArray(new GeneralName[0])));
}
// add mandatory key usage constraints.
if (keyUsage != null) {
generator.addExtension(X509Extensions.KeyUsage, true, keyUsage);
}
// add optional extended key usage constraints.
if (extendedKeyUsage != null) {
generator.addExtension(X509Extensions.ExtendedKeyUsage, false, extendedKeyUsage);
}
X509Certificate cert = generator.generate(pkey);
return cert;
}
}