@svta/cml-c2pa 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/index.d.ts +36 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +659 -272
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -1,7 +1,225 @@
|
|
|
1
|
-
import { decode, decodeMultiple } from "cbor-x/decode";
|
|
2
|
-
import { encode } from "cbor-x/encode";
|
|
3
1
|
import { findIsoBox, readEmsg, readIsoBoxes } from "@svta/cml-iso-bmff";
|
|
4
2
|
|
|
3
|
+
//#region src/cbor/readCborItem.ts
|
|
4
|
+
const TEXT_DECODER$4 = /* @__PURE__ */ new TextDecoder("utf-8", { ignoreBOM: true });
|
|
5
|
+
const MAJOR_UINT = 0;
|
|
6
|
+
const MAJOR_NEGINT = 1;
|
|
7
|
+
const MAJOR_BYTES = 2;
|
|
8
|
+
const MAJOR_TEXT = 3;
|
|
9
|
+
const MAJOR_ARRAY = 4;
|
|
10
|
+
const MAJOR_MAP = 5;
|
|
11
|
+
const MAJOR_SIMPLE = 7;
|
|
12
|
+
const INFO_INDEFINITE = 31;
|
|
13
|
+
const INDEFINITE_COUNT = -1;
|
|
14
|
+
const BREAK = 255;
|
|
15
|
+
const SIMPLE_FALSE = 20;
|
|
16
|
+
const SIMPLE_TRUE = 21;
|
|
17
|
+
const SIMPLE_NULL = 22;
|
|
18
|
+
const SIMPLE_UNDEFINED = 23;
|
|
19
|
+
const INFO_HALF_FLOAT = 25;
|
|
20
|
+
const INFO_SINGLE_FLOAT = 26;
|
|
21
|
+
const INFO_DOUBLE_FLOAT = 27;
|
|
22
|
+
const MAX_NESTING_DEPTH = 128;
|
|
23
|
+
const ASCII_FAST_PATH_MAX_LENGTH = 32;
|
|
24
|
+
const MAX_SAFE_HIGH_WORD = 2097152;
|
|
25
|
+
const TWO_POW_32 = 4294967296;
|
|
26
|
+
/**
|
|
27
|
+
* A CBOR tag (RFC 8949 section 3.4) with the bytes of the whole tagged item.
|
|
28
|
+
*
|
|
29
|
+
* A class keeps a tag distinct from a CBOR map with `tag` and `value` keys.
|
|
30
|
+
*
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
var CborTag = class {
|
|
34
|
+
constructor(tag, value, bytes) {
|
|
35
|
+
this.tag = tag;
|
|
36
|
+
this.value = value;
|
|
37
|
+
this.bytes = bytes;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
function malformed() {
|
|
41
|
+
return /* @__PURE__ */ new RangeError("Malformed CBOR data item");
|
|
42
|
+
}
|
|
43
|
+
function incomplete() {
|
|
44
|
+
return /* @__PURE__ */ new RangeError("Incomplete CBOR data item");
|
|
45
|
+
}
|
|
46
|
+
function readUint32(bytes, offset) {
|
|
47
|
+
return (bytes[offset] << 24 >>> 0) + (bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3]);
|
|
48
|
+
}
|
|
49
|
+
function readArgument(reader, info) {
|
|
50
|
+
if (info < 24) return info;
|
|
51
|
+
if (info > INFO_DOUBLE_FLOAT) throw malformed();
|
|
52
|
+
const size = 1 << info - 24;
|
|
53
|
+
const { bytes } = reader;
|
|
54
|
+
const start = reader.offset;
|
|
55
|
+
if (start + size > bytes.length) throw incomplete();
|
|
56
|
+
reader.offset = start + size;
|
|
57
|
+
if (size === 1) return bytes[start];
|
|
58
|
+
if (size === 2) return bytes[start] << 8 | bytes[start + 1];
|
|
59
|
+
if (size === 4) return readUint32(bytes, start);
|
|
60
|
+
const high = readUint32(bytes, start);
|
|
61
|
+
const low = readUint32(bytes, start + 4);
|
|
62
|
+
if (high < MAX_SAFE_HIGH_WORD) return high * TWO_POW_32 + low;
|
|
63
|
+
return BigInt(high) * BigInt(TWO_POW_32) + BigInt(low);
|
|
64
|
+
}
|
|
65
|
+
function halfFloatToNumber(half) {
|
|
66
|
+
const sign = half & 32768 ? -1 : 1;
|
|
67
|
+
const exponent = half >> 10 & 31;
|
|
68
|
+
const fraction = half & 1023;
|
|
69
|
+
if (exponent === 0) return sign * fraction * 2 ** -24;
|
|
70
|
+
if (exponent === 31) return fraction ? NaN : sign * Infinity;
|
|
71
|
+
return sign * (1 + fraction / 1024) * 2 ** (exponent - 15);
|
|
72
|
+
}
|
|
73
|
+
function readSimple(reader, info) {
|
|
74
|
+
if (info < 24) {
|
|
75
|
+
if (info === SIMPLE_FALSE) return false;
|
|
76
|
+
if (info === SIMPLE_TRUE) return true;
|
|
77
|
+
if (info === SIMPLE_NULL) return null;
|
|
78
|
+
if (info === SIMPLE_UNDEFINED) return void 0;
|
|
79
|
+
throw malformed();
|
|
80
|
+
}
|
|
81
|
+
if (info < INFO_HALF_FLOAT || info > INFO_DOUBLE_FLOAT) throw malformed();
|
|
82
|
+
const size = 1 << info - 24;
|
|
83
|
+
const { bytes } = reader;
|
|
84
|
+
const start = reader.offset;
|
|
85
|
+
if (start + size > bytes.length) throw incomplete();
|
|
86
|
+
reader.offset = start + size;
|
|
87
|
+
if (info === INFO_HALF_FLOAT) return halfFloatToNumber(bytes[start] << 8 | bytes[start + 1]);
|
|
88
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset + start, size);
|
|
89
|
+
return info === INFO_SINGLE_FLOAT ? view.getFloat32(0) : view.getFloat64(0);
|
|
90
|
+
}
|
|
91
|
+
function readBytes(reader, length) {
|
|
92
|
+
const { bytes } = reader;
|
|
93
|
+
const start = reader.offset;
|
|
94
|
+
if (typeof length !== "number" || length > bytes.length - start) throw incomplete();
|
|
95
|
+
reader.offset = start + length;
|
|
96
|
+
return bytes.subarray(start, reader.offset);
|
|
97
|
+
}
|
|
98
|
+
function readText(view) {
|
|
99
|
+
if (view.length > ASCII_FAST_PATH_MAX_LENGTH) return TEXT_DECODER$4.decode(view);
|
|
100
|
+
let text = "";
|
|
101
|
+
for (const byte of view) {
|
|
102
|
+
if (byte > 127) return TEXT_DECODER$4.decode(view);
|
|
103
|
+
text += String.fromCharCode(byte);
|
|
104
|
+
}
|
|
105
|
+
return text;
|
|
106
|
+
}
|
|
107
|
+
function enter(reader) {
|
|
108
|
+
if (++reader.depth > MAX_NESTING_DEPTH) throw new RangeError("CBOR nesting deeper than 128 levels");
|
|
109
|
+
}
|
|
110
|
+
function atBreak(reader) {
|
|
111
|
+
if (reader.offset >= reader.bytes.length) throw incomplete();
|
|
112
|
+
if (reader.bytes[reader.offset] !== BREAK) return false;
|
|
113
|
+
reader.offset++;
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
function readArray(reader, count) {
|
|
117
|
+
enter(reader);
|
|
118
|
+
const items = [];
|
|
119
|
+
if (count === INDEFINITE_COUNT) while (!atBreak(reader)) items.push(readItem(reader));
|
|
120
|
+
else {
|
|
121
|
+
if (typeof count !== "number" || count > reader.bytes.length - reader.offset) throw incomplete();
|
|
122
|
+
for (let i = 0; i < count; i++) items.push(readItem(reader));
|
|
123
|
+
}
|
|
124
|
+
reader.depth--;
|
|
125
|
+
return items;
|
|
126
|
+
}
|
|
127
|
+
function setEntry(map, key, value) {
|
|
128
|
+
if (key !== null && typeof key === "object") throw new RangeError("Unsupported CBOR map key");
|
|
129
|
+
const name = String(key);
|
|
130
|
+
if (name === "__proto__") Object.defineProperty(map, name, {
|
|
131
|
+
value,
|
|
132
|
+
enumerable: true,
|
|
133
|
+
writable: true,
|
|
134
|
+
configurable: true
|
|
135
|
+
});
|
|
136
|
+
else map[name] = value;
|
|
137
|
+
}
|
|
138
|
+
function readMap(reader, count) {
|
|
139
|
+
enter(reader);
|
|
140
|
+
const map = {};
|
|
141
|
+
if (count === INDEFINITE_COUNT) while (!atBreak(reader)) setEntry(map, readItem(reader), readItem(reader));
|
|
142
|
+
else {
|
|
143
|
+
if (typeof count !== "number" || count * 2 > reader.bytes.length - reader.offset) throw incomplete();
|
|
144
|
+
for (let i = 0; i < count; i++) setEntry(map, readItem(reader), readItem(reader));
|
|
145
|
+
}
|
|
146
|
+
reader.depth--;
|
|
147
|
+
return map;
|
|
148
|
+
}
|
|
149
|
+
function readTag(reader, tag, start) {
|
|
150
|
+
if (typeof tag !== "number") throw malformed();
|
|
151
|
+
enter(reader);
|
|
152
|
+
const value = readItem(reader);
|
|
153
|
+
reader.depth--;
|
|
154
|
+
return new CborTag(tag, value, reader.bytes.subarray(start, reader.offset));
|
|
155
|
+
}
|
|
156
|
+
function readItem(reader) {
|
|
157
|
+
const { bytes } = reader;
|
|
158
|
+
const start = reader.offset;
|
|
159
|
+
if (start >= bytes.length) throw incomplete();
|
|
160
|
+
const initialByte = bytes[start];
|
|
161
|
+
reader.offset = start + 1;
|
|
162
|
+
const majorType = initialByte >> 5;
|
|
163
|
+
const info = initialByte & 31;
|
|
164
|
+
if (majorType === MAJOR_SIMPLE) return readSimple(reader, info);
|
|
165
|
+
if (info === INFO_INDEFINITE) {
|
|
166
|
+
if (majorType === MAJOR_ARRAY) return readArray(reader, INDEFINITE_COUNT);
|
|
167
|
+
if (majorType === MAJOR_MAP) return readMap(reader, INDEFINITE_COUNT);
|
|
168
|
+
throw malformed();
|
|
169
|
+
}
|
|
170
|
+
const argument = readArgument(reader, info);
|
|
171
|
+
switch (majorType) {
|
|
172
|
+
case MAJOR_UINT: return argument;
|
|
173
|
+
case MAJOR_NEGINT: return typeof argument === "number" && argument < Number.MAX_SAFE_INTEGER ? -1 - argument : BigInt(-1) - BigInt(argument);
|
|
174
|
+
case MAJOR_BYTES: return readBytes(reader, argument);
|
|
175
|
+
case MAJOR_TEXT: return readText(readBytes(reader, argument));
|
|
176
|
+
case MAJOR_ARRAY: return readArray(reader, argument);
|
|
177
|
+
case MAJOR_MAP: return readMap(reader, argument);
|
|
178
|
+
default: return readTag(reader, argument, start);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Reads the CBOR data item that starts at `offset` (RFC 8949 section 3).
|
|
183
|
+
*
|
|
184
|
+
* Integers with a magnitude up to `Number.MAX_SAFE_INTEGER` are numbers, larger ones are BigInts.
|
|
185
|
+
* Byte strings are views of the input. Maps are plain objects with string keys. Every tag is a
|
|
186
|
+
* {@link CborTag}. The work and the memory are bounded by the length of the input: no container is
|
|
187
|
+
* allocated from a declared length, and the nesting of arrays, maps, and tags stops at 128 levels.
|
|
188
|
+
*
|
|
189
|
+
* @param bytes - CBOR bytes
|
|
190
|
+
* @param offset - Offset of the first byte of the data item
|
|
191
|
+
* @returns The decoded value and the offset after the last byte of the data item
|
|
192
|
+
* @throws RangeError If the data item is malformed, incomplete, or nested too deep
|
|
193
|
+
*
|
|
194
|
+
* @internal
|
|
195
|
+
*/
|
|
196
|
+
function readCborItem(bytes, offset = 0) {
|
|
197
|
+
const reader = {
|
|
198
|
+
bytes,
|
|
199
|
+
offset,
|
|
200
|
+
depth: 0
|
|
201
|
+
};
|
|
202
|
+
return {
|
|
203
|
+
value: readItem(reader),
|
|
204
|
+
end: reader.offset
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Decodes CBOR bytes that hold exactly one data item.
|
|
209
|
+
*
|
|
210
|
+
* @param bytes - CBOR bytes of one data item
|
|
211
|
+
* @returns The decoded value, see {@link readCborItem}
|
|
212
|
+
* @throws RangeError If the data item is malformed, incomplete, nested too deep, or followed by trailing bytes
|
|
213
|
+
*
|
|
214
|
+
* @internal
|
|
215
|
+
*/
|
|
216
|
+
function decodeCbor(bytes) {
|
|
217
|
+
const item = readCborItem(bytes);
|
|
218
|
+
if (item.end !== bytes.length) throw new RangeError("Trailing bytes after CBOR data item");
|
|
219
|
+
return item.value;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
//#endregion
|
|
5
223
|
//#region src/LiveVideoStatusCode.ts
|
|
6
224
|
/**
|
|
7
225
|
* Standard C2PA failure status codes for live video validation,
|
|
@@ -23,36 +241,100 @@ const LiveVideoStatusCode = {
|
|
|
23
241
|
SESSIONKEY_INVALID: "livevideo.sessionkey.invalid"
|
|
24
242
|
};
|
|
25
243
|
|
|
244
|
+
//#endregion
|
|
245
|
+
//#region src/cbor/projectCborTags.ts
|
|
246
|
+
const TAG_DATE_TIME_STRING = 0;
|
|
247
|
+
const TAG_EPOCH_DATE_TIME = 1;
|
|
248
|
+
const MILLISECONDS_PER_SECOND$1 = 1e3;
|
|
249
|
+
function setKey(map, key, value) {
|
|
250
|
+
if (key === "__proto__") Object.defineProperty(map, key, {
|
|
251
|
+
value,
|
|
252
|
+
enumerable: true,
|
|
253
|
+
writable: true,
|
|
254
|
+
configurable: true
|
|
255
|
+
});
|
|
256
|
+
else map[key] = value;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Projects a tree from {@link readCborItem} to its public shape.
|
|
260
|
+
*
|
|
261
|
+
* Tag 0 with a text string and tag 1 with a number become a `Date` (RFC 8949 sections 3.4.1 and 3.4.2).
|
|
262
|
+
* Every other {@link CborTag} becomes a plain `{ tag, value }` object. Arrays and maps are copied.
|
|
263
|
+
* Byte strings and primitive values are returned as they are.
|
|
264
|
+
*
|
|
265
|
+
* @param value - A value from {@link readCborItem}
|
|
266
|
+
* @returns The projected value
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* {@includeCode ../../test/cbor/cborEquivalence.test.ts#example}
|
|
270
|
+
*
|
|
271
|
+
* @internal
|
|
272
|
+
*/
|
|
273
|
+
function projectCborTags(value) {
|
|
274
|
+
if (value instanceof CborTag) {
|
|
275
|
+
const content = value.value;
|
|
276
|
+
if (value.tag === TAG_DATE_TIME_STRING && typeof content === "string") return new Date(content);
|
|
277
|
+
if (value.tag === TAG_EPOCH_DATE_TIME && typeof content === "number") return new Date(Math.round(content * MILLISECONDS_PER_SECOND$1));
|
|
278
|
+
return {
|
|
279
|
+
tag: value.tag,
|
|
280
|
+
value: projectCborTags(content)
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
if (Array.isArray(value)) return value.map(projectCborTags);
|
|
284
|
+
if (value !== null && typeof value === "object" && !(value instanceof Uint8Array)) {
|
|
285
|
+
const map = {};
|
|
286
|
+
for (const key of Object.keys(value)) setKey(map, key, projectCborTags(value[key]));
|
|
287
|
+
return map;
|
|
288
|
+
}
|
|
289
|
+
return value;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
//#endregion
|
|
293
|
+
//#region src/C2paStatusCode.ts
|
|
294
|
+
/**
|
|
295
|
+
* Standard C2PA validation status codes for manifest integrity checks,
|
|
296
|
+
* as defined in the C2PA specification chapters 15 and 18.
|
|
297
|
+
*
|
|
298
|
+
* @see {@link https://c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_claim_validation | C2PA Spec §15.10.3}
|
|
299
|
+
*
|
|
300
|
+
* @enum
|
|
301
|
+
*
|
|
302
|
+
* @public
|
|
303
|
+
*/
|
|
304
|
+
const C2paStatusCode = {
|
|
305
|
+
ASSERTION_HASHEDURI_MISMATCH: "assertion.hashedURI.mismatch",
|
|
306
|
+
ASSERTION_MISSING: "assertion.missing",
|
|
307
|
+
ASSERTION_MULTIPLE_HARD_BINDINGS: "assertion.multipleHardBindings",
|
|
308
|
+
ASSERTION_UNDECLARED: "assertion.undeclared",
|
|
309
|
+
ASSERTION_ACTION_INGREDIENT_MISMATCH: "assertion.action.ingredientMismatch",
|
|
310
|
+
CLAIM_SIGNATURE_MISMATCH: "claimSignature.mismatch",
|
|
311
|
+
CLAIM_SIGNATURE_MISSING: "claimSignature.missing",
|
|
312
|
+
CLAIM_MISSING: "claim.missing",
|
|
313
|
+
CLAIM_CBOR_INVALID: "claim.cbor.invalid",
|
|
314
|
+
CLAIM_MALFORMED: "claim.malformed",
|
|
315
|
+
CLAIM_MULTIPLE: "claim.multiple",
|
|
316
|
+
ASSERTION_BMFFHASH_MALFORMED: "assertion.bmffHash.malformed",
|
|
317
|
+
ASSERTION_BMFFHASH_MISMATCH: "assertion.bmffHash.mismatch"
|
|
318
|
+
};
|
|
319
|
+
|
|
26
320
|
//#endregion
|
|
27
321
|
//#region src/cose/decodeCoseSign1.ts
|
|
28
|
-
const
|
|
29
|
-
const COSE_SIGN1_TAG_TWO_BYTE_FIRST = 216;
|
|
30
|
-
const COSE_SIGN1_TAG_TWO_BYTE_SECOND = 18;
|
|
322
|
+
const COSE_SIGN1_TAG = 18;
|
|
31
323
|
const COSE_SIGN1_ARRAY_LENGTH = 4;
|
|
32
324
|
const COSE_KEY_KID = 4;
|
|
33
325
|
const COSE_KEY_ALG = 1;
|
|
34
|
-
function coseGet$1(header, key) {
|
|
35
|
-
if (header instanceof Map) return header.get(key);
|
|
36
|
-
return header[key];
|
|
37
|
-
}
|
|
38
|
-
function stripCoseTag(bytes) {
|
|
39
|
-
if (bytes[0] === COSE_SIGN1_TAG_SINGLE_BYTE) return bytes.subarray(1);
|
|
40
|
-
if (bytes[0] === COSE_SIGN1_TAG_TWO_BYTE_FIRST && bytes[1] === COSE_SIGN1_TAG_TWO_BYTE_SECOND) return bytes.subarray(2);
|
|
41
|
-
return bytes;
|
|
42
|
-
}
|
|
43
326
|
function toUint8Array$1(value) {
|
|
44
327
|
if (value instanceof Uint8Array) return value;
|
|
45
|
-
|
|
46
|
-
throw new Error(`Expected Uint8Array or number[], got ${typeof value}`);
|
|
328
|
+
throw new Error(`Expected a byte string, got ${typeof value}`);
|
|
47
329
|
}
|
|
48
330
|
/**
|
|
49
|
-
* Decodes a `
|
|
331
|
+
* Decodes a `COSE_Sign1_Tagged` structure from raw bytes (RFC 9052 section 4.2).
|
|
50
332
|
*
|
|
51
|
-
*
|
|
333
|
+
* The bytes must carry CBOR tag 18 in any encoding length (C2PA section 14).
|
|
52
334
|
*
|
|
53
|
-
* @param coseBytes - Raw
|
|
335
|
+
* @param coseBytes - Raw `COSE_Sign1_Tagged` bytes
|
|
54
336
|
* @returns The decoded COSE_Sign1 structure
|
|
55
|
-
* @throws If the bytes do not represent a
|
|
337
|
+
* @throws If the bytes do not represent a `COSE_Sign1` structure with CBOR tag 18
|
|
56
338
|
*
|
|
57
339
|
* @example
|
|
58
340
|
* {@includeCode ../../test/cose/decodeCoseSign1.test.ts#example}
|
|
@@ -61,16 +343,18 @@ function toUint8Array$1(value) {
|
|
|
61
343
|
*/
|
|
62
344
|
function decodeCoseSign1(coseBytes) {
|
|
63
345
|
try {
|
|
64
|
-
const
|
|
346
|
+
const tagged = decodeCbor(coseBytes);
|
|
347
|
+
if (!(tagged instanceof CborTag) || tagged.tag !== COSE_SIGN1_TAG) throw new Error("Invalid COSE_Sign1 structure: expected CBOR tag 18");
|
|
348
|
+
const coseArray = tagged.value;
|
|
65
349
|
if (!Array.isArray(coseArray) || coseArray.length !== COSE_SIGN1_ARRAY_LENGTH) throw new Error("Invalid COSE_Sign1 structure: expected array with 4 elements");
|
|
66
350
|
const [protectedRaw, unprotectedRaw, payloadRaw, signatureRaw] = coseArray;
|
|
67
351
|
const protectedBytes = toUint8Array$1(protectedRaw);
|
|
68
352
|
let protectedHeader = {};
|
|
69
|
-
if (protectedBytes.length > 0) protectedHeader =
|
|
353
|
+
if (protectedBytes.length > 0) protectedHeader = decodeCbor(protectedBytes);
|
|
70
354
|
const unprotectedHeader = unprotectedRaw ?? {};
|
|
71
|
-
const kidRaw =
|
|
355
|
+
const kidRaw = protectedHeader[COSE_KEY_KID] ?? unprotectedHeader[COSE_KEY_KID] ?? null;
|
|
72
356
|
const kid = kidRaw != null ? toUint8Array$1(kidRaw) : null;
|
|
73
|
-
const alg =
|
|
357
|
+
const alg = protectedHeader[COSE_KEY_ALG] ?? unprotectedHeader[COSE_KEY_ALG] ?? null;
|
|
74
358
|
return {
|
|
75
359
|
protectedBytes,
|
|
76
360
|
protectedHeader,
|
|
@@ -234,14 +518,16 @@ function matchesUuid(usertype, expected) {
|
|
|
234
518
|
}
|
|
235
519
|
/** @internal */
|
|
236
520
|
function toUint8Array(value) {
|
|
237
|
-
|
|
238
|
-
if (Array.isArray(value)) return new Uint8Array(value);
|
|
239
|
-
return null;
|
|
521
|
+
return value instanceof Uint8Array ? value : null;
|
|
240
522
|
}
|
|
241
523
|
/** @internal */
|
|
242
524
|
function asInteger(value) {
|
|
243
525
|
return typeof value === "number" && Number.isInteger(value) ? value : null;
|
|
244
526
|
}
|
|
527
|
+
/** @internal */
|
|
528
|
+
function asUnsignedInteger(value) {
|
|
529
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
530
|
+
}
|
|
245
531
|
function isC2paUuid(usertype) {
|
|
246
532
|
return matchesUuid(usertype, C2PA_MANIFEST_UUID) || matchesUuid(usertype, JUMBF_UUID);
|
|
247
533
|
}
|
|
@@ -258,7 +544,7 @@ function findC2paUuidBox(boxes) {
|
|
|
258
544
|
}
|
|
259
545
|
const FULLBOX_HEADER_SIZE = 4;
|
|
260
546
|
const AUX_UUID_OFFSET_SIZE = 8;
|
|
261
|
-
const TEXT_DECODER$2 = new TextDecoder();
|
|
547
|
+
const TEXT_DECODER$2 = /* @__PURE__ */ new TextDecoder();
|
|
262
548
|
/** @internal */
|
|
263
549
|
function readUuidBoxPurpose(payload) {
|
|
264
550
|
if (payload.length < FULLBOX_HEADER_SIZE) return null;
|
|
@@ -348,12 +634,12 @@ function readElementRaw(bytes, offset) {
|
|
|
348
634
|
//#endregion
|
|
349
635
|
//#region src/x509/extractCertificateInfo.ts
|
|
350
636
|
const TEXT_DECODER$1 = /* @__PURE__ */ new TextDecoder();
|
|
351
|
-
const OID_COMMON_NAME = new Uint8Array([
|
|
637
|
+
const OID_COMMON_NAME = /* @__PURE__ */ new Uint8Array([
|
|
352
638
|
85,
|
|
353
639
|
4,
|
|
354
640
|
3
|
|
355
641
|
]);
|
|
356
|
-
const OID_ORG_NAME = new Uint8Array([
|
|
642
|
+
const OID_ORG_NAME = /* @__PURE__ */ new Uint8Array([
|
|
357
643
|
85,
|
|
358
644
|
4,
|
|
359
645
|
10
|
|
@@ -487,8 +773,10 @@ function parseAssertionsInternal(assertionStoreBoxes) {
|
|
|
487
773
|
if (!label) continue;
|
|
488
774
|
const contentBox = inner.find((b) => b.type === "cbor" || b.type === "json" || b.type === "jumc" || b.type === "jp2c" || b.type === "bidb");
|
|
489
775
|
let data = null;
|
|
776
|
+
let taggedData;
|
|
490
777
|
if (contentBox) if (contentBox.type === "cbor") try {
|
|
491
|
-
|
|
778
|
+
taggedData = decodeCbor(contentBox.data);
|
|
779
|
+
data = projectCborTags(taggedData);
|
|
492
780
|
} catch {
|
|
493
781
|
data = contentBox.data;
|
|
494
782
|
}
|
|
@@ -501,7 +789,8 @@ function parseAssertionsInternal(assertionStoreBoxes) {
|
|
|
501
789
|
assertions.push({
|
|
502
790
|
label,
|
|
503
791
|
data,
|
|
504
|
-
rawBoxPayload: box.data
|
|
792
|
+
rawBoxPayload: box.data,
|
|
793
|
+
taggedData
|
|
505
794
|
});
|
|
506
795
|
}
|
|
507
796
|
return assertions;
|
|
@@ -535,6 +824,25 @@ function parseSignatureInfo(signatureBytes) {
|
|
|
535
824
|
};
|
|
536
825
|
}
|
|
537
826
|
}
|
|
827
|
+
function readClaim(bytes) {
|
|
828
|
+
let value;
|
|
829
|
+
try {
|
|
830
|
+
value = decodeCbor(bytes);
|
|
831
|
+
} catch {
|
|
832
|
+
return {
|
|
833
|
+
data: null,
|
|
834
|
+
code: C2paStatusCode.CLAIM_CBOR_INVALID
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
if (value === null || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype) return {
|
|
838
|
+
data: null,
|
|
839
|
+
code: C2paStatusCode.CLAIM_MALFORMED
|
|
840
|
+
};
|
|
841
|
+
return {
|
|
842
|
+
data: projectCborTags(value),
|
|
843
|
+
code: null
|
|
844
|
+
};
|
|
845
|
+
}
|
|
538
846
|
function extractClaimAssertionRefs(claimData) {
|
|
539
847
|
const created = claimData["created_assertions"];
|
|
540
848
|
const gathered = claimData["gathered_assertions"];
|
|
@@ -550,10 +858,10 @@ function extractClaimAssertionRefs(claimData) {
|
|
|
550
858
|
const e = entry;
|
|
551
859
|
const url = e["url"];
|
|
552
860
|
const hash = e["hash"];
|
|
553
|
-
if (!url ||
|
|
861
|
+
if (!url || hash == null) continue;
|
|
554
862
|
refs.push({
|
|
555
863
|
url,
|
|
556
|
-
hash: hash instanceof Uint8Array ? hash : new Uint8Array(
|
|
864
|
+
hash: hash instanceof Uint8Array ? hash : new Uint8Array(0),
|
|
557
865
|
alg: e["alg"] ?? null
|
|
558
866
|
});
|
|
559
867
|
}
|
|
@@ -590,8 +898,12 @@ function readC2paManifest(bytes, preParsedBoxes) {
|
|
|
590
898
|
const manifestBoxes = resolveManifestBoxes(jumbfBoxes);
|
|
591
899
|
let claimData = null;
|
|
592
900
|
let claimCborBytes = null;
|
|
901
|
+
let claimCode = null;
|
|
593
902
|
let internalAssertions = [];
|
|
594
903
|
let signatureBytes = null;
|
|
904
|
+
let claimBoxes = 0;
|
|
905
|
+
let assertionStores = 0;
|
|
906
|
+
let signatureBoxes = 0;
|
|
595
907
|
for (const box of manifestBoxes) {
|
|
596
908
|
if (box.type !== "jumb") continue;
|
|
597
909
|
const inner = parseJumbfBoxes(box.data);
|
|
@@ -600,19 +912,30 @@ function readC2paManifest(bytes, preParsedBoxes) {
|
|
|
600
912
|
const label = parseJumbfLabel(jumd.data);
|
|
601
913
|
if (!label) continue;
|
|
602
914
|
if (label === "c2pa.claim" || label === "c2pa.claim.v2") {
|
|
915
|
+
claimBoxes++;
|
|
603
916
|
const contentBox = inner.find((b) => b.type === "cbor");
|
|
604
917
|
if (contentBox) {
|
|
605
918
|
claimCborBytes = contentBox.data;
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
919
|
+
const claim = readClaim(contentBox.data);
|
|
920
|
+
claimData = claim.data;
|
|
921
|
+
claimCode = claim.code;
|
|
609
922
|
}
|
|
610
|
-
} else if (label === "c2pa.assertions")
|
|
611
|
-
|
|
923
|
+
} else if (label === "c2pa.assertions") {
|
|
924
|
+
assertionStores++;
|
|
925
|
+
internalAssertions = parseAssertionsInternal(inner);
|
|
926
|
+
} else if (label === "c2pa.signature") {
|
|
927
|
+
signatureBoxes++;
|
|
612
928
|
const contentBox = inner.find((b) => b.type === "cbor" || b.type === "jumc");
|
|
613
929
|
if (contentBox) signatureBytes = contentBox.data;
|
|
614
930
|
}
|
|
615
931
|
}
|
|
932
|
+
if (claimBoxes > 1) {
|
|
933
|
+
claimData = null;
|
|
934
|
+
claimCborBytes = null;
|
|
935
|
+
claimCode = C2paStatusCode.CLAIM_MULTIPLE;
|
|
936
|
+
}
|
|
937
|
+
if (assertionStores > 1) internalAssertions = [];
|
|
938
|
+
if (signatureBoxes > 1) signatureBytes = null;
|
|
616
939
|
const { issuer, certNotBefore } = parseSignatureInfo(signatureBytes);
|
|
617
940
|
const instanceId = claimData?.["instanceID"] ?? claimData?.["instance_id"] ?? null;
|
|
618
941
|
const claimGenerator = claimData?.["claim_generator"] ?? claimData?.["claimGenerator"] ?? null;
|
|
@@ -635,6 +958,7 @@ function readC2paManifest(bytes, preParsedBoxes) {
|
|
|
635
958
|
},
|
|
636
959
|
claimAssertionRefs,
|
|
637
960
|
claimCborBytes,
|
|
961
|
+
claimCode,
|
|
638
962
|
signatureBytes,
|
|
639
963
|
assertions: internalAssertions
|
|
640
964
|
};
|
|
@@ -746,27 +1070,53 @@ async function computeBmffHash(segmentBytes, options) {
|
|
|
746
1070
|
}
|
|
747
1071
|
|
|
748
1072
|
//#endregion
|
|
749
|
-
//#region src/
|
|
1073
|
+
//#region src/bmff/parseExclusions.ts
|
|
1074
|
+
function isMap(value) {
|
|
1075
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1076
|
+
}
|
|
1077
|
+
function parseConstraints(rawConstraints) {
|
|
1078
|
+
if (!Array.isArray(rawConstraints)) return null;
|
|
1079
|
+
const constraints = [];
|
|
1080
|
+
for (const rawConstraint of rawConstraints) {
|
|
1081
|
+
if (!isMap(rawConstraint)) return null;
|
|
1082
|
+
const offset = asUnsignedInteger(rawConstraint["offset"]);
|
|
1083
|
+
const value = toUint8Array(rawConstraint["value"]);
|
|
1084
|
+
if (offset === null || !value) return null;
|
|
1085
|
+
constraints.push({
|
|
1086
|
+
offset,
|
|
1087
|
+
value
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
return constraints;
|
|
1091
|
+
}
|
|
750
1092
|
/**
|
|
751
|
-
*
|
|
752
|
-
* as defined in the C2PA specification chapters 15 and 18.
|
|
753
|
-
*
|
|
754
|
-
* @see {@link https://c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_claim_validation | C2PA Spec §15.10.3}
|
|
1093
|
+
* Parses the `exclusions` array of a `c2pa.hash.bmff.v3` assertion (C2PA section 18.6).
|
|
755
1094
|
*
|
|
756
|
-
* @
|
|
1095
|
+
* @param rawExclusions - The decoded `exclusions` field
|
|
1096
|
+
* @returns The exclusions, an empty array for an absent field, or `null` if the field does not conform
|
|
757
1097
|
*
|
|
758
|
-
* @
|
|
1098
|
+
* @internal
|
|
759
1099
|
*/
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
1100
|
+
function parseExclusions(rawExclusions) {
|
|
1101
|
+
if (rawExclusions == null) return [];
|
|
1102
|
+
if (!Array.isArray(rawExclusions)) return null;
|
|
1103
|
+
const exclusions = [];
|
|
1104
|
+
for (const rawExclusion of rawExclusions) {
|
|
1105
|
+
if (!isMap(rawExclusion) || typeof rawExclusion["xpath"] !== "string") return null;
|
|
1106
|
+
const xpath = rawExclusion["xpath"];
|
|
1107
|
+
if (rawExclusion["data"] == null) {
|
|
1108
|
+
exclusions.push({ xpath });
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
const constraints = parseConstraints(rawExclusion["data"]);
|
|
1112
|
+
if (!constraints) return null;
|
|
1113
|
+
exclusions.push(constraints.length > 0 ? {
|
|
1114
|
+
xpath,
|
|
1115
|
+
data: constraints
|
|
1116
|
+
} : { xpath });
|
|
1117
|
+
}
|
|
1118
|
+
return exclusions;
|
|
1119
|
+
}
|
|
770
1120
|
|
|
771
1121
|
//#endregion
|
|
772
1122
|
//#region src/extractManifestCertificate.ts
|
|
@@ -836,6 +1186,8 @@ function validateActionIngredients(assertions) {
|
|
|
836
1186
|
*
|
|
837
1187
|
* Handles both absolute (`self#jumbf=/c2pa/<manifest>/c2pa.assertions/<label>`)
|
|
838
1188
|
* and relative URI forms by returning the last path segment.
|
|
1189
|
+
*
|
|
1190
|
+
* @internal
|
|
839
1191
|
*/
|
|
840
1192
|
function extractLabelFromUrl(url) {
|
|
841
1193
|
const fragmentIndex = url.indexOf("#jumbf=");
|
|
@@ -844,35 +1196,64 @@ function extractLabelFromUrl(url) {
|
|
|
844
1196
|
return lastSlash >= 0 ? path.substring(lastSlash + 1) : path;
|
|
845
1197
|
}
|
|
846
1198
|
/**
|
|
847
|
-
* Validates assertion hashes and presence per C2PA §15.10.3.1.
|
|
1199
|
+
* Validates assertion hashes and presence per C2PA §8.4.1 and §15.10.3.1.
|
|
848
1200
|
*
|
|
849
1201
|
* For each assertion referenced in the claim's hashed URI list:
|
|
850
|
-
* -
|
|
1202
|
+
* - Resolves the label to a box in the assertion store. If the store holds no box
|
|
1203
|
+
* with that label, or more than one, the reference is unresolved (`assertion.missing`, §8.4.1).
|
|
851
1204
|
* - Recomputes the hash of the raw JUMBF box payload and compares it to the
|
|
852
|
-
* expected hash from the claim (`assertion.hashedURI.mismatch`)
|
|
1205
|
+
* expected hash from the claim (`assertion.hashedURI.mismatch`).
|
|
1206
|
+
*
|
|
1207
|
+
* The `verified` list holds only the assertions that resolved to one box with a matching
|
|
1208
|
+
* hash. A caller that consumes an assertion by label must read it from `verified`, so that
|
|
1209
|
+
* an unverified or ambiguous box is never used.
|
|
853
1210
|
*
|
|
854
1211
|
* @param claimRefs - Hashed URI references from the claim
|
|
855
1212
|
* @param assertions - Parsed assertions with preserved raw box payloads
|
|
856
|
-
* @returns
|
|
1213
|
+
* @returns The status codes for any failures found, and the verified assertions
|
|
857
1214
|
*
|
|
858
1215
|
* @internal
|
|
859
1216
|
*/
|
|
860
1217
|
async function validateAssertionHashes(claimRefs, assertions) {
|
|
861
|
-
if (claimRefs.length === 0) return
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1218
|
+
if (claimRefs.length === 0) return {
|
|
1219
|
+
codes: [],
|
|
1220
|
+
verified: []
|
|
1221
|
+
};
|
|
1222
|
+
const boxesByLabel = /* @__PURE__ */ new Map();
|
|
1223
|
+
for (const assertion of assertions) {
|
|
1224
|
+
const existing = boxesByLabel.get(assertion.label);
|
|
1225
|
+
if (existing) existing.push(assertion);
|
|
1226
|
+
else boxesByLabel.set(assertion.label, [assertion]);
|
|
1227
|
+
}
|
|
865
1228
|
const hashChecks = claimRefs.map(async (ref) => {
|
|
866
1229
|
const label = extractLabelFromUrl(ref.url);
|
|
867
|
-
const
|
|
868
|
-
if (!
|
|
1230
|
+
const boxes = boxesByLabel.get(label);
|
|
1231
|
+
if (!boxes || boxes.length !== 1) return {
|
|
1232
|
+
code: C2paStatusCode.ASSERTION_MISSING,
|
|
1233
|
+
verified: null
|
|
1234
|
+
};
|
|
1235
|
+
const assertion = boxes[0];
|
|
869
1236
|
const alg = ref.alg ? normalizeAlgorithmName(ref.alg) : "SHA-256";
|
|
870
|
-
if (!hashesEqual(new Uint8Array(await crypto.subtle.digest(alg, assertion.rawBoxPayload)), ref.hash)) return
|
|
871
|
-
|
|
1237
|
+
if (!hashesEqual(new Uint8Array(await crypto.subtle.digest(alg, assertion.rawBoxPayload)), ref.hash)) return {
|
|
1238
|
+
code: C2paStatusCode.ASSERTION_HASHEDURI_MISMATCH,
|
|
1239
|
+
verified: null
|
|
1240
|
+
};
|
|
1241
|
+
return {
|
|
1242
|
+
code: null,
|
|
1243
|
+
verified: assertion
|
|
1244
|
+
};
|
|
872
1245
|
});
|
|
873
1246
|
const results = await Promise.all(hashChecks);
|
|
874
|
-
|
|
875
|
-
|
|
1247
|
+
const codes = [];
|
|
1248
|
+
const verified = [];
|
|
1249
|
+
for (const result of results) {
|
|
1250
|
+
if (result.code) codes.push(result.code);
|
|
1251
|
+
if (result.verified) verified.push(result.verified);
|
|
1252
|
+
}
|
|
1253
|
+
return {
|
|
1254
|
+
codes,
|
|
1255
|
+
verified
|
|
1256
|
+
};
|
|
876
1257
|
}
|
|
877
1258
|
|
|
878
1259
|
//#endregion
|
|
@@ -1001,20 +1382,33 @@ function buildSigStructure(protectedBytes, payload, externalAad = new Uint8Array
|
|
|
1001
1382
|
result.set(payload, offset);
|
|
1002
1383
|
return result;
|
|
1003
1384
|
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Encodes bytes as a CBOR byte string (RFC 8949 §3.1, major type 2), without a tag.
|
|
1387
|
+
*
|
|
1388
|
+
* @param bytes - Bytes to encode
|
|
1389
|
+
* @returns The byte string header, followed by `bytes`
|
|
1390
|
+
*
|
|
1391
|
+
* @internal
|
|
1392
|
+
*/
|
|
1393
|
+
function encodeCborByteString(bytes) {
|
|
1394
|
+
const result = new Uint8Array(byteStringHeaderSize(bytes.length) + bytes.length);
|
|
1395
|
+
result.set(bytes, writeByteStringHeader(result, 0, bytes.length));
|
|
1396
|
+
return result;
|
|
1397
|
+
}
|
|
1004
1398
|
|
|
1005
1399
|
//#endregion
|
|
1006
1400
|
//#region src/cose/verifyCoseSign1.ts
|
|
1007
1401
|
const DER_SEQUENCE_TAG = 48;
|
|
1008
1402
|
const DER_INTEGER_TAG = 2;
|
|
1009
1403
|
const CURVE_COMPONENT_BYTES = {
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1404
|
+
"P-256": 32,
|
|
1405
|
+
"P-384": 48,
|
|
1406
|
+
"P-521": 66
|
|
1013
1407
|
};
|
|
1014
1408
|
const RSA_PSS_SALT_LENGTH = {
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1409
|
+
"SHA-256": 32,
|
|
1410
|
+
"SHA-384": 48,
|
|
1411
|
+
"SHA-512": 64
|
|
1018
1412
|
};
|
|
1019
1413
|
function getComponentSize(publicKey) {
|
|
1020
1414
|
const curve = publicKey.algorithm.namedCurve;
|
|
@@ -1168,7 +1562,7 @@ function extractCertificateSpki(certDER) {
|
|
|
1168
1562
|
|
|
1169
1563
|
//#endregion
|
|
1170
1564
|
//#region src/x509/normalizeRsaPssSpki.ts
|
|
1171
|
-
const OID_RSASSA_PSS = new Uint8Array([
|
|
1565
|
+
const OID_RSASSA_PSS = /* @__PURE__ */ new Uint8Array([
|
|
1172
1566
|
6,
|
|
1173
1567
|
9,
|
|
1174
1568
|
42,
|
|
@@ -1181,7 +1575,7 @@ const OID_RSASSA_PSS = new Uint8Array([
|
|
|
1181
1575
|
1,
|
|
1182
1576
|
10
|
|
1183
1577
|
]);
|
|
1184
|
-
const RSA_ENCRYPTION_ALGORITHM_ID = new Uint8Array([
|
|
1578
|
+
const RSA_ENCRYPTION_ALGORITHM_ID = /* @__PURE__ */ new Uint8Array([
|
|
1185
1579
|
48,
|
|
1186
1580
|
13,
|
|
1187
1581
|
6,
|
|
@@ -1286,13 +1680,26 @@ async function verifyClaimSignature(signatureBytes, claimCborBytes, certificateD
|
|
|
1286
1680
|
|
|
1287
1681
|
//#endregion
|
|
1288
1682
|
//#region src/claim/validateManifestIntegrity.ts
|
|
1683
|
+
const HARD_BINDING_LABELS = /* @__PURE__ */ new Set([
|
|
1684
|
+
"c2pa.hash.data",
|
|
1685
|
+
"c2pa.hash.boxes",
|
|
1686
|
+
"c2pa.hash.collection.data",
|
|
1687
|
+
"c2pa.hash.bmff.v2",
|
|
1688
|
+
"c2pa.hash.bmff.v3"
|
|
1689
|
+
]);
|
|
1690
|
+
const ASSERTION_INSTANCE_SUFFIX = /__\d+$/;
|
|
1289
1691
|
/**
|
|
1290
1692
|
* Checks the claim signature per §15.7 and fails closed: a manifest without a claim box,
|
|
1291
1693
|
* without a `c2pa.signature` box, or with a signature that carries no certificate or does
|
|
1292
|
-
* not verify over the claim bytes, is reported as a failure.
|
|
1694
|
+
* not verify over the claim bytes, is reported as a failure. A manifest with more than one
|
|
1695
|
+
* claim box reports `CLAIM_MULTIPLE` through `claimCode` (§15.6.1), so no signature check runs.
|
|
1293
1696
|
*/
|
|
1294
|
-
async function validateClaimSignature({ claimCborBytes, signatureBytes }) {
|
|
1697
|
+
async function validateClaimSignature({ claimCborBytes, claimCode, signatureBytes }) {
|
|
1295
1698
|
const certificate = signatureBytes ? extractCertificateFromSignatureBytes(signatureBytes) : null;
|
|
1699
|
+
if (claimCode === C2paStatusCode.CLAIM_MULTIPLE) return {
|
|
1700
|
+
code: null,
|
|
1701
|
+
certificate
|
|
1702
|
+
};
|
|
1296
1703
|
if (!claimCborBytes) return {
|
|
1297
1704
|
code: C2paStatusCode.CLAIM_MISSING,
|
|
1298
1705
|
certificate
|
|
@@ -1313,27 +1720,37 @@ async function validateClaimSignature({ claimCborBytes, signatureBytes }) {
|
|
|
1313
1720
|
/**
|
|
1314
1721
|
* Validates the integrity of a C2PA manifest per Chapter 15 and Chapter 18.
|
|
1315
1722
|
*
|
|
1316
|
-
* Runs
|
|
1317
|
-
* 1. Assertion hash verification (§15.10.3.1)
|
|
1318
|
-
* 2.
|
|
1319
|
-
* 3.
|
|
1320
|
-
* 4.
|
|
1723
|
+
* Runs these checks, in parallel where possible:
|
|
1724
|
+
* 1. Assertion reference resolution and hash verification (§8.4.1, §15.10.3.1)
|
|
1725
|
+
* 2. Undeclared assertion detection (§15.10.3.1)
|
|
1726
|
+
* 3. Hard binding count (§9.1, §15.10.1.2)
|
|
1727
|
+
* 4. Action ingredient validation (§18.15.4.7)
|
|
1728
|
+
* 5. Claim signature verification (§15.7)
|
|
1321
1729
|
*
|
|
1322
1730
|
* The claim signature is verified with the end-entity certificate from the `x5chain`
|
|
1323
1731
|
* header of the signature. The certificate is not checked against a trust list.
|
|
1324
1732
|
*
|
|
1325
1733
|
* @param internal - Enriched manifest data with raw assertion bytes and claim references
|
|
1326
|
-
* @returns The status codes for any failures found,
|
|
1734
|
+
* @returns The status codes for any failures found, the certificate of the claim signature, and the claim-verified assertions
|
|
1327
1735
|
*
|
|
1328
1736
|
* @internal
|
|
1329
1737
|
*/
|
|
1330
1738
|
async function validateManifestIntegrity(internal) {
|
|
1331
|
-
const [
|
|
1332
|
-
const codes = [...
|
|
1739
|
+
const [assertionHashes, signature] = await Promise.all([validateAssertionHashes(internal.claimAssertionRefs, internal.assertions), validateClaimSignature(internal)]);
|
|
1740
|
+
const codes = [...assertionHashes.codes, ...validateActionIngredients(internal.assertions)];
|
|
1741
|
+
let hardBindingCount = 0;
|
|
1742
|
+
for (const assertion of internal.assertions) if (HARD_BINDING_LABELS.has(assertion.label.replace(ASSERTION_INSTANCE_SUFFIX, ""))) hardBindingCount++;
|
|
1743
|
+
if (hardBindingCount > 1) codes.push(C2paStatusCode.ASSERTION_MULTIPLE_HARD_BINDINGS);
|
|
1744
|
+
if (internal.claimCborBytes && !internal.claimCode) {
|
|
1745
|
+
const referencedLabels = new Set(internal.claimAssertionRefs.map((ref) => extractLabelFromUrl(ref.url)));
|
|
1746
|
+
if (internal.assertions.some((assertion) => !referencedLabels.has(assertion.label))) codes.push(C2paStatusCode.ASSERTION_UNDECLARED);
|
|
1747
|
+
}
|
|
1333
1748
|
if (signature.code) codes.push(signature.code);
|
|
1749
|
+
if (internal.claimCode) codes.push(internal.claimCode);
|
|
1334
1750
|
return {
|
|
1335
|
-
codes,
|
|
1336
|
-
certificate: signature.certificate
|
|
1751
|
+
codes: [...new Set(codes)],
|
|
1752
|
+
certificate: signature.certificate,
|
|
1753
|
+
verifiedAssertions: assertionHashes.verified
|
|
1337
1754
|
};
|
|
1338
1755
|
}
|
|
1339
1756
|
|
|
@@ -1352,12 +1769,7 @@ const OKP_CURVE_NAMES = {
|
|
|
1352
1769
|
6: "Ed25519",
|
|
1353
1770
|
7: "Ed448"
|
|
1354
1771
|
};
|
|
1355
|
-
function
|
|
1356
|
-
if (key instanceof Map) return key.get(intKey);
|
|
1357
|
-
return key[intKey];
|
|
1358
|
-
}
|
|
1359
|
-
function toBase64Url(value) {
|
|
1360
|
-
const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
1772
|
+
function toBase64Url(bytes) {
|
|
1361
1773
|
let binary = "";
|
|
1362
1774
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
1363
1775
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
@@ -1366,8 +1778,7 @@ function toBase64Url(value) {
|
|
|
1366
1778
|
* Converts a COSE public key (RFC 9052 / IANA COSE Key registry) to JWK format.
|
|
1367
1779
|
*
|
|
1368
1780
|
* Supports EC2 keys (P-256, P-384, P-521) and OKP keys (Ed25519, Ed448, X25519, X448).
|
|
1369
|
-
*
|
|
1370
|
-
* object with integer keys.
|
|
1781
|
+
* The input is a plain object whose keys are the integer labels of the COSE key, as the CBOR reader decodes a map.
|
|
1371
1782
|
*
|
|
1372
1783
|
* @param coseKey - COSE key structure as decoded from a C2PA `c2pa.session-keys` assertion
|
|
1373
1784
|
* @returns JWK representation of the public key
|
|
@@ -1380,13 +1791,13 @@ function toBase64Url(value) {
|
|
|
1380
1791
|
*/
|
|
1381
1792
|
function convertCoseKeyToJwk(coseKey) {
|
|
1382
1793
|
const key = coseKey;
|
|
1383
|
-
const kty =
|
|
1384
|
-
const crv =
|
|
1385
|
-
const x =
|
|
1794
|
+
const kty = key[1];
|
|
1795
|
+
const crv = key[-1];
|
|
1796
|
+
const x = key[-2];
|
|
1386
1797
|
if (kty === EC2_KEY_TYPE) {
|
|
1387
1798
|
const curveName = EC_CURVE_NAMES[crv];
|
|
1388
1799
|
if (!curveName) throw new Error(`Unsupported EC curve: ${crv}`);
|
|
1389
|
-
const y =
|
|
1800
|
+
const y = key[-3];
|
|
1390
1801
|
if (!(x instanceof Uint8Array)) throw new Error("EC2 key missing or invalid x coordinate");
|
|
1391
1802
|
if (!(y instanceof Uint8Array)) throw new Error("EC2 key missing or invalid y coordinate");
|
|
1392
1803
|
return {
|
|
@@ -1399,6 +1810,7 @@ function convertCoseKeyToJwk(coseKey) {
|
|
|
1399
1810
|
if (kty === OKP_KEY_TYPE) {
|
|
1400
1811
|
const curveName = OKP_CURVE_NAMES[crv];
|
|
1401
1812
|
if (!curveName) throw new Error(`Unsupported OKP curve: ${crv}`);
|
|
1813
|
+
if (!(x instanceof Uint8Array)) throw new Error("OKP key missing or invalid x coordinate");
|
|
1402
1814
|
return {
|
|
1403
1815
|
kty: "OKP",
|
|
1404
1816
|
crv: curveName,
|
|
@@ -1420,17 +1832,24 @@ function resolveImportAlgorithm(jwk) {
|
|
|
1420
1832
|
|
|
1421
1833
|
//#endregion
|
|
1422
1834
|
//#region src/cose/verifySignerBinding.ts
|
|
1835
|
+
async function verifyPayload(coseSign1, payload, publicKey) {
|
|
1836
|
+
const embeddedPayload = coseSign1.payload;
|
|
1837
|
+
return (!embeddedPayload?.length || hashesEqual(embeddedPayload, payload)) && verifyCoseSign1(coseSign1, payload, publicKey);
|
|
1838
|
+
}
|
|
1423
1839
|
/**
|
|
1424
|
-
* Verifies a C2PA signer binding
|
|
1425
|
-
*
|
|
1840
|
+
* Verifies a C2PA signer binding (C2PA spec §18.25.2). A signer binding is a
|
|
1841
|
+
* `COSE_Sign1` structure that proves the content signer authorized a session key.
|
|
1426
1842
|
*
|
|
1427
|
-
* The `Sig_Structure` payload is
|
|
1843
|
+
* The `Sig_Structure` payload is the end-entity certificate. The function also accepts
|
|
1844
|
+
* a payload that encodes the certificate as a CBOR byte string.
|
|
1845
|
+
* The payload field of the `COSE_Sign1` structure must be nil, empty, or equal to
|
|
1846
|
+
* the `Sig_Structure` payload that verifies.
|
|
1428
1847
|
* DER-encoded ECDSA signatures are automatically normalized to raw format.
|
|
1429
1848
|
*
|
|
1430
1849
|
* @param signerBindingBytes - Raw `COSE_Sign1` bytes of the signer binding
|
|
1431
1850
|
* @param sessionCoseKey - COSE public key from the `c2pa.session-keys` assertion
|
|
1432
1851
|
* @param signerCertBytes - DER-encoded end-entity certificate (from `x5chain`)
|
|
1433
|
-
* @returns `true` if the signer binding
|
|
1852
|
+
* @returns `true` if the signature and the payload field of the signer binding are valid
|
|
1434
1853
|
* @throws If the COSE key type is not supported or decoding fails
|
|
1435
1854
|
*
|
|
1436
1855
|
* @example
|
|
@@ -1442,45 +1861,8 @@ async function verifySignerBinding(signerBindingBytes, sessionCoseKey, signerCer
|
|
|
1442
1861
|
const coseSign1 = decodeCoseSign1(signerBindingBytes);
|
|
1443
1862
|
const jwk = convertCoseKeyToJwk(sessionCoseKey);
|
|
1444
1863
|
const publicKey = await crypto.subtle.importKey("jwk", jwk, resolveImportAlgorithm(jwk), false, ["verify"]);
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
//#endregion
|
|
1449
|
-
//#region src/bmff/parseExclusions.ts
|
|
1450
|
-
function parseConstraints(rawConstraints) {
|
|
1451
|
-
if (!Array.isArray(rawConstraints)) return [];
|
|
1452
|
-
const constraints = [];
|
|
1453
|
-
for (const c of rawConstraints) {
|
|
1454
|
-
if (!c || typeof c !== "object") continue;
|
|
1455
|
-
const record = c;
|
|
1456
|
-
if (typeof record["offset"] !== "number") continue;
|
|
1457
|
-
const value = toUint8Array(record["value"]);
|
|
1458
|
-
if (value) constraints.push({
|
|
1459
|
-
offset: record["offset"],
|
|
1460
|
-
value
|
|
1461
|
-
});
|
|
1462
|
-
}
|
|
1463
|
-
return constraints;
|
|
1464
|
-
}
|
|
1465
|
-
/**
|
|
1466
|
-
* Parses the `exclusions` array of a `c2pa.hash.bmff.v3` assertion.
|
|
1467
|
-
*
|
|
1468
|
-
* @internal
|
|
1469
|
-
*/
|
|
1470
|
-
function parseExclusions(rawExclusions) {
|
|
1471
|
-
if (!Array.isArray(rawExclusions)) return [];
|
|
1472
|
-
const exclusions = [];
|
|
1473
|
-
for (const exc of rawExclusions) {
|
|
1474
|
-
if (!exc || typeof exc !== "object") continue;
|
|
1475
|
-
const record = exc;
|
|
1476
|
-
if (typeof record["xpath"] !== "string") continue;
|
|
1477
|
-
const constraints = parseConstraints(record["data"]);
|
|
1478
|
-
exclusions.push(constraints.length > 0 ? {
|
|
1479
|
-
xpath: record["xpath"],
|
|
1480
|
-
data: constraints
|
|
1481
|
-
} : { xpath: record["xpath"] });
|
|
1482
|
-
}
|
|
1483
|
-
return exclusions;
|
|
1864
|
+
if (await verifyPayload(coseSign1, signerCertBytes, publicKey)) return true;
|
|
1865
|
+
return verifyPayload(coseSign1, encodeCborByteString(signerCertBytes), publicKey);
|
|
1484
1866
|
}
|
|
1485
1867
|
|
|
1486
1868
|
//#endregion
|
|
@@ -1499,6 +1881,7 @@ function extractMerkleMaps(bmffHashAssertionData) {
|
|
|
1499
1881
|
const rawMerkle = bmffHashAssertionData["merkle"];
|
|
1500
1882
|
if (!Array.isArray(rawMerkle) || rawMerkle.length === 0) return null;
|
|
1501
1883
|
const exclusions = parseExclusions(bmffHashAssertionData["exclusions"]);
|
|
1884
|
+
if (!exclusions) return null;
|
|
1502
1885
|
const assertionAlg = bmffHashAssertionData["alg"];
|
|
1503
1886
|
const offsetPrefixSize = bmffHashAssertionData["hash"] == null ? 8 : 0;
|
|
1504
1887
|
const maps = [];
|
|
@@ -1561,42 +1944,26 @@ async function validateMerkleMaps(bytes, assertion, codes) {
|
|
|
1561
1944
|
const BMFF_HASH_ASSERTION_LABEL$1 = "c2pa.hash.bmff.v3";
|
|
1562
1945
|
const SESSION_KEYS_ASSERTION_LABEL = "c2pa.session-keys";
|
|
1563
1946
|
const COSE_KEY_ID_LABEL = 2;
|
|
1564
|
-
const CBOR_TAGGED_KEY = "@@TAGGED@@";
|
|
1565
|
-
function extractCborTaggedValue(value) {
|
|
1566
|
-
if (typeof value !== "object" || value === null) return null;
|
|
1567
|
-
const obj = value;
|
|
1568
|
-
if (typeof obj["tag"] === "number" && "value" in obj) return obj["value"];
|
|
1569
|
-
const tagged = obj[CBOR_TAGGED_KEY];
|
|
1570
|
-
if (Array.isArray(tagged) && tagged.length === 2) return tagged[1];
|
|
1571
|
-
return null;
|
|
1572
|
-
}
|
|
1573
1947
|
function normalizeToUint8Array(value) {
|
|
1574
1948
|
if (value instanceof Uint8Array) return value;
|
|
1575
|
-
if (
|
|
1576
|
-
if (extractCborTaggedValue(value) !== null) return encode(value);
|
|
1949
|
+
if (value instanceof CborTag) return value.bytes;
|
|
1577
1950
|
throw new Error("Cannot convert value to Uint8Array");
|
|
1578
1951
|
}
|
|
1579
1952
|
function ensureDecodedCbor(value) {
|
|
1580
|
-
|
|
1581
|
-
if (Array.isArray(value) && value.length > 0 && typeof value[0] === "number") return decode(new Uint8Array(value));
|
|
1582
|
-
return value;
|
|
1953
|
+
return value instanceof Uint8Array ? decodeCbor(value) : value;
|
|
1583
1954
|
}
|
|
1955
|
+
const RFC_3339_DATE_TIME = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])T([01]\d|2[0-3])(:[0-5]\d){2}(\.\d+)?(Z|[+-]([01]\d|2[0-3]):[0-5]\d)$/;
|
|
1956
|
+
const CBOR_TAG_DATE_TIME = 0;
|
|
1584
1957
|
function parseCreatedAt(value) {
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
if (
|
|
1588
|
-
return null;
|
|
1958
|
+
if (!(value instanceof CborTag) || value.tag !== CBOR_TAG_DATE_TIME) return null;
|
|
1959
|
+
const text = value.value;
|
|
1960
|
+
if (typeof text !== "string" || !RFC_3339_DATE_TIME.test(text)) return null;
|
|
1961
|
+
if (new Date(text.slice(0, 10)).getUTCDate() !== Number(text.slice(8, 10))) return null;
|
|
1962
|
+
return new Date(text).toISOString();
|
|
1589
1963
|
}
|
|
1590
|
-
function extractKidHex(
|
|
1591
|
-
const
|
|
1592
|
-
|
|
1593
|
-
if (typeof kid === "string") return kid;
|
|
1594
|
-
if (Array.isArray(kid) && kid.length > 0) return bytesToHex(new Uint8Array(kid));
|
|
1595
|
-
const coseKeyLike = coseKey;
|
|
1596
|
-
const coseKid = coseKeyLike instanceof Map ? coseKeyLike.get(COSE_KEY_ID_LABEL) : coseKeyLike[COSE_KEY_ID_LABEL];
|
|
1597
|
-
if (coseKid instanceof Uint8Array) return bytesToHex(coseKid);
|
|
1598
|
-
if (Array.isArray(coseKid) && coseKid.length > 0) return bytesToHex(new Uint8Array(coseKid));
|
|
1599
|
-
return null;
|
|
1964
|
+
function extractKidHex(coseKey) {
|
|
1965
|
+
const coseKid = coseKey[COSE_KEY_ID_LABEL];
|
|
1966
|
+
return coseKid instanceof Uint8Array ? bytesToHex(coseKid) : null;
|
|
1600
1967
|
}
|
|
1601
1968
|
function extractKeyArray(data) {
|
|
1602
1969
|
if (Array.isArray(data)) return data;
|
|
@@ -1616,72 +1983,84 @@ async function validateBmffHashAssertion(bytes, assertion) {
|
|
|
1616
1983
|
const data = assertion.data;
|
|
1617
1984
|
const rawHash = data["hash"] ?? data["value"];
|
|
1618
1985
|
if (!rawHash) return true;
|
|
1619
|
-
|
|
1986
|
+
if (!(rawHash instanceof Uint8Array)) return false;
|
|
1620
1987
|
const alg = normalizeAlgorithmName(data["alg"]);
|
|
1988
|
+
const exclusions = parseExclusions(data["exclusions"]);
|
|
1989
|
+
if (!exclusions) return false;
|
|
1621
1990
|
return hashesEqual(await computeBmffHash(bytes, {
|
|
1622
|
-
exclusions
|
|
1991
|
+
exclusions,
|
|
1623
1992
|
alg,
|
|
1624
1993
|
offsetPrefixSize: 8
|
|
1625
|
-
}),
|
|
1994
|
+
}), rawHash);
|
|
1626
1995
|
}
|
|
1627
1996
|
function extractSessionKeyFields(entry) {
|
|
1628
1997
|
const keyData = entry;
|
|
1629
|
-
const minSequenceNumber = keyData["minSequenceNumber"];
|
|
1630
|
-
const validityPeriod = keyData["validityPeriod"];
|
|
1998
|
+
const minSequenceNumber = asUnsignedInteger(keyData["minSequenceNumber"]);
|
|
1999
|
+
const validityPeriod = asUnsignedInteger(keyData["validityPeriod"]);
|
|
1631
2000
|
const createdAt = parseCreatedAt(keyData["createdAt"]);
|
|
1632
|
-
if (minSequenceNumber
|
|
1633
|
-
if (/* @__PURE__ */ new Date() < new Date(createdAt) || isKeyExpired(createdAt, Number(validityPeriod))) return null;
|
|
2001
|
+
if (minSequenceNumber === null || validityPeriod === null || !createdAt) return null;
|
|
1634
2002
|
const coseKey = ensureDecodedCbor(keyData["key"]);
|
|
1635
|
-
const kid = extractKidHex(
|
|
2003
|
+
const kid = extractKidHex(coseKey);
|
|
1636
2004
|
if (!kid) return null;
|
|
1637
2005
|
const signerBindingRaw = keyData["signerBinding"];
|
|
1638
2006
|
if (!signerBindingRaw) return null;
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
};
|
|
1648
|
-
} catch {
|
|
1649
|
-
return null;
|
|
1650
|
-
}
|
|
2007
|
+
return {
|
|
2008
|
+
minSequenceNumber,
|
|
2009
|
+
validityPeriod,
|
|
2010
|
+
createdAt,
|
|
2011
|
+
kid,
|
|
2012
|
+
coseKey,
|
|
2013
|
+
signerBindingBytes: normalizeToUint8Array(signerBindingRaw)
|
|
2014
|
+
};
|
|
1651
2015
|
}
|
|
1652
2016
|
async function verifyAndConvertKey(fields, certificate) {
|
|
1653
2017
|
if (!await verifySignerBinding(fields.signerBindingBytes, fields.coseKey, certificate)) return null;
|
|
2018
|
+
return {
|
|
2019
|
+
kid: fields.kid,
|
|
2020
|
+
jwk: convertCoseKeyToJwk(fields.coseKey),
|
|
2021
|
+
minSequenceNumber: fields.minSequenceNumber,
|
|
2022
|
+
validityPeriod: fields.validityPeriod,
|
|
2023
|
+
createdAt: fields.createdAt
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
async function validateSingleSessionKey(entry, certificate) {
|
|
1654
2027
|
try {
|
|
1655
|
-
const
|
|
1656
|
-
return
|
|
1657
|
-
|
|
1658
|
-
jwk,
|
|
1659
|
-
minSequenceNumber: fields.minSequenceNumber,
|
|
1660
|
-
validityPeriod: fields.validityPeriod,
|
|
1661
|
-
createdAt: fields.createdAt
|
|
1662
|
-
};
|
|
2028
|
+
const fields = extractSessionKeyFields(entry);
|
|
2029
|
+
if (!fields) return null;
|
|
2030
|
+
return await verifyAndConvertKey(fields, certificate);
|
|
1663
2031
|
} catch {
|
|
1664
2032
|
return null;
|
|
1665
2033
|
}
|
|
1666
2034
|
}
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
if (!fields) return null;
|
|
1670
|
-
return verifyAndConvertKey(fields, certificate);
|
|
2035
|
+
function isWithinValidityPeriod(key) {
|
|
2036
|
+
return !isKeyExpired(key.createdAt, key.validityPeriod);
|
|
1671
2037
|
}
|
|
1672
2038
|
async function validateSessionKeys(assertion, certificate) {
|
|
1673
|
-
const keyEntries = extractKeyArray(
|
|
1674
|
-
|
|
2039
|
+
const keyEntries = extractKeyArray(assertion.taggedData);
|
|
2040
|
+
const results = await Promise.all(keyEntries.map((entry) => validateSingleSessionKey(entry, certificate)));
|
|
2041
|
+
const validKeys = results.filter((key) => key !== null);
|
|
2042
|
+
return {
|
|
2043
|
+
sessionKeys: validKeys.filter(isWithinValidityPeriod),
|
|
2044
|
+
hasInvalidSessionKey: keyEntries.length === 0 || validKeys.length < results.length
|
|
2045
|
+
};
|
|
1675
2046
|
}
|
|
1676
2047
|
/**
|
|
1677
2048
|
* Validates a C2PA init segment: parses the manifest, extracts and verifies
|
|
1678
2049
|
* the certificate, validates the BMFF hard binding hash, verifies all
|
|
1679
2050
|
* session keys from the `c2pa.session-keys` assertion, and performs
|
|
1680
|
-
* manifest integrity checks (assertion hashes,
|
|
1681
|
-
*
|
|
2051
|
+
* manifest integrity checks (assertion references and hashes, undeclared
|
|
2052
|
+
* assertions, the hard binding count, duplicate manifest boxes, action
|
|
2053
|
+
* ingredients, and claim signature verification).
|
|
1682
2054
|
*
|
|
1683
2055
|
* Only session keys with a valid signer binding and an unexpired validity period
|
|
1684
|
-
* are included in the result.
|
|
2056
|
+
* are included in the result. The result holds `sessionKeys` and `merkleMaps` only when
|
|
2057
|
+
* every manifest integrity check passes, so check `isValid` before you use either list.
|
|
2058
|
+
*
|
|
2059
|
+
* The result includes `LiveVideoStatusCode.SESSIONKEY_INVALID` if the function finds an invalid
|
|
2060
|
+
* session key (C2PA section 19.7.3). A session key is invalid if it does not conform to section 18.25.2,
|
|
2061
|
+
* or if its signer binding fails verification. The function does not check every rule of section 18.25.2.
|
|
2062
|
+
* An expired session key does not count as invalid. A `c2pa.session-keys` assertion without a session key,
|
|
2063
|
+
* or with CBOR that does not decode, also causes `SESSIONKEY_INVALID`.
|
|
1685
2064
|
*
|
|
1686
2065
|
* @param bytes - Raw init segment bytes
|
|
1687
2066
|
* @returns Structured validation result (with `INIT_INVALID` error code if `mdat` box is present)
|
|
@@ -1705,15 +2084,22 @@ async function validateC2paInitSegment(bytes) {
|
|
|
1705
2084
|
};
|
|
1706
2085
|
const internalData = readC2paManifest(bytes, boxes);
|
|
1707
2086
|
const { manifest } = internalData;
|
|
1708
|
-
const { codes: integrityCodes, certificate } = await validateManifestIntegrity(internalData);
|
|
1709
|
-
const bmffHashAssertion =
|
|
2087
|
+
const { codes: integrityCodes, certificate, verifiedAssertions } = await validateManifestIntegrity(internalData);
|
|
2088
|
+
const bmffHashAssertion = verifiedAssertions.find((a) => a.label === BMFF_HASH_ASSERTION_LABEL$1) ?? null;
|
|
1710
2089
|
const bmffHashValid = await validateBmffHashAssertion(bytes, bmffHashAssertion);
|
|
1711
|
-
const sessionKeysAssertion = manifest.assertions.find((a) => a.label === SESSION_KEYS_ASSERTION_LABEL);
|
|
1712
|
-
const sessionKeys = sessionKeysAssertion && certificate ? await validateSessionKeys(sessionKeysAssertion, certificate) : [];
|
|
1713
2090
|
const codes = /* @__PURE__ */ new Set();
|
|
1714
2091
|
const merkleMaps = await validateMerkleMaps(bytes, bmffHashAssertion, codes);
|
|
2092
|
+
const sessionKeysAssertion = verifiedAssertions.find((a) => a.label === SESSION_KEYS_ASSERTION_LABEL);
|
|
2093
|
+
const sessionKeysValidation = sessionKeysAssertion && certificate ? await validateSessionKeys(sessionKeysAssertion, certificate) : {
|
|
2094
|
+
sessionKeys: [],
|
|
2095
|
+
hasInvalidSessionKey: false
|
|
2096
|
+
};
|
|
2097
|
+
const integrityPassed = integrityCodes.length === 0;
|
|
2098
|
+
const sessionKeys = integrityPassed ? sessionKeysValidation.sessionKeys : [];
|
|
2099
|
+
const exposedMerkleMaps = integrityPassed ? merkleMaps ?? [] : [];
|
|
1715
2100
|
if (!bmffHashValid) codes.add(LiveVideoStatusCode.INIT_INVALID);
|
|
1716
|
-
if (
|
|
2101
|
+
if (sessionKeysValidation.hasInvalidSessionKey) codes.add(LiveVideoStatusCode.SESSIONKEY_INVALID);
|
|
2102
|
+
if (sessionKeysValidation.sessionKeys.length === 0 && merkleMaps === null) codes.add(LiveVideoStatusCode.SESSIONKEY_INVALID);
|
|
1717
2103
|
for (const code of integrityCodes) codes.add(code);
|
|
1718
2104
|
const errorCodes = [...codes];
|
|
1719
2105
|
return {
|
|
@@ -1721,7 +2107,7 @@ async function validateC2paInitSegment(bytes) {
|
|
|
1721
2107
|
certificate,
|
|
1722
2108
|
manifestId: manifest.instanceId,
|
|
1723
2109
|
sessionKeys,
|
|
1724
|
-
merkleMaps:
|
|
2110
|
+
merkleMaps: exposedMerkleMaps,
|
|
1725
2111
|
isValid: errorCodes.length === 0,
|
|
1726
2112
|
errorCodes
|
|
1727
2113
|
};
|
|
@@ -1770,16 +2156,20 @@ function extractVsiEmsgBox(segmentBytes) {
|
|
|
1770
2156
|
* @internal
|
|
1771
2157
|
*/
|
|
1772
2158
|
function decodeVsiMap(vsiCborBytes) {
|
|
1773
|
-
const raw =
|
|
2159
|
+
const raw = decodeCbor(vsiCborBytes);
|
|
1774
2160
|
if (typeof raw !== "object" || raw === null) throw new Error("VSI map must be a CBOR map");
|
|
1775
|
-
const
|
|
1776
|
-
|
|
2161
|
+
const rawSequenceNumber = raw["sequenceNumber"];
|
|
2162
|
+
const sequenceNumber = asUnsignedInteger(rawSequenceNumber);
|
|
2163
|
+
if (sequenceNumber === null) {
|
|
2164
|
+
const received = typeof rawSequenceNumber === "number" || typeof rawSequenceNumber === "bigint" ? String(rawSequenceNumber) : typeof rawSequenceNumber;
|
|
2165
|
+
throw new Error(`VSI map sequenceNumber must be an unsigned integer up to ${Number.MAX_SAFE_INTEGER}, got ${received}`);
|
|
2166
|
+
}
|
|
1777
2167
|
const bmffHashRaw = raw["bmffHash"];
|
|
1778
2168
|
if (!bmffHashRaw || typeof bmffHashRaw !== "object") throw new Error("VSI map missing bmffHash");
|
|
1779
2169
|
const hash = bmffHashRaw["hash"];
|
|
1780
2170
|
if (!(hash instanceof Uint8Array)) throw new Error("VSI map bmffHash.hash must be a Uint8Array");
|
|
1781
|
-
const exclusions = bmffHashRaw["exclusions"];
|
|
1782
|
-
if (
|
|
2171
|
+
const exclusions = parseExclusions(bmffHashRaw["exclusions"]);
|
|
2172
|
+
if (!exclusions) throw new Error("VSI map bmffHash.exclusions must be an array of maps with an xpath text string, and data constraints with an unsigned integer offset and a byte string value");
|
|
1783
2173
|
const manifestId = raw["manifestId"];
|
|
1784
2174
|
if (typeof manifestId !== "string") throw new Error("VSI map missing or invalid manifestId");
|
|
1785
2175
|
return {
|
|
@@ -1787,7 +2177,7 @@ function decodeVsiMap(vsiCborBytes) {
|
|
|
1787
2177
|
bmffHash: {
|
|
1788
2178
|
hash,
|
|
1789
2179
|
alg: normalizeAlgorithmName(bmffHashRaw["alg"]),
|
|
1790
|
-
exclusions
|
|
2180
|
+
exclusions
|
|
1791
2181
|
},
|
|
1792
2182
|
manifestId
|
|
1793
2183
|
};
|
|
@@ -1965,10 +2355,13 @@ function findSessionKey(sessionKeys, kidHex) {
|
|
|
1965
2355
|
*
|
|
1966
2356
|
* Returns `null` if the segment does not contain a C2PA EMSG box.
|
|
1967
2357
|
*
|
|
2358
|
+
* A segment whose signature, content hash, or key validity does not verify
|
|
2359
|
+
* returns the received `sequenceState` unchanged.
|
|
2360
|
+
*
|
|
1968
2361
|
* @param segmentBytes - Raw segment bytes
|
|
1969
2362
|
* @param sessionKeys - Available session keys from the init segment
|
|
1970
2363
|
* @param sequenceState - Current sequence state for this stream
|
|
1971
|
-
* @returns Validation result and
|
|
2364
|
+
* @returns Validation result and the sequence state for the next call, or `null` if no C2PA EMSG box
|
|
1972
2365
|
*
|
|
1973
2366
|
* @example
|
|
1974
2367
|
* {@includeCode ../../test/segment/validateC2paSegment.test.ts#example}
|
|
@@ -2000,7 +2393,7 @@ async function validateC2paSegment(segmentBytes, sessionKeys, sequenceState = cr
|
|
|
2000
2393
|
isValid: false,
|
|
2001
2394
|
errorCodes: [LiveVideoStatusCode.SEGMENT_INVALID]
|
|
2002
2395
|
},
|
|
2003
|
-
nextSequenceState
|
|
2396
|
+
nextSequenceState: sequenceState
|
|
2004
2397
|
};
|
|
2005
2398
|
const algorithm = resolveImportAlgorithm(sessionKey.jwk);
|
|
2006
2399
|
const publicKey = await crypto.subtle.importKey("jwk", sessionKey.jwk, algorithm, false, ["verify"]);
|
|
@@ -2009,11 +2402,12 @@ async function validateC2paSegment(segmentBytes, sessionKeys, sequenceState = cr
|
|
|
2009
2402
|
alg: vsi.bmffHash.alg
|
|
2010
2403
|
})]);
|
|
2011
2404
|
const sequenceAboveMin = vsi.sequenceNumber >= sessionKey.minSequenceNumber;
|
|
2405
|
+
const keyNotYetActive = /* @__PURE__ */ new Date() < new Date(sessionKey.createdAt);
|
|
2012
2406
|
const keyExpired = isKeyExpired(sessionKey.createdAt, sessionKey.validityPeriod);
|
|
2407
|
+
const authentic = signatureValid && hashValid && !keyNotYetActive && !keyExpired;
|
|
2013
2408
|
const codes = /* @__PURE__ */ new Set();
|
|
2014
|
-
if (!
|
|
2409
|
+
if (!authentic || !sequenceAboveMin) codes.add(LiveVideoStatusCode.SEGMENT_INVALID);
|
|
2015
2410
|
if (!sequenceResult.isValid) codes.add(LiveVideoStatusCode.ASSERTION_INVALID);
|
|
2016
|
-
if (keyExpired) codes.add(LiveVideoStatusCode.SESSIONKEY_INVALID);
|
|
2017
2411
|
const errorCodes = [...codes];
|
|
2018
2412
|
return {
|
|
2019
2413
|
result: {
|
|
@@ -2021,7 +2415,7 @@ async function validateC2paSegment(segmentBytes, sessionKeys, sequenceState = cr
|
|
|
2021
2415
|
isValid: errorCodes.length === 0,
|
|
2022
2416
|
errorCodes
|
|
2023
2417
|
},
|
|
2024
|
-
nextSequenceState
|
|
2418
|
+
nextSequenceState: authentic ? nextSequenceState : sequenceState
|
|
2025
2419
|
};
|
|
2026
2420
|
}
|
|
2027
2421
|
|
|
@@ -2048,7 +2442,7 @@ function parseLiveVideoAssertion(assertions) {
|
|
|
2048
2442
|
const rawStreamId = data?.["streamId"];
|
|
2049
2443
|
const rawContinuity = data?.["continuityMethod"];
|
|
2050
2444
|
return {
|
|
2051
|
-
sequenceNumber:
|
|
2445
|
+
sequenceNumber: asUnsignedInteger(rawSeq),
|
|
2052
2446
|
previousManifestId: typeof rawPrev === "string" ? rawPrev : null,
|
|
2053
2447
|
streamId: typeof rawStreamId === "string" ? rawStreamId : null,
|
|
2054
2448
|
continuityMethod: typeof rawContinuity === "string" ? rawContinuity : null,
|
|
@@ -2059,19 +2453,25 @@ const EMPTY_BMFF_HASH = {
|
|
|
2059
2453
|
hashBytes: null,
|
|
2060
2454
|
hashHex: null,
|
|
2061
2455
|
exclusions: [],
|
|
2062
|
-
alg: null
|
|
2456
|
+
alg: null,
|
|
2457
|
+
malformed: false
|
|
2063
2458
|
};
|
|
2064
2459
|
function parseBmffHashAssertion(assertions) {
|
|
2065
2460
|
const assertion = assertions.find((a) => a.label === BMFF_HASH_ASSERTION_LABEL);
|
|
2066
2461
|
if (!assertion) return EMPTY_BMFF_HASH;
|
|
2067
2462
|
const data = extractAssertionData(assertion.data);
|
|
2068
2463
|
if (!data) return EMPTY_BMFF_HASH;
|
|
2069
|
-
const
|
|
2464
|
+
const rawHash = data["hash"] ?? data["value"];
|
|
2465
|
+
const hashBytes = toUint8Array(rawHash);
|
|
2466
|
+
const hashHex = hashBytes ? bytesToHex(hashBytes) : null;
|
|
2467
|
+
const exclusions = parseExclusions(data["exclusions"]);
|
|
2468
|
+
const alg = typeof data["alg"] === "string" ? normalizeAlgorithmName(data["alg"]) : null;
|
|
2070
2469
|
return {
|
|
2071
2470
|
hashBytes,
|
|
2072
|
-
hashHex
|
|
2073
|
-
exclusions:
|
|
2074
|
-
alg
|
|
2471
|
+
hashHex,
|
|
2472
|
+
exclusions: exclusions ?? [],
|
|
2473
|
+
alg,
|
|
2474
|
+
malformed: rawHash != null && hashBytes === null || exclusions === null
|
|
2075
2475
|
};
|
|
2076
2476
|
}
|
|
2077
2477
|
function parseManifest(bytes) {
|
|
@@ -2081,23 +2481,17 @@ function parseManifest(bytes) {
|
|
|
2081
2481
|
if (!manifest) return {
|
|
2082
2482
|
manifest: null,
|
|
2083
2483
|
issuer: null,
|
|
2084
|
-
liveVideo: null,
|
|
2085
|
-
bmff: EMPTY_BMFF_HASH,
|
|
2086
2484
|
internalData: null
|
|
2087
2485
|
};
|
|
2088
2486
|
return {
|
|
2089
2487
|
manifest,
|
|
2090
2488
|
issuer: manifest.signatureInfo?.issuer ?? null,
|
|
2091
|
-
liveVideo: parseLiveVideoAssertion(manifest.assertions),
|
|
2092
|
-
bmff: parseBmffHashAssertion(manifest.assertions),
|
|
2093
2489
|
internalData
|
|
2094
2490
|
};
|
|
2095
2491
|
} catch {
|
|
2096
2492
|
return {
|
|
2097
2493
|
manifest: null,
|
|
2098
2494
|
issuer: null,
|
|
2099
|
-
liveVideo: null,
|
|
2100
|
-
bmff: EMPTY_BMFF_HASH,
|
|
2101
2495
|
internalData: null
|
|
2102
2496
|
};
|
|
2103
2497
|
}
|
|
@@ -2111,8 +2505,7 @@ function collectErrorCodes(hasManifest, hasLiveVideo, streamIdValid, sequenceNum
|
|
|
2111
2505
|
if (!bmffHashMatches) codes.add(LiveVideoStatusCode.SEGMENT_INVALID);
|
|
2112
2506
|
return [...codes];
|
|
2113
2507
|
}
|
|
2114
|
-
async function validateContinuity(
|
|
2115
|
-
const { manifest, liveVideo } = parsed;
|
|
2508
|
+
async function validateContinuity(manifest, liveVideo, lastManifestId, options) {
|
|
2116
2509
|
const method = liveVideo?.continuityMethod ?? null;
|
|
2117
2510
|
if (!method) return [LiveVideoStatusCode.CONTINUITY_METHOD_INVALID];
|
|
2118
2511
|
if (method === CONTINUITY_METHOD_MANIFEST_ID) {
|
|
@@ -2131,16 +2524,21 @@ async function validateContinuity(parsed, lastManifestId, options) {
|
|
|
2131
2524
|
* Recomputes the `c2pa.hash.bmff.v3` content hash from the raw segment bytes and compares
|
|
2132
2525
|
* it against the expected hash in the manifest assertion. Checks live-video assertions
|
|
2133
2526
|
* (sequenceNumber, streamId, continuityMethod) and manifest-ID chain continuity.
|
|
2527
|
+
* The live-video and BMFF hash fields of the result come only from assertions whose
|
|
2528
|
+
* hash matches a claim reference. Check `isValid` before you use any field.
|
|
2134
2529
|
* A validator for an implementer-defined continuity method can be registered
|
|
2135
2530
|
* via {@link ManifestBoxValidationOptions}.
|
|
2136
2531
|
*
|
|
2137
2532
|
* This function is **pure** — it does not access any external state. The
|
|
2138
2533
|
* caller is responsible for persisting `nextManifestId` and `nextState`
|
|
2139
|
-
* between calls.
|
|
2534
|
+
* between calls. A segment that fails verification, is on another stream, or
|
|
2535
|
+
* does not move the sequence number forward returns the received
|
|
2536
|
+
* `lastManifestId` and `state` unchanged. A segment that fails only the
|
|
2537
|
+
* continuity check still updates them.
|
|
2140
2538
|
*
|
|
2141
2539
|
* @param bytes - Raw segment bytes
|
|
2142
|
-
* @param lastManifestId - Manifest ID from the
|
|
2143
|
-
* @param state - Optional state from the
|
|
2540
|
+
* @param lastManifestId - Manifest ID from the last segment that updated the state, or `null` for the first segment
|
|
2541
|
+
* @param state - Optional state from the last segment that updated the state, for the streamId and sequenceNumber checks
|
|
2144
2542
|
* @param options - Optional custom continuity method validator
|
|
2145
2543
|
* @returns Validation result, the manifest ID, and state to persist for the next call
|
|
2146
2544
|
*
|
|
@@ -2150,7 +2548,11 @@ async function validateContinuity(parsed, lastManifestId, options) {
|
|
|
2150
2548
|
* @public
|
|
2151
2549
|
*/
|
|
2152
2550
|
async function validateC2paManifestBoxSegment(bytes, lastManifestId, state, options) {
|
|
2153
|
-
const { manifest, issuer,
|
|
2551
|
+
const { manifest, issuer, internalData } = parseManifest(bytes);
|
|
2552
|
+
const integrity = internalData ? await validateManifestIntegrity(internalData) : null;
|
|
2553
|
+
const verifiedAssertions = integrity?.verifiedAssertions ?? [];
|
|
2554
|
+
const liveVideo = parseLiveVideoAssertion(verifiedAssertions);
|
|
2555
|
+
const bmff = parseBmffHashAssertion(verifiedAssertions);
|
|
2154
2556
|
const sequenceNumber = liveVideo?.sequenceNumber ?? null;
|
|
2155
2557
|
const previousManifestId = liveVideo?.previousManifestId ?? null;
|
|
2156
2558
|
const streamId = liveVideo?.streamId ?? null;
|
|
@@ -2163,17 +2565,12 @@ async function validateC2paManifestBoxSegment(bytes, lastManifestId, state, opti
|
|
|
2163
2565
|
alg: bmff.alg ?? void 0,
|
|
2164
2566
|
offsetPrefixSize: 8
|
|
2165
2567
|
}), bmff.hashBytes);
|
|
2166
|
-
const continuityCodes = await validateContinuity(
|
|
2167
|
-
manifest,
|
|
2168
|
-
issuer,
|
|
2169
|
-
liveVideo,
|
|
2170
|
-
bmff,
|
|
2171
|
-
internalData
|
|
2172
|
-
}, lastManifestId, options);
|
|
2568
|
+
const continuityCodes = await validateContinuity(manifest, liveVideo, lastManifestId, options);
|
|
2173
2569
|
const liveVideoCodes = [...new Set([...collectErrorCodes(manifest !== null, liveVideo !== null, streamIdValid, sequenceNumberValid, bmffHashMatches), ...continuityCodes])];
|
|
2174
|
-
const integrity = internalData ? await validateManifestIntegrity(internalData) : null;
|
|
2175
2570
|
const integrityCodes = integrity?.codes ?? [];
|
|
2176
2571
|
const errorCodes = [...liveVideoCodes, ...integrityCodes];
|
|
2572
|
+
if (bmff.malformed) errorCodes.push(C2paStatusCode.ASSERTION_BMFFHASH_MALFORMED);
|
|
2573
|
+
const advance = manifest !== null && liveVideo !== null && integrity !== null && integrity.codes.length === 0 && bmffHashMatches && !bmff.malformed && streamIdValid && sequenceNumberValid;
|
|
2177
2574
|
const currentManifestId = manifest?.instanceId ?? null;
|
|
2178
2575
|
return {
|
|
2179
2576
|
result: {
|
|
@@ -2188,11 +2585,11 @@ async function validateC2paManifestBoxSegment(bytes, lastManifestId, state, opti
|
|
|
2188
2585
|
isValid: errorCodes.length === 0,
|
|
2189
2586
|
errorCodes
|
|
2190
2587
|
},
|
|
2191
|
-
nextManifestId: currentManifestId ?? lastManifestId,
|
|
2192
|
-
nextState: {
|
|
2588
|
+
nextManifestId: advance ? currentManifestId ?? lastManifestId : lastManifestId,
|
|
2589
|
+
nextState: advance ? {
|
|
2193
2590
|
lastStreamId: streamId ?? state?.lastStreamId,
|
|
2194
2591
|
lastSequenceNumber: sequenceNumber ?? state?.lastSequenceNumber
|
|
2195
|
-
}
|
|
2592
|
+
} : state ?? {}
|
|
2196
2593
|
};
|
|
2197
2594
|
}
|
|
2198
2595
|
|
|
@@ -2215,32 +2612,22 @@ function extractMerkleAuxBoxes(segmentBytes) {
|
|
|
2215
2612
|
}
|
|
2216
2613
|
return payloads;
|
|
2217
2614
|
}
|
|
2218
|
-
function readMapField(map, name) {
|
|
2219
|
-
if (map instanceof Map) return map.get(name);
|
|
2220
|
-
return map[name];
|
|
2221
|
-
}
|
|
2222
2615
|
function decodeFirstCbor(payload) {
|
|
2223
|
-
let first;
|
|
2224
|
-
let found = false;
|
|
2225
2616
|
try {
|
|
2226
|
-
|
|
2227
|
-
first = value;
|
|
2228
|
-
found = true;
|
|
2229
|
-
return false;
|
|
2230
|
-
});
|
|
2617
|
+
return readCborItem(payload).value;
|
|
2231
2618
|
} catch {
|
|
2232
2619
|
return;
|
|
2233
2620
|
}
|
|
2234
|
-
return found ? first : void 0;
|
|
2235
2621
|
}
|
|
2236
2622
|
function parseBmffMerkleMap(payload) {
|
|
2237
2623
|
const decoded = decodeFirstCbor(payload);
|
|
2238
2624
|
if (decoded === null || decoded === void 0 || typeof decoded !== "object") return null;
|
|
2239
|
-
const
|
|
2240
|
-
const
|
|
2241
|
-
const
|
|
2625
|
+
const map = decoded;
|
|
2626
|
+
const uniqueId = asInteger(map["uniqueId"]);
|
|
2627
|
+
const localId = asInteger(map["localId"]);
|
|
2628
|
+
const location = asInteger(map["location"]);
|
|
2242
2629
|
if (uniqueId === null || localId === null || location === null) return null;
|
|
2243
|
-
const rawHashes =
|
|
2630
|
+
const rawHashes = map["hashes"];
|
|
2244
2631
|
if (rawHashes == null) return {
|
|
2245
2632
|
uniqueId,
|
|
2246
2633
|
localId,
|