docs(plans): Step-Up M2 full implementation plan
This commit is contained in:
496
docs/plans/2026-06-12-step-up-m2-full.md
Normal file
496
docs/plans/2026-06-12-step-up-m2-full.md
Normal file
@@ -0,0 +1,496 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user