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 static java.nio.charset.StandardCharsets.UTF_8;
017import static java.util.Objects.requireNonNull;
018import static java.util.stream.Collectors.joining;
019import static org.shredzone.acme4j.toolbox.AcmeUtils.toAce;
020
021import java.io.IOException;
022import java.io.OutputStream;
023import java.io.OutputStreamWriter;
024import java.io.Writer;
025import java.net.InetAddress;
026import java.security.KeyPair;
027import java.security.interfaces.ECKey;
028import java.util.ArrayList;
029import java.util.Arrays;
030import java.util.Collection;
031import java.util.List;
032import java.util.Objects;
033
034import edu.umd.cs.findbugs.annotations.Nullable;
035import org.bouncycastle.asn1.ASN1ObjectIdentifier;
036import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
037import org.bouncycastle.asn1.x500.X500Name;
038import org.bouncycastle.asn1.x500.X500NameBuilder;
039import org.bouncycastle.asn1.x500.style.BCStyle;
040import org.bouncycastle.asn1.x509.Extension;
041import org.bouncycastle.asn1.x509.ExtensionsGenerator;
042import org.bouncycastle.asn1.x509.GeneralName;
043import org.bouncycastle.asn1.x509.GeneralNames;
044import org.bouncycastle.operator.OperatorCreationException;
045import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
046import org.bouncycastle.pkcs.PKCS10CertificationRequest;
047import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
048import org.bouncycastle.util.io.pem.PemObject;
049import org.bouncycastle.util.io.pem.PemWriter;
050import org.shredzone.acme4j.Identifier;
051
052/**
053 * Generator for a CSR (Certificate Signing Request) suitable for ACME servers.
054 * <p>
055 * Requires {@code Bouncy Castle}. The
056 * {@link org.bouncycastle.jce.provider.BouncyCastleProvider} must be added as security
057 * provider.
058 */
059public class CSRBuilder {
060    private static final String SIGNATURE_ALG = "SHA256withRSA";
061    private static final String EC_SIGNATURE_ALG = "SHA256withECDSA";
062
063    private final X500NameBuilder namebuilder = new X500NameBuilder(X500Name.getDefaultStyle());
064    private final List<String> namelist = new ArrayList<>();
065    private final List<InetAddress> iplist = new ArrayList<>();
066    private @Nullable PKCS10CertificationRequest csr = null;
067    private boolean hasCnSet = false;
068
069    /**
070     * Adds a domain name to the CSR. All domain names will be added as <em>Subject
071     * Alternative Name</em>.
072     * <p>
073     * IDN domain names are ACE encoded automatically.
074     * <p>
075     * For wildcard certificates, the domain name must be prefixed with {@code "*."}.
076     *
077     * @param domain
078     *         Domain name to add
079     */
080    public void addDomain(String domain) {
081        namelist.add(toAce(requireNonNull(domain)));
082    }
083
084    /**
085     * Adds a {@link Collection} of domains.
086     * <p>
087     * IDN domain names are ACE encoded automatically.
088     *
089     * @param domains
090     *            Collection of domain names to add
091     */
092    public void addDomains(Collection<String> domains) {
093        domains.forEach(this::addDomain);
094    }
095
096    /**
097     * Adds multiple domain names.
098     * <p>
099     * IDN domain names are ACE encoded automatically.
100     *
101     * @param domains
102     *            Domain names to add
103     */
104    public void addDomains(String... domains) {
105        Arrays.stream(domains).forEach(this::addDomain);
106    }
107
108    /**
109     * Adds an {@link InetAddress}. All IP addresses will be set as iPAddress <em>Subject
110     * Alternative Name</em>.
111     *
112     * @param address
113     *            {@link InetAddress} to add
114     * @since 2.4
115     */
116    public void addIP(InetAddress address) {
117        iplist.add(requireNonNull(address));
118    }
119
120    /**
121     * Adds a {@link Collection} of IP addresses.
122     *
123     * @param ips
124     *            Collection of IP addresses to add
125     * @since 2.4
126     */
127    public void addIPs(Collection<InetAddress> ips) {
128        ips.forEach(this::addIP);
129    }
130
131    /**
132     * Adds multiple IP addresses.
133     *
134     * @param ips
135     *            IP addresses to add
136     * @since 2.4
137     */
138    public void addIPs(InetAddress... ips) {
139        Arrays.stream(ips).forEach(this::addIP);
140    }
141
142    /**
143     * Adds an {@link Identifier}. Only DNS and IP types are supported.
144     *
145     * @param id
146     *            {@link Identifier} to add
147     * @since 2.7
148     */
149    public void addIdentifier(Identifier id) {
150        requireNonNull(id);
151        if (Identifier.TYPE_DNS.equals(id.getType())) {
152            addDomain(id.getDomain());
153        } else if (Identifier.TYPE_IP.equals(id.getType())) {
154            addIP(id.getIP());
155        } else {
156            throw new IllegalArgumentException("Unknown identifier type: " + id.getType());
157        }
158    }
159
160    /**
161     * Adds a {@link Collection} of {@link Identifier}.
162     *
163     * @param ids
164     *            Collection of Identifiers to add
165     * @since 2.7
166     */
167    public void addIdentifiers(Collection<Identifier> ids) {
168        ids.forEach(this::addIdentifier);
169    }
170
171    /**
172     * Adds multiple {@link Identifier}.
173     *
174     * @param ids
175     *            Identifiers to add
176     * @since 2.7
177     */
178    public void addIdentifiers(Identifier... ids) {
179        Arrays.stream(ids).forEach(this::addIdentifier);
180    }
181
182    /**
183     * Sets an entry of the subject used for the CSR.
184     * <p>
185     * This method is meant as "expert mode" for setting attributes that are not covered
186     * by the other methods. It is at the discretion of the ACME server to accept this
187     * parameter.
188     *
189     * @param attName
190     *         The BCStyle attribute name
191     * @param value
192     *         The value
193     * @since 2.14
194     */
195    public void addValue(String attName, String value) {
196        var oid = X500Name.getDefaultStyle().attrNameToOID(requireNonNull(attName, "attribute name must not be null"));
197        addValue(oid, value);
198    }
199
200    /**
201     * Sets an entry of the subject used for the CSR.
202     * <p>
203     * This method is meant as "expert mode" for setting attributes that are not covered
204     * by the other methods. It is at the discretion of the ACME server to accept this
205     * parameter.
206     *
207     * @param oid
208     *         The OID of the attribute to be added
209     * @param value
210     *         The value
211     * @since 2.14
212     */
213    public void addValue(ASN1ObjectIdentifier oid, String value) {
214        if (requireNonNull(oid, "OID must not be null").equals(BCStyle.CN)) {
215            addDomain(value);
216            if (hasCnSet) {
217                return;
218            }
219            hasCnSet = true;
220        }
221        namebuilder.addRDN(oid, requireNonNull(value, "attribute value must not be null"));
222    }
223
224    /**
225     * Sets the common name.
226     * <p>
227     * Note that it is at the discretion of the ACME server to accept this parameter.
228     *
229     * @since 3.2.0
230     */
231    public void setCommonName(String cn) {
232        namebuilder.addRDN(BCStyle.CN, requireNonNull(cn));
233    }
234
235    /**
236     * Sets the organization.
237     * <p>
238     * Note that it is at the discretion of the ACME server to accept this parameter.
239     */
240    public void setOrganization(String o) {
241        namebuilder.addRDN(BCStyle.O, requireNonNull(o));
242    }
243
244    /**
245     * Sets the organizational unit.
246     * <p>
247     * Note that it is at the discretion of the ACME server to accept this parameter.
248     */
249    public void setOrganizationalUnit(String ou) {
250        namebuilder.addRDN(BCStyle.OU, requireNonNull(ou));
251    }
252
253    /**
254     * Sets the city or locality.
255     * <p>
256     * Note that it is at the discretion of the ACME server to accept this parameter.
257     */
258    public void setLocality(String l) {
259        namebuilder.addRDN(BCStyle.L, requireNonNull(l));
260    }
261
262    /**
263     * Sets the state or province.
264     * <p>
265     * Note that it is at the discretion of the ACME server to accept this parameter.
266     */
267    public void setState(String st) {
268        namebuilder.addRDN(BCStyle.ST, requireNonNull(st));
269    }
270
271    /**
272     * Sets the country.
273     * <p>
274     * Note that it is at the discretion of the ACME server to accept this parameter.
275     */
276    public void setCountry(String c) {
277        namebuilder.addRDN(BCStyle.C, requireNonNull(c));
278    }
279
280    /**
281     * Signs the completed CSR.
282     *
283     * @param keypair
284     *            {@link KeyPair} to sign the CSR with
285     */
286    public void sign(KeyPair keypair) throws IOException {
287        Objects.requireNonNull(keypair, "keypair");
288        if (namelist.isEmpty() && iplist.isEmpty()) {
289            throw new IllegalStateException("No domain or IP address was set");
290        }
291
292        try {
293            var ix = 0;
294            var gns = new GeneralName[namelist.size() + iplist.size()];
295            for (var name : namelist) {
296                gns[ix++] = new GeneralName(GeneralName.dNSName, name);
297            }
298            for (var ip : iplist) {
299                gns[ix++] = new GeneralName(GeneralName.iPAddress, ip.getHostAddress());
300            }
301            var subjectAltName = new GeneralNames(gns);
302
303            var p10Builder = new JcaPKCS10CertificationRequestBuilder(namebuilder.build(), keypair.getPublic());
304
305            var extensionsGenerator = new ExtensionsGenerator();
306            extensionsGenerator.addExtension(Extension.subjectAlternativeName, false, subjectAltName);
307
308            p10Builder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extensionsGenerator.generate());
309
310            var pk = keypair.getPrivate();
311            var csBuilder = new JcaContentSignerBuilder(pk instanceof ECKey ? EC_SIGNATURE_ALG : SIGNATURE_ALG);
312            var signer = csBuilder.build(pk);
313
314            csr = p10Builder.build(signer);
315        } catch (OperatorCreationException ex) {
316            throw new IOException("Could not generate CSR", ex);
317        }
318    }
319
320    /**
321     * Gets the PKCS#10 certification request.
322     */
323    public PKCS10CertificationRequest getCSR() {
324        if (csr == null) {
325            throw new IllegalStateException("sign CSR first");
326        }
327
328        return csr;
329    }
330
331    /**
332     * Gets an encoded PKCS#10 certification request.
333     */
334    public byte[] getEncoded() throws IOException {
335        return getCSR().getEncoded();
336    }
337
338    /**
339     * Writes the signed certificate request to a {@link Writer}.
340     *
341     * @param w
342     *            {@link Writer} to write the PEM file to. The {@link Writer} is closed
343     *            after use.
344     */
345    public void write(Writer w) throws IOException {
346        if (csr == null) {
347            throw new IllegalStateException("sign CSR first");
348        }
349
350        try (var pw = new PemWriter(w)) {
351            pw.writeObject(new PemObject("CERTIFICATE REQUEST", getEncoded()));
352        }
353    }
354
355    /**
356     * Writes the signed certificate request to an {@link OutputStream}.
357     *
358     * @param out
359     *            {@link OutputStream} to write the PEM file to. The {@link OutputStream}
360     *            is closed after use.
361     */
362    public void write(OutputStream out) throws IOException {
363        write(new OutputStreamWriter(out, UTF_8));
364    }
365
366    @Override
367    public String toString() {
368        var sb = new StringBuilder();
369        sb.append(namebuilder.build());
370        if (!namelist.isEmpty()) {
371            sb.append(namelist.stream().collect(joining(",DNS=", ",DNS=", "")));
372        }
373        if (!iplist.isEmpty()) {
374            sb.append(iplist.stream()
375                    .map(InetAddress::getHostAddress)
376                    .collect(joining(",IP=", ",IP=", "")));
377        }
378        return sb.toString();
379    }
380
381}