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
|
||||
* (see {@link PersonalizationApplet} and {@link CoseVerifier}).
|
||||
*
|
||||
* <p>M2B.1 implements {@link #decodeHeader} only — element span walking,
|
||||
* IssuerAuth location, and canonical encoders land in M2B.2/M2B.3/M2B.4.
|
||||
* <p>M2B.1 implements {@link #decodeHeader}; M2B.2 adds {@link #elementSpan}.
|
||||
* IssuerAuth location and canonical encoders land in M2B.3/M2B.4.
|
||||
*/
|
||||
final class StructuralCbor {
|
||||
|
||||
@@ -126,4 +126,133 @@ final class StructuralCbor {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user