Initial snapshot: Aliro applet, harness, Nucleo NFC10A1 reader port

Three components, all bench-validated to varying depths:

- applet/: CSA Aliro v1.0 Java Card applet for J3R180. AUTH0 + AUTH1
  expedited-standard flow end-to-end green via PC/SC bench reader
  (aliro-bench-test). Userland AES-256-GCM and HMAC-SHA-256 layered
  on top of J3R180's primitives because the card lacks both natively.
  P-256 curve params seeded explicitly per J3R180's quirk.

- harness/: Python orchestrator (aliro-trustgen, aliro-personalize,
  aliro-bench-test) for trust-bundle generation, card personalization
  via PersonalizationApplet, and PC/SC AUTH0+AUTH1 transactions. 126
  pytest cases passing.

- reader/STM32CubeExpansion_ALIRO_V1_0_0/: ST X-CUBE-ALIRO V1.0.0
  with our NFC10A1 port (NUCLEO-U545RE-Q + X-NUCLEO-NFC10A1, ST25R200
  shared with NFC09A1). nfc10-only/ project, NFC10A1 BSP shim,
  ALIRO_TRUST_OVERRIDE include into vendor's provisioning.c, and an
  ALIRO_APDU_TRACE wrapper around demoTransceiveBlocking. Boots,
  detects the J3R180, completes SELECT + AUTH0; AUTH1 currently fails
  with RFAL ERR_PROTO (0xB) — under investigation, see
  docs/plans/2026-04-20-nucleo-nfc10a1-port.md and bench-notes/.

Excluded: x-cube-aliro.zip vendor archive, harness/.venv, build dirs,
generated aliro_trust.h (contains private reader scalar), all PEMs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-02 10:17:46 -07:00
commit 782074f6ae
8786 changed files with 2902373 additions and 0 deletions

View File

@@ -0,0 +1,513 @@
# Bring up X-NUCLEO-NFC10A1 as an Aliro Reader on NUCLEO-U545RE-Q Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Produce a second independent end-to-end validation of the Aliro applet by running ST's X-CUBE-ALIRO reader firmware on NUCLEO-U545RE-Q + X-NUCLEO-NFC10A1, hitting AUTH0 + AUTH1 green against our personalized J3R180. The PC/SC bench reader (`aliro-bench-test`) already proves the applet — this adds an MCU-driven reader path.
**Architecture:** X-NUCLEO-NFC10A1 and X-NUCLEO-NFC09A1 both carry the **same** ST25R200 NFC reader IC (ST UM3414 covers both boards). X-CUBE-ALIRO already ships a working NFC09A1 project at `Projects/nucleo-u5/Applications/Aliro/nfc-only/` that uses `Drivers/BSP/Components/st25r200/` — the chip driver is reusable verbatim. The port is board-level only: clone the `nfc-only/` project, remap pin/LED assignments from NFC09A1's Arduino header layout to NFC10A1's (plus add an explicit reset pin that NFC10A1 exposes), wire `aliro-trustgen`'s generated header into the build so the reader's long-term keys match what the J3R180 was personalized with, and add instrumentation. **No new chip driver fetch is required** (an earlier draft of this plan wrongly claimed NFC10A1 was ST25R3916 and would need X-CUBE-NFC7/NFC6 ported in — memory [j3r180_ec_curve_seeding.md](.claude/projects/-home-work-VSCodeProjects-aliro-project/memory/x_cube_aliro_supported_boards.md) now captures the correct chip pairings).
**Tech Stack:** NUCLEO-U545RE-Q (STM32U545RE MCU), X-NUCLEO-NFC10A1 (ST25R200 reader IC), CMake + `arm-none-eabi-gcc` 13.2+, STM32CubeProgrammer (or `st-flash`/OpenOCD) for flashing, ST-LINK onboard the Nucleo, RFAL + ST25R200 driver (both already in the X-CUBE-ALIRO vendor tree).
---
## Pre-flight toolchain gaps
Before Task 4 (build) the host needs:
```bash
sudo apt install ninja-build
```
Before Task 5 (flash) the host needs one of:
- **STM32CubeProgrammer** (preferred — ST-LINK out of the box): [st.com/en/development-tools/stm32cubeprog.html](https://www.st.com/en/development-tools/stm32cubeprog.html)
- `sudo apt install stlink-tools` (provides `st-flash`)
- `sudo apt install openocd`
`arm-none-eabi-gcc` 13.2.1, `cmake` 3.28 are already present (verified).
---
## Critical files and locations
**Template project (DO NOT MODIFY — clone from here):**
- [reader/STM32CubeExpansion_ALIRO_V1_0_0/Projects/nucleo-u5/Applications/Aliro/nfc-only/](reader/STM32CubeExpansion_ALIRO_V1_0_0/Projects/nucleo-u5/Applications/Aliro/nfc-only/)
**Shared code (DO NOT MODIFY — inherited as-is by new project):**
- `reader/STM32CubeExpansion_ALIRO_V1_0_0/Drivers/BSP/Components/st25r200/` — chip driver
- `reader/STM32CubeExpansion_ALIRO_V1_0_0/Drivers/BSP/NFC09A1/` — BSP shim (the current project uses NFC09A1's macros; we'll add an NFC10A1 one)
- `reader/STM32CubeExpansion_ALIRO_V1_0_0/Middlewares/ST/rfal/` — RFAL middleware
- `reader/STM32CubeExpansion_ALIRO_V1_0_0/Projects/Common/Aliro/` — Aliro protocol layer (where trust is injected in Task 4)
**New dirs we will create:**
- `reader/STM32CubeExpansion_ALIRO_V1_0_0/Drivers/BSP/NFC10A1/` — BSP shim copied + renamed from NFC09A1
- `reader/STM32CubeExpansion_ALIRO_V1_0_0/Projects/nucleo-u5/Applications/Aliro/nfc10-only/` — project root cloned from `nfc-only/`
**NFC10A1 pin-map reference (stm32duino):**
- `/tmp/x-nucleo-nfc10a1-ref/examples/X_NUCLEO_NFC10A1_HelloWorld/X_NUCLEO_NFC10A1_HelloWorld.ino` (already cloned this session)
- The `#define` block in that file is the authoritative Arduino-header pin map for NFC10A1.
**Trust bundle:** `~/aliro-trust/` (from `aliro-trustgen init`, same bundle the J3R180 was personalized with).
**Harness-side comparison point:** `aliro-bench-test --trust-dir ~/aliro-trust``RESULT: OK` is the known-good reference.
---
## Task 1: Clone the BSP shim for NFC10A1
**Files:**
- Create: `reader/STM32CubeExpansion_ALIRO_V1_0_0/Drivers/BSP/NFC10A1/nfc10a1.h`
- Create: `reader/STM32CubeExpansion_ALIRO_V1_0_0/Drivers/BSP/NFC10A1/nfc10a1.c`
- Reference (read-only): `reader/STM32CubeExpansion_ALIRO_V1_0_0/Drivers/BSP/NFC09A1/nfc09a1.{h,c}`
**Step 1: Copy the NFC09A1 shim**
```bash
cd reader/STM32CubeExpansion_ALIRO_V1_0_0/Drivers/BSP
cp -r NFC09A1 NFC10A1
cd NFC10A1
mv nfc09a1.h nfc10a1.h
mv nfc09a1.c nfc10a1.c
```
**Step 2: Rename identifiers inside the files**
```bash
sed -i 's/NFC09A1/NFC10A1/g; s/nfc09a1/nfc10a1/g' nfc10a1.h nfc10a1.c
```
The chip driver include (`st25r200.h`) must STAY unchanged — same chip. Grep to confirm:
```bash
grep -c st25r200 nfc10a1.{h,c} # should be ≥1 in each file
grep -c st25r500 nfc10a1.{h,c} # must be 0
```
**Step 3: Verify BSP API surface is unchanged**
The top-level `Projects/Common/Aliro/Src/app_nfc.c` calls e.g. `BSP_NFC0XCOMM_SendRecv(...)` — those symbol names are generic (NFC0X prefix) and don't change between NFC09A1 and NFC10A1. Confirm by grepping `nfc10a1.c` for `BSP_NFC0XCOMM_` — symbols must match the NFC09A1 original.
**Step 4: Commit (if reader/ is under git; currently the project isn't a git repo — create a note file instead)**
Since [/home/work/VSCodeProjects/aliro_project](.) isn't a git repository, track changes in a plain ledger file at `reader/bench-notes/nfc10a1-port-log.md`: append a line noting Task 1 completion with a timestamp.
---
## Task 2: Clone `nfc-only/` → `nfc10-only/` project
**Files:**
- Create: `reader/STM32CubeExpansion_ALIRO_V1_0_0/Projects/nucleo-u5/Applications/Aliro/nfc10-only/` (whole subtree)
**Step 1: Recursive copy**
```bash
cd reader/STM32CubeExpansion_ALIRO_V1_0_0/Projects/nucleo-u5/Applications/Aliro
cp -r nfc-only nfc10-only
```
**Step 2: Rename the per-board Target directory**
```bash
cd nfc10-only
mv X-CUBE-NFC9 X-CUBE-NFC10
```
**Step 3: Update nfc_conf.h for NFC10A1 layout**
Edit `nfc10-only/X-CUBE-NFC10/Target/nfc_conf.h`:
- Change include guard: `__NFC09A1_CONF_H__``__NFC10A1_CONF_H__`
- Rename every `NFC09A1_*` macro to `NFC10A1_*` (LED1-LED6 RCC clock enables + pin definitions)
- **Pin remap per NFC10A1 Arduino header:**
| Function | NFC09A1 (current) | NFC10A1 (new) | Nucleo-U545RE-Q pin |
|----------|-------------------|---------------|---------------------|
| SPI CS (`ST25R_SS`) | PC9 (line 87-88) | D10 → PB6 (verify against `stm32u5xx_nucleo_bus.h`) | **update** |
| SPI IRQ (`ST25R_INT`) | PA0 (line 89-90) | A0 → PA0 (same) | unchanged |
| **Reset** (NEW — NFC10A1 exposes) | — | D8 → PA9 (typical U5 mapping; verify) | **add new** |
| LED A | PB6 | A2 → PA4 (verify) | **update** |
| LED B | PB0 | A3 → PB0 (may be same) | **verify** |
| LED F | PB10 | D5 → PB4 (verify) | **update** |
| LED V | PA8 | D7 → PA8 (may be same) | **verify** |
| LED AP2P | PB10 | D6 → PB10 (may be same) | **verify** |
| LED FIELD | PA1 | A1 → PA1 (may be same) | **verify** |
**Authoritative source for Arduino→MCU mapping:** `reader/STM32CubeExpansion_ALIRO_V1_0_0/Drivers/BSP/NUCLEO-U545RE-Q/stm32u5xx_nucleo_bus.h` — open it and grep for `ARDUINO_D10_PIN`, `ARDUINO_A0_PIN`, etc. If the header uses macros like `ARDUINO_Dx_Pin` / `ARDUINO_Ax_Pin`, reference those directly (avoids hardcoding PAx literals that could drift).
- Add reset pin definitions (NFC10A1-specific, absent from NFC09A1):
```c
#define NFC10A1_RESET_PIN GPIO_PIN_9 /* or whatever D8 resolves to */
#define NFC10A1_RESET_PIN_PORT GPIOA
#define NFC10A1_RESET_PIN_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE()
```
Plumb the reset line through to the `st25r200` driver if the driver has a reset hook (check `st25r200.c` for a function like `st25r200_Reset()` or similar; the NFC12 template uses `BUS_SPI1_RST_GPIO_PIN` per the explorer's diff).
- Include path update: `#include "rfal_defConfig.h"` stays the same (shared); the board-specific bits should now reference `nfc10a1.h` (not `nfc09a1.h`) if included.
**Step 4: Update rfal_platform.h**
Open `nfc10-only/X-CUBE-NFC10/Target/rfal_platform.h`. The platform macros (`platformSpiSelect`, `platformSpiTxRx`, `platformProtectST25RComm`) bind to `ST25R_SS_PIN/PORT` etc. — those come from nfc_conf.h so if the pins there are right, this file just needs the header guard rename (and any `NFC09A1`/`NFC9` references → `NFC10A1`/`NFC10`).
**Step 5: Update CMake**
`nfc10-only/cmake/stm32cubemx/CMakeLists.txt`:
- Change project/target name: any `nfc-only` or `nfc_only` → `nfc10-only` / `nfc10_only`
- Swap include dir: `Drivers/BSP/NFC09A1` → `Drivers/BSP/NFC10A1`
- Update Target include: `X-CUBE-NFC9/Target` → `X-CUBE-NFC10/Target`
The `ST25R200` CMake define stays — same chip.
`nfc10-only/CMakeLists.txt` (top-level): update project name.
**Step 6: Update main.c init**
In `nfc10-only/Core/Src/main.c`, search for `NFC09A1_` or `BSP_NFC09A1_` and replace with `NFC10A1_` / `BSP_NFC10A1_` equivalents. The `BSP_NFC0XCOMM_*` generic names stay.
**Step 7: Append to the port-log ledger**
```
reader/bench-notes/nfc10a1-port-log.md
```
---
## Task 3: First build — iterate on errors
**Files:** no new files; fix whatever the build surfaces.
**Step 1: Configure**
```bash
cd reader/STM32CubeExpansion_ALIRO_V1_0_0/Projects/nucleo-u5/Applications/Aliro/nfc10-only
cmake -B build -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=cmake/stm32cubemx/gcc-arm-none-eabi.cmake \
-DCMAKE_BUILD_TYPE=Debug
```
(If ninja wasn't installed, fall back to `-G "Unix Makefiles"` and use `cmake --build build` / `make -j`.)
**Step 2: Build and triage**
```bash
cmake --build build 2>&1 | tee build.log
```
Expected first-pass errors (iterate):
- **Undefined reference to `BSP_NFC10A1_Init`** or similar → Task 1 rename incomplete; grep for stale `NFC09A1` in the new shim.
- **`nfc09a1.h: No such file`** → stale include somewhere in nfc10-only; replace with `nfc10a1.h`.
- **`st25r200.h: No such file`** should NOT appear — if it does, the Components include path was accidentally dropped in Task 2 Step 5.
- **`ARDUINO_Dx_Pin undefined`** → the pin macro doesn't exist on this board's `stm32u5xx_nucleo_bus.h`; fall back to hardcoded PAx literals per NUCLEO-U545RE-Q schematic (UM3414 App A for pinout tables).
- **Reset pin symbol unused** → you defined `NFC10A1_RESET_*` but didn't wire it into init. Either remove the def if the driver doesn't need it, or add a `HAL_GPIO_WritePin` pulse in `nfc10a1.c::BSP_NFC10A1_Init()`.
Fix one class of error at a time.
**Step 3: Confirm artifacts produced**
```bash
ls -la build/nfc10-only.elf build/nfc10-only.bin
```
Expected: both exist, `.bin` ~100-200 KB.
**Step 4: Record build success**
Append to `reader/bench-notes/nfc10a1-port-log.md`.
---
## Task 4: Wire `aliro-trustgen` output into the firmware build
**Files:**
- Possibly modify: `reader/STM32CubeExpansion_ALIRO_V1_0_0/Projects/Common/Aliro/Src/provisioning.c` (or equivalent — find the hardcoded trust first)
- Create: `reader/STM32CubeExpansion_ALIRO_V1_0_0/Projects/nucleo-u5/Applications/Aliro/nfc10-only/Core/Inc/aliro_trust.h` (gitignored, generated)
- Possibly modify: `harness/src/aliro_harness/trustgen/header.py` (to add an `emit-header` CLI subcommand if missing)
- Modify: `nfc10-only/cmake/stm32cubemx/CMakeLists.txt` (add custom_command)
**Step 1: Find the hardcoded trust**
```bash
grep -rln "reader_priv\|reader_pub\|issuer_pub\|credential_issuer" \
reader/STM32CubeExpansion_ALIRO_V1_0_0/Projects/Common/Aliro/
```
Expected: one or two files in `Src/` with `static const uint8_t` byte arrays. Note extern names — the generated header must match.
**Step 2: Verify `aliro-trustgen` can emit the expected header**
```bash
aliro-trustgen --help
aliro-trustgen init --out-dir /tmp/trust-test
ls /tmp/trust-test/
```
If there's already an `emit-header` subcommand (or `init` writes `aliro_trust.h`), use it. If not, add a one-liner subcommand that reads `reader.pem` + `access_credential.pem` + `reader_group_id.bin` + `reader_group_sub_id.bin` and writes a C header matching the vendor's extern names.
A bare-minimum template at `harness/src/aliro_harness/trustgen/header.py::emit_c_header(trust_dir, out_path)`:
```python
def emit_c_header(trust_dir: Path, out_path: Path) -> None:
rp = load_priv(trust_dir / "reader.pem") # 32 bytes
rP = load_pubk(trust_dir / "reader.pem") # 64 bytes x||y
iP = load_pubk(trust_dir / "access_credential.pem") # 64 bytes x||y
gid = (trust_dir / "reader_group_id.bin").read_bytes() # 16 bytes
sid = (trust_dir / "reader_group_sub_id.bin").read_bytes() # 16 bytes
with out_path.open("w") as f:
f.write("/* Auto-generated by aliro-trustgen — do not edit */\n")
f.write("#pragma once\n#include <stdint.h>\n\n")
f.write(f"static const uint8_t reader_priv_key[32] = {{ {fmt(rp)} }};\n")
f.write(f"static const uint8_t reader_pub_key[64] = {{ {fmt(rP)} }};\n")
f.write(f"static const uint8_t issuer_pub_key[64] = {{ {fmt(iP)} }};\n")
f.write(f"static const uint8_t reader_group_id[16] = {{ {fmt(gid)} }};\n")
f.write(f"static const uint8_t reader_group_sub_id[16] = {{ {fmt(sid)} }};\n")
```
Plus a Click command that calls it. Write tests in `harness/tests/test_trustgen_header.py`.
**Step 3: Replace vendor hardcoded bytes with the generated header**
In the file found in Step 1:
```c
// was: static const uint8_t reader_priv_key[32] = { 0xAA, 0xBB, ... };
#include "aliro_trust.h"
```
(If vendor declared its trust bytes as `static const` and consumed them locally, the header defining the same names with `static const` linkage is a drop-in. If vendor declared them `extern`, swap the header to match.)
**Step 4: CMake custom_command to regenerate on trust change**
In `nfc10-only/cmake/stm32cubemx/CMakeLists.txt`:
```cmake
set(ALIRO_TRUST_DIR "$ENV{HOME}/aliro-trust" CACHE PATH "Trust bundle directory")
set(ALIRO_TRUST_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/../../Core/Inc/aliro_trust.h")
add_custom_command(
OUTPUT "${ALIRO_TRUST_HEADER}"
COMMAND aliro-trustgen emit-header --trust-dir "${ALIRO_TRUST_DIR}" --out "${ALIRO_TRUST_HEADER}"
DEPENDS "${ALIRO_TRUST_DIR}/reader.pem" "${ALIRO_TRUST_DIR}/access_credential.pem"
COMMENT "Regenerating aliro_trust.h from ${ALIRO_TRUST_DIR}"
VERBATIM
)
add_custom_target(aliro_trust_header DEPENDS "${ALIRO_TRUST_HEADER}")
add_dependencies(${CMAKE_PROJECT_NAME}.elf aliro_trust_header)
```
**Step 5: Ignore the generated header**
```bash
# Since project isn't git, create a LOCAL .gitignore-equivalent ledger:
echo "Projects/nucleo-u5/Applications/Aliro/nfc10-only/Core/Inc/aliro_trust.h" \
>> reader/STM32CubeExpansion_ALIRO_V1_0_0/.distignore
```
(Documentational — mostly a reminder.)
**Step 6: Rebuild + verify the bytes made it into the ELF**
```bash
rm -rf build && cmake -B build -G Ninja ...
cmake --build build
arm-none-eabi-objdump -s -j .rodata build/nfc10-only.elf | grep -A2 reader_priv
```
Expected: 32 bytes matching the first 32 bytes of the PEM-decoded reader private scalar.
---
## Task 5: Flash + first-boot verification (HARDWARE REQUIRED)
**Files:** none — on-device only.
**Step 1: Connect hardware**
- NUCLEO-U545RE-Q to host USB (enumerates as `/dev/ttyACM0` for VCP + ST-LINK DFU)
- X-NUCLEO-NFC10A1 plugged onto Arduino header (ensure correct orientation — D0 pin aligned)
**Step 2: Flash**
```bash
# With STM32CubeProgrammer:
STM32_Programmer_CLI -c port=SWD -w build/nfc10-only.elf -v -rst
# Or with st-flash:
arm-none-eabi-objcopy -O binary build/nfc10-only.elf build/nfc10-only.bin
st-flash write build/nfc10-only.bin 0x08000000
```
**Step 3: Open UART console**
```bash
minicom -D /dev/ttyACM0 -b 115200
# or: picocom -b 115200 /dev/ttyACM0
```
Expected: within a second of reset, banner text like `Aliro Reader — NFC10A1 / ST25R200` followed by init messages and a polling loop.
**Step 4: Verify NFC field activates**
Within a few seconds of boot the ST25R200 should drive the antenna. NFC10A1 has a FIELD LED — expect it to light solid (or blink during active polling).
**Step 5: If the FIELD LED doesn't light**
Common causes — debug in order:
1. Chip-ID read returns 0xFF → SPI isn't talking (check CS pin resolves to the right MCU GPIO; probe with logic analyzer if available)
2. Reset pin held low → the reset wiring added in Task 2 isn't pulsed correctly
3. Antenna tuning failed → check UART log for specific RFAL init error code
**Step 6: Stop here if field isn't coming up — no point chasing the protocol layer yet.**
---
## Task 6: Empty-field + random-card discovery (HARDWARE REQUIRED)
**Step 1: With no card present** — UART should log periodic `polling, no target` (or equivalent). Every ~500ms.
**Step 2: Present a random NFC-A card (transit, hotel keycard)** — UART should log `target detected: NFC-A, UID=...`. Proves polling + anti-collision.
**Step 3: Present the J3R180** — UART should log target detected and attempt a SELECT of the Aliro AID. Expected either success (proceed to Task 7) or an SW that points at which APDU exchange failed.
**Step 4: Capture UART log to `reader/bench-notes/nfc10a1-discovery.log`.**
---
## Task 7: Full AUTH0+AUTH1 against J3R180 (HARDWARE REQUIRED)
**Step 1: Confirm baseline still green**
```bash
aliro-bench-test --trust-dir ~/aliro-trust
```
Expected: `RESULT: OK`. If not, stop — something regressed in the applet.
**Step 2: Ensure card personalized from same trust bundle**
(No action if card hasn't been re-personalized since Task 4 wired the same bundle into the firmware. If uncertain, re-run `aliro-personalize --trust-dir ~/aliro-trust` on a fresh card.)
**Step 3: Present J3R180 to the Nucleo reader**
Expected UART log sequence:
```
target detected: NFC-A
SELECT Aliro AID → SW=9000
AUTH0 (lc=N) → response N bytes, SW=9000
AUTH1 (lc=N) → response N bytes, SW=9000
AUTH1 decrypt OK
UD signature verify OK
GRANT — signaling_bitmap=0x0005
```
**Step 4: Failure triage**
- **`SW=6A82` on SELECT** — AID mismatch. Vendor firmware's SELECT target must be `A0000009 09 ACCE 55 01`.
- **`SW=6F00` on AUTH0** — uncaught applet exception; correlate with bench-test behavior, should NOT appear if bench-test is green.
- **AUTH1 decrypt fail** — ExpeditedSKDevice mismatch. Either firmware's trust bundle differs from the card's, or Table 8-12 bytes are assembled differently. Compare Table 8-12 hex from firmware UART log vs bench-test `--debug` output.
- **Signature verify fail after decrypt** — firmware's `issuer_pub_key` / `credential_pub_key` extern doesn't match the trust bundle used to personalize the card.
**Step 5: On success**
Capture UART log to `reader/bench-notes/nfc10a1-auth1-green.log`. Update memory [pcsc_bench_reader.md](.claude/projects/-home-work-VSCodeProjects-aliro-project/memory/pcsc_bench_reader.md): append a note that Nucleo-NFC10A1 is now a second validation path.
---
## Task 8: Instrumentation — UART APDU trace + LED state machine
**Files:**
- Create: `reader/STM32CubeExpansion_ALIRO_V1_0_0/Projects/Common/Aliro/Inc/aliro_log.h`
- Modify: `reader/STM32CubeExpansion_ALIRO_V1_0_0/Projects/Common/Aliro/Src/app_nfc.c` — add APDU log points around each transceive
- Modify: `nfc10-only/Core/Src/main.c` — LED helpers
**Step 1: aliro_log.h**
```c
#pragma once
#include <stdio.h>
#include <stdint.h>
#define ALIRO_LOG_INFO(fmt, ...) printf("[INFO] " fmt "\n", ##__VA_ARGS__)
#define ALIRO_LOG_ERROR(fmt, ...) printf("[ERROR] " fmt "\n", ##__VA_ARGS__)
static inline void aliro_log_hex(const char *dir, const uint8_t *buf, uint16_t len) {
printf("[APDU %s] ", dir);
for (uint16_t i = 0; i < len; i++) printf("%02x", buf[i]);
printf("\n");
}
```
**Step 2: Instrument the APDU pump**
In `app_nfc.c` (or wherever ISO-DEP TX/RX happens), wrap:
```c
aliro_log_hex(">", txBuf, txLen);
// ... transceive
aliro_log_hex("<", rxBuf, rxLen);
```
Match the format the bench-test logs emit (see [harness/src/aliro_harness/reader/transaction.py](harness/src/aliro_harness/reader/transaction.py)).
**Step 3: LED helpers**
In `nfc10-only/Core/Src/main.c`:
```c
static void aliro_led_grant(void) { HAL_GPIO_WritePin(PLATFORM_LED_A_PORT, PLATFORM_LED_A_PIN, GPIO_PIN_SET); }
static void aliro_led_deny(void) { HAL_GPIO_WritePin(PLATFORM_LED_B_PORT, PLATFORM_LED_B_PIN, GPIO_PIN_SET); }
static void aliro_led_active(bool on) { HAL_GPIO_WritePin(PLATFORM_LED_FIELD_PORT, PLATFORM_LED_FIELD_PIN, on ? GPIO_PIN_SET : GPIO_PIN_RESET); }
```
Call from the Aliro state machine: `aliro_led_active(true)` at AUTH0 start, grant/deny at AUTH1 finish.
**Step 4: Verify visually**
J3R180 → FIELD LED (A1) lights during transaction, LED A (A2) lights for ~1s on grant, clears.
Random card → LED B (A3) lights on AUTH0 failure.
---
## Task 9: A/B compare — Nucleo vs PC/SC bench reader
**Files:**
- Create: `reader/bench-notes/ab-compare-2026-04-XX.md`
- Possibly modify: [harness/src/aliro_harness/reader/cli.py](harness/src/aliro_harness/reader/cli.py) — add `--debug` flag that dumps APDU TX/RX to stderr
**Step 1: Bench-test transcript with APDU trace**
If `--debug` doesn't exist, add it (one or two lines in the transmit wrapper at cli.py to `click.echo(f"[APDU >] {apdu.hex()}", err=True)` + same for response).
```bash
aliro-bench-test --trust-dir ~/aliro-trust --debug 2> /tmp/bench-trace.log
```
**Step 2: Nucleo transcript**
```bash
# Present card DURING this capture:
(timeout 10 cat /dev/ttyACM0) > /tmp/nucleo-trace.log
```
**Step 3: Diff APDU streams**
```bash
diff <(grep '^\[APDU' /tmp/bench-trace.log) <(grep '^\[APDU' /tmp/nucleo-trace.log)
```
Expected: differences only in randomized fields (reader ephemeral pubkey, transaction_identifier, encrypted payload ciphertext). Fixed fields (SELECT AID, command_parameters, protocol_version, reader_identifier, response TLV tags) match byte-for-byte.
**Step 4: Document findings**
`reader/bench-notes/ab-compare-2026-04-XX.md` captures:
- Same card + same trust bundle → both GRANT
- APDU diff: expected-only deltas
- Any surprise (extra TLV, unexpected field order, SW difference)
---
## Follow-up work NOT in this plan
- Step-up phase on the Nucleo (SELECT STEP_UP + CBOR ENVELOPE — large, separate plan)
- Antenna tuning against DT's implantable form factor (different tuning than external cards)
- CI build: compile the ELF in CI to catch regressions (no probe needed, just compile)
- Structured binary telemetry log format (replace `printf` — nice-to-have, not needed for bench)
---
## Verification checkpoint after all tasks complete
- [ ] `nfc10-only.elf` builds cleanly from the vendor tree
- [ ] Flasher writes and reader boots with NFC field active
- [ ] `aliro-bench-test --trust-dir ~/aliro-trust` still green (no applet regressions)
- [ ] Nucleo reader → J3R180 → GRANT + full APDU log + LED state
- [ ] A/B diff shows only expected randomness
- [ ] Memory entry updated: Nucleo-NFC10A1 is a second validation path alongside PC/SC