I've been working on enabling ML-KEM in python-pkcs11. That project tests against SoftHSMv2 and opencryptoki. Unfortunately both of these implementations disagree on the handling of CKA_VALUE_LEN when calling C_DecapsulateKey, and after digging a little deeper, I think SoftHSMv2 is in the wrong here.
I think there are actually two bugs in play here.
Bug 1: C_DecapsulateKey rejects CKA_VALUE_LEN with CKR_ATTRIBUTE_READ_ONLY
C_DecapsulateKey calls CreateObject with OBJECT_OP_UNWRAP, which forbids setting CKA_VALUE_LEN:
|
rv = this->CreateObject(hSession, secretAttribs, secretAttribsCount, phKey, OBJECT_OP_UNWRAP); |
|
CK_RV P11AttrValueLen::updateAttr(Token* /*token*/, bool /*isPrivate*/, CK_VOID_PTR pValue, CK_ULONG ulValueLen, int op) |
|
{ |
|
// Attribute specific checks |
|
|
|
if (op != OBJECT_OP_GENERATE && op != OBJECT_OP_DERIVE) |
|
{ |
|
return CKR_ATTRIBUTE_READ_ONLY; |
SoftHSMv2 seemingly assumes that the intended output length is 32 bytes (i.e. the natural output length of the ML-KEM decapsulate operation) and returns that unconditionally.
Assuming the semantics of C_UnwrapKey in the spec also apply to C_DecapsulateKey, I believe CKA_VALUE_LEN should be allowed, and it's actually the only way to unwrap a key that is less than 32 bytes long.
Bug 2: C_EncapsulateKey stores a CKA_VALUE_LEN that disagrees with the actual key material
The created object inherits CKA_VALUE_LEN from the caller template, but at the same time, the byteLen in this code is initialised to 32 and never updated, so this branch is never taken:
|
// Truncate value when requested, remove from the leading end |
|
if (byteLen < secretValue.size()) |
|
secretValue.resize(byteLen); |
Expected behaviour
Unlike C_UnwrapKey, C_DecapsulateKey doesn't take a key length parameter, only a ciphertext length, so CKA_VALUE_LEN is the only way to convey the expected plaintext length.
I think this is the way things should work given what the spec says:
CKA_VALUE_LEN absent: produce a key of the full 32-byte ML-KEM output.
CKA_VALUE_LEN present, value <= 32: truncate the key material to that length; both CKA_VALUE and the CKA_VALUE_LEN attribute on the resulting object must reflect the truncated size.
CKA_VALUE_LEN present, value > 32: return CKR_WRAPPED_KEY_LEN_RANGE as required by the spec.
One could possibly also validate whether the resulting length makes sense for the mechanism requested (e.g. limit AES keys to 16, 24 or 32 bytes). As it happens, opencryptoki does this, and that's how I found out about this issue: the way the check is implemented over there errors out if CKA_VALUE_LEN is not present in the template.
I hope this makes sense.
I've been working on enabling ML-KEM in python-pkcs11. That project tests against SoftHSMv2 and opencryptoki. Unfortunately both of these implementations disagree on the handling of
CKA_VALUE_LENwhen callingC_DecapsulateKey, and after digging a little deeper, I think SoftHSMv2 is in the wrong here.I think there are actually two bugs in play here.
Bug 1:
C_DecapsulateKeyrejectsCKA_VALUE_LENwithCKR_ATTRIBUTE_READ_ONLYC_DecapsulateKeycallsCreateObjectwithOBJECT_OP_UNWRAP, which forbids settingCKA_VALUE_LEN:SoftHSMv2/src/lib/SoftHSM.cpp
Line 8765 in 3129355
SoftHSMv2/src/lib/P11Attributes.cpp
Lines 2307 to 2313 in 3129355
SoftHSMv2 seemingly assumes that the intended output length is 32 bytes (i.e. the natural output length of the ML-KEM decapsulate operation) and returns that unconditionally.
Assuming the semantics of
C_UnwrapKeyin the spec also apply toC_DecapsulateKey, I believeCKA_VALUE_LENshould be allowed, and it's actually the only way to unwrap a key that is less than 32 bytes long.Bug 2:
C_EncapsulateKeystores aCKA_VALUE_LENthat disagrees with the actual key materialThe created object inherits
CKA_VALUE_LENfrom the caller template, but at the same time, thebyteLenin this code is initialised to 32 and never updated, so this branch is never taken:SoftHSMv2/src/lib/SoftHSM.cpp
Lines 8519 to 8521 in 3129355
Expected behaviour
Unlike
C_UnwrapKey,C_DecapsulateKeydoesn't take a key length parameter, only a ciphertext length, soCKA_VALUE_LENis the only way to convey the expected plaintext length.I think this is the way things should work given what the spec says:
CKA_VALUE_LENabsent: produce a key of the full 32-byte ML-KEM output.CKA_VALUE_LENpresent, value <= 32: truncate the key material to that length; bothCKA_VALUEand theCKA_VALUE_LENattribute on the resulting object must reflect the truncated size.CKA_VALUE_LENpresent, value > 32: returnCKR_WRAPPED_KEY_LEN_RANGEas required by the spec.One could possibly also validate whether the resulting length makes sense for the mechanism requested (e.g. limit AES keys to 16, 24 or 32 bytes). As it happens,
opencryptokidoes this, and that's how I found out about this issue: the way the check is implemented over there errors out ifCKA_VALUE_LENis not present in the template.I hope this makes sense.