# 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.**

```java
@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."**

```bash
./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`.

```java
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.**

```bash
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.**

```bash
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.**

```bash
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.**

```bash
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.**

```bash
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
```

5. **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.

```bash
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?
