# Step-Up Phase Implementation Plan

> **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 on the Java Card applet so the Access Document and Revocation Document become retrievable over NFC, replacing the current `INS_EXCHANGE` stub with real ENVELOPE/GET RESPONSE handling against any spec-conformant reader.

**Architecture:** All Step-Up code lives in `StepUpApplet` (`A000000909ACCE5502`). The Step-Up phase shares the `AliroGcm` instance with `AliroApplet` (rekeyed between phases — saves ~368 B transient; relies on ExpeditedSK ≠ StepUpSK to preserve key/IV uniqueness). Access Document parsing is *structural only* — we validate CBOR major-type/length boundaries and locate `IssuerAuth`, but do not interpret mdoc field semantics. `IssuerAuth` COSE_Sign1 verification is performed *once at personalization* (inside `JCSystem.beginTransaction()` with the document write) and cached as a persistent verified-flag, saving ~100 ms per transaction. The DeviceResponse is stream-encrypted as ENVELOPE response chunks are emitted, never buffering the full ciphertext.

**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 paths), COSE_Sign1 (RFC 9052) over `IssuerAuth`, mdoc DeviceRequest/DeviceResponse CBOR shapes with Aliro-remapped integer keys (spec Tables 8-21, 8-22).

**Reference Material:**
- Aliro 1.0 spec §8.4 (Step-up phase), §8.3.3.5 (EXCHANGE), §10.2.1 (SELECT), Tables 8-14/8-19/8-20/8-21/8-22
- RFC 8949 (CBOR core deterministic encoding)
- RFC 9052 (COSE_Sign1)
- ISO/IEC 18013-5 §8 (mdoc DeviceRequest/DeviceResponse base; Aliro remaps the integer keys)
- Existing applet code: `AliroApplet.java` (Expedited phase), `AliroGcm.java` (rekeyable target), `CredentialStore.java` (Access Document storage), `StepUpApplet.java` (scaffold + extensive TODO comments)
- Session memory: `MEMORY.md` includes the Nucleo bring-up state, j3r452 chip-target decision, transient RAM budget
- This-session traces and decisions: vendor firmware (X-CUBE-ALIRO) violates §10.2 SHALL on step-up AID SELECT — our work should still be spec-conformant; the existing `AliroApplet.INS_EXCHANGE` stub is vendor-specific and gets retired when this lands

**Out of scope for this plan:**
- BLE Step-Up (separate transport layer; only NFC here)
- mdoc field-level interpretation (we transport bytes; reader interprets)
- Revocation Document handling beyond exposing it via the same path (signaling_bitmap bit 1; same shape as Access Document — implementation is identical, just a different blob)

---

## Phase A — Foundation: cache IssuerAuth verification at personalization

Unblocks every later phase: Step-Up just emits the bytes and trusts the cached verification.

### Task A1: Add persistent verified-flag to CredentialStore

**Files:**
- Modify: [applet/src/main/java/com/dangerousthings/aliro/CredentialStore.java](applet/src/main/java/com/dangerousthings/aliro/CredentialStore.java)
- Test: [applet/src/test/java/com/dangerousthings/aliro/CredentialStoreSerializationTest.java](applet/src/test/java/com/dangerousthings/aliro/CredentialStoreSerializationTest.java)

**Step 1:** Write failing test for `accessDocumentVerified` getter.

```java
@Test
void freshlyConstructedStoreReportsAccessDocumentNotVerified() {
    CredentialStore store = CredentialStore.bootstrap();
    assertFalse(store.isAccessDocumentVerified());
}
```

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

`./scripts/dt-mvn.sh test -Dtest=CredentialStoreSerializationTest#freshlyConstructedStoreReportsAccessDocumentNotVerified`

**Step 3:** Add the field + getter to `CredentialStore`.

```java
// Inside the class — alongside accessDocumentFinalized
private boolean accessDocumentVerified;  // persistent EEPROM byte

boolean isAccessDocumentVerified() {
    return accessDocumentVerified;
}
```

**Step 4:** Run test, expect PASS.

**Step 5:** Update `CredentialStoreSerializationTest`'s contract test to include the new field in the serialization layout (the test asserts byte-stable layout to catch silent reorders — adjust the expected layout bytes).

**Step 6:** Run full test suite, expect 81/81 PASS.

`./scripts/dt-mvn.sh test`

**Step 7:** Commit.

```bash
git add applet/src/main/java/com/dangerousthings/aliro/CredentialStore.java applet/src/test/java/com/dangerousthings/aliro/CredentialStoreSerializationTest.java
git commit -m "feat: add accessDocumentVerified persistent flag to CredentialStore"
```

---

### Task A2: Add ECDSA-P256 COSE_Sign1 verifier method

**Files:**
- Create: `applet/src/main/java/com/dangerousthings/aliro/CoseVerifier.java`
- Test: `applet/src/test/java/com/dangerousthings/aliro/CoseVerifierTest.java`

**Step 1:** Write failing test using a known-good COSE_Sign1 + key.

```java
@Test
void verifiesValidCoseSign1AgainstP256Key() throws Exception {
    // Use a fixed test vector — generate offline with cose-go or pycose,
    // bake in. payload + signature + EC point of signer.
    byte[] coseSign1 = HEX.decode("8443a10126a05446b6173685f74657374...");  // full vector
    byte[] signerPubXY = HEX.decode("04abcd...");  // 65 B uncompressed P-256
    boolean ok = CoseVerifier.verifyCoseSign1(coseSign1, (short)0, (short)coseSign1.length,
                                              signerPubXY, (short)0);
    assertTrue(ok);
}
```

**Step 2:** Run, expect FAIL ("class not found").

**Step 3:** Implement `CoseVerifier.verifyCoseSign1`:

- Parse the COSE_Sign1 outer CBOR array: `[protected_header, unprotected_header, payload, signature]`
- Construct the Sig_structure: `["Signature1", protected_header, external_aad=empty bstr, payload]` per RFC 9052 §4.4
- CBOR-encode the Sig_structure deterministically (RFC 8949 §4.2.1)
- SHA-256 the encoded Sig_structure
- ECDSA-P256 verify the signature against the digest using the signer pubkey

Uses existing `MessageDigest.ALG_SHA_256` and `Signature.ALG_ECDSA_SHA_256` instances — refactor `AliroApplet.ecdsaVerifier` access if needed (move to a shared helper class).

**Step 4:** Run, expect PASS.

**Step 5:** Add 3 more test vectors: invalid signature (verify returns false), tampered payload (verify returns false), wrong signer key (verify returns false).

**Step 6:** Run all 4 tests, expect PASS.

**Step 7:** Commit.

```bash
git add applet/src/main/java/com/dangerousthings/aliro/CoseVerifier.java applet/src/test/java/com/dangerousthings/aliro/CoseVerifierTest.java
git commit -m "feat: add COSE_Sign1 verifier for IssuerAuth (P-256)"
```

---

### Task A3: Wire IssuerAuth verification into PersonalizationApplet.finalizeAccessDocument

**Files:**
- Modify: `applet/src/main/java/com/dangerousthings/aliro/PersonalizationApplet.java`
- Modify: `applet/src/main/java/com/dangerousthings/aliro/CredentialStore.java` (`finalizeAccessDocument` becomes `finalizeAccessDocumentVerified` taking the Credential Issuer pubkey)
- Test: `applet/src/test/java/com/dangerousthings/aliro/PersonalizationAppletTest.java`

**Step 1:** Write failing test.

```java
@Test
void finalizeAccessDocumentSetsVerifiedFlagAtomically() throws Exception {
    // Provision Access Document with a valid IssuerAuth COSE_Sign1
    byte[] validAd = TestVectors.validAccessDocumentWithIssuerAuth();
    // ... write chunks ...
    boolean ok = store.finalizeAccessDocumentVerified((short) validAd.length);
    assertTrue(ok);
    assertTrue(store.isAccessDocumentVerified());
}

@Test
void finalizeAccessDocumentRejectsInvalidIssuerAuth() throws Exception {
    byte[] tamperedAd = TestVectors.validAccessDocumentWithIssuerAuth();
    tamperedAd[100] ^= 0x01;  // flip a payload byte
    // ... write chunks ...
    boolean ok = store.finalizeAccessDocumentVerified((short) tamperedAd.length);
    assertFalse(ok);
    assertFalse(store.isAccessDocumentVerified());
    assertFalse(store.hasAccessDocument(), "tampered AD must not be marked finalized either");
}
```

**Step 2:** Run, expect FAIL.

**Step 3:** Replace `CredentialStore.finalizeAccessDocument(short)` with `finalizeAccessDocumentVerified(short, byte[] issuerKeyBuf, short issuerKeyOff)`:

```java
boolean finalizeAccessDocumentVerified(short totalLen, byte[] issuerKey, short issuerKeyOff) {
    if (totalLen < 0 || totalLen > ACCESS_DOC_MAX_LEN) return false;
    // Locate the IssuerAuth COSE_Sign1 inside accessDocument[0..totalLen)
    // (structural CBOR walk — opt 2 — implemented in Phase B; for now use a
    // helper that walks the outer CBOR map looking for the well-known key)
    short[] iaSpan = StructuralCbor.locateIssuerAuth(accessDocument, (short) 0, totalLen);
    if (iaSpan == null) return false;
    boolean verified = CoseVerifier.verifyCoseSign1(
        accessDocument, iaSpan[0], iaSpan[1], issuerKey, issuerKeyOff);
    if (!verified) return false;

    JCSystem.beginTransaction();
    accessDocumentLen = totalLen;
    accessDocumentFinalized = true;
    accessDocumentVerified = true;
    JCSystem.commitTransaction();
    return true;
}
```

**Step 4:** Update `PersonalizationApplet.process()` call site to pass the Credential Issuer pubkey (held in trust anchors).

**Step 5:** Run tests, expect PASS.

**Step 6:** Sanity check — `PersonalizationAppletTest` still passes 14/14 with the updated finalize signature.

**Step 7:** Commit.

```bash
git add applet/src/main/java/com/dangerousthings/aliro/CredentialStore.java applet/src/main/java/com/dangerousthings/aliro/PersonalizationApplet.java applet/src/test/java/com/dangerousthings/aliro/PersonalizationAppletTest.java
git commit -m "feat: verify IssuerAuth at personalization, cache result (opt 4a)"
```

---

## Phase B — Structural CBOR codec

Boundary-walker only — we don't interpret mdoc field semantics (opt 2).

### Task B1: CBOR major-type / argument decoder

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

**Step 1:** Write failing tests for the 8 CBOR major types' length decoding (uint, nint, bstr, tstr, array, map, tag, simple/float).

```java
@Test void decodesSingleByteUint() { /* 0x07 → value=7, len=1 */ }
@Test void decodesOneByteArg() { /* 0x18 0xFF → value=0xFF, len=2 */ }
@Test void decodesTwoByteArg() { /* 0x19 0x01 0x00 → value=0x100, len=3 */ }
@Test void decodesFourByteArg() { /* 0x1A 0x00 0x00 0x10 0x00 → value=0x1000, len=5 */ }
@Test void rejectsIndefiniteLengthInDeterministicMode() { /* 0x9F (start-array, indefinite) → throws */ }
// ... 4 more for the remaining major types
```

**Step 2:** Run, expect FAIL.

**Step 3:** Implement `StructuralCbor.decodeHeader(byte[] buf, short off)` returning `{majorType, value, headerLen}` as a `short[3]`. RFC 8949 §3 mapping. Reject indefinite-length encodings (canonical-only).

**Step 4:** Run, expect PASS.

**Step 5:** Commit.

```bash
git add applet/src/main/java/com/dangerousthings/aliro/StructuralCbor.java applet/src/test/java/com/dangerousthings/aliro/StructuralCborTest.java
git commit -m "feat: structural CBOR header decoder (RFC 8949 §3, canonical only)"
```

---

### Task B2: CBOR element-span walker

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

**Step 1:** Write failing test for "given an offset to a CBOR element, return the offset+length of that element including nested children."

```java
@Test
void elementSpanIncludesNestedArrayChildren() {
    // [1, [2, 3], 4] = 0x83 01 82 02 03 04 — total 6 bytes
    byte[] buf = HEX.decode("8301820203 04");
    short[] span = StructuralCbor.elementSpan(buf, (short) 0);
    assertEquals(0, span[0]);
    assertEquals(6, span[1]);
}
```

**Step 2-5:** Implement, test, commit recursive span walker. Add 4 more tests: nested map, byte string with payload bytes, tagged value, deep nesting (5 levels).

```bash
git commit -m "feat: structural CBOR element-span walker (recursive)"
```

---

### Task B3: Locate IssuerAuth inside Access Document

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

**Step 1:** Write failing test using a sample Access Document (mdoc IssuerSigned wrapper). Walk to the well-known map key for IssuerAuth.

```java
@Test
void locatesIssuerAuthInValidAccessDocument() throws Exception {
    byte[] ad = TestVectors.validAccessDocumentWithIssuerAuth();
    short[] span = StructuralCbor.locateIssuerAuth(ad, (short) 0, (short) ad.length);
    assertNotNull(span);
    // First byte of IssuerAuth COSE_Sign1 must be 0x84 (array of 4 items)
    assertEquals((byte) 0x84, ad[span[0]]);
}
```

**Step 2-5:** Implement, test, commit. The IssuerAuth key in mdoc IssuerSigned per ISO/IEC 18013-5 is the text string "issuerAuth" — walk the outer map keys looking for that string and return the span of the corresponding value.

```bash
git commit -m "feat: locate IssuerAuth COSE_Sign1 inside mdoc IssuerSigned wrapper"
```

---

### Task B4: Canonical CBOR encoder for the small primitives we emit

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

**Step 1:** Write failing tests for encoding uint, bstr, tstr (the only types we emit in DeviceResponse).

**Step 2-5:** Implement RFC 8949 §4.2.1 deterministic encoding of those three primitives. (No need for arrays/maps as host-side built — we only emit small primitives interleaved with pre-built CBOR blobs.) Test, commit.

```bash
git commit -m "feat: canonical CBOR encoder for uint/bstr/tstr emission"
```

---

## Phase C — GCM session rekey for Step-Up (opt 1)

Reuse the existing `AliroGcm` instance via `aesKey.setKey(stepUpSKDevice)` between phases.

### Task C1: Expose AliroGcm key-rekey + AES-GCM stream interface

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

**Step 1:** Write failing test: same AliroGcm instance encrypts with key K1 then K2, both round-trip correctly via NIST GCM test vectors.

**Step 2-5:** Add a `setKeyAndIv()` method that rebuilds H + M-table + J0 from a new key + IV. Verify with NIST TC vectors for both keys. Commit.

```bash
git commit -m "feat: AliroGcm supports key rekey for Step-Up phase reuse (opt 1)"
```

---

### Task C2: Step-Up GCM IV derivation per spec §8.4.3

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

**Step 1:** Write failing test for IV construction (12-byte IV = 4-byte salt + 4-byte counter + 4-byte device/reader identifier per §8.4.3 — confirm exact bytes against spec).

**Step 2-5:** Implement IV builder + monotonic counter. Test, commit. The counter MUST persist within the session — `CLEAR_ON_DESELECT` transient. Reuse across rekey forbidden (would break GCM nonce-uniqueness).

```bash
git commit -m "feat: Step-Up GCM IV derivation + monotonic counter (§8.4.3)"
```

---

## Phase D — ENVELOPE / GET RESPONSE INS handling on StepUpApplet

### Task D1: SELECT FCI for StepUpApplet (improve existing scaffold)

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

**Step 1:** Write failing test for full FCI per §10.2.1.2 (must advertise APDU command/response sizes).

**Step 2-5:** Extend `sendStepUpFci` to include the proprietary information TLV. Test, commit.

```bash
git commit -m "feat: StepUpApplet SELECT FCI advertises APDU sizes (§10.2.1.2)"
```

---

### Task D2: INS_ENVELOPE handler skeleton (no chaining yet)

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

**Step 1:** Write failing test: single-shot ENVELOPE (no chain bit set) with a known DeviceRequest payload returns a DeviceResponse with the cached-verified Access Document encrypted.

```java
@Test
void singleShotEnvelopeReturnsEncryptedDeviceResponse() throws Exception {
    // Personalize with valid AD + IssuerAuth (Phase A)
    // Drive AUTH0 + AUTH1 to derive StepUpSK
    // Send SELECT to ACCE5502
    // Send ENVELOPE C3 00 00 [encrypted DeviceRequest]
    ResponseAPDU r = sendEnvelope(deviceRequestBytes);
    assertEquals(0x9000, r.getSW());
    byte[] devResp = decryptStepUpResponse(stepUpSKDevice, r.getData());
    assertContainsAccessDocument(devResp);
}
```

**Step 2-5:** Implement `INS_ENVELOPE = 0xC3`. Decrypt the payload with StepUpSKReader. Discard the DeviceRequest (we don't parse mdoc fields — opt 2; just trust the reader asked correctly). Emit a DeviceResponse: top-level CBOR map containing the cached-verified Access Document bytes, encrypted with StepUpSKDevice. Test, commit.

```bash
git commit -m "feat: StepUpApplet ENVELOPE single-shot handler (no chain yet)"
```

---

### Task D3: ENVELOPE response chaining via GET RESPONSE (opt 3 — stream encrypt)

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

**Step 1:** Write failing tests:
- Large DeviceResponse triggers GET RESPONSE (`INS = 0xC0`) flow
- Multiple chained ENVELOPE commands accumulate the request payload before responding
- Response chunks come out via repeated GET RESPONSE

**Step 2-5:** Implement chaining. Critical: stream-encrypt during emit (opt 3). Maintain transient state for "GCM partial state, plaintext remaining offset, response bytes emitted so far." Each GET RESPONSE picks up where the last chunk left off, runs more plaintext through GCM, emits up to APDU response size. Final chunk includes the authentication tag and SW=9000; intermediate chunks return SW=61xx (more data available). Test, commit.

```bash
git commit -m "feat: ENVELOPE/GET RESPONSE chaining with streaming GCM (opt 3)"
```

---

## Phase E — Wire up the bitmap honesty + retire the stub

### Task E1: Remove the X-CUBE-ALIRO-specific INS_EXCHANGE stub from AliroApplet

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

**Step 1:** Write failing test: `AliroApplet` returns SW_INS_NOT_SUPPORTED for INS 0xC9. (Spec-conformant readers route EXCHANGE to ACCE5502 now that it works there.)

**Step 2-5:** Remove the INS_EXCHANGE case from `AliroApplet.process()`. Update the comment cluster. Test, commit. (Note: this may regress X-CUBE-ALIRO behavior; if so, document explicitly in the commit + flag in the Nucleo bring-up memory.)

```bash
git commit -m "refactor: retire INS_EXCHANGE stub on AliroApplet (Step-Up now real)"
```

---

### Task E2: End-to-end integration test on real hardware

**Step 1:** Build the j3r452 CAP. Verify all 80+ existing tests still pass + new ones land green.

**Step 2:** Reinstall on the personalized j3r452. Run `aliro-bench-test` — Expedited AUTH1 still 3.2 s.

**Step 3:** Add an `aliro-bench-stepup` CLI to the harness that drives the full sequence (AUTH0 + AUTH1 + SELECT step-up AID + ENVELOPE/GET RESPONSE for Access Document retrieval). Verify the returned Access Document bytes match what `aliro-personalize` wrote.

**Step 4:** Move card to Nucleo. Capture UART. Confirm `DOOR OPERATION SUCCEEDED`.

**Step 5:** Update Nucleo bring-up memory with the success record.

**Step 6:** Commit.

```bash
git commit -m "feat: end-to-end Step-Up verified on Nucleo NFC10A1 + j3r452"
```

---

## Phase F — Cleanup + documentation

### Task F1: Update memory + plan close-out

- Mark this plan complete in `MEMORY.md`
- Update `nucleo_nfc10a1_bringup_state.md` with the Step-Up end-to-end success
- Add a `step_up_implementation_notes.md` memory capturing the four optimizations' final form + any deviations from this plan

### Task F2: Documentation pass on the public README

- Document the four optimizations + their security/spec deviations
- Note the bitmap-honesty change (no longer claiming Step-Up before it worked)
- Reference the §10.2 vendor bug discussion as a closed-out concern

---

## Risk register

| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| j3r452 transient RAM doesn't fit additional Step-Up buffers | Medium | High | Phase 2 (RAM diet from main todo list) lands ~488 B before this work; stream-encrypting (opt 3) limits the worst case. Re-measure transient usage after Phase D2. |
| CBOR canonical encoding subtleties (negative zero floats, NaN canonicalization) | Low | Medium | We don't emit floats — only uint/bstr/tstr. RFC 8949 §4.2.1 is fully unambiguous for those three. |
| ECDSA-P256 verify on j3r452 is "native" but quirky like HMAC was | Low | High | We already use ECDSA verify in AUTH1 reader-signature path (works). No new risk introduced. |
| Vendor firmware (X-CUBE-ALIRO) still bypasses step-up AID SELECT even with bitmap=0x0005 | High | Medium | Already known. Once Step-Up is real, the vendor bug becomes the vendor's problem — our applet is spec-conformant on `ACCE5502` and any reader that honors §10.2 will work. X-CUBE-ALIRO users will need to file the bug we already characterized. |
| EEPROM wear from `accessDocumentVerified` flag flips | Low | Low | Set once at personalization, never updated. Single-write to a single byte; no wear concern. |

---

## Acceptance criteria

- Step-Up phase end-to-end works against a spec-conformant Aliro reader (PC/SC bench reader after harness extension)
- Step-Up end-to-end works against X-CUBE-ALIRO Nucleo firmware after vendor firmware bug is fixed (or we document the workaround in `app_aliro.c` if we patch it ourselves)
- `signaling_bitmap` reflects actual capabilities (no advertising what we don't serve)
- All four optimizations land as documented:
  - Opt 1: only one `AliroGcm` instance for both phases
  - Opt 2: no field-level mdoc parsing on the card
  - Opt 3: stream-encrypt during ENVELOPE emit (no full-buffer staging)
  - Opt 4: IssuerAuth verified once at personalization; per-transaction skip
- Transient RAM headroom: AliroApplet + StepUpApplet combined ≤ 2,500 B `CLEAR_ON_DESELECT` (i.e., ≥ 600 B headroom on j3r452's 3,120 B pool)
- All 80+ existing tests + new Step-Up tests pass under the existing `./scripts/dt-mvn.sh test` harness
- INS_EXCHANGE stub on AliroApplet removed (no vendor-firmware-specific compat shim shipped)
- Plan + this risk register reviewed by a code-reviewer subagent before merge

---

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

**1. Subagent-Driven (this session)** — I dispatch 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?
