diff --git a/applet/src/main/java/com/dangerousthings/aliro/CoseVerifier.java b/applet/src/main/java/com/dangerousthings/aliro/CoseVerifier.java index 2f024c3..657c265 100644 --- a/applet/src/main/java/com/dangerousthings/aliro/CoseVerifier.java +++ b/applet/src/main/java/com/dangerousthings/aliro/CoseVerifier.java @@ -24,17 +24,13 @@ import javacard.security.Signature; * the JC {@code Signature.ALG_ECDSA_SHA_256} primitive expects DER, so * we transcode raw → DER before {@link Signature#verify}. * - *
For now (M2A.2) the CBOR walk uses hardcoded offsets that assume the - * exact wire shape produced by {@code aliro_harness.issuer.cose}: - *
The CBOR walk uses {@link StructuralCbor#decodeHeader} + + * {@link StructuralCbor#elementSpan} for the 4-element COSE_Sign1 array: + * decode the outer array header, decode each bstr header at elements 0/2/3 + * (protected / payload / signature) to capture value offsets, and span-skip + * the unprotected map at element 1. Any malformed input throws + * {@code ISOException(SW_DATA_INVALID)} which the outer try/catch collapses + * to "not verified". * *
The verifier owns a reusable {@link ECPublicKey} slot, seeded with * P-256 curve params at construction. Each call calls {@code setW} with @@ -129,65 +125,60 @@ final class CoseVerifier { private boolean verifyInternal( byte[] coseSign1, short coseOff, short coseLen, 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 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. - if ((short) (p + 1) > end) return false; - if (coseSign1[p] != (byte) 0x84) return false; - p++; + // Outer array(4): expect major type 4, argument == 4. + short hdr = StructuralCbor.decodeHeader( + coseSign1, p, remaining, scratch, argOff); + 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. - short protHdrOff = p; - short protLen = readBstrLen(coseSign1, p, end); - if (protLen < 0 || protLen > MAX_PROTECTED) return false; - short protHdrLen = bstrHeaderLen(protLen); - short protValOff = (short) (p + protHdrLen); - if ((short) (protValOff + protLen) > end) return false; - p = (short) (protValOff + protLen); + // Element 0: protected_bstr — decode header, capture value off/len. + hdr = StructuralCbor.decodeHeader( + coseSign1, p, remaining, scratch, argOff); + if ((short) ((hdr >> 8) & 0x07) != 2) return false; + short protLen = readArg(scratch, argOff); + if (protLen > MAX_PROTECTED) return false; + consumed = (short) (hdr & 0xFF); + 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 - // Sig_structure. Walk the map(1) and its single key/value pair. - // CBOR major type 5 (map). We only need to skip; assume map(1) - // shape per IssuerAuth profile (0xA1). - if ((short) (p + 1) > end) return false; - 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; - } + // Element 1: unprotected_map — span-skip, contents don't enter Sig_structure. + short mapSpan = StructuralCbor.elementSpan( + coseSign1, p, remaining, scratch, argOff); + p += mapSpan; + remaining -= mapSpan; - // payload_bstr. - short payloadHdrOff = p; - short payloadLen = readBstrLen(coseSign1, p, end); - if (payloadLen < 0 || payloadLen > MAX_PAYLOAD) return false; - short payloadHdrLen = bstrHeaderLen(payloadLen); - short payloadValOff = (short) (p + payloadHdrLen); - if ((short) (payloadValOff + payloadLen) > end) return false; - p = (short) (payloadValOff + payloadLen); + // Element 2: payload_bstr — decode header, capture value off/len. + hdr = StructuralCbor.decodeHeader( + coseSign1, p, remaining, scratch, argOff); + if ((short) ((hdr >> 8) & 0x07) != 2) return false; + short payloadLen = readArg(scratch, argOff); + if (payloadLen > MAX_PAYLOAD) return false; + consumed = (short) (hdr & 0xFF); + 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). - short sigLen = readBstrLen(coseSign1, p, end); + // Element 3: signature_bstr — raw 64-byte P-256 ECDSA (r||s). + 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; - short sigHdrLen = bstrHeaderLen(sigLen); - short sigValOff = (short) (p + sigHdrLen); - if ((short) (sigValOff + sigLen) > end) return false; + consumed = (short) (hdr & 0xFF); + short sigValOff = (short) (p + consumed); + if ((short) (consumed + sigLen) > remaining) return false; // Build Sig_structure into scratch. short s = (short) 0; @@ -213,38 +204,13 @@ final class CoseVerifier { scratch, derOff, derLen); } - /** - * Reads a CBOR byte-string length field at {@code off}. Supports the - * inline (len ≤ 23, major-type byte 0x40+len), 1-byte-extended - * (0x58 + uint8), and 2-byte-extended (0x59 + uint16) encodings. - * - * @return the bstr value length, or -1 if not a bstr or overflow. - */ - 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; + /** Reads the 4-byte big-endian argument that + * {@link StructuralCbor#decodeHeader} wrote into the scratch as a short. + * StructuralCbor itself caps argument values at {@code Short.MAX_VALUE} + * before they can reach us, so the high half is guaranteed zero. */ + private static short readArg(byte[] buf, short off) { + return (short) (((buf[(short) (off + 2)] & 0xFF) << 8) + | (buf[(short) (off + 3)] & 0xFF)); } /** Copies {@code len} bytes from {@code src[srcOff..]} into {@code dst} @@ -274,8 +240,7 @@ final class CoseVerifier { * *
Duplicates {@code AliroApplet.rawSigToDer} verbatim — both * 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 - * consolidate. + * package-level helper without a separate utility class. */ private static short rawSigToDer(byte[] raw, short rawOff, byte[] out, short outOff) {