All Downloads are FREE. Search and download functionalities are using the official Maven repository.

fi.protonode.certy.Credential Maven / Gradle / Ivy

The newest version!
/*
 * Copyright Certy Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package fi.protonode.certy;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.interfaces.ECPublicKey;
import java.security.spec.EllipticCurve;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.CRLDistPoint;
import org.bouncycastle.asn1.x509.DistributionPoint;
import org.bouncycastle.asn1.x509.DistributionPointName;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.cert.CertIOException;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;

/**
 * Credential is a builder class for generating certificates and PKI hierarchies programmatically.
 * It is intended to be used in unit tests to create test certificates on-demand, to make it unnecessary to commit them into git repo as test data.
 */
public class Credential {

    /** Key type values for {@link #keyType}. */
    public enum KeyType {
        /**
         * EC key type.
         */
        EC,
        /**
         * RSA key type.
         */
        RSA,
        /**
         * Ed25519 key type. Ed25519 is supported in JDK 15 and later.
         */
        ED25519
    }

    /** Key usage values for {@link #keyUsages}. */
    public enum KeyUsage {
        DIGITAL_SIGNATURE(org.bouncycastle.asn1.x509.KeyUsage.digitalSignature),
        NON_REPUDIATION(org.bouncycastle.asn1.x509.KeyUsage.nonRepudiation),
        KEY_ENCIPHERMENT(org.bouncycastle.asn1.x509.KeyUsage.keyEncipherment),
        DATA_ENCIPHERMENT(org.bouncycastle.asn1.x509.KeyUsage.dataEncipherment),
        KEY_AGREEMENT(org.bouncycastle.asn1.x509.KeyUsage.keyAgreement),
        KEY_CERT_SIGN(org.bouncycastle.asn1.x509.KeyUsage.keyCertSign),
        CRL_SIGN(org.bouncycastle.asn1.x509.KeyUsage.cRLSign),
        ENCIPHER_ONLY(org.bouncycastle.asn1.x509.KeyUsage.encipherOnly),
        DECIPHER_ONLY(org.bouncycastle.asn1.x509.KeyUsage.decipherOnly);

        private int val;

        private KeyUsage(int val) {
            this.val = val;
        }

        public int getValue() {
            return val;
        }
    }

    /** Extended key usage values for {@link #extKeyUsages}. */
    public enum ExtKeyUsage {
        ANY(KeyPurposeId.anyExtendedKeyUsage),
        SERVER_AUTH(KeyPurposeId.id_kp_serverAuth),
        CLIENT_AUTH(KeyPurposeId.id_kp_clientAuth),
        CODE_SIGNING(KeyPurposeId.id_kp_codeSigning),
        EMAIL_PROTECTION(KeyPurposeId.id_kp_emailProtection),
        TIME_STAMPING(KeyPurposeId.id_kp_timeStamping),
        OCSP_SIGNING(KeyPurposeId.id_kp_OCSPSigning);

        private KeyPurposeId val;

        private ExtKeyUsage(KeyPurposeId val) {
            this.val = val;
        }

        public KeyPurposeId getValue() {
            return val;
        }
    }

    // Attributes set by user via builder methods.
    protected X500Name subject;
    private GeneralNames subjectAltNames;
    private KeyType keyType;
    private int keySize;
    private Duration expires;
    private Date notBefore;
    private Date notAfter;
    private List keyUsages;
    private List extKeyUsages;
    protected Credential issuer;
    private Boolean isCa;
    protected BigInteger serial;
    private String crlDistributionPointUri;

    // Generated attributes.
    protected KeyPair keyPair;
    protected Certificate certificate;

    /**
     * Creates new credential builder.
     */
    public Credential() {
        keyUsages = new ArrayList<>();
        extKeyUsages = new ArrayList<>();
        // Defaults will be set after builder methods, when generate() is called.
    }

    /**
     * Defines the distinguished name for the certificate (mandatory).

* Example: {@code "CN=Joe"}. * * @param val Subject name. * @return The Credential itself. */ public Credential subject(String val) { this.subject = new X500Name(val); return this; } /** * Defines an optional list of values for x509 Subject Alternative Name extension.

* Examples: {@code "DNS:www.example.com"}, * {@code "IP:1.2.3.4"}, * {@code "URI:https://www.example.com"}. * @param val List of subject alternative names. * @return The Credential itself. */ public Credential subjectAltNames(List val) { this.subjectAltNames = asGeneralNames(val); return this; } /** * Defines an optional value for x509 Subject Alternative Name extension.

* Examples: {@code "DNS:www.example.com"}, * {@code "IP:1.2.3.4"}, * {@code "URI:https://www.example.com"}. * * @param val Subject alternative name. * @return The Credential itself. */ public Credential subjectAltName(String val) { this.subjectAltNames = asGeneralNames(Arrays.asList(val)); return this; } /** * Defines the certificate key algorithm. * Defaults to {@code KeyType.EC} if not set. * * @param val Key type. * @return The Credential itself. */ public Credential keyType(KeyType val) { this.keyType = val; return this; } /** * Defines the key length in bits. * Default value is 256 (EC) or 2048 (RSA) if keySize is not set. *

* Examples: * For keyType EC: 256, 384, 521. * For keyType RSA: 1024, 2048, 4096. * For keyType ED25519: 255. * * @param val Key size. * @return The Credential itself. */ public Credential keySize(int val) { this.keySize = val; return this; } /** * Defines {@link #notAfter} by duration from current time. * {@link #notAfter} takes precedence over expires. * The default value is 1 year if {@code expires} is not set. * * @param val Time until expiration. * @return The Credential itself. */ public Credential expires(Duration val) { this.expires = val; return this; } /** * Defines certificate not to be valid before given time. * The default value is current time if {@code notBefore} is not set. * * @param val Time when certificate becomes valid. * @return The Credential itself. */ public Credential notBefore(Date val) { this.notBefore = val; return this; } /** * Defines certificate not to be valid after given time. * Default value is current time + expires if {@code notAfter} is not set. * * @param val Time when certificate expires. * @return The Credential itself. */ public Credential notAfter(Date val) { this.notAfter = val; return this; } /** * Defines a sequence of values for x509 key usage extension.

* * Following defaults are used if {@code keyUsages} is not set:

* CertSign and CRLSign are set for CA certificates. * KeyEncipherment and DigitalSignature are set for end-entity certificates with RSA key. * KeyEncipherment, DigitalSignature and KeyAgreement are set for end-entity certificates with EC key. * * @param val List of key usages. * @return The Credential itself. */ public Credential keyUsages(List val) { this.keyUsages = val; return this; } /** * Defines an optional list of x509 extended key usages. * * @param val List of extended key usages. * @return The Credential itself. */ public Credential extKeyUsages(List val) { this.extKeyUsages = val; return this; } /** * Defines the issuer Certificate. * Self-signed certificate is generated if issuer is not defined. * * @param val Instance of {@code Credential} that will be used to sign this certificate. * @return The Credential itself. */ public Credential issuer(Credential val) { this.issuer = val; return this; } /** * Defines basic constraints CA attribute. * Self-signed certificates are automatically set {@code CA:true}, others default to {@code CA:false}. * * @param val Value for CA attribute of basic constraints. * @return The Credential itself. */ public Credential ca(Boolean val) { this.isCa = val; return this; } /** * Defines serial number. * Default value is current time in milliseconds. * * @param val Value for serial number. * @return The Credential itself. */ public Credential serial(BigInteger val) { this.serial = val; return this; } /** * Defines URI for CRL distribution point extension. * * @param val URI for CRL distribution point. * @return The Credential itself. */ public Credential crlDistributionPointUri(String val) { this.crlDistributionPointUri = val; return this; } /** * (Re)generate certificate and private key with currently set values. * * @return The Credential itself. */ public Credential generate() throws CertificateException, NoSuchAlgorithmException { try { // Traverse the certificate hierarchy recursively to ensure issuing CAs have // been generated as well. if (issuer != null) { issuer.ensureGenerated(); } setDefaults(); keyPair = newKeyPair(keyType, keySize); // Calculate the validity dates according to given values and current time. Date effectiveNotBefore; Date effectiveNotAfter; if (notBefore != null) { effectiveNotBefore = notBefore; } else { effectiveNotBefore = new Date(); // Now. } if (notAfter != null) { effectiveNotAfter = notAfter; } else { effectiveNotAfter = Date.from(effectiveNotBefore.toInstant().plus(expires)); } // In theory subject could be empty but did not find a way to allow empty X500Name in Bouncy Castle. if (subject == null) { throw new IllegalArgumentException("subject name must be set"); } X500Name effectiveIssuer; ContentSigner signer; if (issuer == null) { effectiveIssuer = subject; signer = new JcaContentSignerBuilder(signatureAlgorithm(keyPair.getPublic())) .build(keyPair.getPrivate()); } else { effectiveIssuer = issuer.subject; signer = new JcaContentSignerBuilder(signatureAlgorithm(issuer.keyPair.getPublic())) .build(issuer.keyPair.getPrivate()); } JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder( effectiveIssuer, serial, effectiveNotBefore, effectiveNotAfter, subject, keyPair.getPublic()); JcaX509ExtensionUtils utils = new JcaX509ExtensionUtils(); builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(isCa)) .addExtension(Extension.subjectKeyIdentifier, false, utils.createSubjectKeyIdentifier(keyPair.getPublic())) .addExtension(Extension.keyUsage, true, new org.bouncycastle.asn1.x509.KeyUsage( keyUsages.stream().collect(Collectors.summingInt(KeyUsage::getValue)))); if (subjectAltNames != null) { // If subject could be null, subjectAltName would be set critical. // But did not find a way to set empty subject in Bouncy Castle, so subject == null is never true. builder.addExtension(Extension.subjectAlternativeName, subject == null, subjectAltNames); } if (!extKeyUsages.isEmpty()) { builder.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage( extKeyUsages.stream().map(ExtKeyUsage::getValue).toArray(KeyPurposeId[]::new))); } if (crlDistributionPointUri != null) { DistributionPointName dp = new DistributionPointName(new GeneralNames(new GeneralName( GeneralName.uniformResourceIdentifier, crlDistributionPointUri))); builder.addExtension(Extension.cRLDistributionPoints, false, new CRLDistPoint( new DistributionPoint[] { new DistributionPoint(dp, null, null) })); } certificate = new JcaX509CertificateConverter().setProvider(new BouncyCastleProvider()) .getCertificate(builder.build(signer)); } catch (CertIOException | OperatorCreationException e) { throw new CertificateException(e.toString()); } return this; } /** * Returns PEM block containing X509 certificate. * To get PEM bundle including certificate chain see {@link #getCertificatesAsPem}. * * @return String containing the certificate as PEM. */ public String getCertificateAsPem() throws CertificateException, NoSuchAlgorithmException, IOException { ensureGenerated(); StringWriter writer = new StringWriter(); JcaPEMWriter pemWriter = new JcaPEMWriter(writer); pemWriter.writeObject(certificate); pemWriter.flush(); pemWriter.close(); return writer.toString(); } /** * Returns PEM bundle containing X509 certificate and its chain (if any). * * @return String containing PEM bundle. */ public String getCertificatesAsPem() throws CertificateException, NoSuchAlgorithmException, IOException { ensureGenerated(); StringWriter writer = new StringWriter(); JcaPEMWriter pemWriter = new JcaPEMWriter(writer); for (Certificate c : getChain()) { pemWriter.writeObject(c); } pemWriter.flush(); pemWriter.close(); return writer.toString(); } /** * Returns PEM block containing private key in PKCS8 format. * * @return String containing the private key. */ public String getPrivateKeyAsPem() throws IOException, CertificateException, NoSuchAlgorithmException { ensureGenerated(); StringWriter writer = new StringWriter(); JcaPEMWriter pemWriter = new JcaPEMWriter(writer); pemWriter.writeObject(new JcaPKCS8Generator(keyPair.getPrivate(), null)); pemWriter.flush(); pemWriter.close(); return writer.toString(); } /** * Writes X509 certificate to a file as PEM block. * To write PEM bundle including certificate chain see {@link #writeCertificatesAsPem}. * * @param out Path to write the PEM file to. * @return The Credential itself. */ public Credential writeCertificateAsPem(Path out) throws IOException, CertificateException, NoSuchAlgorithmException { ensureGenerated(); try (BufferedWriter writer = Files.newBufferedWriter(out, StandardCharsets.UTF_8)) { JcaPEMWriter pemWriter = new JcaPEMWriter(writer); pemWriter.writeObject(certificate); pemWriter.flush(); pemWriter.close(); } return this; } /** * Writes PEM bundle containing X509 certificate and its chain (if any). * * @param out Path to write the PEM file to. * @return The Credential itself. */ public Credential writeCertificatesAsPem(Path out) throws IOException, CertificateException, NoSuchAlgorithmException { ensureGenerated(); try (BufferedWriter writer = Files.newBufferedWriter(out, StandardCharsets.UTF_8)) { JcaPEMWriter pemWriter = new JcaPEMWriter(writer); for (Certificate c : getChain()) { pemWriter.writeObject(c); } pemWriter.flush(); pemWriter.close(); } return this; } /** * Writes private key in PKCS8 format to a file as PEM block. * * @param out Path to write the PEM file to. * @return The Credential itself. */ public Credential writePrivateKeyAsPem(Path out) throws IOException, CertificateException, NoSuchAlgorithmException { ensureGenerated(); try (BufferedWriter writer = Files.newBufferedWriter(out, StandardCharsets.UTF_8)) { JcaPEMWriter pemWriter = new JcaPEMWriter(writer); pemWriter.writeObject(new JcaPKCS8Generator(keyPair.getPrivate(), null)); pemWriter.flush(); pemWriter.close(); } return this; } /** * Returns certificate. * To get certificate including certificate chain see {@link #getCertificates} * * @return Certificate. */ public Certificate getCertificate() throws CertificateException, NoSuchAlgorithmException { ensureGenerated(); return certificate; } /** * Returns certificate and its chain (if any). * * @return Array of certificates. */ public Certificate[] getCertificates() throws CertificateException, NoSuchAlgorithmException { ensureGenerated(); return getChain(); } /** * Returns certificate. * * @return Certificate as {@code X509Certificate}. */ public X509Certificate getX509Certificate() throws CertificateException, NoSuchAlgorithmException { ensureGenerated(); return (X509Certificate) certificate; } /** * Returns certificate and its chain (if any). * * @return Array of certificates as {@code X509Certificate}. */ public X509Certificate[] getX509Certificates() throws CertificateException, NoSuchAlgorithmException { ensureGenerated(); return Arrays.stream(getChain()).map(c -> (X509Certificate) c).toArray(X509Certificate[]::new); } /** * Returns private key. * * @return Private key. */ public PrivateKey getPrivateKey() throws CertificateException, NoSuchAlgorithmException { ensureGenerated(); return keyPair.getPrivate(); } // Generates certificate and key pair unless they have been already generated. protected void ensureGenerated() throws CertificateException, NoSuchAlgorithmException { if (certificate == null || keyPair == null) { generate(); } } // Fill in defaults for attributes that caller has not set. private void setDefaults() { if (keyType == null) { keyType = KeyType.EC; } if (keySize == 0) { if (keyType == KeyType.EC) { keySize = 256; } else if (keyType == KeyType.RSA) { keySize = 2048; } else if (keyType == KeyType.ED25519) { keySize = 255; } } if (expires == null && notAfter == null) { expires = Duration.of(365, ChronoUnit.DAYS); } if (isCa == null) { boolean noExplicitIssuer = (issuer == null); isCa = noExplicitIssuer; } if (keyUsages.isEmpty()) { if (Boolean.TRUE.equals(isCa)) { keyUsages = Arrays.asList(KeyUsage.KEY_CERT_SIGN, KeyUsage.CRL_SIGN); } else if (keyType == KeyType.EC) { // https://github.com/openjdk/jdk/blob/0530f4e517be5d5b3ff10be8a0764e564f068c06/src/java.base/share/classes/sun/security/ssl/X509KeyManagerImpl.java#L604-L618 keyUsages = Arrays.asList(KeyUsage.KEY_ENCIPHERMENT, KeyUsage.DIGITAL_SIGNATURE, KeyUsage.KEY_AGREEMENT); } else { keyUsages = Arrays.asList(KeyUsage.KEY_ENCIPHERMENT, KeyUsage.DIGITAL_SIGNATURE); } } if (serial == null) { serial = BigInteger.valueOf(Instant.now().toEpochMilli()); // Current time in milliseconds. } } // Returns new key pair. private static KeyPair newKeyPair(KeyType keyType, int keySize) throws NoSuchAlgorithmException { KeyPairGenerator keyGen; keyGen = KeyPairGenerator.getInstance(keyType.name()); SecureRandom prng = new SecureRandom(); keyGen.initialize(keySize, prng); return keyGen.genKeyPair(); } // Return preferred signature algorithm for given key. protected static String signatureAlgorithm(PublicKey pub) { switch (pub.getAlgorithm()) { case "EC": EllipticCurve curve = ((ECPublicKey) pub).getParams().getCurve(); switch (curve.getField().getFieldSize()) { case 224: case 256: return "SHA256withECDSA"; case 384: return "SHA384withECDSA"; case 521: return "SHA512withECDSA"; default: throw new IllegalArgumentException("unknown elliptic curve: " + curve); } case "RSA": return "SHA256WithRSAEncryption"; case "EdDSA": return "Ed25519"; default: throw new UnsupportedOperationException("unsupported private key algorithm: " + pub.getAlgorithm()); } } // Parse string-based subject alt names into GeneralNames. private static GeneralNames asGeneralNames(List sans) { List altNames = new ArrayList<>(); for (String name : sans) { // Parse type and value. int separatorPos = name.indexOf(":"); if (separatorPos == -1) { throw new IllegalArgumentException("cannot parse " + name + ": all subjectAltNames must be of format: DNS:www.example.com, IP:1.2.3.4, URI:https://www.example.com"); } String type = name.substring(0, separatorPos); String value = name.substring(separatorPos + 1); // Convert to GeneralName. switch (type) { case "DNS": altNames.add(new GeneralName(GeneralName.dNSName, value)); break; case "IP": altNames.add(new GeneralName(GeneralName.iPAddress, value)); break; case "URI": altNames.add(new GeneralName(GeneralName.uniformResourceIdentifier, value)); break; default: break; } } if (altNames.isEmpty()) { throw new IllegalArgumentException( "subjectAltNames must be of format: DNS:www.example.com, IP:1.2.3.4, URI:https://www.example.com"); } return GeneralNames.getInstance(new DERSequence(altNames.toArray(new GeneralName[0]))); } // Returns certificate and issuing chain certificates up to but not including root CA. private Certificate[] getChain() { List chain = new ArrayList<>(); // Add the certificate itself. chain.add(certificate); // Add chain. Credential parent = issuer; while (parent != null && parent.issuer != null) { chain.add(parent.certificate); parent = parent.issuer; } return chain.toArray(new Certificate[0]); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy