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

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

> The **real-time provisioning plane**: the station forwards a fob handshake to
> Clypeum via a **DLL over mTLS**; Clypeum derives the static + session keys
> using the **NetHSM** (which holds the **NXP master key**) and returns a
> **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 cached content comes from).

> **Note on identity:** the station knows **only the fob UID** (read from the
> card). **FESN and SPID are OEM-supplied content** — they live in the key
> package (Flow 1) and come **back in the response**, not in the request.

---

## 1. Actors

| Actor | Role |
|---|---|
| **Station** (`kf-prod-station`) | PC/SC to fob; **DLL → 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; looks up cached content **by UID**; 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"** (RFC 4493-over-ECB, exact
> call counts, handle lifecycle).

---

## 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, TTL ~5 s)
  + the **pre-encrypted** A003 ciphertext. Never the master key, static keys, or S-DEK.
- The **station never sees FESN/SPID** until the response — they are OEM content.
- The **card is authenticated by Clypeum** (`card_cryptogram` check) before any
  session keys are issued — a rogue/clone fob is rejected.
- Link security: **Station ↔ Clypeum = mTLS** (via DLL); **Clypeum ↔ NetHSM** local/TLS.

---

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

The station forwards **only the UID + the fob handshake**. No FESN/SPID — Clypeum
resolves the content from the UID.

```json
{
  "transaction_id": "550e8400-e29b-…",
  "station_id":     "EOL-LINE-03",
  "fob": {
    "uid":         "00002317229409204524",   // 10-byte JCOP UID — the only fob identity the station knows
    "key_version": 1
  },
  "scp03": {
    "host_challenge":   "CBFE5C3E9E6C15DE",
    "card_challenge":   "4D3D5E7A2F1B0C88",
    "sequence_counter": "000001",
    "card_cryptogram":  "A1B2C3D4E5F60718"
  }
}
```

> **Error path — no package for this UID:** Clypeum returns `NoPackage` (the
> watchdog hasn't seen this fob's content yet). The station halts that fob and
> surfaces a setup error; the watchdog prioritizes the missing UID.

---

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

Clypeum resolves the package by UID and returns the content (incl. **FESN** and
**SPID**) + session keys. SPID is delivered as the `A001` personalization item;
FESN is included as container metadata (for audit/labeling).

```json
{
  "transaction_id": "550e8400-…",
  "authenticated":  true,
  "expires_at":     "2026-06-30T08:57:18Z",
  "fob": {
    "fesn": "1KM0001E",                  // from the OEM key package (station learns it here)
    "spid": "59918C00…4BD1"              // also present as A001 below
  },
  "scp03": {
    "ses_enc": "9F2A…", "ses_mac": "17B0…", "ses_rmac": "C4D1…",
    "host_cryptogram": "7E51…", "security_level": "C_MAC"
  },
  "content": {
    "transaction_id": "klms-trx-987",
    "personalization_items": [
      { "dgi": "A001", "data": "<ASCII hex SPID>",   "encrypted": false },
      { "dgi": "A002", "data": "<DER hex>",           "encrypted": false },
      { "dgi": "A003", "data": "<48-byte hex>",       "encrypted": true  },
      { "dgi": "A004", "data": "<DER hex>",           "encrypted": false },
      { "dgi": "A005", "data": "<DER hex>",           "encrypted": false },
      { "dgi": "A006", "data": "<16-byte hex>",       "encrypted": false }
    ],
    "post_perso_commands": [ "00DB0000030A0101", "00DB0000030B0101" ]
  }
}
```

---

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

The KLMS looks up the fob's content in the cache **by UID** (populated by Flow 1),
then 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 |
| 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 (EA sequence)

| # | From → To | Message | Payload |
|---|---|---|---|
| 1 | Station → Fob | INITIALIZE UPDATE | `80 50 <KVN> 00 08 ‖ host_challenge[8]` |
| 2 | Fob → Station | INIT UPDATE resp | `UID[10] ‖ key_info ‖ seq_counter[3] ‖ card_challenge[8] ‖ card_cryptogram[8]` |
| 3 | Station → Clypeum | establish session (DLL/mTLS) | `transaction_id, station_id, fob{uid, key_version}, scp03{host_challenge,card_challenge,sequence_counter,card_cryptogram}` |
| 4 | Clypeum (self) | cache lookup **by UID** | → content + FESN + SPID (from Flow 1) |
| 5 | Clypeum → NetHSM | AES-ECB ×7 (KDF3, see Flow 2b §A) | `master handle, derivation blocks` → S-ENC/MAC/DEK |
| 6 | Clypeum → NetHSM | AES-ECB-assembled CMAC (card-cg verify) | `SES-MAC handle, derivation data` |
| 7 | Clypeum (self) | authenticate | compare (#2 vs #6); reject rogue fob |
| 8 | Clypeum → NetHSM | AES-ECB-assembled CMACs (session KDF) | `S-ENC/S-MAC handles, derivation data` → SES-* |
| 9 | Clypeum → NetHSM | AES-ECB-assembled CMAC (host cryptogram) | `SES-MAC handle, derivation data` |
| 10 | Clypeum → NetHSM | AES-ECB-assembled CBC (A003 encrypt) | `S-DEK handle, private_key` |
| 11 | NetHSM → Clypeum | results | `ses*, host_cryptogram, A003_ciphertext` |
| 12 | Clypeum → Station | container (response) | `fob{fesn,spid}, scp03{ses*,host_cryptogram,security_level}, content{items incl. encrypted A003, post_perso}, expires_at` |
| 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]` |
| 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 → Clypeum | provision result | `transaction_id, fesn, result, stage_reached, apdu_count, timestamp` |

### Alt / error paths
- **No package for UID / low stock** (#4): `NoPackage` returned; watchdog priority-fetch for that UID; station retries after brief wait.
- **Card not authenticated** (#7): no container/session keys issued (#12 = error); station halts.
- **TTL expiry** (`expires_at` before EXTERNAL AUTH): fob rejects #13 (`69 82`); restart from #1.
- **NetHSM unreachable** (#5–#11): `KlmsError::Client`; station surfaces a setup error.

---

## 7. ASCII overview

```
Fob         Station          Clypeum KLMS (orch+cache)        NetHSM (master key, AES-ECB only)
 │ INIT UPD   │                  │                               │
 │◄───────────┤ 80 50 … hc       │                               │
 │ UID‖cc‖cg  │                  │                               │
 ├───────────►│ DLL/mTLS(uid)    │                               │
 │            ├─────────────────►│ cache lookup(uid) → FESN/SPID │
 │            │                  │   +content (from Flow 1)      │
 │            │                  │ 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 ───────│
 │            │ container        │                               │
 │            │  (FESN/SPID+ses*)│                               │
 │            │◄─────────────────┤                               │
 │ EXT AUTH   │                  │                               │
 │◄───────────┤ 84 82 … cg+MAC   │                               │
 │ 9000       │ STORE DATA×N …   │                               │
 ├───────────►│ lifecycle+perso  │                               │
 │ 9000       │ result(fesn)     │                               │
 ├───────────►├─────────────────►│ (audit + retire cached pkg)   │
```

---

## 8. Code alignment (our boundary)

Our boundary is the **Station ↔ Clypeum contract only** (this note's §3/§4).
Clypeum internals (cache, NetHSM AES orchestration, master-key ECB) are Clypeum's.

| Type | Status |
|---|---|
| `SessionRequest` | add `transaction_id`, `station_id`; `fob` = `{uid, key_version}` only (**no fesn/spid**) |
| `SessionResponse` | add `fob{fesn, spid}` + `authenticated`, `expires_at`; content carries A001 (SPID) |
| `KlmsContent` | sufficient |
| `KlmsProvisioner` | already enforces `encrypt_with_dek=false` (app holds no S-DEK) |
| DLL-backed `KlmsClient` | maps 1:1 onto the request/response; link = mTLS |
| `Scp03Channel::establish_from_session_keys` | injects the container's session keys |
| `kf-crypto` (`aes_cmac_with_subkeys`/`aes_cmac_via_ecb`/`CmacSubkeys`) | the exact RFC-4493-over-ECB assembly the KLMS↔NetHSM path uses (Flow 2b §2) |
| `kf-hsm` Nitrokey `CKM_AES_ECB` backend | dev model of the same AES-ECB primitive (Flow 2b §1) |

---

## 9. Open decisions

- [ ] **DLL interface** (the real `KlmsClient` binding) + mTLS details.
- [ ] Production **security level**: C-MAC only, or +R-MAC/+C-DEC.
- [ ] Session-key **TTL** (~5 s) + single-use enforcement.
- [ ] **Key-version strategy** (how the station knows which KVN the fob expects).
- [ ] Whether the KLMS **verifies card_cryptogram** (recommended — the mock does).
- [ ] NetHSM AES-primitive surface: **AES-ECB-only** (Flow 2b) vs **native CMAC**
      (Flow 2b §6 alt — keeps static keys HSM-resident).
- [ ] Cache index key = UID only (confirm unique per fob) + `NoPackage` retry policy.

id: 3806c04b10e24dad817835acb66e1480
parent_id: 61ba8f290b7b4faf9199916387419665
created_time: 2026-06-30T08:04:42.947Z
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: 1782806682947
user_created_time: 2026-06-30T08:04:42.947Z
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