Keyfob Flow 2 — Provisioning: Station ↔ Key Fob ↔ Clypeum

# Keyfob Flow 2 — Provisioning: Station ↔ Key Fob ↔ Clypeum

> The **real-time provisioning plane** (updated 2026-08-17 to reflect the implemented
> REST transport): the station identifies the fob, runs INITIALIZE UPDATE, and
> requests a container from Clypeum via **REST over mTLS**; Clypeum derives the
> static + session keys using the **NetHSM** (which holds the **NXP master key**)
> and returns a **binary Clypeum container** with all content + session keys. The
> station never holds the master key, static keys, or S-DEK. Companion: **"Keyfob
> Flow 1 — Content Supply"** (where the content originates) and **"Keyfob Flow 2b —
> KLMS ↔ NetHSM AES Orchestration"** (the per-message HSM table).
>
> **Note on identity:** the station knows **only the fob UID** (read via GET DATA
> after SELECT ISD). **FESN and SPID are OEM-supplied content** — they come **back
> in the container**, not in the request.

---

## 1. Actors

| Actor | Role |
|---|---|
| **Station** (`kf-dev-station` KLMS panel; prod-station pending) | PC/SC to fob; **REST/mTLS** to Clypeum; consumes session keys; SCP03 state machine |
| **Key Fob** | NXP NCJ37x being provisioned; holds its factory-loaded static keys + UID |
| **Clypeum KLMS** | **Orchestrates** SCP03; generates containers on demand (seed-based); assembles the container |
| **NetHSM** (Clypeum-side) | Holds the **NXP master key**; performs **AES primitives only** (single-block AES-ECB) |

> The **same NetHSM** used in Flow 1 (bundle decrypt) also holds the **NXP master
> key** (imported from NXP, never leaves). It does **not** understand SCP03 and
> cannot compute CMAC/CBC — the **KLMS orchestrates** the entire KDF by calling the
> HSM for single-block AES-ECB, assembling each CMAC/CBC itself. Full detail in
> **"Keyfob Flow 2b — KLMS ↔ NetHSM AES Orchestration"**.

---

## 2. Trust & invariants

- The **NXP master key lives in the NetHSM** (Clypeum-side); imported from NXP,
  never exported. Only **single-block AES** crosses the HSM boundary.
- The **station receives only ephemeral session keys** (SES-ENC/MAC/RMAC) + the
  **pre-encrypted** A003 ciphertext (AES-128-CBC, IV=0, M2 — card-verified format).
  Never the master key, static keys, or S-DEK.
- The **station never sees FESN/SPID** until the container arrives — they are OEM content.
- The **card is authenticated twice**: by the KLMS (`card_cryptogram` check in
  Phase C, gate before keys issue) **and locally by the station** after container
  decrypt, before EXTERNAL AUTHENTICATE.
- Link security: **Station ↔ Clypeum = mTLS + bearer token** (IAM/Keycloak, client
  cert); **Clypeum ↔ NetHSM** local/TLS.

---

## 3. Request — Station → Clypeum (REST over mTLS)

`POST {ccs}/cryptoContainers`, `application/x-www-form-urlencoded`:

| Form field | Value |
|---|---|
| `typeID` | `0x0900` (config: `container_type_id`) |
| `seedUUID` | from `POST {ccs}/seeds` at connect (one per session, reused per tap) |
| `productSerial` | `KF-<chip UID hex>` (7-byte PC/SC chip UID) |
| `extra` | JSON below |

```json
{
  "transaction_id": "TPS-01-<uid8>-<unix-sec>",
  "station_id":     "TPS-01",
  "fob": {
    "oef":         "B252",
    "uid":         "00002317229409204524",
    "key_version": "255"
  },
  "scp03": {
    "host_challenge":   "CBFE5C3E9E6C15DE",
    "card_challenge":   "4D3D5E7A2F1B0C88",
    "sequence_counter": "000001",
    "card_cryptogram":  "A1B2C3D4E5F60718"
  },
  "keyBundleType": "dev"
}
```

> **Error path:** any HTTP error (e.g. the 2026-08-14 HTTP 500 during their
> card-ready rollout) — the station classifies it as a **server-stage failure**,
> retries the generation once without touching the card, then surfaces the error.

---

## 4. Response — Clypeum → Station (the **container**)

**201 Created** with `application/octet-stream`: the **binary Clypeum container**
(wrapper-key encrypted, see **"Clypeum Crypto Container Binary Format"**), 13 TLV
fields after decrypt:

| # | Field | Notes |
|---|---|---|
| 0 | UID | echo |
| 1 | FESN | OEM content — station learns it here |
| 2 | SPID | also delivered as A001 |
| 3 | Authenticated | status text |
| 4–6 | S-ENC / S-MAC / S-RMAC | **session keys** |
| 7 | Host Cryptogram | for EXTERNAL AUTHENTICATE |
| 8 | Fob Private Key | **card-ready**: 48 B, AES-128-CBC(IV=0)+M2 under S-DEK — forwarded untouched |
| 9 | Fob Public Cert | → A002 |
| 10 | IRK | → A006 |
| 11 | ICA Cert | → A004 |
| 12 | CMS Root Cert | → A005 |

Response headers carry `containeruuid` (and `cryptodatacontainerid` — currently
empty, open item). The wrapper key comes from the seed.

> The earlier design sketch (JSON container with `expires_at` TTL) was superseded
> by the binary format; no TTL is currently enforced by the API.

---

## 5. Clypeum internal orchestration (via NetHSM AES primitives)

The KLMS drives the SCP03 derivation. The NetHSM exposes **only single-block
AES-ECB**; the KLMS assembles each CMAC/CBC from it (see **Flow 2b** for the full
RFC-4493-over-ECB sequence, byte layouts, and ~23 ECB calls per transaction):

| step | NetHSM primitive | purpose |
|---|---|---|
| Phase 1 (KDF3) | `AES-ECB(master)` ×7 (CMAC assembled on host) | S-ENC / S-MAC / S-DEK (purpose 0x40/0x60/0x70, raw 10-byte UID) |
| verify card | `AES-ECB(SES-MAC)`-assembled CMAC → compare to `card_cryptogram` | authenticate fob (gate) |
| Phase 2 (session) | `AES-ECB(S-ENC/S-MAC)`-assembled CMAC | SES-ENC / SES-MAC / SES-RMAC (const 0x04/0x06/0x07) |
| host cryptogram | `AES-ECB(SES-MAC)`-assembled CMAC | for EXTERNAL AUTHENTICATE |
| A003 encrypt | `AES-ECB(S-DEK)`-assembled CBC (IV=0, pad=M2) | pre-encrypt device private key |

> Static keys (S-ENC/MAC/DEK) and SES-MAC are held transiently to key later
> AES-ECB calls; they are imported as ephemeral HSM handles and destroyed + zeroized
> after the transaction. The master key is never returned.

---

## 6. Message table (station-side sequence)

| # | From → To | Message | Payload |
|---|---|---|---|
| 0 | Station → Fob | SELECT ISD + GET DATA | OEF ID + UID (UID **not** in INIT UPDATE response) |
| 1 | Station → Fob | INITIALIZE UPDATE | `80 50 <KVN> 00 08 ‖ host_challenge[8]` |
| 2 | Fob → Station | INIT UPDATE resp | `div(10) ‖ key_version ‖ scp_id ‖ i ‖ card_challenge(8) ‖ card_cryptogram(8) ‖ seq_counter(3)` |
| 3 | Station → Clypeum | `POST /cryptoContainers` (mTLS+token) | form: typeID, seedUUID, productSerial, extra{...} |
| 4–10 | Clypeum ↔ NetHSM | KDF3 → card-cg gate → session keys → host-cg → A003 CBC | see Flow 2b (23 ECB + 4 Import + 4 Destroy) |
| 11 | Clypeum → Station | 201 + binary container | 13 TLV fields (§4); `containeruuid` header |
| 12 | Station (self) | decrypt + verify | wrapper-key decrypt; session-key extraction (tag-keyed); **local card-cryptogram check** |
| 13 | Station → Fob | EXTERNAL AUTHENTICATE | `84 82 <level> 00 ‖ host_cryptogram ‖ MAC[8]` |
| 14 | Fob → Station | success | `90 00` |
| 15 | Station → Fob | STORE DATA blocks (loop) | `84 E2 <P1> <P2> ‖ DGI payload ‖ MAC[8]` (9 blocks typical) |
| 16 | Station → Fob | lifecycle transition | `84 E2 80 <P2> 00 ‖ MAC[8]` → FACTORY |
| 17 | Station → Fob | post-perso (raw) | `00 DB 00 00 03 0A 01 01`, `…0B 01 01` |
| 18 | Station → Fob | validation | certs/FESN/SPID read-back + **GA ECDSA verify** |
| 19 | Station → Clypeum | provision result (`report_usage`) | **PENDING** — awaiting KLMS status semantics |

### Alt / error paths
- **HTTP error (#3):** server-stage → one plain retry (no card re-install), then fail.
- **Card-stage rejection (#13–#16, e.g. 69 82 / 6A 80):** re-install applet + retry once.
- **Session-key / card-cryptogram mismatch (#12):** server-stage (bad container) → plain retry.
- **Card removed mid-flow:** reconnect fails → run fails; operator re-taps.

---

## 7. ASCII overview

```
Fob         Station          Clypeum KLMS (orch)             NetHSM (master key, AES-ECB only)
 │ SELECT ISD │                  │                               │
 │ GET DATA   │                  │                               │
 │◄───────────┤ OEF+UID          │                               │
 │ INIT UPD   │                  │                               │
 │◄───────────┤ 80 50 … hc       │                               │
 │ div‖cc‖cg  │                  │                               │
 ├───────────►│ POST /cryptoContainers (mTLS+token)              │
 │            ├─────────────────►│ generate (seedUUID)           │
 │            │                  │ AES-ECB×7 (KDF3 master+UID) ──►│  (CMAC assembled on host)
 │            │                  │◄── S-ENC/MAC/DEK ──────────────│
 │            │                  │ AES-ECB CMAC(verify)+session ─►│
 │            │                  │   +host_cg +CBC(A003)         │
 │            │                  │◄── ses*+host_cg+A003_ct ───────│
 │            │ 201 + container  │                               │
 │            │  (13 TLV fields) │                               │
 │            │◄─────────────────┤                               │
 │ EXT AUTH   │  (local card-cg verify first)                    │
 │◄───────────┤ 84 82 … cg+MAC   │                               │
 │ 9000       │ STORE DATA×9 …   │                               │
 ├───────────►│ lifecycle+perso  │                               │
 │ 9000       │ validate (GA)    │                               │
 ├───────────►│ result           │                               │
 │            ├─────────────────►│ report_usage — PENDING        │
```

---

## 8. Code alignment (our boundary, as implemented)

| Piece | Status |
|---|---|
| `klms_client.rs` (kf-dev-station) | **REST client implemented**: token auth, seed, generate (form), report_usage (defined, unwired) |
| `container_parser.rs` | binary Clypeum container parse + wrapper-key decrypt, tag-keyed fields |
| `ContainerTlvSource` | fields → DGI items; card-ready A003 forwarded untouched (CBC+IV0+M2) |
| `establish_klms_channel` (nfc_reader) | session-key extraction + **local card-cryptogram gate** + EXTERNAL AUTHENTICATE |
| `Scp03Channel::establish_from_session_keys` | injects the container's session keys |
| `FlowError` classification | server-stage → plain retry; card-stage → re-install + retry |
| Config (`klms-config.toml`) | typeID, keyBundleType, accept_invalid_certs, station_id |
| HW verification | **2026-08-17**: two fobs, GA ECDSA verify true, 24 APDUs each |

---

## 9. Open decisions

- [ ] **`report_usage` semantics** — status values/payload; also populates productSerial
      in the KLMS admin (currently blank).
- [ ] **`cryptoDataContainerId` header** — currently empty; `validate_cid` ready.
- [ ] Production **security level**: C-MAC only, or +R-MAC/+C-DEC.
- [ ] **Key-version strategy** (how the station knows which KVN the fob expects).
- [ ] NetHSM AES-primitive surface: **AES-ECB-only** (Flow 2b) vs **native CMAC**
      (Flow 2b §6 alt — keeps static keys HSM-resident).
- [ ] Seed lifecycle policy (single-use? TTL?) — currently one seed per connect,
      reused across taps; accepted by dev KLMS.

id: 3806c04b10e24dad817835acb66e1480
parent_id: 61ba8f290b7b4faf9199916387419665
created_time: 2026-06-30T08:04:42.947Z
updated_time: 2026-08-17T10:37:23.574Z
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: 1782806682947
user_created_time: 2026-06-30T08:04:42.947Z
user_updated_time: 2026-08-17T10:37:23.574Z
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
is_locked: 0
extracted_resource_ids: 
type_: 1