feat(applet): StructuralCbor.elementSpan (M2B.2)
This commit is contained in:
@@ -23,8 +23,8 @@ import javacard.framework.ISOException;
|
|||||||
* matching the codebase's "fail closed on malformed input" convention
|
* matching the codebase's "fail closed on malformed input" convention
|
||||||
* (see {@link PersonalizationApplet} and {@link CoseVerifier}).
|
* (see {@link PersonalizationApplet} and {@link CoseVerifier}).
|
||||||
*
|
*
|
||||||
* <p>M2B.1 implements {@link #decodeHeader} only — element span walking,
|
* <p>M2B.1 implements {@link #decodeHeader}; M2B.2 adds {@link #elementSpan}.
|
||||||
* IssuerAuth location, and canonical encoders land in M2B.2/M2B.3/M2B.4.
|
* IssuerAuth location and canonical encoders land in M2B.3/M2B.4.
|
||||||
*/
|
*/
|
||||||
final class StructuralCbor {
|
final class StructuralCbor {
|
||||||
|
|
||||||
@@ -126,4 +126,133 @@ final class StructuralCbor {
|
|||||||
|
|
||||||
return (short) ((major << 8) | consumed);
|
return (short) ((major << 8) | consumed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the total byte length of the CBOR data item that begins at
|
||||||
|
* {@code buf[bufOff]}, recursing into arrays / maps / tags so the result
|
||||||
|
* spans the complete (possibly nested) element.
|
||||||
|
*
|
||||||
|
* <p>By major type (per RFC 8949 §3):
|
||||||
|
* <ul>
|
||||||
|
* <li>0 (uint), 1 (nint): span = header bytes consumed</li>
|
||||||
|
* <li>2 (bstr), 3 (tstr): span = header + payload length (argument)</li>
|
||||||
|
* <li>4 (array): span = header + sum(elementSpan of {@code argument} children)</li>
|
||||||
|
* <li>5 (map): span = header + sum(elementSpan of {@code 2*argument} items)</li>
|
||||||
|
* <li>6 (tag): span = header + elementSpan of the single tagged element</li>
|
||||||
|
* <li>7: unreachable — {@link #decodeHeader} already rejects it.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Implementation is recursive. Worst-case Aliro mdoc nesting depth is
|
||||||
|
* ~5 (DeviceResponse map → documents array → entry map → issuerSigned map
|
||||||
|
* → issuerAuth array), so the JC stack budget is comfortable. Length /
|
||||||
|
* count fields use the 4B big-endian argument written into
|
||||||
|
* {@code argScratch[argScratchOff..argScratchOff+4)} by {@code decodeHeader}.
|
||||||
|
* The scratch is overwritten at every recursive descent — that's fine
|
||||||
|
* because the parent has already consumed its argument before recursing.
|
||||||
|
*
|
||||||
|
* @param buf buffer holding the CBOR stream
|
||||||
|
* @param bufOff offset of the first byte of the element
|
||||||
|
* @param bufLen number of valid bytes remaining at {@code bufOff}
|
||||||
|
* @param argScratch 4-byte scratch for {@code decodeHeader}'s argument
|
||||||
|
* @param argScratchOff offset within {@code argScratch} (4 bytes needed)
|
||||||
|
* @return total bytes the element (including children) occupies
|
||||||
|
* @throws ISOException SW_DATA_INVALID on malformed input — either
|
||||||
|
* {@link #decodeHeader} rejected a header, the
|
||||||
|
* declared bstr / tstr payload runs past {@code bufLen},
|
||||||
|
* or a child element runs past the parent's bounds.
|
||||||
|
*/
|
||||||
|
static short elementSpan(
|
||||||
|
byte[] buf, short bufOff, short bufLen,
|
||||||
|
byte[] argScratch, short argScratchOff) {
|
||||||
|
|
||||||
|
short header = decodeHeader(buf, bufOff, bufLen, argScratch, argScratchOff);
|
||||||
|
short major = (short) ((header >> 8) & 0x07);
|
||||||
|
short consumed = (short) (header & 0xFF);
|
||||||
|
|
||||||
|
// 4-byte big-endian read of the argument decodeHeader just wrote.
|
||||||
|
// Aliro mdoc payloads fit in a short-sized buffer, so we cap argument
|
||||||
|
// values at Short.MAX_VALUE when used as a length / count; any larger
|
||||||
|
// value is malformed input and rejected.
|
||||||
|
short argument = readArgAsShort(argScratch, argScratchOff);
|
||||||
|
|
||||||
|
// Span starts with the header bytes; children (if any) extend it.
|
||||||
|
short span = consumed;
|
||||||
|
|
||||||
|
if (major == 0 || major == 1) {
|
||||||
|
// uint / nint — header is the whole element.
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
if (major == 2 || major == 3) {
|
||||||
|
// bstr / tstr — header + payload bytes.
|
||||||
|
short total = (short) (span + argument);
|
||||||
|
// Reject if the declared payload extends past bufLen, otherwise a
|
||||||
|
// truncated bstr would silently report a span larger than the
|
||||||
|
// available buffer and confuse later callers.
|
||||||
|
if (argument < 0 || total < 0 || total > bufLen) {
|
||||||
|
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
if (major == 4) {
|
||||||
|
// Array of `argument` children.
|
||||||
|
for (short i = 0; i < argument; i++) {
|
||||||
|
short childOff = (short) (bufOff + span);
|
||||||
|
short childLen = (short) (bufLen - span);
|
||||||
|
if (childLen <= 0) {
|
||||||
|
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
|
||||||
|
}
|
||||||
|
short childSpan = elementSpan(
|
||||||
|
buf, childOff, childLen, argScratch, argScratchOff);
|
||||||
|
span = (short) (span + childSpan);
|
||||||
|
}
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
if (major == 5) {
|
||||||
|
// Map of `argument` key/value pairs — 2 * argument items.
|
||||||
|
short items = (short) (argument << 1);
|
||||||
|
for (short i = 0; i < items; i++) {
|
||||||
|
short childOff = (short) (bufOff + span);
|
||||||
|
short childLen = (short) (bufLen - span);
|
||||||
|
if (childLen <= 0) {
|
||||||
|
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
|
||||||
|
}
|
||||||
|
short childSpan = elementSpan(
|
||||||
|
buf, childOff, childLen, argScratch, argScratchOff);
|
||||||
|
span = (short) (span + childSpan);
|
||||||
|
}
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
// major == 6 (tag) — header + single tagged element.
|
||||||
|
short childOff = (short) (bufOff + span);
|
||||||
|
short childLen = (short) (bufLen - span);
|
||||||
|
if (childLen <= 0) {
|
||||||
|
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
|
||||||
|
}
|
||||||
|
short childSpan = elementSpan(
|
||||||
|
buf, childOff, childLen, argScratch, argScratchOff);
|
||||||
|
return (short) (span + childSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the 4-byte big-endian argument {@code decodeHeader} wrote into
|
||||||
|
* the scratch and returns it as a short. Argument values larger than
|
||||||
|
* {@code Short.MAX_VALUE} (32767) are out of buffer range for Aliro mdoc
|
||||||
|
* payloads and rejected as malformed input.
|
||||||
|
*/
|
||||||
|
private static short readArgAsShort(byte[] scratch, short off) {
|
||||||
|
short hi = (short) (((scratch[off] & 0xFF) << 8)
|
||||||
|
| (scratch[(short) (off + 1)] & 0xFF));
|
||||||
|
if (hi != 0) {
|
||||||
|
// 32-bit argument with any of the upper 16 bits set won't fit
|
||||||
|
// a short used as length / count.
|
||||||
|
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
|
||||||
|
}
|
||||||
|
short v = (short) (((scratch[(short) (off + 2)] & 0xFF) << 8)
|
||||||
|
| (scratch[(short) (off + 3)] & 0xFF));
|
||||||
|
if (v < 0) {
|
||||||
|
// Top bit set = value > Short.MAX_VALUE.
|
||||||
|
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -251,6 +251,115 @@ class StructuralCborTest {
|
|||||||
assertEquals(ISO7816.SW_DATA_INVALID, ex.getReason() & 0xFFFF);
|
assertEquals(ISO7816.SW_DATA_INVALID, ex.getReason() & 0xFFFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// elementSpan tests (M2B.2) — span of a complete CBOR element including
|
||||||
|
// nested children, used by M2B.3 / M2D.1 to walk IssuerSigned / docRequests.
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Shared 4B scratch for elementSpan's decodeHeader calls. */
|
||||||
|
private final byte[] scratch = new byte[4];
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void elementSpan_uintImmediate() {
|
||||||
|
// 0x17 = uint 23, immediate argument. One header byte, no payload.
|
||||||
|
byte[] buf = hex("17");
|
||||||
|
short span = StructuralCbor.elementSpan(
|
||||||
|
buf, (short) 0, (short) buf.length, scratch, (short) 0);
|
||||||
|
assertEquals(1, span);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void elementSpan_uintOneByteArgument() {
|
||||||
|
// 0x18 0x64 = uint 100. Header byte + 1B argument, no payload.
|
||||||
|
byte[] buf = hex("1864");
|
||||||
|
short span = StructuralCbor.elementSpan(
|
||||||
|
buf, (short) 0, (short) buf.length, scratch, (short) 0);
|
||||||
|
assertEquals(2, span);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void elementSpan_bstrEmpty() {
|
||||||
|
// 0x40 = bstr len 0. One header byte, zero payload.
|
||||||
|
byte[] buf = hex("40");
|
||||||
|
short span = StructuralCbor.elementSpan(
|
||||||
|
buf, (short) 0, (short) buf.length, scratch, (short) 0);
|
||||||
|
assertEquals(1, span);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void elementSpan_bstrLen3() {
|
||||||
|
// 0x43 0x01 0x02 0x03 = bstr h'010203'. Header + 3 payload bytes.
|
||||||
|
byte[] buf = hex("43010203");
|
||||||
|
short span = StructuralCbor.elementSpan(
|
||||||
|
buf, (short) 0, (short) buf.length, scratch, (short) 0);
|
||||||
|
assertEquals(4, span);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void elementSpan_tstrHello() {
|
||||||
|
// 0x65 + "hello" = tstr "hello". Header + 5 payload bytes.
|
||||||
|
byte[] buf = hex("6568656c6c6f");
|
||||||
|
short span = StructuralCbor.elementSpan(
|
||||||
|
buf, (short) 0, (short) buf.length, scratch, (short) 0);
|
||||||
|
assertEquals(6, span);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void elementSpan_arrayOfThreeUints() {
|
||||||
|
// 0x83 0x01 0x02 0x03 = [1, 2, 3]. Header + 3 single-byte children.
|
||||||
|
byte[] buf = hex("83010203");
|
||||||
|
short span = StructuralCbor.elementSpan(
|
||||||
|
buf, (short) 0, (short) buf.length, scratch, (short) 0);
|
||||||
|
assertEquals(4, span);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void elementSpan_nestedArray() {
|
||||||
|
// 0x82 0x82 0x01 0x02 0x03 = [[1, 2], 3]. Exercises recursion.
|
||||||
|
byte[] buf = hex("8282010203");
|
||||||
|
short span = StructuralCbor.elementSpan(
|
||||||
|
buf, (short) 0, (short) buf.length, scratch, (short) 0);
|
||||||
|
assertEquals(5, span);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void elementSpan_mapTwoUintPairs() {
|
||||||
|
// 0xa2 0x01 0x02 0x03 0x04 = {1: 2, 3: 4}. Header + 4 single-byte items.
|
||||||
|
byte[] buf = hex("a201020304");
|
||||||
|
short span = StructuralCbor.elementSpan(
|
||||||
|
buf, (short) 0, (short) buf.length, scratch, (short) 0);
|
||||||
|
assertEquals(5, span);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void elementSpan_mapWithBstrUintPairs() {
|
||||||
|
// 0xa2 0x43 010203 0x1864 0x41 ff 0x17
|
||||||
|
// = { h'010203': 100, h'ff': 23 }
|
||||||
|
// Header(1) + bstr(4) + uint(2) + bstr(2) + uint(1) = 10.
|
||||||
|
byte[] buf = hex("a2 43 010203 1864 41 ff 17");
|
||||||
|
short span = StructuralCbor.elementSpan(
|
||||||
|
buf, (short) 0, (short) buf.length, scratch, (short) 0);
|
||||||
|
assertEquals(10, span);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void elementSpan_tagUint() {
|
||||||
|
// 0xc1 0x01 = tag(1, 1). Tag header + tagged element span.
|
||||||
|
byte[] buf = hex("c101");
|
||||||
|
short span = StructuralCbor.elementSpan(
|
||||||
|
buf, (short) 0, (short) buf.length, scratch, (short) 0);
|
||||||
|
assertEquals(2, span);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void elementSpan_honoursBufOff() {
|
||||||
|
// Pad with 4 junk bytes, then a 4-byte array [1,2,3]; span starts at offset 4.
|
||||||
|
byte[] buf = hex("deadbeef" + "83010203");
|
||||||
|
short span = StructuralCbor.elementSpan(
|
||||||
|
buf, (short) 4, (short) (buf.length - 4), scratch, (short) 0);
|
||||||
|
assertEquals(4, span);
|
||||||
|
}
|
||||||
|
|
||||||
private static byte[] hex(String s) {
|
private static byte[] hex(String s) {
|
||||||
s = s.replaceAll("\\s+", "");
|
s = s.replaceAll("\\s+", "");
|
||||||
byte[] out = new byte[s.length() / 2];
|
byte[] out = new byte[s.length() / 2];
|
||||||
|
|||||||
Reference in New Issue
Block a user