Getting Started with Nitrokey HSM using Rust
I recently decided to experiment with a Nitrokey HSM 2 using the cryptoki Rust crate for performing PKCS11 operations with it. This documents my experiences.
The Nitrokey HSM is a tamperproof smart card embedded into a USB device, it can be interacted with through PKCS11 and provided tools. HSM come in all shapes and sizes, but this one is a cheap, easily integrated and opensource module for storing secrets outside of the memory of the computer itself.
The cryptoki API is a good Rust wrapper around the PKCS11 primitives, but it had a few gotchas to getting started with a real device. There didn't seem to be any straight forward and easy to follow examples using the crate. I decided to publish this as a simple example and explain some of the non-obvious steps required in this post.
Objective
The repository code describes attaching to the device, which is expected to have a pre-existing RSA key stored. It then extracts the public key in order to encrypt data on the host and then finally tests the decryption via the device.
Preparation
Prior to starting this assumes that the Nitrokey device has been set up following the getting started instructions, an RSA key has been generated, and the opensc libraries have been installed onto the host.
Walk through
In the code firstly we set up the PKCS11 client, passing the path of the shared
object for the type of device we're interacting with. On Arch (btw) it was
/usr/lib/opensc-pkcs11.so
after installing all the dependencies from the
Nitrokey installation instructions. The initialize call needs to be called, but
it seems to currently only have one possible argument.
let pkcs11client = Pkcs11::new(opt.module)?;
pkcs11client.initialize(CInitializeArgs::OsThreads)?;
Next set up the PIN for the type of user you want to login to the HSM as. For this example we use the regular 'User' PIN because we're later going to be accessing the secrets. This needs to be set before the login attempt. The slot here is usually a parameter to allow multiple devices.
pkcs11client.set_pin(slot, opt.pin.as_str())?;
Next up the flags now need to be set up to start a logged-in session with the device. Importantly the 'serial session' flag must be set on all sessions and without it it'll reject it as a deprecated access.
let mut flags = Flags::new();
flags.set_serial_session(true);
Once we have opened the session, we log in as the 'User' type.
let session = pkcs11client.open_session_no_callback(slot, flags)?;
session.login(cryptoki::types::session::UserType::User)?;
Next we want to find the objects at a particular Key ID location on the device,
this is a user-provided ID to identify a stored key. The location is split into
separate public and private objects, suitable for encrypting and decrypting
respectively. The session is used to query for both the Id
and
Encrypt
/Decrypt
attributes.
let enc_objects = session.find_objects(&[
Attribute::Encrypt(true.into()),
Attribute::Id(keyid.clone()),
])?;
let dec_objects = session.find_objects(&[
Attribute::Decrypt(true.into()),
Attribute::Id(keyid.clone()),
])?;
if enc_objects.len() != 1 && dec_objects.len() != 1 {
bail!("Can't uniquely determine encryption and decryption objects for key id: {}", opt.id);
}
One we have an ObjectHandle
we can query its stored attributes. Attributes
are optional, but the interface supports multiple at once, so we define a
couple of helper functions to extract the public key parts of an RSA key.
fn extract_modulus(session: &Session, object: ObjectHandle) -> Result<BigUint> {
let attributes = session.get_attributes(object, &[AttributeType::Modulus])?;
if let Some(Attribute::Modulus(vec)) = attributes.get(0) {
Ok(BigUint::from_bytes_be(&vec))
} else {
bail!("Modulus Attribute is not available");
}
}
fn extract_public_exponent(session: &Session, object: ObjectHandle) -> Result<BigUint> {
// ... Similar implementation ...
}
We then use the helper functions to extract the modulus and public exponents to generate the public key.
let modulus = extract_modulus(&session, enc_objects[0])?;
let pubexp = extract_public_exponent(&session, enc_objects[0])?;
The excellent RustCrypto project provides cryptographic primitives, at the time of writing there's been an audit of the RSA crate but it's results have not been published. However, for this example program it's clearly good enough. We construct the public key and then encrypt a test message with a provided padding scheme.
It's important to note that the device doesn't support encryption on the device itself, so we have to do it on the host. Although cryptoki does expose the encryption action, the Nitrokey HSM device doesn't support it.
let mut rng = OsRng;
let pubkey = rsa::RSAPublicKey::new(modulus, pubexp)?;
let secret = "This is my secret".as_bytes().to_vec();
let output = pubkey.encrypt(&mut rng, PaddingScheme::new_pkcs1v15_encrypt(), &secret)?;
Lastly as the key-material only lives on the device, conversely we must do the decrypt using the device. This is hilariously slow for a 4096-bit RSA operation, ie multiple seconds, as this is just a low-powered smart card type device. As these devices are designed to be readily available, easily installed and used only for securing key encryption keys it's not a problem.
let plaintext = session.decrypt(
&cryptoki::types::mechanism::Mechanism::RsaPkcs,
dec_objects[0],
&output,
)?;
And finally...
This was a fun exercise in trying out PKCS11, and was surprisingly easy (with a few quirks) to get going on a rainy Saturday. I'm excited to see Rust crypto crates working well and getting audited; however, as with any project like this: this is for educational purposes only, do not trust it as safe or secure production code.