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