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.ASN1ObjectIdentifier; 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}. The {@link org.bouncycastle.jce.provider.BouncyCastleProvider} 059 * must also be added as security provider. 060 */ 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 @Nullable 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 an entry of the subject used for the CSR. 190 * <p> 191 * This method is meant as "expert mode" for setting attributes that are not covered 192 * by the other methods. It is at the discretion of the ACME server to accept this 193 * parameter. 194 * 195 * @param attName 196 * The BCStyle attribute name 197 * @param value 198 * The value 199 * @since 2.14 200 */ 201 public void addValue(String attName, String value) { 202 ASN1ObjectIdentifier oid = X500Name.getDefaultStyle().attrNameToOID(requireNonNull(attName, "attribute name must not be null")); 203 addValue(oid, value); 204 } 205 206 /** 207 * Sets an entry of the subject used for the CSR. 208 * <p> 209 * This method is meant as "expert mode" for setting attributes that are not covered 210 * by the other methods. It is at the discretion of the ACME server to accept this 211 * parameter. 212 * 213 * @param oid 214 * The OID of the attribute to be added 215 * @param value 216 * The value 217 * @since 2.14 218 */ 219 public void addValue(ASN1ObjectIdentifier oid, String value) { 220 if (requireNonNull(oid, "OID must not be null").equals(BCStyle.CN)) { 221 addDomain(value); 222 return; 223 } 224 namebuilder.addRDN(oid, requireNonNull(value, "attribute value must not be null")); 225 } 226 227 /** 228 * Sets the organization. 229 * <p> 230 * Note that it is at the discretion of the ACME server to accept this parameter. 231 */ 232 public void setOrganization(String o) { 233 namebuilder.addRDN(BCStyle.O, requireNonNull(o)); 234 } 235 236 /** 237 * Sets the organizational unit. 238 * <p> 239 * Note that it is at the discretion of the ACME server to accept this parameter. 240 */ 241 public void setOrganizationalUnit(String ou) { 242 namebuilder.addRDN(BCStyle.OU, requireNonNull(ou)); 243 } 244 245 /** 246 * Sets the city or locality. 247 * <p> 248 * Note that it is at the discretion of the ACME server to accept this parameter. 249 */ 250 public void setLocality(String l) { 251 namebuilder.addRDN(BCStyle.L, requireNonNull(l)); 252 } 253 254 /** 255 * Sets the state or province. 256 * <p> 257 * Note that it is at the discretion of the ACME server to accept this parameter. 258 */ 259 public void setState(String st) { 260 namebuilder.addRDN(BCStyle.ST, requireNonNull(st)); 261 } 262 263 /** 264 * Sets the country. 265 * <p> 266 * Note that it is at the discretion of the ACME server to accept this parameter. 267 */ 268 public void setCountry(String c) { 269 namebuilder.addRDN(BCStyle.C, requireNonNull(c)); 270 } 271 272 /** 273 * Signs the completed CSR. 274 * 275 * @param keypair 276 * {@link KeyPair} to sign the CSR with 277 */ 278 public void sign(KeyPair keypair) throws IOException { 279 Objects.requireNonNull(keypair, "keypair"); 280 if (namelist.isEmpty() && iplist.isEmpty()) { 281 throw new IllegalStateException("No domain or IP address was set"); 282 } 283 284 try { 285 int ix = 0; 286 GeneralName[] gns = new GeneralName[namelist.size() + iplist.size()]; 287 for (String name : namelist) { 288 gns[ix++] = new GeneralName(GeneralName.dNSName, name); 289 } 290 for (InetAddress ip : iplist) { 291 gns[ix++] = new GeneralName(GeneralName.iPAddress, ip.getHostAddress()); 292 } 293 GeneralNames subjectAltName = new GeneralNames(gns); 294 295 PKCS10CertificationRequestBuilder p10Builder = 296 new JcaPKCS10CertificationRequestBuilder(namebuilder.build(), keypair.getPublic()); 297 298 ExtensionsGenerator extensionsGenerator = new ExtensionsGenerator(); 299 extensionsGenerator.addExtension(Extension.subjectAlternativeName, false, subjectAltName); 300 301 p10Builder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extensionsGenerator.generate()); 302 303 PrivateKey pk = keypair.getPrivate(); 304 JcaContentSignerBuilder csBuilder = new JcaContentSignerBuilder( 305 pk instanceof ECKey ? EC_SIGNATURE_ALG : SIGNATURE_ALG); 306 ContentSigner signer = csBuilder.build(pk); 307 308 csr = p10Builder.build(signer); 309 } catch (OperatorCreationException ex) { 310 throw new IOException("Could not generate CSR", ex); 311 } 312 } 313 314 /** 315 * Gets the PKCS#10 certification request. 316 */ 317 public PKCS10CertificationRequest getCSR() { 318 if (csr == null) { 319 throw new IllegalStateException("sign CSR first"); 320 } 321 322 return csr; 323 } 324 325 /** 326 * Gets an encoded PKCS#10 certification request. 327 */ 328 public byte[] getEncoded() throws IOException { 329 return getCSR().getEncoded(); 330 } 331 332 /** 333 * Writes the signed certificate request to a {@link Writer}. 334 * 335 * @param w 336 * {@link Writer} to write the PEM file to. The {@link Writer} is closed 337 * after use. 338 */ 339 public void write(Writer w) throws IOException { 340 if (csr == null) { 341 throw new IllegalStateException("sign CSR first"); 342 } 343 344 try (PemWriter pw = new PemWriter(w)) { 345 pw.writeObject(new PemObject("CERTIFICATE REQUEST", getEncoded())); 346 } 347 } 348 349 /** 350 * Writes the signed certificate request to an {@link OutputStream}. 351 * 352 * @param out 353 * {@link OutputStream} to write the PEM file to. The {@link OutputStream} 354 * is closed after use. 355 */ 356 public void write(OutputStream out) throws IOException { 357 write(new OutputStreamWriter(out, UTF_8)); 358 } 359 360 @Override 361 public String toString() { 362 StringBuilder sb = new StringBuilder(); 363 sb.append(namebuilder.build()); 364 if (!namelist.isEmpty()) { 365 sb.append(namelist.stream().collect(joining(",DNS=", ",DNS=", ""))); 366 } 367 if (!iplist.isEmpty()) { 368 sb.append(iplist.stream() 369 .map(InetAddress::getHostAddress) 370 .collect(joining(",IP=", ",IP=", ""))); 371 } 372 return sb.toString(); 373 } 374 375}