feat(applet): DeviceRequestParser structural validation (M2D.1)

This commit is contained in:
michael
2026-06-17 16:19:12 -07:00
parent d5945be60e
commit a7bc7e4478
2 changed files with 408 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
package com.dangerousthings.aliro;
import javacard.framework.ISO7816;
import javacard.framework.ISOException;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
/**
* Tests for {@link DeviceRequestParser} — structural validation of the inbound
* mdoc DeviceRequest (ISO 18013-5 §8.3.2.1).
*
* <p>M2D's ENVELOPE handler receives a DeviceRequest, must confirm its shape
* before discarding the contents (we always return our single Access Document),
* and reject malformed input with the canonical SW codes:
* <ul>
* <li>{@code SW_DATA_INVALID (0x6984)} — malformed CBOR or wrong shape</li>
* <li>{@code SW_CONDITIONS_NOT_SATISFIED (0x6985)} — unsupported version</li>
* </ul>
*
* <p>Test vectors were generated via cbor2 (canonical encoding); see the
* generator script in the M2D.1 plan. The Python equivalents:
* <pre>
* VALID = cbor2.dumps({"version":"1.0","docRequests":[{"itemsRequest":cbor2.dumps({"docType":"org.iso.18013.5.1.mDL","nameSpaces":{}})}]}, canonical=True)
* BAD_VERSION = cbor2.dumps({"version":"2.0","docRequests":[{"itemsRequest":b"\x40"}]}, canonical=True)
* TRUNCATED = VALID[:8]
* </pre>
*/
class DeviceRequestParserTest {
/**
* Canonical CBOR for a minimal-but-valid DeviceRequest:
* {@code {"version":"1.0","docRequests":[{"itemsRequest":<bstr>}]}}
* with itemsRequest containing an inner CBOR
* {@code {"docType":"org.iso.18013.5.1.mDL","nameSpaces":{}}}.
*/
private static final byte[] VALID = hex(
"a26776657273696f6e63312e306b646f63526571756573747381"
+ "a16c6974656d7352657175657374582ba267646f6354797065"
+ "756f72672e69736f2e31383031332e352e312e6d444c"
+ "6a6e616d65537061636573a0");
/** Same shape as VALID but version tstr value is "2.0". */
private static final byte[] BAD_VERSION = hex(
"a26776657273696f6e63322e306b646f63526571756573747381"
+ "a16c6974656d73526571756573744140");
/** First 8 bytes of VALID — truncated mid-key, malformed CBOR. */
private static final byte[] TRUNCATED = hex("a26776657273696f");
@Test
void parseDeviceRequest_validShape_returnsOk() {
byte[] scratch = new byte[4];
// Should not throw.
DeviceRequestParser.validate(
VALID, (short) 0, (short) VALID.length,
scratch, (short) 0);
}
@Test
void parseDeviceRequest_unknownVersion_throwsConditionsNotSatisfied() {
byte[] scratch = new byte[4];
ISOException ex = assertThrows(ISOException.class, () ->
DeviceRequestParser.validate(
BAD_VERSION, (short) 0, (short) BAD_VERSION.length,
scratch, (short) 0));
assertEquals(ISO7816.SW_CONDITIONS_NOT_SATISFIED, ex.getReason(),
"unknown version must throw SW_CONDITIONS_NOT_SATISFIED (0x6985)");
}
@Test
void parseDeviceRequest_malformedCbor_throwsDataInvalid() {
byte[] scratch = new byte[4];
ISOException ex = assertThrows(ISOException.class, () ->
DeviceRequestParser.validate(
TRUNCATED, (short) 0, (short) TRUNCATED.length,
scratch, (short) 0));
assertEquals(ISO7816.SW_DATA_INVALID, ex.getReason(),
"truncated CBOR must throw SW_DATA_INVALID (0x6984)");
}
private static byte[] hex(String s) {
s = s.replaceAll("\\s+", "");
byte[] out = new byte[s.length() / 2];
for (int i = 0; i < out.length; i++) {
out[i] = (byte) Integer.parseInt(s.substring(i * 2, i * 2 + 2), 16);
}
return out;
}
}