- Add docs/plans/README.md as the roadmap index cataloguing all design/plan docs. - Add the custom ISO14443-A command handling design + plan (L3 NTAG I2C SECTOR_SELECT / cross-sector reads + native auth; L4 static APDUs + WTX relay), produced from a multi-agent design workflow. - Backfill previously-uncommitted plan docs (trace-formatter, ndef-trace-decode, live-sniff, firmware-upstream-rebase, 14a-live-trace) and NTAG5_SECURITY.md. - Sync CLAUDE.md package structure with the committed transponder models. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
249 lines
20 KiB
Markdown
249 lines
20 KiB
Markdown
# Custom ISO14443-A command handling — Implementation Plan
|
||
|
||
**Status:** Planned, unimplemented · **Date:** 2026-07-05 · **Design:** [2026-07-05-14a-custom-commands-design.md](2026-07-05-14a-custom-commands-design.md)
|
||
|
||
Serve custom/vendored ISO14443-A commands in the card sim, layered on the native-EML
|
||
14a sim. Two independently shippable tracks: **L3** (NTAG I2C SECTOR_SELECT + cross-sector
|
||
reads, static vendor reads, native auth) and **L4** (static ISO-DEP APDUs + WTX relay for
|
||
dynamic crypto). Reuses the proven 15693 `sim_table` server, the 120-byte `TableEntry`
|
||
format, and the `TableCompiler`. All firmware line numbers are anchors against the current
|
||
`firmware/armsrc/iso14443a.c`; each firmware change lands as an atomic single-file commit
|
||
anchored to **stable symbols** (`ISO14443A_CMD_RATS`, the final `else`,
|
||
`EmSendPrecompiledCmd`), not line numbers.
|
||
|
||
---
|
||
|
||
## L3 track — mechanisms
|
||
|
||
- **(A) Native-EML (unchanged):** REQA/WUPA/anticoll/SELECT/PPS/GET_VERSION/READ_SIG/
|
||
READ/FAST_READ/WRITE/COMPAT_WRITE/READ_CNT/INCR_CNT/HALT/RATS. Byte-correct within
|
||
86µs, zero new code. WRITE mutates the EM BigBuf so later reads reflect writes natively.
|
||
- **(B) Native-EML + sector offset (graft):** cross-sector READ/FAST_READ.
|
||
`sim_active_sector()` maps the `sim_table` active-group bits → `{0,1,3}`; native READ
|
||
(`:2033-2058`) and FAST_READ (`:2081-2093`) add `sim_active_sector()*0x400` to the
|
||
`emlGet` source offset. Python pre-zeroes invalid pages in the uploaded multi-sector EML
|
||
image so native 00-fill falls out for free. Reads stay O(1)-native, sector-correct.
|
||
- **(C) Static `sim_table` (new `:2493` hook):** the SECTOR_SELECT *state machine* +
|
||
genuinely-custom static reads only (see worked example). No per-page entries.
|
||
- **(D) Native firmware auth (existing branches):** UL-C 3DES (`:2242`), UL-AES
|
||
(`:2273/2324/2355`), MFC Crypto1 — model key in EML, firmware computes challenge/response,
|
||
first cut fixed-nonce. Not table, not relay.
|
||
|
||
**L3 miss policy:** reader-retry relay is debug-only (86µs FDT is unreachable over USB).
|
||
Everything reachable at L3 is covered by A–D.
|
||
|
||
## L4 track — mechanisms
|
||
|
||
Gated by a new `iso_dep_active` flag (set at RATS→ATS `:2234`; cleared on HALT / S(DESELECT)).
|
||
|
||
- **(E) Static `sim_table`** (same `:2493` hook): deterministic ISO-DEP APDUs — Type-4
|
||
NDEF SELECT-App(AID) / SELECT-File(CC,NDEF) / ReadBinary, canned GET/PUT DATA, fixed
|
||
`90 00`. Firmware masks the PCB (`(pcb&0xE2)==0x02` = I-block), computes
|
||
`off = 1 + CID + NAD`, strips the trailing CRC, `MATCH_PREFIX` on the bare APDU; on hit
|
||
re-wraps (`response[0]=pcb`, CID re-inserted at `response[1]` when `pcb&0x08`, payload at
|
||
`response+off`, `AddCrc14A`). **Overrides the bogus blanket `90 00`** at `:2529-2545` —
|
||
which is exactly why the hook sits at `:2493` (before the switch), not `:2576`.
|
||
- **(F) WTX relay** (dynamic crypto): on an I-block table miss with `iso_dep_active`,
|
||
firmware sends a tag-side **S(WTX)** to buy time (FWT×59 ≈ 4.8s), relays the normalized
|
||
APDU to Python, spin-waits for the computed response, re-wraps and sends. The Python
|
||
transponder model owns per-session nonce/IV/auth state.
|
||
|
||
**L4 chaining** (gating dependency for DESFire/NDEF coverage): tag block-number toggle
|
||
(PCB bit0 per I-block), reader→tag reassembly (PCB bit4 → R(ACK) `0xA2|bn`), tag→reader
|
||
FSD split into chained I-blocks. `DYNAMIC_RESPONSE_BUFFER_SIZE` bumped `64→256` first.
|
||
v1 gated to single-frame + `0xAF` continuation until chaining lands.
|
||
|
||
---
|
||
|
||
## Firmware steps
|
||
|
||
| ID | File | Change |
|
||
|----|------|--------|
|
||
| **FW-1** | `armsrc/sim_table.h` + `.c` | Generalize `sim_table_execute` from `(iso15_tag_t *tag)` to `(uint8_t *eml_data, uint16_t eml_size, uint32_t *auth_state)`; replace `tag->data`/`sizeof(tag->data)`/`tag->auth_state` at `sim_table.c:132-169`. Drop `#include iso15.h` from the header. Add `#define SIM_FLAG_RESP_4BIT 0x04`. Struct unchanged (120 bytes PACKED). Fork-local. |
|
||
| **FW-2** | `armsrc/iso15693.c` | Update the single `sim_table_execute` call site (`~:3081`) to the new signature: `(tag->data, sizeof(tag->data), &tag->auth_state)`. Keeps 15693 green. |
|
||
| **FW-3** | `armsrc/iso14443a.c` (commit A) | `#include sim_table.h`. Add file-scope `static uint32_t s_auth_state` and `sim_active_sector()` near the sim-trace statics (`~:1727`); reset `s_auth_state=0` in the `FLAG_SIM_TRACE` arm. Bump `DYNAMIC_RESPONSE_BUFFER_SIZE 64→256` and the modulation buffer. Insert **host-poll** block at loop top after `sim_trace_flush` (`~:1907`): `CMD_SIM_TABLE_UPLOAD` (4-byte `initial_groups` + 120B entries → `sim_table_init/add`), `UPDATE`, `CLEAR`, `CMD_HF_MIFARE_EML_MEMSET` (`emlSet`), `CMD_BREAK_LOOP`. Copied ~verbatim from `iso15693.c:2472-2508`. |
|
||
| **FW-4** | `armsrc/iso14443a.c` (commit B) | **L3 table-check** at `:2493` (after the `response_n`/`modulation_n` reset, before `tagType==10` and the fake-`90 00` switch): `sim_table_lookup` + `sim_table_execute`; honor `SIM_FLAG_RESP_4BIT` (`EmSend4bit`), `response_len==0` (passive-ACK silence), byte response (join the `:2591` build path), `SIM_FLAG_APPEND_CRC`. **Sector-offset graft** on native READ/FAST_READ (`sim_active_sector()*0x400`). Add labels `build_send:` (`:2591`) and `after_send:` (past `:2620`). |
|
||
| **FW-5** | `include/pm3_cmd.h` | Append `CMD_SIM_RELAY_EVENT 0x0953` and `CMD_SIM_RELAY_RESP 0x0954` in the existing `0x095x` block. Additive. |
|
||
| **FW-6** | `armsrc/iso14443a.c` (commit C) | Add file-scope `bool iso_dep_active` (near `order` `:1870`); set at RATS→ATS (`:2234`), clear on HALT (`:2214`) and S(DESELECT). Extend the `:2493` hook: detect I-block, `off=1+CID+NAD`, strip PCB/CID/NAD+CRC, `MATCH_PREFIX` on the bare APDU; on hit re-wrap and join `build_send`. Overrides fake `90 00`. |
|
||
| **FW-7** | `armsrc/iso14443a.c` (commit D, then E) | **D:** `EmSendWtx(uint8_t wtxm)` tag-side S(WTX) sender modeled on `EmSendCmd` (the `:3787-3808` `send_wtx` is reader-side, not reusable). On L4 miss: `EmSendWtx(59)` → await reader S(WTX) echo (`(b&0xF2)==0xF2`) → `reply_ng(CMD_SIM_RELAY_EVENT,{L4,apdu})` → spin `data_available()`/`receive_ng()` for `CMD_SIM_RELAY_RESP` (GetTickCount timeout) → re-wrap + `EmSendCmd`. **E:** block-number toggle + reader→tag chaining reassembly + tag→reader FSD split. |
|
||
|
||
## pm3py steps
|
||
|
||
| ID | File | Change |
|
||
|----|------|--------|
|
||
| **PY-1** | `pm3py/core/protocol.py` | **FIX id drift:** repoint `SIM_TABLE_UPLOAD/CLEAR/UPDATE` from stale `0x0900/0901/0902` → `0x0950/0951/0952`. Add `SIM_RELAY_EVENT=0x0953`, `SIM_RELAY_RESP=0x0954`. Reuse `HF_MIFARE_EML_MEMSET=0x0602` for EML load. |
|
||
| **PY-2** | `pm3py/sim/table_compiler.py` | Add `RESP_FLAG_4BIT=0x04` + group constants (`GRP_SEL_PENDING=1, GRP_SECTOR0=2, GRP_SECTOR1=3, GRP_SECTOR3=4`). Add `compile_ntag_i2c(tag)`: `base=compile_14a(tag)` + **only** the SECTOR_SELECT machine (no per-page reads). Add an `enumerate_table_entries()` hook so a model can yield full `TableEntry` objects (groups/flags) alongside the legacy `enumerate_responses` 3-tuple. |
|
||
| **PY-3** | `pm3py/transponders/hf/iso14443a/nxp/ntag_i2c.py` | Add `build_eml_image()` laying out `[mfu prefix][sector0, invalid pages zeroed][sector1][sector3 regs]` at `0x400` stride, reusing `_masked_page`/`_page_readable`. Add `enumerate_table_entries()` yielding the SECTOR_SELECT machine. Keep `enumerate_responses` (sector-0) for other consumers. |
|
||
| **PY-4** | `pm3py/sim/sim_session.py` (L3) | Fix constants. In `start_14a`: if the tag is table-backed, load the EML image (`CMD_HF_MIFARE_EML_MEMSET`) then `_compile_and_upload_table` with a 14a dispatch branch (`NtagI2C→compile_ntag_i2c`, `MifareClassic→compile_mifare`, else `compile_14a`). Factor the `initial_groups`+120B chunked uploader into a shared `_upload(table, initial_groups)`. |
|
||
| **PY-5** | `pm3py/sim/sim_session.py` (L4) | Async relay path for L4-crypto tags: send `HF_ISO14443A_SIMULATE` via async `PM3Transport`, `create_task(self._relay_loop(tag))`; **do not** spawn the sync `_trace_reader` thread (single async reader owns trace + relay on one fd). Flesh out `_relay_loop`: `CMD_SIM_RELAY_EVENT` → `RESELECT`→`tag.power_on()`; `L4`→`out=await tag.handle_frame(RFFrame.from_bytes(apdu))`, reply `CMD_SIM_RELAY_RESP` with `resp_flags` + bare bytes. |
|
||
|
||
## New / reused command IDs & flags
|
||
|
||
- `CMD_SIM_RELAY_EVENT = 0x0953` **(new)** fw→host: normalized L4 APDU on table miss.
|
||
- `CMD_SIM_RELAY_RESP = 0x0954` **(new)** host→fw: computed L4 response.
|
||
- `CMD_SIM_TABLE_UPLOAD/CLEAR/UPDATE = 0x0950/0951/0952` **(reused;** fix `protocol.Cmd` stale `0x0900` series).
|
||
- `CMD_HF_MIFARE_EML_MEMSET = 0x0602` **(reused)** to load the multi-sector 14a EML image (native sim already reads tag memory from the EM BigBuf via `emlGet`).
|
||
- `SIM_FLAG_RESP_4BIT = 0x04` **(new)** response-flag (fw `sim_table.h` + `RESP_FLAG_4BIT` in `table_compiler.py`) — not a wire command.
|
||
- `GRP_SEL_PENDING=1 / GRP_SECTOR0=2 / GRP_SECTOR1=3 / GRP_SECTOR3=4` — compiler-side group-bit convention (max 31 groups), not firmware constants.
|
||
|
||
## Wire formats
|
||
|
||
**`sim_table_entry_t`** (120-byte PACKED, reused byte-for-byte, LE
|
||
`<32s B B 64s B B B H B B B B B I I B B B B>`): `match[32]@0`, `match_len@32`,
|
||
`match_mode@33` (0=EXACT, 1=PREFIX), `response[64]@34`, `response_len@98`,
|
||
`response_flags@99` (bit0=`APPEND_CRC`, **bit2=`RESP_4BIT`** new), `eml_action@100`,
|
||
`eml_offset(u16)@101`, `eml_len@103`, `eml_resp_insert@104`, `cmd_data_offset@105`,
|
||
`cmd_data_len@106`, `group@107`, `activate_groups(u32)@108`, `deactivate_groups(u32)@112`,
|
||
`set_auth@116`, `clear_auth@117`, `flags@118` (bit1=`CONSUME`), `_pad@119`.
|
||
`TableEntry.serialize()` already emits exactly this.
|
||
|
||
**Upload** (unchanged): `CMD_SIM_TABLE_UPLOAD` first frame = 4-byte `initial_groups` (LE)
|
||
+ N×120B entries filling `512-4` bytes; `CMD_SIM_TABLE_UPDATE` = `(512//120)*120`B chunks;
|
||
`CMD_SIM_TABLE_CLEAR` = empty.
|
||
|
||
**Relay event** (`CMD_SIM_RELAY_EVENT` payload): `byte[0]=ev_type` (0=RESELECT, 3=L3-debug,
|
||
4=L4), `byte[1]=norm_len`, `byte[2..]=normalized frame` (L4: PCB/CID/NAD+CRC stripped).
|
||
**Relay resp** (`CMD_SIM_RELAY_RESP` payload): `byte[0]=ev_type echo`, `byte[1]=resp_flags`
|
||
(bit0=append-CRC, bit1=4BIT, bit2=passive-ACK/no-tx, bit4=chaining-more), `byte[2..]=bare
|
||
response`. `tag.handle_frame` returns **bare bytes** — firmware owns PCB/CID/NAD framing
|
||
and block-number.
|
||
|
||
## NTAG I2C worked example (NT3H2211, 2k) — reading Sector 1
|
||
|
||
**Compile (`compile_ntag_i2c`):** `initial_groups` seeds `GRP_SECTOR0` active. Multi-sector
|
||
EML image loaded via `CMD_HF_MIFARE_EML_MEMSET`. ~6 table entries (far under any FDT/entry
|
||
limit):
|
||
- `E1` `[C2 FF]` EXACT → `[0A]` `RESP_4BIT`, activate `GRP_SEL_PENDING`.
|
||
- `E2` `[00 00 00 00]` EXACT, group=`GRP_SEL_PENDING`, `response_len=0`, activate
|
||
`GRP_SECTOR0`, deactivate `SECTOR1|SECTOR3|SEL_PENDING`.
|
||
- `E3` `[01 00 00 00]` → `GRP_SECTOR1`. `E4` `[03 00 00 00]` → `GRP_SECTOR3`.
|
||
- `E5` `[]` PREFIX (last) in `GRP_SEL_PENDING` → `[00]` 4-bit NAK, deactivate `SEL_PENDING`.
|
||
|
||
**Runtime:**
|
||
1. Native anticoll/SELECT (unchanged). `sim_active_sector()==0`.
|
||
2. Reader → `C2 FF` → `:2493` hit `E1` → `EmSend4bit(0x0A)`, `active_groups |= SEL_PENDING`.
|
||
3. Reader → `01 00 00 00` (bare sector byte) → `:2493` in `SEL_PENDING` hit `E3` →
|
||
`response_len==0` ⇒ apply state (+`SECTOR1`, clear others + `SEL_PENDING`), **transmit
|
||
nothing** = the >1ms passive-ACK. `sim_active_sector()` now returns 1.
|
||
4. Reader → `FAST_READ 3A 00 0F` → **native** FAST_READ fires first (correct), reads
|
||
`emlGet` at `base + 1*0x400` = sector-1 memory, returns via `EmSendCmd`. Sector-correct,
|
||
O(1), wire-speed, no table scan, no USB.
|
||
5. Back to sector 0: `C2 FF` (E1), `00 00 00 00` (E2 → `SECTOR0`, silence).
|
||
6. Invalid sector: `C2 FF` then `07 00 00 00` → `E5` → 4-bit NAK, `SEL_PENDING` cleared.
|
||
|
||
All frames trace via `EmLogTrace`/`sim_trace_push` automatically.
|
||
|
||
## Stateful command handling — five representations
|
||
|
||
1. **In-table group state** (no host, wire-speed): SECTOR_SELECT active sector,
|
||
SLIX2-style privacy, PWD_AUTH-gated read sets. `activate_groups`/`deactivate_groups`
|
||
masks; two-phase handshakes use a transient PENDING group so a stray frame can't
|
||
false-flip; silent success = `response_len==0`; 4-bit ACK/NAK = `RESP_4BIT`.
|
||
2. **Firmware sector projection** (the graft): `sim_active_sector()` derives the active
|
||
sector *read-only* from `active_groups` — single source of truth, native reads O(1).
|
||
3. **EML-backed mutable state** (no host): live WRITE persists into EM BigBuf; later
|
||
native reads reflect it.
|
||
4. **Native-auth state** (no host): UL-C/UL-AES/MFC via existing firmware branches with
|
||
key in EML; `auth_state` word + firmware nonce chain carry session state. First cut
|
||
fixed-nonce.
|
||
5. **Relay state** (host, L4 only): dynamic ISO-DEP crypto; Python owns per-session
|
||
nonce/IV/auth in `handle_frame`. A challenge-prefix entry may be `SIM_FLAG_CONSUME`
|
||
(one-shot) to hand the encrypted tail to the relay.
|
||
|
||
**Session reset:** on REQA/WUPA/HALT firmware resets `active_groups` + `iso_dep_active`
|
||
and (for L4 relay tags) emits `CMD_SIM_RELAY_EVENT{RESELECT}` so Python calls
|
||
`tag.power_on()`, keeping the C and Python state machines in lockstep across a reader
|
||
give-up → re-anticollision.
|
||
|
||
## Test plan
|
||
|
||
- **UNIT (pm3py):** `TableEntry.serialize()` round-trips `RESP_4BIT`/group fields at exact
|
||
byte offsets (99, 107-118), `sizeof==120`.
|
||
- **UNIT:** `compile_ntag_i2c` emits exactly the E1-E5 machine (correct match/groups/masks/
|
||
flags); assert **no** per-page sector reads.
|
||
- **UNIT:** `build_eml_image()` lays out `[prefix][sector0 zeroed-invalid][sector1]
|
||
[sector3]` at 0x400 stride with per-sector PWD/PACK masking (no secret leakage).
|
||
- **UNIT:** `protocol.Cmd` drift fixed (`SIM_TABLE_* == 0x0950/1/2`, relay ids); sim_session
|
||
constants match; FLAG_SIM_TRACE bit confirmed against firmware.
|
||
- **UNIT:** `_relay_loop` parses `CMD_SIM_RELAY_EVENT{L4}`, calls `handle_frame`, replies
|
||
with correct `resp_flags`; RESELECT triggers `power_on`.
|
||
- **FIRMWARE UNIT (host build of `sim_table.c`):** group gating + activate/deactivate
|
||
transitions E1→E3; `response_len==0` path; `RESP_4BIT` path; `sim_active_sector()` maps
|
||
bits → `{0,1,3}`.
|
||
- **FIRMWARE UNIT:** worst-case `sim_table_lookup` latency on the 48MHz ARM for the ~30-entry
|
||
table vs 86µs FDT — **measure, do not hand-wave** (must-pass gate).
|
||
- **HARDWARE L3:** PM3 Easy sims NT3H2211; reader issues `C2 FF` + `01 00 00 00` +
|
||
FAST_READ; live-trace confirms 4-bit ACK, >1ms silence, sector-1 data, invalid NAK; and
|
||
that a table hit fully suppresses the fake-`90 00` path.
|
||
- **HARDWARE L3:** verify passive-ACK phase-2 emits **nothing** (no spurious frame the
|
||
reader misreads as NAK).
|
||
- **HARDWARE L4 static:** Type-4 NDEF SELECT-App/File/ReadBinary from the table, overriding
|
||
fake `90 00`; TagInfo reads NDEF.
|
||
- **HARDWARE L4 relay:** DESFire GET_VERSION (`0xAF` chaining) and a >FSD NDEF read via WTX
|
||
relay; assert S(WTX) echo, block-number toggle, chained reassembly both directions.
|
||
- **REGRESSION:** 15693 sim still works after the `sim_table_execute` signature change; full
|
||
pytest suite green.
|
||
|
||
## Sequencing
|
||
|
||
**L3 track (ship first, independently testable):**
|
||
1. FW-1 (`sim_table` signature + `RESP_4BIT`) → FW-2 (`iso15693.c` call site, keeps 15693 green).
|
||
2. FW-3 (commit A: include, statics, buffer bump, host-poll upload).
|
||
3. FW-4 (commit B: L3 table-check + sector-offset graft).
|
||
4. PY-1 (id fix) → PY-2 (`compile_ntag_i2c`) → PY-3 (`build_eml_image` + entries) → PY-4 (`start_14a` upload + dispatch).
|
||
5. **Test L3 on hardware.** Commit + bump firmware gitlink.
|
||
|
||
**L4 track (independent, builds on the L3 host-poll hook):**
|
||
6. FW-5 (`pm3_cmd.h` relay ids).
|
||
7. FW-6 (commit C: `iso_dep_active` + static-APDU serve overriding fake `90 00`).
|
||
8. FW-7 (commit D: `EmSendWtx` + WTX relay; commit E: block-number toggle + chaining).
|
||
9. PY-5 (async `start_14a` relay path + `_relay_loop`).
|
||
10. **Test L4 on hardware** (DESFire chaining, NDEF>FSD).
|
||
|
||
## Risks & mitigations
|
||
|
||
| Risk | Mitigation |
|
||
|------|------------|
|
||
| **FDT scan blowout** — linear `sim_table_lookup` approaches 86µs on a big table. | Sector-offset graft keeps the 14a table tiny (~6-30 entries); cross-sector reads are native (O(1)). Firmware-host unit test **measures** worst-case latency vs 86µs before shipping. Contingency: first-byte bucket index in `sim_table.c` (benefits 15693 too). |
|
||
| **Dispatch order** — native READ/FAST_READ fire before `:2493` and would serve flat sector-0 data. | Resolved by design: cross-sector reads are **not** routed through the table; native branches are made sector-aware via one `emlGet` offset. Native-first is now correct. |
|
||
| **Passive-ACK phase-2** emits a spurious frame the reader reads as NAK. | `sim_table_execute` sets `*resp_len=0`; hook sets `p_response=NULL` and jumps past send; the `if(response_n>0)` build guard yields silence. Hardware test asserts >1ms silence. |
|
||
| **Command-id drift** — `protocol.py` at stale `0x0900` series mismatches framing. | PY-1 fixes it as the first Python commit; unit test asserts the values. |
|
||
| **UL-C/UL-AES miscategorized** as L4. | Routed to existing native firmware auth branches with model key via EML; documented fixed-nonce fidelity gap. Not claimed under table/relay. |
|
||
| **L4 relay claims DESFire/NDEF** but chaining/block-number don't exist in FW. | Chaining is an explicit gating commit (FW-7 E) before DESFire is claimed; buffer bumped to 256 first; v1 gated to single-frame + `0xAF`. |
|
||
| **`iso14443a.c` is a hot upstream file** — hooks churn on rebase. | Atomic single-file commits anchored to stable symbols; heavy logic in self-contained blocks; `sim_table`/`sim_crypto` fork-local; only one 15693 call site changes. |
|
||
| **CONSUME one-shots** not reset until re-UPLOAD. | Session-reset emits `RESELECT`; where a CONSUME challenge must re-arm, Python re-uploads/re-adds; documented constraint. |
|
||
|
||
## Open decisions
|
||
|
||
1. **`FLAG_SIM_TRACE` bit:** `sim_session.py` uses `0x2000`, the 14a live-trace plan doc
|
||
says `0x4000`; confirm the firmware's actual bit before wiring the async relay.
|
||
*(Note: reconciled this session — firmware F1 landed `FLAG_SIM_TRACE 0x2000`; the doc's
|
||
`0x4000` is stale. Verify against `pm3_cmd.h`.)*
|
||
2. **Multi-sector EML layout:** confirm the EM BigBuf `CARD_MEMORY` fits sector0+1+3
|
||
(~2KB + config) and that `CMD_HF_MIFARE_EML_MEMSET` writes at the offset the sector graft
|
||
expects; else add a dedicated `CMD_HF_ISO14443A_EML_SETMEM` (fallback id `0x033A`).
|
||
3. **NTAG I2C no-rollover fidelity:** native READ may roll over at emulator-memory end, not
|
||
sector end. Decide pre-zeroing vs an explicit sector-length clamp in the READ branch.
|
||
4. **NTAG21x first-READ NFC counter bump** is a side effect the native/static path loses.
|
||
Accept as documented deviation, or force that first READ through a CONSUME entry (same
|
||
for EV1 INCR_CNT edges).
|
||
5. **UL-C 3DES fixed nonce vs model random `RndB`:** acceptable for sim; decide whether
|
||
firmware 3DES-with-supplied-nonce is wanted or a downscope note suffices.
|
||
6. **L4 chaining scope for v1:** minimum viable = single-frame APDUs + DESFire `0xAF` +
|
||
tag→reader FSD split. Confirm whether reader→tag chaining is needed for any target
|
||
reader in the first cut or can be deferred.
|
||
7. **Empirical reader behaviour:** do target NTAG I2C reader stacks actually issue
|
||
SECTOR_SELECT for sector 1, or only read sector-0 NDEF? If the latter, sector-1 can be
|
||
validated later without blocking the L3 ship.
|
||
8. **Apply hooks to `SimulateIso14443aTagAID` (`:4716`)?** Deferred; `TagEx` is the primary
|
||
`FLAG_SIM_TRACE` path.
|
||
|
||
## Provenance
|
||
|
||
Produced by the `plan-14a-custom-commands` design workflow (13 agents: 6 readers → 3
|
||
designs → 3 adversarial judges → synthesis). Judge outcome: static-table-first and hybrid
|
||
"adopt-with-changes" (reuse 5/5, fdt_safety 3-4); relay-first "reject" (FDT-unsafe at L3).
|
||
The recommended architecture is static-table-first L3 grafted with a firmware sector
|
||
projection to resolve the SECTOR_SELECT/entry-explosion risk, plus a WTX relay for dynamic
|
||
L4 crypto.
|