feat(applet): DeviceRequestParser structural validation (M2D.1)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user