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 java.io.ByteArrayInputStream; 017import java.io.IOException; 018import java.io.InputStream; 019import java.io.InputStreamReader; 020import java.math.BigInteger; 021import java.nio.charset.StandardCharsets; 022import java.security.InvalidKeyException; 023import java.security.KeyPair; 024import java.security.NoSuchAlgorithmException; 025import java.security.PrivateKey; 026import java.security.PublicKey; 027import java.security.cert.CertificateException; 028import java.security.cert.CertificateFactory; 029import java.security.cert.X509Certificate; 030import java.time.Duration; 031import java.time.Instant; 032import java.util.Date; 033import java.util.Objects; 034import java.util.function.Function; 035 036import org.bouncycastle.asn1.ASN1ObjectIdentifier; 037import org.bouncycastle.asn1.DEROctetString; 038import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; 039import org.bouncycastle.asn1.x500.X500Name; 040import org.bouncycastle.asn1.x509.Extension; 041import org.bouncycastle.asn1.x509.Extensions; 042import org.bouncycastle.asn1.x509.GeneralName; 043import org.bouncycastle.asn1.x509.GeneralNames; 044import org.bouncycastle.cert.CertIOException; 045import org.bouncycastle.cert.X509CertificateHolder; 046import org.bouncycastle.cert.jcajce.JcaX509v1CertificateBuilder; 047import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; 048import org.bouncycastle.openssl.PEMParser; 049import org.bouncycastle.operator.ContentSigner; 050import org.bouncycastle.operator.OperatorCreationException; 051import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; 052import org.bouncycastle.pkcs.PKCS10CertificationRequest; 053import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest; 054import org.shredzone.acme4j.Identifier; 055import org.shredzone.acme4j.challenge.TlsAlpn01Challenge; 056 057/** 058 * Utility class offering convenience methods for certificates. 059 * <p> 060 * Requires {@code Bouncy Castle}. 061 */ 062public final class CertificateUtils { 063 064 /** 065 * The {@code acmeValidation} object identifier. 066 * 067 * @since 2.1 068 */ 069 public static final ASN1ObjectIdentifier ACME_VALIDATION = 070 new ASN1ObjectIdentifier(TlsAlpn01Challenge.ACME_VALIDATION_OID).intern(); 071 072 private CertificateUtils() { 073 // utility class without constructor 074 } 075 076 /** 077 * Reads a CSR PEM file. 078 * 079 * @param in 080 * {@link InputStream} to read the CSR from. The {@link InputStream} is 081 * closed after use. 082 * @return CSR that was read 083 */ 084 public static PKCS10CertificationRequest readCSR(InputStream in) throws IOException { 085 try (var pemParser = new PEMParser(new InputStreamReader(in, StandardCharsets.US_ASCII))) { 086 var parsedObj = pemParser.readObject(); 087 if (!(parsedObj instanceof PKCS10CertificationRequest)) { 088 throw new IOException("Not a PKCS10 CSR"); 089 } 090 return (PKCS10CertificationRequest) parsedObj; 091 } 092 } 093 094 /** 095 * Creates a self-signed {@link X509Certificate} that can be used for the 096 * {@link TlsAlpn01Challenge}. The certificate is valid for 7 days. 097 * 098 * @param keypair 099 * A domain {@link KeyPair} to be used for the challenge 100 * @param id 101 * The {@link Identifier} that is to be validated 102 * @param acmeValidation 103 * The value that is returned by 104 * {@link TlsAlpn01Challenge#getAcmeValidation()} 105 * @return Created certificate 106 * @since 2.6 107 */ 108 public static X509Certificate createTlsAlpn01Certificate(KeyPair keypair, Identifier id, byte[] acmeValidation) 109 throws IOException { 110 Objects.requireNonNull(keypair, "keypair"); 111 Objects.requireNonNull(id, "id"); 112 if (acmeValidation == null || acmeValidation.length != 32) { 113 throw new IllegalArgumentException("Bad acmeValidation parameter"); 114 } 115 116 var now = System.currentTimeMillis(); 117 118 var issuer = new X500Name("CN=acme.invalid"); 119 var serial = BigInteger.valueOf(now); 120 var notBefore = Instant.ofEpochMilli(now); 121 var notAfter = notBefore.plus(Duration.ofDays(7)); 122 123 var certBuilder = new JcaX509v3CertificateBuilder( 124 issuer, serial, Date.from(notBefore), Date.from(notAfter), 125 issuer, keypair.getPublic()); 126 127 var gns = new GeneralName[1]; 128 129 switch (id.getType()) { 130 case Identifier.TYPE_DNS: 131 gns[0] = new GeneralName(GeneralName.dNSName, id.getDomain()); 132 break; 133 134 case Identifier.TYPE_IP: 135 gns[0] = new GeneralName(GeneralName.iPAddress, id.getIP().getHostAddress()); 136 break; 137 138 default: 139 throw new IllegalArgumentException("Unsupported Identifier type " + id.getType()); 140 } 141 certBuilder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(gns)); 142 certBuilder.addExtension(ACME_VALIDATION, true, new DEROctetString(acmeValidation)); 143 144 return buildCertificate(certBuilder::build, keypair.getPrivate()); 145 } 146 147 /** 148 * Creates a self-signed root certificate. 149 * <p> 150 * The generated certificate is only meant for testing purposes! 151 * 152 * @param subject 153 * This certificate's subject X.500 name. 154 * @param notBefore 155 * {@link Instant} before which the certificate is not valid. 156 * @param notAfter 157 * {@link Instant} after which the certificate is not valid. 158 * @param keypair 159 * {@link KeyPair} that is to be used for this certificate. 160 * @return Generated {@link X509Certificate} 161 * @since 2.8 162 */ 163 public static X509Certificate createTestRootCertificate(String subject, 164 Instant notBefore, Instant notAfter, KeyPair keypair) { 165 Objects.requireNonNull(subject, "subject"); 166 Objects.requireNonNull(notBefore, "notBefore"); 167 Objects.requireNonNull(notAfter, "notAfter"); 168 Objects.requireNonNull(keypair, "keypair"); 169 170 var certBuilder = new JcaX509v1CertificateBuilder( 171 new X500Name(subject), 172 BigInteger.valueOf(System.currentTimeMillis()), 173 Date.from(notBefore), 174 Date.from(notAfter), 175 new X500Name(subject), 176 keypair.getPublic() 177 ); 178 179 return buildCertificate(certBuilder::build, keypair.getPrivate()); 180 } 181 182 /** 183 * Creates an intermediate certificate that is signed by an issuer. 184 * <p> 185 * The generated certificate is only meant for testing purposes! 186 * 187 * @param subject 188 * This certificate's subject X.500 name. 189 * @param notBefore 190 * {@link Instant} before which the certificate is not valid. 191 * @param notAfter 192 * {@link Instant} after which the certificate is not valid. 193 * @param intermediatePublicKey 194 * {@link PublicKey} of this certificate 195 * @param issuer 196 * The issuer's {@link X509Certificate}. 197 * @param issuerPrivateKey 198 * {@link PrivateKey} of the issuer. This is not the private key of this 199 * intermediate certificate. 200 * @return Generated {@link X509Certificate} 201 * @since 2.8 202 */ 203 public static X509Certificate createTestIntermediateCertificate(String subject, 204 Instant notBefore, Instant notAfter, PublicKey intermediatePublicKey, 205 X509Certificate issuer, PrivateKey issuerPrivateKey) { 206 Objects.requireNonNull(subject, "subject"); 207 Objects.requireNonNull(notBefore, "notBefore"); 208 Objects.requireNonNull(notAfter, "notAfter"); 209 Objects.requireNonNull(intermediatePublicKey, "intermediatePublicKey"); 210 Objects.requireNonNull(issuer, "issuer"); 211 Objects.requireNonNull(issuerPrivateKey, "issuerPrivateKey"); 212 213 var certBuilder = new JcaX509v1CertificateBuilder( 214 new X500Name(issuer.getIssuerX500Principal().getName()), 215 BigInteger.valueOf(System.currentTimeMillis()), 216 Date.from(notBefore), 217 Date.from(notAfter), 218 new X500Name(subject), 219 intermediatePublicKey 220 ); 221 222 return buildCertificate(certBuilder::build, issuerPrivateKey); 223 } 224 225 /** 226 * Creates a signed end entity certificate from the given CSR. 227 * <p> 228 * This method is only meant for testing purposes! Do not use it in a real-world CA 229 * implementation. 230 * <p> 231 * Do not assume that real-world certificates have a similar structure. It's up to the 232 * discretion of the CA which distinguished names, validity dates, extensions and 233 * other parameters are transferred from the CSR to the generated certificate. 234 * 235 * @param csr 236 * CSR to create the certificate from 237 * @param notBefore 238 * {@link Instant} before which the certificate is not valid. 239 * @param notAfter 240 * {@link Instant} after which the certificate is not valid. 241 * @param issuer 242 * The issuer's {@link X509Certificate}. 243 * @param issuerPrivateKey 244 * {@link PrivateKey} of the issuer. This is not the private key the CSR was 245 * signed with. 246 * @return Generated {@link X509Certificate} 247 * @since 2.8 248 */ 249 public static X509Certificate createTestCertificate(PKCS10CertificationRequest csr, 250 Instant notBefore, Instant notAfter, X509Certificate issuer, PrivateKey issuerPrivateKey) { 251 Objects.requireNonNull(csr, "csr"); 252 Objects.requireNonNull(notBefore, "notBefore"); 253 Objects.requireNonNull(notAfter, "notAfter"); 254 Objects.requireNonNull(issuer, "issuer"); 255 Objects.requireNonNull(issuerPrivateKey, "issuerPrivateKey"); 256 257 try { 258 var jcaCsr = new JcaPKCS10CertificationRequest(csr); 259 260 var certBuilder = new JcaX509v3CertificateBuilder( 261 new X500Name(issuer.getIssuerX500Principal().getName()), 262 BigInteger.valueOf(System.currentTimeMillis()), 263 Date.from(notBefore), 264 Date.from(notAfter), 265 csr.getSubject(), 266 jcaCsr.getPublicKey()); 267 268 var attr = csr.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest); 269 if (attr.length > 0) { 270 var extensions = attr[0].getAttrValues().toArray(); 271 if (extensions.length > 0 && extensions[0] instanceof Extensions) { 272 var san = GeneralNames.fromExtensions((Extensions) extensions[0], Extension.subjectAlternativeName); 273 var critical = csr.getSubject().getRDNs().length == 0; 274 certBuilder.addExtension(Extension.subjectAlternativeName, critical, san); 275 } 276 } 277 278 return buildCertificate(certBuilder::build, issuerPrivateKey); 279 } catch (NoSuchAlgorithmException | InvalidKeyException | CertIOException ex) { 280 throw new IllegalArgumentException("Invalid CSR", ex); 281 } 282 } 283 284 /** 285 * Build a {@link X509Certificate} from a builder. 286 * 287 * @param builder 288 * Builder method that receives a {@link ContentSigner} and returns a {@link 289 * X509CertificateHolder}. 290 * @param privateKey 291 * {@link PrivateKey} to sign the certificate with 292 * @return The generated {@link X509Certificate} 293 */ 294 private static X509Certificate buildCertificate(Function<ContentSigner, X509CertificateHolder> builder, PrivateKey privateKey) { 295 try { 296 var signerBuilder = new JcaContentSignerBuilder("SHA256withRSA"); 297 var cert = builder.apply(signerBuilder.build(privateKey)).getEncoded(); 298 var certificateFactory = CertificateFactory.getInstance("X.509"); 299 return (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(cert)); 300 } catch (CertificateException | OperatorCreationException | IOException ex) { 301 throw new IllegalArgumentException("Could not build certificate", ex); 302 } 303 } 304 305}