2.4. Using SafeQuard in Thales ProtectServer¶
2.4.1. Overview¶
SafeQuard integrates with the Thales ProtectServer HSM through a Functionality Module (FM) using a patched PKCS#11 interface.
The SafeQuard FM extends the standard PKCS#11 interface by patching the C_DeriveKey() function. The host application continues to call the standard PKCS#11 function, while the SafeQuard FM intercepts requests that use SafeQuard-specific mechanisms.
Thales describes this type of FM as a PKCS#11-patched function. A patched function follows the standard PKCS#11 interface, while the FM can perform additional processing before passing the request to the original PKCS#11 implementation.
For SafeQuard, C_DeriveKey() is used as the entry point for ML-KEM operations.
The following SafeQuard mechanisms are supported:
CKM_PQCEE_MLKEM_KEY_PAIR_GEN
CKM_PQCEE_MLKEM_GET_PUB_KEY
CKM_PQCEE_MLKEM_ENCAP
CKM_PQCEE_MLKEM_DECAP
2.4.2. Architecture¶
The host application does not directly communicate with a custom SafeQuard function.
Instead, the application uses the standard PKCS#11 interface:
SafeQuard Host Application
|
| C_DeriveKey()
v
PKCS#11 Interface
|
v
ProtectServer HSM
|
v
SafeQuard FM
|
| Detect SafeQuard mechanism
v
ML-KEM operation
The important distinction is that the host application calls the standard C_DeriveKey() function, while the mechanism supplied in CK_MECHANISM determines which SafeQuard operation is requested.
Thales documents that incoming PKCS#11 requests are checked against the list of patched functions. If the function is patched, control is passed to the FM implementation; otherwise, the normal firmware implementation is used. PKCS#11 calls made by the FM directly access the firmware implementation and bypass the message-processing modules.
2.4.3. Host Function¶
The SafeQuard FM patches the following PKCS#11 function:
CK_RV FM_C_DeriveKey(
CK_SESSION_HANDLE hSession,
CK_MECHANISM_PTR pMechanism,
CK_OBJECT_HANDLE hBaseKey,
CK_ATTRIBUTE_PTR pTemplate,
CK_ULONG ulAttributeCount,
CK_OBJECT_HANDLE_PTR phKey
);
The host application uses the standard C_DeriveKey() API:
rv = C_DeriveKey(
hSession,
&mechanism,
hBaseKey,
template,
templateCount,
&hKey
);
The SafeQuard FM determines the requested operation from pMechanism->mechanism.
2.4.4. ML-KEM Parameter¶
This is an example mlkem.h file that defines the ML-KEM parameter values:
#include <cryptoki.h>
#define CKM_PQCEE_MLKEM_KEY_PAIR_GEN (CKM_VENDOR_DEFINED | 0x80000010)
#define CKM_PQCEE_MLKEM_GET_PUB_KEY (CKM_VENDOR_DEFINED | 0x80000020)
#define CKM_PQCEE_MLKEM_ENCAP (CKM_VENDOR_DEFINED | 0x80000030)
#define CKM_PQCEE_MLKEM_DECAP (CKM_VENDOR_DEFINED | 0x80000040)
#define CKM_PQCEE_MLKEM_768_BIT_KEY 0x80000001
2.4.5. CKM_PQCEE_MLKEM_KEY_PAIR_GEN¶
The CKM_PQCEE_MLKEM_KEY_PAIR_GEN mechanism is used to generate an ML-KEM key pair inside the ProtectServer HSM loaded with SafeQuard FM.
The ML-KEM parameter specifies the requested ML-KEM security level.
CK_OBJECT_HANDLE hPrivKey = 0;
UINT4 param = CKM_PQCEE_MLKEM_768_BIT_KEY;
CK_MECHANISM keygenMech = {
.mechanism = CKM_PQCEE_MLKEM_KEY_PAIR_GEN,
.pParameter = ¶m,
.ulParameterLen = sizeof(param)
};
The hBaseKey argument is set to 0 because the operation is being used as the SafeQuard ML-KEM key-generation entry point rather than deriving a key from an existing PKCS#11 base key.
rv = C_DeriveKey(
hSession,
&keygenMech,
0, // Base = 0 for key generation
NULL, // No template needed for key generation
0,
&hPrivKey
);
if (rv != CKR_OK) {
printf("[HOST] KeyGen failed: 0x%lx\n", rv);
exit(EXIT_FAILURE);
}
printf("[HOST] Private Key Handle: %lu\n", hPrivKey);
If the operation succeeds, hPrivKey contains the returned object handle.
2.4.6. CKM_PQCEE_MLKEM_GET_PUB_KEY¶
The CKM_PQCEE_MLKEM_GET_PUB_KEY mechanism is used to retrieve the public
key associated with an ML-KEM key pair.
The operation is issued through the same patched C_DeriveKey() entry
point.
The mechanism identifies the requested SafeQuard operation, while the key handle identifies the ML-KEM key object to operate on.
C_DeriveKey()
|
+-- CKM_PQCEE_MLKEM_GET_PUB_KEY
|
v
SafeQuard FM
|
v
ML-KEM public key
This is an example of how the host application retrieves the public key:
CK_OBJECT_HANDLE hPubKey = 0;
CK_MECHANISM getPubMech = {
.mechanism = CKM_PQCEE_MLKEM_GET_PUB_KEY,
.pParameter = NULL,
.ulParameterLen = 0
};
CK_OBJECT_CLASS getPubClass = CKO_DATA;
CK_ATTRIBUTE getPubTemplate[] = {
{CKA_CLASS, &getPubClass, sizeof(getPubClass)},
{CKA_TOKEN, &(CK_BBOOL){CK_TRUE}, sizeof(CK_BBOOL)}
};
rv = C_DeriveKey(
hSession,
&getPubMech,
hPrivKey, // Base = private key handle
NULL, // No template needed for public key retrieval
0,
&(hPubKey)
);
if (rv != CKR_OK) {
printf("[HOST] GetPubKey failed: 0x%lx\n", rv);
exit(EXIT_FAILURE);
}
printf("[HOST] Public Key Handle: %lu\n", hPubKey);
// Try to read the public key value (if stored as CKA_VALUE)
CK_ATTRIBUTE attr;
attr.type = CKA_VALUE;
attr.pValue = NULL;
attr.ulValueLen = 0;
rv = C_GetAttributeValue(hSession, hPubKey, &attr, 1);
if (rv == CKR_OK && attr.ulValueLen > 0) {
char *pubKey = (char *)malloc(attr.ulValueLen + 1);
attr.pValue = pubKey;
rv = C_GetAttributeValue(hSession, hPubKey, &attr, 1);
if (rv == CKR_OK) {
pubKey[attr.ulValueLen] = '\0';
printf("[HOST] Public Key from CKA_VALUE (length: %lu): %s\n", attr.ulValueLen, pubKey);
}
free(pubKey);
} else {
printf("[HOST] Public key value not available as attribute\n");
}
If the operation succeeds, hPubKey contains the returned object handle.
2.4.7. CKM_PQCEE_MLKEM_ENCAP¶
The CKM_PQCEE_MLKEM_ENCAP mechanism is used to perform ML-KEM
encapsulation.
The host application supplies the required ML-KEM public-key object and
parameters through the patched C_DeriveKey() request.
Conceptually, the operation is:
ML-KEM Public Key
|
v
CKM_PQCEE_MLKEM_ENCAP
|
v
SafeQuard FM
|
+----> Ciphertext (CKA_LABEL)
|
+----> Shared Secret
Important
The shared secret is the key to be used for symmetric encryption (AES-256) of the data. The ciphertext is return as CKA_LABEL attribute of the derived key object.
This is an example of the parameter structure used to perform ML-KEM encapsulation:
// ss_cls, keyType and aesKeyBits will be the same as used in the decapsulation step
CK_OBJECT_CLASS ss_cls = CKO_SECRET_KEY;
CK_KEY_TYPE keyType = CKK_AES; // Specify it's an AES key
UINT4 aesKeyBits = 256; // 256 bits for AES-256
CK_OBJECT_HANDLE hEncapState = 0;
// For encapsulation, we don't need to pass parameters
// The ciphertext will be generated and stored in the FM
CK_MECHANISM encapMech = {
.mechanism = CKM_PQCEE_MLKEM_ENCAP,
.pParameter = NULL,
.ulParameterLen = 0
};
// This is an example of the template for the derived symmetric key object that will hold the shared secret for encryption.
CK_ATTRIBUTE encapTemplate[] = {
{CKA_CLASS, &ss_cls, sizeof(ss_cls)},
{CKA_KEY_TYPE, &keyType, sizeof(keyType)},
{CKA_VALUE_LEN, &aesKeyBits, sizeof(aesKeyBits)}, // Key size in bits
{CKA_TOKEN, &(CK_BBOOL){CK_TRUE}, sizeof(CK_BBOOL)},
// encap should only allow encryption, not decryption, since it's a derived symmetric key that should only be used for encryption.
// The encrypt flag should be true, and the decrypt flag should be false.
{CKA_ENCRYPT, &(CK_BBOOL){CK_TRUE}, sizeof(CK_BBOOL)},
{CKA_DECRYPT, &(CK_BBOOL){CK_FALSE}, sizeof(CK_BBOOL)},
{CKA_PRIVATE, &(CK_BBOOL){CK_FALSE}, sizeof(CK_BBOOL)} // Allow access
};
rv = C_DeriveKey(
hSession,
&encapMech,
hPubKey, // Base = public key handle
encapTemplate, // No template needed for encapsulation state
sizeof(encapTemplate) / sizeof(CK_ATTRIBUTE),
&(hEncapState)
);
if (rv != CKR_OK) {
printf("[HOST] Encapsulation failed: 0x%lx\n", rv);
exit(EXIT_FAILURE);
}
printf("[HOST] Encapsulation State Handle: %lu\n", hEncapState);
If the operation succeeds, hEncapState contains the returned shared secret object handle.
Below is an example of how to retrieve the ciphertext from the encap key object:
CK_ATTRIBUTE labelAttr;
labelAttr.type = CKA_LABEL;
labelAttr.pValue = NULL;
labelAttr.ulValueLen = 0;
rv = C_GetAttributeValue(hSession, hEncapState, &labelAttr, 1);
if (rv == CKR_OK && labelAttr.ulValueLen > 0) {
ciphertext = (char *)malloc(labelAttr.ulValueLen + 1);
labelAttr.pValue = ciphertext;
rv = C_GetAttributeValue(hSession, hEncapState, &labelAttr, 1);
if (rv == CKR_OK) {
ciphertext[labelAttr.ulValueLen] = '\0';
ciphertext_len = labelAttr.ulValueLen;
printf("[HOST] Ciphertext retrieved from CKA_LABEL (length: %lu): %s\n", ciphertext_len, ciphertext);
} else {
printf("[HOST] Failed to get CKA_LABEL value: 0x%lx\n", rv);
free(ciphertext);
ciphertext = NULL;
}
}
2.4.8. CKM_PQCEE_MLKEM_DECAP¶
The CKM_PQCEE_MLKEM_DECAP mechanism is used to perform ML-KEM
decapsulation.
The host application supplies the ML-KEM private-key object and the ciphertext required by the operation.
Conceptually, the operation is:
ML-KEM Private Key
+
Ciphertext
|
v
CKM_PQCEE_MLKEM_DECAP
|
v
SafeQuard FM
|
v
Shared Secret
Important
Using the ciphertext obtained from the encapsulation step, the decapsulation operation returns the same shared secret that was generated during encapsulation. This shared secret can then be used for symmetric encryption (AES-256) of the data.
This is an example of the parameter structure used to perform ML-KEM decapsulation:
// ss_cls, keyType and aesKeyBits are the same as used in the encapsulation step
CK_OBJECT_CLASS ss_cls = CKO_SECRET_KEY;
CK_KEY_TYPE keyType = CKK_AES; // Specify it's an AES key
UINT4 aesKeyBits = 256; // 256 bits for AES-256
CK_OBJECT_HANDLE hDecapState = 0;
// For decapsulation, you need to pass the ciphertext as a parameter
// You would get this ciphertext from the encapsulation step
CK_MECHANISM decapMech = {
.mechanism = CKM_PQCEE_MLKEM_DECAP,
.pParameter = ciphertext,
.ulParameterLen = ciphertext_len
};
// This is an example of the template for the derived symmetric key object that will hold the shared secret for encryption.
CK_ATTRIBUTE decapTemplate[] = {
{CKA_CLASS, &ss_cls, sizeof(ss_cls)},
{CKA_KEY_TYPE, &keyType, sizeof(keyType)},
{CKA_VALUE_LEN, &aesKeyBits, sizeof(aesKeyBits)}, // Key size in bits
{CKA_TOKEN, &(CK_BBOOL){CK_TRUE}, sizeof(CK_BBOOL)},
// decap should only allow decryption, not encryption, since it's a derived symmetric key that should only be used for decryption.
// The encrypt flag should be false, and the decrypt flag should be true.
{CKA_ENCRYPT, &(CK_BBOOL){CK_FALSE}, sizeof(CK_BBOOL)},
{CKA_DECRYPT, &(CK_BBOOL){CK_TRUE}, sizeof(CK_BBOOL)},
{CKA_PRIVATE, &(CK_BBOOL){CK_FALSE}, sizeof(CK_BBOOL)} // Allow access
};
rv = C_DeriveKey(
hSession,
&decapMech,
hPrivKey, // Base = PRIVATE KEY
decapTemplate,
sizeof(decapTemplate) / sizeof(CK_ATTRIBUTE),
&(hDecapState)
);
free(ciphertext);
if (rv != CKR_OK) {
printf("[HOST] Decapsulation failed: 0x%lx\n", rv);
printf("[HOST] This might indicate the ciphertext is corrupted or wrong key\n");
}
printf("[HOST] Decapsulation State Handle: %lu\n", hDecapState);
If the operation succeeds, hDecapState contains the returned shared secret object handle.
2.4.9. Reference¶
For additional information about FM architecture and PKCS#11-patched functions, refer to the Thales ProtectServer 3 FM SDK documentation: