Security pitfalls using PKCS#11

Image Description
Kobus Grobler

We came across several security challenges while developing firmware for the ellipticSecure eHSM PKCS#11 hardware security module. We’ll go into the details, but for now the short version is that setting the correct attributes on keys are critically important. The PKCS#11 API is flexible but complex, so care must be taken to use it correctly.

This example is from a junit test for the eHSM module using the standard SunPKCS11 provider. The junit test is to test key wrapping using the eHSM.

First, configure the SunPKCS11 provider with a .cfg file:

name = eHSM
library = /usr/lib/libehsm.so

attributes(*, CKO_PUBLIC_KEY, CKK_RSA) = {
    CKA_PUBLIC_EXPONENT = 65537
    CKA_WRAP = true
}

attributes(*, CKO_PRIVATE_KEY, CKK_RSA) = {
    CKA_UNWRAP = true
    CKA_DECRYPT = true
}
...

Next, generate an AES key and a RSA key pair in the hardware module:

    ...
    SunPKCS11 p = new SunPKCS11(configName);
    ...
    KeyStore ks = KeyStore.getInstance("PKCS11", p);
    ks.load(null, password);
    KeyGenerator symGen = KeyGenerator.getInstance("AES",p);
    symGen.init(128);
    SecretKey aeskey = symGen.generateKey();  // generate AES key in hsm
    
    KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA",p);
    keyGen.initialize(1536);
    KeyPair wrapKeyPair = keyGen.generateKeyPair(); // generate RSA key in hsm

So far so good - now we have the keys securely stored in the HSM.

Let’s test the key wrapping:

    // wrap aes key with RSA key in HSM
    Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding",p);
    cipher.init(Cipher.WRAP_MODE,wrapKeyPair.getPublic());
    byte wrappedKey[] = cipher.wrap(aeskey);

    cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding",p);
    cipher.init(Cipher.UNWRAP_MODE,wrapKeyPair.getPrivate());
    Key unwrapped = cipher.unwrap(wrappedKey,"RSA/ECB/PKCS1Padding",Cipher.SECRET_KEY); // unwrap AES key in hsm

Now, the intended result here was to have the wrapped AES key unwrapped and stored as a new key in the HSM. Ideally we should never have access to the plain text AES key in software, right? However, on closer inspection cipher.unwrap() returned the plain text key here!

After a bit more digging in the SunPKCS11 source and logging the HSM library calls, it turns out that SunPKCS11 does not support the PKCS#11 C_Unwrap() function. Instead it uses the C_Decrypt() function to unwrap and, since we have the CKA_DECRYPT attribute enabled on RSA keys, the HSM allows the call and we end up with a decrypted AES key in software.

The lesson learned? Ensure that the correct attributes are set for your keys and understand the implications of those attributes. In addition, be explicit in providing attributes: do not rely on the HSM’s default attributes, they might not be what you want in every use case.

In the particular case of key wrapping: never enable CKA_DECRYPT and CKA_UNWRAP on a key intended to be used for key wrapping - unless you do not mind having keys exposed to software. However, that kind of defeats the purpose of using an HSM in the first place. So your config file should rather look like this:

...
attributes(*, CKO_PRIVATE_KEY, CKK_RSA) = {
    CKA_UNWRAP = true
    CKA_DECRYPT = false
}
...

The SunPKCS11 provider is a bit inflexible here in the sense that (at least without some reflection) it applies these attributes to all RSA private keys, but that is a subject that I will discuss in another post.

In general, it seems best practice to use the proprietary export and import mechanisms of the manufacturer to backup an HSM rather than key wrapping.