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.stream.Collectors.joining;
017import static org.shredzone.acme4j.toolbox.AcmeUtils.toAce;
018
019import java.io.IOException;
020import java.io.OutputStream;
021import java.io.OutputStreamWriter;
022import java.io.Writer;
023import java.security.KeyPair;
024import java.security.PrivateKey;
025import java.security.interfaces.ECKey;
026import java.util.ArrayList;
027import java.util.Arrays;
028import java.util.Collection;
029import java.util.List;
030import java.util.Objects;
031
032import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
033import org.bouncycastle.asn1.x500.X500Name;
034import org.bouncycastle.asn1.x500.X500NameBuilder;
035import org.bouncycastle.asn1.x500.style.BCStyle;
036import org.bouncycastle.asn1.x509.Extension;
037import org.bouncycastle.asn1.x509.ExtensionsGenerator;
038import org.bouncycastle.asn1.x509.GeneralName;
039import org.bouncycastle.asn1.x509.GeneralNames;
040import org.bouncycastle.operator.ContentSigner;
041import org.bouncycastle.operator.OperatorCreationException;
042import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
043import org.bouncycastle.pkcs.PKCS10CertificationRequest;
044import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder;
045import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
046import org.bouncycastle.util.io.pem.PemObject;
047import org.bouncycastle.util.io.pem.PemWriter;
048
049/**
050 * Generator for a CSR (Certificate Signing Request) suitable for ACME servers.
051 * <p>
052 * Requires {@code Bouncy Castle}. This class is part of the {@code acme4j-utils} module.
053 */
054public class CSRBuilder {
055    private static final String SIGNATURE_ALG = "SHA256withRSA";
056    private static final String EC_SIGNATURE_ALG = "SHA256withECDSA";
057
058    private final X500NameBuilder namebuilder = new X500NameBuilder(X500Name.getDefaultStyle());
059    private final List<String> namelist = new ArrayList<>();
060    private PKCS10CertificationRequest csr = null;
061
062    /**
063     * Adds a domain name to the CSR. The first domain name added will also be the
064     * <em>Common Name</em>. All domain names will be added as <em>Subject Alternative
065     * Name</em>.
066     * <p>
067     * IDN domain names are ACE encoded automatically.
068     * <p>
069     * Note that ACME servers may not accept wildcard domains!
070     *
071     * @param domain
072     *            Domain name to add
073     */
074    public void addDomain(String domain) {
075        String ace = toAce(domain);
076        if (namelist.isEmpty()) {
077            namebuilder.addRDN(BCStyle.CN, ace);
078        }
079        namelist.add(ace);
080    }
081
082    /**
083     * Adds a {@link Collection} of domains.
084     * <p>
085     * IDN domain names are ACE encoded automatically.
086     *
087     * @param domains
088     *            Collection of domain names to add
089     */
090    public void addDomains(Collection<String> domains) {
091        domains.forEach(this::addDomain);
092    }
093
094    /**
095     * Adds multiple domain names.
096     * <p>
097     * IDN domain names are ACE encoded automatically.
098     *
099     * @param domains
100     *            Domain names to add
101     */
102    public void addDomains(String... domains) {
103        Arrays.stream(domains).forEach(this::addDomain);
104    }
105
106    /**
107     * Sets the organization.
108     * <p>
109     * Note that it is at the discretion of the ACME server to accept this parameter.
110     */
111    public void setOrganization(String o) {
112        namebuilder.addRDN(BCStyle.O, o);
113    }
114
115    /**
116     * Sets the organizational unit.
117     * <p>
118     * Note that it is at the discretion of the ACME server to accept this parameter.
119     */
120    public void setOrganizationalUnit(String ou) {
121        namebuilder.addRDN(BCStyle.OU, ou);
122    }
123
124    /**
125     * Sets the city or locality.
126     * <p>
127     * Note that it is at the discretion of the ACME server to accept this parameter.
128     */
129    public void setLocality(String l) {
130        namebuilder.addRDN(BCStyle.L, l);
131    }
132
133    /**
134     * Sets the state or province.
135     * <p>
136     * Note that it is at the discretion of the ACME server to accept this parameter.
137     */
138    public void setState(String st) {
139        namebuilder.addRDN(BCStyle.ST, st);
140    }
141
142    /**
143     * Sets the country.
144     * <p>
145     * Note that it is at the discretion of the ACME server to accept this parameter.
146     */
147    public void setCountry(String c) {
148        namebuilder.addRDN(BCStyle.C, c);
149    }
150
151    /**
152     * Signs the completed CSR.
153     *
154     * @param keypair
155     *            {@link KeyPair} to sign the CSR with
156     */
157    public void sign(KeyPair keypair) throws IOException {
158        Objects.requireNonNull(keypair, "keypair");
159        if (namelist.isEmpty()) {
160            throw new IllegalStateException("No domain was set");
161        }
162
163        try {
164            GeneralName[] gns = new GeneralName[namelist.size()];
165            for (int ix = 0; ix < namelist.size(); ix++) {
166                gns[ix] = new GeneralName(GeneralName.dNSName, namelist.get(ix));
167            }
168            GeneralNames subjectAltName = new GeneralNames(gns);
169
170            PKCS10CertificationRequestBuilder p10Builder =
171                            new JcaPKCS10CertificationRequestBuilder(namebuilder.build(), keypair.getPublic());
172
173            ExtensionsGenerator extensionsGenerator = new ExtensionsGenerator();
174            extensionsGenerator.addExtension(Extension.subjectAlternativeName, false, subjectAltName);
175            p10Builder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extensionsGenerator.generate());
176
177            PrivateKey pk = keypair.getPrivate();
178            JcaContentSignerBuilder csBuilder = new JcaContentSignerBuilder(
179                            pk instanceof ECKey ? EC_SIGNATURE_ALG : SIGNATURE_ALG);
180            ContentSigner signer = csBuilder.build(pk);
181
182            csr = p10Builder.build(signer);
183        } catch (OperatorCreationException ex) {
184            throw new IOException("Could not generate CSR", ex);
185        }
186    }
187
188    /**
189     * Gets the PKCS#10 certification request.
190     */
191    public PKCS10CertificationRequest getCSR() {
192        if (csr == null) {
193            throw new IllegalStateException("sign CSR first");
194        }
195
196        return csr;
197    }
198
199    /**
200     * Gets an encoded PKCS#10 certification request.
201     */
202    public byte[] getEncoded() throws IOException {
203        return getCSR().getEncoded();
204    }
205
206    /**
207     * Writes the signed certificate request to a {@link Writer}.
208     *
209     * @param w
210     *            {@link Writer} to write the PEM file to. The {@link Writer} is closed
211     *            after use.
212     */
213    public void write(Writer w) throws IOException {
214        if (csr == null) {
215            throw new IllegalStateException("sign CSR first");
216        }
217
218        try (PemWriter pw = new PemWriter(w)) {
219            pw.writeObject(new PemObject("CERTIFICATE REQUEST", getEncoded()));
220        }
221    }
222
223    /**
224     * Writes the signed certificate request to an {@link OutputStream}.
225     *
226     * @param out
227     *            {@link OutputStream} to write the PEM file to. The {@link OutputStream}
228     *            is closed after use.
229     */
230    public void write(OutputStream out) throws IOException {
231        write(new OutputStreamWriter(out, "utf-8"));
232    }
233
234    @Override
235    public String toString() {
236        StringBuilder sb = new StringBuilder();
237        sb.append(namebuilder.build());
238        sb.append(namelist.stream().collect(joining(",DNS=", ",DNS=", "")));
239        return sb.toString();
240    }
241
242}