mirror of https://github.com/shred/acme4j
Add utility methods to generate test certificates
parent
c1b3b26185
commit
b5550a3fdb
|
@ -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,9 +120,7 @@ public final class CertificateUtils {
|
|||
}
|
||||
|
||||
final long now = System.currentTimeMillis();
|
||||
final String signatureAlg = "SHA256withRSA";
|
||||
|
||||
try {
|
||||
X500Name issuer = new X500Name("CN=acme.invalid");
|
||||
BigInteger serial = BigInteger.valueOf(now);
|
||||
Instant notBefore = Instant.ofEpochMilli(now);
|
||||
|
@ -133,17 +145,165 @@ public final class CertificateUtils {
|
|||
throw new IllegalArgumentException("Unsupported Identifier type " + id.getType());
|
||||
}
|
||||
certBuilder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(gns));
|
||||
|
||||
certBuilder.addExtension(ACME_VALIDATION, true, new DEROctetString(acmeValidation));
|
||||
|
||||
JcaContentSignerBuilder signerBuilder = new JcaContentSignerBuilder(signatureAlg);
|
||||
return buildCertificate(certBuilder::build, keypair.getPrivate());
|
||||
}
|
||||
|
||||
byte[] cert = certBuilder.build(signerBuilder.build(keypair.getPrivate())).getEncoded();
|
||||
/**
|
||||
* 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 {
|
||||
JcaPKCS10CertificationRequest jcaCsr = new JcaPKCS10CertificationRequest(csr);
|
||||
|
||||
JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
|
||||
new X500Name(issuer.getIssuerX500Principal().getName()),
|
||||
BigInteger.valueOf(System.currentTimeMillis()),
|
||||
Date.from(notBefore),
|
||||
Date.from(notAfter),
|
||||
csr.getSubject(),
|
||||
jcaCsr.getPublicKey());
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue