Add utility methods to generate test certificates

pull/81/head
Richard Körber 2019-06-20 14:15:55 +02:00
parent c1b3b26185
commit b5550a3fdb
No known key found for this signature in database
GPG Key ID: AAB9FD19C78AA3E0
2 changed files with 286 additions and 28 deletions

View File

@ -19,7 +19,11 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
@ -27,21 +31,31 @@ import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import java.util.Objects;
import java.util.function.Function;
import javax.annotation.ParametersAreNonnullByDefault;
import javax.annotation.WillClose;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.pkcs.Attribute;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.cert.CertIOException;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509v1CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest;
import org.shredzone.acme4j.Identifier;
import org.shredzone.acme4j.challenge.TlsAlpn01Challenge;
@ -106,44 +120,190 @@ public final class CertificateUtils {
}
final long now = System.currentTimeMillis();
final String signatureAlg = "SHA256withRSA";
X500Name issuer = new X500Name("CN=acme.invalid");
BigInteger serial = BigInteger.valueOf(now);
Instant notBefore = Instant.ofEpochMilli(now);
Instant notAfter = notBefore.plus(Duration.ofDays(7));
JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
issuer, serial, Date.from(notBefore), Date.from(notAfter),
issuer, keypair.getPublic());
GeneralName[] gns = new GeneralName[1];
switch (id.getType()) {
case Identifier.TYPE_DNS:
gns[0] = new GeneralName(GeneralName.dNSName, id.getDomain());
break;
case Identifier.TYPE_IP:
gns[0] = new GeneralName(GeneralName.iPAddress, id.getIP().getHostAddress());
break;
default:
throw new IllegalArgumentException("Unsupported Identifier type " + id.getType());
}
certBuilder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(gns));
certBuilder.addExtension(ACME_VALIDATION, true, new DEROctetString(acmeValidation));
return buildCertificate(certBuilder::build, keypair.getPrivate());
}
/**
* Creates a self-signed root certificate.
* <p>
* The generated certificate is only meant for testing purposes!
*
* @param subject
* This certificate's subject X.500 name.
* @param notBefore
* {@link Instant} before which the certificate is not valid.
* @param notAfter
* {@link Instant} after which the certificate is not valid.
* @param keypair
* {@link KeyPair} that is to be used for this certificate.
* @return Generated {@link X509Certificate}
* @since 2.8
*/
public static X509Certificate createTestRootCertificate(String subject,
Instant notBefore, Instant notAfter, KeyPair keypair) {
Objects.requireNonNull(subject, "subject");
Objects.requireNonNull(notBefore, "notBefore");
Objects.requireNonNull(notAfter, "notAfter");
Objects.requireNonNull(keypair, "keypair");
JcaX509v1CertificateBuilder certBuilder = new JcaX509v1CertificateBuilder(
new X500Name(subject),
BigInteger.valueOf(System.currentTimeMillis()),
Date.from(notBefore),
Date.from(notAfter),
new X500Name(subject),
keypair.getPublic()
);
return buildCertificate(certBuilder::build, keypair.getPrivate());
}
/**
* Creates an intermediate certificate that is signed by an issuer.
* <p>
* The generated certificate is only meant for testing purposes!
*
* @param subject
* This certificate's subject X.500 name.
* @param notBefore
* {@link Instant} before which the certificate is not valid.
* @param notAfter
* {@link Instant} after which the certificate is not valid.
* @param intermediatePublicKey
* {@link PublicKey} of this certificate
* @param issuer
* The issuer's {@link X509Certificate}.
* @param issuerPrivateKey
* {@link PrivateKey} of the issuer. This is not the private key of this
* intermediate certificate.
* @return Generated {@link X509Certificate}
* @since 2.8
*/
public static X509Certificate createTestIntermediateCertificate(String subject,
Instant notBefore, Instant notAfter, PublicKey intermediatePublicKey,
X509Certificate issuer, PrivateKey issuerPrivateKey) {
Objects.requireNonNull(subject, "subject");
Objects.requireNonNull(notBefore, "notBefore");
Objects.requireNonNull(notAfter, "notAfter");
Objects.requireNonNull(intermediatePublicKey, "intermediatePublicKey");
Objects.requireNonNull(issuer, "issuer");
Objects.requireNonNull(issuerPrivateKey, "issuerPrivateKey");
JcaX509v1CertificateBuilder certBuilder = new JcaX509v1CertificateBuilder(
new X500Name(issuer.getIssuerX500Principal().getName()),
BigInteger.valueOf(System.currentTimeMillis()),
Date.from(notBefore),
Date.from(notAfter),
new X500Name(subject),
intermediatePublicKey
);
return buildCertificate(certBuilder::build, issuerPrivateKey);
}
/**
* Creates a signed end entity certificate from the given CSR.
* <p>
* This method is only meant for testing purposes! Do not use it in a real-world CA
* implementation.
* <p>
* Do not assume that real-world certificates have a similar structure. It's up to the
* discretion of the CA which distinguished names, validity dates, extensions and
* other parameters are transferred from the CSR to the generated certificate.
*
* @param csr
* CSR to create the certificate from
* @param notBefore
* {@link Instant} before which the certificate is not valid.
* @param notAfter
* {@link Instant} after which the certificate is not valid.
* @param issuer
* The issuer's {@link X509Certificate}.
* @param issuerPrivateKey
* {@link PrivateKey} of the issuer. This is not the private key the CSR was
* signed with.
* @return Generated {@link X509Certificate}
* @since 2.8
*/
public static X509Certificate createTestCertificate(PKCS10CertificationRequest csr,
Instant notBefore, Instant notAfter, X509Certificate issuer, PrivateKey issuerPrivateKey) {
Objects.requireNonNull(csr, "csr");
Objects.requireNonNull(notBefore, "notBefore");
Objects.requireNonNull(notAfter, "notAfter");
Objects.requireNonNull(issuer, "issuer");
Objects.requireNonNull(issuerPrivateKey, "issuerPrivateKey");
try {
X500Name issuer = new X500Name("CN=acme.invalid");
BigInteger serial = BigInteger.valueOf(now);
Instant notBefore = Instant.ofEpochMilli(now);
Instant notAfter = notBefore.plus(Duration.ofDays(7));
JcaPKCS10CertificationRequest jcaCsr = new JcaPKCS10CertificationRequest(csr);
JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
issuer, serial, Date.from(notBefore), Date.from(notAfter),
issuer, keypair.getPublic());
new X500Name(issuer.getIssuerX500Principal().getName()),
BigInteger.valueOf(System.currentTimeMillis()),
Date.from(notBefore),
Date.from(notAfter),
csr.getSubject(),
jcaCsr.getPublicKey());
GeneralName[] gns = new GeneralName[1];
switch (id.getType()) {
case Identifier.TYPE_DNS:
gns[0] = new GeneralName(GeneralName.dNSName, id.getDomain());
break;
case Identifier.TYPE_IP:
gns[0] = new GeneralName(GeneralName.iPAddress, id.getIP().getHostAddress());
break;
default:
throw new IllegalArgumentException("Unsupported Identifier type " + id.getType());
Attribute[] attr = csr.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest);
if (attr.length > 0) {
ASN1Encodable[] extensions = attr[0].getAttrValues().toArray();
if (extensions.length > 0 && extensions[0] instanceof Extensions) {
GeneralNames san = GeneralNames.fromExtensions((Extensions) extensions[0], Extension.subjectAlternativeName);
certBuilder.addExtension(Extension.subjectAlternativeName, false, san);
}
}
certBuilder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(gns));
certBuilder.addExtension(ACME_VALIDATION, true, new DEROctetString(acmeValidation));
JcaContentSignerBuilder signerBuilder = new JcaContentSignerBuilder(signatureAlg);
byte[] cert = certBuilder.build(signerBuilder.build(keypair.getPrivate())).getEncoded();
return buildCertificate(certBuilder::build, issuerPrivateKey);
} catch (NoSuchAlgorithmException | InvalidKeyException | CertIOException ex) {
throw new IllegalArgumentException("Invalid CSR", ex);
}
}
/**
* Build a {@link X509Certificate} from a builder.
*
* @param builder
* Builder method that receives a {@link ContentSigner} and returns a {@link
* X509CertificateHolder}.
* @param privateKey
* {@link PrivateKey} to sign the certificate with
* @return The generated {@link X509Certificate}
*/
private static X509Certificate buildCertificate(Function<ContentSigner, X509CertificateHolder> builder, PrivateKey privateKey) {
try {
JcaContentSignerBuilder signerBuilder = new JcaContentSignerBuilder("SHA256withRSA");
byte[] cert = builder.apply(signerBuilder.build(privateKey)).getEncoded();
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
return (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(cert));
} catch (CertificateException | OperatorCreationException ex) {
throw new IOException(ex);
} catch (CertificateException | OperatorCreationException | IOException ex) {
throw new IllegalArgumentException("Could not build certificate", ex);
}
}

View File

@ -13,6 +13,7 @@
*/
package org.shredzone.acme4j.util;
import static java.time.temporal.ChronoUnit.SECONDS;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
@ -25,6 +26,8 @@ import java.lang.reflect.Modifier;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.time.Duration;
@ -142,6 +145,101 @@ public class CertificateUtilsTest {
assertThat(getIpSANs(cert), contains(subject));
}
/**
* Test if {@link CertificateUtils#createTestRootCertificate(String, Instant, Instant,
* KeyPair)} generates a valid root certificate.
*/
@Test
public void testCreateTestRootCertificate() throws Exception {
KeyPair keypair = KeyPairUtils.createKeyPair(2048);
String subject = "CN=Test Root Certificate";
Instant notBefore = Instant.now().truncatedTo(SECONDS);
Instant notAfter = notBefore.plus(Duration.ofDays(14)).truncatedTo(SECONDS);
X509Certificate cert = CertificateUtils.createTestRootCertificate(subject,
notBefore, notAfter, keypair);
assertThat(cert.getIssuerX500Principal().getName(), is(subject));
assertThat(cert.getSubjectX500Principal().getName(), is(subject));
assertThat(cert.getNotBefore().toInstant(), is(notBefore));
assertThat(cert.getNotAfter().toInstant(), is(notAfter));
assertThat(cert.getSerialNumber(), not(nullValue()));
assertThat(cert.getPublicKey(), is(keypair.getPublic()));
cert.verify(cert.getPublicKey()); // self-signed
}
/**
* Test if {@link CertificateUtils#createTestIntermediateCertificate(String, Instant,
* Instant, PublicKey, X509Certificate, PrivateKey)} generates a valid intermediate
* certificate.
*/
@Test
public void testCreateTestIntermediateCertificate() throws Exception {
KeyPair rootKeypair = KeyPairUtils.createKeyPair(2048);
String rootSubject = "CN=Test Root Certificate";
Instant rootNotBefore = Instant.now().minus(Duration.ofDays(1)).truncatedTo(SECONDS);
Instant rootNotAfter = rootNotBefore.plus(Duration.ofDays(14)).truncatedTo(SECONDS);
X509Certificate rootCert = CertificateUtils.createTestRootCertificate(rootSubject,
rootNotBefore, rootNotAfter, rootKeypair);
KeyPair keypair = KeyPairUtils.createKeyPair(2048);
String subject = "CN=Test Intermediate Certificate";
Instant notBefore = Instant.now().truncatedTo(SECONDS);
Instant notAfter = notBefore.plus(Duration.ofDays(7)).truncatedTo(SECONDS);
X509Certificate cert = CertificateUtils.createTestIntermediateCertificate(subject,
notBefore, notAfter, keypair.getPublic(), rootCert, rootKeypair.getPrivate());
assertThat(cert.getIssuerX500Principal().getName(), is(rootSubject));
assertThat(cert.getSubjectX500Principal().getName(), is(subject));
assertThat(cert.getNotBefore().toInstant(), is(notBefore));
assertThat(cert.getNotAfter().toInstant(), is(notAfter));
assertThat(cert.getSerialNumber(), not(nullValue()));
assertThat(cert.getSerialNumber(), not(rootCert.getSerialNumber()));
assertThat(cert.getPublicKey(), is(keypair.getPublic()));
cert.verify(rootKeypair.getPublic()); // signed by root
}
/**
* Test if {@link CertificateUtils#createTestCertificate(PKCS10CertificationRequest,
* Instant, Instant, X509Certificate, PrivateKey)} generates a valid certificate.
*/
@Test
public void testCreateTestCertificate() throws Exception {
KeyPair rootKeypair = KeyPairUtils.createKeyPair(2048);
String rootSubject = "CN=Test Root Certificate";
Instant rootNotBefore = Instant.now().minus(Duration.ofDays(1)).truncatedTo(SECONDS);
Instant rootNotAfter = rootNotBefore.plus(Duration.ofDays(14)).truncatedTo(SECONDS);
X509Certificate rootCert = CertificateUtils.createTestRootCertificate(rootSubject,
rootNotBefore, rootNotAfter, rootKeypair);
KeyPair keypair = KeyPairUtils.createKeyPair(2048);
Instant notBefore = Instant.now().truncatedTo(SECONDS);
Instant notAfter = notBefore.plus(Duration.ofDays(7)).truncatedTo(SECONDS);
CSRBuilder builder = new CSRBuilder();
builder.addDomains("example.org", "www.example.org");
builder.addIP(InetAddress.getByName("192.168.0.1"));
builder.sign(keypair);
PKCS10CertificationRequest csr = builder.getCSR();
X509Certificate cert = CertificateUtils.createTestCertificate(csr, notBefore,
notAfter, rootCert, rootKeypair.getPrivate());
assertThat(cert.getIssuerX500Principal().getName(), is(rootSubject));
assertThat(cert.getSubjectX500Principal().getName(), is("CN=example.org"));
assertThat(getSANs(cert), contains("example.org", "www.example.org"));
assertThat(getIpSANs(cert), contains(InetAddress.getByName("192.168.0.1")));
assertThat(cert.getNotBefore().toInstant(), is(notBefore));
assertThat(cert.getNotAfter().toInstant(), is(notAfter));
assertThat(cert.getSerialNumber(), not(nullValue()));
assertThat(cert.getSerialNumber(), not(rootCert.getSerialNumber()));
assertThat(cert.getPublicKey(), is(keypair.getPublic()));
cert.verify(rootKeypair.getPublic()); // signed by root
}
/**
* Extracts all DNSName SANs from a certificate.
*