001/*
002 * acme4j - Java ACME client
003 *
004 * Copyright (C) 2015 Richard "Shred" Körber
005 *   http://acme4j.shredzone.org
006 *
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
013 */
014package org.shredzone.acme4j.util;
015
016import java.io.ByteArrayInputStream;
017import java.io.IOException;
018import java.io.InputStream;
019import java.io.InputStreamReader;
020import java.math.BigInteger;
021import java.nio.charset.StandardCharsets;
022import java.security.InvalidKeyException;
023import java.security.KeyPair;
024import java.security.NoSuchAlgorithmException;
025import java.security.PrivateKey;
026import java.security.PublicKey;
027import java.security.cert.CertificateException;
028import java.security.cert.CertificateFactory;
029import java.security.cert.X509Certificate;
030import java.time.Duration;
031import java.time.Instant;
032import java.util.Date;
033import java.util.Objects;
034import java.util.function.Function;
035
036import org.bouncycastle.asn1.ASN1Encodable;
037import org.bouncycastle.asn1.ASN1ObjectIdentifier;
038import org.bouncycastle.asn1.DEROctetString;
039import org.bouncycastle.asn1.pkcs.Attribute;
040import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
041import org.bouncycastle.asn1.x500.X500Name;
042import org.bouncycastle.asn1.x509.Extension;
043import org.bouncycastle.asn1.x509.Extensions;
044import org.bouncycastle.asn1.x509.GeneralName;
045import org.bouncycastle.asn1.x509.GeneralNames;
046import org.bouncycastle.cert.CertIOException;
047import org.bouncycastle.cert.X509CertificateHolder;
048import org.bouncycastle.cert.jcajce.JcaX509v1CertificateBuilder;
049import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
050import org.bouncycastle.openssl.PEMParser;
051import org.bouncycastle.operator.ContentSigner;
052import org.bouncycastle.operator.OperatorCreationException;
053import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
054import org.bouncycastle.pkcs.PKCS10CertificationRequest;
055import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest;
056import org.shredzone.acme4j.Identifier;
057import org.shredzone.acme4j.challenge.TlsAlpn01Challenge;
058
059/**
060 * Utility class offering convenience methods for certificates.
061 * <p>
062 * Requires {@code Bouncy Castle}. This class is part of the {@code acme4j-utils} module.
063 */
064public final class CertificateUtils {
065
066    /**
067     * The {@code acmeValidation} object identifier.
068     *
069     * @since 2.1
070     */
071    public static final ASN1ObjectIdentifier ACME_VALIDATION =
072                    new ASN1ObjectIdentifier(TlsAlpn01Challenge.ACME_VALIDATION_OID).intern();
073
074    private CertificateUtils() {
075        // utility class without constructor
076    }
077
078    /**
079     * Reads a CSR PEM file.
080     *
081     * @param in
082     *            {@link InputStream} to read the CSR from. The {@link InputStream} is
083     *            closed after use.
084     * @return CSR that was read
085     */
086    public static PKCS10CertificationRequest readCSR(InputStream in) throws IOException {
087        try (PEMParser pemParser = new PEMParser(new InputStreamReader(in, StandardCharsets.US_ASCII))) {
088            Object parsedObj = pemParser.readObject();
089            if (!(parsedObj instanceof PKCS10CertificationRequest)) {
090                throw new IOException("Not a PKCS10 CSR");
091            }
092            return (PKCS10CertificationRequest) parsedObj;
093        }
094    }
095
096    /**
097     * Creates a self-signed {@link X509Certificate} that can be used for the
098     * {@link TlsAlpn01Challenge}. The certificate is valid for 7 days.
099     *
100     * @param keypair
101     *            A domain {@link KeyPair} to be used for the challenge
102     * @param id
103     *            The {@link Identifier} that is to be validated
104     * @param acmeValidation
105     *            The value that is returned by
106     *            {@link TlsAlpn01Challenge#getAcmeValidation()}
107     * @return Created certificate
108     * @since 2.6
109     */
110    public static X509Certificate createTlsAlpn01Certificate(KeyPair keypair, Identifier id, byte[] acmeValidation)
111                throws IOException {
112        Objects.requireNonNull(keypair, "keypair");
113        Objects.requireNonNull(id, "id");
114        if (acmeValidation == null || acmeValidation.length != 32) {
115            throw new IllegalArgumentException("Bad acmeValidation parameter");
116        }
117
118        final long now = System.currentTimeMillis();
119
120        X500Name issuer = new X500Name("CN=acme.invalid");
121        BigInteger serial = BigInteger.valueOf(now);
122        Instant notBefore = Instant.ofEpochMilli(now);
123        Instant notAfter = notBefore.plus(Duration.ofDays(7));
124
125        JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
126                    issuer, serial, Date.from(notBefore), Date.from(notAfter),
127                    issuer, keypair.getPublic());
128
129        GeneralName[] gns = new GeneralName[1];
130
131        switch (id.getType()) {
132            case Identifier.TYPE_DNS:
133                gns[0] = new GeneralName(GeneralName.dNSName, id.getDomain());
134                break;
135
136            case Identifier.TYPE_IP:
137                gns[0] = new GeneralName(GeneralName.iPAddress, id.getIP().getHostAddress());
138                break;
139
140            default:
141                throw new IllegalArgumentException("Unsupported Identifier type " + id.getType());
142        }
143        certBuilder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(gns));
144        certBuilder.addExtension(ACME_VALIDATION, true, new DEROctetString(acmeValidation));
145
146        return buildCertificate(certBuilder::build, keypair.getPrivate());
147    }
148
149    /**
150     * Creates a self-signed root certificate.
151     * <p>
152     * The generated certificate is only meant for testing purposes!
153     *
154     * @param subject
155     *         This certificate's subject X.500 name.
156     * @param notBefore
157     *         {@link Instant} before which the certificate is not valid.
158     * @param notAfter
159     *         {@link Instant} after which the certificate is not valid.
160     * @param keypair
161     *         {@link KeyPair} that is to be used for this certificate.
162     * @return Generated {@link X509Certificate}
163     * @since 2.8
164     */
165    public static X509Certificate createTestRootCertificate(String subject,
166            Instant notBefore, Instant notAfter, KeyPair keypair) {
167        Objects.requireNonNull(subject, "subject");
168        Objects.requireNonNull(notBefore, "notBefore");
169        Objects.requireNonNull(notAfter, "notAfter");
170        Objects.requireNonNull(keypair, "keypair");
171
172        JcaX509v1CertificateBuilder certBuilder = new JcaX509v1CertificateBuilder(
173                new X500Name(subject),
174                BigInteger.valueOf(System.currentTimeMillis()),
175                Date.from(notBefore),
176                Date.from(notAfter),
177                new X500Name(subject),
178                keypair.getPublic()
179        );
180
181        return buildCertificate(certBuilder::build, keypair.getPrivate());
182    }
183
184    /**
185     * Creates an intermediate certificate that is signed by an issuer.
186     * <p>
187     * The generated certificate is only meant for testing purposes!
188     *
189     * @param subject
190     *         This certificate's subject X.500 name.
191     * @param notBefore
192     *         {@link Instant} before which the certificate is not valid.
193     * @param notAfter
194     *         {@link Instant} after which the certificate is not valid.
195     * @param intermediatePublicKey
196     *         {@link PublicKey} of this certificate
197     * @param issuer
198     *         The issuer's {@link X509Certificate}.
199     * @param issuerPrivateKey
200     *         {@link PrivateKey} of the issuer. This is not the private key of this
201     *         intermediate certificate.
202     * @return Generated {@link X509Certificate}
203     * @since 2.8
204     */
205    public static X509Certificate createTestIntermediateCertificate(String subject,
206            Instant notBefore, Instant notAfter, PublicKey intermediatePublicKey,
207            X509Certificate issuer, PrivateKey issuerPrivateKey) {
208        Objects.requireNonNull(subject, "subject");
209        Objects.requireNonNull(notBefore, "notBefore");
210        Objects.requireNonNull(notAfter, "notAfter");
211        Objects.requireNonNull(intermediatePublicKey, "intermediatePublicKey");
212        Objects.requireNonNull(issuer, "issuer");
213        Objects.requireNonNull(issuerPrivateKey, "issuerPrivateKey");
214
215        JcaX509v1CertificateBuilder certBuilder = new JcaX509v1CertificateBuilder(
216                new X500Name(issuer.getIssuerX500Principal().getName()),
217                BigInteger.valueOf(System.currentTimeMillis()),
218                Date.from(notBefore),
219                Date.from(notAfter),
220                new X500Name(subject),
221                intermediatePublicKey
222        );
223
224        return buildCertificate(certBuilder::build, issuerPrivateKey);
225    }
226
227    /**
228     * Creates a signed end entity certificate from the given CSR.
229     * <p>
230     * This method is only meant for testing purposes! Do not use it in a real-world CA
231     * implementation.
232     * <p>
233     * Do not assume that real-world certificates have a similar structure. It's up to the
234     * discretion of the CA which distinguished names, validity dates, extensions and
235     * other parameters are transferred from the CSR to the generated certificate.
236     *
237     * @param csr
238     *         CSR to create the certificate from
239     * @param notBefore
240     *         {@link Instant} before which the certificate is not valid.
241     * @param notAfter
242     *         {@link Instant} after which the certificate is not valid.
243     * @param issuer
244     *         The issuer's {@link X509Certificate}.
245     * @param issuerPrivateKey
246     *         {@link PrivateKey} of the issuer. This is not the private key the CSR was
247     *         signed with.
248     * @return Generated {@link X509Certificate}
249     * @since 2.8
250     */
251    public static X509Certificate createTestCertificate(PKCS10CertificationRequest csr,
252            Instant notBefore, Instant notAfter, X509Certificate issuer, PrivateKey issuerPrivateKey) {
253        Objects.requireNonNull(csr, "csr");
254        Objects.requireNonNull(notBefore, "notBefore");
255        Objects.requireNonNull(notAfter, "notAfter");
256        Objects.requireNonNull(issuer, "issuer");
257        Objects.requireNonNull(issuerPrivateKey, "issuerPrivateKey");
258
259        try {
260            JcaPKCS10CertificationRequest jcaCsr = new JcaPKCS10CertificationRequest(csr);
261
262            JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
263                    new X500Name(issuer.getIssuerX500Principal().getName()),
264                    BigInteger.valueOf(System.currentTimeMillis()),
265                    Date.from(notBefore),
266                    Date.from(notAfter),
267                    csr.getSubject(),
268                    jcaCsr.getPublicKey());
269
270            Attribute[] attr = csr.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest);
271            if (attr.length > 0) {
272                ASN1Encodable[] extensions = attr[0].getAttrValues().toArray();
273                if (extensions.length > 0 && extensions[0] instanceof Extensions) {
274                    GeneralNames san = GeneralNames.fromExtensions((Extensions) extensions[0], Extension.subjectAlternativeName);
275                    certBuilder.addExtension(Extension.subjectAlternativeName, false, san);
276                }
277            }
278
279            return buildCertificate(certBuilder::build, issuerPrivateKey);
280        } catch (NoSuchAlgorithmException | InvalidKeyException | CertIOException ex) {
281            throw new IllegalArgumentException("Invalid CSR", ex);
282        }
283    }
284
285    /**
286     * Build a {@link X509Certificate} from a builder.
287     *
288     * @param builder
289     *         Builder method that receives a {@link ContentSigner} and returns a {@link
290     *         X509CertificateHolder}.
291     * @param privateKey
292     *         {@link PrivateKey} to sign the certificate with
293     * @return The generated {@link X509Certificate}
294     */
295    private static X509Certificate buildCertificate(Function<ContentSigner, X509CertificateHolder> builder, PrivateKey privateKey) {
296        try {
297            JcaContentSignerBuilder signerBuilder = new JcaContentSignerBuilder("SHA256withRSA");
298            byte[] cert = builder.apply(signerBuilder.build(privateKey)).getEncoded();
299            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
300            return (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(cert));
301        } catch (CertificateException | OperatorCreationException | IOException ex) {
302            throw new IllegalArgumentException("Could not build certificate", ex);
303        }
304    }
305
306}