@svta/cml-c2pa 1.3.0 → 1.4.1
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 +260 -236
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +734 -368
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
package/dist/index.js
CHANGED
|
@@ -1,7 +1,223 @@
|
|
|
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
|
+
//#region src/cbor/readCborItem.ts
|
|
3
|
+
const TEXT_DECODER$4 = /* @__PURE__ */ new TextDecoder("utf-8", { ignoreBOM: true });
|
|
4
|
+
const MAJOR_UINT = 0;
|
|
5
|
+
const MAJOR_NEGINT = 1;
|
|
6
|
+
const MAJOR_BYTES = 2;
|
|
7
|
+
const MAJOR_TEXT = 3;
|
|
8
|
+
const MAJOR_ARRAY = 4;
|
|
9
|
+
const MAJOR_MAP = 5;
|
|
10
|
+
const MAJOR_SIMPLE = 7;
|
|
11
|
+
const INFO_INDEFINITE = 31;
|
|
12
|
+
const INDEFINITE_COUNT = -1;
|
|
13
|
+
const BREAK = 255;
|
|
14
|
+
const SIMPLE_FALSE = 20;
|
|
15
|
+
const SIMPLE_TRUE = 21;
|
|
16
|
+
const SIMPLE_NULL = 22;
|
|
17
|
+
const SIMPLE_UNDEFINED = 23;
|
|
18
|
+
const INFO_HALF_FLOAT = 25;
|
|
19
|
+
const INFO_SINGLE_FLOAT = 26;
|
|
20
|
+
const INFO_DOUBLE_FLOAT = 27;
|
|
21
|
+
const MAX_NESTING_DEPTH = 128;
|
|
22
|
+
const ASCII_FAST_PATH_MAX_LENGTH = 32;
|
|
23
|
+
const MAX_SAFE_HIGH_WORD = 2097152;
|
|
24
|
+
const TWO_POW_32 = 4294967296;
|
|
25
|
+
/**
|
|
26
|
+
* A CBOR tag (RFC 8949 section 3.4) with the bytes of the whole tagged item.
|
|
27
|
+
*
|
|
28
|
+
* A class keeps a tag distinct from a CBOR map with `tag` and `value` keys.
|
|
29
|
+
*
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
var CborTag = class {
|
|
33
|
+
constructor(tag, value, bytes) {
|
|
34
|
+
this.tag = tag;
|
|
35
|
+
this.value = value;
|
|
36
|
+
this.bytes = bytes;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
function malformed() {
|
|
40
|
+
return /* @__PURE__ */ new RangeError("Malformed CBOR data item");
|
|
41
|
+
}
|
|
42
|
+
function incomplete() {
|
|
43
|
+
return /* @__PURE__ */ new RangeError("Incomplete CBOR data item");
|
|
44
|
+
}
|
|
45
|
+
function readUint32(bytes, offset) {
|
|
46
|
+
return (bytes[offset] << 24 >>> 0) + (bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3]);
|
|
47
|
+
}
|
|
48
|
+
function readArgument(reader, info) {
|
|
49
|
+
if (info < 24) return info;
|
|
50
|
+
if (info > INFO_DOUBLE_FLOAT) throw malformed();
|
|
51
|
+
const size = 1 << info - 24;
|
|
52
|
+
const { bytes } = reader;
|
|
53
|
+
const start = reader.offset;
|
|
54
|
+
if (start + size > bytes.length) throw incomplete();
|
|
55
|
+
reader.offset = start + size;
|
|
56
|
+
if (size === 1) return bytes[start];
|
|
57
|
+
if (size === 2) return bytes[start] << 8 | bytes[start + 1];
|
|
58
|
+
if (size === 4) return readUint32(bytes, start);
|
|
59
|
+
const high = readUint32(bytes, start);
|
|
60
|
+
const low = readUint32(bytes, start + 4);
|
|
61
|
+
if (high < MAX_SAFE_HIGH_WORD) return high * TWO_POW_32 + low;
|
|
62
|
+
return BigInt(high) * BigInt(TWO_POW_32) + BigInt(low);
|
|
63
|
+
}
|
|
64
|
+
function halfFloatToNumber(half) {
|
|
65
|
+
const sign = half & 32768 ? -1 : 1;
|
|
66
|
+
const exponent = half >> 10 & 31;
|
|
67
|
+
const fraction = half & 1023;
|
|
68
|
+
if (exponent === 0) return sign * fraction * 2 ** -24;
|
|
69
|
+
if (exponent === 31) return fraction ? NaN : sign * Infinity;
|
|
70
|
+
return sign * (1 + fraction / 1024) * 2 ** (exponent - 15);
|
|
71
|
+
}
|
|
72
|
+
function readSimple(reader, info) {
|
|
73
|
+
if (info < 24) {
|
|
74
|
+
if (info === SIMPLE_FALSE) return false;
|
|
75
|
+
if (info === SIMPLE_TRUE) return true;
|
|
76
|
+
if (info === SIMPLE_NULL) return null;
|
|
77
|
+
if (info === SIMPLE_UNDEFINED) return void 0;
|
|
78
|
+
throw malformed();
|
|
79
|
+
}
|
|
80
|
+
if (info < INFO_HALF_FLOAT || info > INFO_DOUBLE_FLOAT) throw malformed();
|
|
81
|
+
const size = 1 << info - 24;
|
|
82
|
+
const { bytes } = reader;
|
|
83
|
+
const start = reader.offset;
|
|
84
|
+
if (start + size > bytes.length) throw incomplete();
|
|
85
|
+
reader.offset = start + size;
|
|
86
|
+
if (info === INFO_HALF_FLOAT) return halfFloatToNumber(bytes[start] << 8 | bytes[start + 1]);
|
|
87
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset + start, size);
|
|
88
|
+
return info === INFO_SINGLE_FLOAT ? view.getFloat32(0) : view.getFloat64(0);
|
|
89
|
+
}
|
|
90
|
+
function readBytes(reader, length) {
|
|
91
|
+
const { bytes } = reader;
|
|
92
|
+
const start = reader.offset;
|
|
93
|
+
if (typeof length !== "number" || length > bytes.length - start) throw incomplete();
|
|
94
|
+
reader.offset = start + length;
|
|
95
|
+
return bytes.subarray(start, reader.offset);
|
|
96
|
+
}
|
|
97
|
+
function readText(view) {
|
|
98
|
+
if (view.length > ASCII_FAST_PATH_MAX_LENGTH) return TEXT_DECODER$4.decode(view);
|
|
99
|
+
let text = "";
|
|
100
|
+
for (const byte of view) {
|
|
101
|
+
if (byte > 127) return TEXT_DECODER$4.decode(view);
|
|
102
|
+
text += String.fromCharCode(byte);
|
|
103
|
+
}
|
|
104
|
+
return text;
|
|
105
|
+
}
|
|
106
|
+
function enter(reader) {
|
|
107
|
+
if (++reader.depth > MAX_NESTING_DEPTH) throw new RangeError("CBOR nesting deeper than 128 levels");
|
|
108
|
+
}
|
|
109
|
+
function atBreak(reader) {
|
|
110
|
+
if (reader.offset >= reader.bytes.length) throw incomplete();
|
|
111
|
+
if (reader.bytes[reader.offset] !== BREAK) return false;
|
|
112
|
+
reader.offset++;
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
function readArray(reader, count) {
|
|
116
|
+
enter(reader);
|
|
117
|
+
const items = [];
|
|
118
|
+
if (count === INDEFINITE_COUNT) while (!atBreak(reader)) items.push(readItem(reader));
|
|
119
|
+
else {
|
|
120
|
+
if (typeof count !== "number" || count > reader.bytes.length - reader.offset) throw incomplete();
|
|
121
|
+
for (let i = 0; i < count; i++) items.push(readItem(reader));
|
|
122
|
+
}
|
|
123
|
+
reader.depth--;
|
|
124
|
+
return items;
|
|
125
|
+
}
|
|
126
|
+
function setEntry(map, key, value) {
|
|
127
|
+
if (key !== null && typeof key === "object") throw new RangeError("Unsupported CBOR map key");
|
|
128
|
+
const name = String(key);
|
|
129
|
+
if (name === "__proto__") Object.defineProperty(map, name, {
|
|
130
|
+
value,
|
|
131
|
+
enumerable: true,
|
|
132
|
+
writable: true,
|
|
133
|
+
configurable: true
|
|
134
|
+
});
|
|
135
|
+
else map[name] = value;
|
|
136
|
+
}
|
|
137
|
+
function readMap(reader, count) {
|
|
138
|
+
enter(reader);
|
|
139
|
+
const map = {};
|
|
140
|
+
if (count === INDEFINITE_COUNT) while (!atBreak(reader)) setEntry(map, readItem(reader), readItem(reader));
|
|
141
|
+
else {
|
|
142
|
+
if (typeof count !== "number" || count * 2 > reader.bytes.length - reader.offset) throw incomplete();
|
|
143
|
+
for (let i = 0; i < count; i++) setEntry(map, readItem(reader), readItem(reader));
|
|
144
|
+
}
|
|
145
|
+
reader.depth--;
|
|
146
|
+
return map;
|
|
147
|
+
}
|
|
148
|
+
function readTag(reader, tag, start) {
|
|
149
|
+
if (typeof tag !== "number") throw malformed();
|
|
150
|
+
enter(reader);
|
|
151
|
+
const value = readItem(reader);
|
|
152
|
+
reader.depth--;
|
|
153
|
+
return new CborTag(tag, value, reader.bytes.subarray(start, reader.offset));
|
|
154
|
+
}
|
|
155
|
+
function readItem(reader) {
|
|
156
|
+
const { bytes } = reader;
|
|
157
|
+
const start = reader.offset;
|
|
158
|
+
if (start >= bytes.length) throw incomplete();
|
|
159
|
+
const initialByte = bytes[start];
|
|
160
|
+
reader.offset = start + 1;
|
|
161
|
+
const majorType = initialByte >> 5;
|
|
162
|
+
const info = initialByte & 31;
|
|
163
|
+
if (majorType === MAJOR_SIMPLE) return readSimple(reader, info);
|
|
164
|
+
if (info === INFO_INDEFINITE) {
|
|
165
|
+
if (majorType === MAJOR_ARRAY) return readArray(reader, INDEFINITE_COUNT);
|
|
166
|
+
if (majorType === MAJOR_MAP) return readMap(reader, INDEFINITE_COUNT);
|
|
167
|
+
throw malformed();
|
|
168
|
+
}
|
|
169
|
+
const argument = readArgument(reader, info);
|
|
170
|
+
switch (majorType) {
|
|
171
|
+
case MAJOR_UINT: return argument;
|
|
172
|
+
case MAJOR_NEGINT: return typeof argument === "number" && argument < Number.MAX_SAFE_INTEGER ? -1 - argument : BigInt(-1) - BigInt(argument);
|
|
173
|
+
case MAJOR_BYTES: return readBytes(reader, argument);
|
|
174
|
+
case MAJOR_TEXT: return readText(readBytes(reader, argument));
|
|
175
|
+
case MAJOR_ARRAY: return readArray(reader, argument);
|
|
176
|
+
case MAJOR_MAP: return readMap(reader, argument);
|
|
177
|
+
default: return readTag(reader, argument, start);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Reads the CBOR data item that starts at `offset` (RFC 8949 section 3).
|
|
182
|
+
*
|
|
183
|
+
* Integers with a magnitude up to `Number.MAX_SAFE_INTEGER` are numbers, larger ones are BigInts.
|
|
184
|
+
* Byte strings are views of the input. Maps are plain objects with string keys. Every tag is a
|
|
185
|
+
* {@link CborTag}. The work and the memory are bounded by the length of the input: no container is
|
|
186
|
+
* allocated from a declared length, and the nesting of arrays, maps, and tags stops at 128 levels.
|
|
187
|
+
*
|
|
188
|
+
* @param bytes - CBOR bytes
|
|
189
|
+
* @param offset - Offset of the first byte of the data item
|
|
190
|
+
* @returns The decoded value and the offset after the last byte of the data item
|
|
191
|
+
* @throws RangeError If the data item is malformed, incomplete, or nested too deep
|
|
192
|
+
*
|
|
193
|
+
* @internal
|
|
194
|
+
*/
|
|
195
|
+
function readCborItem(bytes, offset = 0) {
|
|
196
|
+
const reader = {
|
|
197
|
+
bytes,
|
|
198
|
+
offset,
|
|
199
|
+
depth: 0
|
|
200
|
+
};
|
|
201
|
+
return {
|
|
202
|
+
value: readItem(reader),
|
|
203
|
+
end: reader.offset
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Decodes CBOR bytes that hold exactly one data item.
|
|
208
|
+
*
|
|
209
|
+
* @param bytes - CBOR bytes of one data item
|
|
210
|
+
* @returns The decoded value, see {@link readCborItem}
|
|
211
|
+
* @throws RangeError If the data item is malformed, incomplete, nested too deep, or followed by trailing bytes
|
|
212
|
+
*
|
|
213
|
+
* @internal
|
|
214
|
+
*/
|
|
215
|
+
function decodeCbor(bytes) {
|
|
216
|
+
const item = readCborItem(bytes);
|
|
217
|
+
if (item.end !== bytes.length) throw new RangeError("Trailing bytes after CBOR data item");
|
|
218
|
+
return item.value;
|
|
219
|
+
}
|
|
220
|
+
//#endregion
|
|
5
221
|
//#region src/LiveVideoStatusCode.ts
|
|
6
222
|
/**
|
|
7
223
|
* Standard C2PA failure status codes for live video validation,
|
|
@@ -14,45 +230,130 @@ import { findIsoBox, readEmsg, readIsoBoxes } from "@svta/cml-iso-bmff";
|
|
|
14
230
|
* @public
|
|
15
231
|
*/
|
|
16
232
|
const LiveVideoStatusCode = {
|
|
233
|
+
/** Init segment contains an `mdat` box (§19.7.1) */
|
|
17
234
|
INIT_INVALID: "livevideo.init.invalid",
|
|
235
|
+
/** C2PA Manifest Box failed standard validation (§19.7.1) */
|
|
18
236
|
MANIFEST_INVALID: "livevideo.manifest.invalid",
|
|
237
|
+
/** Segment invalid: missing Manifest Box/emsg, signature/hash/key failure, or session key outside its validity period (§19.7) */
|
|
19
238
|
SEGMENT_INVALID: "livevideo.segment.invalid",
|
|
239
|
+
/**
|
|
240
|
+
* Live video assertion field invalid: sequenceNumber or streamId mismatch
|
|
241
|
+
* (§19.7.2). Also reused for VOD Merkle per-track location discontinuity
|
|
242
|
+
* (§15.12.2), which has no dedicated code.
|
|
243
|
+
*/
|
|
20
244
|
ASSERTION_INVALID: "livevideo.assertion.invalid",
|
|
245
|
+
/** continuityMethod absent, unsupported, or companion fields incorrect (§19.7.2) */
|
|
21
246
|
CONTINUITY_METHOD_INVALID: "livevideo.continuityMethod.invalid",
|
|
247
|
+
/** Custom continuityMethod with no registered validator (§19.7.2) */
|
|
22
248
|
CONTINUITY_METHOD_UNSUPPORTED: "livevideo.continuityMethod.unsupported",
|
|
249
|
+
/** Session key invalid: signerBinding failed or required fields absent (§19.7.3) */
|
|
23
250
|
SESSIONKEY_INVALID: "livevideo.sessionkey.invalid"
|
|
24
251
|
};
|
|
25
|
-
|
|
252
|
+
//#endregion
|
|
253
|
+
//#region src/cbor/projectCborTags.ts
|
|
254
|
+
const TAG_DATE_TIME_STRING = 0;
|
|
255
|
+
const TAG_EPOCH_DATE_TIME = 1;
|
|
256
|
+
const MILLISECONDS_PER_SECOND$1 = 1e3;
|
|
257
|
+
function setKey(map, key, value) {
|
|
258
|
+
if (key === "__proto__") Object.defineProperty(map, key, {
|
|
259
|
+
value,
|
|
260
|
+
enumerable: true,
|
|
261
|
+
writable: true,
|
|
262
|
+
configurable: true
|
|
263
|
+
});
|
|
264
|
+
else map[key] = value;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Projects a tree from {@link readCborItem} to its public shape.
|
|
268
|
+
*
|
|
269
|
+
* 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).
|
|
270
|
+
* Every other {@link CborTag} becomes a plain `{ tag, value }` object. Arrays and maps are copied.
|
|
271
|
+
* Byte strings and primitive values are returned as they are.
|
|
272
|
+
*
|
|
273
|
+
* @param value - A value from {@link readCborItem}
|
|
274
|
+
* @returns The projected value
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* {@includeCode ../../test/cbor/cborEquivalence.test.ts#example}
|
|
278
|
+
*
|
|
279
|
+
* @internal
|
|
280
|
+
*/
|
|
281
|
+
function projectCborTags(value) {
|
|
282
|
+
if (value instanceof CborTag) {
|
|
283
|
+
const content = value.value;
|
|
284
|
+
if (value.tag === TAG_DATE_TIME_STRING && typeof content === "string") return new Date(content);
|
|
285
|
+
if (value.tag === TAG_EPOCH_DATE_TIME && typeof content === "number") return new Date(Math.round(content * MILLISECONDS_PER_SECOND$1));
|
|
286
|
+
return {
|
|
287
|
+
tag: value.tag,
|
|
288
|
+
value: projectCborTags(content)
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
if (Array.isArray(value)) return value.map(projectCborTags);
|
|
292
|
+
if (value !== null && typeof value === "object" && !(value instanceof Uint8Array)) {
|
|
293
|
+
const map = {};
|
|
294
|
+
for (const key of Object.keys(value)) setKey(map, key, projectCborTags(value[key]));
|
|
295
|
+
return map;
|
|
296
|
+
}
|
|
297
|
+
return value;
|
|
298
|
+
}
|
|
299
|
+
//#endregion
|
|
300
|
+
//#region src/C2paStatusCode.ts
|
|
301
|
+
/**
|
|
302
|
+
* Standard C2PA validation status codes for manifest integrity checks,
|
|
303
|
+
* as defined in the C2PA specification chapters 15 and 18.
|
|
304
|
+
*
|
|
305
|
+
* @see {@link https://c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_claim_validation | C2PA Spec §15.10.3}
|
|
306
|
+
*
|
|
307
|
+
* @enum
|
|
308
|
+
*
|
|
309
|
+
* @public
|
|
310
|
+
*/
|
|
311
|
+
const C2paStatusCode = {
|
|
312
|
+
/** Assertion hash does not match the hashed URI in the claim (§15.10.3.1) */
|
|
313
|
+
ASSERTION_HASHEDURI_MISMATCH: "assertion.hashedURI.mismatch",
|
|
314
|
+
/** The assertion store has no box for a claim reference, or more than one box for its label (§8.4.1, §15.10.3.1) */
|
|
315
|
+
ASSERTION_MISSING: "assertion.missing",
|
|
316
|
+
/** The manifest has more than one hard binding assertion (§9.1, §15.10.1.2) */
|
|
317
|
+
ASSERTION_MULTIPLE_HARD_BINDINGS: "assertion.multipleHardBindings",
|
|
318
|
+
/** The assertion store holds an assertion that the claim does not reference (§15.10.3.1) */
|
|
319
|
+
ASSERTION_UNDECLARED: "assertion.undeclared",
|
|
320
|
+
/** An action requiring an ingredient reference does not have one (§18.15.4.7) */
|
|
321
|
+
ASSERTION_ACTION_INGREDIENT_MISMATCH: "assertion.action.ingredientMismatch",
|
|
322
|
+
/** Claim signature verification failed, or the signature carries no certificate (§15.7) */
|
|
323
|
+
CLAIM_SIGNATURE_MISMATCH: "claimSignature.mismatch",
|
|
324
|
+
/** The manifest has no `c2pa.signature` box, so the claim signature cannot be found (§15.7) */
|
|
325
|
+
CLAIM_SIGNATURE_MISSING: "claimSignature.missing",
|
|
326
|
+
/** The manifest has no claim box (§15.6.1) */
|
|
327
|
+
CLAIM_MISSING: "claim.missing",
|
|
328
|
+
/** The claim box holds CBOR that does not decode (§15.6.2) */
|
|
329
|
+
CLAIM_CBOR_INVALID: "claim.cbor.invalid",
|
|
330
|
+
/** The claim box holds CBOR that is not a map (§15.6.2). The library does not check the fields of the claim. */
|
|
331
|
+
CLAIM_MALFORMED: "claim.malformed",
|
|
332
|
+
/** The manifest has more than one claim box (§15.6.1) */
|
|
333
|
+
CLAIM_MULTIPLE: "claim.multiple",
|
|
334
|
+
/** BMFF hash assertion or Merkle structure is malformed (§15.2.2.3) */
|
|
335
|
+
ASSERTION_BMFFHASH_MALFORMED: "assertion.bmffHash.malformed",
|
|
336
|
+
/** BMFF content hash does not match the committed value (§15.2.2.3) */
|
|
337
|
+
ASSERTION_BMFFHASH_MISMATCH: "assertion.bmffHash.mismatch"
|
|
338
|
+
};
|
|
26
339
|
//#endregion
|
|
27
340
|
//#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;
|
|
341
|
+
const COSE_SIGN1_TAG = 18;
|
|
31
342
|
const COSE_SIGN1_ARRAY_LENGTH = 4;
|
|
32
343
|
const COSE_KEY_KID = 4;
|
|
33
344
|
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
345
|
function toUint8Array$1(value) {
|
|
44
346
|
if (value instanceof Uint8Array) return value;
|
|
45
|
-
|
|
46
|
-
throw new Error(`Expected Uint8Array or number[], got ${typeof value}`);
|
|
347
|
+
throw new Error(`Expected a byte string, got ${typeof value}`);
|
|
47
348
|
}
|
|
48
349
|
/**
|
|
49
|
-
* Decodes a `
|
|
350
|
+
* Decodes a `COSE_Sign1_Tagged` structure from raw bytes (RFC 9052 section 4.2).
|
|
50
351
|
*
|
|
51
|
-
*
|
|
352
|
+
* The bytes must carry CBOR tag 18 in any encoding length (C2PA section 14).
|
|
52
353
|
*
|
|
53
|
-
* @param coseBytes - Raw
|
|
354
|
+
* @param coseBytes - Raw `COSE_Sign1_Tagged` bytes
|
|
54
355
|
* @returns The decoded COSE_Sign1 structure
|
|
55
|
-
* @throws If the bytes do not represent a
|
|
356
|
+
* @throws If the bytes do not represent a `COSE_Sign1` structure with CBOR tag 18
|
|
56
357
|
*
|
|
57
358
|
* @example
|
|
58
359
|
* {@includeCode ../../test/cose/decodeCoseSign1.test.ts#example}
|
|
@@ -61,16 +362,18 @@ function toUint8Array$1(value) {
|
|
|
61
362
|
*/
|
|
62
363
|
function decodeCoseSign1(coseBytes) {
|
|
63
364
|
try {
|
|
64
|
-
const
|
|
365
|
+
const tagged = decodeCbor(coseBytes);
|
|
366
|
+
if (!(tagged instanceof CborTag) || tagged.tag !== COSE_SIGN1_TAG) throw new Error("Invalid COSE_Sign1 structure: expected CBOR tag 18");
|
|
367
|
+
const coseArray = tagged.value;
|
|
65
368
|
if (!Array.isArray(coseArray) || coseArray.length !== COSE_SIGN1_ARRAY_LENGTH) throw new Error("Invalid COSE_Sign1 structure: expected array with 4 elements");
|
|
66
369
|
const [protectedRaw, unprotectedRaw, payloadRaw, signatureRaw] = coseArray;
|
|
67
370
|
const protectedBytes = toUint8Array$1(protectedRaw);
|
|
68
371
|
let protectedHeader = {};
|
|
69
|
-
if (protectedBytes.length > 0) protectedHeader =
|
|
372
|
+
if (protectedBytes.length > 0) protectedHeader = decodeCbor(protectedBytes);
|
|
70
373
|
const unprotectedHeader = unprotectedRaw ?? {};
|
|
71
|
-
const kidRaw =
|
|
374
|
+
const kidRaw = protectedHeader[COSE_KEY_KID] ?? unprotectedHeader[COSE_KEY_KID] ?? null;
|
|
72
375
|
const kid = kidRaw != null ? toUint8Array$1(kidRaw) : null;
|
|
73
|
-
const alg =
|
|
376
|
+
const alg = protectedHeader[COSE_KEY_ALG] ?? unprotectedHeader[COSE_KEY_ALG] ?? null;
|
|
74
377
|
return {
|
|
75
378
|
protectedBytes,
|
|
76
379
|
protectedHeader,
|
|
@@ -84,7 +387,6 @@ function decodeCoseSign1(coseBytes) {
|
|
|
84
387
|
throw new Error(`Failed to decode COSE_Sign1: ${error.message}`);
|
|
85
388
|
}
|
|
86
389
|
}
|
|
87
|
-
|
|
88
390
|
//#endregion
|
|
89
391
|
//#region src/jumbf/parseJumbfBoxes.ts
|
|
90
392
|
/**
|
|
@@ -107,11 +409,9 @@ function parseJumbfBoxes(bytes) {
|
|
|
107
409
|
data: box.view.readData(box.view.bytesRemaining)
|
|
108
410
|
}));
|
|
109
411
|
}
|
|
110
|
-
|
|
111
412
|
//#endregion
|
|
112
413
|
//#region src/jumbf/parseJumbfLabel.ts
|
|
113
414
|
const TEXT_DECODER$3 = /* @__PURE__ */ new TextDecoder();
|
|
114
|
-
const JUMD_UUID_SIZE = 16;
|
|
115
415
|
const JUMD_TOGGLES_OFFSET = 16;
|
|
116
416
|
const JUMD_LABEL_START = 17;
|
|
117
417
|
const LABEL_FLAG_MASK = 3;
|
|
@@ -131,14 +431,13 @@ const LABEL_FLAG_EXPECTED = 3;
|
|
|
131
431
|
* @internal
|
|
132
432
|
*/
|
|
133
433
|
function parseJumbfLabel(jumdData) {
|
|
134
|
-
if (jumdData.length <
|
|
434
|
+
if (jumdData.length < 17) return null;
|
|
135
435
|
if ((jumdData[JUMD_TOGGLES_OFFSET] & LABEL_FLAG_MASK) !== LABEL_FLAG_EXPECTED) return null;
|
|
136
436
|
let end = JUMD_LABEL_START;
|
|
137
437
|
while (end < jumdData.length && jumdData[end] !== 0) end++;
|
|
138
438
|
if (end >= jumdData.length) return null;
|
|
139
439
|
return TEXT_DECODER$3.decode(jumdData.subarray(JUMD_LABEL_START, end));
|
|
140
440
|
}
|
|
141
|
-
|
|
142
441
|
//#endregion
|
|
143
442
|
//#region src/utils.ts
|
|
144
443
|
const MILLISECONDS_PER_SECOND = 1e3;
|
|
@@ -234,14 +533,16 @@ function matchesUuid(usertype, expected) {
|
|
|
234
533
|
}
|
|
235
534
|
/** @internal */
|
|
236
535
|
function toUint8Array(value) {
|
|
237
|
-
|
|
238
|
-
if (Array.isArray(value)) return new Uint8Array(value);
|
|
239
|
-
return null;
|
|
536
|
+
return value instanceof Uint8Array ? value : null;
|
|
240
537
|
}
|
|
241
538
|
/** @internal */
|
|
242
539
|
function asInteger(value) {
|
|
243
540
|
return typeof value === "number" && Number.isInteger(value) ? value : null;
|
|
244
541
|
}
|
|
542
|
+
/** @internal */
|
|
543
|
+
function asUnsignedInteger(value) {
|
|
544
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
545
|
+
}
|
|
245
546
|
function isC2paUuid(usertype) {
|
|
246
547
|
return matchesUuid(usertype, C2PA_MANIFEST_UUID) || matchesUuid(usertype, JUMBF_UUID);
|
|
247
548
|
}
|
|
@@ -258,7 +559,7 @@ function findC2paUuidBox(boxes) {
|
|
|
258
559
|
}
|
|
259
560
|
const FULLBOX_HEADER_SIZE = 4;
|
|
260
561
|
const AUX_UUID_OFFSET_SIZE = 8;
|
|
261
|
-
const TEXT_DECODER$2 = new TextDecoder();
|
|
562
|
+
const TEXT_DECODER$2 = /* @__PURE__ */ new TextDecoder();
|
|
262
563
|
/** @internal */
|
|
263
564
|
function readUuidBoxPurpose(payload) {
|
|
264
565
|
if (payload.length < FULLBOX_HEADER_SIZE) return null;
|
|
@@ -286,21 +587,8 @@ function stripJumbfUuidPrefix(payload) {
|
|
|
286
587
|
if (!prefix || prefix.rest.length < AUX_UUID_OFFSET_SIZE) return null;
|
|
287
588
|
return prefix.rest.subarray(AUX_UUID_OFFSET_SIZE);
|
|
288
589
|
}
|
|
289
|
-
|
|
290
590
|
//#endregion
|
|
291
591
|
//#region src/x509/asn1.ts
|
|
292
|
-
/**
|
|
293
|
-
* Minimal ASN.1 DER parsing helpers for X.509 certificate navigation.
|
|
294
|
-
*
|
|
295
|
-
* @internal
|
|
296
|
-
*/
|
|
297
|
-
const ASN1_TAG_INTEGER = 2;
|
|
298
|
-
const ASN1_TAG_OBJECT_IDENTIFIER = 6;
|
|
299
|
-
const ASN1_TAG_SEQUENCE = 48;
|
|
300
|
-
const ASN1_TAG_SET = 49;
|
|
301
|
-
const ASN1_TAG_UTC_TIME = 23;
|
|
302
|
-
const ASN1_TAG_GENERALIZED_TIME = 24;
|
|
303
|
-
const ASN1_TAG_CONTEXT_0 = 160;
|
|
304
592
|
const ASN1_LONG_FORM_FLAG = 128;
|
|
305
593
|
const ASN1_LONG_FORM_MASK = 127;
|
|
306
594
|
function readLength(bytes, offset) {
|
|
@@ -344,16 +632,15 @@ function readElementRaw(bytes, offset) {
|
|
|
344
632
|
if (!el) return null;
|
|
345
633
|
return bytes.subarray(offset, offset + el.totalSize);
|
|
346
634
|
}
|
|
347
|
-
|
|
348
635
|
//#endregion
|
|
349
636
|
//#region src/x509/extractCertificateInfo.ts
|
|
350
637
|
const TEXT_DECODER$1 = /* @__PURE__ */ new TextDecoder();
|
|
351
|
-
const OID_COMMON_NAME = new Uint8Array([
|
|
638
|
+
const OID_COMMON_NAME = /* @__PURE__ */ new Uint8Array([
|
|
352
639
|
85,
|
|
353
640
|
4,
|
|
354
641
|
3
|
|
355
642
|
]);
|
|
356
|
-
const OID_ORG_NAME = new Uint8Array([
|
|
643
|
+
const OID_ORG_NAME = /* @__PURE__ */ new Uint8Array([
|
|
357
644
|
85,
|
|
358
645
|
4,
|
|
359
646
|
10
|
|
@@ -365,16 +652,16 @@ function matchesOID(value, oid) {
|
|
|
365
652
|
}
|
|
366
653
|
function parseTime(element) {
|
|
367
654
|
const text = TEXT_DECODER$1.decode(element.value);
|
|
368
|
-
if (element.tag ===
|
|
655
|
+
if (element.tag === 23) {
|
|
369
656
|
const yy = parseInt(text.substring(0, 2), 10);
|
|
370
657
|
return `${yy >= 50 ? 1900 + yy : 2e3 + yy}-${text.substring(2, 4)}-${text.substring(4, 6)}T${text.substring(6, 8)}:${text.substring(8, 10)}:${text.substring(10, 12)}Z`;
|
|
371
658
|
}
|
|
372
|
-
if (element.tag ===
|
|
659
|
+
if (element.tag === 24) return `${text.substring(0, 4)}-${text.substring(4, 6)}-${text.substring(6, 8)}T${text.substring(8, 10)}:${text.substring(10, 12)}:${text.substring(12, 14)}Z`;
|
|
373
660
|
return null;
|
|
374
661
|
}
|
|
375
662
|
function findOidValueInSequence(seqValue, targetOID) {
|
|
376
663
|
const oidEl = readElement(seqValue, 0);
|
|
377
|
-
if (!oidEl || oidEl.tag !==
|
|
664
|
+
if (!oidEl || oidEl.tag !== 6) return null;
|
|
378
665
|
if (!matchesOID(oidEl.value, targetOID)) return null;
|
|
379
666
|
const valEl = readElement(seqValue, oidEl.totalSize);
|
|
380
667
|
return valEl ? TEXT_DECODER$1.decode(valEl.value) : null;
|
|
@@ -383,11 +670,11 @@ function findRDNValue(issuerValue, targetOID) {
|
|
|
383
670
|
let offset = 0;
|
|
384
671
|
while (offset < issuerValue.length) {
|
|
385
672
|
const setEl = readElement(issuerValue, offset);
|
|
386
|
-
if (!setEl || setEl.tag !==
|
|
673
|
+
if (!setEl || setEl.tag !== 49) break;
|
|
387
674
|
let setOffset = 0;
|
|
388
675
|
while (setOffset < setEl.value.length) {
|
|
389
676
|
const seqEl = readElement(setEl.value, setOffset);
|
|
390
|
-
if (!seqEl || seqEl.tag !==
|
|
677
|
+
if (!seqEl || seqEl.tag !== 48) break;
|
|
391
678
|
const found = findOidValueInSequence(seqEl.value, targetOID);
|
|
392
679
|
if (found) return found;
|
|
393
680
|
setOffset += seqEl.totalSize;
|
|
@@ -414,29 +701,29 @@ function findRDNValue(issuerValue, targetOID) {
|
|
|
414
701
|
function extractCertificateInfo(certDER) {
|
|
415
702
|
try {
|
|
416
703
|
const cert = readElement(certDER, 0);
|
|
417
|
-
if (!cert || cert.tag !==
|
|
704
|
+
if (!cert || cert.tag !== 48) return null;
|
|
418
705
|
const tbs = readElement(cert.value, 0);
|
|
419
|
-
if (!tbs || tbs.tag !==
|
|
706
|
+
if (!tbs || tbs.tag !== 48) return null;
|
|
420
707
|
let offset = 0;
|
|
421
708
|
let el = readElement(tbs.value, offset);
|
|
422
709
|
if (!el) return null;
|
|
423
|
-
if (el.tag ===
|
|
710
|
+
if (el.tag === 160) {
|
|
424
711
|
offset += el.totalSize;
|
|
425
712
|
el = readElement(tbs.value, offset);
|
|
426
713
|
if (!el) return null;
|
|
427
714
|
}
|
|
428
|
-
if (el.tag !==
|
|
715
|
+
if (el.tag !== 2) return null;
|
|
429
716
|
offset += el.totalSize;
|
|
430
717
|
el = readElement(tbs.value, offset);
|
|
431
|
-
if (!el || el.tag !==
|
|
718
|
+
if (!el || el.tag !== 48) return null;
|
|
432
719
|
offset += el.totalSize;
|
|
433
720
|
el = readElement(tbs.value, offset);
|
|
434
|
-
if (!el || el.tag !==
|
|
721
|
+
if (!el || el.tag !== 48) return null;
|
|
435
722
|
const issuer = findRDNValue(el.value, OID_COMMON_NAME) ?? findRDNValue(el.value, OID_ORG_NAME) ?? "Unknown Issuer";
|
|
436
723
|
offset += el.totalSize;
|
|
437
724
|
el = readElement(tbs.value, offset);
|
|
438
725
|
let notBefore = null;
|
|
439
|
-
if (el && el.tag ===
|
|
726
|
+
if (el && el.tag === 48) {
|
|
440
727
|
const timeEl = readElement(el.value, 0);
|
|
441
728
|
if (timeEl) notBefore = parseTime(timeEl);
|
|
442
729
|
}
|
|
@@ -448,7 +735,6 @@ function extractCertificateInfo(certDER) {
|
|
|
448
735
|
return null;
|
|
449
736
|
}
|
|
450
737
|
}
|
|
451
|
-
|
|
452
738
|
//#endregion
|
|
453
739
|
//#region src/readC2paManifest.ts
|
|
454
740
|
const TEXT_DECODER = /* @__PURE__ */ new TextDecoder();
|
|
@@ -487,21 +773,26 @@ 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;
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
776
|
+
let taggedData;
|
|
777
|
+
if (contentBox) {
|
|
778
|
+
if (contentBox.type === "cbor") try {
|
|
779
|
+
taggedData = decodeCbor(contentBox.data);
|
|
780
|
+
data = projectCborTags(taggedData);
|
|
781
|
+
} catch {
|
|
782
|
+
data = contentBox.data;
|
|
783
|
+
}
|
|
784
|
+
else if (contentBox.type === "json") try {
|
|
785
|
+
data = JSON.parse(TEXT_DECODER.decode(contentBox.data));
|
|
786
|
+
} catch {
|
|
787
|
+
data = contentBox.data;
|
|
788
|
+
}
|
|
789
|
+
else data = contentBox.data;
|
|
499
790
|
}
|
|
500
|
-
else data = contentBox.data;
|
|
501
791
|
assertions.push({
|
|
502
792
|
label,
|
|
503
793
|
data,
|
|
504
|
-
rawBoxPayload: box.data
|
|
794
|
+
rawBoxPayload: box.data,
|
|
795
|
+
taggedData
|
|
505
796
|
});
|
|
506
797
|
}
|
|
507
798
|
return assertions;
|
|
@@ -535,6 +826,25 @@ function parseSignatureInfo(signatureBytes) {
|
|
|
535
826
|
};
|
|
536
827
|
}
|
|
537
828
|
}
|
|
829
|
+
function readClaim(bytes) {
|
|
830
|
+
let value;
|
|
831
|
+
try {
|
|
832
|
+
value = decodeCbor(bytes);
|
|
833
|
+
} catch {
|
|
834
|
+
return {
|
|
835
|
+
data: null,
|
|
836
|
+
code: C2paStatusCode.CLAIM_CBOR_INVALID
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
if (value === null || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype) return {
|
|
840
|
+
data: null,
|
|
841
|
+
code: C2paStatusCode.CLAIM_MALFORMED
|
|
842
|
+
};
|
|
843
|
+
return {
|
|
844
|
+
data: projectCborTags(value),
|
|
845
|
+
code: null
|
|
846
|
+
};
|
|
847
|
+
}
|
|
538
848
|
function extractClaimAssertionRefs(claimData) {
|
|
539
849
|
const created = claimData["created_assertions"];
|
|
540
850
|
const gathered = claimData["gathered_assertions"];
|
|
@@ -550,10 +860,10 @@ function extractClaimAssertionRefs(claimData) {
|
|
|
550
860
|
const e = entry;
|
|
551
861
|
const url = e["url"];
|
|
552
862
|
const hash = e["hash"];
|
|
553
|
-
if (!url ||
|
|
863
|
+
if (!url || hash == null) continue;
|
|
554
864
|
refs.push({
|
|
555
865
|
url,
|
|
556
|
-
hash: hash instanceof Uint8Array ? hash : new Uint8Array(
|
|
866
|
+
hash: hash instanceof Uint8Array ? hash : /* @__PURE__ */ new Uint8Array(0),
|
|
557
867
|
alg: e["alg"] ?? null
|
|
558
868
|
});
|
|
559
869
|
}
|
|
@@ -590,8 +900,12 @@ function readC2paManifest(bytes, preParsedBoxes) {
|
|
|
590
900
|
const manifestBoxes = resolveManifestBoxes(jumbfBoxes);
|
|
591
901
|
let claimData = null;
|
|
592
902
|
let claimCborBytes = null;
|
|
903
|
+
let claimCode = null;
|
|
593
904
|
let internalAssertions = [];
|
|
594
905
|
let signatureBytes = null;
|
|
906
|
+
let claimBoxes = 0;
|
|
907
|
+
let assertionStores = 0;
|
|
908
|
+
let signatureBoxes = 0;
|
|
595
909
|
for (const box of manifestBoxes) {
|
|
596
910
|
if (box.type !== "jumb") continue;
|
|
597
911
|
const inner = parseJumbfBoxes(box.data);
|
|
@@ -600,19 +914,30 @@ function readC2paManifest(bytes, preParsedBoxes) {
|
|
|
600
914
|
const label = parseJumbfLabel(jumd.data);
|
|
601
915
|
if (!label) continue;
|
|
602
916
|
if (label === "c2pa.claim" || label === "c2pa.claim.v2") {
|
|
917
|
+
claimBoxes++;
|
|
603
918
|
const contentBox = inner.find((b) => b.type === "cbor");
|
|
604
919
|
if (contentBox) {
|
|
605
920
|
claimCborBytes = contentBox.data;
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
921
|
+
const claim = readClaim(contentBox.data);
|
|
922
|
+
claimData = claim.data;
|
|
923
|
+
claimCode = claim.code;
|
|
609
924
|
}
|
|
610
|
-
} else if (label === "c2pa.assertions")
|
|
611
|
-
|
|
925
|
+
} else if (label === "c2pa.assertions") {
|
|
926
|
+
assertionStores++;
|
|
927
|
+
internalAssertions = parseAssertionsInternal(inner);
|
|
928
|
+
} else if (label === "c2pa.signature") {
|
|
929
|
+
signatureBoxes++;
|
|
612
930
|
const contentBox = inner.find((b) => b.type === "cbor" || b.type === "jumc");
|
|
613
931
|
if (contentBox) signatureBytes = contentBox.data;
|
|
614
932
|
}
|
|
615
933
|
}
|
|
934
|
+
if (claimBoxes > 1) {
|
|
935
|
+
claimData = null;
|
|
936
|
+
claimCborBytes = null;
|
|
937
|
+
claimCode = C2paStatusCode.CLAIM_MULTIPLE;
|
|
938
|
+
}
|
|
939
|
+
if (assertionStores > 1) internalAssertions = [];
|
|
940
|
+
if (signatureBoxes > 1) signatureBytes = null;
|
|
616
941
|
const { issuer, certNotBefore } = parseSignatureInfo(signatureBytes);
|
|
617
942
|
const instanceId = claimData?.["instanceID"] ?? claimData?.["instance_id"] ?? null;
|
|
618
943
|
const claimGenerator = claimData?.["claim_generator"] ?? claimData?.["claimGenerator"] ?? null;
|
|
@@ -635,11 +960,11 @@ function readC2paManifest(bytes, preParsedBoxes) {
|
|
|
635
960
|
},
|
|
636
961
|
claimAssertionRefs,
|
|
637
962
|
claimCborBytes,
|
|
963
|
+
claimCode,
|
|
638
964
|
signatureBytes,
|
|
639
965
|
assertions: internalAssertions
|
|
640
966
|
};
|
|
641
967
|
}
|
|
642
|
-
|
|
643
968
|
//#endregion
|
|
644
969
|
//#region src/bmff/shouldExcludeBox.ts
|
|
645
970
|
function bytesMatchAt(boxData, offset, expected) {
|
|
@@ -678,7 +1003,6 @@ function shouldExcludeBox(boxType, boxData, exclusions) {
|
|
|
678
1003
|
}
|
|
679
1004
|
return false;
|
|
680
1005
|
}
|
|
681
|
-
|
|
682
1006
|
//#endregion
|
|
683
1007
|
//#region src/bmff/computeBmffHash.ts
|
|
684
1008
|
const MINIMUM_BOX_SIZE = 8;
|
|
@@ -744,30 +1068,54 @@ async function computeBmffHash(segmentBytes, options) {
|
|
|
744
1068
|
const hashBuffer = await crypto.subtle.digest(alg, hashInput);
|
|
745
1069
|
return new Uint8Array(hashBuffer);
|
|
746
1070
|
}
|
|
747
|
-
|
|
748
1071
|
//#endregion
|
|
749
|
-
//#region src/
|
|
1072
|
+
//#region src/bmff/parseExclusions.ts
|
|
1073
|
+
function isMap(value) {
|
|
1074
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1075
|
+
}
|
|
1076
|
+
function parseConstraints(rawConstraints) {
|
|
1077
|
+
if (!Array.isArray(rawConstraints)) return null;
|
|
1078
|
+
const constraints = [];
|
|
1079
|
+
for (const rawConstraint of rawConstraints) {
|
|
1080
|
+
if (!isMap(rawConstraint)) return null;
|
|
1081
|
+
const offset = asUnsignedInteger(rawConstraint["offset"]);
|
|
1082
|
+
const value = toUint8Array(rawConstraint["value"]);
|
|
1083
|
+
if (offset === null || !value) return null;
|
|
1084
|
+
constraints.push({
|
|
1085
|
+
offset,
|
|
1086
|
+
value
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
return constraints;
|
|
1090
|
+
}
|
|
750
1091
|
/**
|
|
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}
|
|
1092
|
+
* Parses the `exclusions` array of a `c2pa.hash.bmff.v3` assertion (C2PA section 18.6).
|
|
755
1093
|
*
|
|
756
|
-
* @
|
|
1094
|
+
* @param rawExclusions - The decoded `exclusions` field
|
|
1095
|
+
* @returns The exclusions, an empty array for an absent field, or `null` if the field does not conform
|
|
757
1096
|
*
|
|
758
|
-
* @
|
|
1097
|
+
* @internal
|
|
759
1098
|
*/
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1099
|
+
function parseExclusions(rawExclusions) {
|
|
1100
|
+
if (rawExclusions == null) return [];
|
|
1101
|
+
if (!Array.isArray(rawExclusions)) return null;
|
|
1102
|
+
const exclusions = [];
|
|
1103
|
+
for (const rawExclusion of rawExclusions) {
|
|
1104
|
+
if (!isMap(rawExclusion) || typeof rawExclusion["xpath"] !== "string") return null;
|
|
1105
|
+
const xpath = rawExclusion["xpath"];
|
|
1106
|
+
if (rawExclusion["data"] == null) {
|
|
1107
|
+
exclusions.push({ xpath });
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
const constraints = parseConstraints(rawExclusion["data"]);
|
|
1111
|
+
if (!constraints) return null;
|
|
1112
|
+
exclusions.push(constraints.length > 0 ? {
|
|
1113
|
+
xpath,
|
|
1114
|
+
data: constraints
|
|
1115
|
+
} : { xpath });
|
|
1116
|
+
}
|
|
1117
|
+
return exclusions;
|
|
1118
|
+
}
|
|
771
1119
|
//#endregion
|
|
772
1120
|
//#region src/extractManifestCertificate.ts
|
|
773
1121
|
const X5CHAIN_COSE_HEADER = 33;
|
|
@@ -787,11 +1135,10 @@ function extractCertificateFromSignatureBytes(signatureBytes) {
|
|
|
787
1135
|
return null;
|
|
788
1136
|
}
|
|
789
1137
|
}
|
|
790
|
-
|
|
791
1138
|
//#endregion
|
|
792
1139
|
//#region src/claim/validateActionIngredients.ts
|
|
793
|
-
const ACTIONS_LABELS = new Set(["c2pa.actions", "c2pa.actions.v2"]);
|
|
794
|
-
const ACTIONS_REQUIRING_INGREDIENTS = new Set([
|
|
1140
|
+
const ACTIONS_LABELS = /* @__PURE__ */ new Set(["c2pa.actions", "c2pa.actions.v2"]);
|
|
1141
|
+
const ACTIONS_REQUIRING_INGREDIENTS = /* @__PURE__ */ new Set([
|
|
795
1142
|
"c2pa.opened",
|
|
796
1143
|
"c2pa.placed",
|
|
797
1144
|
"c2pa.removed"
|
|
@@ -828,7 +1175,6 @@ function validateActionIngredients(assertions) {
|
|
|
828
1175
|
}
|
|
829
1176
|
return codes;
|
|
830
1177
|
}
|
|
831
|
-
|
|
832
1178
|
//#endregion
|
|
833
1179
|
//#region src/claim/validateAssertionHashes.ts
|
|
834
1180
|
/**
|
|
@@ -836,6 +1182,8 @@ function validateActionIngredients(assertions) {
|
|
|
836
1182
|
*
|
|
837
1183
|
* Handles both absolute (`self#jumbf=/c2pa/<manifest>/c2pa.assertions/<label>`)
|
|
838
1184
|
* and relative URI forms by returning the last path segment.
|
|
1185
|
+
*
|
|
1186
|
+
* @internal
|
|
839
1187
|
*/
|
|
840
1188
|
function extractLabelFromUrl(url) {
|
|
841
1189
|
const fragmentIndex = url.indexOf("#jumbf=");
|
|
@@ -844,37 +1192,65 @@ function extractLabelFromUrl(url) {
|
|
|
844
1192
|
return lastSlash >= 0 ? path.substring(lastSlash + 1) : path;
|
|
845
1193
|
}
|
|
846
1194
|
/**
|
|
847
|
-
* Validates assertion hashes and presence per C2PA §15.10.3.1.
|
|
1195
|
+
* Validates assertion hashes and presence per C2PA §8.4.1 and §15.10.3.1.
|
|
848
1196
|
*
|
|
849
1197
|
* For each assertion referenced in the claim's hashed URI list:
|
|
850
|
-
* -
|
|
1198
|
+
* - Resolves the label to a box in the assertion store. If the store holds no box
|
|
1199
|
+
* with that label, or more than one, the reference is unresolved (`assertion.missing`, §8.4.1).
|
|
851
1200
|
* - Recomputes the hash of the raw JUMBF box payload and compares it to the
|
|
852
|
-
* expected hash from the claim (`assertion.hashedURI.mismatch`)
|
|
1201
|
+
* expected hash from the claim (`assertion.hashedURI.mismatch`).
|
|
1202
|
+
*
|
|
1203
|
+
* The `verified` list holds only the assertions that resolved to one box with a matching
|
|
1204
|
+
* hash. A caller that consumes an assertion by label must read it from `verified`, so that
|
|
1205
|
+
* an unverified or ambiguous box is never used.
|
|
853
1206
|
*
|
|
854
1207
|
* @param claimRefs - Hashed URI references from the claim
|
|
855
1208
|
* @param assertions - Parsed assertions with preserved raw box payloads
|
|
856
|
-
* @returns
|
|
1209
|
+
* @returns The status codes for any failures found, and the verified assertions
|
|
857
1210
|
*
|
|
858
1211
|
* @internal
|
|
859
1212
|
*/
|
|
860
1213
|
async function validateAssertionHashes(claimRefs, assertions) {
|
|
861
|
-
if (claimRefs.length === 0) return
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1214
|
+
if (claimRefs.length === 0) return {
|
|
1215
|
+
codes: [],
|
|
1216
|
+
verified: []
|
|
1217
|
+
};
|
|
1218
|
+
const boxesByLabel = /* @__PURE__ */ new Map();
|
|
1219
|
+
for (const assertion of assertions) {
|
|
1220
|
+
const existing = boxesByLabel.get(assertion.label);
|
|
1221
|
+
if (existing) existing.push(assertion);
|
|
1222
|
+
else boxesByLabel.set(assertion.label, [assertion]);
|
|
1223
|
+
}
|
|
865
1224
|
const hashChecks = claimRefs.map(async (ref) => {
|
|
866
1225
|
const label = extractLabelFromUrl(ref.url);
|
|
867
|
-
const
|
|
868
|
-
if (!
|
|
1226
|
+
const boxes = boxesByLabel.get(label);
|
|
1227
|
+
if (!boxes || boxes.length !== 1) return {
|
|
1228
|
+
code: C2paStatusCode.ASSERTION_MISSING,
|
|
1229
|
+
verified: null
|
|
1230
|
+
};
|
|
1231
|
+
const assertion = boxes[0];
|
|
869
1232
|
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
|
-
|
|
1233
|
+
if (!hashesEqual(new Uint8Array(await crypto.subtle.digest(alg, assertion.rawBoxPayload)), ref.hash)) return {
|
|
1234
|
+
code: C2paStatusCode.ASSERTION_HASHEDURI_MISMATCH,
|
|
1235
|
+
verified: null
|
|
1236
|
+
};
|
|
1237
|
+
return {
|
|
1238
|
+
code: null,
|
|
1239
|
+
verified: assertion
|
|
1240
|
+
};
|
|
872
1241
|
});
|
|
873
1242
|
const results = await Promise.all(hashChecks);
|
|
874
|
-
|
|
875
|
-
|
|
1243
|
+
const codes = [];
|
|
1244
|
+
const verified = [];
|
|
1245
|
+
for (const result of results) {
|
|
1246
|
+
if (result.code) codes.push(result.code);
|
|
1247
|
+
if (result.verified) verified.push(result.verified);
|
|
1248
|
+
}
|
|
1249
|
+
return {
|
|
1250
|
+
codes,
|
|
1251
|
+
verified
|
|
1252
|
+
};
|
|
876
1253
|
}
|
|
877
|
-
|
|
878
1254
|
//#endregion
|
|
879
1255
|
//#region src/cose/constants.ts
|
|
880
1256
|
const ECDSA_ALGORITHM = "ECDSA";
|
|
@@ -886,10 +1262,9 @@ const CURVE_P521 = "P-521";
|
|
|
886
1262
|
const HASH_SHA256 = "SHA-256";
|
|
887
1263
|
const HASH_SHA384 = "SHA-384";
|
|
888
1264
|
const HASH_SHA512 = "SHA-512";
|
|
889
|
-
|
|
890
1265
|
//#endregion
|
|
891
1266
|
//#region src/cose/resolveAlgorithmFromCoseAlg.ts
|
|
892
|
-
const COSE_ALGORITHM_MAP = new Map([
|
|
1267
|
+
const COSE_ALGORITHM_MAP = /* @__PURE__ */ new Map([
|
|
893
1268
|
[-7, {
|
|
894
1269
|
name: ECDSA_ALGORITHM,
|
|
895
1270
|
namedCurve: CURVE_P256
|
|
@@ -931,7 +1306,6 @@ function resolveAlgorithmFromCoseAlg(coseAlg) {
|
|
|
931
1306
|
if (!algorithm) throw new Error(`Unsupported COSE algorithm: ${coseAlg}`);
|
|
932
1307
|
return algorithm;
|
|
933
1308
|
}
|
|
934
|
-
|
|
935
1309
|
//#endregion
|
|
936
1310
|
//#region src/cose/buildSigStructure.ts
|
|
937
1311
|
const CBOR_ARRAY_FOUR_ITEMS = 132;
|
|
@@ -983,7 +1357,7 @@ function writeByteStringHeader(output, offset, length) {
|
|
|
983
1357
|
*
|
|
984
1358
|
* @public
|
|
985
1359
|
*/
|
|
986
|
-
function buildSigStructure(protectedBytes, payload, externalAad = new Uint8Array(0)) {
|
|
1360
|
+
function buildSigStructure(protectedBytes, payload, externalAad = /* @__PURE__ */ new Uint8Array(0)) {
|
|
987
1361
|
const totalLength = 2 + COSE_SIG1_CONTEXT_BYTES.length + byteStringHeaderSize(protectedBytes.length) + protectedBytes.length + byteStringHeaderSize(externalAad.length) + externalAad.length + byteStringHeaderSize(payload.length) + payload.length;
|
|
988
1362
|
const result = new Uint8Array(totalLength);
|
|
989
1363
|
let offset = 0;
|
|
@@ -1001,20 +1375,32 @@ function buildSigStructure(protectedBytes, payload, externalAad = new Uint8Array
|
|
|
1001
1375
|
result.set(payload, offset);
|
|
1002
1376
|
return result;
|
|
1003
1377
|
}
|
|
1004
|
-
|
|
1378
|
+
/**
|
|
1379
|
+
* Encodes bytes as a CBOR byte string (RFC 8949 §3.1, major type 2), without a tag.
|
|
1380
|
+
*
|
|
1381
|
+
* @param bytes - Bytes to encode
|
|
1382
|
+
* @returns The byte string header, followed by `bytes`
|
|
1383
|
+
*
|
|
1384
|
+
* @internal
|
|
1385
|
+
*/
|
|
1386
|
+
function encodeCborByteString(bytes) {
|
|
1387
|
+
const result = new Uint8Array(byteStringHeaderSize(bytes.length) + bytes.length);
|
|
1388
|
+
result.set(bytes, writeByteStringHeader(result, 0, bytes.length));
|
|
1389
|
+
return result;
|
|
1390
|
+
}
|
|
1005
1391
|
//#endregion
|
|
1006
1392
|
//#region src/cose/verifyCoseSign1.ts
|
|
1007
1393
|
const DER_SEQUENCE_TAG = 48;
|
|
1008
1394
|
const DER_INTEGER_TAG = 2;
|
|
1009
1395
|
const CURVE_COMPONENT_BYTES = {
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1396
|
+
"P-256": 32,
|
|
1397
|
+
"P-384": 48,
|
|
1398
|
+
"P-521": 66
|
|
1013
1399
|
};
|
|
1014
1400
|
const RSA_PSS_SALT_LENGTH = {
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1401
|
+
"SHA-256": 32,
|
|
1402
|
+
"SHA-384": 48,
|
|
1403
|
+
"SHA-512": 64
|
|
1018
1404
|
};
|
|
1019
1405
|
function getComponentSize(publicKey) {
|
|
1020
1406
|
const curve = publicKey.algorithm.namedCurve;
|
|
@@ -1068,22 +1454,25 @@ function derToRawEcdsaSignature(der, componentSize) {
|
|
|
1068
1454
|
}
|
|
1069
1455
|
function resolveVerifyAlgorithm(publicKey) {
|
|
1070
1456
|
const { name } = publicKey.algorithm;
|
|
1071
|
-
if (name ===
|
|
1072
|
-
if (name ===
|
|
1457
|
+
if (name === "Ed25519") return { name: ED25519_ALGORITHM };
|
|
1458
|
+
if (name === "ECDSA") {
|
|
1073
1459
|
const curve = publicKey.algorithm.namedCurve;
|
|
1074
1460
|
return {
|
|
1075
1461
|
name: ECDSA_ALGORITHM,
|
|
1076
|
-
hash: { name: curve ===
|
|
1462
|
+
hash: { name: curve === "P-384" ? HASH_SHA384 : curve === "P-521" ? HASH_SHA512 : HASH_SHA256 }
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
if (name === "RSA-PSS") {
|
|
1466
|
+
const hashName = publicKey.algorithm.hash.name;
|
|
1467
|
+
return {
|
|
1468
|
+
name: RSA_PSS_ALGORITHM,
|
|
1469
|
+
saltLength: RSA_PSS_SALT_LENGTH[hashName] ?? 32
|
|
1077
1470
|
};
|
|
1078
1471
|
}
|
|
1079
|
-
if (name === RSA_PSS_ALGORITHM) return {
|
|
1080
|
-
name: RSA_PSS_ALGORITHM,
|
|
1081
|
-
saltLength: RSA_PSS_SALT_LENGTH[publicKey.algorithm.hash.name] ?? 32
|
|
1082
|
-
};
|
|
1083
1472
|
throw new Error(`Unsupported public key algorithm: ${name}`);
|
|
1084
1473
|
}
|
|
1085
1474
|
function normalizeSignature(signature, publicKey) {
|
|
1086
|
-
if (publicKey.algorithm.name !==
|
|
1475
|
+
if (publicKey.algorithm.name !== "ECDSA") return signature;
|
|
1087
1476
|
const componentSize = getComponentSize(publicKey);
|
|
1088
1477
|
if (isDerEncoded(signature, componentSize * 2)) return derToRawEcdsaSignature(signature, componentSize);
|
|
1089
1478
|
return signature;
|
|
@@ -1114,7 +1503,6 @@ async function verifyCoseSign1(coseSign1, payload, publicKey) {
|
|
|
1114
1503
|
const signature = normalizeSignature(coseSign1.signature, publicKey);
|
|
1115
1504
|
return crypto.subtle.verify(algorithm, publicKey, signature, sigStructure);
|
|
1116
1505
|
}
|
|
1117
|
-
|
|
1118
1506
|
//#endregion
|
|
1119
1507
|
//#region src/x509/extractCertificateSpki.ts
|
|
1120
1508
|
/**
|
|
@@ -1135,40 +1523,39 @@ async function verifyCoseSign1(coseSign1, payload, publicKey) {
|
|
|
1135
1523
|
function extractCertificateSpki(certDER) {
|
|
1136
1524
|
try {
|
|
1137
1525
|
const cert = readElement(certDER, 0);
|
|
1138
|
-
if (!cert || cert.tag !==
|
|
1526
|
+
if (!cert || cert.tag !== 48) return null;
|
|
1139
1527
|
const tbs = readElement(cert.value, 0);
|
|
1140
|
-
if (!tbs || tbs.tag !==
|
|
1528
|
+
if (!tbs || tbs.tag !== 48) return null;
|
|
1141
1529
|
let offset = 0;
|
|
1142
1530
|
let el = readElement(tbs.value, offset);
|
|
1143
1531
|
if (!el) return null;
|
|
1144
|
-
if (el.tag ===
|
|
1532
|
+
if (el.tag === 160) {
|
|
1145
1533
|
offset += el.totalSize;
|
|
1146
1534
|
el = readElement(tbs.value, offset);
|
|
1147
1535
|
if (!el) return null;
|
|
1148
1536
|
}
|
|
1149
|
-
if (el.tag !==
|
|
1537
|
+
if (el.tag !== 2) return null;
|
|
1150
1538
|
offset += el.totalSize;
|
|
1151
1539
|
el = readElement(tbs.value, offset);
|
|
1152
|
-
if (!el || el.tag !==
|
|
1540
|
+
if (!el || el.tag !== 48) return null;
|
|
1153
1541
|
offset += el.totalSize;
|
|
1154
1542
|
el = readElement(tbs.value, offset);
|
|
1155
|
-
if (!el || el.tag !==
|
|
1543
|
+
if (!el || el.tag !== 48) return null;
|
|
1156
1544
|
offset += el.totalSize;
|
|
1157
1545
|
el = readElement(tbs.value, offset);
|
|
1158
|
-
if (!el || el.tag !==
|
|
1546
|
+
if (!el || el.tag !== 48) return null;
|
|
1159
1547
|
offset += el.totalSize;
|
|
1160
1548
|
el = readElement(tbs.value, offset);
|
|
1161
|
-
if (!el || el.tag !==
|
|
1549
|
+
if (!el || el.tag !== 48) return null;
|
|
1162
1550
|
offset += el.totalSize;
|
|
1163
1551
|
return readElementRaw(tbs.value, offset);
|
|
1164
1552
|
} catch {
|
|
1165
1553
|
return null;
|
|
1166
1554
|
}
|
|
1167
1555
|
}
|
|
1168
|
-
|
|
1169
1556
|
//#endregion
|
|
1170
1557
|
//#region src/x509/normalizeRsaPssSpki.ts
|
|
1171
|
-
const OID_RSASSA_PSS = new Uint8Array([
|
|
1558
|
+
const OID_RSASSA_PSS = /* @__PURE__ */ new Uint8Array([
|
|
1172
1559
|
6,
|
|
1173
1560
|
9,
|
|
1174
1561
|
42,
|
|
@@ -1181,7 +1568,7 @@ const OID_RSASSA_PSS = new Uint8Array([
|
|
|
1181
1568
|
1,
|
|
1182
1569
|
10
|
|
1183
1570
|
]);
|
|
1184
|
-
const RSA_ENCRYPTION_ALGORITHM_ID = new Uint8Array([
|
|
1571
|
+
const RSA_ENCRYPTION_ALGORITHM_ID = /* @__PURE__ */ new Uint8Array([
|
|
1185
1572
|
48,
|
|
1186
1573
|
13,
|
|
1187
1574
|
6,
|
|
@@ -1222,15 +1609,15 @@ function containsRsaPssOid(bytes) {
|
|
|
1222
1609
|
function normalizeRsaPssSpki(spkiBytes) {
|
|
1223
1610
|
if (!containsRsaPssOid(spkiBytes)) return spkiBytes;
|
|
1224
1611
|
const outer = readElement(spkiBytes, 0);
|
|
1225
|
-
if (!outer || outer.tag !==
|
|
1612
|
+
if (!outer || outer.tag !== 48) return spkiBytes;
|
|
1226
1613
|
const algId = readElement(outer.value, 0);
|
|
1227
|
-
if (!algId || algId.tag !==
|
|
1614
|
+
if (!algId || algId.tag !== 48) return spkiBytes;
|
|
1228
1615
|
const publicKeyBitString = outer.value.subarray(algId.totalSize);
|
|
1229
1616
|
const innerLength = RSA_ENCRYPTION_ALGORITHM_ID.length + publicKeyBitString.length;
|
|
1230
1617
|
const headerBytes = encodeDerLength(innerLength);
|
|
1231
1618
|
const result = new Uint8Array(1 + headerBytes.length + innerLength);
|
|
1232
1619
|
let offset = 0;
|
|
1233
|
-
result[offset++] =
|
|
1620
|
+
result[offset++] = 48;
|
|
1234
1621
|
result.set(headerBytes, offset);
|
|
1235
1622
|
offset += headerBytes.length;
|
|
1236
1623
|
result.set(RSA_ENCRYPTION_ALGORITHM_ID, offset);
|
|
@@ -1253,7 +1640,6 @@ function encodeDerLength(length) {
|
|
|
1253
1640
|
length & 255
|
|
1254
1641
|
]);
|
|
1255
1642
|
}
|
|
1256
|
-
|
|
1257
1643
|
//#endregion
|
|
1258
1644
|
//#region src/claim/verifyClaimSignature.ts
|
|
1259
1645
|
/**
|
|
@@ -1283,16 +1669,28 @@ async function verifyClaimSignature(signatureBytes, claimCborBytes, certificateD
|
|
|
1283
1669
|
return false;
|
|
1284
1670
|
}
|
|
1285
1671
|
}
|
|
1286
|
-
|
|
1287
1672
|
//#endregion
|
|
1288
1673
|
//#region src/claim/validateManifestIntegrity.ts
|
|
1674
|
+
const HARD_BINDING_LABELS = /* @__PURE__ */ new Set([
|
|
1675
|
+
"c2pa.hash.data",
|
|
1676
|
+
"c2pa.hash.boxes",
|
|
1677
|
+
"c2pa.hash.collection.data",
|
|
1678
|
+
"c2pa.hash.bmff.v2",
|
|
1679
|
+
"c2pa.hash.bmff.v3"
|
|
1680
|
+
]);
|
|
1681
|
+
const ASSERTION_INSTANCE_SUFFIX = /__\d+$/;
|
|
1289
1682
|
/**
|
|
1290
1683
|
* Checks the claim signature per §15.7 and fails closed: a manifest without a claim box,
|
|
1291
1684
|
* 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.
|
|
1685
|
+
* not verify over the claim bytes, is reported as a failure. A manifest with more than one
|
|
1686
|
+
* claim box reports `CLAIM_MULTIPLE` through `claimCode` (§15.6.1), so no signature check runs.
|
|
1293
1687
|
*/
|
|
1294
|
-
async function validateClaimSignature({ claimCborBytes, signatureBytes }) {
|
|
1688
|
+
async function validateClaimSignature({ claimCborBytes, claimCode, signatureBytes }) {
|
|
1295
1689
|
const certificate = signatureBytes ? extractCertificateFromSignatureBytes(signatureBytes) : null;
|
|
1690
|
+
if (claimCode === C2paStatusCode.CLAIM_MULTIPLE) return {
|
|
1691
|
+
code: null,
|
|
1692
|
+
certificate
|
|
1693
|
+
};
|
|
1296
1694
|
if (!claimCborBytes) return {
|
|
1297
1695
|
code: C2paStatusCode.CLAIM_MISSING,
|
|
1298
1696
|
certificate
|
|
@@ -1313,30 +1711,39 @@ async function validateClaimSignature({ claimCborBytes, signatureBytes }) {
|
|
|
1313
1711
|
/**
|
|
1314
1712
|
* Validates the integrity of a C2PA manifest per Chapter 15 and Chapter 18.
|
|
1315
1713
|
*
|
|
1316
|
-
* Runs
|
|
1317
|
-
* 1. Assertion hash verification (§15.10.3.1)
|
|
1318
|
-
* 2.
|
|
1319
|
-
* 3.
|
|
1320
|
-
* 4.
|
|
1714
|
+
* Runs these checks, in parallel where possible:
|
|
1715
|
+
* 1. Assertion reference resolution and hash verification (§8.4.1, §15.10.3.1)
|
|
1716
|
+
* 2. Undeclared assertion detection (§15.10.3.1)
|
|
1717
|
+
* 3. Hard binding count (§9.1, §15.10.1.2)
|
|
1718
|
+
* 4. Action ingredient validation (§18.15.4.7)
|
|
1719
|
+
* 5. Claim signature verification (§15.7)
|
|
1321
1720
|
*
|
|
1322
1721
|
* The claim signature is verified with the end-entity certificate from the `x5chain`
|
|
1323
1722
|
* header of the signature. The certificate is not checked against a trust list.
|
|
1324
1723
|
*
|
|
1325
1724
|
* @param internal - Enriched manifest data with raw assertion bytes and claim references
|
|
1326
|
-
* @returns The status codes for any failures found,
|
|
1725
|
+
* @returns The status codes for any failures found, the certificate of the claim signature, and the claim-verified assertions
|
|
1327
1726
|
*
|
|
1328
1727
|
* @internal
|
|
1329
1728
|
*/
|
|
1330
1729
|
async function validateManifestIntegrity(internal) {
|
|
1331
|
-
const [
|
|
1332
|
-
const codes = [...
|
|
1730
|
+
const [assertionHashes, signature] = await Promise.all([validateAssertionHashes(internal.claimAssertionRefs, internal.assertions), validateClaimSignature(internal)]);
|
|
1731
|
+
const codes = [...assertionHashes.codes, ...validateActionIngredients(internal.assertions)];
|
|
1732
|
+
let hardBindingCount = 0;
|
|
1733
|
+
for (const assertion of internal.assertions) if (HARD_BINDING_LABELS.has(assertion.label.replace(ASSERTION_INSTANCE_SUFFIX, ""))) hardBindingCount++;
|
|
1734
|
+
if (hardBindingCount > 1) codes.push(C2paStatusCode.ASSERTION_MULTIPLE_HARD_BINDINGS);
|
|
1735
|
+
if (internal.claimCborBytes && !internal.claimCode) {
|
|
1736
|
+
const referencedLabels = new Set(internal.claimAssertionRefs.map((ref) => extractLabelFromUrl(ref.url)));
|
|
1737
|
+
if (internal.assertions.some((assertion) => !referencedLabels.has(assertion.label))) codes.push(C2paStatusCode.ASSERTION_UNDECLARED);
|
|
1738
|
+
}
|
|
1333
1739
|
if (signature.code) codes.push(signature.code);
|
|
1740
|
+
if (internal.claimCode) codes.push(internal.claimCode);
|
|
1334
1741
|
return {
|
|
1335
|
-
codes,
|
|
1336
|
-
certificate: signature.certificate
|
|
1742
|
+
codes: [...new Set(codes)],
|
|
1743
|
+
certificate: signature.certificate,
|
|
1744
|
+
verifiedAssertions: assertionHashes.verified
|
|
1337
1745
|
};
|
|
1338
1746
|
}
|
|
1339
|
-
|
|
1340
1747
|
//#endregion
|
|
1341
1748
|
//#region src/cose/convertCoseKeyToJwk.ts
|
|
1342
1749
|
const OKP_KEY_TYPE = 1;
|
|
@@ -1352,12 +1759,7 @@ const OKP_CURVE_NAMES = {
|
|
|
1352
1759
|
6: "Ed25519",
|
|
1353
1760
|
7: "Ed448"
|
|
1354
1761
|
};
|
|
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);
|
|
1762
|
+
function toBase64Url(bytes) {
|
|
1361
1763
|
let binary = "";
|
|
1362
1764
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
1363
1765
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
@@ -1366,8 +1768,7 @@ function toBase64Url(value) {
|
|
|
1366
1768
|
* Converts a COSE public key (RFC 9052 / IANA COSE Key registry) to JWK format.
|
|
1367
1769
|
*
|
|
1368
1770
|
* Supports EC2 keys (P-256, P-384, P-521) and OKP keys (Ed25519, Ed448, X25519, X448).
|
|
1369
|
-
*
|
|
1370
|
-
* object with integer keys.
|
|
1771
|
+
* The input is a plain object whose keys are the integer labels of the COSE key, as the CBOR reader decodes a map.
|
|
1371
1772
|
*
|
|
1372
1773
|
* @param coseKey - COSE key structure as decoded from a C2PA `c2pa.session-keys` assertion
|
|
1373
1774
|
* @returns JWK representation of the public key
|
|
@@ -1380,13 +1781,13 @@ function toBase64Url(value) {
|
|
|
1380
1781
|
*/
|
|
1381
1782
|
function convertCoseKeyToJwk(coseKey) {
|
|
1382
1783
|
const key = coseKey;
|
|
1383
|
-
const kty =
|
|
1384
|
-
const crv =
|
|
1385
|
-
const x =
|
|
1784
|
+
const kty = key[1];
|
|
1785
|
+
const crv = key[-1];
|
|
1786
|
+
const x = key[-2];
|
|
1386
1787
|
if (kty === EC2_KEY_TYPE) {
|
|
1387
1788
|
const curveName = EC_CURVE_NAMES[crv];
|
|
1388
1789
|
if (!curveName) throw new Error(`Unsupported EC curve: ${crv}`);
|
|
1389
|
-
const y =
|
|
1790
|
+
const y = key[-3];
|
|
1390
1791
|
if (!(x instanceof Uint8Array)) throw new Error("EC2 key missing or invalid x coordinate");
|
|
1391
1792
|
if (!(y instanceof Uint8Array)) throw new Error("EC2 key missing or invalid y coordinate");
|
|
1392
1793
|
return {
|
|
@@ -1399,6 +1800,7 @@ function convertCoseKeyToJwk(coseKey) {
|
|
|
1399
1800
|
if (kty === OKP_KEY_TYPE) {
|
|
1400
1801
|
const curveName = OKP_CURVE_NAMES[crv];
|
|
1401
1802
|
if (!curveName) throw new Error(`Unsupported OKP curve: ${crv}`);
|
|
1803
|
+
if (!(x instanceof Uint8Array)) throw new Error("OKP key missing or invalid x coordinate");
|
|
1402
1804
|
return {
|
|
1403
1805
|
kty: "OKP",
|
|
1404
1806
|
crv: curveName,
|
|
@@ -1407,7 +1809,6 @@ function convertCoseKeyToJwk(coseKey) {
|
|
|
1407
1809
|
}
|
|
1408
1810
|
throw new Error(`Unsupported COSE key type: ${kty}`);
|
|
1409
1811
|
}
|
|
1410
|
-
|
|
1411
1812
|
//#endregion
|
|
1412
1813
|
//#region src/cose/resolveImportAlgorithm.ts
|
|
1413
1814
|
const KEY_TYPE_OKP = "OKP";
|
|
@@ -1417,20 +1818,26 @@ function resolveImportAlgorithm(jwk) {
|
|
|
1417
1818
|
namedCurve: jwk.crv
|
|
1418
1819
|
};
|
|
1419
1820
|
}
|
|
1420
|
-
|
|
1421
1821
|
//#endregion
|
|
1422
1822
|
//#region src/cose/verifySignerBinding.ts
|
|
1823
|
+
async function verifyPayload(coseSign1, payload, publicKey) {
|
|
1824
|
+
const embeddedPayload = coseSign1.payload;
|
|
1825
|
+
return (!embeddedPayload?.length || hashesEqual(embeddedPayload, payload)) && verifyCoseSign1(coseSign1, payload, publicKey);
|
|
1826
|
+
}
|
|
1423
1827
|
/**
|
|
1424
|
-
* Verifies a C2PA signer binding
|
|
1425
|
-
*
|
|
1828
|
+
* Verifies a C2PA signer binding (C2PA spec §18.25.2). A signer binding is a
|
|
1829
|
+
* `COSE_Sign1` structure that proves the content signer authorized a session key.
|
|
1426
1830
|
*
|
|
1427
|
-
* The `Sig_Structure` payload is
|
|
1831
|
+
* The `Sig_Structure` payload is the end-entity certificate. The function also accepts
|
|
1832
|
+
* a payload that encodes the certificate as a CBOR byte string.
|
|
1833
|
+
* The payload field of the `COSE_Sign1` structure must be nil, empty, or equal to
|
|
1834
|
+
* the `Sig_Structure` payload that verifies.
|
|
1428
1835
|
* DER-encoded ECDSA signatures are automatically normalized to raw format.
|
|
1429
1836
|
*
|
|
1430
1837
|
* @param signerBindingBytes - Raw `COSE_Sign1` bytes of the signer binding
|
|
1431
1838
|
* @param sessionCoseKey - COSE public key from the `c2pa.session-keys` assertion
|
|
1432
1839
|
* @param signerCertBytes - DER-encoded end-entity certificate (from `x5chain`)
|
|
1433
|
-
* @returns `true` if the signer binding
|
|
1840
|
+
* @returns `true` if the signature and the payload field of the signer binding are valid
|
|
1434
1841
|
* @throws If the COSE key type is not supported or decoding fails
|
|
1435
1842
|
*
|
|
1436
1843
|
* @example
|
|
@@ -1442,47 +1849,9 @@ async function verifySignerBinding(signerBindingBytes, sessionCoseKey, signerCer
|
|
|
1442
1849
|
const coseSign1 = decodeCoseSign1(signerBindingBytes);
|
|
1443
1850
|
const jwk = convertCoseKeyToJwk(sessionCoseKey);
|
|
1444
1851
|
const publicKey = await crypto.subtle.importKey("jwk", jwk, resolveImportAlgorithm(jwk), false, ["verify"]);
|
|
1445
|
-
|
|
1852
|
+
if (await verifyPayload(coseSign1, signerCertBytes, publicKey)) return true;
|
|
1853
|
+
return verifyPayload(coseSign1, encodeCborByteString(signerCertBytes), publicKey);
|
|
1446
1854
|
}
|
|
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;
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
1855
|
//#endregion
|
|
1487
1856
|
//#region src/merkle/validateMerkleMaps.ts
|
|
1488
1857
|
function parseMerkleRow(rawHashes) {
|
|
@@ -1499,6 +1868,7 @@ function extractMerkleMaps(bmffHashAssertionData) {
|
|
|
1499
1868
|
const rawMerkle = bmffHashAssertionData["merkle"];
|
|
1500
1869
|
if (!Array.isArray(rawMerkle) || rawMerkle.length === 0) return null;
|
|
1501
1870
|
const exclusions = parseExclusions(bmffHashAssertionData["exclusions"]);
|
|
1871
|
+
if (!exclusions) return null;
|
|
1502
1872
|
const assertionAlg = bmffHashAssertionData["alg"];
|
|
1503
1873
|
const offsetPrefixSize = bmffHashAssertionData["hash"] == null ? 8 : 0;
|
|
1504
1874
|
const maps = [];
|
|
@@ -1555,48 +1925,31 @@ async function validateMerkleMaps(bytes, assertion, codes) {
|
|
|
1555
1925
|
}
|
|
1556
1926
|
return merkleMaps;
|
|
1557
1927
|
}
|
|
1558
|
-
|
|
1559
1928
|
//#endregion
|
|
1560
1929
|
//#region src/init/validateC2paInitSegment.ts
|
|
1561
1930
|
const BMFF_HASH_ASSERTION_LABEL$1 = "c2pa.hash.bmff.v3";
|
|
1562
1931
|
const SESSION_KEYS_ASSERTION_LABEL = "c2pa.session-keys";
|
|
1563
1932
|
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
1933
|
function normalizeToUint8Array(value) {
|
|
1574
1934
|
if (value instanceof Uint8Array) return value;
|
|
1575
|
-
if (
|
|
1576
|
-
if (extractCborTaggedValue(value) !== null) return encode(value);
|
|
1935
|
+
if (value instanceof CborTag) return value.bytes;
|
|
1577
1936
|
throw new Error("Cannot convert value to Uint8Array");
|
|
1578
1937
|
}
|
|
1579
1938
|
function ensureDecodedCbor(value) {
|
|
1580
|
-
|
|
1581
|
-
if (Array.isArray(value) && value.length > 0 && typeof value[0] === "number") return decode(new Uint8Array(value));
|
|
1582
|
-
return value;
|
|
1939
|
+
return value instanceof Uint8Array ? decodeCbor(value) : value;
|
|
1583
1940
|
}
|
|
1941
|
+
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)$/;
|
|
1942
|
+
const CBOR_TAG_DATE_TIME = 0;
|
|
1584
1943
|
function parseCreatedAt(value) {
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
if (
|
|
1588
|
-
return null;
|
|
1944
|
+
if (!(value instanceof CborTag) || value.tag !== CBOR_TAG_DATE_TIME) return null;
|
|
1945
|
+
const text = value.value;
|
|
1946
|
+
if (typeof text !== "string" || !RFC_3339_DATE_TIME.test(text)) return null;
|
|
1947
|
+
if (new Date(text.slice(0, 10)).getUTCDate() !== Number(text.slice(8, 10))) return null;
|
|
1948
|
+
return new Date(text).toISOString();
|
|
1589
1949
|
}
|
|
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;
|
|
1950
|
+
function extractKidHex(coseKey) {
|
|
1951
|
+
const coseKid = coseKey[COSE_KEY_ID_LABEL];
|
|
1952
|
+
return coseKid instanceof Uint8Array ? bytesToHex(coseKid) : null;
|
|
1600
1953
|
}
|
|
1601
1954
|
function extractKeyArray(data) {
|
|
1602
1955
|
if (Array.isArray(data)) return data;
|
|
@@ -1616,72 +1969,84 @@ async function validateBmffHashAssertion(bytes, assertion) {
|
|
|
1616
1969
|
const data = assertion.data;
|
|
1617
1970
|
const rawHash = data["hash"] ?? data["value"];
|
|
1618
1971
|
if (!rawHash) return true;
|
|
1619
|
-
|
|
1972
|
+
if (!(rawHash instanceof Uint8Array)) return false;
|
|
1620
1973
|
const alg = normalizeAlgorithmName(data["alg"]);
|
|
1974
|
+
const exclusions = parseExclusions(data["exclusions"]);
|
|
1975
|
+
if (!exclusions) return false;
|
|
1621
1976
|
return hashesEqual(await computeBmffHash(bytes, {
|
|
1622
|
-
exclusions
|
|
1977
|
+
exclusions,
|
|
1623
1978
|
alg,
|
|
1624
1979
|
offsetPrefixSize: 8
|
|
1625
|
-
}),
|
|
1980
|
+
}), rawHash);
|
|
1626
1981
|
}
|
|
1627
1982
|
function extractSessionKeyFields(entry) {
|
|
1628
1983
|
const keyData = entry;
|
|
1629
|
-
const minSequenceNumber = keyData["minSequenceNumber"];
|
|
1630
|
-
const validityPeriod = keyData["validityPeriod"];
|
|
1984
|
+
const minSequenceNumber = asUnsignedInteger(keyData["minSequenceNumber"]);
|
|
1985
|
+
const validityPeriod = asUnsignedInteger(keyData["validityPeriod"]);
|
|
1631
1986
|
const createdAt = parseCreatedAt(keyData["createdAt"]);
|
|
1632
|
-
if (minSequenceNumber
|
|
1633
|
-
if (/* @__PURE__ */ new Date() < new Date(createdAt) || isKeyExpired(createdAt, Number(validityPeriod))) return null;
|
|
1987
|
+
if (minSequenceNumber === null || validityPeriod === null || !createdAt) return null;
|
|
1634
1988
|
const coseKey = ensureDecodedCbor(keyData["key"]);
|
|
1635
|
-
const kid = extractKidHex(
|
|
1989
|
+
const kid = extractKidHex(coseKey);
|
|
1636
1990
|
if (!kid) return null;
|
|
1637
1991
|
const signerBindingRaw = keyData["signerBinding"];
|
|
1638
1992
|
if (!signerBindingRaw) return null;
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
};
|
|
1648
|
-
} catch {
|
|
1649
|
-
return null;
|
|
1650
|
-
}
|
|
1993
|
+
return {
|
|
1994
|
+
minSequenceNumber,
|
|
1995
|
+
validityPeriod,
|
|
1996
|
+
createdAt,
|
|
1997
|
+
kid,
|
|
1998
|
+
coseKey,
|
|
1999
|
+
signerBindingBytes: normalizeToUint8Array(signerBindingRaw)
|
|
2000
|
+
};
|
|
1651
2001
|
}
|
|
1652
2002
|
async function verifyAndConvertKey(fields, certificate) {
|
|
1653
2003
|
if (!await verifySignerBinding(fields.signerBindingBytes, fields.coseKey, certificate)) return null;
|
|
2004
|
+
return {
|
|
2005
|
+
kid: fields.kid,
|
|
2006
|
+
jwk: convertCoseKeyToJwk(fields.coseKey),
|
|
2007
|
+
minSequenceNumber: fields.minSequenceNumber,
|
|
2008
|
+
validityPeriod: fields.validityPeriod,
|
|
2009
|
+
createdAt: fields.createdAt
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
async function validateSingleSessionKey(entry, certificate) {
|
|
1654
2013
|
try {
|
|
1655
|
-
const
|
|
1656
|
-
return
|
|
1657
|
-
|
|
1658
|
-
jwk,
|
|
1659
|
-
minSequenceNumber: fields.minSequenceNumber,
|
|
1660
|
-
validityPeriod: fields.validityPeriod,
|
|
1661
|
-
createdAt: fields.createdAt
|
|
1662
|
-
};
|
|
2014
|
+
const fields = extractSessionKeyFields(entry);
|
|
2015
|
+
if (!fields) return null;
|
|
2016
|
+
return await verifyAndConvertKey(fields, certificate);
|
|
1663
2017
|
} catch {
|
|
1664
2018
|
return null;
|
|
1665
2019
|
}
|
|
1666
2020
|
}
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
if (!fields) return null;
|
|
1670
|
-
return verifyAndConvertKey(fields, certificate);
|
|
2021
|
+
function isWithinValidityPeriod(key) {
|
|
2022
|
+
return !isKeyExpired(key.createdAt, key.validityPeriod);
|
|
1671
2023
|
}
|
|
1672
2024
|
async function validateSessionKeys(assertion, certificate) {
|
|
1673
|
-
const keyEntries = extractKeyArray(
|
|
1674
|
-
|
|
2025
|
+
const keyEntries = extractKeyArray(assertion.taggedData);
|
|
2026
|
+
const results = await Promise.all(keyEntries.map((entry) => validateSingleSessionKey(entry, certificate)));
|
|
2027
|
+
const validKeys = results.filter((key) => key !== null);
|
|
2028
|
+
return {
|
|
2029
|
+
sessionKeys: validKeys.filter(isWithinValidityPeriod),
|
|
2030
|
+
hasInvalidSessionKey: keyEntries.length === 0 || validKeys.length < results.length
|
|
2031
|
+
};
|
|
1675
2032
|
}
|
|
1676
2033
|
/**
|
|
1677
2034
|
* Validates a C2PA init segment: parses the manifest, extracts and verifies
|
|
1678
2035
|
* the certificate, validates the BMFF hard binding hash, verifies all
|
|
1679
2036
|
* session keys from the `c2pa.session-keys` assertion, and performs
|
|
1680
|
-
* manifest integrity checks (assertion hashes,
|
|
1681
|
-
*
|
|
2037
|
+
* manifest integrity checks (assertion references and hashes, undeclared
|
|
2038
|
+
* assertions, the hard binding count, duplicate manifest boxes, action
|
|
2039
|
+
* ingredients, and claim signature verification).
|
|
1682
2040
|
*
|
|
1683
2041
|
* Only session keys with a valid signer binding and an unexpired validity period
|
|
1684
|
-
* are included in the result.
|
|
2042
|
+
* are included in the result. The result holds `sessionKeys` and `merkleMaps` only when
|
|
2043
|
+
* every manifest integrity check passes, so check `isValid` before you use either list.
|
|
2044
|
+
*
|
|
2045
|
+
* The result includes `LiveVideoStatusCode.SESSIONKEY_INVALID` if the function finds an invalid
|
|
2046
|
+
* session key (C2PA section 19.7.3). A session key is invalid if it does not conform to section 18.25.2,
|
|
2047
|
+
* or if its signer binding fails verification. The function does not check every rule of section 18.25.2.
|
|
2048
|
+
* An expired session key does not count as invalid. A `c2pa.session-keys` assertion without a session key,
|
|
2049
|
+
* or with CBOR that does not decode, also causes `SESSIONKEY_INVALID`.
|
|
1685
2050
|
*
|
|
1686
2051
|
* @param bytes - Raw init segment bytes
|
|
1687
2052
|
* @returns Structured validation result (with `INIT_INVALID` error code if `mdat` box is present)
|
|
@@ -1705,15 +2070,22 @@ async function validateC2paInitSegment(bytes) {
|
|
|
1705
2070
|
};
|
|
1706
2071
|
const internalData = readC2paManifest(bytes, boxes);
|
|
1707
2072
|
const { manifest } = internalData;
|
|
1708
|
-
const { codes: integrityCodes, certificate } = await validateManifestIntegrity(internalData);
|
|
1709
|
-
const bmffHashAssertion =
|
|
2073
|
+
const { codes: integrityCodes, certificate, verifiedAssertions } = await validateManifestIntegrity(internalData);
|
|
2074
|
+
const bmffHashAssertion = verifiedAssertions.find((a) => a.label === BMFF_HASH_ASSERTION_LABEL$1) ?? null;
|
|
1710
2075
|
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
2076
|
const codes = /* @__PURE__ */ new Set();
|
|
1714
2077
|
const merkleMaps = await validateMerkleMaps(bytes, bmffHashAssertion, codes);
|
|
2078
|
+
const sessionKeysAssertion = verifiedAssertions.find((a) => a.label === SESSION_KEYS_ASSERTION_LABEL);
|
|
2079
|
+
const sessionKeysValidation = sessionKeysAssertion && certificate ? await validateSessionKeys(sessionKeysAssertion, certificate) : {
|
|
2080
|
+
sessionKeys: [],
|
|
2081
|
+
hasInvalidSessionKey: false
|
|
2082
|
+
};
|
|
2083
|
+
const integrityPassed = integrityCodes.length === 0;
|
|
2084
|
+
const sessionKeys = integrityPassed ? sessionKeysValidation.sessionKeys : [];
|
|
2085
|
+
const exposedMerkleMaps = integrityPassed ? merkleMaps ?? [] : [];
|
|
1715
2086
|
if (!bmffHashValid) codes.add(LiveVideoStatusCode.INIT_INVALID);
|
|
1716
|
-
if (
|
|
2087
|
+
if (sessionKeysValidation.hasInvalidSessionKey) codes.add(LiveVideoStatusCode.SESSIONKEY_INVALID);
|
|
2088
|
+
if (sessionKeysValidation.sessionKeys.length === 0 && merkleMaps === null) codes.add(LiveVideoStatusCode.SESSIONKEY_INVALID);
|
|
1717
2089
|
for (const code of integrityCodes) codes.add(code);
|
|
1718
2090
|
const errorCodes = [...codes];
|
|
1719
2091
|
return {
|
|
@@ -1721,12 +2093,11 @@ async function validateC2paInitSegment(bytes) {
|
|
|
1721
2093
|
certificate,
|
|
1722
2094
|
manifestId: manifest.instanceId,
|
|
1723
2095
|
sessionKeys,
|
|
1724
|
-
merkleMaps:
|
|
2096
|
+
merkleMaps: exposedMerkleMaps,
|
|
1725
2097
|
isValid: errorCodes.length === 0,
|
|
1726
2098
|
errorCodes
|
|
1727
2099
|
};
|
|
1728
2100
|
}
|
|
1729
|
-
|
|
1730
2101
|
//#endregion
|
|
1731
2102
|
//#region src/emsg/extractVsiEmsgBox.ts
|
|
1732
2103
|
const EMSG_BOX_TYPE = "emsg";
|
|
@@ -1752,7 +2123,6 @@ function extractVsiEmsgBox(segmentBytes) {
|
|
|
1752
2123
|
}
|
|
1753
2124
|
return null;
|
|
1754
2125
|
}
|
|
1755
|
-
|
|
1756
2126
|
//#endregion
|
|
1757
2127
|
//#region src/vsi/decodeVsiMap.ts
|
|
1758
2128
|
/**
|
|
@@ -1770,16 +2140,17 @@ function extractVsiEmsgBox(segmentBytes) {
|
|
|
1770
2140
|
* @internal
|
|
1771
2141
|
*/
|
|
1772
2142
|
function decodeVsiMap(vsiCborBytes) {
|
|
1773
|
-
const raw =
|
|
2143
|
+
const raw = decodeCbor(vsiCborBytes);
|
|
1774
2144
|
if (typeof raw !== "object" || raw === null) throw new Error("VSI map must be a CBOR map");
|
|
1775
|
-
const
|
|
1776
|
-
|
|
2145
|
+
const rawSequenceNumber = raw["sequenceNumber"];
|
|
2146
|
+
const sequenceNumber = asUnsignedInteger(rawSequenceNumber);
|
|
2147
|
+
if (sequenceNumber === null) throw new Error(`VSI map sequenceNumber must be an unsigned integer up to ${Number.MAX_SAFE_INTEGER}, got ${typeof rawSequenceNumber === "number" || typeof rawSequenceNumber === "bigint" ? String(rawSequenceNumber) : typeof rawSequenceNumber}`);
|
|
1777
2148
|
const bmffHashRaw = raw["bmffHash"];
|
|
1778
2149
|
if (!bmffHashRaw || typeof bmffHashRaw !== "object") throw new Error("VSI map missing bmffHash");
|
|
1779
2150
|
const hash = bmffHashRaw["hash"];
|
|
1780
2151
|
if (!(hash instanceof Uint8Array)) throw new Error("VSI map bmffHash.hash must be a Uint8Array");
|
|
1781
|
-
const exclusions = bmffHashRaw["exclusions"];
|
|
1782
|
-
if (
|
|
2152
|
+
const exclusions = parseExclusions(bmffHashRaw["exclusions"]);
|
|
2153
|
+
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
2154
|
const manifestId = raw["manifestId"];
|
|
1784
2155
|
if (typeof manifestId !== "string") throw new Error("VSI map missing or invalid manifestId");
|
|
1785
2156
|
return {
|
|
@@ -1787,12 +2158,11 @@ function decodeVsiMap(vsiCborBytes) {
|
|
|
1787
2158
|
bmffHash: {
|
|
1788
2159
|
hash,
|
|
1789
2160
|
alg: normalizeAlgorithmName(bmffHashRaw["alg"]),
|
|
1790
|
-
exclusions
|
|
2161
|
+
exclusions
|
|
1791
2162
|
},
|
|
1792
2163
|
manifestId
|
|
1793
2164
|
};
|
|
1794
2165
|
}
|
|
1795
|
-
|
|
1796
2166
|
//#endregion
|
|
1797
2167
|
//#region src/vsi/createSequenceState.ts
|
|
1798
2168
|
/**
|
|
@@ -1811,7 +2181,6 @@ function createSequenceState() {
|
|
|
1811
2181
|
seenSequences: /* @__PURE__ */ new Set()
|
|
1812
2182
|
};
|
|
1813
2183
|
}
|
|
1814
|
-
|
|
1815
2184
|
//#endregion
|
|
1816
2185
|
//#region src/vsi/SequenceState.ts
|
|
1817
2186
|
/**
|
|
@@ -1824,13 +2193,17 @@ function createSequenceState() {
|
|
|
1824
2193
|
* @public
|
|
1825
2194
|
*/
|
|
1826
2195
|
const SequenceValidationReason = {
|
|
2196
|
+
/** Sequence number is the next expected value */
|
|
1827
2197
|
VALID: "valid",
|
|
2198
|
+
/** Sequence number was already seen */
|
|
1828
2199
|
DUPLICATE: "duplicate",
|
|
2200
|
+
/** One or more sequence numbers were skipped */
|
|
1829
2201
|
GAP_DETECTED: "gap_detected",
|
|
2202
|
+
/** Sequence number is less than the last seen */
|
|
1830
2203
|
OUT_OF_ORDER: "out_of_order",
|
|
2204
|
+
/** Below the session key's minSequenceNumber */
|
|
1831
2205
|
SEQUENCE_NUMBER_BELOW_MINIMUM: "sequence_number_below_minimum"
|
|
1832
2206
|
};
|
|
1833
|
-
|
|
1834
2207
|
//#endregion
|
|
1835
2208
|
//#region src/vsi/validateSequenceNumber.ts
|
|
1836
2209
|
const SEEN_SEQUENCES_WINDOW_SIZE = 32;
|
|
@@ -1919,7 +2292,6 @@ function validateSequenceNumber(state, sequenceNumber, minSequenceNumber) {
|
|
|
1919
2292
|
}
|
|
1920
2293
|
};
|
|
1921
2294
|
}
|
|
1922
|
-
|
|
1923
2295
|
//#endregion
|
|
1924
2296
|
//#region src/bmff/validateBmffHash.ts
|
|
1925
2297
|
const OFFSET_PREFIX_SIZES_TO_TRY = [8, 0];
|
|
@@ -1949,7 +2321,6 @@ async function validateBmffHash(segmentBytes, expectedHash, options) {
|
|
|
1949
2321
|
}), expectedHash)) return true;
|
|
1950
2322
|
return false;
|
|
1951
2323
|
}
|
|
1952
|
-
|
|
1953
2324
|
//#endregion
|
|
1954
2325
|
//#region src/segment/validateC2paSegment.ts
|
|
1955
2326
|
function findSessionKey(sessionKeys, kidHex) {
|
|
@@ -1965,10 +2336,13 @@ function findSessionKey(sessionKeys, kidHex) {
|
|
|
1965
2336
|
*
|
|
1966
2337
|
* Returns `null` if the segment does not contain a C2PA EMSG box.
|
|
1967
2338
|
*
|
|
2339
|
+
* A segment whose signature, content hash, or key validity does not verify
|
|
2340
|
+
* returns the received `sequenceState` unchanged.
|
|
2341
|
+
*
|
|
1968
2342
|
* @param segmentBytes - Raw segment bytes
|
|
1969
2343
|
* @param sessionKeys - Available session keys from the init segment
|
|
1970
2344
|
* @param sequenceState - Current sequence state for this stream
|
|
1971
|
-
* @returns Validation result and
|
|
2345
|
+
* @returns Validation result and the sequence state for the next call, or `null` if no C2PA EMSG box
|
|
1972
2346
|
*
|
|
1973
2347
|
* @example
|
|
1974
2348
|
* {@includeCode ../../test/segment/validateC2paSegment.test.ts#example}
|
|
@@ -2000,7 +2374,7 @@ async function validateC2paSegment(segmentBytes, sessionKeys, sequenceState = cr
|
|
|
2000
2374
|
isValid: false,
|
|
2001
2375
|
errorCodes: [LiveVideoStatusCode.SEGMENT_INVALID]
|
|
2002
2376
|
},
|
|
2003
|
-
nextSequenceState
|
|
2377
|
+
nextSequenceState: sequenceState
|
|
2004
2378
|
};
|
|
2005
2379
|
const algorithm = resolveImportAlgorithm(sessionKey.jwk);
|
|
2006
2380
|
const publicKey = await crypto.subtle.importKey("jwk", sessionKey.jwk, algorithm, false, ["verify"]);
|
|
@@ -2009,11 +2383,12 @@ async function validateC2paSegment(segmentBytes, sessionKeys, sequenceState = cr
|
|
|
2009
2383
|
alg: vsi.bmffHash.alg
|
|
2010
2384
|
})]);
|
|
2011
2385
|
const sequenceAboveMin = vsi.sequenceNumber >= sessionKey.minSequenceNumber;
|
|
2386
|
+
const keyNotYetActive = /* @__PURE__ */ new Date() < new Date(sessionKey.createdAt);
|
|
2012
2387
|
const keyExpired = isKeyExpired(sessionKey.createdAt, sessionKey.validityPeriod);
|
|
2388
|
+
const authentic = signatureValid && hashValid && !keyNotYetActive && !keyExpired;
|
|
2013
2389
|
const codes = /* @__PURE__ */ new Set();
|
|
2014
|
-
if (!
|
|
2390
|
+
if (!authentic || !sequenceAboveMin) codes.add(LiveVideoStatusCode.SEGMENT_INVALID);
|
|
2015
2391
|
if (!sequenceResult.isValid) codes.add(LiveVideoStatusCode.ASSERTION_INVALID);
|
|
2016
|
-
if (keyExpired) codes.add(LiveVideoStatusCode.SESSIONKEY_INVALID);
|
|
2017
2392
|
const errorCodes = [...codes];
|
|
2018
2393
|
return {
|
|
2019
2394
|
result: {
|
|
@@ -2021,10 +2396,9 @@ async function validateC2paSegment(segmentBytes, sessionKeys, sequenceState = cr
|
|
|
2021
2396
|
isValid: errorCodes.length === 0,
|
|
2022
2397
|
errorCodes
|
|
2023
2398
|
},
|
|
2024
|
-
nextSequenceState
|
|
2399
|
+
nextSequenceState: authentic ? nextSequenceState : sequenceState
|
|
2025
2400
|
};
|
|
2026
2401
|
}
|
|
2027
|
-
|
|
2028
2402
|
//#endregion
|
|
2029
2403
|
//#region src/manifestbox/validateC2paManifestBoxSegment.ts
|
|
2030
2404
|
const LIVE_VIDEO_ASSERTION_LABEL = "c2pa.livevideo.segment";
|
|
@@ -2048,7 +2422,7 @@ function parseLiveVideoAssertion(assertions) {
|
|
|
2048
2422
|
const rawStreamId = data?.["streamId"];
|
|
2049
2423
|
const rawContinuity = data?.["continuityMethod"];
|
|
2050
2424
|
return {
|
|
2051
|
-
sequenceNumber:
|
|
2425
|
+
sequenceNumber: asUnsignedInteger(rawSeq),
|
|
2052
2426
|
previousManifestId: typeof rawPrev === "string" ? rawPrev : null,
|
|
2053
2427
|
streamId: typeof rawStreamId === "string" ? rawStreamId : null,
|
|
2054
2428
|
continuityMethod: typeof rawContinuity === "string" ? rawContinuity : null,
|
|
@@ -2059,19 +2433,25 @@ const EMPTY_BMFF_HASH = {
|
|
|
2059
2433
|
hashBytes: null,
|
|
2060
2434
|
hashHex: null,
|
|
2061
2435
|
exclusions: [],
|
|
2062
|
-
alg: null
|
|
2436
|
+
alg: null,
|
|
2437
|
+
malformed: false
|
|
2063
2438
|
};
|
|
2064
2439
|
function parseBmffHashAssertion(assertions) {
|
|
2065
2440
|
const assertion = assertions.find((a) => a.label === BMFF_HASH_ASSERTION_LABEL);
|
|
2066
2441
|
if (!assertion) return EMPTY_BMFF_HASH;
|
|
2067
2442
|
const data = extractAssertionData(assertion.data);
|
|
2068
2443
|
if (!data) return EMPTY_BMFF_HASH;
|
|
2069
|
-
const
|
|
2444
|
+
const rawHash = data["hash"] ?? data["value"];
|
|
2445
|
+
const hashBytes = toUint8Array(rawHash);
|
|
2446
|
+
const hashHex = hashBytes ? bytesToHex(hashBytes) : null;
|
|
2447
|
+
const exclusions = parseExclusions(data["exclusions"]);
|
|
2448
|
+
const alg = typeof data["alg"] === "string" ? normalizeAlgorithmName(data["alg"]) : null;
|
|
2070
2449
|
return {
|
|
2071
2450
|
hashBytes,
|
|
2072
|
-
hashHex
|
|
2073
|
-
exclusions:
|
|
2074
|
-
alg
|
|
2451
|
+
hashHex,
|
|
2452
|
+
exclusions: exclusions ?? [],
|
|
2453
|
+
alg,
|
|
2454
|
+
malformed: rawHash != null && hashBytes === null || exclusions === null
|
|
2075
2455
|
};
|
|
2076
2456
|
}
|
|
2077
2457
|
function parseManifest(bytes) {
|
|
@@ -2081,23 +2461,17 @@ function parseManifest(bytes) {
|
|
|
2081
2461
|
if (!manifest) return {
|
|
2082
2462
|
manifest: null,
|
|
2083
2463
|
issuer: null,
|
|
2084
|
-
liveVideo: null,
|
|
2085
|
-
bmff: EMPTY_BMFF_HASH,
|
|
2086
2464
|
internalData: null
|
|
2087
2465
|
};
|
|
2088
2466
|
return {
|
|
2089
2467
|
manifest,
|
|
2090
2468
|
issuer: manifest.signatureInfo?.issuer ?? null,
|
|
2091
|
-
liveVideo: parseLiveVideoAssertion(manifest.assertions),
|
|
2092
|
-
bmff: parseBmffHashAssertion(manifest.assertions),
|
|
2093
2469
|
internalData
|
|
2094
2470
|
};
|
|
2095
2471
|
} catch {
|
|
2096
2472
|
return {
|
|
2097
2473
|
manifest: null,
|
|
2098
2474
|
issuer: null,
|
|
2099
|
-
liveVideo: null,
|
|
2100
|
-
bmff: EMPTY_BMFF_HASH,
|
|
2101
2475
|
internalData: null
|
|
2102
2476
|
};
|
|
2103
2477
|
}
|
|
@@ -2111,8 +2485,7 @@ function collectErrorCodes(hasManifest, hasLiveVideo, streamIdValid, sequenceNum
|
|
|
2111
2485
|
if (!bmffHashMatches) codes.add(LiveVideoStatusCode.SEGMENT_INVALID);
|
|
2112
2486
|
return [...codes];
|
|
2113
2487
|
}
|
|
2114
|
-
async function validateContinuity(
|
|
2115
|
-
const { manifest, liveVideo } = parsed;
|
|
2488
|
+
async function validateContinuity(manifest, liveVideo, lastManifestId, options) {
|
|
2116
2489
|
const method = liveVideo?.continuityMethod ?? null;
|
|
2117
2490
|
if (!method) return [LiveVideoStatusCode.CONTINUITY_METHOD_INVALID];
|
|
2118
2491
|
if (method === CONTINUITY_METHOD_MANIFEST_ID) {
|
|
@@ -2131,16 +2504,21 @@ async function validateContinuity(parsed, lastManifestId, options) {
|
|
|
2131
2504
|
* Recomputes the `c2pa.hash.bmff.v3` content hash from the raw segment bytes and compares
|
|
2132
2505
|
* it against the expected hash in the manifest assertion. Checks live-video assertions
|
|
2133
2506
|
* (sequenceNumber, streamId, continuityMethod) and manifest-ID chain continuity.
|
|
2507
|
+
* The live-video and BMFF hash fields of the result come only from assertions whose
|
|
2508
|
+
* hash matches a claim reference. Check `isValid` before you use any field.
|
|
2134
2509
|
* A validator for an implementer-defined continuity method can be registered
|
|
2135
2510
|
* via {@link ManifestBoxValidationOptions}.
|
|
2136
2511
|
*
|
|
2137
2512
|
* This function is **pure** — it does not access any external state. The
|
|
2138
2513
|
* caller is responsible for persisting `nextManifestId` and `nextState`
|
|
2139
|
-
* between calls.
|
|
2514
|
+
* between calls. A segment that fails verification, is on another stream, or
|
|
2515
|
+
* does not move the sequence number forward returns the received
|
|
2516
|
+
* `lastManifestId` and `state` unchanged. A segment that fails only the
|
|
2517
|
+
* continuity check still updates them.
|
|
2140
2518
|
*
|
|
2141
2519
|
* @param bytes - Raw segment bytes
|
|
2142
|
-
* @param lastManifestId - Manifest ID from the
|
|
2143
|
-
* @param state - Optional state from the
|
|
2520
|
+
* @param lastManifestId - Manifest ID from the last segment that updated the state, or `null` for the first segment
|
|
2521
|
+
* @param state - Optional state from the last segment that updated the state, for the streamId and sequenceNumber checks
|
|
2144
2522
|
* @param options - Optional custom continuity method validator
|
|
2145
2523
|
* @returns Validation result, the manifest ID, and state to persist for the next call
|
|
2146
2524
|
*
|
|
@@ -2150,7 +2528,11 @@ async function validateContinuity(parsed, lastManifestId, options) {
|
|
|
2150
2528
|
* @public
|
|
2151
2529
|
*/
|
|
2152
2530
|
async function validateC2paManifestBoxSegment(bytes, lastManifestId, state, options) {
|
|
2153
|
-
const { manifest, issuer,
|
|
2531
|
+
const { manifest, issuer, internalData } = parseManifest(bytes);
|
|
2532
|
+
const integrity = internalData ? await validateManifestIntegrity(internalData) : null;
|
|
2533
|
+
const verifiedAssertions = integrity?.verifiedAssertions ?? [];
|
|
2534
|
+
const liveVideo = parseLiveVideoAssertion(verifiedAssertions);
|
|
2535
|
+
const bmff = parseBmffHashAssertion(verifiedAssertions);
|
|
2154
2536
|
const sequenceNumber = liveVideo?.sequenceNumber ?? null;
|
|
2155
2537
|
const previousManifestId = liveVideo?.previousManifestId ?? null;
|
|
2156
2538
|
const streamId = liveVideo?.streamId ?? null;
|
|
@@ -2163,17 +2545,12 @@ async function validateC2paManifestBoxSegment(bytes, lastManifestId, state, opti
|
|
|
2163
2545
|
alg: bmff.alg ?? void 0,
|
|
2164
2546
|
offsetPrefixSize: 8
|
|
2165
2547
|
}), bmff.hashBytes);
|
|
2166
|
-
const continuityCodes = await validateContinuity(
|
|
2167
|
-
|
|
2168
|
-
issuer,
|
|
2169
|
-
liveVideo,
|
|
2170
|
-
bmff,
|
|
2171
|
-
internalData
|
|
2172
|
-
}, lastManifestId, options);
|
|
2173
|
-
const liveVideoCodes = [...new Set([...collectErrorCodes(manifest !== null, liveVideo !== null, streamIdValid, sequenceNumberValid, bmffHashMatches), ...continuityCodes])];
|
|
2174
|
-
const integrity = internalData ? await validateManifestIntegrity(internalData) : null;
|
|
2548
|
+
const continuityCodes = await validateContinuity(manifest, liveVideo, lastManifestId, options);
|
|
2549
|
+
const liveVideoCodes = [.../* @__PURE__ */ new Set([...collectErrorCodes(manifest !== null, liveVideo !== null, streamIdValid, sequenceNumberValid, bmffHashMatches), ...continuityCodes])];
|
|
2175
2550
|
const integrityCodes = integrity?.codes ?? [];
|
|
2176
2551
|
const errorCodes = [...liveVideoCodes, ...integrityCodes];
|
|
2552
|
+
if (bmff.malformed) errorCodes.push(C2paStatusCode.ASSERTION_BMFFHASH_MALFORMED);
|
|
2553
|
+
const advance = manifest !== null && liveVideo !== null && integrity !== null && integrity.codes.length === 0 && bmffHashMatches && !bmff.malformed && streamIdValid && sequenceNumberValid;
|
|
2177
2554
|
const currentManifestId = manifest?.instanceId ?? null;
|
|
2178
2555
|
return {
|
|
2179
2556
|
result: {
|
|
@@ -2188,14 +2565,13 @@ async function validateC2paManifestBoxSegment(bytes, lastManifestId, state, opti
|
|
|
2188
2565
|
isValid: errorCodes.length === 0,
|
|
2189
2566
|
errorCodes
|
|
2190
2567
|
},
|
|
2191
|
-
nextManifestId: currentManifestId ?? lastManifestId,
|
|
2192
|
-
nextState: {
|
|
2568
|
+
nextManifestId: advance ? currentManifestId ?? lastManifestId : lastManifestId,
|
|
2569
|
+
nextState: advance ? {
|
|
2193
2570
|
lastStreamId: streamId ?? state?.lastStreamId,
|
|
2194
2571
|
lastSequenceNumber: sequenceNumber ?? state?.lastSequenceNumber
|
|
2195
|
-
}
|
|
2572
|
+
} : state ?? {}
|
|
2196
2573
|
};
|
|
2197
2574
|
}
|
|
2198
|
-
|
|
2199
2575
|
//#endregion
|
|
2200
2576
|
//#region src/merkle/validateC2paMerkleSegment.ts
|
|
2201
2577
|
const MERKLE_BOX_PURPOSE = "merkle";
|
|
@@ -2215,32 +2591,22 @@ function extractMerkleAuxBoxes(segmentBytes) {
|
|
|
2215
2591
|
}
|
|
2216
2592
|
return payloads;
|
|
2217
2593
|
}
|
|
2218
|
-
function readMapField(map, name) {
|
|
2219
|
-
if (map instanceof Map) return map.get(name);
|
|
2220
|
-
return map[name];
|
|
2221
|
-
}
|
|
2222
2594
|
function decodeFirstCbor(payload) {
|
|
2223
|
-
let first;
|
|
2224
|
-
let found = false;
|
|
2225
2595
|
try {
|
|
2226
|
-
|
|
2227
|
-
first = value;
|
|
2228
|
-
found = true;
|
|
2229
|
-
return false;
|
|
2230
|
-
});
|
|
2596
|
+
return readCborItem(payload).value;
|
|
2231
2597
|
} catch {
|
|
2232
2598
|
return;
|
|
2233
2599
|
}
|
|
2234
|
-
return found ? first : void 0;
|
|
2235
2600
|
}
|
|
2236
2601
|
function parseBmffMerkleMap(payload) {
|
|
2237
2602
|
const decoded = decodeFirstCbor(payload);
|
|
2238
2603
|
if (decoded === null || decoded === void 0 || typeof decoded !== "object") return null;
|
|
2239
|
-
const
|
|
2240
|
-
const
|
|
2241
|
-
const
|
|
2604
|
+
const map = decoded;
|
|
2605
|
+
const uniqueId = asInteger(map["uniqueId"]);
|
|
2606
|
+
const localId = asInteger(map["localId"]);
|
|
2607
|
+
const location = asInteger(map["location"]);
|
|
2242
2608
|
if (uniqueId === null || localId === null || location === null) return null;
|
|
2243
|
-
const rawHashes =
|
|
2609
|
+
const rawHashes = map["hashes"];
|
|
2244
2610
|
if (rawHashes == null) return {
|
|
2245
2611
|
uniqueId,
|
|
2246
2612
|
localId,
|
|
@@ -2377,7 +2743,7 @@ async function validateC2paMerkleSegment(segmentBytes, merkleMaps, state) {
|
|
|
2377
2743
|
nextState: { lastLocations: nextLocations }
|
|
2378
2744
|
};
|
|
2379
2745
|
}
|
|
2380
|
-
|
|
2381
2746
|
//#endregion
|
|
2382
2747
|
export { C2paStatusCode, LiveVideoStatusCode, SequenceValidationReason, validateC2paInitSegment, validateC2paManifestBoxSegment, validateC2paMerkleSegment, validateC2paSegment };
|
|
2748
|
+
|
|
2383
2749
|
//# sourceMappingURL=index.js.map
|