Generating X509Certificate using bouncycastle X509v3CertificateBuilder

20,063

I was able to accomplish this with help from Rene Mayrhofer's code. I've provided my implementation which has only be tested in a local testing environment but it appears to work:

package net.jxta.impl.membership.pse;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.SignatureException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPrivateCrtKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.util.Calendar;
import java.util.Date;
import java.util.logging.Logger;

import javax.security.auth.x500.X500Principal;

import org.apache.commons.codec.binary.Hex;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERBMPString;
import org.bouncycastle.asn1.DERBitString;
import org.bouncycastle.asn1.DERInteger;
import org.bouncycastle.asn1.DERNull;
import org.bouncycastle.asn1.DERObjectIdentifier;
import org.bouncycastle.asn1.DEROutputStream;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.DigestInfo;
import org.bouncycastle.asn1.x509.RSAPublicKeyStructure;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.asn1.x509.TBSCertificateStructure;
import org.bouncycastle.asn1.x509.Time;
import org.bouncycastle.asn1.x509.V3TBSCertificateGenerator;
import org.bouncycastle.asn1.x509.X509CertificateStructure;
import org.bouncycastle.asn1.x509.X509Name;
import org.bouncycastle.asn1.x509.X509ObjectIdentifiers;
import org.bouncycastle.crypto.AsymmetricBlockCipher;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.CryptoException;
import org.bouncycastle.crypto.DataLengthException;
import org.bouncycastle.crypto.digests.SHA1Digest;
import org.bouncycastle.crypto.encodings.PKCS1Encoding;
import org.bouncycastle.crypto.engines.RSAEngine;
import org.bouncycastle.crypto.generators.RSAKeyPairGenerator;
import org.bouncycastle.crypto.params.RSAKeyGenerationParameters;
import org.bouncycastle.crypto.params.RSAKeyParameters;
import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters;
import org.bouncycastle.jce.PrincipalUtil;
import org.bouncycastle.jce.interfaces.PKCS12BagAttributeCarrier;
import org.bouncycastle.jce.provider.X509CertificateObject;
import org.bouncycastle.x509.X509Util;
import org.bouncycastle.x509.extension.SubjectKeyIdentifierStructure;

/** This class uses the Bouncycastle lightweight API to generate X.509 certificates programmatically.
 * It assumes a CA certificate and its private key to be available and can sign the new certificate with
 * this CA. Some of the code for this class was taken from 
 * org.bouncycastle.x509.X509V3CertificateGenerator, but adapted to work with the lightweight API instead of
 * JCE (which is usually not available on MIDP2.0). 
 * 
 * @author Rene Mayrhofer
 */
public class X509CertificateGenerator {
    /** Our log4j logger. */
    private static Logger logger = Logger.getLogger(X509CertificateGenerator.class.getName());

    /** This holds the certificate of the CA used to sign the new certificate. The object is created in the constructor. */
    private X509Certificate caCert;
    /** This holds the private key of the CA used to sign the new certificate. The object is created in the constructor. */
    private RSAPrivateCrtKeyParameters caPrivateKey;

    private boolean useBCAPI;
    private boolean useCACert;

    public X509CertificateGenerator(String caFile, String caPassword, String caAlias, boolean useBCAPI) 
            throws KeyStoreException, NoSuchAlgorithmException, CertificateException, FileNotFoundException, IOException, UnrecoverableKeyException, InvalidKeyException, NoSuchProviderException, SignatureException {
        this.useBCAPI = useBCAPI;
        this.useCACert = true;
        logger.info("Loading CA certificate and private key from file '" + caFile + "', using alias '" + caAlias + "' with "
                + (this.useBCAPI ? "Bouncycastle lightweight API" : "JCE API"));
        KeyStore caKs = KeyStore.getInstance("PKCS12");
        caKs.load(new FileInputStream(new File(caFile)), caPassword.toCharArray());

        // load the key entry from the keystore
        Key key = caKs.getKey(caAlias, caPassword.toCharArray());
        if (key == null) {
            throw new RuntimeException("Got null key from keystore!"); 
        }
        RSAPrivateCrtKey privKey = (RSAPrivateCrtKey) key;
        caPrivateKey = new RSAPrivateCrtKeyParameters(privKey.getModulus(), privKey.getPublicExponent(), privKey.getPrivateExponent(),
                privKey.getPrimeP(), privKey.getPrimeQ(), privKey.getPrimeExponentP(), privKey.getPrimeExponentQ(), privKey.getCrtCoefficient());
        // and get the certificate
        caCert = (X509Certificate) caKs.getCertificate(caAlias);
        if (caCert == null) {
            throw new RuntimeException("Got null cert from keystore!"); 
        }
        logger.info("Successfully loaded CA key and certificate. CA DN is '" + caCert.getSubjectDN().getName() + "'");
        caCert.verify(caCert.getPublicKey());
        logger.info("Successfully verified CA certificate with its own public key.");
    }
    public X509CertificateGenerator(boolean useBCAPI) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, FileNotFoundException, IOException, UnrecoverableKeyException, InvalidKeyException, NoSuchProviderException, SignatureException {
        this.useBCAPI = useBCAPI;
        this.useCACert = false;
    }

    public X509Certificate createCertificate(String dn, int validityDays, X500Principal issuer, KeyPair keypairca) throws 
            IOException, InvalidKeyException, SecurityException, SignatureException, NoSuchAlgorithmException, DataLengthException, CryptoException, KeyStoreException, NoSuchProviderException, CertificateException, InvalidKeySpecException {
        logger.info("Generating certificate for distinguished subject name '" + 
                dn + "', valid for " + validityDays + " days");

        RSAPrivateCrtKey pK = (RSAPrivateCrtKey) keypairca.getPrivate();
        caPrivateKey = new RSAPrivateCrtKeyParameters(pK.getModulus(), pK.getPublicExponent(), pK.getPrivateExponent(),
                pK.getPrimeP(), pK.getPrimeQ(), pK.getPrimeExponentP(), pK.getPrimeExponentQ(), pK.getCrtCoefficient());

        SecureRandom sr = new SecureRandom();

        PublicKey pubKey;
        PrivateKey privKey;

        logger.info("Creating RSA keypair");
        // generate the keypair for the new certificate
        if (useBCAPI) {
            RSAKeyPairGenerator gen = new RSAKeyPairGenerator();
            gen.init(new RSAKeyGenerationParameters(BigInteger.valueOf(3), sr, 1024, 80));
            AsymmetricCipherKeyPair keypair = gen.generateKeyPair();
            logger.info("Generated keypair, extracting components and creating public structure for certificate");
            RSAKeyParameters publicKey = (RSAKeyParameters) keypair.getPublic();
            RSAPrivateCrtKeyParameters privateKey = (RSAPrivateCrtKeyParameters) keypair.getPrivate();
            // used to get proper encoding for the certificate
            RSAPublicKeyStructure pkStruct = new RSAPublicKeyStructure(publicKey.getModulus(), publicKey.getExponent());
            logger.info("New public key is '" + new String(Hex.encodeHex(pkStruct.getEncoded())) + 
                    ", exponent=" + publicKey.getExponent() + ", modulus=" + publicKey.getModulus());
            // JCE format needed for the certificate - because getEncoded() is necessary...
            pubKey = KeyFactory.getInstance("RSA").generatePublic(
                    new RSAPublicKeySpec(publicKey.getModulus(), publicKey.getExponent()));
            // and this one for the KeyStore
            privKey = KeyFactory.getInstance("RSA").generatePrivate(
                    new RSAPrivateCrtKeySpec(publicKey.getModulus(), publicKey.getExponent(),
                            privateKey.getExponent(), privateKey.getP(), privateKey.getQ(), 
                            privateKey.getDP(), privateKey.getDQ(), privateKey.getQInv()));
        }
        else {
            // this is the JSSE way of key generation
            KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
            keyGen.initialize(1024, sr);
            KeyPair keypair = keyGen.generateKeyPair();
            privKey = keypair.getPrivate();
            pubKey = keypair.getPublic();
        }

        Calendar expiry = Calendar.getInstance();
        expiry.add(Calendar.DAY_OF_YEAR, validityDays);

        X509Name x509Name = new X509Name("CN=" + dn);
        X509Name x509Issuer = new X509Name(issuer.getName());
        V3TBSCertificateGenerator certGen = new V3TBSCertificateGenerator();
        certGen.setSerialNumber(new DERInteger(BigInteger.valueOf(System.currentTimeMillis())));
        certGen.setIssuer(x509Issuer);//issuer.getName());//PrincipalUtil.getSubjectX509Principal(caCert));
        certGen.setSubject(x509Name);
        DERObjectIdentifier sigOID = X509Util.getAlgorithmOID("SHA1WithRSAEncryption");
        AlgorithmIdentifier sigAlgId = new AlgorithmIdentifier(sigOID, new DERNull());
        certGen.setSignature(sigAlgId);
        certGen.setSubjectPublicKeyInfo(new SubjectPublicKeyInfo((ASN1Sequence)new ASN1InputStream(
                new ByteArrayInputStream(pubKey.getEncoded())).readObject()));
        certGen.setStartDate(new Time(new Date(System.currentTimeMillis())));
        certGen.setEndDate(new Time(expiry.getTime()));

        logger.info("Certificate structure generated, creating SHA1 digest");
        // attention: hard coded to be SHA1+RSA!
        SHA1Digest digester = new SHA1Digest();
        AsymmetricBlockCipher rsa = new PKCS1Encoding(new RSAEngine());
        TBSCertificateStructure tbsCert = certGen.generateTBSCertificate();

        ByteArrayOutputStream   bOut = new ByteArrayOutputStream();
        DEROutputStream         dOut = new DEROutputStream(bOut);
        dOut.writeObject(tbsCert);

        // and now sign
        byte[] signature;
        if (useBCAPI) {
            byte[] certBlock = bOut.toByteArray();
            // first create digest
            logger.info("Block to sign is '" + new String(Hex.encodeHex(certBlock)) + "'");     
            digester.update(certBlock, 0, certBlock.length);
            byte[] hash = new byte[digester.getDigestSize()];
            digester.doFinal(hash, 0);
            // and sign that
            rsa.init(true, caPrivateKey);
            DigestInfo dInfo = new DigestInfo( new AlgorithmIdentifier(X509ObjectIdentifiers.id_SHA1, null), hash);
            byte[] digest = dInfo.getEncoded(ASN1Encodable.DER);
            signature = rsa.processBlock(digest, 0, digest.length);
        }
        else {
            // or the JCE way
            PrivateKey caPrivKey = KeyFactory.getInstance("RSA").generatePrivate(
                    new RSAPrivateCrtKeySpec(caPrivateKey.getModulus(), caPrivateKey.getPublicExponent(),
                            caPrivateKey.getExponent(), caPrivateKey.getP(), caPrivateKey.getQ(), 
                            caPrivateKey.getDP(), caPrivateKey.getDQ(), caPrivateKey.getQInv()));

            Signature sig = Signature.getInstance(sigOID.getId());
            sig.initSign(caPrivKey, sr);
            sig.update(bOut.toByteArray());
            signature = sig.sign();
        }
        logger.info("SHA1/RSA signature of digest is '" + new String(Hex.encodeHex(signature)) + "'");

        // and finally construct the certificate structure
        ASN1EncodableVector  v = new ASN1EncodableVector();

        v.add(tbsCert);
        v.add(sigAlgId);
        v.add(new DERBitString(signature));

        X509CertificateObject clientCert = new X509CertificateObject(new X509CertificateStructure(new DERSequence(v))); 
        logger.info("Verifying certificate for correct signature with CA public key");

        if(useCACert) clientCert.verify(caCert.getPublicKey());

        return clientCert;
    }


}

Share:
20,063
Chuck D
Author by

Chuck D

20 year IT professional

Updated on July 09, 2022

Comments

  • Chuck D
    Chuck D almost 2 years

    I'm attempting to port JXTA to run on App Engine. Given that the BouncyCastle "BC" provider is not yet supported on App Engine, I have to port the existing JXTA code to generate a X509Certificate using white-listed classes. My knowledge of Crypto is minimal and i'm not certain that what i'm trying to accomplish is even possible. Here is the original code from PSEUtils.java from the JXTA project:

    PSEUtils.java

    There's a helper class which contains the java.security.cert.X509Certificate:

    public static class IssuerInfo {
        public X509Certificate cert; // subject Cert
        public PrivateKey subjectPkey; // subject private key
        public X509Certificate issuer; // issuer Cert
        public PrivateKey issuerPkey; // issuer private key
    }
    

    In the method:

    public static IssuerInfo genCert(X500Principal subject, KeyPair keypair, IssuerInfo issuerinfo)
    

    I'm passing in subject as:

    new X500Principal("CN="+useCN)
    

    keypair as (from original code):

    KeyPairGenerator g = KeyPairGenerator.getInstance("RSA");
    g.initialize(1024, UTILS.srng);
    KeyPair keypair = g.generateKeyPair();
    

    and the jxta coded IssuerInfo.

    Now since I can't pull in the bouncycastle.jce packages, I've had to remove the X509Principal and X509V3CertificateGenerator code that JXTA uses and attempt to replace it with an implementation that complies with the GAE restrictions. Here is what I currently have for the genCert method using org.bouncycastle.X509.X509v3CertificateBuilder.

    SubjectPublicKeyInfo subPubKeyInfo =  SubjectPublicKeyInfo.getInstance(keypair.getPublic().getEncoded());
    
    X509v3CertificateBuilder v3CertGen = new X509v3CertificateBuilder(
         new X500Name(issuer.getName()), 
         BigInteger.ONE, 
         today, until, 
         new X500Name(subject.getName()), 
         subPubKeyInfo);
    

    The problem is that I can't get keypair.getPublic().getEncoded() to work with the SubjectPublicKeyInfo.getInstance() method. Throws java.lang.IllegalArgumentException: unknown object in factory: [B

    Public Key appears to be populated upon inspection:

    Sun RSA public key, 1024 bits
      modulus: 117521430893506212334140912845641570591161279468597426442875306202350445904550279678434051874985419676760802566018092318362676224355315431299979507080364677679613392086245588766565617009250512996843008784370448997729071786062596049780632058501646041736216482596596901215941577208285499619376322050871534546271
      public exponent: 65537
    

    I've found the following SO link which demonstrates this code working:

    Sign CSR using Bouncy Castle

    My attempt to convert genCert is below but for some reason, I can't get past creating SubjectPublicKeyInfo from the encoded public key?

    Any help is greatly appreciated.

    public static IssuerInfo genCert(X500Principal subject, KeyPair keypair, IssuerInfo issuerinfo)  {
        IssuerInfo info = new IssuerInfo();
        try {
            // set up issuer
            PrivateKey signer;
            X500Principal issuer;
    
            if (null == issuerinfo) { // self-signed root cert
                signer = keypair.getPrivate();
                issuer = new X500Principal(subject.getEncoded());
            } else { // issuer signed service sert
                signer = issuerinfo.subjectPkey;
                X500Principal issuer_subject = issuerinfo.cert.getSubjectX500Principal();
                issuer = new X500Principal(issuer_subject.getEncoded());
            }
    
            // set validity 10 years from today
            Date today = new Date();
            Calendar cal = Calendar.getInstance();
    
            cal.setTime(today);
            cal.add(Calendar.YEAR, 10);
            Date until = cal.getTime();
    
            SubjectPublicKeyInfo subPubKeyInfo =  SubjectPublicKeyInfo.getInstance(keypair.getPublic().getEncoded());
    
        //**Can't get here so i'm not sure if the rest of this works?**
    
            AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA1withRSA");
            AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId);
    
            RSAPrivateCrtKeyParameters cps = (RSAPrivateCrtKeyParameters) keypair.getPrivate();
            ContentSigner sigGen = new BcRSAContentSignerBuilder(sigAlgId, digAlgId).build(cps);            
    
         X509CertificateHolder certHolder = v3CertGen.build(sigGen);
    
         CertificateFactory cf = CertificateFactory.getInstance("X.509");
    
         // Read user Certificate
         InputStream is1 = new ByteArrayInputStream(certHolder.getEncoded());
         X509Certificate eeCert = (X509Certificate) cf.generateCertificate(is1);
         is1.close();
    
  • RaceBase
    RaceBase about 11 years
    Could you please update this class as many of these methods/classes are now deprecated in BC?
  • Chuck D
    Chuck D about 11 years
    BC is now supported in App Engine so it's not needed.
  • Phil
    Phil over 10 years
    @ChukDiesel How about for those of us not using App Engine? Please?
  • Praveen Prajapati
    Praveen Prajapati about 3 years
    Not entirely sure but if the question involves X509V3CertificateBuilder, isn't the solution supposed to at least contain that string anywhere - if only in a comment?
  • Chuck D
    Chuck D almost 3 years
    This question was answered back in '12. Please be proactive in your resolution moving forward.