feat: EXCHANGE Reader Status request validation + spec-shape response (M2E.1, M2E.2)
M2E.1: parse the EXCHANGE plaintext as Aliro §8.3.3.5 / Table 8-19 [sub_event_id, payload_len, payload], reject SW_WRONG_LENGTH on truncation or length mismatch and SW_DATA_INVALID on unknown sub_event_id (M2 only supports 0x01 = ReaderStatusRequest). M2E.2: emit a Reader Status sub-event RESPONSE (Table 8-20) plaintext [0x01, 0x00, 0x00] in place in the APDU buffer, GCM-encrypted under StepUpSKDevice + deviceIv(stepup_device_counter) per §8.3.1.6. Ciphertext+ tag = 19 B, single APDU, no chaining. Harness verify_step_up_m2 + the test mock now ship the spec request shape, decrypt the 19 B EXCHANGE response under deviceCounter=1, and decrypt the subsequent ENVELOPE response under deviceCounter=2 (EXCHANGE consumed 1). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -2,13 +2,17 @@
|
||||
|
||||
Drives the M2 applet path post-AUTH1:
|
||||
- SELECT ACCE5502 (StepUpApplet)
|
||||
- INS=0xC9 EXCHANGE encrypted with StepUpSKReader -> expect SW=9000, empty body.
|
||||
(M1 behavior, unchanged in this commit; M2E.x will rework it.)
|
||||
- INS=0xC9 EXCHANGE encrypted with StepUpSKReader carrying a Reader Status
|
||||
sub-event REQUEST plaintext (spec §8.3.3.5 / Table 8-19):
|
||||
sub_event_id = 0x01 (ReaderStatusRequest), payload_len = 0
|
||||
-> applet returns 19 B = 3 B response plaintext + 16 B GCM tag, decrypts
|
||||
under StepUpSKDevice + deviceIv(1) to:
|
||||
sub_event_id = 0x01, status = 0x00, payload_len = 0 (Table 8-20).
|
||||
- INS=0xC3 ENVELOPE encrypted with StepUpSKReader carrying a canonical CBOR
|
||||
mdoc DeviceRequest -> applet returns the first chunk of an encrypted
|
||||
DeviceResponse with SW=61xx.
|
||||
- INS=0xC0 GET RESPONSE repeated until SW=9000 — concatenated body decrypts
|
||||
under StepUpSKDevice + deviceIv(1).
|
||||
under StepUpSKDevice + deviceIv(2) (EXCHANGE consumed deviceIv(1)).
|
||||
- Plaintext is a canonical CBOR DeviceResponse; we walk
|
||||
``documents[0].issuerSigned.issuerAuth`` and assert it round-trips the
|
||||
Access Document the reader was provisioned with.
|
||||
@@ -16,8 +20,9 @@ Drives the M2 applet path post-AUTH1:
|
||||
IV layout per applet (StepUpApplet.processExchange / processEnvelope):
|
||||
reader -> device : 0x00*8 || counter(4B BE)
|
||||
device -> reader : 0x00*7 || 0x01 || counter(4B BE)
|
||||
Both counters init to 1; each advances by 1 after use. The ENVELOPE response
|
||||
uses deviceCounter=1 (it's the first device-side message).
|
||||
Both counters init to 1; each advances by 1 after use. EXCHANGE now consumes
|
||||
deviceCounter=1 (encrypted Reader Status response), so the ENVELOPE response
|
||||
ciphertext decrypts under deviceCounter=2.
|
||||
"""
|
||||
|
||||
import cbor2
|
||||
@@ -105,21 +110,32 @@ def verify_step_up_m2(
|
||||
if sw != SW_OK:
|
||||
return False, f"SELECT ACCE5502 failed: SW=0x{sw:04X}"
|
||||
|
||||
# M1B.1 -- EXCHANGE: M1 behavior (any plaintext, expect 9000+empty). M2E.x
|
||||
# will rework this leg; until then we keep the M1 shape so the round-trip
|
||||
# exercises both crypto contexts (EXCHANGE + ENVELOPE) under fresh keys.
|
||||
pt = b"\x00"
|
||||
ct = AESGCM(sk_reader).encrypt(_iv_reader(reader_counter), pt, None)
|
||||
# M2E.1 + M2E.2 -- EXCHANGE: ship a Reader Status sub-event REQUEST
|
||||
# ([sub_event_id=0x01, payload_len=0x00]), expect a 19 B encrypted Reader
|
||||
# Status sub-event RESPONSE back ([sub_event_id=0x01, status=0x00,
|
||||
# payload_len=0x00] under SKDevice + deviceIv(1)).
|
||||
request_pt = b"\x01\x00"
|
||||
ct = AESGCM(sk_reader).encrypt(_iv_reader(reader_counter), request_pt, None)
|
||||
apdu = bytes([CLA_PROPRIETARY, INS_EXCHANGE, 0x00, 0x00, len(ct)]) + ct + b"\x00"
|
||||
data, sw = transmit(apdu)
|
||||
if sw != SW_OK:
|
||||
return False, f"M2 EXCHANGE failed: SW=0x{sw:04X}"
|
||||
if len(data) != 0:
|
||||
return False, f"M2 EXCHANGE expected empty body, got {len(data)}B: {data.hex()}"
|
||||
if len(data) != 19:
|
||||
return False, f"M2 EXCHANGE expected 19B response, got {len(data)}B: {data.hex()}"
|
||||
try:
|
||||
exchange_pt = AESGCM(sk_device).decrypt(_iv_device(device_counter), data, None)
|
||||
except Exception as e:
|
||||
return False, f"M2 EXCHANGE response decrypt failed (tag/key mismatch): {e}"
|
||||
if exchange_pt != b"\x01\x00\x00":
|
||||
return False, (
|
||||
"M2 EXCHANGE response plaintext mismatch: expected "
|
||||
f"[0x01, 0x00, 0x00], got {exchange_pt.hex()}"
|
||||
)
|
||||
reader_counter += 1
|
||||
device_counter += 1
|
||||
|
||||
# M2 -- ENVELOPE: ship a valid CBOR DeviceRequest, drain GET RESPONSE
|
||||
# chaining, decrypt, and assert the round-tripped AD.
|
||||
# chaining, decrypt under deviceCounter=2, and assert the round-tripped AD.
|
||||
ct = AESGCM(sk_reader).encrypt(_iv_reader(reader_counter), _DEVICE_REQUEST, None)
|
||||
apdu = bytes([CLA_ISO, INS_ENVELOPE, 0x00, 0x00, len(ct)]) + ct + b"\x00"
|
||||
first_body, first_sw = transmit(apdu)
|
||||
|
||||
Reference in New Issue
Block a user