feat(applet): StructuralCbor.decodeHeader (M2B.1)
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
package com.dangerousthings.aliro;
|
||||
|
||||
import javacard.framework.ISO7816;
|
||||
import javacard.framework.ISOException;
|
||||
|
||||
/**
|
||||
* Structural CBOR utilities for the Aliro Access Document / DeviceResponse
|
||||
* walk. CBOR is defined by RFC 8949; we implement only the canonical subset
|
||||
* actually used by Aliro / ISO 18013-5 mdoc messages:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Major types 0..6 (uint, nint, bstr, tstr, array, map, tag) — major
|
||||
* type 7 (floats / null / true / false) is rejected.</li>
|
||||
* <li>Argument sizes immediate (0..23), 1-byte, 2-byte, 4-byte — the
|
||||
* 8-byte form is rejected because Aliro mdoc payloads fit comfortably
|
||||
* in a short-bounded buffer and supporting it would force long
|
||||
* arithmetic across the rest of the codec.</li>
|
||||
* <li>Definite lengths only — indefinite-length (additional info 31) is
|
||||
* rejected; canonical Aliro mdoc encoding never uses it.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>All rejection paths throw {@code ISOException(SW_DATA_INVALID = 0x6984)},
|
||||
* 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.
|
||||
*/
|
||||
final class StructuralCbor {
|
||||
|
||||
private StructuralCbor() {
|
||||
// Utility class — no instances.
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a single CBOR header at {@code buf[bufOff..bufOff+bufLen)} per
|
||||
* RFC 8949 §3.
|
||||
*
|
||||
* <p>The header byte's top 3 bits are the major type and the bottom 5
|
||||
* bits are the additional info; values 0..23 are the immediate argument,
|
||||
* 24/25/26 mean a 1/2/4-byte big-endian uint argument follows. All
|
||||
* other additional-info values are rejected.
|
||||
*
|
||||
* <p>Result encoding (single short, lazy):
|
||||
* <ul>
|
||||
* <li>high byte = major type (0..6)</li>
|
||||
* <li>low byte = bytes consumed (1, 2, 3, or 5)</li>
|
||||
* </ul>
|
||||
* The argument value is written big-endian into
|
||||
* {@code argOut[argOff..argOff+4)}. For arguments smaller than 4 bytes
|
||||
* the result is zero-extended at the high end (so the caller can always
|
||||
* read 4 bytes BE and get the correct integer).
|
||||
*
|
||||
* @param buf buffer holding the CBOR stream
|
||||
* @param bufOff offset of the header byte
|
||||
* @param bufLen number of valid bytes remaining at {@code bufOff}
|
||||
* @param argOut destination for the 4-byte big-endian argument
|
||||
* @param argOff offset within {@code argOut} to write the argument
|
||||
* @return packed (major type << 8) | bytesConsumed
|
||||
* @throws ISOException SW_DATA_INVALID on any of: empty buffer, truncated
|
||||
* argument, major type 7, indefinite length, reserved
|
||||
* additional info (28..30), or 8-byte argument (27).
|
||||
*/
|
||||
static short decodeHeader(
|
||||
byte[] buf, short bufOff, short bufLen,
|
||||
byte[] argOut, short argOff) {
|
||||
|
||||
if (bufLen < 1) {
|
||||
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
|
||||
}
|
||||
|
||||
byte b0 = buf[bufOff];
|
||||
short major = (short) ((b0 >> 5) & 0x07);
|
||||
short addInfo = (short) (b0 & 0x1F);
|
||||
|
||||
// Major type 7 (floats / null / true / false / break) — out of
|
||||
// scope for Aliro mdoc which uses only the data-bearing major
|
||||
// types 0..6.
|
||||
if (major == 7) {
|
||||
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
|
||||
}
|
||||
|
||||
// Zero-extend the 4-byte big-endian argument slot up front. For
|
||||
// immediate / 1B / 2B arguments the upper bytes must read as 0.
|
||||
argOut[argOff] = (byte) 0;
|
||||
argOut[(short) (argOff + 1)] = (byte) 0;
|
||||
argOut[(short) (argOff + 2)] = (byte) 0;
|
||||
argOut[(short) (argOff + 3)] = (byte) 0;
|
||||
|
||||
short consumed;
|
||||
if (addInfo <= 23) {
|
||||
// Immediate argument: the additional-info bits ARE the value.
|
||||
argOut[(short) (argOff + 3)] = (byte) addInfo;
|
||||
consumed = 1;
|
||||
} else if (addInfo == 24) {
|
||||
// 1-byte uint argument follows.
|
||||
if (bufLen < 2) {
|
||||
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
|
||||
}
|
||||
argOut[(short) (argOff + 3)] = buf[(short) (bufOff + 1)];
|
||||
consumed = 2;
|
||||
} else if (addInfo == 25) {
|
||||
// 2-byte big-endian uint argument follows.
|
||||
if (bufLen < 3) {
|
||||
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
|
||||
}
|
||||
argOut[(short) (argOff + 2)] = buf[(short) (bufOff + 1)];
|
||||
argOut[(short) (argOff + 3)] = buf[(short) (bufOff + 2)];
|
||||
consumed = 3;
|
||||
} else if (addInfo == 26) {
|
||||
// 4-byte big-endian uint argument follows.
|
||||
if (bufLen < 5) {
|
||||
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
|
||||
}
|
||||
argOut[argOff] = buf[(short) (bufOff + 1)];
|
||||
argOut[(short) (argOff + 1)] = buf[(short) (bufOff + 2)];
|
||||
argOut[(short) (argOff + 2)] = buf[(short) (bufOff + 3)];
|
||||
argOut[(short) (argOff + 3)] = buf[(short) (bufOff + 4)];
|
||||
consumed = 5;
|
||||
} else {
|
||||
// additional info 27 (8-byte arg), 28..30 (reserved), 31
|
||||
// (indefinite length) — all rejected.
|
||||
ISOException.throwIt(ISO7816.SW_DATA_INVALID);
|
||||
return 0; // unreachable; satisfies javac.
|
||||
}
|
||||
|
||||
return (short) ((major << 8) | consumed);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user