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:
-
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.
-
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.
-
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.
-
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
-
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();
}
}
-
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.