Commit Graph

25 Commits

Author SHA1 Message Date
michael
62ceef89e9 test(applet): adversarial GCM regression tests (post-M2 security pass)
Pin five negative-space properties of AliroGcm.decrypt that the NIST KAT
suite doesn't exercise. Catches the implementation flaws most likely to
slip into a userland AEAD impl:

- decrypt_tamperedCiphertextByte: single bit flip in ct rejects + out[]
  unchanged (proves tag verify runs BEFORE GCTR-decrypt)
- decrypt_tamperedTagByte: single bit flip in tag rejects + out[]
  unchanged (proves no early-exit / no conditional plaintext leak)
- decrypt_wrongKey: single-bit key difference rejects (key confusion)
- decrypt_wrongIv: single-bit IV difference rejects (replay-with-rebind)
- decrypt_inputShorterThanTag: <16 B input throws ILLEGAL_VALUE
  without touching out[] (early bounds check)

13/13 AliroGcmTest tests pass (was 8 + 5 new). Full suite 152/0/0.
2026-06-17 21:22:46 -07:00
michael
d24d72e119 chore(applet): drop M1 stub comments + dead paths + naming consistency (M2F.2)
Sweep stale M1-era cruft and milestone task markers obsoleted by M2:

- StepUpApplet class Javadoc: fix stale @link target for
  finalizeAccessDocument (now takes verifier+scratch65), drop "TODO"
  prose for shipped M2A.3 verify-at-finalize, rewrite "INS_EXCHANGE stub
  was retired" past-tense note as present-tense ownership statement.
- StepUpApplet: drop "M1B / M1C" / "M2E.1" / "M2E.2" / "M2D.3+M2D.4"
  task markers from comments and method Javadocs; describe current
  behavior, not milestone provenance.
- StepUpApplet: rewrite "decrypt-and-discard" sink Javadoc — that sink
  now also stages the EXCHANGE Reader Status request plaintext.
- StepUpSession: drop "matching M1 behaviour" Javadoc trailers.
- AliroApplet bitmap comment: align with the new method name and drop
  the M1 historical aside that's now adjacent to the only behavior left.
- AliroAppletTest: rewrite "Once M1B.1 / M1C.1 land..." as present tense.
- CredentialStore.markAccessDocumentVerified() → ForTesting suffix to
  match the sibling markAccessDocumentFinalizedForTesting() and add an
  explanatory Javadoc clarifying that production callers reach the
  verified state via finalizeAccessDocument(short, CoseVerifier, byte[]).
  Two test call sites updated.

No behavior change. Tests: 147 / 0 / 3 pre-existing GCM errors. Net
diff +46/-44 = +2 LOC (the rename's explanatory Javadoc minus the cruft).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 17:01:12 -07:00
michael
ed316102bf fix(applet): signaling_bitmap reflects actual AD capability (M2F.1)
Replaces the hardcoded 0x0005 in Table 8-11 with a computed value:
- Bit 2 (step-up AID SELECT required) is always set — our architecture
  always uses split AID (5501 expedited + 5502 step-up) so the channel
  is always advertised, even when no AD is present.
- Bit 0 (AD retrievable) is set only when hasAccessDocument() AND
  isAccessDocumentVerified() — we don't advertise a capability we
  can't deliver. The "finalized but not verified" branch is defensive
  (post-M2A.3 finalize requires verify) but matches the contract.

Tests in AliroAppletAuth1Test:
- auth1_bitmap_adNotProvisioned_returns0x0004 (replaces the old
  "all-zero" expectation — bit 2 is now always on)
- auth1_bitmap_adProvisionedButNotVerified_returns0x0004 (new;
  uses markAccessDocumentFinalizedForTesting without
  markAccessDocumentVerified)
- auth1_bitmap_adProvisionedAndVerified_returns0x0005 (the previous
  M2A.3 case, now correctly gated on the verified flag)

Tests run: 147, Failures: 0, Errors: 3 (pre-existing GCM only).
2026-06-17 16:54:45 -07:00
michael
0c7d4c642e feat: EXCHANGE Reader Status request validation + spec-shape response (M2E.1, M2E.2)
M2E.1: parse the EXCHANGE plaintext as Aliro §8.3.3.5 / Table 8-19
[sub_event_id, payload_len, payload], reject SW_WRONG_LENGTH on truncation
or length mismatch and SW_DATA_INVALID on unknown sub_event_id (M2 only
supports 0x01 = ReaderStatusRequest).

M2E.2: emit a Reader Status sub-event RESPONSE (Table 8-20) plaintext
[0x01, 0x00, 0x00] in place in the APDU buffer, GCM-encrypted under
StepUpSKDevice + deviceIv(stepup_device_counter) per §8.3.1.6. Ciphertext+
tag = 19 B, single APDU, no chaining.

Harness verify_step_up_m2 + the test mock now ship the spec request shape,
decrypt the 19 B EXCHANGE response under deviceCounter=1, and decrypt the
subsequent ENVELOPE response under deviceCounter=2 (EXCHANGE consumed 1).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 16:50:47 -07:00
michael
9879668472 feat(applet): real DeviceResponse via ENVELOPE + GET RESPONSE chaining (M2D.3, M2D.4)
The ENVELOPE handler now consumes the decrypted DeviceRequest through
DeviceRequestParser.validate (M2D.1), builds a canonical-CBOR
DeviceResponse around the cached Access Document via
DeviceResponseBuilder.build (M2D.2), and AES-256-GCM-encrypts the
result under StepUpSKDevice. For our standard 272 B AD the encrypted
output is 388 B (372 B body + 16 B GCM tag), so we ship the first
252 B inline with SW=61xx and let the reader pull the remaining 136 B
via GET RESPONSE (INS=0xC0). The chaining offset/remaining state is
held in a CLEAR_ON_DESELECT short pair; a fresh ENVELOPE resets both
slots, and GET RESPONSE outside an in-flight chain returns
SW_CONDITIONS_NOT_SATISFIED.

Tests:
- envelopeAfterStepUpSelectReturnsRealDeviceResponseViaChaining drives
  AUTH0/AUTH1, sends a valid CBOR DeviceRequest under the StepUpSKReader
  GCM, drains GET RESPONSE until SW=9000, and asserts the decrypted
  plaintext matches the M2D.2 canonical-CBOR DeviceResponse byte-for-byte.
- envelopeWithMalformedDeviceRequestReturnsDataInvalid pins the
  SW_DATA_INVALID propagation path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 16:34:45 -07:00
michael
9e31da0cdc feat(applet): DeviceResponseBuilder (M2D.2)
Pure builder for the mdoc DeviceResponse (ISO 18013-5 / Aliro Table 8-22)
carrying the cached Access Document at
documents[0].issuerSigned.issuerAuth. Single fixed shape — one mDL
document, empty nameSpaces, no deviceSigned. AD bytes embedded verbatim
(not re-encoded), so the issuer's signed bytes pass through unchanged.

Map keys canonical-CBOR sorted (length-then-lex). Headers for the fixed
counts inlined (map(0/2/3), array(1)); tstr headers/values go through
StructuralCbor for the canonical length encoding.

For a 272 B AD the output is 372 B (100 B wrapper). Reference vector
generated from /home/work/aliro-trust/access_document.bin and asserted
byte-for-byte.

M2D.3 will wire this into StepUpApplet.processEnvelope behind GCM
encryption; M2D.4 adds GET RESPONSE chaining.
2026-06-17 16:24:03 -07:00
michael
a7bc7e4478 feat(applet): DeviceRequestParser structural validation (M2D.1) 2026-06-17 16:19:12 -07:00
michael
d5945be60e refactor(applet): AliroGcm.setKeyAndIv consolidates re-keying (M2C.2) 2026-06-17 16:14:53 -07:00
michael
4fb87b6200 refactor(applet): extract StepUpSession from StepUpApplet (M2C.1)
Move the four StepUp session-state fields (StepUpSKDevice, StepUpSKReader,
stepup_reader_counter, stepup_device_counter) and the IV-stamp +
counter-advance math out of StepUpApplet into a dedicated StepUpSession
holder. Adds a unit-testable surface for the spec §8.3.1.6/8/9 IV layout
and §8.4.3 counter-advance behaviour (4 new tests pinning reader/device IV
layout, carry across byte 2, and the 32-bit BE wrap at 0xFFFFFFFF).

Behaviour-preserving: full StepUpAppletTest stays green with no
test-assertion changes (the test-only getters keep their signatures and
just delegate to session.skDevice / session.skReader).

StepUpApplet shrinks 444 -> 394 LOC. Full suite: 138 tests, 0 failures,
3 errors (pre-existing GCM trio).
2026-06-17 16:10:14 -07:00
michael
047503e8c5 feat(applet): canonical CBOR uint/bstr/tstr encoders (M2B.4) 2026-06-17 16:04:05 -07:00
michael
6f96c2f8a2 feat(applet): StructuralCbor.elementSpan (M2B.2) 2026-06-17 16:04:05 -07:00
michael
da4c3fa1e4 feat(applet): StructuralCbor.decodeHeader (M2B.1) 2026-06-17 16:04:05 -07:00
michael
a94e2b498e feat: IssuerAuth verify at AD finalize + issuer pubkey provisioning (M2A.3)
Expands M2A.3 from the original plan because the credential issuer pubkey
had no provisioning path. Adds INS_SET_CREDENTIAL_ISSUER_PUBK = 0x25 to
PersonalizationApplet, bumps CredentialStore FIELD_VERSION 2 -> 3 to add
a 64 B credentialIssuerPubKey slot + set-flag, and pivots
finalizeAccessDocument to run a one-shot CoseVerifier RFC 9052
COSE_Sign1 verify against the staged AD bytes before atomically setting
accessDocumentLen + accessDocumentFinalized + accessDocumentVerified
inside a JCSystem transaction.

PersonalizationApplet holds one CoseVerifier instance constructed at
install (the 768 B CLEAR_ON_DESELECT scratch only allocates once) plus
a 65 B EEPROM scratch for staging the uncompressed issuer pubkey.

Status word mapping:
- missing issuer pubkey at finalize -> SW_CONDITIONS_NOT_SATISFIED (0x6985)
- IssuerAuth signature mismatch     -> SW_DATA_INVALID            (0x6984)
- length out of range               -> SW_WRONG_DATA               (0x6A80)
On verify failure the store stays untouched -- both
accessDocumentFinalized and accessDocumentVerified remain false.

Harness side: TrustArtifacts gains credential_issuer_pub (loaded from
trust_dir/issuer.pem). The orchestrator sends INS 0x25 after the
existing key writes and before the AD chunks so the trust anchor is
staged by the time FINALIZE_AD runs.

Test vector: AD bytes + matching issuer pubkey x||y are hardcoded into
PersonalizationAppletTest from the canonical trustgen artifacts at
/home/work/aliro-trust (272 B AD against a known issuer.pem).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 16:04:05 -07:00
michael
3868678bf9 feat(applet): CoseVerifier RFC 9052 verify (M2A.2)
Adds CoseVerifier.verifyCoseSign1 — single-purpose RFC 9052 COSE_Sign1
verifier for the Aliro IssuerAuth signature, used once at personalization
(M2A.3) to set CredentialStore.accessDocumentVerified.

Wire walk uses hardcoded offsets that assume the
aliro_harness.issuer.cose shape (matches RFC 9052 §3 but is not a generic
CBOR parser). TODO marker for M2B.3 to swap in StructuralCbor.elementSpan.

Extracts SECP256R1_{P,A,B,G,R} into Secp256r1Params so AliroApplet and
CoseVerifier share one copy. seed(KeyPair) handles full keypair seeding;
seedPublic(ECPublicKey) covers the verify-only case for the new verifier.

Test vector: deterministic-seeded P-256 key + literal payload, signed
once via the harness cose helpers and self-verified, hex-pasted into
CoseVerifierTest. Two cases (good signature; flipped payload byte).

Regression: 80 tests green; the 3 pre-existing GCM errors from the
jcardsim m2-volume swap (AliroCryptoTest#jcardsimSupportsAesGcm,
AliroGcmTest#{partialFinalBlock,roundTripAgainstJcardsimAEADCipher})
still error, unrelated to this change.
2026-06-17 16:04:05 -07:00
michael
531b1d3186 feat(applet): accessDocumentVerified flag in CredentialStore (M2A.1)
Persistent boolean flag for the cached IssuerAuth verify result. Field
sits beside accessDocumentFinalized in writeTo/readFrom (FIELD_VERSION
bumped 1->2 -- schema change requires re-personalize). reset() clears.
Test pins the serialize/deserialize round-trip via the existing
RecordingSink/ReplaySource helpers.

Foundation for M2A.2 (CoseVerifier) and M2A.3 (verify-at-finalize wiring).
2026-06-17 16:03:22 -07:00
michael
ebefc4e358 refactor(applet): retire AliroApplet INS_EXCHANGE stub
The stub was added earlier this session as an X-CUBE-ALIRO compat
shim back when we (incorrectly) believed the vendor firmware ignored
the §10.2 step-up AID SELECT requirement and routed EXCHANGE directly
to the Expedited AID (ACCE5501). Today's Path X work proved the
vendor firmware actually DOES the §10.2 SELECT correctly once the
upstream crypto interop is right.

With M1B.1 / M1C.1 now landing the real EXCHANGE and ENVELOPE handlers
on StepUpApplet (ACCE5502), the AliroApplet stub is strictly wrong:
it would let stale expedited_device_counter state confuse the vendor
library if the reader ever hit it. Spec-conformant readers route
EXCHANGE to ACCE5502 after the step-up SELECT and never touch
ACCE5501 with INS=0xC9. So AliroApplet returns SW_INS_NOT_SUPPORTED
(0x6D00) for INS=0xC9 like any other unknown INS, and we ship one
less compat shim.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-11 11:01:57 -07:00
michael
e213c65d26 feat(applet): StepUpApplet INS_ENVELOPE (CLA=0x00) decrypt + encrypted ack
X-CUBE-ALIRO firmware sends ENVELOPE (CLA=0x00 INS=0xC3) carrying the
encrypted mdoc DeviceRequest after the step-up AID SELECT. Spec §8.3.1.9
defines the inbound IV (reader-side: 0x0000000000000000 || stepup_reader_counter).
For Milestone 1 we decrypt-and-discard the DeviceRequest, then return a
spec-shape encrypted empty CBOR map (1 plaintext byte 0xA0, GCM-encrypted
with StepUpSKDevice using device-side IV per §8.3.1.6:
0x0000000000000001 || stepup_device_counter). Total response: 17 bytes
(1 ct + 16 tag).

Adds stepUpDeviceCounter[4] CLEAR_ON_DESELECT alongside the existing
stepUpReaderCounter; both init to [0,0,0,1] when StepUpApplet.select()
derives session keys (i.e. each Step-Up phase entry).

Restructures the StepUpApplet dispatch so CLA=0x00 ENVELOPE coexists
with CLA=0x80 EXCHANGE -- ENVELOPE uses the ISO-standard CLA per
spec/ISO 7816 convention, EXCHANGE remains Aliro-proprietary.

M2 will replace the empty CBOR map with a real mdoc DeviceResponse
carrying the cached-verified Access Document bytes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-11 10:55:09 -07:00
michael
96816bc232 feat(applet): StepUpApplet INS_EXCHANGE decrypt-and-discard with 9000 ack
X-CUBE-ALIRO firmware sends a Reader Status sub-event via EXCHANGE
(CLA=0x80, INS=0xC9) after the step-up AID SELECT, per Aliro §8.3.3.5
Table 8-14. The payload is encrypted with StepUpSKReader using AES-256-GCM
with IV = 0x0000000000000000 || stepup_reader_counter (4B BE) per §8.3.1.8.

For Milestone 1 we decrypt-and-discard: tag verification proves we have
matching session keys, then we return SW=9000 with empty payload. M2
will add a proper encrypted Reader Status response sub-event.

Extends CryptoSingletons to hold the shared AliroGcm instance too --
opt 1 sibling of the AliroCrypto sharing from M1A.2. Adds AliroGcm.decrypt
since the prior pipeline only encrypted (AUTH1 response path).

Counter starts at 1 per session-bound init (spec §8.4.3 -> mdoc [6]
§9.1.1.5), incremented after each successful decrypt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-11 10:45:22 -07:00
michael
8e999b770d feat(applet): StepUpApplet derives session keys on SELECT when armed
After AUTH1 parks StepUpSK in SessionContext, the reader issues SELECT
to the Step-Up AID per spec §10.2. StepUpApplet.select() now picks up
the parked SK, derives StepUpSKDevice / StepUpSKReader via the
§8.4.3 HKDF (already implemented in AliroCrypto.deriveStepUpSessionKeys),
and stages both in transient CLEAR_ON_DESELECT fields for the ENVELOPE
(M1C.1) and EXCHANGE (M1B.1) handlers.

Introduces CryptoSingletons -- a lazy package-private holder for the
single AliroCrypto instance shared between AliroApplet and StepUpApplet.
Saves ~352 B of transient (kdfWorkbuf + hkdfPrevT + expandScratch)
versus a per-applet duplicate. Java Card forbids new in <clinit> so
the singleton uses lazy null-check init. Opt 1 prelude per
docs/plans/2026-06-11-step-up-implementation-v2.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-11 10:33:46 -07:00
michael
cedefef70e test(applet): pin deriveStepUpSessionKeys to spec ASCII info strings
Adds a second regression test for AliroCrypto.deriveStepUpSessionKeys
that hard-codes the exact RFC 5869 reference computation (empty salt,
"SKDevice" / "SKReader" ASCII info) with a different IKM byte pattern
and independent expand-once helper. Complements the existing
deriveStepUpSessionKeysMatchesManualHkdf so a future spec misread in
shared helper code can't slip through.

Implementation itself was landed earlier in f94e416 ("fix: AUTH1 crypto
interop with stock X-CUBE-ALIRO + EXCHANGE compat stub"); this is the
M1A.1 task's tightened pin from the Step-Up v2 plan.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-11 10:23:44 -07:00
michael
f94e416c99 fix: AUTH1 crypto interop with stock X-CUBE-ALIRO + EXCHANGE compat stub
Three independent spec-misreads found via Path X investigation against
the X-CUBE-ALIRO vendor library, all causing
ACWG_Error_Crypto_EncryptDecrypt on the vendor's processAUTH1ResponsePayload.
Each was symmetric between this applet and our PC/SC reader, so
aliro-bench-test passed against our own host-side reader but failed
against any spec-compliant third-party reader. Path X also surfaced
an X-CUBE-ALIRO-specific compat shim (bitmap + EXCHANGE stub) which is
documented to be retired by the Step-Up Milestone 1 work.

1. salt_volatile dropped x(credential_long_term_pub) at the end.
   §8.3.1.13 salt_volatile ends at the 0xA5 proprietary information TLV;
   the credential key belongs in `info` (and even there it's the
   EPHEMERAL one, which buildInfo already does correctly).

2. Kdh now uses X9.63 KDF per §8.3.1.4 instead of HKDF.
   The §8.3.1.4 closing note ("actual key derivation is performed using
   §8.3.1.5") refers to subsequent session-key derivation from Kdh
   (§8.3.1.13 -> §8.3.1.5 HKDF), NOT a substitution for Kdh itself.
   For 32-byte output X9.63 KDF reduces to:
     Kdh = SHA-256(ZAB || 0x00000001 || transaction_identifier)
   Added a native SHA-256 instance to AliroCrypto for this one-shot.

3. salt_volatile flag uses AUTH1's command_parameters, not AUTH0's.
   §8.3.1.13 says "command_parameters || authentication_policy from the
   command data field". When §8.3.1.13 runs (after AUTH1), the active
   request is AUTH1; authentication_policy only exists in AUTH0 so it's
   still pulled from saved AUTH0 state, but command_parameters is the
   AUTH1 value (typically 0x01 = "request credential_PubK in response").

4. signaling_bitmap kept at 0x0005 when AD provisioned + INS_EXCHANGE
   stub on AliroApplet returns 9000 with empty payload. Empirically the
   X-CUBE-ALIRO vendor library errors on bitmap=0x0000 even though the
   spec allows it (separate vendor quirk worth filing); EXCHANGE stub
   exists because the firmware unconditionally sends 0xC9 post-AUTH1
   for the Reader Status sub-event report. Both shims are documented to
   be retired in Step-Up Milestone 1 -- StepUpApplet will handle 0xC9
   on its own AID (ACCE5502) per §10.2.1 after the spec-mandated
   step-up AID SELECT.

PC/SC bench-test still passes: AUTH1=9000, ~3.2 s, bitmap=0x0005.
Nucleo X-CUBE-ALIRO firmware now reports retval=ACWG_OK on
processAUTH1ResponsePayload (confirmed against j3r452 UID
04565E4A0B2190 in /tmp/nucleo-three-fixes.log). The remaining
"DOOR OPERATION FAILED" on Nucleo is downstream Step-Up not being
implemented yet -- StepUpApplet is still the scaffold and returns
6D00/6E00 to ENVELOPE / EXCHANGE. That's Milestone 1 work.

80/80 Java tests + 126/126 Python tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-11 10:18:42 -07:00
michael
276f9f189a test: tighten CredentialStoreSerializationTest against silent reorders
Code review of 40a079a flagged two gaps:

- Round-trip test set all 5 booleans to true, so a reorder among the
  boolean writeTo lines would not break the test. Now uses a distinguishable
  mix (T/T/F/T/F) and asserts each predicate explicitly.
- FIELD_VERSION rejection branch in readFrom had zero coverage. Adds a
  test that forges an out-of-band version byte and asserts
  ISOException(SW_DATA_INVALID).

Reuses the existing hasAccessDocument() predicate (which already returns
accessDocumentFinalized) rather than adding a new test-only helper.

Manually verified the round-trip test catches a silent boolean reorder
by swapping committed/credentialPubKeySet writes in writeTo and observing
isCommitted() assertion fail (reverted before commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 12:37:36 -07:00
michael
40a079a539 feat: define CredentialStore serialization contract
Adds in-tree SerializationSink/Source interfaces matching the shape of
the AMD-H Element API, plus writeTo/readFrom on CredentialStore and a
round-trip test. The AMD-H adapter (added in a later epic) just bridges
Element -> these interfaces, so the field layout is testable today
without depending on a JCOP 4.5 image.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 10:50:47 -07:00
michael
1f54a690d0 refactor: own CredentialStore from PersonalizationApplet instance
Prepares for AMD-H by moving the singleton's anchor off a static field
onto the PersonalizationApplet instance. The static INSTANCE remains
solely as an in-package publish-point so AliroApplet/StepUpApplet keep
working unchanged.

Adds CredentialStore.bootstrap() (called from PersonalizationApplet's
constructor) and CredentialStore.republish() (reserved for the upcoming
onRestore hook). Drops the lazy-init path in get() — every install path
now goes through PersonalizationApplet first, so INSTANCE is always set
by the time AliroApplet looks it up. Test setUps that previously called
CredentialStore.get().resetForTesting() before the applet was installed
are reordered or dropped: each test now gets a fresh store via the
constructor's bootstrap() call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 20:20:22 -07:00
782074f6ae 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>
2026-05-02 10:17:46 -07:00