Using SafeQuard in Thales ProtectServer ======================================== 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` Architecture ---------------------------- The host application does not directly communicate with a custom SafeQuard function. Instead, the application uses the standard PKCS#11 interface: .. code-block:: text 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. Host Function ---------------------------- The SafeQuard FM patches the following PKCS#11 function: .. code-block:: c 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: .. code-block:: c rv = C_DeriveKey( hSession, &mechanism, hBaseKey, template, templateCount, &hKey ); The SafeQuard FM determines the requested operation from `pMechanism->mechanism`. ML-KEM Parameter ---------------------------- This is an example mlkem.h file that defines the ML-KEM parameter values: .. code-block:: c #include #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 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. .. code-block:: c 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. .. code-block:: c 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. 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. .. code-block:: text 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: .. code-block:: c 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)} }; .. code-block:: c 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. 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: .. code-block:: text 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: .. code-block:: c // 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 }; .. code-block:: c 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: .. code-block:: c 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; } } 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: .. code-block:: text 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: .. code-block:: c // 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 }; .. code-block:: c 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. .. Operation Flow .. -------------- .. A SafeQuard ML-KEM request follows this general flow: .. .. code-block:: text .. 1. Host application .. | .. | C_DeriveKey() .. v .. 2. ProtectToolkit PKCS#11 interface .. | .. v .. 3. ProtectServer message processing .. | .. | Function is patched .. v .. 4. SafeQuard FM .. | .. | Inspect CK_MECHANISM .. | .. +------------------------------+ .. | | .. v v .. ML-KEM Key Generation ML-KEM Operation .. | | .. | | .. +--------------+---------------+ .. | .. v .. HSM firmware .. | .. v .. Result .. | .. v .. Host application .. Important Considerations .. ------------------------ .. Patched PKCS#11 Function .. The host application should call the normal PKCS#11 `C_DeriveKey()` .. function. The SafeQuard-specific behavior is selected using the custom .. mechanism. .. Do not treat `FM_C_DeriveKey()` as a separate host API. The FM-side .. function is the patched implementation that receives control when the .. corresponding PKCS#11 function has been patched. .. Only One Patched PKCS#11 FM .. Thales documents that only one PKCS#11-patched FM can be loaded and used at .. a time. Loading another patched FM overwrites the existing patched FM. .. Therefore, the SafeQuard FM must be considered when deploying other .. PKCS#11-patched functionality. .. FM Security .. ~~~~~~~~~~~ .. FMs execute as part of the HSM firmware. Thales requires downloaded FM .. images to have a valid signature, and the certificate used to validate the .. FM must exist in the HSM Admin Token. The FM download and verification also .. require the HSM Administrator's participation. .. Because a patched function can change the behavior of standard PKCS#11 .. operations, changes to patched functions should be tested carefully before .. deployment. Thales specifically warns that incorrectly implemented function .. patching can make an HSM unusable. .. Testing .. ------- .. SafeQuard FM development should be tested in two stages. .. First, test the FM in **Emulation Mode**. This allows the host application .. and FM behavior to be tested before deployment to an HSM. .. After the emulation tests pass, test the FM on the actual ProtectServer HSM. .. Thales notes that the HSM testing stage is important because components such .. as function patching and host-to-HSM message dispatch are not fully tested .. during emulation. .. The recommended development flow is: .. .. code-block:: text .. Development .. | .. v .. Emulation Build .. | .. v .. Emulation Test .. | .. v .. Adapter Build .. | .. v .. HSM Test .. | .. v .. Production Build .. | .. v .. Acceptance Test Reference --------- For additional information about FM architecture and PKCS#11-patched functions, refer to the Thales ProtectServer 3 FM SDK documentation: `Thales ProtectServer 3 FM architecture `_