package com.dangerousthings.aliro; import org.junit.jupiter.api.Test; import java.util.Arrays; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; /** * Tests for {@link DeviceResponseBuilder} — emits the mdoc DeviceResponse * (ISO 18013-5 §8.3.2.1.2.2 / Aliro Table 8-22) carrying a cached Access * Document at {@code documents[0].issuerSigned.issuerAuth}. * *

The reference {@link #DEVICE_RESPONSE_HEX} was generated against the * real Access Document at {@code /home/work/aliro-trust/access_document.bin} * via a manual canonical-CBOR builder (NOT {@code cbor2.dumps(canonical=True)} * on the whole structure — that would re-encode the inner COSE_Sign1 and the * applet doesn't re-encode it; the AD bytes are embedded verbatim). See the * M2D.2 plan for the generator script. * *

Canonical key ordering at each map level (RFC 8949 §4.2.1: sort by * encoded-key-length then lexicographically): *

*/ class DeviceResponseBuilderTest { /** * Real Access Document from {@code /home/work/aliro-trust/access_document.bin} * — a 272-byte COSE_Sign1 (4-element array, tag-less per Aliro Table 8-22's * literal "COSE_Sign1" embedding). */ private static final byte[] ACCESS_DOC_HEX = hex( "8443a10126a104488dae9624eed9280c58bca7613163312e30613267534" + "8412d3235366133a06134a16131a401022001215820aa3115ead5d1fec" + "ca289aef3598790a6dba23edbe9b14e6818ac683e31a5af0222582083" + "9dcc32bcd32924a942c3b9999f6cbf46960396e69606fe4295e83a0c7" + "787a2613567616c69726f2d616136a36131c074323032362d30342d3" + "1395432303a32393a31365a6132c074323032362d30342d3139543230" + "3a32393a31365a6133c074323032372d30342d31395432303a32393a3" + "1365a6137f458404927c33ec9c475768b269bb4ee2a098be8d64ac436" + "44e92c8106d9c537d6215dc00e131e4ecf00b37ebc6ac8c26210f939d" + "38df6f1c5b1caf685c365b22a3f2a"); /** * Expected DeviceResponse bytes for {@link #ACCESS_DOC_HEX}. 372 bytes * total (272 AD + 100 B wrapper). */ private static final byte[] DEVICE_RESPONSE_HEX = hex( "a366737461747573006776657273696f6e63312e3069646f63756d656e" + "747381a267646f6354797065756f72672e69736f2e31383031332e352e" + "312e6d444c6c6973737565725369676e6564a26a697373756572417574" + "688443a10126a104488dae9624eed9280c58bca7613163312e30613267" + "5348412d3235366133a06134a16131a401022001215820aa3115ead5d1" + "fecca289aef3598790a6dba23edbe9b14e6818ac683e31a5af02225820" + "839dcc32bcd32924a942c3b9999f6cbf46960396e69606fe4295e83a0c" + "7787a2613567616c69726f2d616136a36131c074323032362d30342d31" + "395432303a32393a31365a6132c074323032362d30342d31395432303a" + "32393a31365a6133c074323032372d30342d31395432303a32393a3136" + "5a6137f458404927c33ec9c475768b269bb4ee2a098be8d64ac43644e9" + "2c8106d9c537d6215dc00e131e4ecf00b37ebc6ac8c26210f939d38df6" + "f1c5b1caf685c365b22a3f2a6a6e616d65537061636573a0"); @Test void buildDeviceResponse_outputBytes_matchPythonReference() { // Buffer sized generously — actual is 372 B for a 272 B AD. byte[] out = new byte[ACCESS_DOC_HEX.length + 128]; short written = DeviceResponseBuilder.build( ACCESS_DOC_HEX, (short) 0, (short) ACCESS_DOC_HEX.length, out, (short) 0); assertEquals(DEVICE_RESPONSE_HEX.length, written, "DeviceResponse byte count must match Python canonical-CBOR reference"); assertArrayEquals( DEVICE_RESPONSE_HEX, Arrays.copyOfRange(out, 0, written), "DeviceResponse bytes must match Python canonical-CBOR reference byte-for-byte"); } /** * Decodes the builder output and confirms the spec shape: top-level * 3-entry map, single document with docType + issuerSigned, issuerSigned * has empty nameSpaces and the AD embedded verbatim at issuerAuth. * *

Uses {@link StructuralCbor#elementSpan} to walk — that's the same * tooling M2D.3 will use on the receiving side, so a passing structural * walk here also confirms the output is parseable by our own parser. */ @Test void buildDeviceResponse_shape_matchesAliroTable8_22() { byte[] out = new byte[ACCESS_DOC_HEX.length + 128]; short written = DeviceResponseBuilder.build( ACCESS_DOC_HEX, (short) 0, (short) ACCESS_DOC_HEX.length, out, (short) 0); byte[] scratch = new byte[4]; // Whole-response span must equal `written` — no trailing garbage. short span = StructuralCbor.elementSpan( out, (short) 0, written, scratch, (short) 0); assertEquals(written, span, "DeviceResponse span must consume exactly the written bytes"); // Top-level: map(3) — major type 5, additional info 3. assertEquals((byte) 0xA3, out[0], "top-level must be a 3-entry CBOR map (0xA3)"); // Locate the AD inside the output and confirm it's byte-identical. // Brute-force scan for the AD's first byte (0x84 — COSE_Sign1 array // header) and verify the run matches. Only one such run should exist // because the wrapper itself doesn't contain that byte sequence. int adOffset = indexOf(out, 0, written, ACCESS_DOC_HEX); assertEquals(true, adOffset >= 0, "Access Document bytes must appear verbatim somewhere in the output"); assertArrayEquals( ACCESS_DOC_HEX, Arrays.copyOfRange(out, adOffset, adOffset + ACCESS_DOC_HEX.length), "Embedded Access Document must be byte-identical to the input"); } /** Naive byte-array indexOf — sufficient for the small test buffers. */ private static int indexOf(byte[] haystack, int from, int to, byte[] needle) { outer: for (int i = from; i <= to - needle.length; i++) { for (int j = 0; j < needle.length; j++) { if (haystack[i + j] != needle[j]) continue outer; } return i; } return -1; } 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; } }