![JAR search and dependency download from the Maven repository](/logo.png)
org.shredzone.acme4j.util.CertificateUtils Maven / Gradle / Ivy
/*
* acme4j - Java ACME client
*
* Copyright (C) 2015 Richard "Shred" Körber
* http://acme4j.shredzone.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*/
package org.shredzone.acme4j.util;
import java.io.ByteArrayInputStream;
import java.io.IOException;
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;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import java.util.Objects;
import java.util.function.Function;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.DEROctetString;
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;
/**
* Utility class offering convenience methods for certificates.
*
* Requires {@code Bouncy Castle}.
*/
public final class CertificateUtils {
/**
* The {@code acmeValidation} object identifier.
*
* @since 2.1
*/
public static final ASN1ObjectIdentifier ACME_VALIDATION =
new ASN1ObjectIdentifier(TlsAlpn01Challenge.ACME_VALIDATION_OID).intern();
private CertificateUtils() {
// utility class without constructor
}
/**
* Reads a CSR PEM file.
*
* @param in
* {@link InputStream} to read the CSR from. The {@link InputStream} is
* closed after use.
* @return CSR that was read
*/
public static PKCS10CertificationRequest readCSR(InputStream in) throws IOException {
try (var pemParser = new PEMParser(new InputStreamReader(in, StandardCharsets.US_ASCII))) {
var parsedObj = pemParser.readObject();
if (!(parsedObj instanceof PKCS10CertificationRequest)) {
throw new IOException("Not a PKCS10 CSR");
}
return (PKCS10CertificationRequest) parsedObj;
}
}
/**
* Creates a self-signed {@link X509Certificate} that can be used for the
* {@link TlsAlpn01Challenge}. The certificate is valid for 7 days.
*
* @param keypair
* A domain {@link KeyPair} to be used for the challenge
* @param id
* The {@link Identifier} that is to be validated
* @param acmeValidation
* The value that is returned by
* {@link TlsAlpn01Challenge#getAcmeValidation()}
* @return Created certificate
* @since 2.6
*/
public static X509Certificate createTlsAlpn01Certificate(KeyPair keypair, Identifier id, byte[] acmeValidation)
throws IOException {
Objects.requireNonNull(keypair, "keypair");
Objects.requireNonNull(id, "id");
if (acmeValidation == null || acmeValidation.length != 32) {
throw new IllegalArgumentException("Bad acmeValidation parameter");
}
var now = System.currentTimeMillis();
var issuer = new X500Name("CN=acme.invalid");
var serial = BigInteger.valueOf(now);
var notBefore = Instant.ofEpochMilli(now);
var notAfter = notBefore.plus(Duration.ofDays(7));
var certBuilder = new JcaX509v3CertificateBuilder(
issuer, serial, Date.from(notBefore), Date.from(notAfter),
issuer, keypair.getPublic());
var 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.
*
* 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");
var 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.
*
* 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");
var 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.
*
* This method is only meant for testing purposes! Do not use it in a real-world CA
* implementation.
*
* 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 {
var jcaCsr = new JcaPKCS10CertificationRequest(csr);
var certBuilder = new JcaX509v3CertificateBuilder(
new X500Name(issuer.getIssuerX500Principal().getName()),
BigInteger.valueOf(System.currentTimeMillis()),
Date.from(notBefore),
Date.from(notAfter),
csr.getSubject(),
jcaCsr.getPublicKey());
var attr = csr.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest);
if (attr.length > 0) {
var extensions = attr[0].getAttrValues().toArray();
if (extensions.length > 0 && extensions[0] instanceof Extensions) {
var san = GeneralNames.fromExtensions((Extensions) extensions[0], Extension.subjectAlternativeName);
var critical = csr.getSubject().getRDNs().length == 0;
certBuilder.addExtension(Extension.subjectAlternativeName, critical, 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 builder, PrivateKey privateKey) {
try {
var signerBuilder = new JcaContentSignerBuilder("SHA256withRSA");
var cert = builder.apply(signerBuilder.build(privateKey)).getEncoded();
var certificateFactory = CertificateFactory.getInstance("X.509");
return (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(cert));
} catch (CertificateException | OperatorCreationException | IOException ex) {
throw new IllegalArgumentException("Could not build certificate", ex);
}
}
}