When to Use
Use this skill when a trading or treasury system authorises irreversible on-chain transfers with keys that live inside an HSM (AWS CloudHSM, YubiHSM 2, Thales/Entrust, or a custodian's PKCS#11 endpoint). Holding a private key in a web server's memory means an OS vulnerability or a core dump is a total loss; an HSM keeps the key inside a validated hardware boundary and exposes only C_Sign. The hard part is not buying the device — it is proving the key was never extractable, making sure the bytes you hand to C_Sign are the bytes the chain will verify against, and leaving an audit record of the signing requests you refused as well as the ones you granted.
HsmSigningManagerEngine is the policy and audit layer that sits between your execution logic and the vendor's PKCS#11 binding.
When NOT to Use
- As an HSM, a key store, or a signer. This module holds no key material and computes no signatures. It wraps a
signercallable that must be a real PKCS#11C_Signbinding (python-pkcs11, PyKCS11, a vendor SDK). Version 1.x simulated signing by deriving a "private key" assha256(b"HSM_ENTROPY_SEED_" + alias)and returning an HMAC — every such key was reconstructible from the alias alone and every signature was forgeable offline. Nothing built on 1.x output should be trusted. - As a signature validity check. The engine verifies that a returned signature has the right length and, for secp256k1, that
randsare in range. It cannot verify a signature against a public key without curve arithmetic it deliberately does not implement. There is nois_signature_validfield, because the 1.x one was hard-codedTrue. - As proof that a key is non-exportable. It audits attributes you supply. Those values must be read back from the device with
C_GetAttributeValue— an attribute you typed into a config is a document, not a control. - As the approval gate for a large transfer. Role authorisation here is bookkeeping in your process. Quorum and value thresholds belong in the HSM's own policy engine or the custodian's, outside anything the trading system can rewrite — see
multi-signature-approval-for-large-transfers. - For keys that never touch an online system. An offline signing ceremony has a different threat model; see
air-gapped-signing-workflow-for-cold-storage.
Prerequisites
- A key generated on the device (
C_GenerateKeyPairwithCKA_SENSITIVE=True,CKA_EXTRACTABLE=False), never imported from software. - The key's attributes read back via
C_GetAttributeValue:CKA_SENSITIVE,CKA_EXTRACTABLE,CKA_NEVER_EXTRACTABLE,CKA_ALWAYS_SENSITIVE. - The module's CMVP certificate number, not the datasheet's marketing claim — plus its Historical List date if it is a FIPS 140-2 certificate.
- A PKCS#11 session, PIN/token authentication, and a
signercallable with the signature(HsmKeyMetaData, bytes) -> bytes. - An explicit evaluation clock (
current_time_epoch) for every call, so audit records and FIPS findings are reproducible rather than dependent on wall time.
Workflow
- Register the key from device-read attributes, and refuse to overwrite one.
register_hardware_keyraisesHsmKeyAlreadyRegisteredErroron a duplicate alias. Decision point: this is deliberately not idempotent-by-overwrite. On a real HSM the equivalent mistake — generating over an existing label — destroys the only key that can sign for existing addresses, and silently replacing metadata hides a key swap. Take a new alias instead. - Audit non-exportability on both attributes, not one.
CKA_EXTRACTABLE=FalseblocksC_WrapKey;CKA_SENSITIVE=Trueblocks reading the value withC_GetAttributeValue. They stop different attacks and neither implies the other. Then check the history:CKA_NEVER_EXTRACTABLE=Falsemeans the key was extractable at some point, so a wrapped copy may already exist — today's attribute values say nothing about copies already taken. Treat that as exposed material and rotate; it cannot be repaired, becauseCKA_EXTRACTABLEis one-way. - Check FIPS validation currency against the certificate, not the label. CMVP stopped accepting FIPS 140-2 submissions on 2022-04-01 and moves all remaining FIPS 140-2 certificates to the Historical List on 2026-09-22. Decision point: individual certificates sunset earlier — five years after validation — so AWS CloudHSM
hsm1.medium(cert #4218) went historical on 2026-01-04, months ahead of the program-wide date. Pass the module's ownfips_historical_epoch; the engine falls back to the program date only when you do not know it. Historical status is a migration finding, not an outage: CMVP still supports historical modules for existing systems. - Declare what the signing input actually is, and let the engine refuse the mismatch.
CKM_ECDSAsigns a pre-computed digest; pure Ed25519 (RFC 8032,CKM_EDDSAwithout the prehash parameter) signs the message. Handing a digest to Ed25519 signs the digest, not the transaction. The engine never hashes on your behalf — 1.x ran SHA-256 over whatever it received, so a caller passing an Ethereum Keccak-256 sighash got a perfectly valid signature oversha256(keccak256(tx)), which no verifier will accept. Digest length is checked exactly, becauseCKM_ECDSAtruncates input longer than the base point order and silently signs a different value. - Delegate to
C_Signand validate what comes back. PKCS#11 returns ECDSA as rawr||s, each zero-padded to 32 bytes for secp256k1 — 64 bytes, not DER. A ~70–72 byte value means your binding already re-encoded it and must be decoded before use. - Normalise secp256k1 to low-S before broadcast.
(r, s)and(r, n − s)are both valid, so a high-S signature is malleable: a third party can rewrite it and change the txid. Bitcoin has treated high-S as non-standard since Core 0.11.1 (BIP-146) and Ethereum rejectss > secp256k1n/2outright (EIP-2). PKCS#11 does not require the device to return low-S, soenforce_low_s=True(the default) flips it on the way out. Do not apply this to Ed25519 — RFC 8032 signatures are already canonical. - Treat a failed
C_Signas ambiguous, never as "nothing happened". A timeout can lose the response to an operation the device completed.HsmSignerErrorsays so explicitly. Reconcile against the device's own log before retrying, exactly as you would a broker order — seeorder-placement-idempotency. - Keep the denials. Every outcome —
AUTHORIZATION_DENIED,KEY_NOT_FOUND,KEY_DISABLED,INPUT_DOMAIN_VIOLATION,SIGNER_FAILED,MALFORMED_SIGNATURE,EXPORT_ATTEMPT_REJECTED— is written to the hash-chained log before the exception propagates. An auditor samples for refused and anomalous attempts; 1.x recorded only successes. Callverify_audit_chain()and ship records to append-only external storage (WORM bucket, SIEM), because a chain an attacker can rewrite wholesale is tamper-evident, not tamper-proof.
Full procedure: see
references/workflows.md. Standards reference: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
- Simulating the HSM in application code. Deriving key material from an alias, a config string, or any deterministic seed produces keys anyone can reconstruct. This is what 1.x of this skill did; it is the single most dangerous shortcut in the whole area.
- Reading "non-exportable" as a single flag. A key with
CKA_EXTRACTABLE=FalsebutCKA_SENSITIVE=Falsecan have its private value read straight back withC_GetAttributeValue— no wrapping needed. - Trusting current attributes as lifetime evidence.
CKA_NEVER_EXTRACTABLEandCKA_ALWAYS_SENSITIVEare the only attributes that speak to the key's history. Without them, "extractable=False" only means nobody can wrap it from now on. - Re-hashing an already-hashed payload. SHA-256 over a Keccak-256 sighash yields a valid signature over the wrong value. The chain rejects it, and the failure looks like a key problem rather than a domain problem.
- Handing pure Ed25519 a digest. It signs the 32 bytes you gave it. The signature verifies against those 32 bytes and against nothing the network cares about.
- Passing a digest longer than the curve order to
CKM_ECDSA. It is truncated, not rejected — SHA-512 into a secp256k1 key signs the first 32 bytes. - Assuming the HSM returns DER, or that it returns low-S. PKCS#11 specifies raw
r||sand says nothing about S normalisation. Both assumptions produce transactions that are rejected downstream for reasons that look nothing like the cause. - Retrying a timed-out
C_Sign. The device may have signed already. Two signatures over two different nonces for the same input is not automatically harmful, but two broadcast transactions can be — reconcile first. - Claiming a FIPS level the certificate does not support. "FIPS 140-2 Level 3" is now a sunsetting claim, and the level applies to the module, not to every service in it. AWS CloudHSM supports ed25519 only on
hsm2m.mediumin non-FIPS mode — a cluster-mode choice that is irreversible after creation. - Assuming an HSM makes the nonce safe. ECDSA nonce generation happens inside the device and is not observable; a biased or repeated
kleaks the private key. RFC 6979 deterministic derivation is the mitigation, but you cannot verify from outside that the device does it — treat it as a vendor question with a written answer. - Sharing one PKCS#11 session across trading threads without synchronisation. Sessions are stateful; concurrent
C_SignInit/C_Signon one session interleaves. Use a session pool. The engine's own registry and audit chain are lock-guarded, but that does not make the vendor library thread-safe. - Logging only successes. A rejected export attempt or a burst of
AUTHORIZATION_DENIEDfrom one identity is the earliest signal of a compromised caller, and it is exactly what an unlogged exception throws away.
Verification
- Register a clean secp256k1 key with
fips_certification="FIPS_140_3_LEVEL_3"and a certificate number, and confirmaudit_key_attributesreturns[]. - Register the same alias twice and confirm
HsmKeyAlreadyRegisteredError— 1.x silently overwrote the live key. Confirm""and" "are rejected as aliases; 1.x accepted both. - Register a key with
sensitive=False, extractable=Falseand confirm a CRITICAL finding: non-extractable does not mean non-readable. - Register a key with
never_extractable=Falseand confirm a HIGH finding even though it is currently protected. - Audit a
FIPS_140_2_LEVEL_3key atFIPS_140_2_PROGRAM_HISTORICAL_EPOCHand confirm HIGH; audit it three months earlier and confirm MEDIUM. Supply CloudHSMhsm1.medium's own 2026-01-04 date and confirm it goes HIGH months before the program-wide date. - Call
attempt_export_private_keyand confirm it raises and appends anEXPORT_ATTEMPT_REJECTEDrecord. Repeat withextractable=Trueand confirm it still raises — 1.x returnedNone(a silent pass) in exactly that case. - Sign with a capturing signer and confirm it received the digest verbatim — 1.x re-hashed it.
- Feed a
RAW_MESSAGEto a secp256k1 key, a digest to an Ed25519 key, a 31- and a 33-byte digest, and an unrecognised encoding string: each must raise and produce anINPUT_DOMAIN_VIOLATIONrecord. - Return a high-S signature from the signer and confirm the reported signature is normalised and
was_low_s_normalization_appliedis True. Confirmn/2itself is treated as low-S andn/2 + 1as high. - Return a 71-byte DER blob and confirm
MALFORMED_SIGNATURE. RaiseTimeoutErrorfrom the signer and confirm the error text warns that the operation may still have completed. - Request a signature as
AUDITOR, then asADMIN, and confirm both are denied by default and both appear in the log; construct the engine withallowed_signing_roles=("OPERATOR", "ADMIN")and confirm ADMIN then succeeds. disable_keya key, attempt to sign, and confirmKEY_DISABLED.- Mutate or delete any record in
_audit_logand confirmverify_audit_chain()returns False. Sign from 24 threads and confirm sequence numbers are0..23with no gaps. - Run
python -m unittest discover -s skills/hardware-security-module-hsm-for-signing-keys/scriptsand confirm a 100% pass rate.
Related Skills
crypto-wallet-key-custody-securityhot-cold-wallet-split-for-trading-botsair-gapped-signing-workflow-for-cold-storageshamir-secret-sharing-for-key-backupkey-rotation-schedule-for-hot-wallet-keysmulti-signature-approval-for-large-transfersmulti-party-computation-mpc-custody-solutionssegregation-of-duties-for-custody-operationspost-incident-forensics-for-suspected-key-compromise