feat(applet): DeviceResponseBuilder (M2D.2)
Pure builder for the mdoc DeviceResponse (ISO 18013-5 / Aliro Table 8-22) carrying the cached Access Document at documents[0].issuerSigned.issuerAuth. Single fixed shape — one mDL document, empty nameSpaces, no deviceSigned. AD bytes embedded verbatim (not re-encoded), so the issuer's signed bytes pass through unchanged. Map keys canonical-CBOR sorted (length-then-lex). Headers for the fixed counts inlined (map(0/2/3), array(1)); tstr headers/values go through StructuralCbor for the canonical length encoding. For a 272 B AD the output is 372 B (100 B wrapper). Reference vector generated from /home/work/aliro-trust/access_document.bin and asserted byte-for-byte. M2D.3 will wire this into StepUpApplet.processEnvelope behind GCM encryption; M2D.4 adds GET RESPONSE chaining.
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
package com.dangerousthings.aliro;
|
||||
|
||||
import javacard.framework.Util;
|
||||
|
||||
/**
|
||||
* Builds the mdoc DeviceResponse (ISO 18013-5 §8.3.2.1.2.2 / Aliro Table 8-22)
|
||||
* carrying the cached Access Document at
|
||||
* {@code documents[0].issuerSigned.issuerAuth}.
|
||||
*
|
||||
* <p>The fixed shape (Aliro Step-Up only ever returns a single mDL document
|
||||
* with empty nameSpaces and no deviceSigned):
|
||||
* <pre>
|
||||
* DeviceResponse = {
|
||||
* "status": 0,
|
||||
* "version": "1.0",
|
||||
* "documents": [
|
||||
* {
|
||||
* "docType": "org.iso.18013.5.1.mDL",
|
||||
* "issuerSigned": {
|
||||
* "issuerAuth": <Access Document bytes verbatim>,
|
||||
* "nameSpaces": {}
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>Map keys are canonical-CBOR sorted (RFC 8949 §4.2.1: encoded-key-length
|
||||
* then lexicographic). Both 10-char issuerSigned keys tie on length; 'i' < 'n'
|
||||
* so issuerAuth precedes nameSpaces.
|
||||
*
|
||||
* <p>The Access Document is embedded VERBATIM via
|
||||
* {@link Util#arrayCopyNonAtomic} — it was verified as a 4-element COSE_Sign1
|
||||
* at personalization (see {@code CoseVerifier}), so the applet trusts its
|
||||
* encoding and must not re-encode it (re-encoding could alter the very bytes
|
||||
* the issuer signed over).
|
||||
*
|
||||
* <p>All map / array headers are single-byte for our fixed counts
|
||||
* ({@code map(0)=0xA0, map(2)=0xA2, map(3)=0xA3, array(1)=0x81}) and inlined
|
||||
* — extending {@link StructuralCbor} with encodeMapHeader / encodeArrayHeader
|
||||
* would add a static helper used only here. Tstr key/value bodies go through
|
||||
* {@link StructuralCbor#encodeTstrHeader} so the header / payload split stays
|
||||
* canonical-CBOR-correct (1-byte header for these tiny strings, but the
|
||||
* encoder picks the right size on its own).
|
||||
*
|
||||
* @see CoseVerifier for the personalization-time AD shape check this builder
|
||||
* relies on.
|
||||
*/
|
||||
final class DeviceResponseBuilder {
|
||||
|
||||
// Canonical CBOR map / array headers used by the fixed Aliro DeviceResponse
|
||||
// shape. All entry counts are < 24, so each fits in the immediate
|
||||
// 1-byte form (major type bits 7..5, additional info bits 4..0).
|
||||
private static final byte CBOR_MAP_3 = (byte) 0xA3; // map, 3 entries
|
||||
private static final byte CBOR_MAP_2 = (byte) 0xA2; // map, 2 entries
|
||||
private static final byte CBOR_MAP_0 = (byte) 0xA0; // map, 0 entries (empty)
|
||||
private static final byte CBOR_ARRAY_1 = (byte) 0x81; // array, 1 element
|
||||
|
||||
// Top-level DeviceResponse keys, sorted canonical (length-then-lex).
|
||||
private static final byte[] KEY_STATUS = { 's', 't', 'a', 't', 'u', 's' };
|
||||
private static final byte[] KEY_VERSION = { 'v', 'e', 'r', 's', 'i', 'o', 'n' };
|
||||
private static final byte[] KEY_DOCUMENTS = { 'd', 'o', 'c', 'u', 'm', 'e', 'n', 't', 's' };
|
||||
|
||||
// Document entry keys.
|
||||
private static final byte[] KEY_DOC_TYPE = { 'd', 'o', 'c', 'T', 'y', 'p', 'e' };
|
||||
private static final byte[] KEY_ISSUER_SIGNED = {
|
||||
'i', 's', 's', 'u', 'e', 'r', 'S', 'i', 'g', 'n', 'e', 'd' };
|
||||
|
||||
// issuerSigned entry keys — issuerAuth before nameSpaces ('i' < 'n').
|
||||
private static final byte[] KEY_ISSUER_AUTH = {
|
||||
'i', 's', 's', 'u', 'e', 'r', 'A', 'u', 't', 'h' };
|
||||
private static final byte[] KEY_NAME_SPACES = {
|
||||
'n', 'a', 'm', 'e', 'S', 'p', 'a', 'c', 'e', 's' };
|
||||
|
||||
// Constants for the only docType Aliro Step-Up ever emits.
|
||||
private static final byte[] VAL_VERSION = { '1', '.', '0' };
|
||||
private static final byte[] VAL_MDL_DOC_TYPE = {
|
||||
'o', 'r', 'g', '.', 'i', 's', 'o', '.', '1', '8', '0', '1', '3',
|
||||
'.', '5', '.', '1', '.', 'm', 'D', 'L' };
|
||||
|
||||
private DeviceResponseBuilder() {
|
||||
// Utility class — no instances.
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a canonical-CBOR DeviceResponse carrying {@code ad} at
|
||||
* {@code documents[0].issuerSigned.issuerAuth}. Writes into {@code out}
|
||||
* starting at {@code outOff} and returns the total bytes written.
|
||||
*
|
||||
* <p>For a 272-byte AD the output is 372 bytes (100 B wrapper); the
|
||||
* caller should provision {@code out} with at least {@code adLen + 128}
|
||||
* bytes for headroom against future shape tweaks. This builder is
|
||||
* allocation-free and side-effect-free; the only state it touches is
|
||||
* the slice {@code out[outOff..outOff+return)}.
|
||||
*
|
||||
* @param ad Access Document buffer (treated as a single CBOR blob,
|
||||
* embedded verbatim — caller has already validated shape)
|
||||
* @param adOff AD start offset
|
||||
* @param adLen AD length
|
||||
* @param out output buffer
|
||||
* @param outOff output start offset
|
||||
* @return total bytes written
|
||||
*/
|
||||
static short build(
|
||||
byte[] ad, short adOff, short adLen,
|
||||
byte[] out, short outOff) {
|
||||
|
||||
short p = outOff;
|
||||
|
||||
// DeviceResponse map header — 3 entries: status, version, documents.
|
||||
out[p++] = CBOR_MAP_3;
|
||||
|
||||
// "status": 0
|
||||
p += StructuralCbor.encodeTstrHeader((short) KEY_STATUS.length, out, p);
|
||||
p = arrayCopy(KEY_STATUS, out, p);
|
||||
p += StructuralCbor.encodeUint(0, out, p);
|
||||
|
||||
// "version": "1.0"
|
||||
p += StructuralCbor.encodeTstrHeader((short) KEY_VERSION.length, out, p);
|
||||
p = arrayCopy(KEY_VERSION, out, p);
|
||||
p += StructuralCbor.encodeTstrHeader((short) VAL_VERSION.length, out, p);
|
||||
p = arrayCopy(VAL_VERSION, out, p);
|
||||
|
||||
// "documents": [ <document> ]
|
||||
p += StructuralCbor.encodeTstrHeader((short) KEY_DOCUMENTS.length, out, p);
|
||||
p = arrayCopy(KEY_DOCUMENTS, out, p);
|
||||
out[p++] = CBOR_ARRAY_1;
|
||||
|
||||
// document map header — 2 entries: docType, issuerSigned.
|
||||
out[p++] = CBOR_MAP_2;
|
||||
|
||||
// "docType": "org.iso.18013.5.1.mDL"
|
||||
p += StructuralCbor.encodeTstrHeader((short) KEY_DOC_TYPE.length, out, p);
|
||||
p = arrayCopy(KEY_DOC_TYPE, out, p);
|
||||
p += StructuralCbor.encodeTstrHeader((short) VAL_MDL_DOC_TYPE.length, out, p);
|
||||
p = arrayCopy(VAL_MDL_DOC_TYPE, out, p);
|
||||
|
||||
// "issuerSigned": { ... }
|
||||
p += StructuralCbor.encodeTstrHeader((short) KEY_ISSUER_SIGNED.length, out, p);
|
||||
p = arrayCopy(KEY_ISSUER_SIGNED, out, p);
|
||||
|
||||
// issuerSigned map header — 2 entries: issuerAuth, nameSpaces.
|
||||
out[p++] = CBOR_MAP_2;
|
||||
|
||||
// "issuerAuth": <AD bytes verbatim>
|
||||
p += StructuralCbor.encodeTstrHeader((short) KEY_ISSUER_AUTH.length, out, p);
|
||||
p = arrayCopy(KEY_ISSUER_AUTH, out, p);
|
||||
Util.arrayCopyNonAtomic(ad, adOff, out, p, adLen);
|
||||
p = (short) (p + adLen);
|
||||
|
||||
// "nameSpaces": {}
|
||||
p += StructuralCbor.encodeTstrHeader((short) KEY_NAME_SPACES.length, out, p);
|
||||
p = arrayCopy(KEY_NAME_SPACES, out, p);
|
||||
out[p++] = CBOR_MAP_0;
|
||||
|
||||
return (short) (p - outOff);
|
||||
}
|
||||
|
||||
/** Copy a small constant byte[] into {@code out[off..)} and return the new write cursor. */
|
||||
private static short arrayCopy(byte[] src, byte[] out, short off) {
|
||||
Util.arrayCopyNonAtomic(src, (short) 0, out, off, (short) src.length);
|
||||
return (short) (off + src.length);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user