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.util.Objects.requireNonNull;
017import static java.util.stream.Collectors.joining;
018import static org.shredzone.acme4j.toolbox.AcmeUtils.toAce;
019
020import java.io.IOException;
021import java.io.OutputStream;
022import java.io.OutputStreamWriter;
023import java.io.Writer;
024import java.net.InetAddress;
025import java.security.KeyPair;
026import java.security.PrivateKey;
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 javax.annotation.ParametersAreNonnullByDefault;
035import javax.annotation.WillClose;
036
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}. This class is part of the {@code acme4j-utils} module.
059 */
060@ParametersAreNonnullByDefault
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 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 the organization.
190     * <p>
191     * Note that it is at the discretion of the ACME server to accept this parameter.
192     */
193    public void setOrganization(String o) {
194        namebuilder.addRDN(BCStyle.O, requireNonNull(o));
195    }
196
197    /**
198     * Sets the organizational unit.
199     * <p>
200     * Note that it is at the discretion of the ACME server to accept this parameter.
201     */
202    public void setOrganizationalUnit(String ou) {
203        namebuilder.addRDN(BCStyle.OU, requireNonNull(ou));
204    }
205
206    /**
207     * Sets the city or locality.
208     * <p>
209     * Note that it is at the discretion of the ACME server to accept this parameter.
210     */
211    public void setLocality(String l) {
212        namebuilder.addRDN(BCStyle.L, requireNonNull(l));
213    }
214
215    /**
216     * Sets the state or province.
217     * <p>
218     * Note that it is at the discretion of the ACME server to accept this parameter.
219     */
220    public void setState(String st) {
221        namebuilder.addRDN(BCStyle.ST, requireNonNull(st));
222    }
223
224    /**
225     * Sets the country.
226     * <p>
227     * Note that it is at the discretion of the ACME server to accept this parameter.
228     */
229    public void setCountry(String c) {
230        namebuilder.addRDN(BCStyle.C, requireNonNull(c));
231    }
232
233    /**
234     * Signs the completed CSR.
235     *
236     * @param keypair
237     *            {@link KeyPair} to sign the CSR with
238     */
239    public void sign(KeyPair keypair) throws IOException {
240        Objects.requireNonNull(keypair, "keypair");
241        if (namelist.isEmpty() && iplist.isEmpty()) {
242            throw new IllegalStateException("No domain or IP address was set");
243        }
244
245        try {
246            int ix = 0;
247            GeneralName[] gns = new GeneralName[namelist.size() + iplist.size()];
248            for (String name : namelist) {
249                gns[ix++] = new GeneralName(GeneralName.dNSName, name);
250            }
251            for (InetAddress ip : iplist) {
252                gns[ix++] = new GeneralName(GeneralName.iPAddress, ip.getHostAddress());
253            }
254            GeneralNames subjectAltName = new GeneralNames(gns);
255
256            PKCS10CertificationRequestBuilder p10Builder =
257                            new JcaPKCS10CertificationRequestBuilder(namebuilder.build(), keypair.getPublic());
258
259            ExtensionsGenerator extensionsGenerator = new ExtensionsGenerator();
260            extensionsGenerator.addExtension(Extension.subjectAlternativeName, false, subjectAltName);
261            p10Builder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extensionsGenerator.generate());
262
263            PrivateKey pk = keypair.getPrivate();
264            JcaContentSignerBuilder csBuilder = new JcaContentSignerBuilder(
265                            pk instanceof ECKey ? EC_SIGNATURE_ALG : SIGNATURE_ALG);
266            ContentSigner signer = csBuilder.build(pk);
267
268            csr = p10Builder.build(signer);
269        } catch (OperatorCreationException ex) {
270            throw new IOException("Could not generate CSR", ex);
271        }
272    }
273
274    /**
275     * Gets the PKCS#10 certification request.
276     */
277    public PKCS10CertificationRequest getCSR() {
278        if (csr == null) {
279            throw new IllegalStateException("sign CSR first");
280        }
281
282        return csr;
283    }
284
285    /**
286     * Gets an encoded PKCS#10 certification request.
287     */
288    public byte[] getEncoded() throws IOException {
289        return getCSR().getEncoded();
290    }
291
292    /**
293     * Writes the signed certificate request to a {@link Writer}.
294     *
295     * @param w
296     *            {@link Writer} to write the PEM file to. The {@link Writer} is closed
297     *            after use.
298     */
299    public void write(@WillClose Writer w) throws IOException {
300        if (csr == null) {
301            throw new IllegalStateException("sign CSR first");
302        }
303
304        try (PemWriter pw = new PemWriter(w)) {
305            pw.writeObject(new PemObject("CERTIFICATE REQUEST", getEncoded()));
306        }
307    }
308
309    /**
310     * Writes the signed certificate request to an {@link OutputStream}.
311     *
312     * @param out
313     *            {@link OutputStream} to write the PEM file to. The {@link OutputStream}
314     *            is closed after use.
315     */
316    public void write(@WillClose OutputStream out) throws IOException {
317        write(new OutputStreamWriter(out, "utf-8"));
318    }
319
320    @Override
321    public String toString() {
322        StringBuilder sb = new StringBuilder();
323        sb.append(namebuilder.build());
324        sb.append(namelist.stream().collect(joining(",DNS=", ",DNS=", "")));
325        sb.append(iplist.stream()
326                    .map(InetAddress::getHostAddress)
327                    .collect(joining(",IP=", ",IP=", "")));
328        return sb.toString();
329    }
330
331}