Signing a PDF with Amazon CloudHSM

Using the Amazon CloudHSM with Java to sign a PDF

Amazon Web Services offer a "Cloud HSM" service, which is a FIPS-140 certificate HSM device that can be used to digitally sign or encrypt. This article is a quick walkthrough on how to use one to digitally sign a PDF.

We're going to make the following assumptions:

  1. All the steps up to and including https://docs.aws.amazon.com/cloudhsm/latest/userguide/java-library-install.html have been done, and that the HSM_PARTITION, HSM_PASSWORD and HSM_USER environment variables have been set to allow you to access the HSM, as described in that document.
  2. You want to use a key that can not be extracted from the HSM. This is optional with the Cavium HSM used by Amazon, but - frankly - given the amount of effort involved in using an HSM, exporting the key so you could do the whole thing in software seems a little pointless to us. It's also unlikely such a key would meet the requirements of any AATL CA.

The Amazon Cloud HSM has two Keystore "faces" you can use. The first is the Cavium KeyStore type, which is a direct interface to the CloudHSM device. This type is unable to store X.509 certificates, which we need for for signing a PDF. The second is the CloudHSM KeyStore type, which is effectively a wrapper around the Cavium type that also accepts X.509 certificates: private keys are retrieved from the HSM, and X.509 Certificates are stored in a PKCS#12 file on the local filesystem. This is the type we'll need to use. The use of a KeyStore interface means most of the other articles we've written on this topic continue to apply here.

So here's what we did. There may be other approaches that worked, but we certainly tried quite a few that didn't, so to get things going we'd suggest following this exactly. This article is written in December 2020, the API may change in the future.

  1. Create a new Key and Certificate. You cannot do this with the standard "keytool", as it will create an extractable key. We also could not do this with OpenSSL, nor by generating a key first, say with key_mgmt_util, then adding a Certificate later (although the API hints that this might be possible).

    What you can do is create a new Key in Java, setting the correct option to make the key non-extractable, and generate a self-signed certificate to go with it at the same time. This pair can be stored in the KeyStore. Once you've done that you can create a CSR for that pair, which we'll also do here as part of the same step - although it can be generated after the certificate has been created. The KeyStore will allow you to replace a Certificate with a new one without error.

    We use the BouncyCastle API to generate the certiificate and CSR - you'll need the Jars bcpkix-jdk15on-NNN.jar and bcprov-jdk15on-NNN.jar (we tested with version 1.67)

    import com.cavium.key.*;
    import com.cavium.key.parameter.*;
    import java.io.*;
    import java.math.*;
    import java.util.*;
    import java.security.*;
    import java.security.cert.*;
    import java.security.cert.Certificate;
    import java.security.interfaces.RSAPrivateKey;
    import java.security.interfaces.RSAPublicKey;
    import javax.crypto.KeyGenerator;
    import org.bouncycastle.asn1.x500.X500Name;
    import org.bouncycastle.pkcs.*;
    import org.bouncycastle.pkcs.jcajce.*;
    import org.bouncycastle.cert.jcajce.*;
    import org.bouncycastle.openssl.jcajce.*;
    import org.bouncycastle.operator.jcajce.*;
    import org.bouncycastle.operator.ContentSigner;
    
    public class GenKey {
      public static void main(String[] args) throws Exception {
        String keyStoreFile = "testkeystore";   // Name of new KeyStore file to generate
        String alias = "testkey";           // alias of key in that keystore
        char[] password = "secret".toCharArray();   // password to use
        String dn = "CN=Test Tester, C=GB"; // DN for the identity you're creating
        int bits = 2048;                    // Number of bits
        String alg = "SHA256WithRSA";       // Certificate signature algorithm
        boolean extractable = false;        // This key will not be extractable
    
        Provider provider = new com.cavium.provider.CaviumProvider();
        Security.addProvider(provider);
        KeyStore keyStore = KeyStore.getInstance("CloudHSM");
        keyStore.load(null, null);
    
        // 1. Generate KeyPair with a non-extractable key
        System.err.println("Generating Key");
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", provider);
        kpg.initialize(new CaviumRSAKeyGenParameterSpec(bits, new BigInteger("65537"), alias + ":public", alias, extractable, false));
        KeyPair pair = kpg.generateKeyPair();
        // Is it really non-extractable?
        System.err.println("Generated: extractable = " + ((CaviumRSAPrivateKey)pair.getPrivate()).isExtractable());
    
        // 2. Generate a self-signed certificate to go with the key.
        System.err.println("Generating Certificate");
        Date startDate = new Date();
        Date endDate = new Date(System.currentTimeMillis() + 365*24*60*60*1000l);
        BigInteger serial = new BigInteger(32, new SecureRandom());
        X500Name name = new X500Name(dn);
        ContentSigner signer = new JcaContentSignerBuilder(alg).build(pair.getPrivate());
        JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(name, serial, startDate, endDate, name, pair.getPublic());
        byte[] encoded = certBuilder.build(signer).getEncoded();
    
        // 3. Store the certificate and the key into the KeyStore
        System.err.println("Storing key and Certificate");
        CertificateFactory factory = CertificateFactory.getInstance("X.509");
        X509Certificate cert = (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(encoded));
        KeyStore.PrivateKeyEntry entry = new KeyStore.PrivateKeyEntry(pair.getPrivate(), new Certificate[] { cert });
        keyStore.setEntry(alias, entry, new KeyStore.PasswordProtection(password));
    
        FileOutputStream out = new FileOutputStream(keyStoreFile);
        keyStore.store(out, password);
        out.close();
    
        // 4. Generate a CSR from the newly-generated PrivateKey and PublicKey
        System.err.println("Generating CSR");
        JcaPKCS10CertificationRequestBuilder p10Builder = new JcaPKCS10CertificationRequestBuilder(name, pair.getPublic());
        PKCS10CertificationRequest csr = p10Builder.build(signer);
        StringWriter w = new StringWriter();
        JcaPEMWriter ww = new JcaPEMWriter(w);
        ww.writeObject(csr);
        ww.close();
        System.out.println(w);
      }
    }
    

    This will create a private key on the HSM and generate a public key and X.509 certificate, which are stored in a local file called "testkeystore". This file has a password, but contains no private key data. The CSR is written to System.out, and this can be given to your CA for signing.

  2. Sign the CSR. Normally your CA would do this, but as a quick test we signed this CSR locally, with the same CA we used to sign the HSM certificate during setup

    openssl x509 -req -days 365 -CA customerCA.crt \
      -CAkey customerCA.key -CAcreateserial \
      -in testkey.csr -out testkey.crt
    
  3. Import the signed certificate back into the KeyStore. The standard command to do this, suitably modified to use the "cloudhsm" keystore, should be:

    keytool -importcert -alias testkey -file testkey.crt \
      -keystore testkeystore -trustcacert \
      -providerclass com.cavium.provider.CaviumProvider \
      -storetype CloudHSM -J-classpath '-J/opt/cloudhsm/java/*'
    

    However we couldn't get this to work - it complained with "keytool error: java.lang.Exception: Failed to establish chain from reply". We presume this is because our CA is not a proper CA and didn't come with all the certificates. So we wrote yet another simple Java program to import the Certificate without checking the chain:

    import java.io.*;
    import java.security.*;
    import java.security.cert.*;
    import java.security.cert.Certificate;
    
    public class ImportCert {
      public static void main(String[] args) throws Exception {
        String keyStoreFile = "testkeystore";
        String alias = "testkey";
        char[] password = "secret".toCharArray();
        String certFile = "testkey.crt";
    
        Provider provider = new com.cavium.provider.CaviumProvider();
        Security.addProvider(provider);
        KeyStore keyStore = KeyStore.getInstance("CloudHSM");
        keyStore.load(new FileInputStream(keyStoreFile), password);
    
        Key key = keyStore.getKey(alias, password);
        CertificateFactory factory = CertificateFactory.getInstance("X.509");
        Certificate cert = factory.generateCertificate(new FileInputStream(certFile));
        keyStore.setKeyEntry(alias, key, password, new Certificate[] { cert });
        FileOutputStream out = new FileOutputStream(keyStoreFile);
        keyStore.store(out, password);
        out.close();
      }
    }
    
  4. Sign a PDF with this key. Here's the code we used to do this:
    import java.io.*;
    import java.security.*;
    import org.faceless.pdf2.*;
    
    public class Sign {
      public static void main(String[] args) throws IOException, GeneralSecurityException {
        String filename = args[0];               // The PDF to sign
        String keyStoreFile = "testkeystore";    // The name of our keystore file
        String alias = "testkey";              // The alias into that keystore
        char[] password = "secret".toCharArray(); // The password for that keystore
    
        Provider provider = new com.cavium.provider.CaviumProvider();
        Security.addProvider(provider);
        KeyStore keyStore = KeyStore.getInstance("CloudHSM");
        keyStore.load(new FileInputStream(keyStoreFile), password);
    
        PDF pdf = new PDF(new PDFReader(new File(args[0])));
        AcrobatSignatureHandlerFactory factory = new AcrobatSignatureHandlerFactory();
        factory.setProvider(provider);
        FormSignature sig = new FormSignature();
        pdf.getForm().getElements().put("Sig1", sig);
        sig.sign(keyStore, alias, password, factory);
    
        OutputStream out = new FileOutputStream("Sign.pdf");
        pdf.render(out);
        out.close();
      }
    }
    

    That is the most basic example we could make for this, but of course all the usual options are available - PAdES signatures, LTV signatures and so on. The process from this point on is no different to a regular, software based private key.