Keyfob Flow 2b — KLMS ↔ NetHSM AES Orchestration

# Keyfob Flow 2b — KLMS ↔ NetHSM AES Orchestration

> Detail of how the **KLMS orchestrates every SCP03 AES operation** using a
> NetHSM that exposes **only single-block AES (AES-ECB)** — it cannot compute
> CMAC/CBC itself. The KLMS assembles each CMAC (RFC 4493) and the A003 CBC from
> repeated `AES-ECB` calls, doing the XOR / shift / padding logic itself.
> Companion: **"Keyfob Flow 2 — Provisioning"** (§5 calls into this note).

> **Implementation already models this.** `kf-crypto` provides
> `CmacSubkeys::from_ecb`, `aes_cmac_with_subkeys`, and `aes_cmac_via_ecb`, and
> `kf-hsm`'s Nitrokey backend wires the `ecb` closure to `CKM_AES_ECB`
> (`crates/kf-hsm/src/nitrokey.rs`). The production KLMS path reuses exactly this
> pattern; the byte layouts are in `crates/kf-scp03/src/derive.rs`.

---

## 1. NetHSM primitive surface (what the KLMS can call)

The NetHSM exposes **only** these (SCP03-agnostic) operations:

| Primitive | Signature | Notes |
|---|---|---|
| **AES-ECB encrypt** | `ECB(handle, block[16]) → block[16]` | single 128-bit block; the *only* crypto op for SCP03 |
| **create key object** | `ImportKey(bytes[16]) → handle` | wraps a derived key as an ephemeral handle (for Phase-2/A003 ECB keyed by static/session keys) |
| **destroy key object** | `Destroy(handle)` | wipe ephemeral handles after the transaction |
| **bundle decrypt** | `Decrypt(handle_priv, ct) → pt` | Flow 1 only (the bundle keypair) |

The **master key is a permanent handle `M`**; its 16 bytes are never returned.

> Keyed AES-ECB needs a *handle*, so any key used to key an AES op must be a key
> object. The master key already is. Derived static/session keys that key further
> AES ops (S-MAC, S-DEK, SES-MAC) are **imported as ephemeral handles**.

---

## 2. Why CMAC can't be one call: RFC 4493 over ECB

AES-CMAC(key, message) is defined as (RFC 4493 / NIST SP 800-38B):

**Subkeys** (once per key):
1. `L = ECB(key, 0x00…00)`        ← 1 AES-ECB call
2. host: `K1 = dbl(L)`, `K2 = dbl(K1)`  (`dbl` = left-shift + conditional XOR `0x87`)

**Tag** (message = n blocks, last maybe partial):
3. host: last full block → XOR `K1`; last partial → pad `0x80 00…` then XOR `K2`
4. `C0 = 0`; for i=1..n: `Ci = ECB(key, C(i-1) XOR Mi)`  ← n AES-ECB calls
5. `tag = Cn`

So the host holds all intermediate `Ci` (needed for the XOR chain) and the final
tag. **This is why derived keys are visible to the KLMS** under an ECB-only HSM
(see §6).

`kf-crypto` implements steps 1–5 over an injectable `ecb` closure:
`CmacSubkeys::from_ecb`, `aes_cmac_with_subkeys` (`crates/kf-crypto/src/symmetric.rs`).

---

## 3. Per-transaction phase summary

Notation: `M` = master; `SE/SM/SD` = static S-ENC/MAC/DEK; `SESMAC` = session MAC.
Derivation-data layouts are fixed by `kf-scp03/src/derive.rs`:

- Phase 1 (static): 16-byte header + raw 10-byte UID = **26 bytes** → 2 blocks
  (purpose `0x40/0x60/0x70`). [`build_static_derivation_data`]
- Phase 2 (session): 16-byte header + `host‖card` (16) = **32 bytes** → 2 full
  blocks (constants `0x04/0x06/0x07`). [`build_derivation_data`]
- Cryptograms: same 32-byte shape, constants `0x00` (card) / `0x01` (host),
  output truncated to **8 bytes**, keyed by **SES-MAC**. [`card_cryptogram`/`host_cryptogram`]

| Phase | ECB calls | Result |
|---|---|---|
| A — KDF3 (key M) | 7 | S-ENC/MAC/DEK → import SE/SM/SD |
| B — session keys (SE/SM) | 8 | SES-ENC/MAC/RMAC; SES-MAC → import SESMAC |
| C — cryptograms (SESMAC) | 5 | card-cg verify (gate) + host-cg |
| D — A003 CBC (SD) | 3 | A003 ciphertext |
| **Total** | **≈23** | master key bytes returned: **0** |

### 3.1 Full message sequence — KLMS ↔ NetHSM (per transaction)

> Every message, in order. `M` = master (permanent); `SE/SM/SD`/`SESMAC` = imported
> ephemeral handles. `ECB` = `AES-ECB(handle, block16)→block16`. **host** = pure
> KLMS logic (no HSM call). Subkey rows (1, 11, 12, 20) yield RFC 4493 K1/K2;
> **row 1 may be cached across transactions.** "→station" marks values returned to
> the station in the container.

| # | Phase | KLMS → NetHSM | Handle | In → Out | host step / note |
|---|---|---|---|---|---|
| 1 | M subkeys | `ECB` | M | `0¹⁶ → L_M` | `K1_M=dbl(L_M)`, `K2_M=dbl(K1_M)` |
| 2 | KDF3 ENC (0x40) | `ECB` | M | `block1 → C1` | |
| 3 | | `ECB` | M | `(m2⊕K2_M) → C2` | pad 10-B block2 → 16; **S-ENC = C2** |
| 4 | | `ImportKey` | — | `S-ENC → handle SE` | |
| 5 | KDF3 MAC (0x60) | `ECB` | M | `block1 → C1` | |
| 6 | | `ECB` | M | `(m2⊕K2_M) → C2` | **S-MAC = C2** |
| 7 | | `ImportKey` | — | `S-MAC → handle SM` | |
| 8 | KDF3 DEK (0x70) | `ECB` | M | `block1 → C1` | |
| 9 | | `ECB` | M | `(m2⊕K2_M) → C2` | **S-DEK = C2** |
| 10 | | `ImportKey` | — | `S-DEK → handle SD` | |
| 11 | ses subkeys | `ECB` | SE | `0¹⁶ → L_SE` | `K1_SE`, `K2_SE` |
| 12 | | `ECB` | SM | `0¹⁶ → L_SM` | `K1_SM`, `K2_SM` |
| 13 | SES-ENC (0x04) | `ECB` | SE | `block1 → C1` | |
| 14 | | `ECB` | SE | `(block2⊕K1_SE) → C2` | **SES-ENC = C2** (→station) |
| 15 | SES-MAC (0x06) | `ECB` | SM | `block1 → C1` | |
| 16 | | `ECB` | SM | `(block2⊕K1_SM) → C2` | **SES-MAC = C2** (→station) |
| 17 | | `ImportKey` | — | `SES-MAC → handle SESMAC` | |
| 18 | SES-RMAC (0x07) | `ECB` | SM | `block1 → C1` | reuse `K1_SM` |
| 19 | | `ECB` | SM | `(block2⊕K1_SM) → C2` | **SES-RMAC = C2** (→station) |
| 20 | cg subkeys | `ECB` | SESMAC | `0¹⁶ → L` | `K1`, `K2` |
| 21 | card-cg (0x00) | `ECB` | SESMAC | `block1 → C1` | |
| 22 | | `ECB` | SESMAC | `(block2⊕K1) → C2` | truncate 8 B; **compare fob card_cryptogram → GATE** |
| 23 | host-cg (0x01) | `ECB` | SESMAC | `block1 → C1` | reuse cg `K1/K2` |
| 24 | | `ECB` | SESMAC | `(block2⊕K1) → C2` | truncate 8 B; **host_cryptogram** (→station) |
| 25 | A003 CBC (key SD) | `ECB` | SD | `P1⊕IV(0) → C1` | pad 32→48 B (M2) |
| 26 | | `ECB` | SD | `P2⊕C1 → C2` | |
| 27 | | `ECB` | SD | `P3⊕C2 → C3` | `C1‖C2‖C3` (→station) |
| 28 | cleanup | `Destroy` | — | `SE` | |
| 29 | | `Destroy` | — | `SM` | |
| 30 | | `Destroy` | — | `SD` | |
| 31 | | `Destroy` | — | `SESMAC` | host zeroizes all derived keys |

**Message totals:** 23 `ECB` + 4 `ImportKey` + 4 `Destroy` = **31 NetHSM messages**
per transaction (row 1 cacheable). **Master-key bytes returned: 0.**

> **Batching:** rows 25–27 (and any same-handle CMAC pair) can be issued as a
> single multi-block request if the NetHSM supports batched ECB, cutting latency at
> line rate (see §7).

---

## 4. Worked example — S-ENC derivation (rows 2–3)

Master `M` (handle). Derivation data (S-ENC, raw 10-byte UID, `derive.rs` test
`jcshell_reference_vector`):
```
block1 = 00000000 00000000 00000040 00008001   (16 B)
block2 = deadbeef cafebabe 1234 (10 B)  → host pad → deadbeefcafebabe1234800000000000
```
Steps (host ops in plain text, ECB = NetHSM call):
```
L  = ECB_M(00000000000000000000000000000000)          # row 1, subkey seed
K1 = dbl(L); K2 = dbl(K1)                              # host bit math
C1 = ECB_M(block1 XOR 0000…0)                          # row 2  = ECB_M(block1)
m2 = (deadbeefcafebabe1234 ‖ 80000000 00000000)        # host pads partial block
C2 = ECB_M(m2 XOR K2)                                   # row 3, last-block rule
S-ENC = C2                                              # derived key → import as SE (row 4)
```
(With MK `00112233…eeff`, UID `deadbeef…1234` this yields `29b9a2c3fe159ed9…` — the `jcshell_reference_vector` value.)

---

## 5. Key-handle lifecycle (per transaction)

```
permanent:  M (master key) ─────────────────────────────────────────►  (never leaves)

per fob:    [KDF3]  SE, SM, SD  ──import──►  used in B  ─┐
            [B]     SESMAC     ──import──►  used in C  ─┤
            [D]     SD (reused)              used in A003│
                                                         └──► Destroy(SE,SM,SD,SESMAC); zeroize KLMS RAM
```
Static keys and SES-MAC are **ephemeral**: imported, used, destroyed, and zeroized
in KLMS RAM after the transaction.

---

## 6. Security trade-off (inherent to ECB-only)

Because the host assembles CMAC, **derived static keys (S-ENC/MAC/DEK) and SES-MAC
are visible in KLMS RAM** as CMAC tags / during handle import. This is unavoidable
when the HSM exposes only AES-ECB. Accepted because:

- The **master key (the crown jewel) never leaves the NetHSM.**
- Static keys are **per-fob** and **short-lived** (zeroized after one transaction).
- The KLMS is a **hardened, trusted service** (not the untrusted factory station).
- Compromising the KLMS leaks only the fobs currently in flight, not the fleet
  (that needs the master key, which is sealed).

**Stronger alternative (if an HSM with native CMAC is chosen — e.g. Utimaco / some
YubiHSM2 modes):** `ECB` is replaced by `AES-CMAC(handle, data) → tag`; static keys
can be derived and kept as **internal HSM key objects without ever transiting the
KLMS**, cutting the per-transaction primitive calls and removing static-key RAM
exposure. Same SCP03 logic; only the primitive surface changes (see §7).

---

## 7. Open decisions

- [ ] **Keyed-ECB by handle vs by value:** does the chosen NetHSM accept a raw key
      for `ECB`, or require `ImportKey` handles? (affects the import/destroy churn.)
- [ ] **Native CMAC option:** pick an HSM (Utimaco / YubiHSM2 w/ `CKM_AES_CMAC`)
      that can do CMAC internally → static keys stay HSM-resident (§6 alt).
- [ ] **Subkey caching:** cache `M` subkeys (row 1) across transactions; within a
      transaction reuse `SM`/`SESMAC` subkeys (rows 11/12, 20).
- [ ] **Batching / multi-block:** can the NetHSM take a vector of blocks per call
      (e.g. rows 25–27, or a whole CMAC) to cut latency at line rate?
- [ ] **Ephemeral-handle quotas:** per-transaction handle churn (4 create/destroy)
      — confirm the HSM's object-create throughput is not a bottleneck at line rate.

id: 8368ec0f733b4a15b695947f0f722147
parent_id: 61ba8f290b7b4faf9199916387419665
created_time: 2026-06-30T09:38:00.610Z
updated_time: 2026-07-01T11:06:11.724Z
is_conflict: 0
latitude: 0.00000000
longitude: 0.00000000
altitude: 0.0000
author: 
source_url: 
is_todo: 0
todo_due: 0
todo_completed: 0
source: joplin-desktop
source_application: net.cozic.joplin-desktop
application_data: 
order: 1782812280610
user_created_time: 2026-06-30T09:38:00.610Z
user_updated_time: 2026-07-01T11:06:11.724Z
encryption_cipher_text: 
encryption_applied: 0
markup_language: 1
is_shared: 0
share_id: 
conflict_original_id: 
master_key_id: 
user_data: 
deleted_time: 0
type_: 1