Cybersecurity-Projects/PROJECTS/advanced/hsm-emulator/learn/CONFORMANCE.md

302 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- ©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.
| Call | Return | Basis |
|------|--------|-------|
| `flags` has `CKF_DONT_BLOCK`, no event pending | `CKR_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_SUPPORTED` | A 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_PTR` | `CKR_ARGUMENTS_BAD` | §5.5: `pReserved` is reserved and must be `NULL_PTR` in v2.40. |
| called before `C_Initialize` | `CKR_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:
| Call | Return | Basis |
|------|--------|-------|
| `C_GetFunctionStatus` | `CKR_FUNCTION_NOT_PARALLEL` | §5.15: the only meaningful return for a library that does not run functions in parallel. |
| `C_CancelFunction` | `CKR_FUNCTION_NOT_PARALLEL` | §5.15: same. |
### 1.3 `C_SeedRandom` — §5.14 (random number generation)
| Call | Return | Basis |
|------|--------|-------|
| `C_SeedRandom` | `CKR_RANDOM_SEED_NOT_SUPPORTED` | The 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_GenerateRandom` | fully 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:
| Condition | Return |
|-----------|--------|
| buffered length would exceed 16 MiB (`max_gcm_stream_len`), encrypt | `CKR_DATA_LEN_RANGE` |
| buffered length would exceed 16 MiB, decrypt | `CKR_ENCRYPTED_DATA_LEN_RANGE` |
Strict parameter validation (`CK_GCM_PARAMS`):
| Parameter | Accepted | Else |
|-----------|----------|------|
| `ulIvLen` | exactly 12 bytes | `CKR_MECHANISM_PARAM_INVALID` |
| `ulIvBits` | `0` or `96` | `CKR_MECHANISM_PARAM_INVALID` |
| `ulTagBits` | exactly `128` | `CKR_MECHANISM_PARAM_INVALID` |
| `pIv` | non-NULL | `CKR_MECHANISM_PARAM_INVALID` |
| `ulAADLen` | ≤ 256 bytes | `CKR_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
| Function | Supported modes | Else |
|----------|-----------------|------|
| `C_DigestEncryptUpdate`, `C_SignEncryptUpdate` (encrypt side) | AES-CBC, AES-CBC-PAD, AES-GCM | — |
| `C_DecryptDigestUpdate`, `C_DecryptVerifyUpdate` (decrypt side) | AES-CBC only | non-CBC → `CKR_FUNCTION_NOT_SUPPORTED` |
| any dual-function leg using an RSA sign/verify operation | — | `CKR_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 operation | Return |
|--------------------------|--------|
| `C_EncryptUpdate` / `C_EncryptFinal` | `CKR_FUNCTION_NOT_SUPPORTED` |
| `C_DecryptUpdate` / `C_DecryptFinal` | `CKR_FUNCTION_NOT_SUPPORTED` |
| `C_SignUpdate` / `C_SignFinal` | `CKR_FUNCTION_NOT_SUPPORTED` |
| `C_VerifyUpdate` / `C_VerifyFinal` | `CKR_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
| Mechanism | Supported | Else |
|-----------|-----------|------|
| `CKM_RSA_PKCS` (EMSA-PKCS1-v1.5 type 1, message recoverable from the signature) | yes | other 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**:
| Call | Return | Basis |
|------|--------|-------|
| `C_GetOperationState` with an active sign/verify/encrypt/decrypt/sign-recover/verify-recover operation | `CKR_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 active | `CKR_OPERATION_NOT_INITIALIZED` | §5.6. |
| `C_SetOperationState` with non-zero `hEncryptionKey` or `hAuthenticationKey` | `CKR_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)
| Aspect | Value | Else |
|--------|-------|------|
| KDF | `CKD_NULL` only | other KDF → `CKR_MECHANISM_PARAM_INVALID` |
| peer public point | raw SEC1 uncompressed **or** DER `OCTET STRING`-wrapped | malformed → `CKR_MECHANISM_PARAM_INVALID` |
| curves | P-256, P-384 | — |
| shared data | none (`CKD_NULL` carries no shared data) | — |
### 3.2 RSA — Mechanisms (CKM_RSA_PKCS, _PSS, _OAEP)
| Aspect | Value | Else |
|--------|-------|------|
| key size | 20484096 bits | outside range → `CKR_KEY_SIZE_RANGE` |
| public exponent | fixed at 65537 (F4) | not selectable at keygen |
| PSS / OAEP hash | SHA-256, SHA-384, SHA-512 | other → `CKR_MECHANISM_PARAM_INVALID` |
| MGF hash | must equal the content hash | mismatch → `CKR_MECHANISM_PARAM_INVALID` |
| OAEP label (source) | not supported | `ulSourceDataLen != 0``CKR_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
| Aspect | Value | Else |
|--------|-------|------|
| wrappable target | secret keys (`CKO_SECRET_KEY`) only | asymmetric target → `CKR_KEY_NOT_WRAPPABLE` |
| wrapping mechanisms | `CKM_AES_KEY_WRAP` (RFC 3394), `CKM_RSA_PKCS_OAEP` | other → `CKR_MECHANISM_INVALID` |
| unextractable target | refused | `CKR_KEY_UNEXTRACTABLE` |
| tampered wrapped blob on unwrap | refused | `CKR_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).
| Mechanism | Min | Max | Flags |
|-----------|-----|-----|-------|
| `CKM_SHA256` | 0 | 0 | DIGEST |
| `CKM_SHA384` | 0 | 0 | DIGEST |
| `CKM_SHA512` | 0 | 0 | DIGEST |
| `CKM_SHA256_HMAC` | 32 | 64 | SIGN, VERIFY |
| `CKM_SHA384_HMAC` | 32 | 64 | SIGN, VERIFY |
| `CKM_SHA512_HMAC` | 32 | 64 | SIGN, VERIFY |
| `CKM_AES_KEY_GEN` | 16 | 32 | GENERATE |
| `CKM_AES_CBC` | 16 | 32 | ENCRYPT, DECRYPT |
| `CKM_AES_CBC_PAD` | 16 | 32 | ENCRYPT, DECRYPT |
| `CKM_AES_GCM` | 16 | 32 | ENCRYPT, DECRYPT |
| `CKM_EC_KEY_PAIR_GEN` | 256 | 384 | GENERATE_KEY_PAIR, EC_NAMEDCURVE |
| `CKM_ECDSA` | 256 | 384 | SIGN, VERIFY, EC_NAMEDCURVE |
| `CKM_ECDSA_SHA256` | 256 | 384 | SIGN, VERIFY, EC_NAMEDCURVE |
| `CKM_ECDH1_DERIVE` | 256 | 384 | DERIVE, EC_NAMEDCURVE |
| `CKM_RSA_PKCS_KEY_PAIR_GEN` | 2048 | 4096 | GENERATE_KEY_PAIR |
| `CKM_RSA_PKCS` | 2048 | 4096 | SIGN, VERIFY, ENCRYPT, DECRYPT, SIGN_RECOVER, VERIFY_RECOVER |
| `CKM_SHA256_RSA_PKCS` | 2048 | 4096 | SIGN, VERIFY |
| `CKM_RSA_PKCS_PSS` | 2048 | 4096 | SIGN, VERIFY |
| `CKM_SHA256_RSA_PKCS_PSS` | 2048 | 4096 | SIGN, VERIFY |
| `CKM_RSA_PKCS_OAEP` | 2048 | 4096 | ENCRYPT, DECRYPT, WRAP, UNWRAP |
| `CKM_AES_KEY_WRAP` | 16 | 32 | WRAP, UNWRAP |
---
## 6. Summary: deliberate return codes
| Boundary | Return code |
|----------|-------------|
| `C_WaitForSlotEvent`, non-blocking poll | `CKR_NO_EVENT` |
| `C_WaitForSlotEvent`, blocking mode | `CKR_FUNCTION_NOT_SUPPORTED` |
| `C_WaitForSlotEvent`, `pReserved != NULL` | `CKR_ARGUMENTS_BAD` |
| `C_GetFunctionStatus`, `C_CancelFunction` | `CKR_FUNCTION_NOT_PARALLEL` |
| `C_SeedRandom` | `CKR_RANDOM_SEED_NOT_SUPPORTED` |
| GCM message over 16 MiB | `CKR_DATA_LEN_RANGE` / `CKR_ENCRYPTED_DATA_LEN_RANGE` |
| GCM bad parameters | `CKR_MECHANISM_PARAM_INVALID` / `CKR_ARGUMENTS_BAD` |
| dual-function decrypt side, non-CBC | `CKR_FUNCTION_NOT_SUPPORTED` |
| RSA multi-part (`*Update` / `*Final`) | `CKR_FUNCTION_NOT_SUPPORTED` |
| Sign/Verify-Recover, non-`CKM_RSA_PKCS` | `CKR_MECHANISM_INVALID` |
| Get/SetOperationState, non-digest operation | `CKR_STATE_UNSAVEABLE` |
| SetOperationState with a key handle | `CKR_KEY_NOT_NEEDED` |
| SetOperationState, malformed blob | `CKR_SAVED_STATE_INVALID` |
| ECDH non-`CKD_NULL` KDF | `CKR_MECHANISM_PARAM_INVALID` |
| OAEP with a label | `CKR_MECHANISM_PARAM_INVALID` |
| AES key length not 16/32 bytes | `CKR_KEY_SIZE_RANGE` |
| wrap of an asymmetric target | `CKR_KEY_NOT_WRAPPABLE` |
| wrap of an unextractable key | `CKR_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.