diff --git a/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CertificateUtils.java b/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CertificateUtils.java index 66fdcb60..19a0da4e 100644 --- a/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CertificateUtils.java +++ b/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CertificateUtils.java @@ -19,7 +19,11 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.math.BigInteger; import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; @@ -27,21 +31,31 @@ import java.time.Duration; import java.time.Instant; import java.util.Date; import java.util.Objects; +import java.util.function.Function; import javax.annotation.ParametersAreNonnullByDefault; import javax.annotation.WillClose; +import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.pkcs.Attribute; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.Extensions; import org.bouncycastle.asn1.x509.GeneralName; import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.cert.CertIOException; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509v1CertificateBuilder; import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest; import org.shredzone.acme4j.Identifier; import org.shredzone.acme4j.challenge.TlsAlpn01Challenge; @@ -106,44 +120,190 @@ public final class CertificateUtils { } final long now = System.currentTimeMillis(); - final String signatureAlg = "SHA256withRSA"; + + X500Name issuer = new X500Name("CN=acme.invalid"); + BigInteger serial = BigInteger.valueOf(now); + Instant notBefore = Instant.ofEpochMilli(now); + Instant notAfter = notBefore.plus(Duration.ofDays(7)); + + JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder( + issuer, serial, Date.from(notBefore), Date.from(notAfter), + issuer, keypair.getPublic()); + + GeneralName[] gns = new GeneralName[1]; + + switch (id.getType()) { + case Identifier.TYPE_DNS: + gns[0] = new GeneralName(GeneralName.dNSName, id.getDomain()); + break; + + case Identifier.TYPE_IP: + gns[0] = new GeneralName(GeneralName.iPAddress, id.getIP().getHostAddress()); + break; + + default: + throw new IllegalArgumentException("Unsupported Identifier type " + id.getType()); + } + certBuilder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(gns)); + certBuilder.addExtension(ACME_VALIDATION, true, new DEROctetString(acmeValidation)); + + return buildCertificate(certBuilder::build, keypair.getPrivate()); + } + + /** + * Creates a self-signed root certificate. + *
+ * The generated certificate is only meant for testing purposes! + * + * @param subject + * This certificate's subject X.500 name. + * @param notBefore + * {@link Instant} before which the certificate is not valid. + * @param notAfter + * {@link Instant} after which the certificate is not valid. + * @param keypair + * {@link KeyPair} that is to be used for this certificate. + * @return Generated {@link X509Certificate} + * @since 2.8 + */ + public static X509Certificate createTestRootCertificate(String subject, + Instant notBefore, Instant notAfter, KeyPair keypair) { + Objects.requireNonNull(subject, "subject"); + Objects.requireNonNull(notBefore, "notBefore"); + Objects.requireNonNull(notAfter, "notAfter"); + Objects.requireNonNull(keypair, "keypair"); + + JcaX509v1CertificateBuilder certBuilder = new JcaX509v1CertificateBuilder( + new X500Name(subject), + BigInteger.valueOf(System.currentTimeMillis()), + Date.from(notBefore), + Date.from(notAfter), + new X500Name(subject), + keypair.getPublic() + ); + + return buildCertificate(certBuilder::build, keypair.getPrivate()); + } + + /** + * Creates an intermediate certificate that is signed by an issuer. + *
+ * The generated certificate is only meant for testing purposes! + * + * @param subject + * This certificate's subject X.500 name. + * @param notBefore + * {@link Instant} before which the certificate is not valid. + * @param notAfter + * {@link Instant} after which the certificate is not valid. + * @param intermediatePublicKey + * {@link PublicKey} of this certificate + * @param issuer + * The issuer's {@link X509Certificate}. + * @param issuerPrivateKey + * {@link PrivateKey} of the issuer. This is not the private key of this + * intermediate certificate. + * @return Generated {@link X509Certificate} + * @since 2.8 + */ + public static X509Certificate createTestIntermediateCertificate(String subject, + Instant notBefore, Instant notAfter, PublicKey intermediatePublicKey, + X509Certificate issuer, PrivateKey issuerPrivateKey) { + Objects.requireNonNull(subject, "subject"); + Objects.requireNonNull(notBefore, "notBefore"); + Objects.requireNonNull(notAfter, "notAfter"); + Objects.requireNonNull(intermediatePublicKey, "intermediatePublicKey"); + Objects.requireNonNull(issuer, "issuer"); + Objects.requireNonNull(issuerPrivateKey, "issuerPrivateKey"); + + JcaX509v1CertificateBuilder certBuilder = new JcaX509v1CertificateBuilder( + new X500Name(issuer.getIssuerX500Principal().getName()), + BigInteger.valueOf(System.currentTimeMillis()), + Date.from(notBefore), + Date.from(notAfter), + new X500Name(subject), + intermediatePublicKey + ); + + return buildCertificate(certBuilder::build, issuerPrivateKey); + } + + /** + * Creates a signed end entity certificate from the given CSR. + *
+ * This method is only meant for testing purposes! Do not use it in a real-world CA + * implementation. + *
+ * Do not assume that real-world certificates have a similar structure. It's up to the
+ * discretion of the CA which distinguished names, validity dates, extensions and
+ * other parameters are transferred from the CSR to the generated certificate.
+ *
+ * @param csr
+ * CSR to create the certificate from
+ * @param notBefore
+ * {@link Instant} before which the certificate is not valid.
+ * @param notAfter
+ * {@link Instant} after which the certificate is not valid.
+ * @param issuer
+ * The issuer's {@link X509Certificate}.
+ * @param issuerPrivateKey
+ * {@link PrivateKey} of the issuer. This is not the private key the CSR was
+ * signed with.
+ * @return Generated {@link X509Certificate}
+ * @since 2.8
+ */
+ public static X509Certificate createTestCertificate(PKCS10CertificationRequest csr,
+ Instant notBefore, Instant notAfter, X509Certificate issuer, PrivateKey issuerPrivateKey) {
+ Objects.requireNonNull(csr, "csr");
+ Objects.requireNonNull(notBefore, "notBefore");
+ Objects.requireNonNull(notAfter, "notAfter");
+ Objects.requireNonNull(issuer, "issuer");
+ Objects.requireNonNull(issuerPrivateKey, "issuerPrivateKey");
try {
- X500Name issuer = new X500Name("CN=acme.invalid");
- BigInteger serial = BigInteger.valueOf(now);
- Instant notBefore = Instant.ofEpochMilli(now);
- Instant notAfter = notBefore.plus(Duration.ofDays(7));
+ JcaPKCS10CertificationRequest jcaCsr = new JcaPKCS10CertificationRequest(csr);
JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
- issuer, serial, Date.from(notBefore), Date.from(notAfter),
- issuer, keypair.getPublic());
+ new X500Name(issuer.getIssuerX500Principal().getName()),
+ BigInteger.valueOf(System.currentTimeMillis()),
+ Date.from(notBefore),
+ Date.from(notAfter),
+ csr.getSubject(),
+ jcaCsr.getPublicKey());
- GeneralName[] gns = new GeneralName[1];
-
- switch (id.getType()) {
- case Identifier.TYPE_DNS:
- gns[0] = new GeneralName(GeneralName.dNSName, id.getDomain());
- break;
-
- case Identifier.TYPE_IP:
- gns[0] = new GeneralName(GeneralName.iPAddress, id.getIP().getHostAddress());
- break;
-
- default:
- throw new IllegalArgumentException("Unsupported Identifier type " + id.getType());
+ Attribute[] attr = csr.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest);
+ if (attr.length > 0) {
+ ASN1Encodable[] extensions = attr[0].getAttrValues().toArray();
+ if (extensions.length > 0 && extensions[0] instanceof Extensions) {
+ GeneralNames san = GeneralNames.fromExtensions((Extensions) extensions[0], Extension.subjectAlternativeName);
+ certBuilder.addExtension(Extension.subjectAlternativeName, false, san);
+ }
}
- certBuilder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(gns));
- certBuilder.addExtension(ACME_VALIDATION, true, new DEROctetString(acmeValidation));
-
- JcaContentSignerBuilder signerBuilder = new JcaContentSignerBuilder(signatureAlg);
-
- byte[] cert = certBuilder.build(signerBuilder.build(keypair.getPrivate())).getEncoded();
+ return buildCertificate(certBuilder::build, issuerPrivateKey);
+ } catch (NoSuchAlgorithmException | InvalidKeyException | CertIOException ex) {
+ throw new IllegalArgumentException("Invalid CSR", ex);
+ }
+ }
+ /**
+ * Build a {@link X509Certificate} from a builder.
+ *
+ * @param builder
+ * Builder method that receives a {@link ContentSigner} and returns a {@link
+ * X509CertificateHolder}.
+ * @param privateKey
+ * {@link PrivateKey} to sign the certificate with
+ * @return The generated {@link X509Certificate}
+ */
+ private static X509Certificate buildCertificate(Function