Encrypting a data with a public key using pkcs11 module fails

4.6k Views Asked by At

I am using Python's pkcs11 package to access an X.509 certificate stored on my Yubikey 5. Accessing the certificate, public and private keys using pkcs11 Objects work fine as is signing and signature verification. Howerver, for the life of me, I cannot figure out why encrypting with the public key does not work. Here is my code:

import pkcs11
from pkcs11 import Attribute, ObjectClass, KeyType, util
lib = pkcs11.lib('/usr/lib/x86_64-linux-gnu/pkcs11/onepin-opensc-pkcs11.so')
token = lib.get_token(token_label='PIV Card Holder pin (PIV_II)'
session = token.open(user_pin=pin)
# Getting a private and a public key as pkcs11 Object
private = next(session.get_objects({
    Attribute.CLASS: ObjectClass.PRIVATE_KEY, 
}))
public = next(session.get_objects({
    Attribute.CLASS: ObjectClass.PUBLIC_KEY, 
}))
data = 'Hello, world!'
sig = private.sign(data) # Works!
sig_verif = public.verify(data, sig) # Works!
print("Signature is valid? "+str(sig_verif)) # True
# So far, everything above worked fine.
# ----------
# Now, this is the part that does not work
encrypt_data = public.encrypt(data) # Fails!

The last line above fails with pkcs11.exceptions.FunctionNotSupported error. I did some research, and it the explanation I found seems to imply that this function (encrypt) is not supported by the openSC library file (*.so) that I am using. However, I find it hard to believe considering that signature feature works just fine.

Just to make sure that I can use this particular public key outside of session context, I tried the following code using Crypto package:

from Crypto.Cipher import PKCS1_OAEP
public_key = RSA.importKey(public[Attribute.VALUE]) # The content of pkcs11 public key as DER
cipher = PKCS1_OAEP.new(public_key)
encr_data = cipher.encrypt(data) # This works!

So, it seems that using my stand-alone public key allows me to encrypt data. But why can't I do it in the context of a pkcs11 token session?

Then, I tried using the pkcs11 Object decrypt function to try to decrypt the data generated using Crypto module above:

decrypted = private.decrypt(encr_data) # It fails!

The above failed with pkcs11.exceptions.MechanismInvalid error. I tried using different mechanisms, but all of them resulted in the same error. What is interesting -- it seems that pkcs11 object allows me to at least invoke decrypt function without complaining that it is not supported.

One more thing I should mention. I checked my certificate and saw that under Extension -> Certificate Key Usage, it says:

Critical
Signing
Key Encipherment

I read on the difference between key encipherment and data encipherment and learned that key encipherment is used to encrypt a secret (symmetric) key instead of data. Can it be the reason I can't use encrypt function for this token session?

Any feedback would be greatly appreciated!

2

There are 2 best solutions below

2
On BEST ANSWER

I'm sorry, but I think this is just a shortcoming of the API. As encryption with the public key doesn't require any security, it doesn't make sense to implement it on the Yubikey. It is much faster to export the public key values and perform the encryption on the host.

To be fair, Yubikey could have been nice and implement the functionality in software within the Ubikey PKCS#11 library. If you really want to then you could create a new PKCS#11 "wrapper" library that does contain the missing functionality in software; all other commands that Yubikey does implement can be forwarded to the original Yubikey PKCS#11 library.

2
On

After doing a rather extensive research and taking into consideration this thread's replies, I found that encryption not working in this token session is due to the OpenSC API limitation. As a matter of fact there is a compatibility table on the python-pkcs11 page that shows (in plain text) that OpenSC does not support Encryption, Symmetric keys generation, key wrapping and other functionalities. It does fully support signing/verification and partially supports decryption. Had I done a better search, it would've saved me a lot of time.

As a matter of fact, it is very useful to do a hard token test using 'pkcs11-tool', as it will show the supported functionalities and mechanisms for each PIV slot. In my case, I invoked it as follows:

pkcs11-tool -p $pin -t

and received the following report:

Using slot 0 with a present token (0x0)
C_SeedRandom() and C_GenerateRandom():
  seeding (C_SeedRandom) not supported
  seems to be OK
Digests:
  all 4 digest functions seem to work
  MD5: OK
  SHA-1: OK
  RIPEMD160: OK
Signatures (currently only for RSA)
  testing key 0 (PIV AUTH key) 
  all 4 signature functions seem to work
  testing signature mechanisms:
    RSA-X-509: OK
    RSA-PKCS: OK
    SHA1-RSA-PKCS: OK
    MD5-RSA-PKCS: OK
    RIPEMD160-RSA-PKCS: OK
    SHA256-RSA-PKCS: OK
  testing key 1 (2048 bits, label=SIGN key) with 1 signature mechanism
    RSA-X-509: OK
  testing key 2 (2048 bits, label=KEY MAN key) with 1 signature mechanism -- can't be used to sign/verify, skipping
Verify (currently only for RSA)
  testing key 0 (PIV AUTH key)
    RSA-X-509: OK
    RSA-PKCS: OK
    SHA1-RSA-PKCS: OK
    MD5-RSA-PKCS: OK
    RIPEMD160-RSA-PKCS: OK
  testing key 1 (SIGN key) with 1 mechanism
    RSA-X-509: OK
  testing key 2 (KEY MAN key) with 1 mechanism -- can't be used to sign/verify, skipping
Unwrap: not implemented
Decryption (currently only for RSA)
  testing key 0 (PIV AUTH key) 
    RSA-X-509: OK
    RSA-PKCS: OK
  testing key 1 (SIGN key) 
    RSA-X-509: OK
    RSA-PKCS: OK
  testing key 2 (KEY MAN key) 
    RSA-X-509: OK
    RSA-PKCS: OK
No errors

From there, we can see that decryption is supported for all three occupied slots but only with RSA-X-509 and RSA-PKCS mechanisms (no OAEP).

Now, I am considering fusing the pkcs11-tool and openssl functionalities to do data encryption. I haven't figured out all the intricacies of this kind of flow, but I am thinking something of this sort:

  1. (pkcs11-tool) Export a certificate from a desired PIV slot
  2. (openssl) Create a symmetric (e.g., AES) secret key
  3. (openssl) Encrypt the data with this secret key
  4. (openssl) Use the certificate from step 1 to encrypt the secret key
  5. Send the [symmetrically] encrypted data and [asymmetrically] encrypted secret key to the recipient
  6. (pkcs11-tool) Decrypt the secret key on the secure token
  7. (openssl) Use the decrypted secret key to decrypt the actual data

It looks like I should be able to implement such a workaround either in Linux shell using pkcs11-tool and openssl utilities or in Python using pkcs11 and OpenSSL libraries. The latter seems more preferable if I decide to later do it through GUI. This all seems quite low level, so I am wondering if there is a easier way to encrypt/decrypt data. I know that PGP fuses the encrypted data and wrapped secret key together into one single file, so the end-users only have to execute one command on their end.