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}