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}