CyberLearn
← Back to advanced
Advanced

Reverse-Engineering

Reverse-engineering learning platform.

Log in to track progress
3–4wC

What you'll learn

  • **[Hardware Security Module Emulator](./PROJECTS/advanced/hsm-emulator)**
<!-- ©AngelaMos | 2026 --> <!-- CONFORMANCE.md -->

PKCS#11 v2.40 Conformance Statement

The AngelaMos HSM Emulator implements the full Cryptoki (PKCS#11) v2.40 C ABI: all 68 functions in the canonical CK_FUNCTION_LIST, machine-checked against the vendored OASIS headers at build time. This document records every place where the module narrows behavior: the exact return code it gives, the spec clause that permits it, and why. Every narrowing here is a documented decision with a defined return value.

A function that is not applicable to a fixed software token returns the specific code the spec defines for that situation. The CKR_FUNCTION_NOT_SUPPORTED results that remain mark the boundary between single-shot and multi-part operation surfaces, covered in section 2.

Specifications

  • PKCS#11 Base Specification v2.40 (OASIS, errata 01) — function semantics and return codes.
  • PKCS#11 Current Mechanisms v2.40 (OASIS, errata 01) — per-mechanism parameters.

Section numbers below refer to the Base specification unless a line names the Mechanisms document.


1. Function-level conformance

1.1 C_WaitForSlotEvent — §5.5 (slot and token management)

The module exposes a single fixed slot (ID 0) whose token is always present. No insertion or removal event can ever occur, so the function reports that fact precisely rather than pretending to support hardware slot events.

CallReturnBasis
flags has CKF_DONT_BLOCK, no event pendingCKR_NO_EVENT§5.5: a non-blocking poll with no pending event returns CKR_NO_EVENT. For a fixed slot, no event is ever pending, so this is always the answer.
flags clears CKF_DONT_BLOCK (blocking)CKR_FUNCTION_NOT_SUPPORTEDA blocking wait must not return until an event occurs. For a fixed software slot no event can occur, so blocking would hang the caller forever. The module declines the blocking mode instead.
pReserved != NULL_PTRCKR_ARGUMENTS_BAD§5.5: pReserved is reserved and must be NULL_PTR in v2.40.
called before C_InitializeCKR_CRYPTOKI_NOT_INITIALIZED§5.4 general semantics.

Interop note: pkcs11-tool --wait calls this in blocking mode and therefore receives CKR_FUNCTION_NOT_SUPPORTED immediately rather than blocking. Hosts that poll with CKF_DONT_BLOCK (the common case for slot enumeration) get the correct CKR_NO_EVENT.

1.2 C_GetFunctionStatus, C_CancelFunction — §5.15 (parallel function management)

Both are legacy functions from the era of parallel (asynchronous) Cryptoki calls. v2.40 has no parallel execution model, and the spec defines the canonical answer for a serial implementation:

CallReturnBasis
C_GetFunctionStatusCKR_FUNCTION_NOT_PARALLEL§5.15: the only meaningful return for a library that does not run functions in parallel.
C_CancelFunctionCKR_FUNCTION_NOT_PARALLEL§5.15: same.

1.3 C_SeedRandom — §5.14 (random number generation)

CallReturnBasis
C_SeedRandomCKR_RANDOM_SEED_NOT_SUPPORTEDThe RNG is the operating-system CSPRNG, drawn through std.Io.randomSecure (getrandom(2), arc4random_buf, or /dev/urandom depending on platform and libc). Caller-supplied seed material cannot meaningfully reseed it, so the module declines rather than silently discarding the seed (which would mislead the caller).
C_GenerateRandomfully supported

2. Operation-surface boundaries

These are the deliberate edges of the multi-part operation surface. Each returns a specific code so a caller can distinguish "wrong call for this mechanism" from a runtime failure.

2.1 AES-GCM is RUP-safe buffered — §5.8, §5.9

CKM_AES_GCM multi-part encryption and decryption buffer the entire message and run the authenticated operation once at *Final:

  • C_EncryptUpdate / C_DecryptUpdate append the part to an internal buffer and emit 0 bytes. §5.8/§5.9 permit an Update to produce fewer output bytes than it consumes (block buffering); producing the whole result at *Final is conformant.
  • C_EncryptFinal emits ciphertext + 128-bit tag; C_DecryptFinal verifies the tag and only then releases plaintext.

This is a security decision. A streaming GCM decrypt built on incremental release would hand back unverified plaintext before the tag is checked (release of unverified plaintext, "RUP") — an anti-pattern for an HSM. Buffering until the tag verifies makes RUP impossible by construction.

Because the buffer holds the whole message, a single GCM message is bounded:

ConditionReturn
buffered length would exceed 16 MiB (max_gcm_stream_len), encryptCKR_DATA_LEN_RANGE
buffered length would exceed 16 MiB, decryptCKR_ENCRYPTED_DATA_LEN_RANGE

Strict parameter validation (CK_GCM_PARAMS):

ParameterAcceptedElse
ulIvLenexactly 12 bytesCKR_MECHANISM_PARAM_INVALID
ulIvBits0 or 96CKR_MECHANISM_PARAM_INVALID
ulTagBitsexactly 128CKR_MECHANISM_PARAM_INVALID
pIvnon-NULLCKR_MECHANISM_PARAM_INVALID
ulAADLen≤ 256 bytesCKR_ARGUMENTS_BAD

Interop note: a host MUST request a 128-bit tag and supply a 12-byte IV. With pkcs11-tool that means --iv <24 hex chars> --tag-bits-len 128; omitting either trips CKR_MECHANISM_PARAM_INVALID.

2.2 Dual-function operations — §5.12

FunctionSupported modesElse
C_DigestEncryptUpdate, C_SignEncryptUpdate (encrypt side)AES-CBC, AES-CBC-PAD, AES-GCM
C_DecryptDigestUpdate, C_DecryptVerifyUpdate (decrypt side)AES-CBC onlynon-CBC → CKR_FUNCTION_NOT_SUPPORTED
any dual-function leg using an RSA sign/verify operationCKR_FUNCTION_NOT_SUPPORTED

The decrypt side couples the recovered plaintext of each C_Decrypt*Update into the digest/verify operation. That coupling is only exact when the cipher releases exactly the decrypted bytes on every call:

  • AES-CBC releases each decrypted block immediately, so bytes-out equals bytes-to-digest per call. Supported.
  • AES-CBC-PAD holds back the final block until C_DecryptFinal (it cannot know the padding until the end), so the digest would miss the last block.
  • AES-GCM buffers everything and releases at C_DecryptFinal (§2.1), so the digest would receive nothing incrementally.

Coupling either of the latter would desynchronize the pair, so the module returns CKR_FUNCTION_NOT_SUPPORTED rather than producing a silently wrong digest. The encrypt side has no such constraint — it digests/signs the input plaintext, which is fully available on each call, so all three modes work.

2.3 RSA is single-shot — §5.8, §5.9, §5.11

Every RSA operation is a single modular exponentiation over the whole input, so RSA has no multi-part form:

Call on an RSA operationReturn
C_EncryptUpdate / C_EncryptFinalCKR_FUNCTION_NOT_SUPPORTED
C_DecryptUpdate / C_DecryptFinalCKR_FUNCTION_NOT_SUPPORTED
C_SignUpdate / C_SignFinalCKR_FUNCTION_NOT_SUPPORTED
C_VerifyUpdate / C_VerifyFinalCKR_FUNCTION_NOT_SUPPORTED

Use the one-shot C_Encrypt / C_Decrypt / C_Sign / C_Verify. The hash-then-sign mechanisms (CKM_SHA256_RSA_PKCS, the PSS variants) still need the full message before the single RSA operation, so they too are one-shot.

2.4 Sign / Verify-Recover — §5.11

MechanismSupportedElse
CKM_RSA_PKCS (EMSA-PKCS1-v1.5 type 1, message recoverable from the signature)yesother mechanism → CKR_MECHANISM_INVALID

C_SignRecover rejects input that cannot fit the modulus with PKCS#1 v1.5 overhead (data length + 11 bytes > modulus) with CKR_DATA_LEN_RANGE. C_VerifyRecover returns the recovered message; a signature whose length is not the modulus length returns CKR_SIGNATURE_LEN_RANGE, and a malformed encoding returns CKR_SIGNATURE_INVALID. Raw recover (CKM_RSA_X_509) is not offered.

2.5 Get / SetOperationState — §5.6

Operation state is digest-only:

CallReturnBasis
C_GetOperationState with an active sign/verify/encrypt/decrypt/sign-recover/verify-recover operationCKR_STATE_UNSAVEABLE§5.6: a library may decline to save state it cannot serialize. Only digest state is saveable here.
C_GetOperationState with no operation activeCKR_OPERATION_NOT_INITIALIZED§5.6.
C_SetOperationState with non-zero hEncryptionKey or hAuthenticationKeyCKR_KEY_NOT_NEEDED§5.6: a digest needs no key, so passing one is an error.
C_SetOperationState with a malformed blob (wrong version byte, unknown hasher tag, wrong length)CKR_SAVED_STATE_INVALID§5.6.

The saved blob is [version][hasher tag][raw hasher state], validated on restore. It is opaque and same-build only: it carries the raw standard-library hasher state, whose layout is not stable across builds. PKCS#11 does not promise operation-state portability across implementations or builds (§5.6). Restore is byte-exact within the same binary; any other input fails closed via the version, tag, and exact-length checks above.


3. Mechanism constraints

3.1 ECDH key derivation — §5.13, Mechanisms (CKM_ECDH1_DERIVE)

AspectValueElse
KDFCKD_NULL onlyother KDF → CKR_MECHANISM_PARAM_INVALID
peer public pointraw SEC1 uncompressed or DER OCTET STRING-wrappedmalformed → CKR_MECHANISM_PARAM_INVALID
curvesP-256, P-384
shared datanone (CKD_NULL carries no shared data)

3.2 RSA — Mechanisms (CKM_RSA_PKCS, _PSS, _OAEP)

AspectValueElse
key size2048–4096 bitsoutside range → CKR_KEY_SIZE_RANGE
public exponentfixed at 65537 (F4)not selectable at keygen
PSS / OAEP hashSHA-256, SHA-384, SHA-512other → CKR_MECHANISM_PARAM_INVALID
MGF hashmust equal the content hashmismatch → CKR_MECHANISM_PARAM_INVALID
OAEP label (source)not supportedulSourceDataLen != 0CKR_MECHANISM_PARAM_INVALID

Dedicated CKM_SHA384_RSA_PKCS / CKM_SHA512_RSA_PKCS mechanisms are not advertised; SHA-384 and SHA-512 are reachable through the CKM_RSA_PKCS_PSS and CKM_RSA_PKCS_OAEP parameter hashAlg.

3.3 AES — Mechanisms (CKM_AES_*)

AES-128 and AES-256 only. AES-192 is not implemented (the Zig standard library exposes no 192-bit AES). A key length outside {16, 32} bytes → CKR_KEY_SIZE_RANGE.

3.4 ECDSA — Mechanisms (CKM_ECDSA*)

Curves P-256 and P-384. Mechanisms CKM_ECDSA (pre-hashed input) and CKM_ECDSA_SHA256. CKM_ECDSA_SHA384 and CKM_ECDSA_SHA512 are out of scope.

3.5 Key wrap — §5.13

AspectValueElse
wrappable targetsecret keys (CKO_SECRET_KEY) onlyasymmetric target → CKR_KEY_NOT_WRAPPABLE
wrapping mechanismsCKM_AES_KEY_WRAP (RFC 3394), CKM_RSA_PKCS_OAEPother → CKR_MECHANISM_INVALID
unextractable targetrefusedCKR_KEY_UNEXTRACTABLE
tampered wrapped blob on unwraprefusedCKR_WRAPPED_KEY_INVALID

4. Object and token model (informative)

  • One fixed slot (ID 0), always present, hosting one token. Login is required to see or use private objects.
  • Encrypted at rest. Token objects persist to a file under a selective envelope: only sensitive attribute values are sealed with AES-256-GCM under a per-token master key. The master key is wrapped under a single User-PIN keyslot (Argon2id-derived KEK). There is no SO keyslot for user secrets by design — the Security Officer must not be able to read user key material.
  • Public objects and attributes stay in plaintext and are visible before login, which is spec-correct: only private/sensitive material is gated by login.
  • In memory, sensitive attributes are plaintext only while the User is logged in; logout and session teardown re-seal them and zeroize the master key.

5. Advertised mechanism list

C_GetMechanismList returns these 21 mechanisms. Key-size units are mechanism-dependent per the spec (bits for RSA and EC, bytes for AES and HMAC).

MechanismMinMaxFlags
CKM_SHA25600DIGEST
CKM_SHA38400DIGEST
CKM_SHA51200DIGEST
CKM_SHA256_HMAC3264SIGN, VERIFY
CKM_SHA384_HMAC3264SIGN, VERIFY
CKM_SHA512_HMAC3264SIGN, VERIFY
CKM_AES_KEY_GEN1632GENERATE
CKM_AES_CBC1632ENCRYPT, DECRYPT
CKM_AES_CBC_PAD1632ENCRYPT, DECRYPT
CKM_AES_GCM1632ENCRYPT, DECRYPT
CKM_EC_KEY_PAIR_GEN256384GENERATE_KEY_PAIR, EC_NAMEDCURVE
CKM_ECDSA256384SIGN, VERIFY, EC_NAMEDCURVE
CKM_ECDSA_SHA256256384SIGN, VERIFY, EC_NAMEDCURVE
CKM_ECDH1_DERIVE256384DERIVE, EC_NAMEDCURVE
CKM_RSA_PKCS_KEY_PAIR_GEN20484096GENERATE_KEY_PAIR
CKM_RSA_PKCS20484096SIGN, VERIFY, ENCRYPT, DECRYPT, SIGN_RECOVER, VERIFY_RECOVER
CKM_SHA256_RSA_PKCS20484096SIGN, VERIFY
CKM_RSA_PKCS_PSS20484096SIGN, VERIFY
CKM_SHA256_RSA_PKCS_PSS20484096SIGN, VERIFY
CKM_RSA_PKCS_OAEP20484096ENCRYPT, DECRYPT, WRAP, UNWRAP
CKM_AES_KEY_WRAP1632WRAP, UNWRAP

6. Summary: deliberate return codes

BoundaryReturn code
C_WaitForSlotEvent, non-blocking pollCKR_NO_EVENT
C_WaitForSlotEvent, blocking modeCKR_FUNCTION_NOT_SUPPORTED
C_WaitForSlotEvent, pReserved != NULLCKR_ARGUMENTS_BAD
C_GetFunctionStatus, C_CancelFunctionCKR_FUNCTION_NOT_PARALLEL
C_SeedRandomCKR_RANDOM_SEED_NOT_SUPPORTED
GCM message over 16 MiBCKR_DATA_LEN_RANGE / CKR_ENCRYPTED_DATA_LEN_RANGE
GCM bad parametersCKR_MECHANISM_PARAM_INVALID / CKR_ARGUMENTS_BAD
dual-function decrypt side, non-CBCCKR_FUNCTION_NOT_SUPPORTED
RSA multi-part (*Update / *Final)CKR_FUNCTION_NOT_SUPPORTED
Sign/Verify-Recover, non-CKM_RSA_PKCSCKR_MECHANISM_INVALID
Get/SetOperationState, non-digest operationCKR_STATE_UNSAVEABLE
SetOperationState with a key handleCKR_KEY_NOT_NEEDED
SetOperationState, malformed blobCKR_SAVED_STATE_INVALID
ECDH non-CKD_NULL KDFCKR_MECHANISM_PARAM_INVALID
OAEP with a labelCKR_MECHANISM_PARAM_INVALID
AES key length not 16/32 bytesCKR_KEY_SIZE_RANGE
wrap of an asymmetric targetCKR_KEY_NOT_WRAPPABLE
wrap of an unextractable keyCKR_KEY_UNEXTRACTABLE

Every entry above is exercised by the unit tests, the in-process smoke harness, or a cross-process pkcs11-tool run; the corrected slot/parallel/RNG codes are asserted in examples/smoke.zig against the built shared object.