# Custom ISO14443-A command handling for the card sim — Design **Status:** Design approved, unimplemented · **Date:** 2026-07-05 · **Plan:** [2026-07-05-14a-custom-commands-plan.md](2026-07-05-14a-custom-commands-plan.md) ## Problem The 14a card sim serves the *standard* command set from firmware-native handlers backed by emulator RAM (EML). It has **no mechanism for custom/vendored commands**: - **14a-3 (Layer 3):** proprietary commands common in the parts we model — most importantly **NTAG I2C** `SECTOR_SELECT` (0xC2) + cross-sector `READ`/`FAST_READ`, plus vendor reads (ST25TN system block / product ID). Today an unknown L3 command falls through `SimulateIso14443aTagEx`'s final `else` and gets **no response**. - **14a-4 (Layer 4 / ISO-DEP):** vendor APDUs and crypto (DESFire, JCOP, EMV, custom applets). Today RATS→ATS is answered, but any I-block APDU gets a **blanket fake `90 00`** (`iso14443a.c:2529-2545`) with no parsing — wrong for everything real. This contrasts with the **15693 sim**, which already serves custom commands from a Python-compiled response table in BigBuf (`sim_table`) consulted in its `default:` case (`iso15693.c:~3081`). That mechanism is proven; 14a simply never got it wired in. ## Constraints 1. **86µs FDT at Layer 3.** A tag answer must start ~86µs after the reader command ends. This rules out a live USB round-trip for L3 — no reader-command → host → response is possible inside the window. 2. **Reuse over reinvention.** The firmware is a fork maintained with atomic single-file commits for easy rebase against upstream PM3. The 15693 `sim_table` server, the 120-byte `TableEntry` wire format, the `TableCompiler`, and the `0x0950-0x0952` upload command IDs already exist and are fork-local. 3. **Additive, non-invasive.** The native-EML sim is correct and battle-tested for standard commands. Custom-command support must *layer on top* of it, not replace it. ## Decision **A table-first L3 + WTX-L4 hybrid, layered on the native-EML sim.** Five mechanisms, each the cheapest *correct* one for its command class, all tracing through the existing `EmLogTrace` chokepoint: | # | Layer | Mechanism | Handles | Host round-trip? | |---|-------|-----------|---------|------------------| | A | L3 | **Native-EML** (unchanged) | Standard: anticoll, READ, FAST_READ, WRITE, GET_VERSION, READ_SIG, counters, RATS | No | | B | L3 | **Native-EML + sector offset** (graft) | NTAG I2C cross-sector READ/FAST_READ | No | | C | L3 | **Static `sim_table`** (new hook) | Custom-command *state machine* (SECTOR_SELECT), static vendor reads | No (pre-uploaded) | | D | L3 | **Native firmware auth** (existing branches) | UL-C 3DES, UL-AES, MFC Crypto1 | No | | E | L4 | **Static `sim_table`** (same hook) | Deterministic ISO-DEP APDUs: Type-4 NDEF SELECT/ReadBinary, canned GET/PUT DATA | No (pre-uploaded) | | F | L4 | **WTX relay** (new) | Dynamic ISO-DEP crypto: DESFire/JCOP/EMV, unpredictable reader nonce | Yes (S(WTX) buys seconds) | The whole thing is **three additive firmware hooks** in `SimulateIso14443aTagEx`, copied in spirit from the 15693 sim: 1. **Host-poll** at loop top — `data_available()`/`receive_ng()` handling `CMD_SIM_TABLE_UPLOAD/UPDATE/CLEAR`, EML load, relay responses (mirrors `iso15693.c:2472-2508`). Prerequisite: without it the table is empty forever. 2. **Table-check** at the top of the final `else` (`:2493`) — normalize, `sim_table_lookup`, `sim_table_execute`; placed *before* the ST25TA block and the fake-`90 00` switch so the table overrides those fakes. On an L4 I-block miss with ISO-DEP active, falls into the WTX relay. 3. **Sector-offset graft** on native `READ`/`FAST_READ` — add `sim_active_sector()*0x400` to the `emlGet` source offset. ## Why not the alternatives - **Relay-first at L3 (rejected).** Serving custom L3 commands by relaying to Python on a reader *retry* was scored `correctness 2 / fdt_safety 2` and rejected: the 86µs FDT is unreachable over USB, and a reader that gives up drops to REQA/WUPA + anticollision, losing the very state (selected sector) the relay was trying to serve. Retry-relay survives only as a debug affordance. - **Per-page table for cross-sector reads (rejected).** Representing NTAG I2C sectors as hundreds of per-page `sim_table` entries blows the linear-scan lookup toward the FDT and the entry/response-size ceilings. Instead, **reads stay native** and become sector-aware via a one-line offset — O(1), wire-speed, no entry explosion. ## The SECTOR_SELECT problem → group state + a firmware projection NTAG I2C `SECTOR_SELECT` is the hard case that shaped the design. It is: - **Two-phase:** `C2 FF` (packet 1) → 4-bit ACK, then a *bare* sector byte `s 00 00 00` (packet 2, no opcode) → **silent success** (the tag transmits nothing for >1ms). - **Stateful and persistent:** it changes the addressing of *all subsequent* READs until the next SECTOR_SELECT or a re-selection. This is handled by the `sim_table` **group state machine** plus a read-only firmware projection: - Phase 1 (`C2 FF`) activates a transient `GRP_SEL_PENDING` group and 4-bit-ACKs. - Phase 2 (`s 00 00 00`) matches **only inside `GRP_SEL_PENDING`** (so a stray `00`/`01` frame can't false-flip the sector), sets `GRP_SECTOR_s`, clears the pending group, and responds with `response_len == 0` = the passive-ACK silence. - An invalid-sector catch-all in `GRP_SEL_PENDING` returns a 4-bit NAK. - `sim_active_sector()` derives the active sector **read-only** from the group register, so the group register stays the single source of truth (no drift between routing and read data), and native READ/FAST_READ offset into the right sector. This is the "graft" that resolves both judges' top risks: it keeps the table tiny (~6 entries, not 256) so FDT is trivially safe, and it makes native-firing-first *correct* (reads are sector-aware) instead of a bug that serves flat sector-0 data. ## Crypto taxonomy correction The judges flagged a miscategorization worth stating plainly: **Ultralight-C 3DES (0x1A/0xAF) and UL-AES are Layer-3 framed** (no ISO-DEP). They are servable by *neither* the table (unpredictable reader `RndB`) nor the L4 WTX relay (86µs FDT). They use the **existing native firmware auth branches** (`iso14443a.c:2242/2273/2324/2355`) with the model key loaded into EML — a "native-auth" track (mechanism D), first cut with the firmware's fixed nonce (a documented fidelity gap vs the model's random `RndB`). Only genuinely dynamic *ISO-DEP* crypto (DESFire/JCOP AES) goes to the L4 relay. ## Provenance This design was produced by a multi-agent workflow: 6 parallel readers mapped the 14a sim dispatch, the proven 15693 `sim_table` + retry-relay template, the tag-model custom commands, the `TableCompiler`, the WTX-relay design, and the L3 timing/retry reality; 3 competing designs (static-table-first, relay-first, hybrid) were adversarially judged; and the result was synthesized. Judge outcome: static-table-first and hybrid both "adopt-with-changes" (reuse 5/5); relay-first "reject" (FDT-unsafe). See the plan doc for the sequenced, implementation-grade steps.