feat(applet): StructuralCbor.decodeHeader (M2B.1)

This commit is contained in:
michael
2026-06-17 15:41:03 -07:00
parent a94e2b498e
commit da4c3fa1e4
2 changed files with 391 additions and 0 deletions

View File

@@ -0,0 +1,262 @@
package com.dangerousthings.aliro;
import javacard.framework.ISOException;
import javacard.framework.ISO7816;
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 StructuralCbor#decodeHeader} — RFC 8949 §3 header
* decoding (major type + argument) used by the M2 DeviceResponse / IssuerAuth
* walker.
*
* <p>Header byte layout (RFC 8949 §3):
* <pre>
* bits 7..5: major type (0..7)
* bits 4..0: additional info
* 0..23: immediate argument value
* 24: 1-byte uint argument follows
* 25: 2-byte uint argument follows (big-endian)
* 26: 4-byte uint argument follows (big-endian)
* 27: 8-byte uint argument follows (REJECTED — exceeds short range)
* 28..30: reserved (REJECTED)
* 31: indefinite length (REJECTED — Aliro mdoc is canonical)
* </pre>
*
* <p>Result encoding (matches {@link StructuralCbor#decodeHeader} javadoc):
* <ul>
* <li>return value (short): high byte = major type, low byte = bytes consumed
* <li>argument: written big-endian into {@code argOut[argOff..argOff+4)}
* </ul>
*/
class StructuralCborTest {
/** Helper: decode at offset 0 and return major type. */
private static short majorOf(short result) {
return (short) ((result >> 8) & 0x07);
}
/** Helper: decode at offset 0 and return bytes consumed. */
private static short consumedOf(short result) {
return (short) (result & 0xFF);
}
/** Helper: read the 4-byte big-endian argument the codec wrote. */
private static long argOf(byte[] argOut) {
return ((long) (argOut[0] & 0xFF) << 24)
| ((long) (argOut[1] & 0xFF) << 16)
| ((long) (argOut[2] & 0xFF) << 8)
| ((long) (argOut[3] & 0xFF));
}
@Test
void decodeHeader_uintImmediateZero() {
byte[] buf = hex("00");
byte[] arg = new byte[4];
short r = StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0);
assertEquals(0, majorOf(r), "major type 0 (uint)");
assertEquals(1, consumedOf(r), "1 byte consumed");
assertEquals(0L, argOf(arg), "argument value 0");
}
@Test
void decodeHeader_uintImmediateMax() {
// Additional info 23 is the largest immediate-argument value.
byte[] buf = hex("17");
byte[] arg = new byte[4];
short r = StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0);
assertEquals(0, majorOf(r));
assertEquals(1, consumedOf(r));
assertEquals(23L, argOf(arg));
}
@Test
void decodeHeader_uintOneByteArgument() {
// 0x18 = uint with 1-byte argument follow; argument = 0xff (255).
byte[] buf = hex("18ff");
byte[] arg = new byte[4];
short r = StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0);
assertEquals(0, majorOf(r));
assertEquals(2, consumedOf(r));
assertEquals(255L, argOf(arg));
}
@Test
void decodeHeader_uintTwoByteArgument() {
// 0x19 = uint with 2-byte argument; 0x0100 = 256.
byte[] buf = hex("190100");
byte[] arg = new byte[4];
short r = StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0);
assertEquals(0, majorOf(r));
assertEquals(3, consumedOf(r));
assertEquals(256L, argOf(arg));
}
@Test
void decodeHeader_uintFourByteArgument() {
// 0x1a = uint with 4-byte argument; 0x00010000 = 65536.
byte[] buf = hex("1a00010000");
byte[] arg = new byte[4];
short r = StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0);
assertEquals(0, majorOf(r));
assertEquals(5, consumedOf(r));
assertEquals(65536L, argOf(arg));
}
@Test
void decodeHeader_bstrOneByteLen() {
// 0x58 = bstr major type (2) + additional info 24 (1-byte len follows).
// Length 200 won't overflow short.
byte[] buf = hex("58c8");
byte[] arg = new byte[4];
short r = StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0);
assertEquals(2, majorOf(r), "major type 2 (bstr)");
assertEquals(2, consumedOf(r));
assertEquals(200L, argOf(arg));
}
@Test
void decodeHeader_tstrImmediate() {
// 0x65 = tstr major type (3) + immediate length 5.
byte[] buf = hex("65");
byte[] arg = new byte[4];
short r = StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0);
assertEquals(3, majorOf(r), "major type 3 (tstr)");
assertEquals(1, consumedOf(r));
assertEquals(5L, argOf(arg));
}
@Test
void decodeHeader_arrayImmediate() {
// 0x83 = array major type (4) + immediate count 3.
byte[] buf = hex("83");
byte[] arg = new byte[4];
short r = StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0);
assertEquals(4, majorOf(r), "major type 4 (array)");
assertEquals(1, consumedOf(r));
assertEquals(3L, argOf(arg));
}
@Test
void decodeHeader_mapImmediate() {
// 0xa2 = map major type (5) + immediate count 2.
byte[] buf = hex("a2");
byte[] arg = new byte[4];
short r = StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0);
assertEquals(5, majorOf(r), "major type 5 (map)");
assertEquals(1, consumedOf(r));
assertEquals(2L, argOf(arg));
}
@Test
void decodeHeader_tagImmediate() {
// 0xc0 = tag major type (6) + immediate tag 0.
byte[] buf = hex("c0");
byte[] arg = new byte[4];
short r = StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0);
assertEquals(6, majorOf(r), "major type 6 (tag)");
assertEquals(1, consumedOf(r));
assertEquals(0L, argOf(arg));
}
@Test
void decodeHeader_atNonzeroOffset() {
// Verify decodeHeader honours bufOff: pad with junk, decode at offset 3.
byte[] buf = hex("deadbeef" + "190100");
byte[] arg = new byte[4];
short r = StructuralCbor.decodeHeader(buf, (short) 4, (short) (buf.length - 4), arg, (short) 0);
assertEquals(0, majorOf(r));
assertEquals(3, consumedOf(r));
assertEquals(256L, argOf(arg));
}
@Test
void decodeHeader_argOutAtNonzeroOffset() {
// Verify argOff: write argument into arg[2..6) and leave arg[0..2) zero.
byte[] buf = hex("190100");
byte[] arg = new byte[8];
short r = StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 2);
assertEquals(0, majorOf(r));
assertEquals(3, consumedOf(r));
assertEquals(0, arg[0]);
assertEquals(0, arg[1]);
// Argument bytes land at arg[2..6).
long argVal = ((long) (arg[2] & 0xFF) << 24)
| ((long) (arg[3] & 0xFF) << 16)
| ((long) (arg[4] & 0xFF) << 8)
| ((long) (arg[5] & 0xFF));
assertEquals(256L, argVal);
}
@Test
void decodeHeader_indefiniteLengthRejected() {
// 0x1f = uint major type + additional info 31 (indefinite length).
// We refuse indefinite-length in Aliro mdoc — canonical CBOR only.
byte[] buf = hex("1f");
byte[] arg = new byte[4];
ISOException ex = assertThrows(ISOException.class,
() -> StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0));
assertEquals(ISO7816.SW_DATA_INVALID, ex.getReason() & 0xFFFF,
"indefinite-length must throw SW_DATA_INVALID");
}
@Test
void decodeHeader_eightByteArgumentRejected() {
// 0x1b = uint with 8-byte argument follow. Out of short-buffer scope.
byte[] buf = hex("1b00000000000000ff");
byte[] arg = new byte[4];
ISOException ex = assertThrows(ISOException.class,
() -> StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0));
assertEquals(ISO7816.SW_DATA_INVALID, ex.getReason() & 0xFFFF);
}
@Test
void decodeHeader_majorTypeSevenRejected() {
// 0xf5 = major type 7 (CBOR true). We don't handle floats/null/bool.
byte[] buf = hex("f5");
byte[] arg = new byte[4];
ISOException ex = assertThrows(ISOException.class,
() -> StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0));
assertEquals(ISO7816.SW_DATA_INVALID, ex.getReason() & 0xFFFF);
}
@Test
void decodeHeader_reservedAdditionalInfoRejected() {
// 0x1c = additional info 28 (reserved by RFC 8949).
byte[] buf = hex("1c");
byte[] arg = new byte[4];
ISOException ex = assertThrows(ISOException.class,
() -> StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0));
assertEquals(ISO7816.SW_DATA_INVALID, ex.getReason() & 0xFFFF);
}
@Test
void decodeHeader_truncatedArgumentRejected() {
// 0x19 says "2-byte argument follows" but only 1 byte is present.
byte[] buf = hex("1901");
byte[] arg = new byte[4];
ISOException ex = assertThrows(ISOException.class,
() -> StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0));
assertEquals(ISO7816.SW_DATA_INVALID, ex.getReason() & 0xFFFF);
}
@Test
void decodeHeader_emptyBufferRejected() {
byte[] buf = new byte[0];
byte[] arg = new byte[4];
ISOException ex = assertThrows(ISOException.class,
() -> StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0));
assertEquals(ISO7816.SW_DATA_INVALID, ex.getReason() & 0xFFFF);
}
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;
}
}