/* * 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(); } }