org.shredzone.acme4j.util.CSRBuilder Maven / Gradle / Ivy
/*
* acme4j - Java ACME client
*
* Copyright (C) 2015 Richard "Shred" Körber
* http://acme4j.shredzone.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*/
package org.shredzone.acme4j.util;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.joining;
import static org.shredzone.acme4j.toolbox.AcmeUtils.toAce;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.InetAddress;
import java.security.KeyPair;
import java.security.interfaces.ECKey;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import edu.umd.cs.findbugs.annotations.Nullable;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.ExtensionsGenerator;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemWriter;
import org.shredzone.acme4j.Identifier;
/**
* Generator for a CSR (Certificate Signing Request) suitable for ACME servers.
*
* Requires {@code Bouncy Castle}. The
* {@link org.bouncycastle.jce.provider.BouncyCastleProvider} must be added as security
* provider.
*/
public class CSRBuilder {
private static final String SIGNATURE_ALG = "SHA256withRSA";
private static final String EC_SIGNATURE_ALG = "SHA256withECDSA";
private final X500NameBuilder namebuilder = new X500NameBuilder(X500Name.getDefaultStyle());
private final List namelist = new ArrayList<>();
private final List iplist = new ArrayList<>();
private @Nullable PKCS10CertificationRequest csr = null;
private boolean hasCnSet = false;
/**
* Adds a domain name to the CSR. All domain names will be added as Subject
* Alternative Name.
*
* IDN domain names are ACE encoded automatically.
*
* For wildcard certificates, the domain name must be prefixed with {@code "*."}.
*
* @param domain
* Domain name to add
*/
public void addDomain(String domain) {
namelist.add(toAce(requireNonNull(domain)));
}
/**
* Adds a {@link Collection} of domains.
*
* IDN domain names are ACE encoded automatically.
*
* @param domains
* Collection of domain names to add
*/
public void addDomains(Collection domains) {
domains.forEach(this::addDomain);
}
/**
* Adds multiple domain names.
*
* IDN domain names are ACE encoded automatically.
*
* @param domains
* Domain names to add
*/
public void addDomains(String... domains) {
Arrays.stream(domains).forEach(this::addDomain);
}
/**
* Adds an {@link InetAddress}. All IP addresses will be set as iPAddress Subject
* Alternative Name.
*
* @param address
* {@link InetAddress} to add
* @since 2.4
*/
public void addIP(InetAddress address) {
iplist.add(requireNonNull(address));
}
/**
* Adds a {@link Collection} of IP addresses.
*
* @param ips
* Collection of IP addresses to add
* @since 2.4
*/
public void addIPs(Collection ips) {
ips.forEach(this::addIP);
}
/**
* Adds multiple IP addresses.
*
* @param ips
* IP addresses to add
* @since 2.4
*/
public void addIPs(InetAddress... ips) {
Arrays.stream(ips).forEach(this::addIP);
}
/**
* Adds an {@link Identifier}. Only DNS and IP types are supported.
*
* @param id
* {@link Identifier} to add
* @since 2.7
*/
public void addIdentifier(Identifier id) {
requireNonNull(id);
if (Identifier.TYPE_DNS.equals(id.getType())) {
addDomain(id.getDomain());
} else if (Identifier.TYPE_IP.equals(id.getType())) {
addIP(id.getIP());
} else {
throw new IllegalArgumentException("Unknown identifier type: " + id.getType());
}
}
/**
* Adds a {@link Collection} of {@link Identifier}.
*
* @param ids
* Collection of Identifiers to add
* @since 2.7
*/
public void addIdentifiers(Collection ids) {
ids.forEach(this::addIdentifier);
}
/**
* Adds multiple {@link Identifier}.
*
* @param ids
* Identifiers to add
* @since 2.7
*/
public void addIdentifiers(Identifier... ids) {
Arrays.stream(ids).forEach(this::addIdentifier);
}
/**
* Sets an entry of the subject used for the CSR.
*
* This method is meant as "expert mode" for setting attributes that are not covered
* by the other methods. It is at the discretion of the ACME server to accept this
* parameter.
*
* @param attName
* The BCStyle attribute name
* @param value
* The value
* @since 2.14
*/
public void addValue(String attName, String value) {
var oid = X500Name.getDefaultStyle().attrNameToOID(requireNonNull(attName, "attribute name must not be null"));
addValue(oid, value);
}
/**
* Sets an entry of the subject used for the CSR.
*
* This method is meant as "expert mode" for setting attributes that are not covered
* by the other methods. It is at the discretion of the ACME server to accept this
* parameter.
*
* @param oid
* The OID of the attribute to be added
* @param value
* The value
* @since 2.14
*/
public void addValue(ASN1ObjectIdentifier oid, String value) {
if (requireNonNull(oid, "OID must not be null").equals(BCStyle.CN)) {
addDomain(value);
if (hasCnSet) {
return;
}
hasCnSet = true;
}
namebuilder.addRDN(oid, requireNonNull(value, "attribute value must not be null"));
}
/**
* Sets the common name.
*
* Note that it is at the discretion of the ACME server to accept this parameter.
*
* @since 3.2.0
*/
public void setCommonName(String cn) {
namebuilder.addRDN(BCStyle.CN, requireNonNull(cn));
}
/**
* Sets the organization.
*
* Note that it is at the discretion of the ACME server to accept this parameter.
*/
public void setOrganization(String o) {
namebuilder.addRDN(BCStyle.O, requireNonNull(o));
}
/**
* Sets the organizational unit.
*
* Note that it is at the discretion of the ACME server to accept this parameter.
*/
public void setOrganizationalUnit(String ou) {
namebuilder.addRDN(BCStyle.OU, requireNonNull(ou));
}
/**
* Sets the city or locality.
*
* Note that it is at the discretion of the ACME server to accept this parameter.
*/
public void setLocality(String l) {
namebuilder.addRDN(BCStyle.L, requireNonNull(l));
}
/**
* Sets the state or province.
*
* Note that it is at the discretion of the ACME server to accept this parameter.
*/
public void setState(String st) {
namebuilder.addRDN(BCStyle.ST, requireNonNull(st));
}
/**
* Sets the country.
*
* Note that it is at the discretion of the ACME server to accept this parameter.
*/
public void setCountry(String c) {
namebuilder.addRDN(BCStyle.C, requireNonNull(c));
}
/**
* Signs the completed CSR.
*
* @param keypair
* {@link KeyPair} to sign the CSR with
*/
public void sign(KeyPair keypair) throws IOException {
Objects.requireNonNull(keypair, "keypair");
if (namelist.isEmpty() && iplist.isEmpty()) {
throw new IllegalStateException("No domain or IP address was set");
}
try {
var ix = 0;
var gns = new GeneralName[namelist.size() + iplist.size()];
for (var name : namelist) {
gns[ix++] = new GeneralName(GeneralName.dNSName, name);
}
for (var ip : iplist) {
gns[ix++] = new GeneralName(GeneralName.iPAddress, ip.getHostAddress());
}
var subjectAltName = new GeneralNames(gns);
var p10Builder = new JcaPKCS10CertificationRequestBuilder(namebuilder.build(), keypair.getPublic());
var extensionsGenerator = new ExtensionsGenerator();
extensionsGenerator.addExtension(Extension.subjectAlternativeName, false, subjectAltName);
p10Builder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extensionsGenerator.generate());
var pk = keypair.getPrivate();
var csBuilder = new JcaContentSignerBuilder(pk instanceof ECKey ? EC_SIGNATURE_ALG : SIGNATURE_ALG);
var signer = csBuilder.build(pk);
csr = p10Builder.build(signer);
} catch (OperatorCreationException ex) {
throw new IOException("Could not generate CSR", ex);
}
}
/**
* Gets the PKCS#10 certification request.
*/
public PKCS10CertificationRequest getCSR() {
if (csr == null) {
throw new IllegalStateException("sign CSR first");
}
return csr;
}
/**
* Gets an encoded PKCS#10 certification request.
*/
public byte[] getEncoded() throws IOException {
return getCSR().getEncoded();
}
/**
* Writes the signed certificate request to a {@link Writer}.
*
* @param w
* {@link Writer} to write the PEM file to. The {@link Writer} is closed
* after use.
*/
public void write(Writer w) throws IOException {
if (csr == null) {
throw new IllegalStateException("sign CSR first");
}
try (var pw = new PemWriter(w)) {
pw.writeObject(new PemObject("CERTIFICATE REQUEST", getEncoded()));
}
}
/**
* Writes the signed certificate request to an {@link OutputStream}.
*
* @param out
* {@link OutputStream} to write the PEM file to. The {@link OutputStream}
* is closed after use.
*/
public void write(OutputStream out) throws IOException {
write(new OutputStreamWriter(out, UTF_8));
}
@Override
public String toString() {
var sb = new StringBuilder();
sb.append(namebuilder.build());
if (!namelist.isEmpty()) {
sb.append(namelist.stream().collect(joining(",DNS=", ",DNS=", "")));
}
if (!iplist.isEmpty()) {
sb.append(iplist.stream()
.map(InetAddress::getHostAddress)
.collect(joining(",IP=", ",IP=", "")));
}
return sb.toString();
}
}