refactor(applet): CoseVerifier uses StructuralCbor (M2B.3)
This commit is contained in:
@@ -24,17 +24,13 @@ import javacard.security.Signature;
|
|||||||
* the JC {@code Signature.ALG_ECDSA_SHA_256} primitive expects DER, so
|
* the JC {@code Signature.ALG_ECDSA_SHA_256} primitive expects DER, so
|
||||||
* we transcode raw → DER before {@link Signature#verify}.
|
* we transcode raw → DER before {@link Signature#verify}.
|
||||||
*
|
*
|
||||||
* <p>For now (M2A.2) the CBOR walk uses hardcoded offsets that assume the
|
* <p>The CBOR walk uses {@link StructuralCbor#decodeHeader} +
|
||||||
* exact wire shape produced by {@code aliro_harness.issuer.cose}:
|
* {@link StructuralCbor#elementSpan} for the 4-element COSE_Sign1 array:
|
||||||
* <ul>
|
* decode the outer array header, decode each bstr header at elements 0/2/3
|
||||||
* <li>outer array(4) tag = 0x84 at byte 0
|
* (protected / payload / signature) to capture value offsets, and span-skip
|
||||||
* <li>protected_bstr is a single-byte-length bstr at byte 1
|
* the unprotected map at element 1. Any malformed input throws
|
||||||
* <li>unprotected map is map(1) with kid bstr value
|
* {@code ISOException(SW_DATA_INVALID)} which the outer try/catch collapses
|
||||||
* <li>payload_bstr is either tiny (header 0x40+len) or 1-byte len (0x58 + len)
|
* to "not verified".
|
||||||
* <li>signature_bstr is 0x58 0x40 + 64 bytes
|
|
||||||
* </ul>
|
|
||||||
* M2B.3 will replace these hardcoded offsets with
|
|
||||||
* {@code StructuralCbor.elementSpan} for true structural walking.
|
|
||||||
*
|
*
|
||||||
* <p>The verifier owns a reusable {@link ECPublicKey} slot, seeded with
|
* <p>The verifier owns a reusable {@link ECPublicKey} slot, seeded with
|
||||||
* P-256 curve params at construction. Each call calls {@code setW} with
|
* P-256 curve params at construction. Each call calls {@code setW} with
|
||||||
@@ -129,65 +125,60 @@ final class CoseVerifier {
|
|||||||
private boolean verifyInternal(
|
private boolean verifyInternal(
|
||||||
byte[] coseSign1, short coseOff, short coseLen,
|
byte[] coseSign1, short coseOff, short coseLen,
|
||||||
byte[] issuerPubUncomp, short pubOff) {
|
byte[] issuerPubUncomp, short pubOff) {
|
||||||
short end = (short) (coseOff + coseLen);
|
|
||||||
|
|
||||||
// TODO M2B.3: replace with StructuralCbor.elementSpan — the offset
|
|
||||||
// arithmetic below assumes the exact wire shape produced by
|
|
||||||
// aliro_harness.issuer.cose (matches RFC 9052 §3 but is not a
|
|
||||||
// generic CBOR parser).
|
|
||||||
short p = coseOff;
|
short p = coseOff;
|
||||||
|
short remaining = coseLen;
|
||||||
|
// 4-byte scratch shared across decodeHeader / elementSpan calls.
|
||||||
|
// Reuse the end of `scratch` so we don't allocate.
|
||||||
|
final short argOff = (short) (scratch.length - 4);
|
||||||
|
|
||||||
// Outer array(4): only major type 4, count 4, fits in one byte.
|
// Outer array(4): expect major type 4, argument == 4.
|
||||||
if ((short) (p + 1) > end) return false;
|
short hdr = StructuralCbor.decodeHeader(
|
||||||
if (coseSign1[p] != (byte) 0x84) return false;
|
coseSign1, p, remaining, scratch, argOff);
|
||||||
p++;
|
if ((short) ((hdr >> 8) & 0x07) != 4) return false;
|
||||||
|
if (readArg(scratch, argOff) != 4) return false;
|
||||||
|
short consumed = (short) (hdr & 0xFF);
|
||||||
|
p += consumed;
|
||||||
|
remaining -= consumed;
|
||||||
|
|
||||||
// protected_bstr: read CBOR bstr header → value off/len.
|
// Element 0: protected_bstr — decode header, capture value off/len.
|
||||||
short protHdrOff = p;
|
hdr = StructuralCbor.decodeHeader(
|
||||||
short protLen = readBstrLen(coseSign1, p, end);
|
coseSign1, p, remaining, scratch, argOff);
|
||||||
if (protLen < 0 || protLen > MAX_PROTECTED) return false;
|
if ((short) ((hdr >> 8) & 0x07) != 2) return false;
|
||||||
short protHdrLen = bstrHeaderLen(protLen);
|
short protLen = readArg(scratch, argOff);
|
||||||
short protValOff = (short) (p + protHdrLen);
|
if (protLen > MAX_PROTECTED) return false;
|
||||||
if ((short) (protValOff + protLen) > end) return false;
|
consumed = (short) (hdr & 0xFF);
|
||||||
p = (short) (protValOff + protLen);
|
short protValOff = (short) (p + consumed);
|
||||||
|
if ((short) (consumed + protLen) > remaining) return false;
|
||||||
|
p += (short) (consumed + protLen);
|
||||||
|
remaining -= (short) (consumed + protLen);
|
||||||
|
|
||||||
// unprotected_map: skip — its contents (kid etc.) don't enter
|
// Element 1: unprotected_map — span-skip, contents don't enter Sig_structure.
|
||||||
// Sig_structure. Walk the map(1) and its single key/value pair.
|
short mapSpan = StructuralCbor.elementSpan(
|
||||||
// CBOR major type 5 (map). We only need to skip; assume map(1)
|
coseSign1, p, remaining, scratch, argOff);
|
||||||
// shape per IssuerAuth profile (0xA1).
|
p += mapSpan;
|
||||||
if ((short) (p + 1) > end) return false;
|
remaining -= mapSpan;
|
||||||
if ((coseSign1[p] & (byte) 0xE0) != (byte) 0xA0) return false;
|
|
||||||
short mapCount = (short) (coseSign1[p] & 0x1F);
|
|
||||||
if (mapCount > 23) return false; // we don't handle long-form map headers
|
|
||||||
p++;
|
|
||||||
for (short i = 0; i < mapCount; i++) {
|
|
||||||
// key: small int (kid header field 4 → 1 byte 0x04). We only
|
|
||||||
// support single-byte keys.
|
|
||||||
if ((short) (p + 1) > end) return false;
|
|
||||||
p++;
|
|
||||||
// value: bstr (kid) — read header + skip body.
|
|
||||||
short kLen = readBstrLen(coseSign1, p, end);
|
|
||||||
if (kLen < 0) return false;
|
|
||||||
short kHdr = bstrHeaderLen(kLen);
|
|
||||||
p = (short) (p + kHdr + kLen);
|
|
||||||
if (p > end) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// payload_bstr.
|
// Element 2: payload_bstr — decode header, capture value off/len.
|
||||||
short payloadHdrOff = p;
|
hdr = StructuralCbor.decodeHeader(
|
||||||
short payloadLen = readBstrLen(coseSign1, p, end);
|
coseSign1, p, remaining, scratch, argOff);
|
||||||
if (payloadLen < 0 || payloadLen > MAX_PAYLOAD) return false;
|
if ((short) ((hdr >> 8) & 0x07) != 2) return false;
|
||||||
short payloadHdrLen = bstrHeaderLen(payloadLen);
|
short payloadLen = readArg(scratch, argOff);
|
||||||
short payloadValOff = (short) (p + payloadHdrLen);
|
if (payloadLen > MAX_PAYLOAD) return false;
|
||||||
if ((short) (payloadValOff + payloadLen) > end) return false;
|
consumed = (short) (hdr & 0xFF);
|
||||||
p = (short) (payloadValOff + payloadLen);
|
short payloadValOff = (short) (p + consumed);
|
||||||
|
if ((short) (consumed + payloadLen) > remaining) return false;
|
||||||
|
p += (short) (consumed + payloadLen);
|
||||||
|
remaining -= (short) (consumed + payloadLen);
|
||||||
|
|
||||||
// signature_bstr: must be raw 64-byte P-256 ECDSA (r||s).
|
// Element 3: signature_bstr — raw 64-byte P-256 ECDSA (r||s).
|
||||||
short sigLen = readBstrLen(coseSign1, p, end);
|
hdr = StructuralCbor.decodeHeader(
|
||||||
|
coseSign1, p, remaining, scratch, argOff);
|
||||||
|
if ((short) ((hdr >> 8) & 0x07) != 2) return false;
|
||||||
|
short sigLen = readArg(scratch, argOff);
|
||||||
if (sigLen != RAW_SIG_LEN) return false;
|
if (sigLen != RAW_SIG_LEN) return false;
|
||||||
short sigHdrLen = bstrHeaderLen(sigLen);
|
consumed = (short) (hdr & 0xFF);
|
||||||
short sigValOff = (short) (p + sigHdrLen);
|
short sigValOff = (short) (p + consumed);
|
||||||
if ((short) (sigValOff + sigLen) > end) return false;
|
if ((short) (consumed + sigLen) > remaining) return false;
|
||||||
|
|
||||||
// Build Sig_structure into scratch.
|
// Build Sig_structure into scratch.
|
||||||
short s = (short) 0;
|
short s = (short) 0;
|
||||||
@@ -213,38 +204,13 @@ final class CoseVerifier {
|
|||||||
scratch, derOff, derLen);
|
scratch, derOff, derLen);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Reads the 4-byte big-endian argument that
|
||||||
* Reads a CBOR byte-string length field at {@code off}. Supports the
|
* {@link StructuralCbor#decodeHeader} wrote into the scratch as a short.
|
||||||
* inline (len ≤ 23, major-type byte 0x40+len), 1-byte-extended
|
* StructuralCbor itself caps argument values at {@code Short.MAX_VALUE}
|
||||||
* (0x58 + uint8), and 2-byte-extended (0x59 + uint16) encodings.
|
* before they can reach us, so the high half is guaranteed zero. */
|
||||||
*
|
private static short readArg(byte[] buf, short off) {
|
||||||
* @return the bstr value length, or -1 if not a bstr or overflow.
|
return (short) (((buf[(short) (off + 2)] & 0xFF) << 8)
|
||||||
*/
|
| (buf[(short) (off + 3)] & 0xFF));
|
||||||
private static short readBstrLen(byte[] buf, short off, short end) {
|
|
||||||
if ((short) (off + 1) > end) return -1;
|
|
||||||
byte b0 = buf[off];
|
|
||||||
if ((b0 & (byte) 0xE0) != (byte) 0x40) return -1; // not bstr major type
|
|
||||||
short shortLen = (short) (b0 & 0x1F);
|
|
||||||
if (shortLen <= 23) return shortLen;
|
|
||||||
if (shortLen == 24) {
|
|
||||||
if ((short) (off + 2) > end) return -1;
|
|
||||||
return (short) (buf[(short) (off + 1)] & 0xFF);
|
|
||||||
}
|
|
||||||
if (shortLen == 25) {
|
|
||||||
if ((short) (off + 3) > end) return -1;
|
|
||||||
short hi = (short) ((buf[(short) (off + 1)] & 0xFF) << 8);
|
|
||||||
short lo = (short) (buf[(short) (off + 2)] & 0xFF);
|
|
||||||
return (short) (hi | lo);
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Total header byte count for a bstr of {@code len} bytes (matches
|
|
||||||
* what {@link #readBstrLen} parses). */
|
|
||||||
private static short bstrHeaderLen(short len) {
|
|
||||||
if (len <= 23) return 1;
|
|
||||||
if (len <= 0xFF) return 2;
|
|
||||||
return 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Copies {@code len} bytes from {@code src[srcOff..]} into {@code dst}
|
/** Copies {@code len} bytes from {@code src[srcOff..]} into {@code dst}
|
||||||
@@ -274,8 +240,7 @@ final class CoseVerifier {
|
|||||||
*
|
*
|
||||||
* <p>Duplicates {@code AliroApplet.rawSigToDer} verbatim — both
|
* <p>Duplicates {@code AliroApplet.rawSigToDer} verbatim — both
|
||||||
* callers are private and Java Card 1.7 has no facility for a shared
|
* callers are private and Java Card 1.7 has no facility for a shared
|
||||||
* package-level helper without a separate utility class. M2B.3 may
|
* package-level helper without a separate utility class.
|
||||||
* consolidate.
|
|
||||||
*/
|
*/
|
||||||
private static short rawSigToDer(byte[] raw, short rawOff,
|
private static short rawSigToDer(byte[] raw, short rawOff,
|
||||||
byte[] out, short outOff) {
|
byte[] out, short outOff) {
|
||||||
|
|||||||
Reference in New Issue
Block a user