Ford EoL Key Fob Provisioning System (SCP03)

# Ford EoL Key Fob Provisioning System (SCP03)

## Project Overview

**End-of-Line (EoL) provisioning station** that writes key material to NFC key fobs using **Secure Channel Protocol 03 (SCP03)**. Built in **Rust** for **Windows**, communicating with industrial-grade NFC readers via USB.

| Detail | Value |
|---|---|
| OEM | Ford |
| KLMS | SecOps "Clypeum" |
| Language | Rust |
| Target OS | Windows |
| Protocol | GlobalPlatform SCP03 |
| Smart Card Platform | JCOP 4 |
| Chip | NXP NCJ37x (P71D600) |
| Hardware | Industrial NFC reader via USB (CCID) |

---

## Implementation Status (2026-06-15, rev 3 — committed)

A 7-crate Rust workspace. Everything runs **headless** — no hardware required — against a simulated JCOP applet. **64 unit tests pass, `clippy -D warnings` clean, `fmt` clean.** The pipeline mirrors the **real JCShell provisioning flow** (DGI-based STORE DATA with lifecycle transition) and the container parser handles both available fixture packages.

### Workspace Layout

| Crate | Purpose | Status |
|---|---|---|
| `kf-crypto` | AES-CMAC/CBC (NIST-validated), ISO 9797-1 Method 2 padding, zeroizing keys, `ct_eq` | ✅ Done |
| `kf-apdu` | APDU command/response (short + extended), `StatusWord` enum, GP command builders (STORE DATA E2, lifecycle transition) | ✅ Done |
| `kf-scp03` | Session key derivation, host/card cryptograms, `Scp03Channel` wrap/unwrap, shared `wire` module | ✅ Done |
| `kf-transport` | `SmartcardTransport` trait, `MockTransport`, `SimulatedJcopApplet` (C-MAC only, lifecycle states) | ✅ Done |
| `kf-source` | `ProvisioningSource` trait (async), serde types, `StaticProvisioningSource`, **`FileContainerSource`** + **`StaticCertificates`** | ✅ Done |
| `kf-provision` | `Provisioner` orchestrator + `AuditRecord` + **`perso` module (DGI encoding + block splitting + S-DEK encryption)** | ✅ Done |
| `kf-station` | CLI binary (`self-test`, `provision`) | ✅ Done |

### Implemented & Tested (headless)
- Crypto primitives validated against NIST SP 800-38B AES-CMAC vectors.
- Full SCP03 state machine: INITIALIZE UPDATE → session key derivation → EXTERNAL AUTHENTICATE → secure messaging wrap/unwrap.
- **DGI-based personalization flow** matching the real JCShell script: encode items as DGIs → S-DEK encrypt private key → split into 245-byte blocks → STORE DATA (INS=E2) with incrementing P2 → lifecycle transition (P1=0x80) → raw post-personalization commands.
- **Container directory parser** reads `FESN.txt`, `irk_keyfob.key` (base64), `keyfob_private.der` (PKCS#8 EC scalar extraction), `keyfob_public.der` (device cert) → builds `ProvisioningInput`.
- **Static factory certificates** (`StaticCertificates`): ICA cert (DGI A004, `CN=FMC-NFC-ICA`) and CMS/Root cert (DGI A005, Ford Motor Company KeyFob CA) are loaded from shared files and verified identical across both fixture containers.
- Simulated JCOP applet: C-MAC only mode, DGI data buffering, lifecycle transition to FACTORY, raw post-perso command acceptance.
- End-to-end pipeline against the mock + real fixture data (64 tests, all green).
- Security hardening: CSPRNG host challenge, constant-time compares, zeroizing keys, redacted Debug, R-MAC hard-fail, **TX audit entries redacted** (no secrets in audit record), **empty personalization_items rejected**, **DGI tag length validated before indexing**.

### Code Review Status
Two review passes completed. All CRITICAL findings fixed (panic on short DGI, silent empty-success, audit secret leak). All WARNING findings fixed (applet auth bypass on empty-data STORE DATA, SPID parser fragility, dead `max_block_data` config). Remaining SUGGESTION-level items: orphaned `handle_set_status`/`ins::SET_STATUS`, unused `lifecycle()`/`is_factory()` accessors.

### Stubbed Behind Traits (pending external decisions / hardware)
- **DLL/SO content provider** — trait ready; awaits DLL interface spec.
- **KLMS REST client** — serde types ready; awaits REST API contract + mTLS details.
- **Real `pcsc` transport** — `SmartcardTransport` trait ready; awaits NFC reader hardware.

### Build & Test Commands
```sh
cargo fmt --all
cargo clippy --all-targets -- -D warnings
cargo test --workspace
cargo run -p kf-station -- self-test
```

> See the companion note **"Keyfob Station — Code Review & Open Issues"** for findings and open questions.

### Next Steps
1. **Real-card validation** — run the pipeline against a real JCOP 4.5 sample to validate the SCP03 test vector and the R-ENC path.
2. **DLL/SO content provider** — implement once the interface spec is available.
3. **KLMS REST client** — implement once the API contract is finalized.
4. **Key Version strategy** — confirm KVN=255 vs production value with KLMS.

---

## JCShell Provisioning Flow (from reference script)

### Key Parameters

| Parameter | Value |
|---|---|
| Applet AID | `A000000857` |
| Key Version | 255 (0xFF) |
| Security Level | C-MAC only (`auth mac`, P1=0x01 in EXTERNAL AUTHENTICATE) |
| S-ENC | `8b4b8d7ec45dfa503a35f6df8f6bbdd9` |
| S-MAC | `42fd95e83821260c5b90463af7996313` |
| S-DEK | `1b988a47ac94632f7020734ca172eed8` |

### DGI Tags & Personalization Items

| DGI | Description | Data | Encrypted | Per-fob? |
|---|---|---|---|---|
| `A003` | Private key scalar (P-256) | 32 bytes → AES-CBC(S-DEK, M2 pad) → 48 bytes | Yes (S-DEK) | Yes |
| `A001` | SPID | ASCII hex string (40 bytes = 20-byte hash as text) | No | Yes |
| `A002` | Device certificate | DER (424/422 bytes) | No | Yes |
| `A004` | ICA certificate | DER (435 bytes) | No | **No** (static) |
| `A005` | CMS/Root certificate | DER (722 bytes) | No | **No** (static) |
| `A006` | BLE IRK | 16 bytes | No | Yes |

### DGI Wire Encoding

```
TAG(2 bytes) || length || data
```

Length encoding:
- `< 255`: 1-byte direct length (0x00–0xFE)
- `≥ 255`: `0xFF` + 2-byte big-endian length

### STORE DATA Block Protocol

- **INS = 0xE2** (not 0xDA)
- **P1**: `0x00` = normal block, `0x80` = last block (lifecycle transition)
- **P2**: global incrementing block counter (starts at 0x00)
- **Max block data**: 245 bytes (255-byte short APDU limit − 8-byte MAC − 2-byte slack)
- Large DGIs split: first block carries DGI header, continuation blocks carry raw data
- **Lifecycle transition**: final STORE DATA with `P1=0x80`, empty data → applet transitions to FACTORY state

### S-DEK Pre-Encryption (for DGI A003)

Private key scalar is encrypted **before** DGI wrapping:
1. Pad 32-byte scalar with ISO 9797-1 Method 2 → 48 bytes
2. AES-CBC encrypt with S-DEK, IV = all zeros
3. Result (48 bytes) becomes the DGI A003 payload

### Post-Personalization Commands

After lifecycle transition, raw (non-SM) commands re-configure the applet:
```
00 DB 00 00 03 0A 01 01   — enable UICC transport
00 DB 00 00 03 0B 01 01   — enable BLE transport
```

---

## Container Directory Format

Each fob's provisioning data arrives as a directory:

| File | Format | Content | DGI |
|---|---|---|---|
| `FESN.txt` | ASCII (8 bytes) | Serial number (e.g. `1KM0001E`) | — (dir name component) |
| `irk_keyfob.key` | Base64 (24 chars) | BLE IRK (16 bytes decoded) | `A006` |
| `keyfob_private.der` | DER (138 bytes) | PKCS#8 EC P-256 private key | `A003` (scalar extracted, S-DEK encrypted) |
| `keyfob_public.der` | DER (422-424 bytes) | Device certificate | `A002` |

Directory name format: `<FESN>-<SPID_HEX>` where `<SPID_HEX>` is a 40-char hex string used as the DGI `A001` payload (stored as ASCII hex bytes, not raw binary).

**Static factory certificates** (shared across all fobs):

| File | Content | DGI |
|---|---|---|
| `ica_cert.der` | ICA intermediate CA cert (`CN=FMC-NFC-ICA`, issuer `CN=FMC-NFC-ROOT`, 435 bytes) | `A004` |
| `cms_root_cert.der` | CMS/Root CA cert (Ford Motor Company KeyFob Pair ECC Issuing CA, 722 bytes) | `A005` |

These are loaded via `StaticCertificates::from_dir()` and are **byte-for-byte identical** across both fixture containers (verified).

**Private key scalar extraction**: In PKCS#8 DER, the 32-byte scalar appears as `OCTET STRING(04) || length(20) || 32 bytes`. We scan for the `04 20` byte pair, which appears in the private key OCTET STRING before the public key BIT STRING in DER ordering.

**IRK decoding**: Base64 standard alphabet → 16 raw bytes.

### Fixture Packages Available

| FESN | SPID Hash | Device Cert CN | Cert Size |
|---|---|---|---|
| `1KM0001E` | `59918C0096F859EC8BF6DA454E2E554A14DD4BD1` | `CN=1KM0001E` | 424 bytes |
| `1KM0001F` | `95F12CA4718013F85A5F7C1F02C1382E2F03C7AB` | `CN=1KM0001F` | 422 bytes |

---

## Architecture

### Final System Architecture

The EoL station is a **lean APDU pipeline**:

1. Read fob UID via `pcsc`
2. POST UID to Clypeum KLMS via REST API (mTLS)
3. KLMS returns pre-diversified static keys + Ford's payload
4. Perform session KDF (Phase 2) in software
5. SCP03 wrap → inject DGI personalization data → lifecycle transition → zeroize memory

### Key Design Decision: KLMS Does All KDF

The architecture evolved from local Nitrokey HSM-based key derivation to having the **Clypeum KLMS server perform all KDF**. The EoL station is considered an untrusted factory floor environment.

**Phase 1 (NXP KDF3)** — Performed by Clypeum inside their HSM:
- Master Key → S-ENC, S-MAC, S-DEK using AES-CMAC
- Labels: `00000001` (ENC), `00000002` (MAC), `00000003` (DEK)
- Context: 10-byte JCOP UID
- Key length: `0080` (128-bit)

**Phase 2 (GP SCP03)** — Performed by EoL station in RAM:
- Static keys → Session keys (SES-ENC, SES-MAC, SES-RMAC) using AES-CMAC
- Context: `SeqCnt(3 bytes) || RND.IC(8 bytes) || RND.CC(8 bytes)`

---

## SCP03 Protocol Flow

> **Reference:** [GlobalPlatform Card Spec v2.3 Amendment D — Secure Channel Protocol 03 (GPC_2.3_D_SCP03)](https://globalplatform.org/specs-library/?filter-committee=se)

### Step 1: INITIALIZE UPDATE
- Command: `80 50 <KVN> 00 08 <Host_Challenge(8)>`
- Response (S8 mode): `div(10) || key_info(1) || seq_counter(3) || KVN(2) || card_challenge(8) || card_cryptogram(8)`

### Step 2: Derive Session Keys
- SES-ENC, SES-MAC, SES-RMAC derived from static keys + SeqCnt + RND.IC + RND.CC
- Uses AES-CMAC
- Derivation constants: S-ENC=0x04, S-MAC=0x06, S-RMAC=0x07

### Step 3: EXTERNAL AUTHENTICATE (C-MAC only)
- P1 = 0x01 (C-MAC only security level)
- Calculate Host Cryptogram: `AES-CMAC(S-MAC, derivation_block)`
- Wrap APDU: MAC appended with S-MAC chaining
- Success: `90 00`

### Step 4: Secure Messaging — STORE DATA (INS = E2)
- Each DGI encoded: `TAG(2) || length || data`
- Optional S-DEK pre-encryption for private key DGIs
- Split into 245-byte blocks
- SCP03 C-MAC wrap each block
- P2 = incrementing global counter

### Step 5: Lifecycle Transition
- Final STORE DATA: `84 E2 80 <P2> 00` (P1=0x80, empty data)
- Applet transitions UNPERSONALIZED → FACTORY

### Step 6: Post-Personalization (Raw, No SM)
- Re-configure transport modes via `00 DB 00 00` commands
- Sent outside the SCP03 secure channel

---

## Provisioning Pipeline Stages

| Stage | Description |
|---|---|
| `ReadUid` | Read fob UID from transport |
| `FetchKeys` | Fetch static keys + personalization items from source |
| `InitializeUpdate` | SCP03 INITIALIZE UPDATE |
| `ExternalAuthenticate` | SCP03 EXTERNAL AUTHENTICATE (C-MAC) |
| `PayloadDelivery` | STORE DATA blocks with DGI personalization data |
| `LifecycleTransition` | Final STORE DATA P1=0x80 → FACTORY |
| `PostPersonalization` | Raw commands (enable transports) |

---

## Contacts & Escalation

| Area | Contact |
|---|---|
| IT | Tony |
| Technical Topics | Ashish |
| Project Questions | Onoyom |
| Manufacturing | Daniel |
| SW Development | Mindu |
| Escalation | John, Abdel |
| Ford IVSS | Mustafa, Antony Mihalopulous |
| Project CS | Joe |

---

## Development Samples (OEF B212)

Development samples arrive with **OEF B212** and a **pre-defined static SCP03 key set** already loaded on the chip.

- No KLMS key diversification needed for dev samples — use the known static keys directly
- Only **Phase 2** (session key derivation) is required to establish the SCP03 channel

---

## Roles & Responsibilities

| Role | Entity | Responsibility |
|---|---|---|
| OEM | Ford | Provides application data to be stored on fob |
| Card Issuer | Supplier (us) | Manages SCP03 channel keys, provisions card, locks ISD |
| KLMS | SecOps (Clypeum) | Stores master keys, performs KDF, delivers static keys + payload |
| EoL Station | Our Rust app | PC/SC pipe, session KDF, SCP03 state machine, audit reporting |

---

## Key Diversification (JCOP 4)

> **Reference:** [NXP AN10922 — Symmetric Key Diversifications](https://www.nxp.com/docs/en/application-note/AN10922.pdf)

### KDF3 Input Structure (Phase 1)
```
[Counter: 01] || [Label: 00 00 00 01/02/03] || [Separator: 00] || [Context: 10-byte UID] || [Length: 00 80]
```

### Session Key Context (Phase 2)
```
SeqCnt (3 bytes) || RND.IC (8 bytes) || RND.CC (8 bytes)
```

---

## Rust Technology Stack

| Crate | Purpose |
|---|---|
| `aes` | AES encryption (RustCrypto) |
| `cmac` | AES-CMAC for key derivation and MACing |
| `cbc` | AES-CBC for APDU encryption |
| `cipher` | Block cipher traits |
| `zeroize` | Secure memory wiping after ISD lock |
| `subtle` | Constant-time comparisons |
| `time` | ISO-8601 timestamps for audit records |
| `getrandom` | CSPRNG host challenge |
| `serde` / `serde_json` | KLMS JSON parsing |
| `hex` | Hex encode/decode |
| `base64` | BLE IRK decoding from container files |
| `tracing` | Structured logging |
| `clap` | CLI argument parsing |
| `tokio` | Async runtime |
| `async-trait` | Async trait for `ProvisioningSource` |

---

## Error Handling (APDU Status Words)

| SW1/SW2 | Meaning | Action |
|---|---|---|
| `90 00` | Success | Proceed |
| `69 82` | Security status not satisfied | Halt — wrong key/MAC. Flag for review. |
| `6A 86` | Incorrect P1/P2 | Halt — bug in APDU construction |
| `67 00` | Wrong length | Halt — bug in APDU construction |
| `6D 00` | Instruction not supported | Halt — wrong applet selected? |
| `65 81` | Memory failure | Halt — defective card |
| `63 CX` | Verify fail, X retries left | Analyze — `63 C0` = permanently locked |

---

## Items to Confirm with SecOps/Clypeum

- [ ] REST API contract (request/response JSON format)
- [ ] Mutual TLS (mTLS) for KLMS↔EoL network link
- [ ] Exact NXP KDF3 byte layout (UID padding: raw, left-padded, or right-padded)
- [x] ~~Exact APDU for Ford's data injection~~ → STORE DATA (INS=E2) with DGI tags
- [ ] Key Version strategy (how to know which version the fob expects)
- [ ] Audit payload format and Ford compliance requirements
- [ ] TTL on issued key packages (recommended: 5 seconds)
- [ ] KLMS logging and audit trail capabilities
- [ ] Source of ICA/Root certificates (DGI A004/A005) — resolved: factory-wide static, loaded from shared dir
- [ ] SPID derivation — verified: NOT SHA-1 of FESN; comes from external source (directory name)

---

*Source: Z.ai chat session (2025-06-08). Updated 2026-06-15 rev 3 with JCShell analysis + DGI personalization flow + container parser + static certs + dual-fixture validation + code review fixes.*

id: 445891dba8674cae8b865a6fd2a3faf1
parent_id: 283dd54f183a4ce69366648f091336d1
created_time: 2026-06-08T06:51:37.561Z
updated_time: 2026-06-15T15:43:56.961Z
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: 1780901497561
user_created_time: 2026-06-08T06:51:37.561Z
user_updated_time: 2026-06-15T15:43:56.961Z
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