cryptography icon indicating copy to clipboard operation
cryptography copied to clipboard

PKCS#11 support

Open tsturzl opened this issue 6 months ago • 7 comments

In a conversation (#13115) with @alex, it was determined that neither this project nor pyOpenSSL will likely move to support the OpenSSL provider interface. Without OpenSSL providers, the likely approach will have to be direct support for these functions through the implementation of the key primitives (cryptography.hazmat.primitives.asymmetric). I believe PKCS#11 support is the best approach for supporting HSMs, SmartCards, and TPM2 modules, as it provides a standardized and broadly supported interface for a broad range of cryptographic hardware and services (eg. CloudHSM). Currently there is a high level PKCS#11 library in Python, as well as rust-cryptoki which provides both a high level wrapper as well as access to the low level bindgen. Both bind to the same library, it would just depend on which of these options seems more suitable, alternatively we could manage the rust bindgen in this project directly.

This is something I'd be willing to work on. I have a good amount of Rust, Python, and C experience as well as familiarity of the PKCS#11 standard.

tsturzl avatar Jun 27 '25 20:06 tsturzl

Thanks for filing this. I think the first piece here is to figure out what we want our API to look like. I think it's something like "there's a function you call that returns a key object that implements one of our key loading interfaces", but I don't know what arguments it needs to take or if there's complexity that would prevent this API.

alex avatar Jun 28 '25 02:06 alex

I think it might need to be more than a function. The process for using PKCS#11 modules is usually loading the PKCS#11 module for your specific device, specifying the "slot" your device is on and then selecting or creating a "token" on that slot which can hold a variety of objects. You could pass all of this into a function, but you're going to need to associate any follow up functionality with that context. Like you create the key through this function, the key you get back will still need to leverage that loaded PKCS#11 module, slot, and token. So there's state that needs to be carried around.

We could have a function like you're saying, but under the hood we're actually passing all that context along to the RSAPrivateKey implementation. The downside of this is that if you want to interact with more than one key you're creating another context all over again, I don't think this is a technical issue.

So my hunch is that we have something like this

class PKCS11Context:

  def __init__(self, pkcs11_module: str, slot: int, token: str):
    ...

  def load_public_key(self, uri: str, user_pin: str | None = None) -> PublicKey:
    ...

  def load_private_key(self, uri: str, user_pin: str | None = None) -> PrivateKey:
    ...

  def generate_rsa_key_pair(self) -> (RSAPublicKey, RSAPrivateKey):
    ...

  def generate_ec_key_pair(self) -> (ECPublicKey, ECPrivateKey):
    ...

  def destroy_key(self, uri: str, user_pin: str | None = None):
    ...

Obviously the PrivateKey implementations will be specific to PKCS#11, so maybe we pass out those specific key types since they'll technically be different implementation with different capabilities, such as a PKCS#11 private key can't be retrieved as bytes. It'll still likely extend the primitive, but if you have a PKCS11RSAPrivateKey type you might then know that trying to retrieve the bytes for the key will throw an exception. The individual key implementations will likely need access to the PKCS11Context, so we'll likely need to expose the functionality, possibly as protected methods, which allow the PrivateKey implementation to call the sign, verify, encrypt, and decrypt functions of the PKCS#11 library. This means that each PrivateKey implementation is really just holding a handle to the specific PKCS#11 object and a reference to the PKCS11Context which can use those handles to perform various cryptographic functions with those keys.

The next thing to consider is that PKCS#11 allows for the storage, and retrieval of certificates. I'm not sure how useful or important it is to support this, because the certificate is public information in the sense that it's reasonable to just anticipate the certificate to be located on the filesystem. It's also possible we can just support this by pulling out the certificate as bytes and using the existing load_pem_x509_certificates function to load it.

The current limitation of the rust-cryptoki library is that it doesn't seem to provide any implementation for encoding/decoding to DER or PEM, it just supports "attributes" currently. The Python-PKCS11 library supports this. We could implement this, or possibly make a contribution to that project, or if you think it's reasonable to just use the python PKCS#11 library that's certainly a possibility too.

tsturzl avatar Jun 28 '25 04:06 tsturzl

@alex Do you have any feed back or alternative ideas?

tsturzl avatar Jul 29 '25 20:07 tsturzl

I would propose dropping key generation and deletion for the time being and just focus on key operations. For that, I think this is a reasonable starting point.

alex avatar Jul 29 '25 22:07 alex

This seems like a good starting point modulo Alex's suggestion.

reaperhulk avatar Aug 02 '25 23:08 reaperhulk

Note that PKCS#11 URIs don't have to specify a slot or token name. In that case, all tokens need to be searched for a matching object.

Also, the URI can specify the module name or path. The PIN can also be specified via the URI, so there is no need for a separate pin parameter.

From an application's perspective, I'd want to just pass in a pkcs11: URI and get back a (public/private) key object, without having to parse (parts of) the URI myself or having to implement searching across multiple tokens.

jluebbe avatar Sep 29 '25 14:09 jluebbe

Also, the URI can specify the module name or path. The PIN can also be specified via the URI, so there is no need for a separate pin parameter.

Having the pin as a separate parameter should still be a thing since redacting it out of logs is easier if it's separate.

scj643 avatar Sep 30 '25 19:09 scj643