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.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.ContentSigner;
045import org.bouncycastle.operator.OperatorCreationException;
046import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
047import org.bouncycastle.pkcs.PKCS10CertificationRequest;
048import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder;
049import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
050import org.bouncycastle.util.io.pem.PemObject;
051import org.bouncycastle.util.io.pem.PemWriter;
052import org.shredzone.acme4j.Identifier;
053
054/**
055 * Generator for a CSR (Certificate Signing Request) suitable for ACME servers.
056 * <p>
057 * Requires {@code Bouncy Castle}. The {@link org.bouncycastle.jce.provider.BouncyCastleProvider}
058 * must also be added as security provider.
059 */
060public class CSRBuilder {
061    private static final String SIGNATURE_ALG = "SHA256withRSA";
062    private static final String EC_SIGNATURE_ALG = "SHA256withECDSA";
063
064    private final X500NameBuilder namebuilder = new X500NameBuilder(X500Name.getDefaultStyle());
065    private final List<String> namelist = new ArrayList<>();
066    private final List<InetAddress> iplist = new ArrayList<>();
067    private @Nullable PKCS10CertificationRequest csr = null;
068
069    /**
070     * Adds a domain name to the CSR. The first domain name added will also be the
071     * <em>Common Name</em>. All domain names will be added as <em>Subject Alternative
072     * Name</em>.
073     * <p>
074     * IDN domain names are ACE encoded automatically.
075     * <p>
076     * For wildcard certificates, the domain name must be prefixed with {@code "*."}.
077     *
078     * @param domain
079     *            Domain name to add
080     */
081    public void addDomain(String domain) {
082        String ace = toAce(requireNonNull(domain));
083        if (namelist.isEmpty()) {
084            namebuilder.addRDN(BCStyle.CN, ace);
085        }
086        namelist.add(ace);
087    }
088
089    /**
090     * Adds a {@link Collection} of domains.
091     * <p>
092     * IDN domain names are ACE encoded automatically.
093     *
094     * @param domains
095     *            Collection of domain names to add
096     */
097    public void addDomains(Collection<String> domains) {
098        domains.forEach(this::addDomain);
099    }
100
101    /**
102     * Adds multiple domain names.
103     * <p>
104     * IDN domain names are ACE encoded automatically.
105     *
106     * @param domains
107     *            Domain names to add
108     */
109    public void addDomains(String... domains) {
110        Arrays.stream(domains).forEach(this::addDomain);
111    }
112
113    /**
114     * Adds an {@link InetAddress}. All IP addresses will be set as iPAddress <em>Subject
115     * Alternative Name</em>.
116     *
117     * @param address
118     *            {@link InetAddress} to add
119     * @since 2.4
120     */
121    public void addIP(InetAddress address) {
122        iplist.add(requireNonNull(address));
123    }
124
125    /**
126     * Adds a {@link Collection} of IP addresses.
127     *
128     * @param ips
129     *            Collection of IP addresses to add
130     * @since 2.4
131     */
132    public void addIPs(Collection<InetAddress> ips) {
133        ips.forEach(this::addIP);
134    }
135
136    /**
137     * Adds multiple IP addresses.
138     *
139     * @param ips
140     *            IP addresses to add
141     * @since 2.4
142     */
143    public void addIPs(InetAddress... ips) {
144        Arrays.stream(ips).forEach(this::addIP);
145    }
146
147    /**
148     * Adds an {@link Identifier}. Only DNS and IP types are supported.
149     *
150     * @param id
151     *            {@link Identifier} to add
152     * @since 2.7
153     */
154    public void addIdentifier(Identifier id) {
155        requireNonNull(id);
156        if (Identifier.TYPE_DNS.equals(id.getType())) {
157            addDomain(id.getDomain());
158        } else if (Identifier.TYPE_IP.equals(id.getType())) {
159            addIP(id.getIP());
160        } else {
161            throw new IllegalArgumentException("Unknown identifier type: " + id.getType());
162        }
163    }
164
165    /**
166     * Adds a {@link Collection} of {@link Identifier}.
167     *
168     * @param ids
169     *            Collection of Identifiers to add
170     * @since 2.7
171     */
172    public void addIdentifiers(Collection<Identifier> ids) {
173        ids.forEach(this::addIdentifier);
174    }
175
176    /**
177     * Adds multiple {@link Identifier}.
178     *
179     * @param ids
180     *            Identifiers to add
181     * @since 2.7
182     */
183    public void addIdentifiers(Identifier... ids) {
184        Arrays.stream(ids).forEach(this::addIdentifier);
185    }
186
187    /**
188     * Sets the organization.
189     * <p>
190     * Note that it is at the discretion of the ACME server to accept this parameter.
191     */
192    public void setOrganization(String o) {
193        namebuilder.addRDN(BCStyle.O, requireNonNull(o));
194    }
195
196    /**
197     * Sets the organizational unit.
198     * <p>
199     * Note that it is at the discretion of the ACME server to accept this parameter.
200     */
201    public void setOrganizationalUnit(String ou) {
202        namebuilder.addRDN(BCStyle.OU, requireNonNull(ou));
203    }
204
205    /**
206     * Sets the city or locality.
207     * <p>
208     * Note that it is at the discretion of the ACME server to accept this parameter.
209     */
210    public void setLocality(String l) {
211        namebuilder.addRDN(BCStyle.L, requireNonNull(l));
212    }
213
214    /**
215     * Sets the state or province.
216     * <p>
217     * Note that it is at the discretion of the ACME server to accept this parameter.
218     */
219    public void setState(String st) {
220        namebuilder.addRDN(BCStyle.ST, requireNonNull(st));
221    }
222
223    /**
224     * Sets the country.
225     * <p>
226     * Note that it is at the discretion of the ACME server to accept this parameter.
227     */
228    public void setCountry(String c) {
229        namebuilder.addRDN(BCStyle.C, requireNonNull(c));
230    }
231
232    /**
233     * Signs the completed CSR.
234     *
235     * @param keypair
236     *            {@link KeyPair} to sign the CSR with
237     */
238    public void sign(KeyPair keypair) throws IOException {
239        Objects.requireNonNull(keypair, "keypair");
240        if (namelist.isEmpty() && iplist.isEmpty()) {
241            throw new IllegalStateException("No domain or IP address was set");
242        }
243
244        try {
245            int ix = 0;
246            GeneralName[] gns = new GeneralName[namelist.size() + iplist.size()];
247            for (String name : namelist) {
248                gns[ix++] = new GeneralName(GeneralName.dNSName, name);
249            }
250            for (InetAddress ip : iplist) {
251                gns[ix++] = new GeneralName(GeneralName.iPAddress, ip.getHostAddress());
252            }
253            GeneralNames subjectAltName = new GeneralNames(gns);
254
255            PKCS10CertificationRequestBuilder p10Builder =
256                            new JcaPKCS10CertificationRequestBuilder(namebuilder.build(), keypair.getPublic());
257
258            ExtensionsGenerator extensionsGenerator = new ExtensionsGenerator();
259            extensionsGenerator.addExtension(Extension.subjectAlternativeName, false, subjectAltName);
260
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(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(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        if (!namelist.isEmpty()) {
325            sb.append(namelist.stream().collect(joining(",DNS=", ",DNS=", "")));
326        }
327        if (!iplist.isEmpty()) {
328            sb.append(iplist.stream()
329                    .map(InetAddress::getHostAddress)
330                    .collect(joining(",IP=", ",IP=", "")));
331        }
332        return sb.toString();
333    }
334
335}