Certificates.java
/*-
* #%L
* io.earcam.utilitarian.security
* %%
* Copyright (C) 2017 - 2018 earcam
* %%
* SPDX-License-Identifier: (BSD-3-Clause OR EPL-1.0 OR Apache-2.0 OR MIT)
*
* You <b>must</b> choose to accept, in full - any individual or combination of
* the following licenses:
* <ul>
* <li><a href="https://opensource.org/licenses/BSD-3-Clause">BSD-3-Clause</a></li>
* <li><a href="https://www.eclipse.org/legal/epl-v10.html">EPL-1.0</a></li>
* <li><a href="https://www.apache.org/licenses/LICENSE-2.0">Apache-2.0</a></li>
* <li><a href="https://opensource.org/licenses/MIT">MIT</a></li>
* </ul>
* #L%
*/
package io.earcam.utilitarian.security;
import java.io.StringWriter;
import java.io.Writer;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import javax.annotation.ParametersAreNonnullByDefault;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.BasicConstraints;
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.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.X509KeyUsage;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import io.earcam.unexceptional.Closing;
import io.earcam.unexceptional.Exceptional;
@ParametersAreNonnullByDefault
public class Certificates {
private static final BouncyCastleProvider PROVIDER = new BouncyCastleProvider();
static final String DN_LOCALHOST = "DN=localhost, L=London, C=GB";
public static class CertificateBuilder {
@SuppressWarnings("squid:S1313") // SonarQube false-positive; not an IP address
private static final String EXTENSION_KEY_USAGE = "2.5.29.15";
@SuppressWarnings("squid:S1313") // SonarQube false-positive; not an IP address
static final String EXTENSION_MAY_ACT_AS_CA = "2.5.29.19";
private String issuerName = "acme";
private String subjectName;
private BigInteger serial = BigInteger.ONE;
private boolean canSignOtherCertificates = false;
private LocalDate validFrom = LocalDate.now(ZoneId.systemDefault());
private long duration = 365;
private TimeUnit unit = TimeUnit.DAYS;
private String signatureAlgorithm = "SHA256withRSA";
private KeyPair keyPair;
CertificateBuilder()
{}
public CertificateBuilder issuer(String name)
{
issuerName = name;
return this;
}
public CertificateBuilder subject(String name)
{
subjectName = name;
return this;
}
public CertificateBuilder serial(int number)
{
return serial(BigInteger.valueOf(number));
}
public CertificateBuilder serial(BigInteger number)
{
this.serial = number;
return this;
}
public CertificateBuilder canSignOtherCertificates()
{
canSignOtherCertificates = true;
return this;
}
public CertificateBuilder key(KeyPair pair)
{
this.keyPair = pair;
return this;
}
public CertificateBuilder signedBy(String signatureAlgorithm)
{
this.signatureAlgorithm = signatureAlgorithm;
return this;
}
public CertificateBuilder validFrom(LocalDate from)
{
validFrom = from;
return this;
}
public CertificateBuilder validFor(long duration, TimeUnit unit)
{
this.duration = duration;
this.unit = unit;
return this;
}
public X509Certificate toX509()
{
Objects.requireNonNull(keyPair, "keyPair");
Objects.requireNonNull(issuerName, "issuerName");
Objects.requireNonNull(subjectName, "subjectName");
X500Name issuer = new X500Name(addCnIfMissing(issuerName));
X500Name subject = new X500Name(addCnIfMissing(subjectName));
Date from = javaDate(validFrom);
Date to = new Date(from.getTime() + unit.toMillis(duration));
X509v3CertificateBuilder certificateBuilder = new JcaX509v3CertificateBuilder(
issuer,
serial,
from,
to,
subject,
keyPair.getPublic());
Exceptional.accept(this::addExtensions, certificateBuilder);
X509CertificateHolder signed = sign(keyPair, signatureAlgorithm, certificateBuilder);
JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider(PROVIDER);
return Exceptional.apply(converter::getCertificate, signed);
}
static Date javaDate(LocalDate date)
{
return java.sql.Date.valueOf(date);
}
static LocalDate localDate(Date date)
{
return new java.sql.Date(date.getTime()).toLocalDate();
}
private void addExtensions(X509v3CertificateBuilder certificateBuilder) throws CertIOException
{
certificateBuilder.addExtension(
new ASN1ObjectIdentifier(EXTENSION_MAY_ACT_AS_CA),
false,
new BasicConstraints(canSignOtherCertificates)).addExtension(
new ASN1ObjectIdentifier(EXTENSION_KEY_USAGE),
true,
new X509KeyUsage(
X509KeyUsage.digitalSignature |
X509KeyUsage.nonRepudiation |
X509KeyUsage.keyEncipherment |
X509KeyUsage.dataEncipherment));
}
private String addCnIfMissing(String name)
{
return (name.indexOf('=') == -1) ? "CN=" + name : name;
}
private static X509CertificateHolder sign(KeyPair keyPair, String signatureAlgorithm, X509v3CertificateBuilder certificateBuilder)
{
JcaContentSignerBuilder jcaContentSignerBuilder = new JcaContentSignerBuilder(signatureAlgorithm);
ContentSigner sigGen = Exceptional.apply(jcaContentSignerBuilder::build, keyPair.getPrivate());
return certificateBuilder.build(sigGen);
}
public String toPem()
{
StringWriter writer = new StringWriter();
toPem(writer);
return writer.toString();
}
public void toPem(Writer writer)
{
Closing.closeAfterAccepting(JcaPEMWriter::new, writer, toX509(), JcaPEMWriter::writeObject);
}
}
private Certificates()
{}
public static CertificateBuilder certificate(KeyPair pair, String subjectName)
{
return certificate(pair)
.subject(subjectName);
}
public static CertificateBuilder certificate(KeyPair pair)
{
return certificate().key(pair);
}
public static CertificateBuilder certificate()
{
return new CertificateBuilder();
}
public static X509Certificate localhostCertificate(KeyPair keys)
{
return hostCertificate(keys, DN_LOCALHOST);
}
/**
* @deprecated
* @see #localhostCertificate(KeyPair)
*/
@Deprecated
public static X509Certificate hostCertificate(KeyPair keys)
{
throw new UnsupportedOperationException();
}
public static X509Certificate hostCertificate(KeyPair keys, String hostname)
{
return certificate(keys)
.subject(hostname)
.toX509();
}
}