Signing a JSON Web Token (JWT) with a smart card or HSM

The security of cryptography crumbles when a secret or private key is accidentally leaked or stolen by an attacker. A good strategy to minimise this risk is to put keys on a dedicated device behind a restrictive API. The application is permitted to request the cryptographic operation for which the key is intended, for example signing hashed data. Read or write access to the key itself is blocked by the API.

Smart cards and hardware security modules (HSM) provide just that -- a special external device that stores the keys and exposes only an API for performing selected cryptography operations with them. There is no interface, software or physical, to get at the secret or private key material after it's generated or saved on the device. Such devices are typically built to be tamper resistant, for extra security if lost or stolen.

The Nimbus JOSE+JWT library easily handles smart cards and HSMs to sign or encrypt / decrypt JWTs and other objects. It uses the standard PKCS#11 (cryptoki) interface which is supported by virtually all cryptographic devices on the market today.

Prerequisites

1. Locate PKCS#11 driver

Make sure you have a PKCS#11 driver installed on your system and locate its library file. On Linux computers this driver is usually provided by the OpenSC project:

/usr/lib/x86_64-linux-gnu/opensc-pkcs11.so

2. Insert smart card or HSM, note slot number

Insert your smart card of HSM and check that it's recognised as a PKCS#11 crypto device.

You can use the pkcs11-tool for that. Here it shows that a Nitrokey HSM has been connected (via USB) to the computer:

$ pkcs11-tool --list-slots
Available slots:
Slot 0 (0xffffffffffffffff): Virtual hotplug slot
 (empty)
Slot 1 (0x1): Nitrokey Nitrokey HSM (010000000000000000000000) 00 00
 token label        : SmartCard-HSM (UserPIN)
 token manufacturer : www.CardContact.de
 token model        : PKCS#15 emulated
 token flags        : rng, login required, PIN initialized, token initialized
 hardware version   : 24.13
 firmware version   : 2.0
 serial num         : DENK0100295
Slot 2 (0x5): Alcor Micro AU9540 01 00
 (empty)

Note the slot number where the HSM is connected.

3. Create config file

Create a config file so Java can load the PKCS#11 driver and connect to the plugged in smart card or HSM.

Example hsm-config.cfg:

# Give the HSM device a name
name = NitroKeyHSM

# Path to the PKCS#11 driver
library = /usr/lib/x86_64-linux-gnu/opensc-pkcs11.so

# The HSM slot number
slotListIndex = 1

How to sign a JWT with an RSA key stored in a smart card or HSM

The extra steps that you need to perform here are to load the smart card or HSM as a Java crypto provider and get a handle to the desired RSA key on the crypto device. Everything else -- JWT construction and signing remains unchanged.

import java.security.*;
import com.nimbusds.jose.*;
import com.nimbusds.jwt.*;

// The path to the HSM config file
String configFile = "hsm-config.cfg";

// Load the HSM as a Java crypto provider
Provider hsmProvider = new sun.security.pkcs11.SunPKCS11(configFile);

// Get a handle to the private RSA key for signing
KeyStore hsmKeyStore = KeyStore.getInstance("PKCS11", hsmProvider);
String userPin = "123456"; // The pin to unlock the HSM
keyStore.load(null, userPin.toCharArray());
String keyID = "1"; // The key identifier or alias
String keyPin = ""; // Optional pin to unlock the key
PrivateKey privateKey = (PrivateKey)hsmKeyStore.getKey(keyID, keyPin.toCharArray());

// Create an RSA signer and configure it to use the HSM
RSASSASigner signer = new RSASSASigner(privateKey);
signer.getJCAContext().setProvider(hsmProvider);

// We can now RSA sign JWTs
SignedJWT jwt = new SignedJWT(
            new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(keyID).build(),
            new JWTClaimsSet.Builder().subject("alice").build());

jwt.sign(signer);
String jwtString = jwt.serialize();

How to extract the public RSA key from the smart card or HSM

In order the verify the JWT signature we need the public RSA key. We can get it from the matching X.509 certificate that is stored alongside the private key on the HSM.

// Get the public RSA key from the HSM
RSAPublicKey publicKey = (RSAPublicKey)hsmKeyStore.getCertificate(keyID).getPublicKey();

// Use it to verify the RSA signature
assertTrue(SignedJWT.parse(jwtString).verify(new RSASSAVerifier(publicKey)));

// Use it to export a RSA JWK to clients, etc
JWK jwk = new RSAKey.Builder(publicKey).keyID(keyID).build();

How to list the supported crypto algorithms of a smart card or HSM

The java.security.Provider can be queried for the supported crypto services, such as key generation, signing, etc.

import java.security.*;

// The path to the HSM config file
String configFile = "hsm-config.cfg";

// Load the HSM as a Java crypto provider
Provider hsmProvider = new sun.security.pkcs11.SunPKCS11(configFile);

System.out.println("HSM crypto services:");

for (Provider.Service service: hsmProvider.getServices()) {
    System.out.println(service.getType() + " : " + service.getAlgorithm());
}

A Nitrokey HSM will report the following crypto services:

HSM crypto services:
KeyFactory : RSA
KeyFactory : EC
KeyPairGenerator : RSA
KeyPairGenerator : EC
KeyStore : PKCS11
Signature : MD2withRSA
Signature : MD5withRSA
Signature : SHA224withRSA
Signature : SHA256withRSA
Signature : SHA384withRSA
Signature : SHA512withRSA
Signature : SHA1withRSA
Signature : NONEwithECDSA
Signature : SHA1withECDSA
Signature : SHA224withECDSA
Signature : SHA256withECDSA
Signature : SHA384withECDSA
Signature : SHA512withECDSA
Cipher : RSA/ECB/NoPadding
Cipher : RSA/ECB/PKCS1Padding
KeyAgreement : ECDH
AlgorithmParameters : EC
MessageDigest : MD5
MessageDigest : SHA1
MessageDigest : SHA-256
MessageDigest : SHA-384
MessageDigest : SHA-512
SecureRandom : PKCS11

Caveats

1. HSM throughput

If you're planning to use an HSM, for example with an OpenID Connect server to issue ID tokens, make sure the crypto device is able to handle the expected rate at which objects must be signed or encrypted / decrypted. Otherwise the throughput of your app will become constrained by the throughput of the HSM.

The budget Nitrokey HSM for example has a throughput of about 1.6 RSA 2048-bit signatures per second.

2. Each private key on the smart card / HSM must have a X.509 certificate

The Java java.security.KeyStore will not allow you to get a handle for a private RSA or EC key if there's no X.509 certificate for it in the store.

// Will return null if the key has no associated certificate in the store
hsmKeyStore.getKey(keyID, pin);

Make sure each private key that you create on the smart card or HSM is provisioned with a certificate. This can be a certificate chain to a CA, or a self-signed certificate.


comments powered by Disqus