Keyfob Station — KLMS Data Contract & E2E Sequence

# Keyfob Station — KLMS Data Contract & E2E Sequence

> Spec for the data flowing across **Key Fob · Station · KLMS · Ford/Clypeum
> backend · Station-side HSM**, structured for a sequence diagram in Enterprise
> Architect. Companions: *"Ford EoL Key Fob Provisioning System (SCP03)"* and
> *"Keyfob Station — SCP03 / KLMS Flow Reference"*.

---

## 1. Actors & trust boundaries (revised)

| Actor | Role | Trust | Holds (long-term) |
|---|---|---|---|
| **Key Fob** | NXP NCJ37x / JCOP 4.5 secure element being provisioned | device | per-fob static keys (factory-loaded), UID |
| **Station** (`kf-prod-station`) | Factory PC; PC/SC to fob, REST/DLL to KLMS | **untrusted** | nothing long-term |
| **KLMS** (Clypeum) | Orchestrates SCP03 Phase-2; bundle cache + watchdog | trusted service | decrypted-package cache (2-wk stock); no master key |
| **Ford / Clypeum backend** | Generates per-fob key packages (KDF3) + personalization data; source of encrypted bundles | trusted (OEM/gen) | **master key** (in gen-HSM), FESN/SPID/IRK/certs/privkeys, audit DB |
| **Station-side HSM** (YubiHSM 2 / Nitrokey / Utimaco) | AES primitives + bundle decryption | root of trust (factory side) | **bundle-decryption private key (generated inside it)**; transient per-fob static-key objects |

**Core invariants (revised):**
- The **master key never leaves the generation side** (Ford/Clypeum gen-HSM). It is
  NOT in the station-side HSM and NOT in the KLMS.
- The **station-side HSM holds only the bundle-decryption private key** (generated
  inside it) + performs AES-CMAC/AES-CBC primitives. It does **not** understand
  SCP03/KDF3 — the **KLMS orchestrates** the derivation, calling the HSM per AES op.
- The **station receives only ephemeral session keys** (SES-ENC/MAC/RMAC, TTL ~5 s,
  single transaction) + pre-encrypted A003. No static/master keys ever reach it.

> Two operational **planes**: (A) **async** bundle/cache provisioning (keeps stock
> warm), (B) **real-time** per-fob provisioning. Our code boundary is the
> Station↔KLMS contract in plane B; planes A and the KLMS internals are Clypeum's.

---

## 2. Plane A — async key-package provisioning (continuous)

The KLMS keeps a warm cache of **decrypted per-fob packages** so real-time
provisioning never blocks on the network or the generation side.

### Bundle / key-package format
Each **bundle** = N per-fob **key packages**, encrypted to the **station-side
HSM's public key** (the keypair is generated *inside* the HSM; the private key
never leaves). A single package per fob:

```jsonc
// one key package (plaintext, before bundling/encryption)
{
  "uid":  "00002317229409204524",   // 10-byte JCOP UID
  "fesn": "1KM0001E",
  "static_keys": {                   // diversified at the gen side via KDF3(master, uid)
    "s_enc": "<16-byte hex>",
    "s_mac": "<16-byte hex>",
    "s_dek": "<16-byte hex>"
  },
  "content": {                       // personalization data
    "spid":        "59918C00…4BD1",  // → A001
    "device_cert": "<DER hex>",      // → A002
    "private_key": "<32-byte scalar>", // → A003 (encrypted later with s_dek)
    "irk":         "<16-byte hex>"     // → A006
  }
}
```
> Factory-wide A004/A005 certs are shared (stored once at the KLMS), not per-package.

### One-time setup
| # | From → To | Message | Data |
|---|---|---|---|
| A0 | Station-side HSM → gen side | export bundle-encryption public key | HSM-generated keypair; **private key never leaves** the HSM |

### Bundle flow (recurring)
| # | From → To | Message | Data |
|---|---|---|---|
| A1 | gen side → KLMS | deliver encrypted bundle | `Enc(bundles, pub_hsm)` (+ sig); contains N key packages |
| A2 | KLMS → HSM | decrypt bundle | ciphertext; uses HSM-held private key |
| A3 | HSM → KLMS | decrypted packages | static keys materialize as **HSM key objects** (handles); content → KLMS |
| A4 | KLMS (self) | cache + index | index by `uid`/`fesn`; stock counter++ |
| A5 | KLMS watchdog → gen side | request more bundles | when `stock < 2-week threshold` |

**Watchdog:** a KLMS process continuously monitors the decrypted-package stock and
triggers A1–A4 to keep ≥ **2 weeks of production** buffered (sized from line rate).
This decouples the factory floor from generation-side latency/availability.

> Open: push (gen side pushes bundles) vs pull (KLMS fetches); bundle crypto
> (RSA-OAEP / ECIES / AES-KW); whether `private_key`/`s_dek` stay HSM-resident
> (preferred) vs transit the KLMS.

---

## 3. Plane B — real-time provisioning (per fob)

### Station → KLMS request  (`POST /provision/session`, mTLS)
Unchanged from our `SessionRequest` (+ correlation deltas). The station forwards
the fob handshake; it knows nothing about bundles/cache.

```json
{
  "transaction_id": "550e8400-…",
  "station_id":     "EOL-LINE-03",
  "fob": { "uid": "00002317229409204524", "fesn": "1KM0001E",
           "spid": "59918C00…4BD1", "key_version": 1 },
  "scp03": {
    "host_challenge":   "CBFE5C3E9E6C15DE",
    "card_challenge":   "4D3D5E7A2F1B0C88",
    "sequence_counter": "000001",
    "card_cryptogram":  "A1B2C3D4E5F60718"
  }
}
```

### KLMS → Station response
Unchanged from `SessionResponse`/`KlmsContent` (+ `authenticated`/`expires_at`).

```json
{
  "transaction_id": "550e8400-…",
  "authenticated":  true,
  "scp03": {
    "ses_enc": "…", "ses_mac": "…", "ses_rmac": "…",
    "host_cryptogram": "…", "security_level": "C_MAC"
  },
  "content": {
    "transaction_id": "klms-trx-987",
    "personalization_items": [
      { "dgi": "A003", "data": "<48-byte hex>", "encrypted": true },
      { "dgi": "A001", "data": "<ASCII hex>",  "encrypted": false },
      { "dgi": "A002", "data": "<DER hex>",    "encrypted": false },
      { "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" ]
  },
  "expires_at": "2026-06-30T08:57:18Z"
}
```

### KLMS internals (orchestration via HSM primitives)
The KLMS looks up the cached package for `uid`/`fesn`, then drives the SCP03
Phase-2 by calling the **HSM for every AES op** (the HSM holds the static keys as
key objects; the KLMS never does AES itself):

| step | KLMS → HSM primitive | purpose |
|---|---|---|
| verify card | `AES-CMAC(s_mac_handle, card-cg-deriv-data)` → compare to `card_cryptogram` | authenticate fob |
| session KDF | `AES-CMAC(s_enc_handle, ses-enc-data)`; `AES-CMAC(s_mac_handle, ses-mac & ses-rmac data)` | SES-ENC/MAC/RMAC |
| host cryptogram | `AES-CMAC(s_mac_handle, host-cg-data)` | for EXTERNAL AUTHENTICATE |
| A003 encrypt | `AES-CBC(s_dek_handle, A003_scalar, IV=0, pad=M2)` | pre-encrypt private key |

The HSM returns only the results (session keys, cryptograms, A003 ciphertext).
Master key is absent from this entire plane.

---

## 4. Real-time 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 → KLMS | establish session (mTLS) | `transaction_id, station_id, fob{uid,fesn,spid,key_version}, scp03{host_challenge,card_challenge,sequence_counter,card_cryptogram}` |
| 4 | KLMS (self) | cache lookup | by `uid`/`fesn` → static-key handles + content |
| 5 | KLMS → HSM | AES-CMAC (card-cg verify) | `s_mac_handle, deriv-data` |
| 6 | HSM → KLMS | expected card cryptogram | 8 bytes |
| 7 | KLMS (self) | authenticate | compare (#2 vs #6); reject rogue fob |
| 8 | KLMS → HSM | AES-CMAC ×3 (session KDF) | `s_enc/s_mac handles, deriv-data` |
| 9 | HSM → KLMS | session keys | `ses_enc, ses_mac, ses_rmac` |
| 10 | KLMS → HSM | AES-CMAC (host cryptogram) | `s_mac_handle, deriv-data` |
| 11 | KLMS → HSM | AES-CBC (A003 encrypt) | `s_dek_handle, A003_scalar` |
| 12 | HSM → KLMS | host_cryptogram + A003 ciphertext | 8 B + 48 B |
| 13 | KLMS → Station | session response | `scp03{ses*,host_cryptogram,security_level}, content{items incl. encrypted A003, post_perso}, expires_at` |
| 14 | Station → Fob | EXTERNAL AUTHENTICATE | `84 82 <level> 00 ‖ host_cryptogram ‖ MAC[8]` |
| 15 | Fob → Station | success | `90 00` |
| 16 | Station → Fob | STORE DATA blocks (loop) | `84 E2 <P1> <P2> ‖ DGI payload ‖ MAC[8]` |
| 17 | Station → Fob | lifecycle transition | `84 E2 80 <P2> 00 ‖ MAC[8]` → FACTORY |
| 18 | Station → Fob | post-perso (raw) | `00 DB 00 00 03 0A 01 01`, `…0B 01 01` |
| 19 | Station → KLMS | provision result | `transaction_id, result, stage_reached, apdu_count, timestamp` |
| 20 | KLMS → gen side / Ford | audit + retire package | `fesn, transaction_id, result, timestamp, audit` (consume cached package) |

**Alt / error paths**
- **Cache miss / low stock** (#4): KLMS returns `NoPackage`; watchdog priority-refill;
  station retries after brief wait.
- **Card not authenticated** (#7): no session keys issued (#13 = error); station halts.
- **TTL expiry** (`expires_at` before EXTERNAL AUTH): fob rejects #14 (`69 82`).
- **HSM unreachable** (#5/#8/#11): `KlmsError::Client`; station surfaces setup error.

---

## 5. ASCII overview (real-time plane)

```
Fob         Station          KLMS (cache+orch)      gen/Ford        Station-HSM
 │ INIT UPD   │                  │                     │                │
 │◄───────────┤ 80 50 … hc       │                     │                │
 │ UID‖cc‖cg  │                  │                     │                │
 ├───────────►│ establish(mTLS)  │                     │                │
 │            ├─────────────────►│ cache lookup(uid)   │                │
 │            │                  │ AES-CMAC(verify)    │                │
 │            │                  ├─────────────────────────────────────►│
 │            │                  │◄─────────────────────────────────────┤ card-cg
 │            │                  │ AES-CMAC×3(ses) +MAC(host)+CBC(A003) │
 │            │                  ├─────────────────────────────────────►│
 │            │                  │◄─────────────────────────────────────┤ ses*+cg+A003ct
 │            │ session resp     │                     │                │
 │            │◄─────────────────┤                     │                │
 │ EXT AUTH   │                  │                     │                │
 │◄───────────┤ 84 82 … cg+MAC   │                     │                │
 │ 9000       │                  │                     │                │
 ├───────────►│ STORE DATA×N …   │                     │                │
 │            │ lifecycle+perso  │                     │                │
 │ 9000       │ result           │                     │                │
 ├───────────►├─────────────────►│ audit+retire pkg    │                │
 │            │                  ├────────────────────►│                │
```

---

## 6. HSM product choice (YubiHSM 2 / Nitrokey / Utimaco)

All three satisfy "AES primitives + bundle decryption + generated keypair". The
choice affects **how** the KLMS calls primitives:

| Product | AES-CMAC | Bundle decrypt | Notes |
|---|---|---|---|
| **YubiHSM 2** | native (`CKM_AES_CMAC`); key objects; derive/wrap | RSA-OAEP / ECDH / AES-KW | ergonomic; per-fob key objects easy |
| **Nitrokey HSM 2** (SmartCard-HSM) | via `CKM_AES_ECB`, host assembles CMAC (our `aes_cmac_via_ecb`) | RSA / AES | cheapest; CMAC assembled host-side |
| **Utimaco** | native `CKM_AES_CMAC`; full PKCS#11 | full | enterprise server HSM; most capable |

> Whatever is chosen, the KLMS talks to it through a single **primitive backend**
> trait (`aes_cmac(handle, data)`, `aes_cbc_encrypt(handle, data)`,
> `decrypt_bundle(ct)`, `generate_keypair/export_pub`), so the product is swappable.

---

## 7. Security invariants & open decisions

**Invariants**
- Master key: generation-side only. Never in the station HSM, KLMS, or station.
- Station-side HSM: only the bundle-decryption private key (generated in it) + AES
  primitives + transient per-fob static-key objects.
- KLMS: orchestrates; holds decrypted-package cache + content; no master key.
- Station: only ephemeral session keys (zeroized on drop) + pre-encrypted A003.
- Links: Station↔KLMS = mTLS; KLMS↔HSM = local/TLS; KLMS↔gen side = mTLS.

**Open decisions**
- [ ] HSM product (YubiHSM 2 / Nitrokey / Utimaco).
- [ ] Static-key placement: HSM key objects (preferred) vs KLMS RAM transient.
- [ ] Bundle encryption scheme (RSA-OAEP / ECIES / AES-KW) + push vs pull delivery.
- [ ] Does `A003` plaintext reach the KLMS, or is it encrypted inside the HSM during
      bundle unwrap? (preferred: HSM-side, never in KLMS RAM)
- [ ] 2-week stock sizing (line rate × hours × margin).
- [ ] Session-key TTL (≈5 s) + single-use enforcement.
- [ ] Production security level: C-MAC only, or +R-MAC/+C-DEC.
- [ ] Key-version strategy per batch.

---

## 8. Code alignment (`kf-klms`, our boundary)

Our code boundary is **plane B's Station↔KLMS contract only** — unchanged by this
revision. The KLMS internals (cache/watchdog/HSM primitives/bundle decrypt) are
Clypeum's.

| Type | Status |
|---|---|
| `SessionRequest` / `SessionResponse` / `KlmsContent` | already match §3; add `transaction_id`, `station_id`, `fesn`, `spid`, `authenticated`, `expires_at` for production |
| `KlmsProvisioner` | already enforces `encrypt_with_dek=false` (app holds no S-DEK) |
| DLL-backed `KlmsClient` | maps 1:1 onto `POST /provision/session` |
| `kf-hsm` (`MasterKeyBackend`) | models the **dev** SW-KDF path (master→static). The **production** station-side HSM primitive/bundle-decrypt backend is KLMS-side (Clypeum), not in our binaries. |

> `kf-hsm`'s `aes_cmac_via_ecb` is directly reusable if Clypeum picks the Nitrokey
> SmartCard-HSM (CMAC assembled from on-device AES-ECB).

id: 634604008c3a47b0aa9b265a27f08a8d
parent_id: 283dd54f183a4ce69366648f091336d1
created_time: 2026-06-30T07:01:07.554Z
updated_time: 2026-06-30T08:04:42.964Z
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: 1782802867554
user_created_time: 2026-06-30T07:01:07.554Z
user_updated_time: 2026-06-30T07:17:22.864Z
encryption_cipher_text: 
encryption_applied: 0
markup_language: 1
is_shared: 0
share_id: 
conflict_original_id: 
master_key_id: 
user_data: 
deleted_time: 1782806682964
type_: 1