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>
22 KiB
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 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:
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
sudo apt install stlink-tools(providesst-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):
Shared code (DO NOT MODIFY — inherited as-is by new project):
reader/STM32CubeExpansion_ALIRO_V1_0_0/Drivers/BSP/Components/st25r200/— chip driverreader/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 middlewarereader/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 NFC09A1reader/STM32CubeExpansion_ALIRO_V1_0_0/Projects/nucleo-u5/Applications/Aliro/nfc10-only/— project root cloned fromnfc-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
#defineblock 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
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
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:
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
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
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 toNFC10A1_*(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 forARDUINO_D10_PIN,ARDUINO_A0_PIN, etc. If the header uses macros likeARDUINO_Dx_Pin/ARDUINO_Ax_Pin, reference those directly (avoids hardcoding PAx literals that could drift). -
Add reset pin definitions (NFC10A1-specific, absent from NFC09A1):
#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
st25r200driver if the driver has a reset hook (checkst25r200.cfor a function likest25r200_Reset()or similar; the NFC12 template usesBUS_SPI1_RST_GPIO_PINper the explorer's diff). -
Include path update:
#include "rfal_defConfig.h"stays the same (shared); the board-specific bits should now referencenfc10a1.h(notnfc09a1.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-onlyornfc_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
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
cmake --build build 2>&1 | tee build.log
Expected first-pass errors (iterate):
- Undefined reference to
BSP_NFC10A1_Initor similar → Task 1 rename incomplete; grep for staleNFC09A1in the new shim. nfc09a1.h: No such file→ stale include somewhere in nfc10-only; replace withnfc10a1.h.st25r200.h: No such fileshould 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'sstm32u5xx_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 aHAL_GPIO_WritePinpulse innfc10a1.c::BSP_NFC10A1_Init().
Fix one class of error at a time.
Step 3: Confirm artifacts produced
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 anemit-headerCLI subcommand if missing) - Modify:
nfc10-only/cmake/stm32cubemx/CMakeLists.txt(add custom_command)
Step 1: Find the hardcoded trust
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
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):
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:
// 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:
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
# 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
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/ttyACM0for VCP + ST-LINK DFU) - X-NUCLEO-NFC10A1 plugged onto Arduino header (ensure correct orientation — D0 pin aligned)
Step 2: Flash
# 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
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:
- Chip-ID read returns 0xFF → SPI isn't talking (check CS pin resolves to the right MCU GPIO; probe with logic analyzer if available)
- Reset pin held low → the reset wiring added in Task 2 isn't pulsed correctly
- 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
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=6A82on SELECT — AID mismatch. Vendor firmware's SELECT target must beA0000009 09 ACCE 55 01.SW=6F00on 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
--debugoutput. - Signature verify fail after decrypt — firmware's
issuer_pub_key/credential_pub_keyextern 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: 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
#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:
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).
Step 3: LED helpers
In nfc10-only/Core/Src/main.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 — add
--debugflag 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).
aliro-bench-test --trust-dir ~/aliro-trust --debug 2> /tmp/bench-trace.log
Step 2: Nucleo transcript
# Present card DURING this capture:
(timeout 10 cat /dev/ttyACM0) > /tmp/nucleo-trace.log
Step 3: Diff APDU streams
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.elfbuilds cleanly from the vendor tree- Flasher writes and reader boots with NFC field active
aliro-bench-test --trust-dir ~/aliro-truststill 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