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.interfaces.ECKey; 028import java.util.ArrayList; 029import java.util.Arrays; 030import java.util.Collection; 031import java.util.List; 032import java.util.Objects; 033 034import edu.umd.cs.findbugs.annotations.Nullable; 035import org.bouncycastle.asn1.ASN1ObjectIdentifier; 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.OperatorCreationException; 045import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; 046import org.bouncycastle.pkcs.PKCS10CertificationRequest; 047import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder; 048import org.bouncycastle.util.io.pem.PemObject; 049import org.bouncycastle.util.io.pem.PemWriter; 050import org.shredzone.acme4j.Identifier; 051 052/** 053 * Generator for a CSR (Certificate Signing Request) suitable for ACME servers. 054 * <p> 055 * Requires {@code Bouncy Castle}. The 056 * {@link org.bouncycastle.jce.provider.BouncyCastleProvider} must be added as security 057 * provider. 058 */ 059public class CSRBuilder { 060 private static final String SIGNATURE_ALG = "SHA256withRSA"; 061 private static final String EC_SIGNATURE_ALG = "SHA256withECDSA"; 062 063 private final X500NameBuilder namebuilder = new X500NameBuilder(X500Name.getDefaultStyle()); 064 private final List<String> namelist = new ArrayList<>(); 065 private final List<InetAddress> iplist = new ArrayList<>(); 066 private @Nullable PKCS10CertificationRequest csr = null; 067 private boolean hasCnSet = false; 068 069 /** 070 * Adds a domain name to the CSR. All domain names will be added as <em>Subject 071 * Alternative Name</em>. 072 * <p> 073 * IDN domain names are ACE encoded automatically. 074 * <p> 075 * For wildcard certificates, the domain name must be prefixed with {@code "*."}. 076 * 077 * @param domain 078 * Domain name to add 079 */ 080 public void addDomain(String domain) { 081 namelist.add(toAce(requireNonNull(domain))); 082 } 083 084 /** 085 * Adds a {@link Collection} of domains. 086 * <p> 087 * IDN domain names are ACE encoded automatically. 088 * 089 * @param domains 090 * Collection of domain names to add 091 */ 092 public void addDomains(Collection<String> domains) { 093 domains.forEach(this::addDomain); 094 } 095 096 /** 097 * Adds multiple domain names. 098 * <p> 099 * IDN domain names are ACE encoded automatically. 100 * 101 * @param domains 102 * Domain names to add 103 */ 104 public void addDomains(String... domains) { 105 Arrays.stream(domains).forEach(this::addDomain); 106 } 107 108 /** 109 * Adds an {@link InetAddress}. All IP addresses will be set as iPAddress <em>Subject 110 * Alternative Name</em>. 111 * 112 * @param address 113 * {@link InetAddress} to add 114 * @since 2.4 115 */ 116 public void addIP(InetAddress address) { 117 iplist.add(requireNonNull(address)); 118 } 119 120 /** 121 * Adds a {@link Collection} of IP addresses. 122 * 123 * @param ips 124 * Collection of IP addresses to add 125 * @since 2.4 126 */ 127 public void addIPs(Collection<InetAddress> ips) { 128 ips.forEach(this::addIP); 129 } 130 131 /** 132 * Adds multiple IP addresses. 133 * 134 * @param ips 135 * IP addresses to add 136 * @since 2.4 137 */ 138 public void addIPs(InetAddress... ips) { 139 Arrays.stream(ips).forEach(this::addIP); 140 } 141 142 /** 143 * Adds an {@link Identifier}. Only DNS and IP types are supported. 144 * 145 * @param id 146 * {@link Identifier} to add 147 * @since 2.7 148 */ 149 public void addIdentifier(Identifier id) { 150 requireNonNull(id); 151 if (Identifier.TYPE_DNS.equals(id.getType())) { 152 addDomain(id.getDomain()); 153 } else if (Identifier.TYPE_IP.equals(id.getType())) { 154 addIP(id.getIP()); 155 } else { 156 throw new IllegalArgumentException("Unknown identifier type: " + id.getType()); 157 } 158 } 159 160 /** 161 * Adds a {@link Collection} of {@link Identifier}. 162 * 163 * @param ids 164 * Collection of Identifiers to add 165 * @since 2.7 166 */ 167 public void addIdentifiers(Collection<Identifier> ids) { 168 ids.forEach(this::addIdentifier); 169 } 170 171 /** 172 * Adds multiple {@link Identifier}. 173 * 174 * @param ids 175 * Identifiers to add 176 * @since 2.7 177 */ 178 public void addIdentifiers(Identifier... ids) { 179 Arrays.stream(ids).forEach(this::addIdentifier); 180 } 181 182 /** 183 * Sets an entry of the subject used for the CSR. 184 * <p> 185 * This method is meant as "expert mode" for setting attributes that are not covered 186 * by the other methods. It is at the discretion of the ACME server to accept this 187 * parameter. 188 * 189 * @param attName 190 * The BCStyle attribute name 191 * @param value 192 * The value 193 * @since 2.14 194 */ 195 public void addValue(String attName, String value) { 196 var oid = X500Name.getDefaultStyle().attrNameToOID(requireNonNull(attName, "attribute name must not be null")); 197 addValue(oid, value); 198 } 199 200 /** 201 * Sets an entry of the subject used for the CSR. 202 * <p> 203 * This method is meant as "expert mode" for setting attributes that are not covered 204 * by the other methods. It is at the discretion of the ACME server to accept this 205 * parameter. 206 * 207 * @param oid 208 * The OID of the attribute to be added 209 * @param value 210 * The value 211 * @since 2.14 212 */ 213 public void addValue(ASN1ObjectIdentifier oid, String value) { 214 if (requireNonNull(oid, "OID must not be null").equals(BCStyle.CN)) { 215 addDomain(value); 216 if (hasCnSet) { 217 return; 218 } 219 hasCnSet = true; 220 } 221 namebuilder.addRDN(oid, requireNonNull(value, "attribute value must not be null")); 222 } 223 224 /** 225 * Sets the common name. 226 * <p> 227 * Note that it is at the discretion of the ACME server to accept this parameter. 228 * 229 * @since 3.2.0 230 */ 231 public void setCommonName(String cn) { 232 namebuilder.addRDN(BCStyle.CN, requireNonNull(cn)); 233 } 234 235 /** 236 * Sets the organization. 237 * <p> 238 * Note that it is at the discretion of the ACME server to accept this parameter. 239 */ 240 public void setOrganization(String o) { 241 namebuilder.addRDN(BCStyle.O, requireNonNull(o)); 242 } 243 244 /** 245 * Sets the organizational unit. 246 * <p> 247 * Note that it is at the discretion of the ACME server to accept this parameter. 248 */ 249 public void setOrganizationalUnit(String ou) { 250 namebuilder.addRDN(BCStyle.OU, requireNonNull(ou)); 251 } 252 253 /** 254 * Sets the city or locality. 255 * <p> 256 * Note that it is at the discretion of the ACME server to accept this parameter. 257 */ 258 public void setLocality(String l) { 259 namebuilder.addRDN(BCStyle.L, requireNonNull(l)); 260 } 261 262 /** 263 * Sets the state or province. 264 * <p> 265 * Note that it is at the discretion of the ACME server to accept this parameter. 266 */ 267 public void setState(String st) { 268 namebuilder.addRDN(BCStyle.ST, requireNonNull(st)); 269 } 270 271 /** 272 * Sets the country. 273 * <p> 274 * Note that it is at the discretion of the ACME server to accept this parameter. 275 */ 276 public void setCountry(String c) { 277 namebuilder.addRDN(BCStyle.C, requireNonNull(c)); 278 } 279 280 /** 281 * Signs the completed CSR. 282 * 283 * @param keypair 284 * {@link KeyPair} to sign the CSR with 285 */ 286 public void sign(KeyPair keypair) throws IOException { 287 Objects.requireNonNull(keypair, "keypair"); 288 if (namelist.isEmpty() && iplist.isEmpty()) { 289 throw new IllegalStateException("No domain or IP address was set"); 290 } 291 292 try { 293 var ix = 0; 294 var gns = new GeneralName[namelist.size() + iplist.size()]; 295 for (var name : namelist) { 296 gns[ix++] = new GeneralName(GeneralName.dNSName, name); 297 } 298 for (var ip : iplist) { 299 gns[ix++] = new GeneralName(GeneralName.iPAddress, ip.getHostAddress()); 300 } 301 var subjectAltName = new GeneralNames(gns); 302 303 var p10Builder = new JcaPKCS10CertificationRequestBuilder(namebuilder.build(), keypair.getPublic()); 304 305 var extensionsGenerator = new ExtensionsGenerator(); 306 extensionsGenerator.addExtension(Extension.subjectAlternativeName, false, subjectAltName); 307 308 p10Builder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extensionsGenerator.generate()); 309 310 var pk = keypair.getPrivate(); 311 var csBuilder = new JcaContentSignerBuilder(pk instanceof ECKey ? EC_SIGNATURE_ALG : SIGNATURE_ALG); 312 var signer = csBuilder.build(pk); 313 314 csr = p10Builder.build(signer); 315 } catch (OperatorCreationException ex) { 316 throw new IOException("Could not generate CSR", ex); 317 } 318 } 319 320 /** 321 * Gets the PKCS#10 certification request. 322 */ 323 public PKCS10CertificationRequest getCSR() { 324 if (csr == null) { 325 throw new IllegalStateException("sign CSR first"); 326 } 327 328 return csr; 329 } 330 331 /** 332 * Gets an encoded PKCS#10 certification request. 333 */ 334 public byte[] getEncoded() throws IOException { 335 return getCSR().getEncoded(); 336 } 337 338 /** 339 * Writes the signed certificate request to a {@link Writer}. 340 * 341 * @param w 342 * {@link Writer} to write the PEM file to. The {@link Writer} is closed 343 * after use. 344 */ 345 public void write(Writer w) throws IOException { 346 if (csr == null) { 347 throw new IllegalStateException("sign CSR first"); 348 } 349 350 try (var pw = new PemWriter(w)) { 351 pw.writeObject(new PemObject("CERTIFICATE REQUEST", getEncoded())); 352 } 353 } 354 355 /** 356 * Writes the signed certificate request to an {@link OutputStream}. 357 * 358 * @param out 359 * {@link OutputStream} to write the PEM file to. The {@link OutputStream} 360 * is closed after use. 361 */ 362 public void write(OutputStream out) throws IOException { 363 write(new OutputStreamWriter(out, UTF_8)); 364 } 365 366 @Override 367 public String toString() { 368 var sb = new StringBuilder(); 369 sb.append(namebuilder.build()); 370 if (!namelist.isEmpty()) { 371 sb.append(namelist.stream().collect(joining(",DNS=", ",DNS=", ""))); 372 } 373 if (!iplist.isEmpty()) { 374 sb.append(iplist.stream() 375 .map(InetAddress::getHostAddress) 376 .collect(joining(",IP=", ",IP=", ""))); 377 } 378 return sb.toString(); 379 } 380 381}