# Step-Up Milestone 2 (full) Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans (or superpowers:subagent-driven-development) to implement this plan task-by-task. Each task ships its tests, runs them, watches them fail, then implements + commits.

**Goal:** Replace M1's `0xA0` stub in `StepUpApplet.processEnvelope` with a real spec-shape mdoc DeviceResponse carrying the personalized Access Document, with GET RESPONSE chaining, GCM session reuse, cached IssuerAuth verification, real Reader Status sub-event in EXCHANGE, honest bitmap, and stripped diagnostic INSes. Verified end-to-end against PC/SC.

**Architecture:** Six new modules on the applet side (`CoseVerifier`, `StructuralCbor`, `StepUpSession`, `DeviceResponseBuilder`, real handlers in `StepUpApplet` + `PersonalizationApplet`), plus four small helpers on the harness side to drive the real Step-Up round-trip from `aliro-bench-test`. IssuerAuth verification runs once at personalization (inside `JCSystem.beginTransaction()`); per-transaction Step-Up reuses cached bytes. DeviceResponse is stream-encrypted as we emit, so we never hold both plaintext and ciphertext at once.

**Tech Stack:** Java Card 3.0.5 (J3R452), `javacard.security.{Signature,KeyBuilder,KeyAgreement,MessageDigest}`, GlobalPlatform 2.3. Reader side: Python 3.11 + `cryptography` + `pyscard` against PC/SC. Tests: JUnit 5 on `jcardsim`, pytest on the harness.

**Hardware verdict path:** PC/SC (NXP PR533 + J3R452 card) via `aliro-bench-test --step-up`. Nucleo / X-CUBE-ALIRO is deferred — `ACWG_processAUTH1ResponsePayload` errors upstream of ENVELOPE, so getting Nucleo DOOR-SUCCEEDED requires ST engagement, not M2 code. See [[m1_pcsc_verification]] memory.

---

## Phase M2A — Cache IssuerAuth verification at personalization

The Access Document is a COSE_Sign1 (RFC 9052) signed by the Credential Issuer. Re-verifying every transaction is ~100 ms wasted. Verify once at personalization, persist a flag, trust thereafter. Constrains the trust anchor to the lifetime of the card (fine for DT implants).

### Task M2A.1: Persistent `accessDocumentVerified` flag in `CredentialStore`

**Files:**
- Modify: `applet/src/main/java/com/dangerousthings/aliro/CredentialStore.java:57-309`
- Modify: `applet/src/test/java/com/dangerousthings/aliro/CredentialStoreSerializationTest.java`

**Step 1: Write the failing test**

In `CredentialStoreSerializationTest`, add:
```java
@Test
void accessDocumentVerifiedRoundTripsThroughSerialize() {
    CredentialStore s1 = new CredentialStore();
    s1.markAccessDocumentVerified();
    byte[] blob = serialize(s1);
    CredentialStore s2 = CredentialStore.deserialize(blob);
    assertTrue(s2.isAccessDocumentVerified(), "verified flag must survive serialize/deserialize round-trip");
}
```

**Step 2: Run and verify it fails**

```
cd applet && ./scripts/dt-mvn.sh test -Dtest=CredentialStoreSerializationTest#accessDocumentVerifiedRoundTripsThroughSerialize
```
Expected: compile error "cannot find symbol markAccessDocumentVerified".

**Step 3: Implement the minimal change**

In `CredentialStore.java`:
```java
private boolean accessDocumentVerified;   // beside accessDocumentFinalized

void markAccessDocumentVerified() { accessDocumentVerified = true; }
boolean isAccessDocumentVerified() { return accessDocumentVerified; }
```

Add `sink.write(accessDocumentVerified);` next to the existing `accessDocumentFinalized` write in `serialize` and the matching `readBoolean()` in `deserialize`. Add `accessDocumentVerified = false;` in `reset()`.

**Step 4: Run and verify it passes**

Same command as Step 2. Expected: PASS.

**Step 5: Commit**
```
git add applet/src/main/java/com/dangerousthings/aliro/CredentialStore.java applet/src/test/java/com/dangerousthings/aliro/CredentialStoreSerializationTest.java
git commit -m "feat(applet): accessDocumentVerified flag in CredentialStore (M2A.1)"
```

### Task M2A.2: `CoseVerifier.verifyCoseSign1` (RFC 9052)

**Files:**
- Create: `applet/src/main/java/com/dangerousthings/aliro/CoseVerifier.java`
- Create: `applet/src/test/java/com/dangerousthings/aliro/CoseVerifierTest.java`
- Reference: `applet/src/main/java/com/dangerousthings/aliro/AliroCrypto.java` (Signature.init pattern)

**Step 1: Write the failing tests**

`CoseVerifierTest` with two cases:
- `verifyCoseSign1_validSignature_returnsTrue`: use a fixed-key COSE_Sign1 produced by Python (`harness/tests/test_cose.py` already has fixtures — borrow the same vectors).
- `verifyCoseSign1_tamperedPayload_returnsFalse`: flip one payload byte, assert false.

**Step 2: Run + verify fail**

```
cd applet && ./scripts/dt-mvn.sh test -Dtest=CoseVerifierTest
```
Expected: "class CoseVerifier not found".

**Step 3: Implement**

`CoseVerifier` exposes:
```java
boolean verifyCoseSign1(
    byte[] coseSign1, short coseOff, short coseLen,
    byte[] issuerPubUncomp, short pubOff)
```

Internal: walks the four-element COSE_Sign1 array, constructs the Sig_structure per RFC 9052 §4.4, runs ECDSA-P256 verify via `Signature.ALG_ECDSA_SHA_256`. Reuses the EC keypair seeding pattern from `AliroCrypto` (P-256 curve params).

Uses `StructuralCbor` (built next, in M2B). For now, hardcode the CBOR offsets from the spec-shape COSE_Sign1 the test vector produces, with a TODO to swap for `StructuralCbor.elementSpan` once M2B lands. (M2B.3 will replace the TODO.)

**Step 4: Run + verify pass**

Same command. Expected: both tests pass.

**Step 5: Commit**
```
git commit -am "feat(applet): CoseVerifier RFC 9052 verify (M2A.2)"
```

### Task M2A.3: Wire IssuerAuth verify into `PersonalizationApplet.finalizeAccessDocument`

**Files:**
- Modify: `applet/src/main/java/com/dangerousthings/aliro/PersonalizationApplet.java:158`
- Modify: `applet/src/main/java/com/dangerousthings/aliro/CredentialStore.java:192` (`finalizeAccessDocument`)
- Modify: `applet/src/test/java/com/dangerousthings/aliro/PersonalizationAppletTest.java`

**Step 1: Failing tests**

Two test cases:
- `finalizeAccessDocument_validIssuerAuth_setsVerifiedFlag`
- `finalizeAccessDocument_tamperedIssuerAuth_throws6985_andLeavesUnverified`

Use `ProvisioningHelper` to drive the personalization sequence with a valid then tampered AD; assert state of `accessDocumentVerified` after each.

**Step 2: Run + verify fail**

```
./scripts/dt-mvn.sh test -Dtest=PersonalizationAppletTest
```
Expected: both new tests fail (current finalize doesn't verify).

**Step 3: Implement**

In `CredentialStore.finalizeAccessDocument`, accept the issuer pubkey + an `AliroCrypto` (or extracted `CoseVerifier`) and wrap the state transitions in a single transaction:

```java
boolean finalizeAccessDocument(short totalLen, CoseVerifier verifier, byte[] issuerPubUncomp, short pubOff) {
    if (totalLen < 0 || totalLen > ACCESS_DOC_MAX_LEN) return false;
    boolean verified = verifier.verifyCoseSign1(accessDocument, (short) 0, totalLen, issuerPubUncomp, pubOff);
    if (!verified) return false;
    javacard.framework.JCSystem.beginTransaction();
    try {
        accessDocumentLen = totalLen;
        accessDocumentFinalized = true;
        accessDocumentVerified = true;
        javacard.framework.JCSystem.commitTransaction();
    } catch (Throwable t) {
        javacard.framework.JCSystem.abortTransaction();
        return false;
    }
    return true;
}
```

In `PersonalizationApplet`, pass the credential-issuer pubkey from the credential store + the shared `CoseVerifier`. Update the existing `finalizeAccessDocument` caller and any test fixtures.

**Step 4: Run + verify pass**

Expected: both tests pass.

**Step 5: Commit**
```
git commit -am "feat(applet): IssuerAuth verify cached at personalization (M2A.3)"
```

---

## Phase M2B — Structural CBOR codec

Only what we need: header decode, element span, locate IssuerAuth, canonical uint/bstr/tstr encoders. No floats, no indefinite-length, no tags. Per RFC 8949 §3.

### Task M2B.1: `StructuralCbor.decodeHeader`

**Files:**
- Create: `applet/src/main/java/com/dangerousthings/aliro/StructuralCbor.java`
- Create: `applet/src/test/java/com/dangerousthings/aliro/StructuralCborTest.java`

**Step 1: Failing tests** — 8 cases covering each major type (0..6, skip 7 floats/special) and each argument size (immediate, 1B, 2B, 4B; reject 8B as out-of-scope for short-bounded buffers).

**Step 2: Run + verify fail** (`-Dtest=StructuralCborTest#decodeHeader_*`).

**Step 3: Implement** — single static method returning a packed `short` (`majorType << 8 | argByteLen`) plus an out-parameter for the argument value (or use a tiny 6B scratch). See `harness/src/aliro_harness/issuer/cbor.py` for the algorithm.

**Step 4: Pass.**

**Step 5: Commit** `feat(applet): StructuralCbor.decodeHeader (M2B.1)`.

### Task M2B.2: `StructuralCbor.elementSpan`

**Files:** Same as M2B.1.

**Step 1: Failing tests** — 5 cases: uint, bstr, tstr, array-of-3-uints, map-with-two-bstr-uint pairs. Each asserts the returned span equals the test vector's known length.

**Step 2-4:** Recursive walker built on `decodeHeader`. Throws `ISOException(SW_DATA_INVALID)` on indefinite-length or major-type-7.

**Step 5: Commit** `feat(applet): StructuralCbor.elementSpan (M2B.2)`.

### Task M2B.3: `StructuralCbor.locateIssuerAuth`

**Files:** Same as M2B.1. Also: swap M2A.2's hardcoded offsets in `CoseVerifier` for this helper.

**Step 1: Failing test** — feed the mdoc IssuerSigned wrapper bytes (from `harness/tests/test_cose.py` fixtures), assert returned `(offset, length)` matches the known IssuerAuth element.

**Step 2-4:** Walks the IssuerSigned map looking for key `b"issuerAuth"`, returns the element span.

**Step 5: Commit** `feat(applet): locate IssuerAuth + drop CoseVerifier offset TODO (M2B.3)`.

### Task M2B.4: Canonical CBOR encoder

**Files:** Same as M2B.1.

**Step 1: Failing tests** — canonical encoding of:
- `encodeUint(0)`, `(23)`, `(24)`, `(0xff)`, `(0x100)`, `(0xffff)`, `(0x10000)` — verify each picks the shortest argument size per RFC 8949 §4.2.1
- `encodeBstrHeader(0)`, `(1)`, `(23)`, `(24)`, `(0xff)`, `(0x100)` — header bytes only, caller writes the payload
- `encodeTstrHeader(...)` — same shape as bstr but major-type 3

**Step 2-4:** Three static methods, all writing into a caller-supplied `byte[] out, short outOff` and returning bytes written.

**Step 5: Commit** `feat(applet): canonical CBOR uint/bstr/tstr encoders (M2B.4)`.

---

## Phase M2C — GCM session reuse for Step-Up

M1's `processExchange`/`processEnvelope` already call `CryptoSingletons.getAliroGcm()` directly. M2C wraps the IV construction (counter management, prefix) so M2D doesn't open-code it again. Smallest possible factoring.

### Task M2C.1: `StepUpSession` IV builder

**Files:**
- Create: `applet/src/main/java/com/dangerousthings/aliro/StepUpSession.java`
- Create: `applet/src/test/java/com/dangerousthings/aliro/StepUpSessionTest.java`

**Step 1: Failing tests** — 4 cases:
- `readerIv_counter1_returnsAllZerosThen0001`
- `readerIv_counter256_handlesCarryIntoByte2`
- `deviceIv_counter1_returnsZeros_then01_then0001`
- `incrementCounter_wrapsAt0xFFFFFFFF` (no-op behavior — see existing `StepUpApplet.incrementCounter`)

**Step 2-4:** Lift the IV-build + counter logic from M1's `StepUpApplet` into instance methods on a tiny `StepUpSession` holding `stepUpReaderCounter[4]`, `stepUpDeviceCounter[4]`, and the two 32B keys (CLEAR_ON_DESELECT). Provide `readerIv(byte[] out, short outOff)`, `deviceIv(byte[] out, short outOff)`, `advanceReaderCounter()`, `advanceDeviceCounter()`.

`StepUpApplet` consumes the new session: its `select()` constructs/refreshes one, and `processExchange`/`processEnvelope` ask the session for the IV instead of building it inline. Net change in `StepUpApplet` is ~30 lines simpler.

**Step 5: Commit** `refactor(applet): extract StepUpSession from StepUpApplet (M2C.1)`.

### Task M2C.2: `AliroGcm.setKeyAndIv` rekey helper

**Files:**
- Modify: `applet/src/main/java/com/dangerousthings/aliro/AliroGcm.java`
- Modify: `applet/src/test/java/com/dangerousthings/aliro/AliroGcmTest.java`

**Step 1: Failing test** — set key+iv once, encrypt block A; rekey via the new method, encrypt block B; assert both round-trip through decrypt. Confirms no leftover state.

**Step 2-4:** Add `setKeyAndIv(byte[] key, short keyOff, byte[] iv, short ivOff)` that performs the `aesKey.setKey` + ECB init + GHASH H computation + M-table build. Existing `encrypt`/`decrypt` paths call it instead of inlining those steps. Net change ~20 lines moved, no new state.

**Step 5: Commit** `refactor(applet): AliroGcm.setKeyAndIv consolidates re-keying (M2C.2)`.

---

## Phase M2D — Real ENVELOPE handler with DeviceResponse + chaining

This is the heart of M2. Replaces M1's `0xA0` stub.

### Task M2D.1: mdoc DeviceRequest structural parse

**Files:**
- Create: `applet/src/main/java/com/dangerousthings/aliro/DeviceRequestParser.java`
- Create: `applet/src/test/java/com/dangerousthings/aliro/DeviceRequestParserTest.java`

We do NOT interpret field requests — we have one Access Document, we'll return it. We just structurally walk the request to confirm shape (per Aliro Table 8-19) and extract the namespace+itemRequests offsets in case M3 wants them. Keeps the parser honest about what's been seen.

**Step 1: Failing tests** — 3 cases:
- `parseDeviceRequest_validShape_returnsOk`
- `parseDeviceRequest_unknownVersion_throws6985`
- `parseDeviceRequest_malformedCbor_throws6A80`

**Step 2-4:** Walks the top-level map looking for `b"version"` and `b"docRequests"`, asserts version `"1.0"`, that `docRequests` is an array with ≥1 entry, and that each entry has an `itemsRequest` bstr. Uses `StructuralCbor` exclusively.

**Step 5: Commit** `feat(applet): DeviceRequestParser structural validation (M2D.1)`.

### Task M2D.2: `DeviceResponseBuilder`

**Files:**
- Create: `applet/src/main/java/com/dangerousthings/aliro/DeviceResponseBuilder.java`
- Create: `applet/src/test/java/com/dangerousthings/aliro/DeviceResponseBuilderTest.java`

**Step 1: Failing tests** — 2 cases:
- `buildDeviceResponse_shape_matchesAliroTable8_22`: assert top-level keys `"version"="1.0"`, `"documents"` array of 1, then drill into `[0]["issuerSigned"]["issuerAuth"]` and confirm the bytes equal the personalized AD bytes verbatim.
- `buildDeviceResponse_outputLength_matchesPython`: cross-check against `harness/tests/test_device_response.py` using the same trust bundle.

**Step 2-4:** Builds the response into a caller-supplied buffer using `StructuralCbor` canonical encoders. Embeds the cached AD bytes verbatim (already a complete COSE_Sign1 / IssuerAuth). Returns total byte length. Pure helper — no state.

**Step 5: Commit** `feat(applet): DeviceResponseBuilder (M2D.2)`.

### Task M2D.3: Stream-encrypt during ENVELOPE emit

**Files:**
- Modify: `applet/src/main/java/com/dangerousthings/aliro/StepUpApplet.java` (`processEnvelope`)
- Modify: `applet/src/test/java/com/dangerousthings/aliro/StepUpAppletTest.java`

The lazy-but-correct shape: `AliroGcm.encrypt` already encrypts block-by-block in one pass. We pass a slice of `DeviceResponseBuilder`'s output. The "stream" terminology in the v2 plan means avoiding double-buffering the whole plaintext: we build the DeviceResponse straight into a scratch buffer, encrypt in place to the APDU buffer, append the tag.

**Step 1: Failing test** — drive the full Step-Up sequence in `StepUpAppletTest`:
1. AUTH0/AUTH1 via existing harness
2. SELECT 5502
3. INS=0xC9 EXCHANGE with valid Reader Status sub-event (M2E)
4. INS=0xC3 ENVELOPE with a minimal DeviceRequest
5. Decrypt response with `StepUpSKDevice`
6. Assert decrypted bytes == known-good DeviceResponse bytes from `harness/tests/test_device_response.py`

**Step 2-4:** Replace the `0xA0` literal in `processEnvelope` with a call sequence:
1. `DeviceRequestParser` on the decrypted inbound — reject on shape error.
2. `DeviceResponseBuilder` into `scratchPlaintext`.
3. Build device IV via `stepUpSession.deviceIv(...)`.
4. `CryptoSingletons.getAliroGcm().encrypt(...)` from scratch to apdu buffer.
5. Advance device counter.

If the DeviceResponse exceeds APDU outgoing capacity, the encrypt completes (ciphertext lives in a side buffer) and we return SW=61xx for GET RESPONSE chaining — set up in M2D.4.

**Step 5: Commit** `feat(applet): real DeviceResponse via stream-encrypted ENVELOPE (M2D.3)`.

### Task M2D.4: GET RESPONSE chaining for >248B responses

**Files:**
- Modify: `applet/src/main/java/com/dangerousthings/aliro/StepUpApplet.java`
- Modify: `applet/src/test/java/com/dangerousthings/aliro/StepUpAppletTest.java`

Our AD is 272 B today; wrapped in DeviceResponse + GCM-tagged it's ~370 B. Exceeds the 256 B APDU response cap. Need ISO 7816 GET RESPONSE chaining (SW=`61 xx`, INS=`C0`).

**Step 1: Failing test** — drive a full Step-Up where the DeviceResponse is 400 B. Assert:
- First ENVELOPE returns `SW=61 90` (144 more bytes available) with 256 B body
- GET RESPONSE returns the remaining 144 B + SW=9000
- Concatenated body decrypts cleanly

**Step 2-4:** Add a `responseBuffer[400]` CLEAR_ON_DESELECT plus offset/remaining counters. In `processEnvelope`, after stream-encrypt:
- If total ≤ 252 B: send all + SW=9000.
- If > 252: send first 252, store rest, return SW=`61 ll` (ll = remaining).

Add `processGetResponse(APDU apdu)` for INS=0xC0 on CLA=0x00 — sends next chunk, advances offsets, sets SW=`61 ll` if more remains or `9000` on the last chunk.

**Step 5: Commit** `feat(applet): GET RESPONSE chaining on StepUpApplet (M2D.4)`.

---

## Phase M2E — Real EXCHANGE Reader Status response

M1's EXCHANGE returns `9000 + empty`. Spec §8.3.3.5 Table 8-20 says the response is a Reader Status sub-event payload (status=OK, optional payload). Vendor library may or may not accept the empty form; the real form is short and unambiguous, so just ship it.

### Task M2E.1: Decrypt + structurally validate Reader Status request

**Files:**
- Modify: `applet/src/main/java/com/dangerousthings/aliro/StepUpApplet.java` (`processExchange`)
- Modify: `applet/src/test/java/com/dangerousthings/aliro/StepUpAppletTest.java`

**Step 1: Failing test** — send valid Reader Status request (1B sub-event ID + 0B payload), assert 9000+spec-shape response (built in M2E.2). Send malformed payload, assert `6A80`.

**Step 2-4:** After decrypt, parse the Reader Status sub-event header per Table 8-19: 1B sub-event ID, 1B payload length, payload. Reject unknown sub-event IDs with `SW_DATA_INVALID`.

**Step 5: Commit** `feat(applet): EXCHANGE Reader Status request validation (M2E.1)`.

### Task M2E.2: Build Reader Status response sub-event

**Files:** Same as M2E.1.

**Step 1: Failing test** — assert response bytes (after GCM decrypt with `StepUpSKDevice`) equal `[0x01, 0x00, 0x00]` (sub-event ID=AccessGranted=0x01, status=OK=0x00, payload-len=0).

**Step 2-4:** Build the 3 B Reader Status response sub-event in `scratchPlaintext`, encrypt with `stepUpSession.deviceIv(...)` + `StepUpSKDevice`, send 3+16=19 B with SW=9000.

**Step 5: Commit** `feat(applet): real Reader Status response sub-event (M2E.2)`.

---

## Phase M2F — Bitmap honesty + cleanup

### Task M2F.1: Compute bitmap from capability

**Files:**
- Modify: `applet/src/main/java/com/dangerousthings/aliro/AliroApplet.java` (the constant or builder for `signaling_bitmap`)
- Modify: `applet/src/test/java/com/dangerousthings/aliro/AliroAppletAuth1Test.java`

**Step 1: Failing tests** — 3 cases:
- AD provisioned → bitmap = `0x0005` (bit 0 = AD retrievable, bit 2 = step-up AID required since we use the split-AID architecture)
- AD not provisioned → bitmap = `0x0004` (bit 2 still set, but bit 0 cleared)
- AD provisioned + not verified → bitmap = `0x0004` (treat unverified as not-available)

**Step 2-4:** Replace hardcoded `0x0005` with a computed value reading `CredentialStore.hasAccessDocument() && isAccessDocumentVerified()`.

**Step 5: Commit** `fix(applet): signaling_bitmap reflects actual AD capability (M2F.1)`.

### Task M2F.2: Delete M1 placeholder responses

**Files:**
- Modify: `applet/src/main/java/com/dangerousthings/aliro/StepUpApplet.java`

Strip remaining `// TODO M1: …` comments and any dead code paths from the M1 stub form. Net deletion ~50 lines.

**Step 1-2:** No new tests — existing M2D/M2E tests cover behavior.
**Step 3:** Delete only the comments + dead branches.
**Step 4:** Full applet test suite stays green.
**Step 5: Commit** `chore(applet): drop M1 stub comments + dead paths (M2F.2)`.

### Task M2F.3: Strip diagnostic INSes for production build

**Files:**
- Modify: `applet/src/main/java/com/dangerousthings/aliro/AliroApplet.java` (`DIAGNOSTICS_ENABLED` constant)
- Modify: `applet/pom.xml` (verify j3r452 profile flips it off)

**Step 1:** Build CAP with `DIAGNOSTICS_ENABLED=false`; record before/after CAP size.
**Step 2-4:** Flip the boolean default to false; gate test-only diag-INS tests on a system property so they skip in prod build. Expected CAP shrink: ~3 KB.
**Step 5: Commit** `chore(applet): strip diagnostic INSes from prod CAP (M2F.3)`.

---

## Phase M2G — Cross-reader interop

### Task M2G.1: Extend `aliro-bench-test --step-up` to assert AD round-trip

**Files:**
- Modify: `harness/src/aliro_harness/reader/step_up.py`
- Modify: `harness/src/aliro_harness/reader/cli.py` (no flag change — same `--step-up` does the new thing)
- Modify: `harness/tests/test_reader_step_up.py`

**Step 1: Failing tests** — `verify_step_up_m2(transmit, step_up_sk, expected_ad)` parses the decrypted DeviceResponse, locates `documents[0].issuerSigned.issuerAuth`, asserts bytes-equal to `expected_ad`.

**Step 2-4:**
- Add `verify_step_up_m2` next to `verify_step_up_m1`.
- Extend CLI to load the expected AD from `<trust-dir>/access_document.bin` and pass it in.
- Implement GET RESPONSE chaining in the transmit helper (`while sw1==0x61: send INS=0xC0 P3=sw2`).
- Implement minimal DeviceRequest builder + the structural CBOR decoder for the response (reuse `harness/src/aliro_harness/issuer/cbor.py`).

**Step 5: Commit** `feat(harness): --step-up asserts Access Document round-trip (M2G.1)`.

### Task M2G.2: Run end-to-end verdict on real card

**Files:** None (verdict capture).

**Step 1: Install M2 CAP per applet/INSTALL.md** on a personalized card. Verify UID first per [[feedback_verify_card_uid]].
**Step 2: Run** `aliro-bench-test --trust-dir ~/aliro-trust --step-up`. Save output to `docs/verdicts/2026-06-DD-m2-pcsc-verdict.log`.
**Step 3:** Expected: `RESULT: OK` for Expedited + `STEP-UP M2: OK — Access Document round-trip verified, ABC bytes match`.
**Step 4:** If green: commit the verdict log. If red: open a debugging-skill session before patching anything.

**Step 5: Commit** `chore(verdict): M2 PC/SC verdict — Access Document round-trip OK (M2G.2)`.

### Task M2G.3: File vendor bug reports (parked until M2G.2 green)

Open issues with ST (X-CUBE-ALIRO `ACWG_processAUTH1ResponsePayload` error) and CSA (spec ambiguity on Reader Status sub-event response shape, if any). Independent of M2 code shipping — track in `docs/vendor-bugs/`.

**Step 1: Commit** `docs(vendor-bugs): file ST X-CUBE-ALIRO bug + CSA spec questions (M2G.3)`.

---

## Phase M2H — Documentation + memory

### Task M2H.1: Memory + plan close-out

**Files:**
- New: `/home/work/.claude/projects/-home-work-VSCodeProjects-aliro-project/memory/step_up_implementation_notes.md` (final form of all four optimizations + spec citations)
- Modify: `/home/work/.claude/projects/-home-work-VSCodeProjects-aliro-project/memory/MEMORY.md` (index entry)
- Modify: `docs/plans/2026-06-12-step-up-m2-full.md` (this file — add "Status: COMPLETE — verdict at docs/verdicts/...")
- Modify: `applet/INSTALL.md` (add M2 acceptance criterion: AD round-trip)

**Step 1: Commit** `docs: M2 close-out — memory, plan status, INSTALL.md (M2H.1)`.

---

## Risk register (post-M1 adjustments)

| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Transient RAM headroom blown by `responseBuffer[400]` for chaining | Low | High | Current j3r452 free pool ~1,400 B post-M1. 400 B brings us to ~1,000 B. Comfortable. |
| CBOR encode of bytestr ≥ 24 B picks wrong argument-size class | Low | Medium | M2B.4 tests cover every boundary (23/24/0xff/0x100/0xffff/0x10000). |
| `Signature.ALG_ECDSA_SHA_256` on J3R452 quirks during IssuerAuth verify | Low | High | Already used for AUTH0 reader-sig verify. No new risk. |
| `processGetResponse` race with another applet selection wiping state | None | — | StepUp keys are CLEAR_ON_DESELECT; a SELECT to another applet correctly invalidates the GET RESPONSE chain. By design. |
| Vendor X-CUBE-ALIRO rejects spec-shape DeviceResponse | Deferred | — | Out of scope until ST engagement. M2G.2 verdict is PC/SC only. |

---

## Acceptance criteria

- All applet tests green (~130 cases including new ones)
- All harness tests green (~125 cases)
- `aliro-bench-test --step-up` on a personalized J3R452: `RESULT: OK` for Expedited + `STEP-UP M2: OK — Access Document round-trip verified`
- Production CAP (`DIAGNOSTICS_ENABLED=false`) shrinks ~3 KB vs M1
- Final code-reviewer subagent pass — no spec-conformance or quality blockers
- Memory updated; `step_up_implementation_notes.md` published

---

## Execution

After the plan is saved, the controller decides:

**1. Subagent-Driven (this session)** — `superpowers:subagent-driven-development` dispatches one implementer subagent per task, two-stage review (spec → quality) per task, fresh context per task.

**2. Parallel Session (separate)** — open a new session in this worktree, run `superpowers:executing-plans` with checkpoints.

Either way: **install + verdict on real hardware after every phase boundary** (M2A done → bench-test still green; M2B done → still green; …) so we catch regressions early instead of at M2G.2.
