Files
aliro-project/docs/plans/2026-06-11-step-up-implementation-v2.md
michael 9189b41e7f docs: Step-Up implementation plans v1+v2 + inline TODOs at four opt sites
Two plans land:

- 2026-06-07-step-up-implementation.md (v1): original 6-phase plan
  written before the Path X session. Captured the four optimizations
  (opt 1: reuse AliroGcm; opt 2: structural CBOR only; opt 3:
  stream-encrypt during ENVELOPE emit; opt 4: cache IssuerAuth verify
  at personalization) and the spec citations for each. Superseded by v2
  but retained for the planning-history record.

- 2026-06-11-step-up-implementation-v2.md: revised after Path X
  resolved the three crypto bugs. Splits into Milestone 1 (~400 LOC,
  4-6 hours, gets DOOR OPERATION SUCCEEDED on stock X-CUBE-ALIRO with
  decrypt-and-discard stubs) and Milestone 2 (~1500 LOC, 1-2 weeks,
  real Access Document retrieval via mdoc DeviceResponse + CBOR + COSE).
  The four optimizations are preserved into M2 where they matter; M1
  ships without them since the demo doesn't need real document
  payload yet.

Inline TODO comments mark the four optimization sites in source:

- CredentialStore.finalizeAccessDocument -- opt 4a (verify-flag in a
  JCSystem.beginTransaction block) + opt 4b (Credential Issuer key
  rotation assumption documented).

- StepUpApplet class javadoc -- all four optimizations laid out so the
  implementer (subagent or human) lands them as they build the body.

(The AliroApplet INS_EXCHANGE comment that references the plans is
in the previous fix commit; it'll move/retire when M1 lands.)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-11 10:19:05 -07:00

22 KiB

Step-Up Phase Implementation Plan (v2 — supersedes 2026-06-07)

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Implement Aliro v1.0 §8.4 Step-Up Phase end-to-end against stock X-CUBE-ALIRO firmware so the j3r452 returns DOOR OPERATION SUCCEEDED on a real NFC reader, then incrementally extend to full Access Document retrieval over the same code path.

Architecture: Step-Up handlers live on StepUpApplet (A000000909ACCE5502). The StepUpSK 32-byte session secret is already derived during AUTH1 by AliroApplet and parked in the shared SessionContext (armStepUp()); StepUpApplet reads it via copyStepUpSK() after the vendor firmware issues the spec-mandated step-up AID SELECT. The same AliroGcm instance gets re-keyed for the Step-Up phase (one shared object across both phases — saves ~368 B transient). CBOR parsing is structural-only (major-type + length boundaries). IssuerAuth COSE_Sign1 verification is cached at personalization. The plan ships in two demoable milestones: Milestone 1 = minimum to make X-CUBE-ALIRO say "DOOR SUCCEEDED" (no real document retrieval, just spec-shaped acks), Milestone 2 = real Access Document retrieval.

Tech Stack: Java Card 3.0.5 (target J3R452), userland AES-256-GCM with 4-bit GHASH (existing), native SHA-256 + ECDSA-P256 verify, structural CBOR codec (RFC 8949 §4.2.1 deterministic encoding for emit), COSE_Sign1 (RFC 9052), mdoc DeviceRequest/DeviceResponse shapes (ISO/IEC 18013-5 §8 with Aliro-remapped integer keys per spec Tables 8-21/8-22).

What today's Path X session locked in (no longer in scope to investigate):

  1. Crypto baseline is interop-correct. Three bugs found and fixed:

    • salt_volatile had an extra 32-byte x(credential_long_term_pub) (commit pending; line 720-723 of AliroApplet.buildSaltVolatile)
    • Kdh derived via HKDF instead of X9.63 KDF per §8.3.1.4 (commit pending; AliroCrypto.deriveKdh)
    • salt_volatile used AUTH0's command_parameters instead of AUTH1's (commit pending; same buildSaltVolatile)

    Vendor library's ACWG_processAUTH1ResponsePayload now returns ACWG_OK on a freshly personalized j3r452 — confirmed in /tmp/nucleo-three-fixes.log against UID 04565E4A0B2190.

  2. X-CUBE-ALIRO firmware DOES honor §10.2 step-up AID SELECT. Once processAUTH1ResponsePayload succeeds and signaling_bitmap bit 2 is set, the firmware correctly issues 00 A4 04 00 09 A0 00 00 09 09 AC CE 55 02 00. The "vendor SHALL violation" I was planning to file with ST was actually downstream of the GCM decrypt failure — one less vendor bug to file. Our existing StepUpApplet.sendStepUpFci already responds 9000 with a minimal FCI.

  3. Vendor firmware sends TWO post-AUTH1 commands on Step-Up:

    • 00 C3 00 00 <len> ENVELOPE — encrypted mdoc DeviceRequest (Table 8-21 keys)
    • 80 C9 00 00 <len> EXCHANGE — encrypted Reader Status sub-event (§8.3.3.5.x)

    Both arrive after the SELECT to ACCE5502. Both need to land on StepUpApplet (not AliroApplet). ENVELOPE's CLA is 0x00 — unusual for a proprietary command but matches the spec table. EXCHANGE's CLA is 0x80.

  4. StepUpSK derivation is wired. AliroApplet.processAuth1 extracts bytes 64..96 of derived_keys_volatile and calls SessionContext.armStepUp(stepUpSK, 0). The consumer side (StepUpApplet) just hasn't been written yet.

Out of scope for this plan:

  • BLE Step-Up (separate transport)
  • mdoc field-level interpretation beyond locating IssuerAuth + extracting nameSpaces (we transport bytes; the reader interprets)
  • Revocation Document handling (same shape as Access Document; just a different blob — covered implicitly by the same emit path)

Reference material:

  • Aliro 1.0 spec §8.3.1.5 (HKDF), §8.3.1.13 (key material), §8.4 (Step-up phase), §10.2 (Transaction), Tables 8-14 / 8-21 / 8-22
  • RFC 8949 (CBOR core deterministic encoding)
  • RFC 9052 (COSE_Sign1)
  • ISO/IEC 18013-5 §8 (mdoc DeviceRequest/DeviceResponse; Aliro Tables 8-21/8-22 remap the integer keys)
  • Existing code: AliroApplet.java, AliroGcm.java, AliroCrypto.java, SessionContext.java, StepUpApplet.java (scaffold with TODOs that name each optimization)
  • Today's interop trace: /tmp/nucleo-three-fixes.log (shows the exact ENVELOPE + EXCHANGE bytes the firmware sends)
  • Original Step-Up plan: docs/plans/2026-06-07-step-up-implementation.md (this plan supersedes it; reuses the four optimization names + spec citations)

Milestone 1 — "DOOR OPERATION SUCCEEDED" on X-CUBE-ALIRO (~400 LOC, 4-6 hours)

Result: The Nucleo prints DOOR OPERATION SUCCEEDED against our card. No real Access Document is served — we acknowledge the ENVELOPE and EXCHANGE with spec-shape empty responses. Enough for fundraising demo, leaves the real protocol body for Milestone 2.

Phase M1A — Wire StepUpSKDevice/StepUpSKReader into StepUpApplet

Per §8.4.3, the Step-Up phase uses fresh GCM session keys derived from StepUpSK. Spec text:

Compute StepUpSKDevice = HKDF(IKM=StepUpSK, salt=∅, info="SKDevice", L=32) Compute StepUpSKReader = HKDF(IKM=StepUpSK, salt=∅, info="SKReader", L=32)

The HKDF helpers already exist on AliroCrypto (Extract/Expand are public-package-visible). The info strings are 8 ASCII bytes each.

Task M1A.1: Add a deriveStepUpSessionKeys helper to AliroCrypto

Files:

  • Modify: applet/src/main/java/com/dangerousthings/aliro/AliroCrypto.java
  • Modify: applet/src/test/java/com/dangerousthings/aliro/AliroCryptoTest.java

Step 1: Write a failing test for the device/reader derivations.

@Test
void deriveStepUpSessionKeysMatchesRfc5869WithSpecInfoStrings() throws Exception {
    AliroCrypto crypto = new AliroCrypto();
    byte[] stepUpSK = new byte[32];
    for (byte i = 0; i < 32; i++) stepUpSK[i] = (byte) (0x10 + i);

    byte[] skDevice = new byte[32];
    byte[] skReader = new byte[32];
    crypto.deriveStepUpSessionKeys(stepUpSK, (short) 0, skDevice, (short) 0, skReader, (short) 0);

    // Reference: RFC 5869 HKDF-SHA-256, salt=∅, info=ASCII("SKDevice")/("SKReader")
    javax.crypto.Mac mac = javax.crypto.Mac.getInstance("HmacSHA256");
    mac.init(new javax.crypto.spec.SecretKeySpec(new byte[32], "HmacSHA256"));  // empty salt
    byte[] prk = mac.doFinal(stepUpSK);
    byte[] expectedDevice = hkdfExpandOnce(prk, "SKDevice".getBytes("ASCII"), 32);
    byte[] expectedReader = hkdfExpandOnce(prk, "SKReader".getBytes("ASCII"), 32);
    assertArrayEquals(expectedDevice, skDevice);
    assertArrayEquals(expectedReader, skReader);
}

(hkdfExpandOnce is a tiny BouncyCastle-free helper at the bottom of the test class.)

Step 2: Run, expect FAIL with "method not defined."

./scripts/dt-mvn.sh test -Dtest=AliroCryptoTest#deriveStepUpSessionKeysMatchesRfc5869WithSpecInfoStrings

Step 3: Implement AliroCrypto.deriveStepUpSessionKeys.

Uses existing hkdfExtract + hkdfExpand. Info strings are 8-byte static byte[] fields. The "empty salt" passes a zero-length range to hkdfExtract, which already falls back to emptySaltZeros.

private static final byte[] STEP_UP_INFO_SK_DEVICE = {
    'S','K','D','e','v','i','c','e'
};
private static final byte[] STEP_UP_INFO_SK_READER = {
    'S','K','R','e','a','d','e','r'
};

short deriveStepUpSessionKeys(
        byte[] stepUpSK, short stepUpSKOff,
        byte[] skDeviceOut, short skDeviceOff,
        byte[] skReaderOut, short skReaderOff) {
    // PRK = HKDF-Extract(salt=∅, IKM=StepUpSK)
    hkdfExtract(
        emptySaltZeros, (short) 0, (short) emptySaltZeros.length,
        stepUpSK, stepUpSKOff, HASH_LEN,
        hkdfPrevT, (short) 0);
    // SKDevice = HKDF-Expand(PRK, info="SKDevice", L=32)
    hkdfExpand(
        hkdfPrevT, (short) 0, HASH_LEN,
        STEP_UP_INFO_SK_DEVICE, (short) 0, (short) STEP_UP_INFO_SK_DEVICE.length,
        HASH_LEN,
        skDeviceOut, skDeviceOff);
    // SKReader = HKDF-Expand(PRK, info="SKReader", L=32)
    hkdfExpand(
        hkdfPrevT, (short) 0, HASH_LEN,
        STEP_UP_INFO_SK_READER, (short) 0, (short) STEP_UP_INFO_SK_READER.length,
        HASH_LEN,
        skReaderOut, skReaderOff);
    return HASH_LEN;
}

Step 4: Run test, expect PASS.

Step 5: Run full suite, expect 81/81 PASS (one new test).

Step 6: Commit.

git add applet/src/main/java/com/dangerousthings/aliro/AliroCrypto.java \
        applet/src/test/java/com/dangerousthings/aliro/AliroCryptoTest.java
git commit -m "feat: AliroCrypto.deriveStepUpSessionKeys per §8.4.3"

Task M1A.2: Have StepUpApplet derive its session keys on demand

Files:

  • Modify: applet/src/main/java/com/dangerousthings/aliro/StepUpApplet.java
  • Modify: applet/src/main/java/com/dangerousthings/aliro/SessionContext.java (expose armStepUpAlreadyArmed() if needed; current isStepUpArmed() works)
  • Test: applet/src/test/java/com/dangerousthings/aliro/StepUpAppletTest.java

Step 1: Write failing test — StepUpApplet, freshly selected after an armed SessionContext, can derive its SKDevice/SKReader.

Re-uses existing test plumbing in AliroAppletAuth1Test.auth1SuccessArmsStepUpContextWithExpectedStepUpSK.

Step 2-5: Add private fields stepUpSKDevice[32], stepUpSKReader[32] to StepUpApplet (transient, CLEAR_ON_DESELECT). On select(): if SessionContext.isStepUpArmed(), copy StepUpSK via copyStepUpSK() then call AliroCrypto.deriveStepUpSessionKeys(...). Re-use the singleton AliroCrypto already used by AliroApplet (move it to a package-private static getter on a CryptoSingletons class so both applets share the same instance — opt 1 prelude).

Step 6: Commit.

git commit -m "feat: StepUpApplet derives SKDevice/SKReader on select when StepUpSK armed"

Phase M1B — Decrypt-and-discard EXCHANGE (INS 0xC9) on StepUpApplet

X-CUBE-ALIRO sends a "Reader Status sub-event" via EXCHANGE after the Step-Up SELECT. From today's trace: 20 bytes of payload (4-byte plaintext after AES-GCM strip-tag-16). Per §8.3.3.5 the response is a Reader Status response sub-event — for our purposes an empty acknowledgment works.

Task M1B.1: Decrypt the inbound EXCHANGE payload with StepUpSKReader

Files:

  • Modify: applet/src/main/java/com/dangerousthings/aliro/StepUpApplet.java
  • Test: applet/src/test/java/com/dangerousthings/aliro/StepUpAppletTest.java

Step 1: Write failing test that drives a fake X-CUBE-ALIRO-shape EXCHANGE.

Set up: AUTH0+AUTH1 via existing test harness. Use ReaderSide to derive StepUpSKReader, encrypt a 4-byte payload with expedited_reader_counter=1 per §8.3.1.8, frame as 80 C9 00 00 <len> <ct||tag> 00, send to StepUpApplet via JCardSim. Assert SW=9000.

Step 2-5: Implement INS_EXCHANGE handler that:

  • Reads encrypted_payload via apdu.setIncomingAndReceive()
  • Builds IV = 0x0000000000000000 || expedited_reader_counter (4B BE) per §8.3.1.8
  • Calls gcm.decrypt(StepUpSKReader, IV, ct, tag, plaintext_scratch)
  • Discards plaintext (we don't care what the Reader Status said — log/ack only)
  • Increments expedited_reader_counter
  • Returns apdu.setOutgoingAndSend((short) 0, (short) 0) — 9000 with no payload

But wait: spec says Reader needs the EXCHANGE response in same secure channel. Vendor may or may not accept empty. Test first whether 9000-empty is enough; if not, encrypt-and-return an empty CBOR map (0xA0). Note apdu.setOutgoingAndSend returns after sending — extend with a one-shot encrypt that produces 17 bytes (1B 0xA0 + 16B tag) if needed.

Step 6: Commit.

git commit -m "feat: StepUpApplet INS_EXCHANGE decrypt/discard/ack (minimal Reader Status)"

Phase M1C — Decrypt-and-discard ENVELOPE (INS 0xC3) on StepUpApplet

X-CUBE-ALIRO sends the mdoc DeviceRequest via ENVELOPE with CLA=0x00, INS=0xC3. From today's trace: 75 bytes of payload (the encrypted DeviceRequest CBOR with expedited_device_counter-derived IV).

Task M1C.1: Accept CLA=0x00 on StepUpApplet.process

Current code rejects anything not CLA_PROPRIETARY (0x80). ENVELOPE is 0x00 C3 ... per spec Table 10-? (see app_aliro.c ENVELOPE_POS for the byte values we just observed).

Step 1-5: Add INS dispatch handling for CLA=0x00 INS=0xC3. Same decrypt-and-discard pattern as EXCHANGE but with the device-side key + the device counter starting from 1 + §8.3.1.6 IV format (0x0000000000000001 || expedited_device_counter). Return empty-encrypted DeviceResponse (0xA0 CBOR empty map encrypted with StepUpSKDevice).

Step 6: Commit.

git commit -m "feat: StepUpApplet INS_ENVELOPE (CLA=00) decrypt/discard, return empty CBOR map ack"

Phase M1D — Retire the AliroApplet.INS_EXCHANGE stub

Now that StepUpApplet handles EXCHANGE on its own AID, the bitmap=0x0005 + AliroApplet EXCHANGE stub from June 7 is obsolete. The vendor firmware will route post-AUTH1 commands to ACCE5502 correctly.

Task M1D.1: Remove the stub

Files:

  • Modify: applet/src/main/java/com/dangerousthings/aliro/AliroApplet.java (delete the case INS_EXCHANGE: block)
  • Modify: applet/src/test/java/com/dangerousthings/aliro/AliroAppletTest.java (assert AliroApplet now returns SW_INS_NOT_SUPPORTED for INS 0xC9)

Step 1-5: TDD as usual. Spec-conformant readers route EXCHANGE to ACCE5502 after step-up SELECT; the stub on AliroApplet was vendor-firmware-bug-specific compat and is now strictly wrong (would let stale expedited_device_counter confuse the vendor).

Step 6: Commit.

git commit -m "refactor: retire AliroApplet INS_EXCHANGE stub now that StepUpApplet handles it"

Phase M1E — End-to-end Nucleo retest

Task M1E.1: Rebuild, reinstall, retest on real hardware

  1. Rebuild j3r452 CAP: ./scripts/dt-mvn.sh -Pj3r452 clean package -DskipTests
  2. Verify the card on PC/SC: aliro-bench-test --trust-dir ~/aliro-trust (AUTH1=9000, ~3.2s)
  3. Move to Nucleo + capture via python3 -c "import serial,time; ..." (pyserial — reliable; bash cat is flaky after USB re-enumeration)
  4. Expected verdict:
NFCA Passive ISO-DEP device found. UID: 04565E4A0B2190
SELECT (ACCE5501) → 9000
AUTH0 → 9000
AUTH1 → 9000
SELECT STEPUP (ACCE5502) → 9000
ENVELOPE (00 C3 ...) → 9000 + encrypted empty CBOR map
EXCHANGE (80 C9 ...) → 9000 + (empty or encrypted-empty)
DOOR OPERATION SUCCEEDED       ← THE MILESTONE
  1. If different failure mode: capture full UART, identify the next vendor error class (retval=ACWG_Error_*), iterate.

Task M1E.2: Update memory + commit milestone

  • Update nucleo_nfc10a1_bringup_state.md with the Milestone 1 success and note: real Access Document retrieval is deferred to Milestone 2.
  • Commit any docs changes.
git commit -m "feat: Step-Up Milestone 1 - DOOR OPERATION SUCCEEDED on X-CUBE-ALIRO"

Milestone 2 — Real Access Document retrieval (~1500 LOC, 1-2 weeks)

Result: Spec-conformant Step-Up phase. The Reader receives our actual provisioned Access Document via the mdoc DeviceResponse over ENVELOPE/GET RESPONSE chaining. IssuerAuth verification is cached at personalization time so per-transaction latency stays at ~3.2 s AUTH1 + ~500ms-1s Step-Up. Bitmap goes back to fully honest about capability.

Phase M2A — Foundation: cache IssuerAuth verification at personalization (opt 4a + 4b)

(Tasks A1-A3 from the original 2026-06-07 plan, mostly unchanged.)

  • Task M2A.1: Add accessDocumentVerified persistent boolean to CredentialStore + getter (~30 lines + test)
  • Task M2A.2: Add CoseVerifier.verifyCoseSign1 (RFC 9052, ECDSA-P256) (~150 lines + test vectors)
  • Task M2A.3: Wire IssuerAuth verification into PersonalizationApplet.finalizeAccessDocument inside JCSystem.beginTransaction() (~50 lines + tests for valid/tampered)

Phase M2B — Structural CBOR codec (opt 2)

(Tasks B1-B4 from the original plan, unchanged.)

  • Task M2B.1: StructuralCbor.decodeHeader — RFC 8949 §3 major-type + argument decoder (~80 lines + 8 tests)
  • Task M2B.2: StructuralCbor.elementSpan — recursive span walker (~50 lines + 5 tests)
  • Task M2B.3: StructuralCbor.locateIssuerAuth — walk mdoc IssuerSigned wrapper (~30 lines + test)
  • Task M2B.4: Canonical CBOR encoder for uint/bstr/tstr only (the primitives we emit) (~50 lines + tests)

Phase M2C — GCM session reuse for Step-Up (opt 1)

  • Task M2C.1: Add AliroGcm.setKeyAndIv rekey method (~30 lines)
  • Task M2C.2: StepUpSession IV builder for both directions (~50 lines)

(Note: deriving keys + decrypting the inbound EXCHANGE and ENVELOPE payloads is already done at Milestone 1; M2C just consolidates the GCM instance to share with AliroApplet.)

Phase M2D — Real ENVELOPE handler with DeviceResponse + chaining (opt 3)

  • Task M2D.1: mdoc DeviceRequest structural parse (don't interpret fields, just confirm shape + extract docRequest namespace IDs) (~80 lines + tests)
  • Task M2D.2: Build DeviceResponse with cached-verified Access Document bytes (~120 lines + tests)
    • Top-level CBOR map per spec Table 8-22
    • Embed cached accessDocument bytes verbatim (already CBOR-encoded by issuer)
  • Task M2D.3: Stream-encrypt during ENVELOPE emit (opt 3) (~80 lines + tests)
  • Task M2D.4: GET RESPONSE chaining when DeviceResponse exceeds APDU size (~60 lines + tests)

Phase M2E — Real EXCHANGE Reader Status response

  • Task M2E.1: Decrypt Reader Status sub-event request (~30 lines + tests)
  • Task M2E.2: Build Reader Status response sub-event (status=OK, no payload) per §8.3.3.5 Table 8-20 (~50 lines)

Phase M2F — Bitmap honesty + cleanup

  • Task M2F.1: Switch bitmap from hardcoded 0x0005 back to a computed value reflecting actual capability (bit 0 set iff Access Document provisioned, bit 2 set per spec since we use split AID) (~5 lines + test)
  • Task M2F.2: Delete the Milestone 1 stub responses, replace with real ones (~50 lines net)
  • Task M2F.3: Strip diagnostic INSes (INS_DIAG_*) — flip DIAGNOSTICS_ENABLED=false, run prod build, verify CAP shrinks ~3 KB (M2F.3 is a quick win — Phase 6 of the older RAM-diet/cleanup todo list)

Phase M2G — Cross-reader interop

  • Task M2G.1: Run end-to-end on stock X-CUBE-ALIRO: DOOR SUCCEEDED + Access Document bytes round-tripped (verified by adding a print hook to the firmware that dumps the received DeviceResponse, comparing to what we provisioned)
  • Task M2G.2: Run end-to-end on aliro-bench-test Python reader (after extending the harness with aliro-bench-stepup CLI that drives ENVELOPE/GET RESPONSE)
  • Task M2G.3: File the WTX-cap bug + one or both remaining vendor issues with ST and/or CSA (see "vendor bug reports" backlog item below)

Phase M2H — Documentation + memory

  • Update MEMORY.md with Milestone 2 completion
  • Mark this plan complete; add step_up_implementation_notes.md memory capturing the four optimizations' final form + spec citations
  • README pass on the Step-Up architecture decisions

Risk register (revised based on today's learnings)

Risk Likelihood Impact Mitigation
j3r452 transient RAM doesn't fit additional Step-Up buffers Low (revised down) High Today's measurements show ~1.7 KB used / 3.1 KB available; Milestone 1 adds ~150 B for SK keys; opt 3 streaming caps Milestone 2 at +400 B max. Comfortable headroom.
CBOR canonical encoding subtleties Low Medium We only emit uint/bstr/tstr — fully unambiguous per RFC 8949 §4.2.1. No floats, no special cases.
Vendor library rejects "empty" Reader Status response Medium Low If 9000-empty doesn't work in Milestone 1, fall back to a 17-byte encrypted-empty-CBOR-map. Adds ~30 lines if needed.
Vendor library rejects "empty" DeviceResponse map Medium Low Same fallback: encrypt 0xA0 (empty map) instead of returning empty payload.
ECDSA-P256 verify on j3r452 quirks Low High We already use it for AUTH0 reader-sig verification (works). No new risk.
Spec ambiguity on Reader Status sub-event response shape Medium Medium Try empty first; if vendor errors, capture the new error class + iterate. Same Path-X-style debugging we just demonstrated end-to-end.

Acceptance criteria

Milestone 1:

  • Stock X-CUBE-ALIRO firmware (with only the documented RFAL_ISODEP_MAX_WTX_RETRYS cap bump) prints DOOR OPERATION SUCCEEDED against our card
  • PC/SC aliro-bench-test still passes (≥ regression check)
  • All existing 80+ unit tests pass + new StepUpApplet tests
  • AliroApplet INS_EXCHANGE stub retired
  • Transient RAM usage on j3r452 remains ≤ 2,200 B CLEAR_ON_DESELECT (≥ 900 B headroom on the 3,120 B pool)

Milestone 2 (additional):

  • Real Access Document bytes round-trip through ENVELOPE/GET RESPONSE
  • All four optimizations land as documented (reuse AliroGcm, structural CBOR only, stream-encrypt, IssuerAuth verify cached at personalization)
  • signaling_bitmap honestly reflects capability
  • Diagnostic INSes stripped for production CAP
  • Code-reviewer subagent pass before merge

Vendor bug reports backlog (separate from this plan)

These were genuinely identified during the AUTH1 bring-up; they remain real even after today's progress:

  1. WTX retry cap default of 20 is too aggressive. Card running spec-compliant userland GCM on the most permissive ISO 7816 secure element (j3r452) needs ~11 WTX cycles for AUTH1. ST's 20-cap is conservative; a 50-cap default would cover ecosystem. File with ST as middleware enhancement request.

  2. (Possible — verify during Milestone 1) X-CUBE-ALIRO may still reject empty-payload Reader Status responses. If observed, file with ST as spec-clarification (RFC the Reader Status response shape with CSA).

Item 1 is real; item 2 we'll know during M1E.


Plan complete and saved to docs/plans/2026-06-11-step-up-implementation-v2.md. Two execution options:

1. Subagent-Driven (this session) — I dispatch a fresh subagent per task, review between tasks, fast iteration.

2. Parallel Session (separate) — Open new session with executing-plans, batch execution with checkpoints.

Which approach?