25 KiB
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:
@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:
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.pyalready 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:
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_setsVerifiedFlagfinalizeAccessDocument_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:
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.1encodeBstrHeader(0),(1),(23),(24),(0xff),(0x100)— header bytes only, caller writes the payloadencodeTstrHeader(...)— 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_returnsAllZerosThen0001readerIv_counter256_handlesCarryIntoByte2deviceIv_counter1_returnsZeros_then01_then0001incrementCounter_wrapsAt0xFFFFFFFF(no-op behavior — see existingStepUpApplet.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_returnsOkparseDeviceRequest_unknownVersion_throws6985parseDeviceRequest_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 againstharness/tests/test_device_response.pyusing 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:
- AUTH0/AUTH1 via existing harness
- SELECT 5502
- INS=0xC9 EXCHANGE with valid Reader Status sub-event (M2E)
- INS=0xC3 ENVELOPE with a minimal DeviceRequest
- Decrypt response with
StepUpSKDevice - 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:
DeviceRequestParseron the decrypted inbound — reject on shape error.DeviceResponseBuilderintoscratchPlaintext.- Build device IV via
stepUpSession.deviceIv(...). CryptoSingletons.getAliroGcm().encrypt(...)from scratch to apdu buffer.- 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 forsignaling_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_ENABLEDconstant) - 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-updoes 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_m2next toverify_step_up_m1. - Extend CLI to load the expected AD from
<trust-dir>/access_document.binand 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-upon a personalized J3R452:RESULT: OKfor 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.mdpublished
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.