인디노트

Creating X.509 certificates programmatically in Java 본문

인증기술

Creating X.509 certificates programmatically in Java

인디개발자 2018. 7. 27. 19:34

Creating X.509 certificates programmatically in Java

My probem statement was simple: create a X.509 certificate with only a few fields being configurable, sign it with an already existing CA private key/certificate combination, and write the new certificate in PKCS12 format. Then it became complicated: I needed to it with Java, on a PDA.

I spent about 2 days to get this seemingly simple task to work, so I thought it might be good to share my findings in the hope that they will serve others with similar problems.

Update: This page has seemingly found some interest, as I regularly get email asking about specific details. Please feel free to do so, even if I occasionally need quite some time to respond due to the overall volume of email I receive. This page has been translated e.g. by  Jennifer Indurayne.

Why to create certificates with Java?

There are many command-line tools available for creating X.509 certificates, like the keytool distributed with the Sun JDK or openssl, ported to many platforms. So why go through the hassle of doing it with a Java class? I seriously considered using the mature and proven openssl toolkit and simply calling it with Runtime.Exec, passing appropriate parameters and standard input. However, this brings forth serious problems with platforms not using J2SE, like J2ME JVMs on PDAs (often CDC) or even mobile phones (often CLDC). Executing system commands is sometimes supported with CDC, but practically never with CLDC. Additionally, openssl needs a config file that could

  • either be assumed to already exist on the device to use (read: difficult set-up instructions for the application that is actually written in Java, involving the installation of openssl and of an appropriately written config file)
  • or be managed (read: created, modified, and removed) from Java, causing extra complexity.

Thus, openssl is stable and would work on Desktop-like platforms (J2SE), it is challenging especially for more embedded platforms. I decided to bite the apple and try to implement the certificate creation directly in Java.



Java crypto API possibilities

The obvious choice for doing any crypto work with Java is the JCE/JSSE infrastructure included with the more recent versions of J2SE. JCE is, due to historical and now obsolete reasons of (very broken) US export laws, split into infrastructure and provider parts, where J2SE started shipping the default SunJCE provider with version 1.4. But, similar to the Runtime.Exec situation, JCE is hardly supported on JREs targetted for embedded use, so we can't assume it to be available on PDAs or mobile phones. To make matters worse, JCE is not replaceable by third-party libraries anymore, so it's hardly possible to ship JCE libraries with the application.

An alternative for crypto operations with Java is the excellent Bouncycastle library. It can act as a JCE provider, but it also provides a "lightweight" API to its crypto and utility routines that can be used independently from the JCE infrastructure. The classes from Bouncycastle are available under a very liberal license and can be added to any application, thus providing crypto support for platforms that don't have it in their JRE. I decided to implement the certificate generation with the Bouncycastle lightweight API (referred to as BC from now on).

Note: Unfortunately, theorg.bouncycastle.x509.X509Utilclass is not marked public in the Bouncycastle JAR and can therefore not be used from ouside. For the following code to work, it needs to be made public, typically by modifying the source and re-creating the JAR. For the OpenUAT toolkit, I copied a subset of the Bouncycastle classes to the source tree (not changing the package, though) to be able to modify them sufficiently to work under J2ME and to minimize dependencies to save space. One option is therefore to get the latest OpenUAT source and use the Bouncycastle classes included with it. The other is to download the latest Bouncycastle source and do the required modifications.

Step 1: creating a public/private key pair for the new certificate

Creating a new key pair for the certificate is quite straight-forward with both JCE and BC:




            KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
            keyGen.initialize(1024, sr);
            KeyPair keypair = keyGen.generateKeyPair();
            privKey = keypair.getPrivate();
            pubKey = keypair.getPublic();




creates the key pair using JCE, while




            RSAKeyPairGenerator gen = new RSAKeyPairGenerator();
            gen.init(new RSAKeyGenerationParameters(BigInteger.valueOf(3), sr, 1024, 80));
            AsymmetricCipherKeyPair keypair = gen.generateKeyPair();
            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());
            // 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()));




does with BC. Please note that, at the moment, additional code is there to convert the BC key representation to the JCE representation to create the certificate structure and for exporting into PKCS12. I hope to solve these two issues without the help of JCE soon and thus get rid of the conversion lines.

Step 2: creating the new certificate structure

Now, having available the new public key, it can be put into a new X.509 certificate structure. BC actually provides the X509v3CertificateGenerator utility class for this task, but it relies heavily on the JCE infrastructure. Therefore, I am duplicating some of its code, but using only the BC API wherever possible and thus removing dependencies on JCE classes.




        Calendar expiry = Calendar.getInstance();
        expiry.add(Calendar.DAY_OF_YEAR, validityDays);
 
        X509Name x509Name = new X509Name("CN=" + dn);

        V3TBSCertificateGenerator certGen = new V3TBSCertificateGenerator();
        certGen.setSerialNumber(new DERInteger(BigInteger.valueOf(System.currentTimeMillis())));
        certGen.setIssuer(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()));
        TBSCertificateStructure tbsCert = certGen.generateTBSCertificate();




There are a few rather tricky bits in this code snippet:


  • The issuer name is important to get right! If the string is not bit-for-bit correct, verification of the certificate chain will fail later on. I made an earlier error of setting the issuer to new X509Name(caCert.getSubjectDN().getName()), but this provided an incorrect value in the certificate. The PrincipalUtil class is able to produce the correct name.
  • The object identifier describing the type of the following signature is hard-coded here and needs to match the signing code below.
  • It is also quite hard to get the encoding of the public key within the certificate correct. At first, I tried with

        certGen.setSubjectPublicKeyInfo(new SubjectPublicKeyInfo(sigAlgId, pkStruct.toASN1Object()));

    but this was unreadable by openssl. I have yet to find out why only the getEncoded() method of the JCE version of the public key class produces the correct encoding, or, phrased differently, what the correct encoding is.
  • The subject name can be set to a complete X.509 name (e.g. "O=My organization, OE=My organizational unit, C=AT, CN=Rene Mayrhofer/EMAIL=my@nowhere.at"), but I chose to use only the common name (CN) part of it. Who is using the X.500 structure anyway? I am ok with identifying certificates only be a single name.

Step 3: loading the CA private key and certificate

We assume the complete CA, consisting of its self-signed certificate and the associated private key, to be contained in a PKCS12 file. It can be loaded using the JCE KeyStore class:




        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!"); 
        }
        caCert.verify(caCert.getPublicKey());




I have yet to find a way to do it without using JCE classes.



Step 4: signing the certificate

Signing the created certificate structure with the CA key also turned out to be surprisingly tricky due to the involved block encoding:




        SHA1Digest digester = new SHA1Digest();
        AsymmetricBlockCipher rsa = new PKCS1Encoding(new RSAEngine());
        ByteArrayOutputStream   bOut = new ByteArrayOutputStream();
        DEROutputStream         dOut = new DEROutputStream(bOut);
        dOut.writeObject(tbsCert);
        byte[] signature;
        byte[] certBlock = bOut.toByteArray();
        // first create digest
        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);

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




As can be seen, the use of SHA1 and RSA is hard-coded in this block to match the signature id defined in the certificate. The tricky parts here are to use PKCS1 encoding of the RSA signature and ASN1 DER encoding of the digest before signing it. Once reading the responsible code in the BC JCE provider implementation, it's easy, but without that....



Step 5: writing the certificate as PKCS12 file

In the last step, the in-memory Java objects need to be written to a file so that it can be imported on other machine or just into different software packages.




        X509CertificateObject clientCert = new X509CertificateObject(new X509CertificateStructure(new DERSequence(v))); 
        clientCert.verify(caCert.getPublicKey());

        PKCS12BagAttributeCarrier bagCert = clientCert;
        bagCert.setBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_friendlyName,
                new DERBMPString("My frindly name for the new certificate"));
        bagCert.setBagAttribute(
                PKCSObjectIdentifiers.pkcs_9_at_localKeyId,
                new SubjectKeyIdentifierStructure(pubKey));

        KeyStore store = KeyStore.getInstance("PKCS12");
        store.load(null, null);

        X509Certificate[] chain = new X509Certificate[2];
        // first the client, then the CA certificate
        chain[0] = clientCert;
        chain[1] = caCert;
        
        store.setKeyEntry("My friendly name for the new private key", privKey, exportPassword.toCharArray(), chain);

        FileOutputStream fOut = new FileOutputStream(exportFile);
        store.store(fOut, exportPassword.toCharArray());




This code snippet again heavily relies on JCE code in its current implementation, and I need to find a way to replace that. It does not seem to tricky, the only catch is to set the localKeyId attribute so that the importing code can associate the certificate and the private key with each other.

If anybody knows how to read and write PKCS12 files without using JCE code and how to properly encode the public key in the certificate with BC, then please drop me a line.



Complete code

The complete Java class implementing the creating of X.509 certificates can be found here. It implements both the JCE and the BC methods, because JCE providers are often faster than BC due to the use of native code. All usual rules of the GPL apply, and I would certainly appreciate it if you let me know of any extensions, improvements or bug fixes so that I can update my implementation when somebody works with it.

This page was last modified on 2012-12-18


반응형
Comments