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