diff --git a/applet/src/main/java/com/dangerousthings/aliro/AliroApplet.java b/applet/src/main/java/com/dangerousthings/aliro/AliroApplet.java index 1041a95..b241830 100644 --- a/applet/src/main/java/com/dangerousthings/aliro/AliroApplet.java +++ b/applet/src/main/java/com/dangerousthings/aliro/AliroApplet.java @@ -837,15 +837,14 @@ public class AliroApplet extends Applet { // // Bit 0 reflects whether we can actually serve an AD: it requires // both hasAccessDocument() (finalized) AND isAccessDocumentVerified() - // (IssuerAuth check passed). Post-M2A.3 finalize requires verify so - // the "finalized but not verified" branch is defensive — see - // CredentialStore.markAccessDocumentVerified() callers. + // (IssuerAuth check passed). finalizeAccessDocument() commits both + // flags atomically, so the "finalized but not verified" branch is + // defensive. // - // Historical note: M1 hardcoded 0x0005 because the closed-source - // ACWG_processAUTH1ResponsePayload() in X-CUBE-ALIRO's Aliro.a errors - // out when bits 0 + 2 are clear and AD is provisioned. With bit 2 - // now always set we still keep that library happy, and bit 0 only - // makes a promise we can actually keep. + // Note: the closed-source ACWG_processAUTH1ResponsePayload() in + // X-CUBE-ALIRO's Aliro.a errors out when bits 0 + 2 are clear and AD + // is provisioned. With bit 2 now always set we still keep that + // library happy, and bit 0 only makes a promise we can actually keep. byte bitmapLo = 0x04; // bit 2 — split-AID step-up architecture if (store.hasAccessDocument() && store.isAccessDocumentVerified()) { bitmapLo |= 0x01; // bit 0 — AD retrievable diff --git a/applet/src/main/java/com/dangerousthings/aliro/CredentialStore.java b/applet/src/main/java/com/dangerousthings/aliro/CredentialStore.java index 1d7a89a..7eec6db 100644 --- a/applet/src/main/java/com/dangerousthings/aliro/CredentialStore.java +++ b/applet/src/main/java/com/dangerousthings/aliro/CredentialStore.java @@ -268,7 +268,14 @@ final class CredentialStore { 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; } diff --git a/applet/src/main/java/com/dangerousthings/aliro/StepUpApplet.java b/applet/src/main/java/com/dangerousthings/aliro/StepUpApplet.java index afab6a6..8ab20b3 100644 --- a/applet/src/main/java/com/dangerousthings/aliro/StepUpApplet.java +++ b/applet/src/main/java/com/dangerousthings/aliro/StepUpApplet.java @@ -44,19 +44,17 @@ import javacard.framework.Util; * the entire encrypted DeviceResponse then chunk it; pipe plaintext * through GCM as we emit ENVELOPE response chunks. Reduces transient * footprint AND minimizes plaintext residence in RAM. - *
The {@code AliroApplet.INS_EXCHANGE} stub was retired once StepUpApplet - * landed handling for 0xC9 directly — spec-conformant readers (X-CUBE-ALIRO - * included, once the upstream crypto interop is right) route EXCHANGE here - * after the step-up AID SELECT per §10.2. + *
StepUpApplet handles {@code INS_EXCHANGE} (0xC9) directly: spec-conformant + * readers route EXCHANGE here after the step-up AID SELECT per §10.2. */ 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. */ private final byte[] ivScratch; - /** Persistent EXCHANGE plaintext sink for the decrypt-and-discard path. - * Sized to the largest reasonable Reader Status sub-event we'd see - * during M1 (X-CUBE-ALIRO observed values are well under 64 B); we'll - * resize when the real Reader Status payload size lands. CLEAR_ON_DESELECT - * so post-deselect there's no plaintext residue. */ + /** Transient plaintext sink for ENVELOPE DeviceRequest and EXCHANGE + * Reader Status request decrypts. Sized for the largest reasonable + * reader-supplied payload; CLEAR_ON_DESELECT so post-deselect there's + * no plaintext residue. */ private final byte[] scratchPlaintext; 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 * {@link AliroApplet} just succeeded and parked {@code StepUpSK}), copy * it out and derive {@code StepUpSKDevice}/{@code StepUpSKReader} via - * the §8.4.3 HKDF so the ENVELOPE / EXCHANGE handlers in M1B / M1C can - * AES-256-GCM with them. Always returns true — an un-armed SELECT (e.g. - * a reader that touches the Step-Up AID before AUTH1) still gets a - * successful FCI; downstream handlers will reject commands that need a - * live session. + * the §8.4.3 HKDF so the ENVELOPE / EXCHANGE handlers can AES-256-GCM + * with them. Always returns true — an un-armed SELECT (e.g. a reader + * that touches the Step-Up AID before AUTH1) still gets a successful + * FCI; downstream handlers will reject commands that need a live session. */ @Override public boolean select() { @@ -237,8 +233,8 @@ public class StepUpApplet extends Applet { } /** - * EXCHANGE (CLA=0x80, INS=0xC9) handler — M2E.1 (request validation) + - * M2E.2 (encrypted Reader Status response). + * EXCHANGE (CLA=0x80, INS=0xC9) handler: request validation + encrypted + * Reader Status response. * *
Spec §8.3.3.5 / Tables 8-19 + 8-20. The reader sends * {@code encrypted_payload || authentication_tag} under @@ -309,7 +305,7 @@ public class StepUpApplet extends Applet { // semantically validates). 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. if (ptLen < 2) { 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. Util.arrayFillNonAtomic(scratchPlaintext, (short) 0, ptLen, (byte) 0); - // M2E.2: build the Reader Status response plaintext directly into - // the APDU buffer at offset 0, then encrypt in place. AliroGcm.encrypt - // supports out == pt at the same offset (CTR mode + appended tag). + // Build the Reader Status response plaintext directly into the APDU + // buffer at offset 0, then encrypt in place. AliroGcm.encrypt supports + // out == pt at the same offset (CTR mode + appended tag). buf[0] = (byte) 0x01; // sub_event_id (echoes request) buf[1] = (byte) 0x00; // status = OK 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) + - * M2D.4 (ISO 7816 response chaining). + * ENVELOPE (CLA=0x00, INS=0xC3) handler: emits the canonical-CBOR + * DeviceResponse via ISO 7816 response chaining. * *
Pipeline: *
Keys are NOT zeroed: CLEAR_ON_DESELECT handles that on deselect, * and the surrounding SELECT immediately re-derives them from a fresh - * StepUpSK anyway. Matches StepUpApplet M1 behaviour. + * StepUpSK anyway. */ void reset() { Util.arrayFillNonAtomic(readerCounter, (short) 0, COUNTER_LEN, (byte) 0); diff --git a/applet/src/test/java/com/dangerousthings/aliro/AliroAppletAuth1Test.java b/applet/src/test/java/com/dangerousthings/aliro/AliroAppletAuth1Test.java index 23f05b2..460127f 100644 --- a/applet/src/test/java/com/dangerousthings/aliro/AliroAppletAuth1Test.java +++ b/applet/src/test/java/com/dangerousthings/aliro/AliroAppletAuth1Test.java @@ -215,7 +215,7 @@ class AliroAppletAuth1Test { org.junit.jupiter.api.Assertions.assertTrue( CredentialStore.get().writeAccessDocumentChunk(ad, (short) 0, (short) 0, (short) ad.length)); CredentialStore.get().markAccessDocumentFinalizedForTesting((short) ad.length); - CredentialStore.get().markAccessDocumentVerified(); + CredentialStore.get().markAccessDocumentVerifiedForTesting(); sendStandardAuth0(); ResponseAPDU r = sendValidAuth1(); diff --git a/applet/src/test/java/com/dangerousthings/aliro/AliroAppletTest.java b/applet/src/test/java/com/dangerousthings/aliro/AliroAppletTest.java index 3ee363d..40ff8a4 100644 --- a/applet/src/test/java/com/dangerousthings/aliro/AliroAppletTest.java +++ b/applet/src/test/java/com/dangerousthings/aliro/AliroAppletTest.java @@ -95,12 +95,12 @@ class AliroAppletTest { @Test void auth1ExchangeInsRejectedNowThatStepUpAppletOwnsIt() { - // Once M1B.1 / M1C.1 land StepUpApplet (ACCE5502) as the proper owner - // of INS_EXCHANGE (0xC9), AliroApplet must NOT also answer 0xC9 — a - // spec-conformant reader routes EXCHANGE to ACCE5502 after the §10.2 - // step-up AID SELECT. AliroApplet receiving 0xC9 is a reader bug or - // a stale flow, and must be rejected with SW_INS_NOT_SUPPORTED so it - // can never accidentally interact with expedited session state. + // StepUpApplet (ACCE5502) owns INS_EXCHANGE (0xC9); AliroApplet must + // NOT also answer 0xC9 — a spec-conformant reader routes EXCHANGE to + // ACCE5502 after the §10.2 step-up AID SELECT. AliroApplet receiving + // 0xC9 is a reader bug or a stale flow, and must be rejected with + // SW_INS_NOT_SUPPORTED so it can never accidentally interact with + // expedited session state. selectExpedited(); CommandAPDU exchange = new CommandAPDU(0x80, 0xC9, 0x00, 0x00); ResponseAPDU resp = sim.transmitCommand(exchange); diff --git a/applet/src/test/java/com/dangerousthings/aliro/CredentialStoreSerializationTest.java b/applet/src/test/java/com/dangerousthings/aliro/CredentialStoreSerializationTest.java index 819f6c3..ea3ae01 100644 --- a/applet/src/test/java/com/dangerousthings/aliro/CredentialStoreSerializationTest.java +++ b/applet/src/test/java/com/dangerousthings/aliro/CredentialStoreSerializationTest.java @@ -97,7 +97,7 @@ class CredentialStoreSerializationTest { void accessDocumentVerifiedRoundTripsThroughSerialize() { CredentialStore s1 = CredentialStore.bootstrap(); s1.resetForTesting(); - s1.markAccessDocumentVerified(); + s1.markAccessDocumentVerifiedForTesting(); RecordingSink buf = new RecordingSink(); s1.writeTo(buf);