org.ccnx.ccn.impl.security.crypto.util.MinimalCertificateGenerator.java Source code

Java tutorial

Introduction

Here is the source code for org.ccnx.ccn.impl.security.crypto.util.MinimalCertificateGenerator.java

Source

/*
 * Part of the CCNx Java Library.
 *
 * Copyright (C) 2008, 2009 Palo Alto Research Center, Inc.
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version 2.1
 * as published by the Free Software Foundation. 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details. You should have received
 * a copy of the GNU Lesser General Public License along with this library;
 * if not, write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301 USA.
 */

package org.ccnx.ccn.impl.security.crypto.util;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.net.URI;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.SimpleTimeZone;
import java.util.Vector;

import javax.security.auth.x500.X500Principal;

import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.DERObjectIdentifier;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
import org.bouncycastle.asn1.x509.X509Extensions;
import org.bouncycastle.asn1.x509.X509Name;
import org.bouncycastle.x509.X509V3CertificateGenerator;
import org.ccnx.ccn.impl.support.DataUtils;
import org.ccnx.ccn.impl.support.Log;

/**
 * Wrap BouncyCastle's X.509 certificate generator in a slightly more user-friendly way.
 */
public class MinimalCertificateGenerator {

    /**
      * A few useful OIDs that aren't in X509Extension, plus those that
      * are (because they're protected there).
      */
    public static final DERObjectIdentifier id_kp_serverAuth = new DERObjectIdentifier("1.3.6.1.5.5.7.3.1");
    public static final DERObjectIdentifier id_kp_clientAuth = new DERObjectIdentifier("1.3.6.1.5.5.7.3.2");
    public static final DERObjectIdentifier id_kp_emailProtection = new DERObjectIdentifier("1.3.6.1.5.5.7.3.4");
    public static final DERObjectIdentifier id_kp_ipsec = new DERObjectIdentifier("1.3.6.1.5.5.8.2.2");

    /**
    *  We can't just use null to get the default provider
    *  and have any assurance of what it is, as a user
    *  can change the default provider.
    */
    public static final String SUN_PROVIDER = "SUN";

    /**
     *  SHA is the official JCA name for SHA1
     */
    protected static final String DEFAULT_DIGEST_ALGORITHM = "SHA";

    /**
     * Cache a random number generator (non-secure, used for generating
     * certificate serial numbers.)
     */
    protected static Random cachedRandom = new Random();

    protected static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyMMddHHmmss");
    protected static SimpleTimeZone TZ = new SimpleTimeZone(0, "Z");

    public static long MSEC_IN_YEAR = 1000 * 60 * 60 * 24 * 365;

    static {
        DATE_FORMAT.setTimeZone(TZ);
    }

    protected X509V3CertificateGenerator _generator = new X509V3CertificateGenerator();
    /**
     * Cons up a list of EKUs and SubjectAltNames, then add them en masse just before signing.
     */
    protected Vector<DERObjectIdentifier> _ekus = new Vector<DERObjectIdentifier>();
    protected ASN1EncodableVector _subjectAltNames = new ASN1EncodableVector();
    protected AuthorityKeyIdentifier _aki = null;

    /**
     * Generates a X509 certificate for a specified user , 
     * subject distinguished name and duration.
     * @param userKeyPair the user key pair.
     * @param subjectDN the distinguished name of the user.
     * @param duration the duration of validity of the certificate.
     * @return the X509 certificate.
     * @throws CertificateEncodingException
     * @throws InvalidKeyException
     * @throws IllegalStateException
     * @throws NoSuchAlgorithmException
     * @throws SignatureException
     */
    public static X509Certificate GenerateUserCertificate(PublicKey userPublicKey, String subjectDN, long duration,
            PrivateKey signingKey) throws CertificateEncodingException, InvalidKeyException, IllegalStateException,
            NoSuchAlgorithmException, SignatureException {
        MinimalCertificateGenerator mg = new MinimalCertificateGenerator(subjectDN, userPublicKey, duration, false,
                false);
        mg.setClientAuthenticationUsage();
        return mg.sign(null, signingKey);
    }

    /**
     * Generates a X509 certificate for a specified user , 
     * subject distinguished name and duration.
     * @param userKeyPair the user key pair.
     * @param subjectDN the distinguished name of the user.
     * @param duration the duration of validity of the certificate.
     * @return the X509 certificate.
     * @throws CertificateEncodingException
     * @throws InvalidKeyException
     * @throws IllegalStateException
     * @throws NoSuchAlgorithmException
     * @throws SignatureException
     * @throws IOException 
     */
    public static X509Certificate GenerateUserCertificate(String subjectDN, PublicKey userPublicKey,
            X509Certificate issuerCertificate, long duration, PrivateKey signingKey)
            throws CertificateEncodingException, InvalidKeyException, IllegalStateException,
            NoSuchAlgorithmException, SignatureException, IOException {
        MinimalCertificateGenerator mg = new MinimalCertificateGenerator(subjectDN, userPublicKey,
                issuerCertificate, duration, false, null, false);
        mg.setClientAuthenticationUsage();
        return mg.sign(null, signingKey);
    }

    /**
     * Helper method
     */
    public static X509Certificate GenerateUserCertificate(KeyPair userKeyPair, String subjectDN, long duration)
            throws CertificateEncodingException, InvalidKeyException, IllegalStateException,
            NoSuchAlgorithmException, SignatureException {
        return GenerateUserCertificate(userKeyPair.getPublic(), subjectDN, duration, userKeyPair.getPrivate());
    }

    /**
     * Generates an X509 certificate for a specified user key,
     * subject distinguished name, email address and duration.
     * @param userKeyPair the user key pair.
     * @param subjectDN the distinguished name of the subject.
     * @param emailAddress the email address.
     * @param duration the validity duration of the certificate.
     * @return the X509 certificate.
     * @throws CertificateEncodingException
     * @throws InvalidKeyException
     * @throws IllegalStateException
     * @throws NoSuchAlgorithmException
     * @throws SignatureException
     */
    public static X509Certificate GenerateUserCertificate(PublicKey userPublicKey, String subjectDN,
            String emailAddress, long duration, PrivateKey signingKey) throws CertificateEncodingException,
            InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, SignatureException {
        MinimalCertificateGenerator mg = new MinimalCertificateGenerator(subjectDN, userPublicKey, duration, false,
                false);
        mg.setClientAuthenticationUsage();
        mg.setSecureEmailUsage(emailAddress);
        return mg.sign(null, signingKey);
    }

    /**
     * Helper method
     */
    public static X509Certificate GenerateUserCertificate(KeyPair userKeyPair, String subjectDN,
            String emailAddress, long duration) throws CertificateEncodingException, InvalidKeyException,
            IllegalStateException, NoSuchAlgorithmException, SignatureException {
        return GenerateUserCertificate(userKeyPair.getPublic(), subjectDN, emailAddress, duration,
                userKeyPair.getPrivate());
    }

    /**
     * Certificate issued under an existing CA.
     * @param subjectDN the distinguished name of the subject.
     * @param subjectPublicKey the public key of the subject.
     * @param issuerCertificate the certificate of the issuer.
     * @param duration the validity duration of the certificate.
     * @param isCA 
     * @param allUsage if isCA is true, add "regular" KeyUsage flags, for dual-use cert
     * @throws CertificateEncodingException
     * @throws IOException
     */
    public MinimalCertificateGenerator(String subjectDN, PublicKey subjectPublicKey,
            X509Certificate issuerCertificate, long duration, boolean isCA, Integer chainLength, boolean allUsage)
            throws CertificateEncodingException, IOException {

        this(subjectDN, subjectPublicKey, issuerCertificate.getSubjectX500Principal(), duration, isCA, chainLength,
                allUsage);
        // Pull the existing subject identifier out of the issuer cert. 
        byte[] subjectKeyID = issuerCertificate.getExtensionValue(X509Extensions.SubjectKeyIdentifier.toString());
        if (null == subjectKeyID) {
            subjectKeyID = CryptoUtil.generateKeyID(subjectPublicKey);

        } else {
            // content of extension is wrapped in a DEROctetString
            DEROctetString content = (DEROctetString) CryptoUtil.decode(subjectKeyID);
            byte[] encapsulatedOctetString = content.getOctets();
            DEROctetString octetStringKeyID = (DEROctetString) CryptoUtil.decode(encapsulatedOctetString);
            subjectKeyID = octetStringKeyID.getOctets();
        }
        _aki = new AuthorityKeyIdentifier(subjectKeyID);
    }

    /**
     * Self-signed certificate (which may or may not be a CA).
     * @param subjectDN the distinguished name of the subject.
     * @param subjectPublicKey the public key of the subject.
     * @param duration the validity duration of the certificate.
     * @param isCA add basic constraints
     * @param allUsage if isCA is true, add "regular" KeyUsage flags, for dual-use cert
     */
    public MinimalCertificateGenerator(String subjectDN, PublicKey subjectPublicKey, long duration, boolean isCA,
            boolean allUsage) {

        this(subjectDN, subjectPublicKey, new X500Principal(subjectDN), duration, isCA, null, allUsage);
        // This needs to match what we are using for a subject key identifier.
        _aki = new AuthorityKeyIdentifier(CryptoUtil.generateKeyID(subjectPublicKey));
    }

    /**
     * Basic common path.
     * @param subjectDN the distinguished name of the subject.
     * @param subjectPublicKey the public key of the subject.
     * @param issuerDN the distinguished name of the issuer.
     * @param duration the validity duration of the certificate.
     * @param isCA
     * @param allUsage if isCA is true, add "regular" KeyUsage flags, for dual-use cert
     */
    public MinimalCertificateGenerator(String subjectDN, PublicKey subjectPublicKey, X500Principal issuerDN,
            long duration, boolean isCA, Integer chainLength, boolean allUsage) {

        _generator.setSubjectDN(new X509Name(subjectDN));
        _generator.setIssuerDN(issuerDN);
        _generator.setSerialNumber(new BigInteger(64, cachedRandom));
        _generator.setPublicKey(subjectPublicKey);

        Date startTime = new Date();
        Date stopTime = new Date(startTime.getTime() + duration);
        _generator.setNotBefore(startTime);
        _generator.setNotAfter(stopTime);

        // CA key usage
        final int caKeyUsage = KeyUsage.digitalSignature | KeyUsage.nonRepudiation | KeyUsage.keyCertSign
                | KeyUsage.cRLSign;
        // Non-CA key usage
        final int nonCAKeyUsage = KeyUsage.digitalSignature | KeyUsage.nonRepudiation | KeyUsage.keyEncipherment
                | KeyUsage.dataEncipherment | KeyUsage.keyAgreement;

        int ourUsage;
        if (isCA) {
            if (!allUsage) {
                ourUsage = caKeyUsage;
            } else {
                ourUsage = caKeyUsage | nonCAKeyUsage;
            }
        } else {
            ourUsage = nonCAKeyUsage;
        }
        _generator.addExtension(X509Extensions.KeyUsage, false, new KeyUsage(ourUsage));

        BasicConstraints bc = ((isCA == false) || (null == chainLength)) ? new BasicConstraints(isCA)
                : new BasicConstraints(chainLength.intValue());
        _generator.addExtension(X509Extensions.BasicConstraints, true, bc);

        SubjectKeyIdentifier ski = new SubjectKeyIdentifier(CryptoUtil.generateKeyID(subjectPublicKey));
        _generator.addExtension(X509Extensions.SubjectKeyIdentifier, false, ski);
    }

    /**
     * Both adds the server authentication OID to the EKU
     * extension, and adds the DNS name to the subject alt name
     * extension (not marked critical). (Combines addServerAuthenticationEKU and
     * addDNSNameSubjectAltName).
     * @param serverDNSName the DNS name of the server.
     */
    public void setServerAuthenticationUsage(String serverDNSName) {
        GeneralName name = new GeneralName(GeneralName.dNSName, serverDNSName);
        _subjectAltNames.add(name);
        _ekus.add(id_kp_serverAuth);
    }

    /**
     * Adds client authentication as a usage for this
     * certificate.
     */
    public void setClientAuthenticationUsage() {
        _ekus.add(id_kp_clientAuth);
    }

    /**
     * Both adds the secure email OID to the EKU
     * extension, and adds the email address to the subject alt name
     * extension (not marked critical). (Combines addSecureEmailEKU and addEmailSubjectAltName).
     * @param subjectEmailAddress the email address of the subject.
     */
    public void setSecureEmailUsage(String subjectEmailAddress) {
        GeneralName name = new GeneralName(GeneralName.rfc822Name, subjectEmailAddress);
        _subjectAltNames.add(name);
        _ekus.add(id_kp_emailProtection);
    }

    /**
     * Adds ip address to subjectAltName and IPSec usage to EKU
     * @param ipAddress string form of the IP address. Assumed to be in either
     * IPv4 form, "n.n.n.n", with 0<=n<256, orIPv6 form, 
     * "n.n.n.n.n.n.n.n", where the n's are the HEXADECIMAL form of the
     * 16-bit address components.
     **/
    public void setIPSecUsage(String ipAddress) {
        GeneralName name = new GeneralName(GeneralName.iPAddress, ipAddress);
        _subjectAltNames.add(name);
        _ekus.add(id_kp_ipsec);
    }

    public void setExtendedKeyUsage(String usageOID) {
        DERObjectIdentifier oid = new DERObjectIdentifier(usageOID);
        _ekus.add(oid);
    }

    public void addSubjectAltName(URI subjectURI) {
        GeneralName name = new GeneralName(GeneralName.uniformResourceIdentifier, subjectURI.toString());
        _subjectAltNames.add(name);
    }

    /**
     * Add additional AuthorityKeyIdentifier information. We've already set
     * key ID.
     */
    public void addAuthorityName(URI authorityName) {
        if (null == _aki) {
            _aki = new AuthorityKeyIdentifier(null, null, null);
        }
        _aki.setIssuerName(authorityName);
    }

    /**
     * Generate an X509 certificate, based on the current issuer and subject using the default provider.
     * Use the old form of the BC certificate generation call for compatibility with older versions
     * of BouncyCastle; suppress the deprecation warning on newer platforms.
     * @param digestAlgorithm the digest algorithm.
     * @param signingKey the signing key.
     * @return the X509 certificate.
     * @throws CertificateEncodingException
     * @throws InvalidKeyException
     * @throws IllegalStateException
     * @throws NoSuchAlgorithmException
     * @throws SignatureException
     */
    @SuppressWarnings("deprecation")
    public X509Certificate sign(String digestAlgorithm, PrivateKey signingKey) throws CertificateEncodingException,
            InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, SignatureException {
        /**
         * Finalize extensions.
         */
        addExtendedKeyUsageExtension();
        addSubjectAltNamesExtension();
        addAuthorityKeyIdentifierExtension();

        if (null == digestAlgorithm)
            digestAlgorithm = DEFAULT_DIGEST_ALGORITHM;

        String signatureAlgorithm = OIDLookup.getSignatureAlgorithm(digestAlgorithm, signingKey.getAlgorithm());
        if (null == signatureAlgorithm) {
            Log.warning("Cannot find signature algorithm for digest " + digestAlgorithm + " and key "
                    + signingKey.getAlgorithm() + ".");
        }
        _generator.setSignatureAlgorithm(signatureAlgorithm);

        // Move back to the older form to allow compatibility with BC 1.34
        //return _generator.generate(signingKey);
        return _generator.generateX509Certificate(signingKey);
    }

    /**
     * Adds an extended key usage extension to the certificate.
     */
    protected void addExtendedKeyUsageExtension() {
        if (_ekus.isEmpty())
            return;
        ExtendedKeyUsage eku = new ExtendedKeyUsage(_ekus);
        _generator.addExtension(X509Extensions.ExtendedKeyUsage, false, eku);
    }

    /**
     * Adds an authority key identifier extension to the certificate.
     */
    protected void addAuthorityKeyIdentifierExtension() {
        if (null == _aki)
            return;
        _generator.addExtension(X509Extensions.AuthorityKeyIdentifier, false, _aki);
    }

    /**
     * Adds an subject alternative name extension to the certificate.
     */
    protected void addSubjectAltNamesExtension() {
        if (_subjectAltNames.size() == 0)
            return;
        GeneralNames genNames = new GeneralNames(new DERSequence(_subjectAltNames));
        _generator.addExtension(X509Extensions.SubjectAlternativeName, false, genNames);
    }

    /**
     * Open up the ability to add additional extensions that aren't 
     * EKU or SubjectAltName (which we manage).
     */
    public void addExtension(String oid, boolean critical, byte[] value) {
        if (null == oid)
            throw new IllegalArgumentException("OID cannot be null!");

        DERObjectIdentifier derOID = new DERObjectIdentifier(oid);
        if ((derOID.equals(X509Extensions.ExtendedKeyUsage))
                || (derOID.equals(X509Extensions.SubjectAlternativeName))
                || (derOID.equals(X509Extensions.AuthorityKeyIdentifier))) {
            throw new IllegalArgumentException(
                    "Cannot use addExtension to set ExtendedKeyUsage or SubjectAlternativeName or AuthorityKeyIdentifier!");
        }
        _generator.addExtension(derOID, critical, value);
    }

    /**
     * Writes file of certificates in the form expected by SSL_CTX_load_verify_locations()
     * and (if in the right order) SSL_CTX_use_certificate_chain_file
     * Quoting from the OpenSSL documentation:
     * If CAfile is not NULL, it points to a file of CA certificates in PEM format. 
     * The file can contain several CA certificates identified by
     * -----BEGIN CERTIFICATE-----
     * ... (CA certificate in base64 encoding) ...
     * -----END CERTIFICATE-----
     * sequences. Before, between, and after the certificates text is allowed 
     * which can be used e.g. for descriptions of the certificates.
     * 
     * From documentation: SSL_CTX_use_certificate_chain_file loads a 
     * certificate chain from file into ctx. The certificates must be in 
     * PEM format and must be sorted starting with the subject's 
     * certificate (actual client or server certificate), followed by 
     * intermediate CA certificates if applicable, and ending at the 
     * highest level (root) CA. There is no corresponding function 
     * working on a single SSL object.
     * This method assumes the caller already has ordered the chain.
     * @param userDirectory
     * @param userCertificate if not null, the first cert to write in the chain
     * @param chain a set of certificates to write after any user certificate. Written
     *   in order given, can be used to write an ordered chain or a set of roots where
     *   order doesn't matter.
     * @param chainOffset the index into chain to start writing
     * @param chainCount the number of certs to output.
     * @throws CertificateEncodingException 
     * @throws FileNotFoundException 
     */
    public static void writeCertificateChain(File targetFile, X509Certificate userCertificate,
            List<X509Certificate> chain, int chainOffset, int chainCount)
            throws CertificateEncodingException, FileNotFoundException {
        targetFile.getParentFile().mkdirs();
        PrintWriter writer = new PrintWriter(targetFile.getAbsolutePath());
        if (null != userCertificate) {
            writePEMCertificate(writer, userCertificate);
        }
        for (int i = chainOffset; i < chainOffset + chainCount; ++i) {
            writePEMCertificate(writer, chain.get(i));
        }
        writer.close();
    }

    public static final String BEGIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----";
    public static final String END_CERTIFICATE = "-----END CERTIFICATE-----";
    public static final int CERTIFICATE_WRAP_LENGTH = 40; // TODO what should this be?

    public static void writePEMCertificate(PrintWriter writer, X509Certificate certificate)
            throws CertificateEncodingException {
        writer.println(BEGIN_CERTIFICATE);
        // TODO print or println?
        writer.println(DataUtils.base64Encode(certificate.getEncoded(), CERTIFICATE_WRAP_LENGTH));
        writer.println(END_CERTIFICATE);
        writer.println();
    }
}