chore(applet): drop M1 stub comments + dead paths + naming consistency (M2F.2)

Sweep stale M1-era cruft and milestone task markers obsoleted by M2:

- StepUpApplet class Javadoc: fix stale @link target for
  finalizeAccessDocument (now takes verifier+scratch65), drop "TODO"
  prose for shipped M2A.3 verify-at-finalize, rewrite "INS_EXCHANGE stub
  was retired" past-tense note as present-tense ownership statement.
- StepUpApplet: drop "M1B / M1C" / "M2E.1" / "M2E.2" / "M2D.3+M2D.4"
  task markers from comments and method Javadocs; describe current
  behavior, not milestone provenance.
- StepUpApplet: rewrite "decrypt-and-discard" sink Javadoc — that sink
  now also stages the EXCHANGE Reader Status request plaintext.
- StepUpSession: drop "matching M1 behaviour" Javadoc trailers.
- AliroApplet bitmap comment: align with the new method name and drop
  the M1 historical aside that's now adjacent to the only behavior left.
- AliroAppletTest: rewrite "Once M1B.1 / M1C.1 land..." as present tense.
- CredentialStore.markAccessDocumentVerified() → ForTesting suffix to
  match the sibling markAccessDocumentFinalizedForTesting() and add an
  explanatory Javadoc clarifying that production callers reach the
  verified state via finalizeAccessDocument(short, CoseVerifier, byte[]).
  Two test call sites updated.

No behavior change. Tests: 147 / 0 / 3 pre-existing GCM errors. Net
diff +46/-44 = +2 LOC (the rename's explanatory Javadoc minus the cruft).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
michael
2026-06-17 17:01:12 -07:00
parent ed316102bf
commit d24d72e119
7 changed files with 46 additions and 44 deletions

View File

@@ -837,15 +837,14 @@ public class AliroApplet extends Applet {
// //
// Bit 0 reflects whether we can actually serve an AD: it requires // Bit 0 reflects whether we can actually serve an AD: it requires
// both hasAccessDocument() (finalized) AND isAccessDocumentVerified() // both hasAccessDocument() (finalized) AND isAccessDocumentVerified()
// (IssuerAuth check passed). Post-M2A.3 finalize requires verify so // (IssuerAuth check passed). finalizeAccessDocument() commits both
// the "finalized but not verified" branch is defensive — see // flags atomically, so the "finalized but not verified" branch is
// CredentialStore.markAccessDocumentVerified() callers. // defensive.
// //
// Historical note: M1 hardcoded 0x0005 because the closed-source // Note: the closed-source ACWG_processAUTH1ResponsePayload() in
// ACWG_processAUTH1ResponsePayload() in X-CUBE-ALIRO's Aliro.a errors // X-CUBE-ALIRO's Aliro.a errors out when bits 0 + 2 are clear and AD
// out when bits 0 + 2 are clear and AD is provisioned. With bit 2 // is provisioned. With bit 2 now always set we still keep that
// now always set we still keep that library happy, and bit 0 only // library happy, and bit 0 only makes a promise we can actually keep.
// makes a promise we can actually keep.
byte bitmapLo = 0x04; // bit 2 — split-AID step-up architecture byte bitmapLo = 0x04; // bit 2 — split-AID step-up architecture
if (store.hasAccessDocument() && store.isAccessDocumentVerified()) { if (store.hasAccessDocument() && store.isAccessDocumentVerified()) {
bitmapLo |= 0x01; // bit 0 — AD retrievable bitmapLo |= 0x01; // bit 0 — AD retrievable

View File

@@ -268,7 +268,14 @@ final class CredentialStore {
return accessDocumentFinalized; return accessDocumentFinalized;
} }
void markAccessDocumentVerified() { /**
* Test-only: forces the verified flag without running the IssuerAuth
* COSE_Sign1 verify. Used by tests that exercise downstream behavior
* (signaling bitmap, serialization round-trip) with opaque AD bytes that
* aren't signed by a real issuer key. Production code reaches the verified
* state via {@link #finalizeAccessDocument(short, CoseVerifier, byte[])}.
*/
void markAccessDocumentVerifiedForTesting() {
accessDocumentVerified = true; accessDocumentVerified = true;
} }

View File

@@ -44,19 +44,17 @@ import javacard.framework.Util;
* the entire encrypted DeviceResponse then chunk it; pipe plaintext * the entire encrypted DeviceResponse then chunk it; pipe plaintext
* through GCM as we emit ENVELOPE response chunks. Reduces transient * through GCM as we emit ENVELOPE response chunks. Reduces transient
* footprint AND minimizes plaintext residence in RAM.</li> * footprint AND minimizes plaintext residence in RAM.</li>
* <li><b>Opt 4 — IssuerAuth verify cached at personalization.</b> See * <li><b>Opt 4 — IssuerAuth verify cached at personalization.</b>
* {@link CredentialStore#finalizeAccessDocument(short)} TODO — the * {@link CredentialStore#finalizeAccessDocument} runs the COSE_Sign1
* COSE_Sign1 verify happens once at write time, the persistent * verify once at write time; the persistent
* {@code accessDocumentVerified} flag is checked here at read time. * {@code accessDocumentVerified} flag is checked here at read time.
* Saves ~100 ms per transaction. Trusts that the Credential Issuer * Saves ~100 ms per transaction. Trusts that the Credential Issuer
* trust anchor is fixed for the card's lifetime — true for DT's * trust anchor is fixed for the card's lifetime — true for DT's
* implantable target but document the limitation.</li> * implantable target but document the limitation.</li>
* </ol> * </ol>
* *
* <p>The {@code AliroApplet.INS_EXCHANGE} stub was retired once StepUpApplet * <p>StepUpApplet handles {@code INS_EXCHANGE} (0xC9) directly: spec-conformant
* landed handling for 0xC9 directly — spec-conformant readers (X-CUBE-ALIRO * readers route EXCHANGE here after the step-up AID SELECT per §10.2.
* included, once the upstream crypto interop is right) route EXCHANGE here
* after the step-up AID SELECT per §10.2.
*/ */
public class StepUpApplet extends Applet { public class StepUpApplet extends Applet {
@@ -107,11 +105,10 @@ public class StepUpApplet extends Applet {
* or {@link StepUpSession#deviceIv} before each en/decrypt. CLEAR_ON_DESELECT. */ * or {@link StepUpSession#deviceIv} before each en/decrypt. CLEAR_ON_DESELECT. */
private final byte[] ivScratch; private final byte[] ivScratch;
/** Persistent EXCHANGE plaintext sink for the decrypt-and-discard path. /** Transient plaintext sink for ENVELOPE DeviceRequest and EXCHANGE
* Sized to the largest reasonable Reader Status sub-event we'd see * Reader Status request decrypts. Sized for the largest reasonable
* during M1 (X-CUBE-ALIRO observed values are well under 64 B); we'll * reader-supplied payload; CLEAR_ON_DESELECT so post-deselect there's
* resize when the real Reader Status payload size lands. CLEAR_ON_DESELECT * no plaintext residue. */
* so post-deselect there's no plaintext residue. */
private final byte[] scratchPlaintext; private final byte[] scratchPlaintext;
private static final short SCRATCH_PLAINTEXT_LEN = 256; private static final short SCRATCH_PLAINTEXT_LEN = 256;
@@ -173,11 +170,10 @@ public class StepUpApplet extends Applet {
* SELECT entry point. If {@link SessionContext} is armed (i.e. AUTH1 on * SELECT entry point. If {@link SessionContext} is armed (i.e. AUTH1 on
* {@link AliroApplet} just succeeded and parked {@code StepUpSK}), copy * {@link AliroApplet} just succeeded and parked {@code StepUpSK}), copy
* it out and derive {@code StepUpSKDevice}/{@code StepUpSKReader} via * it out and derive {@code StepUpSKDevice}/{@code StepUpSKReader} via
* the §8.4.3 HKDF so the ENVELOPE / EXCHANGE handlers in M1B / M1C can * the §8.4.3 HKDF so the ENVELOPE / EXCHANGE handlers can AES-256-GCM
* AES-256-GCM with them. Always returns true — an un-armed SELECT (e.g. * with them. Always returns true — an un-armed SELECT (e.g. a reader
* a reader that touches the Step-Up AID before AUTH1) still gets a * that touches the Step-Up AID before AUTH1) still gets a successful
* successful FCI; downstream handlers will reject commands that need a * FCI; downstream handlers will reject commands that need a live session.
* live session.
*/ */
@Override @Override
public boolean select() { public boolean select() {
@@ -237,8 +233,8 @@ public class StepUpApplet extends Applet {
} }
/** /**
* EXCHANGE (CLA=0x80, INS=0xC9) handler — M2E.1 (request validation) + * EXCHANGE (CLA=0x80, INS=0xC9) handler: request validation + encrypted
* M2E.2 (encrypted Reader Status response). * Reader Status response.
* *
* <p>Spec §8.3.3.5 / Tables 8-19 + 8-20. The reader sends * <p>Spec §8.3.3.5 / Tables 8-19 + 8-20. The reader sends
* {@code encrypted_payload || authentication_tag} under * {@code encrypted_payload || authentication_tag} under
@@ -309,7 +305,7 @@ public class StepUpApplet extends Applet {
// semantically validates). // semantically validates).
session.advanceReaderCounter(); session.advanceReaderCounter();
// M2E.1: parse the request shape: [sub_event_id, payload_len, payload]. // Parse the request shape: [sub_event_id, payload_len, payload].
// Need at least sub_event_id + payload_len = 2 bytes. // Need at least sub_event_id + payload_len = 2 bytes.
if (ptLen < 2) { if (ptLen < 2) {
Util.arrayFillNonAtomic(scratchPlaintext, (short) 0, ptLen, (byte) 0); Util.arrayFillNonAtomic(scratchPlaintext, (short) 0, ptLen, (byte) 0);
@@ -331,9 +327,9 @@ public class StepUpApplet extends Applet {
// Wipe the request plaintext — content not needed past validation. // Wipe the request plaintext — content not needed past validation.
Util.arrayFillNonAtomic(scratchPlaintext, (short) 0, ptLen, (byte) 0); Util.arrayFillNonAtomic(scratchPlaintext, (short) 0, ptLen, (byte) 0);
// M2E.2: build the Reader Status response plaintext directly into // Build the Reader Status response plaintext directly into the APDU
// the APDU buffer at offset 0, then encrypt in place. AliroGcm.encrypt // buffer at offset 0, then encrypt in place. AliroGcm.encrypt supports
// supports out == pt at the same offset (CTR mode + appended tag). // out == pt at the same offset (CTR mode + appended tag).
buf[0] = (byte) 0x01; // sub_event_id (echoes request) buf[0] = (byte) 0x01; // sub_event_id (echoes request)
buf[1] = (byte) 0x00; // status = OK buf[1] = (byte) 0x00; // status = OK
buf[2] = (byte) 0x00; // payload_len = 0 buf[2] = (byte) 0x00; // payload_len = 0
@@ -354,8 +350,8 @@ public class StepUpApplet extends Applet {
} }
/** /**
* ENVELOPE (CLA=0x00, INS=0xC3) handler — M2D.3 (real DeviceResponse) + * ENVELOPE (CLA=0x00, INS=0xC3) handler: emits the canonical-CBOR
* M2D.4 (ISO 7816 response chaining). * DeviceResponse via ISO 7816 response chaining.
* *
* <p>Pipeline: * <p>Pipeline:
* <ol> * <ol>

View File

@@ -18,7 +18,7 @@ import javacard.framework.Util;
* <ul> * <ul>
* <li>{@link #reset()} re-initialises both counters to {@code 0x00000001}. * <li>{@link #reset()} re-initialises both counters to {@code 0x00000001}.
* It does NOT zero the session keys -- {@code CLEAR_ON_DESELECT} * It does NOT zero the session keys -- {@code CLEAR_ON_DESELECT}
* handles that on deselect, matching {@link StepUpApplet} M1 behavior.</li> * handles that on deselect.</li>
* <li>{@link #readerIv} / {@link #deviceIv} stamp the current counter into * <li>{@link #readerIv} / {@link #deviceIv} stamp the current counter into
* a 12-byte IV per spec §8.3.1.8/9 (reader) / §8.3.1.6 (device).</li> * a 12-byte IV per spec §8.3.1.8/9 (reader) / §8.3.1.6 (device).</li>
* <li>{@link #advanceReaderCounter} / {@link #advanceDeviceCounter} apply * <li>{@link #advanceReaderCounter} / {@link #advanceDeviceCounter} apply
@@ -71,7 +71,7 @@ final class StepUpSession {
* *
* <p>Keys are NOT zeroed: CLEAR_ON_DESELECT handles that on deselect, * <p>Keys are NOT zeroed: CLEAR_ON_DESELECT handles that on deselect,
* and the surrounding SELECT immediately re-derives them from a fresh * and the surrounding SELECT immediately re-derives them from a fresh
* StepUpSK anyway. Matches StepUpApplet M1 behaviour. * StepUpSK anyway.
*/ */
void reset() { void reset() {
Util.arrayFillNonAtomic(readerCounter, (short) 0, COUNTER_LEN, (byte) 0); Util.arrayFillNonAtomic(readerCounter, (short) 0, COUNTER_LEN, (byte) 0);

View File

@@ -215,7 +215,7 @@ class AliroAppletAuth1Test {
org.junit.jupiter.api.Assertions.assertTrue( org.junit.jupiter.api.Assertions.assertTrue(
CredentialStore.get().writeAccessDocumentChunk(ad, (short) 0, (short) 0, (short) ad.length)); CredentialStore.get().writeAccessDocumentChunk(ad, (short) 0, (short) 0, (short) ad.length));
CredentialStore.get().markAccessDocumentFinalizedForTesting((short) ad.length); CredentialStore.get().markAccessDocumentFinalizedForTesting((short) ad.length);
CredentialStore.get().markAccessDocumentVerified(); CredentialStore.get().markAccessDocumentVerifiedForTesting();
sendStandardAuth0(); sendStandardAuth0();
ResponseAPDU r = sendValidAuth1(); ResponseAPDU r = sendValidAuth1();

View File

@@ -95,12 +95,12 @@ class AliroAppletTest {
@Test @Test
void auth1ExchangeInsRejectedNowThatStepUpAppletOwnsIt() { void auth1ExchangeInsRejectedNowThatStepUpAppletOwnsIt() {
// Once M1B.1 / M1C.1 land StepUpApplet (ACCE5502) as the proper owner // StepUpApplet (ACCE5502) owns INS_EXCHANGE (0xC9); AliroApplet must
// of INS_EXCHANGE (0xC9), AliroApplet must NOT also answer 0xC9 — a // NOT also answer 0xC9 — a spec-conformant reader routes EXCHANGE to
// spec-conformant reader routes EXCHANGE to ACCE5502 after the §10.2 // ACCE5502 after the §10.2 step-up AID SELECT. AliroApplet receiving
// step-up AID SELECT. AliroApplet receiving 0xC9 is a reader bug or // 0xC9 is a reader bug or a stale flow, and must be rejected with
// a stale flow, and must be rejected with SW_INS_NOT_SUPPORTED so it // SW_INS_NOT_SUPPORTED so it can never accidentally interact with
// can never accidentally interact with expedited session state. // expedited session state.
selectExpedited(); selectExpedited();
CommandAPDU exchange = new CommandAPDU(0x80, 0xC9, 0x00, 0x00); CommandAPDU exchange = new CommandAPDU(0x80, 0xC9, 0x00, 0x00);
ResponseAPDU resp = sim.transmitCommand(exchange); ResponseAPDU resp = sim.transmitCommand(exchange);

View File

@@ -97,7 +97,7 @@ class CredentialStoreSerializationTest {
void accessDocumentVerifiedRoundTripsThroughSerialize() { void accessDocumentVerifiedRoundTripsThroughSerialize() {
CredentialStore s1 = CredentialStore.bootstrap(); CredentialStore s1 = CredentialStore.bootstrap();
s1.resetForTesting(); s1.resetForTesting();
s1.markAccessDocumentVerified(); s1.markAccessDocumentVerifiedForTesting();
RecordingSink buf = new RecordingSink(); RecordingSink buf = new RecordingSink();
s1.writeTo(buf); s1.writeTo(buf);