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