Thanks, it works.
For people looking how to do it with python, here is a working example:
from asn1crypto.core import OctetString
from pkcs11 import lib, KeyType, ObjectClass, Attribute, Mechanism
import tool_crypto as tc
PKCS11_MODULE = None
PKCS11_TOKEN = None
PKCS11_PIN = None
ECC_CURVE_SIZES = {
b'\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07': 32, # secp256r1
b'\x06\x05\x2b\x81\x04\x00\x22': 48, # secp384r1
b'\x06\x05\x2b\x81\x04\x00\x23': 66, # secp521r1
}
def check_config(pkcs11_module: [str, None], pkcs11_token: [str, None], pkcs11_pin: [str, None]):
if pkcs11_module is None or pkcs11_token is None or pkcs11_pin is None:
raise ValueError("pkcs11-module, pkcs11-token and pkcs11-pin are mandatory!")
def get_token():
check_config(PKCS11_MODULE, PKCS11_TOKEN, PKCS11_PIN)
return lib(PKCS11_MODULE).get_token(token_label=PKCS11_TOKEN)
def encrypt_method_2(ealgo, key_label, iv, plain):
check_config(PKCS11_MODULE, PKCS11_TOKEN, PKCS11_PIN)
try:
with get_token().open(user_pin=PKCS11_PIN) as session:
sym_keys = list(session.get_objects({
Attribute.CLASS: ObjectClass.SECRET_KEY,
Attribute.LABEL: key_label,
Attribute.KEY_TYPE: KeyType.AES,
}))
if len(sym_keys) != 1:
raise ValueError(f"{len(sym_keys)} keys found for '{key_label} label!' (must be only one!)")
key = sym_keys[0]
if ealgo == "AES-CBC":
mech = Mechanism.AES_CBC
block_size = 16
chunk_size = 1024
cipher = b""
current_iv = iv
for i in range(0, len(plain), chunk_size):
chunk = plain[i:i + chunk_size]
if len(chunk) % block_size != 0:
raise ValueError("Plaintext must be padded to a multiple of 16 bytes before encryption")
enc = key.encrypt(chunk, mechanism=mech, mechanism_param=current_iv)
cipher += enc
current_iv = enc[-block_size:]
else:
raise ValueError(f"Unsupported encryption algorithm : {ealgo}")
except Exception as e:
print(f"[ERROR] encrypt() : {e}")
cipher = None
return cipher
def encrypt_method_1(ealgo, key_label, iv, plain):
check_config(PKCS11_MODULE, PKCS11_TOKEN, PKCS11_PIN)
try:
with get_token().open(user_pin=PKCS11_PIN) as session:
sym_keys = list(session.get_objects({
Attribute.CLASS: ObjectClass.SECRET_KEY,
Attribute.LABEL: key_label,
Attribute.KEY_TYPE: KeyType.AES,
}))
if len(sym_keys) != 1:
raise ValueError(f"{len(sym_keys)} keys found for '{key_label} label!' (must be only one!)")
key = sym_keys[0]
if ealgo == "AES-CBC":
mech = Mechanism.AES_CBC
block_size = 16
cipher = b""
current_iv = iv
for i in range(0, len(plain), block_size):
block = plain[i:i + block_size]
enc = key.encrypt(block, mechanism=mech, mechanism_param=current_iv)
cipher += enc
current_iv = enc[-block_size:]
else:
raise ValueError(f"Unsupported encryption algorithm : {ealgo}")
except Exception as e:
print(f"[ERROR] encrypt() : {e}")
cipher = None
return cipher
def encrypt(ealgo, key_label, iv, plain):
return encrypt_method_1(ealgo, key_label, iv, plain)
...
WARNING:
- I tried different methods, but this is horribly long to sign huge files (in my case : 55.8KBytes + 28.8KBytes + 656.6KBytes + 496.4KBytes + 92.4KBytes)
- Each 1024 bytes blocks takes 4-5 seconds to encrypt

EDIT (just to keep a trace):
- If I use
encrypt_method_2 for AES-CBC encryption, it can encrypt 24 packet of 16 bytes per seconds (according to pcscd logs): 24*16 = 384Bytes/s
- If I use
encrypt_method_1for AES-CBC encryption, it can encrypt 1 packet of 1024 bytes in 4-5seconds. 1024/4=256Bytes/s
- Conclusion:
- I don’t know if the Nitrokey HSM 2 is known to be so slow OR if there is something in the setup that slow down the process.
- The encryption finish faster if we use small packets instead of big packets.