org.usrz.libs.crypto.cert.X509CertificateBuilder.java Source code

Java tutorial

Introduction

Here is the source code for org.usrz.libs.crypto.cert.X509CertificateBuilder.java

Source

/* ========================================================================== *
 * Copyright 2014 USRZ.com and Pier Paolo Fumagalli                           *
 * -------------------------------------------------------------------------- *
 * 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 org.usrz.libs.crypto.cert;

import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.usrz.libs.crypto.hash.Hash.SHA256;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.KeyFactory;
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;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.CRC32;

import javax.security.auth.x500.X500Principal;

import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.DEROctetString;
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.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.CertIOException;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.usrz.libs.crypto.hash.Hash;
import org.usrz.libs.crypto.utils.CryptoUtils;

/**
 * A simple builder to create {@linkplain X509Certificate X.509 certificates}.
 *
 * @author <a href="mailto:pier@usrz.com">Pier Fumagalli</a>
 */
public class X509CertificateBuilder {

    /**
     * An enumeration containing the possible <i>Standard key usages</i> supported
     * by a {@link X509CertificateBuilder}.
     *
     * <p>Key usage extensions define the purpose of the public key contained
     * in a certificate. You can use them to restrict the public key to as few
     * or as many operations as needed. For example, if you have a key used
     * only for signing or verifying a signature, enable the digital signature
     * and/or non-repudiation extensions. Alternatively, if a key is used only
     * for key management, enable key encipherment.</p>
     *
     * @see <a target="_blank" href="http://publib.boulder.ibm.com/infocenter/domhelp/v8r0/index.jsp?topic=%2Fcom.ibm.help.domino.admin.doc%2FDOC%2FH_KEY_USAGE_EXTENSIONS_FOR_INTERNET_CERTIFICATES_1521_OVER.html">IBM's reference</a>
     */
    public enum StandardKeyUsage {
        /**
         * Use when the public key is used with a digital signature mechanism
         * to support security services other than non-repudiation,
         * certificate signing, or CRL signing. A digital signature is often
         * used for entity authentication and data origin authentication with
         * integrity.
         */
        DIGITAL_SIGNATURE(KeyUsage.digitalSignature),

        /**
         * Use when the public key is used to verify digital signatures used
         * to provide a non-repudiation service. Non-repudiation protects
         * against the signing entity falsely denying some action (excluding
         * certificate or CRL signing).
         */
        NON_REPUDIATION(KeyUsage.nonRepudiation),

        /**
         * Use when a certificate will be used with a protocol that encrypts
         * keys. An example is S/MIME enveloping, where a fast (symmetric) key
         * is encrypted with the public key from the certificate. SSL protocol
         * also performs key encipherment.
         */
        KEY_ENCIPHERMENT(KeyUsage.keyEncipherment),

        /**
         * Use when the public key is used for encrypting user data, other
         * than cryptographic keys.
         */
        DATA_ENCIPHERMENT(KeyUsage.keyEncipherment),

        /**
         * Use when the sender and receiver of the public key need to derive
         * the key without using encryption. This key can then can be used to
         * encrypt messages between the sender and receiver. Key agreement is
         * typically used with Diffie-Hellman ciphers.
         */
        KEY_AGREEMENT(KeyUsage.keyAgreement),

        /**
         * Use when the subject public key is used to verify a signature on
         * certificates. This extension can be used only in CA certificates.
         */
        KEY_CERT_SIGN(KeyUsage.keyCertSign),

        /**
         * Use when the subject public key is to verify a signature on
         * revocation information, such as a CRL.
         */
        CRL_SIGN(KeyUsage.cRLSign),

        /**
         * Use only when key agreement is also enabled. This enables the
         * public key to be used only for enciphering data while performing
         * key agreement.
         */
        ENCIPHER_ONLY(KeyUsage.encipherOnly),

        /**
         * Use only when key agreement is also enabled. This enables the
         * public key to be used only for deciphering data while performing
         * key agreement.
         */
        DECIPHER_ONLY(KeyUsage.decipherOnly);

        private int usage;

        private StandardKeyUsage(int usage) {
            this.usage = usage;
        }

        private static int combine(Collection<? extends StandardKeyUsage> collection) {
            int usage = 0;
            for (StandardKeyUsage flag : collection) {
                usage |= flag.usage;
            }
            return usage;
        }

    }

    /**
     * An enumeration containing the possible <i>extended key usages</i>
     * supported by a {@link X509CertificateBuilder}.
     *
     * <p>Extended key usage further refines key usage extensions. An extended
     * key is either critical or non-critical. If the extension is critical,
     * the certificate must be used only for the indicated purpose or purposes.
     * If the certificate is used for another purpose, it is in violation of
     * the CA's policy.</p>
     *
     * <p>If the extension is non-critical, it indicates the intended purpose
     * or purposes of the key and may be used in finding the correct
     * key/certificate of an entity that has multiple keys/certificates.
     * The extension is then only an informational field and does not imply
     * that the CA restricts use of the key to the purpose indicated.
     * Nevertheless, applications that use certificates may require that a
     * particular purpose be indicated in order for the certificate to be
     * acceptable.</p>
     *
     * <p>If a certificate contains both a critical key usage field and a
     * critical extended key usage field, both fields must be processed
     * independently, and the certificate be used only for a purpose
     * consistent with both fields. If there is no purpose consistent with
     * both fields, the certificate must not be used for any purpose.</p>
     *
     * @see <a target="_blank" href="http://publib.boulder.ibm.com/infocenter/domhelp/v8r0/index.jsp?topic=%2Fcom.ibm.help.domino.admin.doc%2FDOC%2FH_KEY_USAGE_EXTENSIONS_FOR_INTERNET_CERTIFICATES_1521_OVER.html">IBM's reference</a>
     */
    public enum ExtendedKeyUsage {
        /** Any extended key usage */
        ANY(KeyPurposeId.anyExtendedKeyUsage),
        /** TLS web server authentication. */
        SERVER_AUTH(KeyPurposeId.id_kp_serverAuth),
        /** TLS web client authentication. */
        CLIENT_AUTH(KeyPurposeId.id_kp_clientAuth),
        /** Sign (downloadable) executable code. */
        CODE_SIGNING(KeyPurposeId.id_kp_codeSigning),
        /** Email protection. */
        EMAIL_PROTECTION(KeyPurposeId.id_kp_emailProtection),
        /** Timestamping. */
        TIME_STAMPING(KeyPurposeId.id_kp_timeStamping),
        /** Sign OCSP <i>(Online Certificate Status Protocol)</i> responses. */
        OCSP_SIGNING(KeyPurposeId.id_kp_OCSPSigning);

        private final KeyPurposeId id;

        private ExtendedKeyUsage(KeyPurposeId id) {
            this.id = id;
        }

        private static org.bouncycastle.asn1.x509.ExtendedKeyUsage combine(
                Collection<? extends ExtendedKeyUsage> collection) {
            final Set<KeyPurposeId> purposes = new HashSet<>();
            for (ExtendedKeyUsage usage : collection)
                purposes.add(usage.id);
            return new org.bouncycastle.asn1.x509.ExtendedKeyUsage(
                    purposes.toArray(new KeyPurposeId[purposes.size()]));
        }
    }

    /**
     * Standard <em>modes</em> for certificates.
     *
     * @author <a href="mailto:pier@usrz.com">Pier Fumagalli</a>
     */
    public enum Mode {
        /**
         * Client mode:
         * {@linkplain StandardKeyUsage#DIGITAL_SIGNATURE digital signature},
         * {@linkplain StandardKeyUsage#KEY_ENCIPHERMENT key encipherment} and
         * {@linkplain ExtendedKeyUsage#CLIENT_AUTH TLS web client authentication}.
         */
        CLIENT(new StandardKeyUsage[] { StandardKeyUsage.DIGITAL_SIGNATURE, StandardKeyUsage.KEY_ENCIPHERMENT },
                new ExtendedKeyUsage[] { ExtendedKeyUsage.CLIENT_AUTH }),

        /**
         * Server mode:
         * {@linkplain StandardKeyUsage#DIGITAL_SIGNATURE digital signature},
         * {@linkplain StandardKeyUsage#KEY_ENCIPHERMENT key encipherment} and
         * {@linkplain ExtendedKeyUsage#SERVER_AUTH TLS web server authentication}.
         */
        SERVER(new StandardKeyUsage[] { StandardKeyUsage.DIGITAL_SIGNATURE, StandardKeyUsage.KEY_ENCIPHERMENT },
                new ExtendedKeyUsage[] { ExtendedKeyUsage.SERVER_AUTH }),

        /**
         * Certificate Authority mode:
         * {@linkplain StandardKeyUsage#KEY_CERT_SIGN certificate signatures},
         * {@linkplain StandardKeyUsage#CRL_SIGN crl signatures},
         * {@linkplain ExtendedKeyUsage#OCSP_SIGNING OCSP response signing} and
         * will enable the <i>certificate authority</i> flag in the Standard
         * constraints of the certificate.
         */
        AUTHORITY(new StandardKeyUsage[] { StandardKeyUsage.KEY_CERT_SIGN, StandardKeyUsage.CRL_SIGN },
                new ExtendedKeyUsage[] { ExtendedKeyUsage.OCSP_SIGNING });

        private Set<StandardKeyUsage> Standard;
        private Set<ExtendedKeyUsage> extended;

        private Mode(StandardKeyUsage[] Standard, ExtendedKeyUsage[] extended) {
            this.Standard = Standard.length == 0 ? Collections.<StandardKeyUsage>emptySet()
                    : EnumSet.of(Standard[0], Standard);
            this.extended = extended.length == 0 ? Collections.<ExtendedKeyUsage>emptySet()
                    : EnumSet.of(extended[0], extended);
        }
    }

    /* ====================================================================== */

    private X500Principal subject;
    private X500Principal issuer;
    private BigInteger serial;
    private Date notBefore;
    private Date notAfter;
    private PrivateKey issuerPrivateKey;
    private PublicKey issuerPublicKey;
    private PublicKey subjectPublicKey;
    private Mode mode;
    private Hash hash;

    private final Set<StandardKeyUsage> standardKeyUsage = new HashSet<>();
    private final Set<ExtendedKeyUsage> extendedKeyUsage = new HashSet<>();
    private final List<GeneralName> alternativeNames = new ArrayList<>();
    private final Set<GeneralName> crlDistributionPoints = new HashSet<>();

    /* ====================================================================== */

    /**
     * Create a new {@link X509CertificateBuilder} with validity
     * {@linkplain #notBefore from} <i>now</i> and
     * {@linkplain #withValidityNotAfter(long, TimeUnit) duration} of one year.
     */
    public X509CertificateBuilder() {
        this(null);
    }

    /**
     * Create a new {@link X509CertificateBuilder} in the specified
     * {@link Mode} with validity
     * {@linkplain #notBefore from} <i>now</i> and
     * {@linkplain #withValidityNotAfter(long, TimeUnit) duration} of one year.
     */
    public X509CertificateBuilder(Mode mode) {

        /* Default "notBefore" to now, "notAfter" to one year later! */
        final long now = System.currentTimeMillis();
        notBefore = new Date(now);
        final Calendar calendar = Calendar.getInstance();
        calendar.setTime(notBefore);
        calendar.add(Calendar.YEAR, 1);
        notAfter = calendar.getTime();

        /* Set the default hashing algorithm and mode */
        withHash(SHA256);
        withMode(mode);

    }

    /* ====================================================================== */

    /**
     * Build the final {@link X509Certificate} instance.
     */
    public X509Certificate build() {
        if (subject == null)
            throw new IllegalStateException("Subject not specified");
        if (issuer == null)
            throw new IllegalStateException("Issuer not specified");
        if (serial == null)
            throw new IllegalStateException("Serial not specified");
        if (!notAfter.after(notBefore))
            throw new IllegalStateException("Date \"not-after\" before or equal to \"not-before\"");
        if (issuerPrivateKey == null)
            throw new IllegalStateException("Issuer private key not specified");
        if (subjectPublicKey == null)
            throw new IllegalStateException("Sobject public key not specified");

        /* Standard subject public key and X500 names */
        final SubjectPublicKeyInfo subjectPublicKeyInfo = SubjectPublicKeyInfo
                .getInstance(subjectPublicKey.getEncoded());
        final X500Name subjectName = X500Name.getInstance(subject.getEncoded());
        final X500Name issuerName = X500Name.getInstance(issuer.getEncoded());

        /* Derive the issuer public key from the private one if needed/possible */
        if ((issuerPublicKey == null) && (issuerPrivateKey instanceof RSAPrivateCrtKey))
            try {
                final RSAPrivateCrtKey key = (RSAPrivateCrtKey) issuerPrivateKey;
                final RSAPublicKeySpec spec = new RSAPublicKeySpec(key.getModulus(), key.getPublicExponent());
                issuerPublicKey = KeyFactory.getInstance("RSA").generatePublic(spec);
            } catch (InvalidKeySpecException | NoSuchAlgorithmException exception) {
                Logger.getLogger(this.getClass().getName()).log(Level.FINE,
                        "Unable to generate public key from private", exception);
            }

        final X509v3CertificateBuilder certificateBuilder = new X509v3CertificateBuilder(issuerName, serial,
                notBefore, notAfter, subjectName, subjectPublicKeyInfo);

        try {
            final JcaX509ExtensionUtils utils = new JcaX509ExtensionUtils();

            /* Are we a certificate authority? */
            certificateBuilder.addExtension(Extension.basicConstraints, true,
                    new BasicConstraints(Mode.AUTHORITY.equals(mode)));

            /* Add our subject key identifier */
            certificateBuilder.addExtension(Extension.subjectKeyIdentifier, false,
                    utils.createSubjectKeyIdentifier(subjectPublicKeyInfo));

            /* Do we have Standard key usages? */
            if (!standardKeyUsage.isEmpty())
                certificateBuilder.addExtension(Extension.keyUsage, false,
                        new KeyUsage(StandardKeyUsage.combine(standardKeyUsage)));

            /* Do we have extended key usages? */
            if (!extendedKeyUsage.isEmpty())
                certificateBuilder.addExtension(Extension.extendedKeyUsage, false,
                        ExtendedKeyUsage.combine(extendedKeyUsage));

            /* Add our authority key identifer */
            if (issuerPublicKey != null) {
                final SubjectPublicKeyInfo authorityPublicKeyInfo = SubjectPublicKeyInfo
                        .getInstance(issuerPublicKey.getEncoded());
                certificateBuilder.addExtension(Extension.authorityKeyIdentifier, false,
                        utils.createAuthorityKeyIdentifier(authorityPublicKeyInfo));
            }

            /* Add our alternative names */
            if (!alternativeNames.isEmpty()) {
                final GeneralName[] names = alternativeNames.toArray(new GeneralName[alternativeNames.size()]);
                certificateBuilder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(names));
            }

            /* Add CRL distribution points */
            if (!crlDistributionPoints.isEmpty()) {
                final DistributionPoint[] distributionPoints = new DistributionPoint[crlDistributionPoints.size()];
                int position = 0;
                for (GeneralName generalName : crlDistributionPoints) {
                    final DistributionPointName distributionPointName = new DistributionPointName(
                            new GeneralNames(generalName));
                    distributionPoints[position++] = new DistributionPoint(distributionPointName, null, null);
                }
                final CRLDistPoint crlDistributionPoint = new CRLDistPoint(distributionPoints);
                certificateBuilder.addExtension(Extension.cRLDistributionPoints, false, crlDistributionPoint);
            }

        } catch (CertIOException | NoSuchAlgorithmException exception) {
            throw new IllegalStateException("Exception adding extensions", exception);
        }

        try {
            final CertificateFactory factory = CertificateFactory.getInstance("X.509");
            final String signatureAlgorithm = CryptoUtils.getSignatureAlgorithm(issuerPrivateKey, hash);
            final ContentSigner signer = new JcaContentSignerBuilder(signatureAlgorithm).build(issuerPrivateKey);
            final X509CertificateHolder certificateHolder = certificateBuilder.build(signer);
            return (X509Certificate) factory
                    .generateCertificate(new ByteArrayInputStream(certificateHolder.getEncoded()));
        } catch (OperatorCreationException exception) {
            throw new IllegalStateException("Unable to create certificate signature", exception);
        } catch (IOException exception) {
            throw new IllegalStateException("Unable to generate certificate data", exception);
        } catch (CertificateException exception) {
            throw new IllegalStateException("Unable to generate certificate", exception);
        }
    }

    /* ====================================================================== */

    /**
     * Set the Standard {@link Mode} of this {@link X509CertificateBuilder}.
     *
     * <p>This will reset both {@linkplain #clearStandardKeyUsage() Standard}
     * and {@linkplain #clearExtendedKeyUsage() extended} key usage flags.
     */
    public X509CertificateBuilder withMode(Mode mode) {
        clearStandardKeyUsage();
        clearExtendedKeyUsage();
        if (mode != null) {
            standardKeyUsage.addAll(mode.Standard);
            extendedKeyUsage.addAll(mode.extended);
        }
        this.mode = mode;
        return this;
    }

    /**
     * Clear all {@linkplain StandardKeyUsage standard key usage} flags.
     */
    public X509CertificateBuilder clearStandardKeyUsage() {
        standardKeyUsage.clear();
        return this;
    }

    /**
     * Add the specified {@linkplain StandardKeyUsage standard key usage} flags.
     */
    public X509CertificateBuilder withStandardKeyUsage(StandardKeyUsage... standardUsages) {
        standardKeyUsage.addAll(Arrays.asList(standardUsages));
        return this;
    }

    /**
     * Clear all {@linkplain ExtendedKeyUsage standard key usage} flags.
     */
    public X509CertificateBuilder clearExtendedKeyUsage() {
        extendedKeyUsage.clear();
        return this;
    }

    /**
     * Add the specified {@linkplain ExtendedKeyUsage standard key usage} flags.
     */
    public X509CertificateBuilder withExtendedKeyUsage(ExtendedKeyUsage... extendedUsages) {
        extendedKeyUsage.addAll(Arrays.asList(extendedUsages));
        return this;
    }

    /* ====================================================================== */

    /**
     * Set the certificate's subject {@linkplain X500Principal principal}.
     */
    public X509CertificateBuilder withSubject(X500Principal subject) {
        if (subject == null)
            throw new NullPointerException("Null subject");
        this.subject = subject;
        return this;
    }

    /**
     * Set the certificate's subject {@linkplain X500Principal principal} by
     * {@linkplain X500Principal#X500Principal(String) parsing} a string.
     */
    public X509CertificateBuilder withSubject(String subject) {
        if (subject == null)
            throw new NullPointerException("Null subject");
        return this.withSubject(new X500Principal(subject));
    }

    /* ====================================================================== */

    /**
     * Set the {@linkplain X509Certificate certificate} of the authority
     * issuing the certificate.
     *
     * <p>This method will set the issuer's
     * {@linkplain #withIssuer(X500Principal) principal},
     * {@linkplain #withIssuerPublicKey(PublicKey) public key}
     * and will attempt to copy the issuer's
     * {@linkplain #withCrlDistributionPoint(URI) CRL distribution points}
     * in the issued certificate.</p>
     */
    public X509CertificateBuilder withIssuer(X509Certificate issuer) {
        if (issuer == null)
            throw new NullPointerException("Null issuer");
        this.withIssuer(issuer.getSubjectX500Principal());
        withIssuerPublicKey(issuer.getPublicKey());

        final byte[] crl = issuer.getExtensionValue(Extension.cRLDistributionPoints.toString());
        if (crl != null)
            try {
                final DEROctetString value = (DEROctetString) ASN1Primitive.fromByteArray(crl);
                final CRLDistPoint crlDistPoint = CRLDistPoint.getInstance(value.getOctets());
                for (DistributionPoint distPoint : crlDistPoint.getDistributionPoints()) {
                    final DistributionPointName distPointName = distPoint.getDistributionPoint();
                    final GeneralNames names = (GeneralNames) distPointName.getName();
                    for (GeneralName name : names.getNames()) {
                        crlDistributionPoints.add(name);
                    }
                }
            } catch (Exception exception) {
                Logger.getLogger(this.getClass().getName()).log(Level.WARNING,
                        "Unable to parse CRL distribution points", exception);
            }

        return this;
    }

    /**
     * Set the certificate's issuer {@linkplain X500Principal principal}.
     */
    public X509CertificateBuilder withIssuer(X500Principal issuer) {
        if (issuer == null)
            throw new NullPointerException("Null issuer");
        this.issuer = issuer;
        return this;
    }

    /**
     * Set the certificate's issuer {@linkplain X500Principal principal} by
     * {@linkplain X500Principal#X500Principal(String) parsing} a string.
     */
    public X509CertificateBuilder withIssuer(String issuer) {
        if (issuer == null)
            throw new NullPointerException("Null issuer");
        return this.withIssuer(new X500Principal(issuer));
    }

    /* ====================================================================== */

    /**
     * Set the serial number of the issued certificate.
     */
    public X509CertificateBuilder withSerial(BigInteger serial) {
        if (serial == null)
            throw new NullPointerException("Null serial");
        if (serial.signum() != 1)
            throw new NullPointerException("Serial must be positive");
        this.serial = serial;
        return this;
    }

    /**
     * Set the serial number of the issued certificate.
     */
    public X509CertificateBuilder withSerial(long serial) {
        return this.withSerial(BigInteger.valueOf(serial));
    }

    /* ====================================================================== */

    /**
     * Set the <em>not-valid-before</em> date of the issued certificate.
     */
    public X509CertificateBuilder withValidityNotBefore(Date notBefore) {
        if (notBefore == null)
            throw new NullPointerException("Null \"not-before\" date");
        this.notBefore = notBefore;
        return this;
    }

    /**
     * Set the <em>not-valid-before</em> date of the issued certificate (in
     * milliseconds from the Epoch).
     */
    public X509CertificateBuilder withValidityNotBefore(long notBefore) {
        return this.withValidityNotBefore(new Date(notBefore));
    }

    /* ====================================================================== */

    /**
     * Set the <em>not-valid-after</em> date of the issued certificate.
     */
    public X509CertificateBuilder withValidityNotAfter(Date notAfter) {
        if (notAfter == null)
            throw new NullPointerException("Null \"not-after\" date");
        this.notAfter = notAfter;
        return this;
    }

    /**
     * Set the <em>not-valid-after</em> date of the issued certificate (in
     * milliseconds from the Epoch).
     */
    public X509CertificateBuilder withValidityNotAfter(long notAfter) {
        return this.withValidityNotAfter(new Date(notAfter));
    }

    /**
     * Set the <em>not-valid-after</em> date of the issued certificate deriving
     * it from the <em>{@linkplain #withValidityNotBefore(Date) not-valid-before}</em> date
     * and, a duration and {@linkplain TimeUnit time unit}.
     *
     * <p>Obviously the <em>{@linkplain #withValidityNotBefore(Date) not-valid-before}</em>
     * date must be set <b>prior</b> to calling this method.</p>
     */
    public X509CertificateBuilder withValidityNotAfter(long duration, TimeUnit unit) {
        if (notBefore == null)
            throw new IllegalStateException("Date \"not-before\" not yet specified");
        return this.withValidityNotAfter(notBefore.getTime() + MILLISECONDS.convert(duration, unit));
    }

    /* ====================================================================== */

    /**
     * Set the {@linkplain Hash hashing algorithm} to generate the certificate's
     * signature (default {@link Hash#SHA256 SHA256}).
     */
    public X509CertificateBuilder withHash(Hash hash) {
        if (hash == null)
            throw new NullPointerException("Null hashing algorithm");
        this.hash = hash;
        return this;
    }

    /* ====================================================================== */

    /**
     * Set the issuer private key that will be used to sign the certificate.
     */
    public X509CertificateBuilder withIssuerPrivateKey(PrivateKey key) {
        if (key == null)
            throw new NullPointerException("Null issuer private key");
        try {
            issuerPrivateKey = key;
            return this;
        } catch (ClassCastException exception) {
            throw new IllegalArgumentException("Key " + key.getClass().getName() + " is not a private key");
        }
    }

    /**
     * Set the (optional) issuer public key that will be included in the
     * generated certificate.
     */
    public X509CertificateBuilder withIssuerPublicKey(PublicKey key) {
        if (key == null)
            throw new NullPointerException("Null issuer public key");
        try {
            issuerPublicKey = key;
            return this;
        } catch (ClassCastException exception) {
            throw new IllegalArgumentException("Key " + key.getClass().getName() + " is not a public key");
        }
    }

    /**
     * Set the issuer {@linkplain #withIssuerPrivateKey(PrivateKey) private}
     * and {@linkplain #withIssuerPublicKey(PublicKey) public} keys from a
     * {@link KeyPair}.
     */
    public X509CertificateBuilder withIssuerKeyPair(KeyPair keyPair) {
        if (keyPair == null)
            throw new NullPointerException("Null issuer key pair");
        withIssuerPrivateKey(keyPair.getPrivate());
        withIssuerPublicKey(keyPair.getPublic());
        return this;
    }

    /**
     * Set the subject public key that will be included in the generated
     * certificate.
     */
    public X509CertificateBuilder withSubjectPublicKey(PublicKey key) {
        if (key == null)
            throw new NullPointerException("Null subject public key");
        try {
            subjectPublicKey = key;
            return this;
        } catch (ClassCastException exception) {
            throw new IllegalArgumentException("Key " + key.getClass().getName() + " is not a public key");
        }
    }

    /* ====================================================================== */

    /**
     * Clear all alternative names that were set up until now.
     */
    public X509CertificateBuilder clearAlternativeNames() {
        alternativeNames.clear();
        return this;
    }

    /**
     * Add an alternative name in the form of an email address to the
     * generated certificate.
     */
    public X509CertificateBuilder withAlternativeNameEmail(String email) {
        if (email == null)
            throw new NullPointerException("Null email");
        alternativeNames.add(new GeneralName(GeneralName.rfc822Name, email));
        return this;
    }

    /**
     * Add an alternative name in the form of a DNS name (a host name) to the
     * generated certificate.
     */
    public X509CertificateBuilder withAlternativeNameDNS(String dnsName) {
        if (dnsName == null)
            throw new NullPointerException("Null DNS name");
        alternativeNames.add(new GeneralName(GeneralName.dNSName, dnsName));
        return this;
    }

    /**
     * Add an alternative name in the form of an {@link URI} to the
     * generated certificate.
     */
    public X509CertificateBuilder withAlternativeNameURI(String uri) {
        if (uri == null)
            throw new NullPointerException("Null URI");
        return withAlternativeNameURI(URI.create(uri));
    }

    /**
     * Add an alternative name in the form of an {@link URI} to the
     * generated certificate.
     */
    public X509CertificateBuilder withAlternativeNameURI(URL url) {
        if (url == null)
            throw new NullPointerException("Null URL");
        try {
            return withAlternativeNameURI(url.toURI());
        } catch (URISyntaxException exception) {
            throw new IllegalArgumentException("Invalid URI " + url.toString(), exception);
        }
    }

    /**
     * Add an alternative name in the form of an {@link URI} to the
     * generated certificate.
     */
    public X509CertificateBuilder withAlternativeNameURI(URI uri) {
        if (uri == null)
            throw new NullPointerException("Null URI");
        final String string = uri.toASCIIString();
        alternativeNames.add(new GeneralName(GeneralName.uniformResourceIdentifier, string));
        return this;
    }

    /**
     * Add an alternative name in the form of an IP address to the
     * generated certificate.
     */
    public X509CertificateBuilder withAlternativeNameIPAddress(InetAddress address) {
        if (address == null)
            throw new NullPointerException("Null address");
        return this.withAlternativeNameIPAddress(address.getHostAddress());
    }

    /**
     * Add an alternative name in the form of an IP address to the
     * generated certificate.
     *
     * <p>Both IPv4 and IPv6 are supported, and network masks can be specified
     * after a slash character in the string.</p>
     */
    public X509CertificateBuilder withAlternativeNameIPAddress(String address) {
        if (address == null)
            throw new NullPointerException("Null address");
        alternativeNames.add(new GeneralName(GeneralName.iPAddress, address));
        return this;
    }

    /* ====================================================================== */

    /**
     * Clear all CRL distribution points that were set up until now.
     */
    public X509CertificateBuilder clearCRLDistributionPoints() {
        crlDistributionPoints.clear();
        return this;
    }

    /**
     * Add a new CRL distribution point to the generated certificate.
     */
    public X509CertificateBuilder withCrlDistributionPoint(String uri) {
        if (uri == null)
            throw new NullPointerException("Null CRL distribution point");
        return this.withCrlDistributionPoint(URI.create(uri));
    }

    /**
     * Add a new CRL distribution point to the generated certificate.
     */
    public X509CertificateBuilder withCrlDistributionPoint(URL url) {
        if (url == null)
            throw new NullPointerException("Null CRL distribution point");
        try {
            return this.withCrlDistributionPoint(url.toURI());
        } catch (URISyntaxException exception) {
            throw new IllegalArgumentException("Invalid URI " + url.toString(), exception);
        }
    }

    /**
     * Add a new CRL distribution point to the generated certificate.
     */
    public X509CertificateBuilder withCrlDistributionPoint(URI uri) {
        if (uri == null)
            throw new NullPointerException("Null CRL distribution point");
        final String string = uri.toASCIIString();
        crlDistributionPoints.add(new GeneralName(GeneralName.uniformResourceIdentifier, string));
        return this;
    }

    /* ====================================================================== */

    /**
     * Set up this {@link X509CertificateBuilder} to prepare a self-signed
     * certificate.
     *
     * <p>This method will set up the current builder in
     * {@linkplain Mode#SERVER server mode}, use the same
     * {@linkplain X500Principal principal} both for the
     * {@linkplain #withIssuer(X500Principal) issuer} and
     * {@linkplain #withIssuer(X500Principal) subject} and
     * will use the various needed keys from the specified {@link KeyPair},
     * while the serial number will be set to the {@linkplain CRC32 CRC32 hash}
     * of the {@linkplain X500Principal#getEncoded() encoded principal}.</p>
     */
    public X509CertificateBuilder selfSigned(X500Principal principal, KeyPair keyPair) {
        if (principal == null)
            throw new NullPointerException("Null principal for self-signed certificate");
        if (keyPair == null)
            throw new NullPointerException("Null key pair for self-signed certificate");

        /* Set the mode (server) for self-signed certificates */
        withMode(Mode.SERVER);

        /* Set the serial to the CRC32 of the subject */
        final CRC32 crc = new CRC32();
        crc.update(principal.getEncoded());
        this.withSerial(crc.getValue());

        /* Set the subject public key and principal */
        withSubjectPublicKey(keyPair.getPublic());
        this.withSubject(principal);

        /* Set the issuer key pair and principal */
        withIssuerKeyPair(keyPair);
        this.withIssuer(principal);

        /* Dun! */
        return this;
    }

    /**
     * Set up this {@link X509CertificateBuilder} to prepare a self-signed
     * certificate.
     *
     * @see #selfSigned(X500Principal, KeyPair)
     */
    public X509CertificateBuilder selfSigned(String principal, KeyPair keyPair) {
        if (principal == null)
            throw new NullPointerException("Null principal");
        return this.selfSigned(new X500Principal(principal), keyPair);
    }

}