@svta/cml-c2pa 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2043 @@
1
+ import { decode } from "cbor-x/decode";
2
+ import { encode } from "cbor-x/encode";
3
+ import { findIsoBox, readEmsg, readIsoBoxes } from "@svta/cml-iso-bmff";
4
+
5
+ //#region src/LiveVideoStatusCode.ts
6
+ /**
7
+ * Standard C2PA failure status codes for live video validation,
8
+ * as defined in the C2PA specification section 19.7.
9
+ *
10
+ * @see {@link https://c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_live_video_validation_process | C2PA Spec §19.7}
11
+ *
12
+ * @enum
13
+ *
14
+ * @public
15
+ */
16
+ const LiveVideoStatusCode = {
17
+ INIT_INVALID: "livevideo.init.invalid",
18
+ MANIFEST_INVALID: "livevideo.manifest.invalid",
19
+ SEGMENT_INVALID: "livevideo.segment.invalid",
20
+ ASSERTION_INVALID: "livevideo.assertion.invalid",
21
+ CONTINUITY_METHOD_INVALID: "livevideo.continuityMethod.invalid",
22
+ SESSIONKEY_INVALID: "livevideo.sessionkey.invalid"
23
+ };
24
+
25
+ //#endregion
26
+ //#region src/cose/decodeCoseSign1.ts
27
+ const COSE_SIGN1_TAG_SINGLE_BYTE = 210;
28
+ const COSE_SIGN1_TAG_TWO_BYTE_FIRST = 216;
29
+ const COSE_SIGN1_TAG_TWO_BYTE_SECOND = 18;
30
+ const COSE_SIGN1_ARRAY_LENGTH = 4;
31
+ const COSE_KEY_KID = 4;
32
+ const COSE_KEY_ALG = 1;
33
+ function coseGet$1(header, key) {
34
+ if (header instanceof Map) return header.get(key);
35
+ return header[key];
36
+ }
37
+ function stripCoseTag(bytes) {
38
+ if (bytes[0] === COSE_SIGN1_TAG_SINGLE_BYTE) return bytes.subarray(1);
39
+ if (bytes[0] === COSE_SIGN1_TAG_TWO_BYTE_FIRST && bytes[1] === COSE_SIGN1_TAG_TWO_BYTE_SECOND) return bytes.subarray(2);
40
+ return bytes;
41
+ }
42
+ function toUint8Array$1(value) {
43
+ if (value instanceof Uint8Array) return value;
44
+ if (Array.isArray(value)) return new Uint8Array(value);
45
+ throw new Error(`Expected Uint8Array or number[], got ${typeof value}`);
46
+ }
47
+ /**
48
+ * Decodes a `COSE_Sign1` structure from raw bytes (RFC 9052).
49
+ *
50
+ * Handles CBOR tag 18 in both single-byte (0xD2) and two-byte (0xD8 0x12) form.
51
+ *
52
+ * @param coseBytes - Raw COSE_Sign1 bytes, optionally prefixed with CBOR tag 18
53
+ * @returns The decoded COSE_Sign1 structure
54
+ * @throws If the bytes do not represent a valid COSE_Sign1 structure
55
+ *
56
+ * @example
57
+ * {@includeCode ../../test/cose/decodeCoseSign1.test.ts#example}
58
+ *
59
+ * @public
60
+ */
61
+ function decodeCoseSign1(coseBytes) {
62
+ try {
63
+ const coseArray = decode(stripCoseTag(coseBytes));
64
+ if (!Array.isArray(coseArray) || coseArray.length !== COSE_SIGN1_ARRAY_LENGTH) throw new Error("Invalid COSE_Sign1 structure: expected array with 4 elements");
65
+ const [protectedRaw, unprotectedRaw, payloadRaw, signatureRaw] = coseArray;
66
+ const protectedBytes = toUint8Array$1(protectedRaw);
67
+ let protectedHeader = {};
68
+ if (protectedBytes.length > 0) protectedHeader = decode(protectedBytes);
69
+ const unprotectedHeader = unprotectedRaw ?? {};
70
+ const kidRaw = coseGet$1(protectedHeader, COSE_KEY_KID) ?? coseGet$1(unprotectedHeader, COSE_KEY_KID) ?? null;
71
+ const kid = kidRaw != null ? toUint8Array$1(kidRaw) : null;
72
+ const alg = coseGet$1(protectedHeader, COSE_KEY_ALG) ?? coseGet$1(unprotectedHeader, COSE_KEY_ALG) ?? null;
73
+ return {
74
+ protectedBytes,
75
+ protectedHeader,
76
+ unprotectedHeader,
77
+ payload: payloadRaw == null ? null : toUint8Array$1(payloadRaw),
78
+ signature: toUint8Array$1(signatureRaw),
79
+ kid,
80
+ alg
81
+ };
82
+ } catch (error) {
83
+ throw new Error(`Failed to decode COSE_Sign1: ${error.message}`);
84
+ }
85
+ }
86
+
87
+ //#endregion
88
+ //#region src/jumbf/parseJumbfBoxes.ts
89
+ /**
90
+ * Parses JUMBF boxes (ISO 19566-5) from raw bytes.
91
+ *
92
+ * JUMBF boxes share the same 4-byte size + 4-byte type structure as ISO BMFF boxes,
93
+ * so they can be parsed with the same reader.
94
+ *
95
+ * @param bytes - Raw bytes containing JUMBF box content
96
+ * @returns Array of parsed JUMBF boxes with their payloads
97
+ *
98
+ * @example
99
+ * {@includeCode ../../test/jumbf/parseJumbfBoxes.test.ts#example}
100
+ *
101
+ * @internal
102
+ */
103
+ function parseJumbfBoxes(bytes) {
104
+ return readIsoBoxes(bytes).map((box) => ({
105
+ type: box.type,
106
+ data: box.view.readData(box.view.bytesRemaining)
107
+ }));
108
+ }
109
+
110
+ //#endregion
111
+ //#region src/jumbf/parseJumbfLabel.ts
112
+ const TEXT_DECODER$2 = new TextDecoder();
113
+ const JUMD_UUID_SIZE = 16;
114
+ const JUMD_TOGGLES_OFFSET = 16;
115
+ const JUMD_LABEL_START = 17;
116
+ const LABEL_FLAG_MASK = 3;
117
+ const LABEL_FLAG_EXPECTED = 3;
118
+ /**
119
+ * Extracts the label from a JUMBF Description Box (`jumd`) data payload.
120
+ *
121
+ * The `jumd` format is: 16-byte UUID + 1-byte toggles + null-terminated label string.
122
+ * A label is only present when bits 0 and 1 of the toggles byte are both set.
123
+ *
124
+ * @param jumdData - Raw bytes of the `jumd` box data (payload after the box header)
125
+ * @returns The label string, or `null` if no label is present
126
+ *
127
+ * @example
128
+ * {@includeCode ../../test/jumbf/parseJumbfLabel.test.ts#example}
129
+ *
130
+ * @internal
131
+ */
132
+ function parseJumbfLabel(jumdData) {
133
+ if (jumdData.length < JUMD_UUID_SIZE + 1) return null;
134
+ if ((jumdData[JUMD_TOGGLES_OFFSET] & LABEL_FLAG_MASK) !== LABEL_FLAG_EXPECTED) return null;
135
+ let end = JUMD_LABEL_START;
136
+ while (end < jumdData.length && jumdData[end] !== 0) end++;
137
+ if (end >= jumdData.length) return null;
138
+ return TEXT_DECODER$2.decode(jumdData.subarray(JUMD_LABEL_START, end));
139
+ }
140
+
141
+ //#endregion
142
+ //#region src/utils.ts
143
+ const MILLISECONDS_PER_SECOND = 1e3;
144
+ const SHA_ALGORITHM_PATTERN = /^sha(\d+)$/i;
145
+ /**
146
+ * Normalizes hash algorithm names to WebCrypto format (e.g. `sha256` → `SHA-256`).
147
+ *
148
+ * Defaults to `'SHA-256'` when no algorithm is provided.
149
+ *
150
+ * @internal
151
+ */
152
+ function normalizeAlgorithmName(rawAlg) {
153
+ return (rawAlg ?? "SHA-256").replace(SHA_ALGORITHM_PATTERN, "SHA-$1");
154
+ }
155
+ const HEX_TABLE = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
156
+ /**
157
+ * Converts a Uint8Array to a lowercase hex string.
158
+ *
159
+ * @internal
160
+ */
161
+ function bytesToHex(bytes) {
162
+ let hex = "";
163
+ for (const byte of bytes) hex += HEX_TABLE[byte];
164
+ return hex;
165
+ }
166
+ /**
167
+ * Checks whether a session key has expired based on its creation time
168
+ * and validity period.
169
+ *
170
+ * @param createdAt - ISO 8601 date string when the key was created
171
+ * @param validityPeriodSeconds - Validity duration in seconds
172
+ * @param now - Current time (defaults to `new Date()`, injectable for testing)
173
+ * @returns `true` if the key has expired
174
+ *
175
+ * @internal
176
+ */
177
+ function isKeyExpired(createdAt, validityPeriodSeconds, now = /* @__PURE__ */ new Date()) {
178
+ const createdAtMs = new Date(createdAt).getTime();
179
+ if (Number.isNaN(createdAtMs)) return true;
180
+ return now > new Date(createdAtMs + validityPeriodSeconds * MILLISECONDS_PER_SECOND);
181
+ }
182
+ /**
183
+ * Constant-time comparison of two hash byte arrays.
184
+ *
185
+ * @internal
186
+ */
187
+ function hashesEqual(a, b) {
188
+ if (a.length !== b.length) return false;
189
+ let diff = 0;
190
+ for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
191
+ return diff === 0;
192
+ }
193
+ const C2PA_MANIFEST_UUID = [
194
+ 216,
195
+ 254,
196
+ 195,
197
+ 214,
198
+ 26,
199
+ 150,
200
+ 79,
201
+ 50,
202
+ 160,
203
+ 246,
204
+ 243,
205
+ 236,
206
+ 249,
207
+ 108,
208
+ 16,
209
+ 234
210
+ ];
211
+ const JUMBF_UUID = [
212
+ 216,
213
+ 254,
214
+ 195,
215
+ 214,
216
+ 27,
217
+ 14,
218
+ 72,
219
+ 60,
220
+ 146,
221
+ 151,
222
+ 88,
223
+ 40,
224
+ 135,
225
+ 126,
226
+ 196,
227
+ 129
228
+ ];
229
+ function matchesUuid(usertype, expected) {
230
+ return usertype.length === expected.length && expected.every((b, i) => b === usertype[i]);
231
+ }
232
+ function isC2paUuid(usertype) {
233
+ return matchesUuid(usertype, C2PA_MANIFEST_UUID) || matchesUuid(usertype, JUMBF_UUID);
234
+ }
235
+ /**
236
+ * Finds the C2PA UUID box in a list of parsed ISO BMFF boxes.
237
+ *
238
+ * Matches against both the C2PA manifest store UUID and the JUMBF UUID
239
+ * (ISO 19566-5), as different tools use different UUIDs.
240
+ *
241
+ * @internal
242
+ */
243
+ function findC2paUuidBox(boxes) {
244
+ return boxes.find((box) => box.type === "uuid" && isC2paUuid(box.usertype ?? []));
245
+ }
246
+ const FULLBOX_HEADER_SIZE = 4;
247
+ const AUX_UUID_OFFSET_SIZE = 8;
248
+ /**
249
+ * Strips the JUMBF UUID box prefix (fullbox header, purpose string, aux offset)
250
+ * to return only the JUMBF manifest data.
251
+ *
252
+ * Structure per ISO 19566-5 / C2PA BMFF storage:
253
+ * version(1) + flags(3) + purpose(null-terminated) + aux_offset(8) + JUMBF data
254
+ *
255
+ * Returns `null` if the payload does not contain a valid JUMBF UUID prefix.
256
+ *
257
+ * @internal
258
+ */
259
+ function stripJumbfUuidPrefix(payload) {
260
+ if (payload.length < FULLBOX_HEADER_SIZE) return null;
261
+ let offset = FULLBOX_HEADER_SIZE;
262
+ while (offset < payload.length && payload[offset] !== 0) offset++;
263
+ if (offset >= payload.length) return null;
264
+ offset++;
265
+ offset += AUX_UUID_OFFSET_SIZE;
266
+ if (offset > payload.length) return null;
267
+ return payload.subarray(offset);
268
+ }
269
+
270
+ //#endregion
271
+ //#region src/x509/asn1.ts
272
+ /**
273
+ * Minimal ASN.1 DER parsing helpers for X.509 certificate navigation.
274
+ *
275
+ * @internal
276
+ */
277
+ const ASN1_TAG_INTEGER = 2;
278
+ const ASN1_TAG_OBJECT_IDENTIFIER = 6;
279
+ const ASN1_TAG_SEQUENCE = 48;
280
+ const ASN1_TAG_SET = 49;
281
+ const ASN1_TAG_UTC_TIME = 23;
282
+ const ASN1_TAG_GENERALIZED_TIME = 24;
283
+ const ASN1_TAG_CONTEXT_0 = 160;
284
+ const ASN1_LONG_FORM_FLAG = 128;
285
+ const ASN1_LONG_FORM_MASK = 127;
286
+ function readLength(bytes, offset) {
287
+ if (offset >= bytes.length) return {
288
+ length: 0,
289
+ bytesRead: 0
290
+ };
291
+ const first = bytes[offset] ?? 0;
292
+ if (first < ASN1_LONG_FORM_FLAG) return {
293
+ length: first,
294
+ bytesRead: 1
295
+ };
296
+ const count = first & ASN1_LONG_FORM_MASK;
297
+ let length = 0;
298
+ for (let i = 0; i < count; i++) length = length << 8 | (bytes[offset + 1 + i] ?? 0);
299
+ return {
300
+ length,
301
+ bytesRead: 1 + count
302
+ };
303
+ }
304
+ function readElement(bytes, offset) {
305
+ if (offset + 2 > bytes.length) return null;
306
+ const tag = bytes[offset] ?? 0;
307
+ const { length, bytesRead } = readLength(bytes, offset + 1);
308
+ const headerSize = 1 + bytesRead;
309
+ const valueEnd = offset + headerSize + length;
310
+ if (valueEnd > bytes.length) return null;
311
+ return {
312
+ tag,
313
+ value: bytes.subarray(offset + headerSize, valueEnd),
314
+ totalSize: headerSize + length
315
+ };
316
+ }
317
+ /**
318
+ * Reads an ASN.1 element and returns its full DER encoding (tag + length + value).
319
+ *
320
+ * @internal
321
+ */
322
+ function readElementRaw(bytes, offset) {
323
+ const el = readElement(bytes, offset);
324
+ if (!el) return null;
325
+ return bytes.subarray(offset, offset + el.totalSize);
326
+ }
327
+
328
+ //#endregion
329
+ //#region src/x509/extractCertificateInfo.ts
330
+ const TEXT_DECODER$1 = new TextDecoder();
331
+ const OID_COMMON_NAME = new Uint8Array([
332
+ 85,
333
+ 4,
334
+ 3
335
+ ]);
336
+ const OID_ORG_NAME = new Uint8Array([
337
+ 85,
338
+ 4,
339
+ 10
340
+ ]);
341
+ function matchesOID(value, oid) {
342
+ if (value.length < oid.length) return false;
343
+ for (let i = 0; i < oid.length; i++) if (value[i] !== oid[i]) return false;
344
+ return true;
345
+ }
346
+ function parseTime(element) {
347
+ const text = TEXT_DECODER$1.decode(element.value);
348
+ if (element.tag === ASN1_TAG_UTC_TIME) {
349
+ const yy = parseInt(text.substring(0, 2), 10);
350
+ 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`;
351
+ }
352
+ if (element.tag === ASN1_TAG_GENERALIZED_TIME) 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`;
353
+ return null;
354
+ }
355
+ function findOidValueInSequence(seqValue, targetOID) {
356
+ const oidEl = readElement(seqValue, 0);
357
+ if (!oidEl || oidEl.tag !== ASN1_TAG_OBJECT_IDENTIFIER) return null;
358
+ if (!matchesOID(oidEl.value, targetOID)) return null;
359
+ const valEl = readElement(seqValue, oidEl.totalSize);
360
+ return valEl ? TEXT_DECODER$1.decode(valEl.value) : null;
361
+ }
362
+ function findRDNValue(issuerValue, targetOID) {
363
+ let offset = 0;
364
+ while (offset < issuerValue.length) {
365
+ const setEl = readElement(issuerValue, offset);
366
+ if (!setEl || setEl.tag !== ASN1_TAG_SET) break;
367
+ let setOffset = 0;
368
+ while (setOffset < setEl.value.length) {
369
+ const seqEl = readElement(setEl.value, setOffset);
370
+ if (!seqEl || seqEl.tag !== ASN1_TAG_SEQUENCE) break;
371
+ const found = findOidValueInSequence(seqEl.value, targetOID);
372
+ if (found) return found;
373
+ setOffset += seqEl.totalSize;
374
+ }
375
+ offset += setEl.totalSize;
376
+ }
377
+ return null;
378
+ }
379
+ /**
380
+ * Extracts the issuer name and `notBefore` date from a DER-encoded X.509 certificate.
381
+ *
382
+ * Uses a minimal ASN.1 parser that handles the subset of DER structures found in
383
+ * typical C2PA signing certificates. Extracts the issuer Common Name (OID 2.5.4.3)
384
+ * or Organization Name (OID 2.5.4.10) as a fallback, and the `notBefore` validity date.
385
+ *
386
+ * @param certDER - DER-encoded X.509 certificate bytes
387
+ * @returns Certificate information, or `null` if parsing fails
388
+ *
389
+ * @example
390
+ * {@includeCode ../../test/x509/extractCertificateInfo.test.ts#example}
391
+ *
392
+ * @public
393
+ */
394
+ function extractCertificateInfo(certDER) {
395
+ try {
396
+ const cert = readElement(certDER, 0);
397
+ if (!cert || cert.tag !== ASN1_TAG_SEQUENCE) return null;
398
+ const tbs = readElement(cert.value, 0);
399
+ if (!tbs || tbs.tag !== ASN1_TAG_SEQUENCE) return null;
400
+ let offset = 0;
401
+ let el = readElement(tbs.value, offset);
402
+ if (!el) return null;
403
+ if (el.tag === ASN1_TAG_CONTEXT_0) {
404
+ offset += el.totalSize;
405
+ el = readElement(tbs.value, offset);
406
+ if (!el) return null;
407
+ }
408
+ if (el.tag !== ASN1_TAG_INTEGER) return null;
409
+ offset += el.totalSize;
410
+ el = readElement(tbs.value, offset);
411
+ if (!el || el.tag !== ASN1_TAG_SEQUENCE) return null;
412
+ offset += el.totalSize;
413
+ el = readElement(tbs.value, offset);
414
+ if (!el || el.tag !== ASN1_TAG_SEQUENCE) return null;
415
+ const issuer = findRDNValue(el.value, OID_COMMON_NAME) ?? findRDNValue(el.value, OID_ORG_NAME) ?? "Unknown Issuer";
416
+ offset += el.totalSize;
417
+ el = readElement(tbs.value, offset);
418
+ let notBefore = null;
419
+ if (el && el.tag === ASN1_TAG_SEQUENCE) {
420
+ const timeEl = readElement(el.value, 0);
421
+ if (timeEl) notBefore = parseTime(timeEl);
422
+ }
423
+ return {
424
+ issuer,
425
+ notBefore
426
+ };
427
+ } catch {
428
+ return null;
429
+ }
430
+ }
431
+
432
+ //#endregion
433
+ //#region src/readC2paManifest.ts
434
+ const TEXT_DECODER = new TextDecoder();
435
+ const MAX_JUMBF_NESTING_DEPTH = 20;
436
+ const X5CHAIN_HEADER_LABEL = 33;
437
+ function resolveManifestBoxes(jumbfBoxes, depth = 0) {
438
+ if (depth >= MAX_JUMBF_NESTING_DEPTH) return jumbfBoxes;
439
+ const contentBoxes = jumbfBoxes.filter((b) => b.type !== "jumd");
440
+ const firstBox = contentBoxes[0];
441
+ if (contentBoxes.length === 1 && firstBox?.type === "jumb") return resolveManifestBoxes(parseJumbfBoxes(firstBox.data), depth + 1);
442
+ return jumbfBoxes;
443
+ }
444
+ function extractManifestLabel(jumbfBoxes) {
445
+ for (const box of jumbfBoxes) {
446
+ if (box.type !== "jumb") continue;
447
+ const inner = parseJumbfBoxes(box.data);
448
+ for (const child of inner) {
449
+ if (child.type !== "jumb") continue;
450
+ const jumd = parseJumbfBoxes(child.data).find((b) => b.type === "jumd");
451
+ if (jumd) {
452
+ const label = parseJumbfLabel(jumd.data);
453
+ if (label) return label;
454
+ }
455
+ }
456
+ }
457
+ return null;
458
+ }
459
+ function parseAssertionsInternal(assertionStoreBoxes) {
460
+ const assertions = [];
461
+ for (const box of assertionStoreBoxes) {
462
+ if (box.type !== "jumb") continue;
463
+ const inner = parseJumbfBoxes(box.data);
464
+ const jumd = inner.find((b) => b.type === "jumd");
465
+ if (!jumd) continue;
466
+ const label = parseJumbfLabel(jumd.data);
467
+ if (!label) continue;
468
+ const contentBox = inner.find((b) => b.type === "cbor" || b.type === "json" || b.type === "jumc" || b.type === "jp2c" || b.type === "bidb");
469
+ let data = null;
470
+ if (contentBox) if (contentBox.type === "cbor") try {
471
+ data = decode(contentBox.data);
472
+ } catch {
473
+ data = contentBox.data;
474
+ }
475
+ else if (contentBox.type === "json") try {
476
+ data = JSON.parse(TEXT_DECODER.decode(contentBox.data));
477
+ } catch {
478
+ data = contentBox.data;
479
+ }
480
+ else data = contentBox.data;
481
+ assertions.push({
482
+ label,
483
+ data,
484
+ rawBoxPayload: box.data
485
+ });
486
+ }
487
+ return assertions;
488
+ }
489
+ function parseSignatureInfo(signatureBytes) {
490
+ if (!signatureBytes) return {
491
+ issuer: null,
492
+ certNotBefore: null
493
+ };
494
+ try {
495
+ const cose = decodeCoseSign1(signatureBytes);
496
+ const x5chain = cose.protectedHeader[X5CHAIN_HEADER_LABEL] ?? cose.unprotectedHeader[X5CHAIN_HEADER_LABEL];
497
+ if (!x5chain) return {
498
+ issuer: null,
499
+ certNotBefore: null
500
+ };
501
+ const certDER = Array.isArray(x5chain) ? x5chain[0] : x5chain;
502
+ if (!(certDER instanceof Uint8Array)) return {
503
+ issuer: null,
504
+ certNotBefore: null
505
+ };
506
+ const certInfo = extractCertificateInfo(certDER);
507
+ return {
508
+ issuer: certInfo?.issuer ?? null,
509
+ certNotBefore: certInfo?.notBefore ?? null
510
+ };
511
+ } catch {
512
+ return {
513
+ issuer: null,
514
+ certNotBefore: null
515
+ };
516
+ }
517
+ }
518
+ function extractClaimAssertionRefs(claimData) {
519
+ const created = claimData["created_assertions"];
520
+ const gathered = claimData["gathered_assertions"];
521
+ const v1 = claimData["assertions"];
522
+ const sources = [
523
+ ...created ?? [],
524
+ ...gathered ?? [],
525
+ ...v1 ?? []
526
+ ];
527
+ const refs = [];
528
+ for (const entry of sources) {
529
+ if (!entry || typeof entry !== "object") continue;
530
+ const e = entry;
531
+ const url = e["url"];
532
+ const hash = e["hash"];
533
+ if (!url || !hash) continue;
534
+ refs.push({
535
+ url,
536
+ hash: hash instanceof Uint8Array ? hash : new Uint8Array(hash),
537
+ alg: e["alg"] ?? null
538
+ });
539
+ }
540
+ return refs;
541
+ }
542
+ /**
543
+ * Reads a C2PA manifest from raw BMFF bytes.
544
+ *
545
+ * Locates the C2PA UUID box (`d8fec3d6-1a96-4f32-a0f6-f3ecf96c10ea`), navigates
546
+ * the JUMBF manifest store structure (ISO 19566-5), and returns the parsed active
547
+ * manifest with its claims, assertions, and signature information.
548
+ *
549
+ * This function performs structural parsing only. It does not verify the
550
+ * cryptographic signature of the claim.
551
+ *
552
+ * @param bytes - Raw BMFF bytes (e.g. an MP4 init segment or media segment)
553
+ * @param preParsedBoxes - Optional pre-parsed ISO boxes to avoid redundant parsing
554
+ * @returns The parsed manifest data including claim, assertions, and signature bytes
555
+ * @throws If no C2PA UUID box is found, or the JUMBF structure is invalid
556
+ *
557
+ * @example
558
+ * {@includeCode ../test/readC2paManifest.test.ts#example}
559
+ *
560
+ * @internal
561
+ */
562
+ function readC2paManifest(bytes, preParsedBoxes) {
563
+ const uuidBox = findC2paUuidBox(preParsedBoxes ?? readIsoBoxes(bytes));
564
+ if (!uuidBox) throw new Error("No C2PA UUID box found in the provided bytes");
565
+ const jumbfPayload = stripJumbfUuidPrefix(uuidBox.view.readData(uuidBox.view.bytesRemaining));
566
+ if (!jumbfPayload) throw new Error("Invalid JUMBF UUID prefix in C2PA box");
567
+ const jumbfBoxes = parseJumbfBoxes(jumbfPayload);
568
+ if (jumbfBoxes.length === 0) throw new Error("No JUMBF boxes found in C2PA UUID box");
569
+ const manifestLabel = extractManifestLabel(jumbfBoxes);
570
+ const manifestBoxes = resolveManifestBoxes(jumbfBoxes);
571
+ let claimData = null;
572
+ let claimCborBytes = null;
573
+ let internalAssertions = [];
574
+ let signatureBytes = null;
575
+ for (const box of manifestBoxes) {
576
+ if (box.type !== "jumb") continue;
577
+ const inner = parseJumbfBoxes(box.data);
578
+ const jumd = inner.find((b) => b.type === "jumd");
579
+ if (!jumd) continue;
580
+ const label = parseJumbfLabel(jumd.data);
581
+ if (!label) continue;
582
+ if (label === "c2pa.claim" || label === "c2pa.claim.v2") {
583
+ const contentBox = inner.find((b) => b.type === "cbor");
584
+ if (contentBox) {
585
+ claimCborBytes = contentBox.data;
586
+ try {
587
+ claimData = decode(contentBox.data);
588
+ } catch {}
589
+ }
590
+ } else if (label === "c2pa.assertions") internalAssertions = parseAssertionsInternal(inner);
591
+ else if (label === "c2pa.signature") {
592
+ const contentBox = inner.find((b) => b.type === "cbor" || b.type === "jumc");
593
+ if (contentBox) signatureBytes = contentBox.data;
594
+ }
595
+ }
596
+ const { issuer, certNotBefore } = parseSignatureInfo(signatureBytes);
597
+ const instanceId = claimData?.["instanceID"] ?? claimData?.["instance_id"] ?? null;
598
+ const claimGenerator = claimData?.["claim_generator"] ?? claimData?.["claimGenerator"] ?? null;
599
+ const resolvedLabel = manifestLabel ?? claimData?.["dc:title"] ?? claimData?.["title"] ?? instanceId ?? "unknown";
600
+ const claimAssertionRefs = claimData ? extractClaimAssertionRefs(claimData) : [];
601
+ const assertions = internalAssertions.map((a) => ({
602
+ label: a.label,
603
+ data: a.data
604
+ }));
605
+ return {
606
+ manifest: {
607
+ label: resolvedLabel,
608
+ instanceId,
609
+ claimGenerator,
610
+ signatureInfo: {
611
+ issuer,
612
+ certNotBefore
613
+ },
614
+ assertions
615
+ },
616
+ claimAssertionRefs,
617
+ claimCborBytes,
618
+ signatureBytes,
619
+ assertions: internalAssertions
620
+ };
621
+ }
622
+
623
+ //#endregion
624
+ //#region src/extractManifestCertificate.ts
625
+ const X5CHAIN_COSE_HEADER = 33;
626
+ /**
627
+ * Extracts the end-entity certificate from raw COSE_Sign1 signature bytes.
628
+ *
629
+ * @internal
630
+ */
631
+ function extractCertificateFromSignatureBytes(signatureBytes) {
632
+ try {
633
+ const cose = decodeCoseSign1(signatureBytes);
634
+ const x5chain = cose.protectedHeader[X5CHAIN_COSE_HEADER] ?? cose.unprotectedHeader[X5CHAIN_COSE_HEADER];
635
+ if (!x5chain) return null;
636
+ const certDER = Array.isArray(x5chain) ? x5chain[0] : x5chain;
637
+ return certDER instanceof Uint8Array ? certDER : null;
638
+ } catch {
639
+ return null;
640
+ }
641
+ }
642
+
643
+ //#endregion
644
+ //#region src/bmff/shouldExcludeBox.ts
645
+ function bytesMatchAt(boxData, offset, expected) {
646
+ const bytes = expected instanceof Uint8Array ? expected : new Uint8Array(expected);
647
+ if (offset + bytes.length > boxData.length) return false;
648
+ for (let i = 0; i < bytes.length; i++) if (boxData[offset + i] !== bytes[i]) return false;
649
+ return true;
650
+ }
651
+ function constraintMatches(boxData, constraint) {
652
+ return bytesMatchAt(boxData, constraint.offset, constraint.value);
653
+ }
654
+ /**
655
+ * Returns `true` if the given BMFF box should be excluded from the
656
+ * C2PA content hash, based on the provided exclusion list.
657
+ *
658
+ * Exclusions use C2PA xpath notation (e.g. `/emsg`, `/moof/traf`).
659
+ * Optional `data` constraints narrow the match to specific box content by byte offset.
660
+ *
661
+ * @param boxType - Four-character box type code (e.g. `'emsg'`, `'moof'`)
662
+ * @param boxData - Full box bytes including the 8-byte size+type header
663
+ * @param exclusions - Exclusion list from the C2PA `c2pa.hash.bmff.v3` assertion
664
+ * @returns `true` if the box should be excluded from the hash input
665
+ *
666
+ * @example
667
+ * {@includeCode ../../test/bmff/shouldExcludeBox.test.ts#example}
668
+ *
669
+ * @public
670
+ */
671
+ function shouldExcludeBox(boxType, boxData, exclusions) {
672
+ for (const exclusion of exclusions) {
673
+ if (!exclusion.xpath) continue;
674
+ if (!(exclusion.xpath === `/${boxType}` || exclusion.xpath.startsWith(`/${boxType}/`))) continue;
675
+ const { data } = exclusion;
676
+ if (!data || data.length === 0) return true;
677
+ if (data.every((constraint) => constraintMatches(boxData, constraint))) return true;
678
+ }
679
+ return false;
680
+ }
681
+
682
+ //#endregion
683
+ //#region src/bmff/computeBmffHash.ts
684
+ const MINIMUM_BOX_SIZE = 8;
685
+ const BOX_TYPE_OFFSET = 4;
686
+ const DEFAULT_HASH_ALG = "SHA-256";
687
+ function readBoxType(bytes, offset) {
688
+ return String.fromCharCode(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]);
689
+ }
690
+ function writeUint64BigEndian(bytes, value) {
691
+ new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).setBigUint64(0, BigInt(value), false);
692
+ }
693
+ function buildHashInput(bytes, exclusions, offsetPrefixSize) {
694
+ if (offsetPrefixSize !== 0 && offsetPrefixSize !== 8) throw new Error(`offsetPrefixSize must be 0 or 8, got ${offsetPrefixSize}`);
695
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
696
+ let totalLength = 0;
697
+ let offset = 0;
698
+ while (offset + MINIMUM_BOX_SIZE <= bytes.length) {
699
+ const boxSize = view.getUint32(offset, false);
700
+ if (boxSize < MINIMUM_BOX_SIZE || offset + boxSize > bytes.length) break;
701
+ if (!shouldExcludeBox(readBoxType(bytes, offset + BOX_TYPE_OFFSET), bytes.subarray(offset, offset + boxSize), exclusions)) totalLength += offsetPrefixSize + boxSize;
702
+ offset += boxSize;
703
+ }
704
+ const hashInput = new Uint8Array(totalLength);
705
+ let writeOffset = 0;
706
+ offset = 0;
707
+ while (offset + MINIMUM_BOX_SIZE <= bytes.length) {
708
+ const boxSize = view.getUint32(offset, false);
709
+ if (boxSize < MINIMUM_BOX_SIZE || offset + boxSize > bytes.length) break;
710
+ const boxType = readBoxType(bytes, offset + BOX_TYPE_OFFSET);
711
+ const boxData = bytes.subarray(offset, offset + boxSize);
712
+ if (!shouldExcludeBox(boxType, boxData, exclusions)) {
713
+ if (offsetPrefixSize > 0) {
714
+ writeUint64BigEndian(hashInput.subarray(writeOffset, writeOffset + offsetPrefixSize), offset);
715
+ writeOffset += offsetPrefixSize;
716
+ }
717
+ hashInput.set(boxData, writeOffset);
718
+ writeOffset += boxSize;
719
+ }
720
+ offset += boxSize;
721
+ }
722
+ return hashInput;
723
+ }
724
+ /**
725
+ * Computes the C2PA BMFF content hash (`c2pa.hash.bmff.v3`) for a DASH segment.
726
+ *
727
+ * Iterates through top-level BMFF boxes, excludes those matching the exclusion list,
728
+ * concatenates the remaining box bytes (optionally prefixed with each box's file offset),
729
+ * and returns the WebCrypto digest.
730
+ *
731
+ * @param segmentBytes - Raw segment bytes to hash
732
+ * @param options - Hash options (exclusions, algorithm, offset prefix size)
733
+ * @returns The computed hash bytes
734
+ *
735
+ * @example
736
+ * {@includeCode ../../test/bmff/computeBmffHash.test.ts#example}
737
+ *
738
+ * @public
739
+ */
740
+ async function computeBmffHash(segmentBytes, options) {
741
+ const exclusions = options?.exclusions ?? [];
742
+ const alg = options?.alg ?? DEFAULT_HASH_ALG;
743
+ const hashInput = buildHashInput(segmentBytes, exclusions, options?.offsetPrefixSize ?? 0);
744
+ const hashBuffer = await crypto.subtle.digest(alg, hashInput);
745
+ return new Uint8Array(hashBuffer);
746
+ }
747
+
748
+ //#endregion
749
+ //#region src/bmff/validateBmffHash.ts
750
+ const OFFSET_PREFIX_SIZES_TO_TRY = [8, 0];
751
+ /**
752
+ * Validates the C2PA BMFF content hash (`c2pa.hash.bmff.v3`) for a DASH segment.
753
+ *
754
+ * Tries both offset-prefix modes — 8-byte file offset prefix and no prefix — to handle
755
+ * inter-signer interoperability until a standard signaling mechanism is defined in the spec.
756
+ *
757
+ * Use {@link computeBmffHash} directly when the offset prefix size is known.
758
+ *
759
+ * @param segmentBytes - Raw segment bytes to validate
760
+ * @param expectedHash - Expected hash bytes from the VSI CBOR map
761
+ * @param options - Validation options (exclusions, algorithm)
762
+ * @returns `true` if the computed hash matches the expected hash in either offset mode
763
+ *
764
+ * @example
765
+ * {@includeCode ../../test/bmff/validateBmffHash.test.ts#example}
766
+ *
767
+ * @public
768
+ */
769
+ async function validateBmffHash(segmentBytes, expectedHash, options) {
770
+ for (const offsetPrefixSize of OFFSET_PREFIX_SIZES_TO_TRY) if (hashesEqual(await computeBmffHash(segmentBytes, {
771
+ ...options,
772
+ offsetPrefixSize
773
+ }), expectedHash)) return true;
774
+ return false;
775
+ }
776
+
777
+ //#endregion
778
+ //#region src/C2paStatusCode.ts
779
+ /**
780
+ * Standard C2PA validation status codes for manifest integrity checks,
781
+ * as defined in the C2PA specification chapters 15 and 18.
782
+ *
783
+ * @see {@link https://c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_claim_validation | C2PA Spec §15.10.3}
784
+ *
785
+ * @enum
786
+ *
787
+ * @public
788
+ */
789
+ const C2paStatusCode = {
790
+ ASSERTION_HASHEDURI_MISMATCH: "assertion.hashedURI.mismatch",
791
+ ASSERTION_MISSING: "assertion.missing",
792
+ ASSERTION_ACTION_INGREDIENT_MISMATCH: "assertion.action.ingredientMismatch",
793
+ CLAIM_SIGNATURE_MISMATCH: "claim.signature.mismatch"
794
+ };
795
+
796
+ //#endregion
797
+ //#region src/claim/validateActionIngredients.ts
798
+ const ACTIONS_LABELS = new Set(["c2pa.actions", "c2pa.actions.v2"]);
799
+ const ACTIONS_REQUIRING_INGREDIENTS = new Set([
800
+ "c2pa.opened",
801
+ "c2pa.placed",
802
+ "c2pa.removed"
803
+ ]);
804
+ /**
805
+ * Validates action ingredient references per C2PA §18.15.4.7.
806
+ *
807
+ * For any `c2pa.actions` or `c2pa.actions.v2` assertion, actions with type
808
+ * `c2pa.opened`, `c2pa.placed`, or `c2pa.removed` must include a
809
+ * `parameters.ingredients` field. Reports `assertion.action.ingredientMismatch`
810
+ * if any such action is missing the required field.
811
+ *
812
+ * @param assertions - Parsed assertions (label + decoded data)
813
+ * @returns Array of C2PA status codes for any failures found
814
+ *
815
+ * @internal
816
+ */
817
+ function validateActionIngredients(assertions) {
818
+ const codes = [];
819
+ for (const assertion of assertions) {
820
+ if (!ACTIONS_LABELS.has(assertion.label)) continue;
821
+ const data = assertion.data;
822
+ if (!data) continue;
823
+ const actions = data["actions"];
824
+ if (!Array.isArray(actions)) continue;
825
+ for (const action of actions) {
826
+ if (!action || typeof action !== "object") continue;
827
+ const record = action;
828
+ const actionType = record["action"];
829
+ if (!actionType || !ACTIONS_REQUIRING_INGREDIENTS.has(actionType)) continue;
830
+ const ingredients = record["parameters"]?.["ingredients"];
831
+ if (!Array.isArray(ingredients) || ingredients.length === 0) codes.push(C2paStatusCode.ASSERTION_ACTION_INGREDIENT_MISMATCH);
832
+ }
833
+ }
834
+ return codes;
835
+ }
836
+
837
+ //#endregion
838
+ //#region src/claim/validateAssertionHashes.ts
839
+ /**
840
+ * Extracts the assertion label from a JUMBF URI.
841
+ *
842
+ * Handles both absolute (`self#jumbf=/c2pa/<manifest>/c2pa.assertions/<label>`)
843
+ * and relative URI forms by returning the last path segment.
844
+ */
845
+ function extractLabelFromUrl(url) {
846
+ const fragmentIndex = url.indexOf("#jumbf=");
847
+ const path = fragmentIndex >= 0 ? url.substring(fragmentIndex + 7) : url;
848
+ const lastSlash = path.lastIndexOf("/");
849
+ return lastSlash >= 0 ? path.substring(lastSlash + 1) : path;
850
+ }
851
+ /**
852
+ * Validates assertion hashes and presence per C2PA §15.10.3.1.
853
+ *
854
+ * For each assertion referenced in the claim's hashed URI list:
855
+ * - Checks that the assertion exists in the assertion store (`assertion.missing`)
856
+ * - Recomputes the hash of the raw JUMBF box payload and compares it to the
857
+ * expected hash from the claim (`assertion.hashedURI.mismatch`)
858
+ *
859
+ * @param claimRefs - Hashed URI references from the claim
860
+ * @param assertions - Parsed assertions with preserved raw box payloads
861
+ * @returns Array of C2PA status codes for any failures found
862
+ *
863
+ * @internal
864
+ */
865
+ async function validateAssertionHashes(claimRefs, assertions) {
866
+ if (claimRefs.length === 0) return [];
867
+ const assertionsByLabel = /* @__PURE__ */ new Map();
868
+ for (const assertion of assertions) assertionsByLabel.set(assertion.label, assertion);
869
+ const codes = [];
870
+ const hashChecks = claimRefs.map(async (ref) => {
871
+ const label = extractLabelFromUrl(ref.url);
872
+ const assertion = assertionsByLabel.get(label);
873
+ if (!assertion) return C2paStatusCode.ASSERTION_MISSING;
874
+ const alg = ref.alg ? normalizeAlgorithmName(ref.alg) : "SHA-256";
875
+ if (!hashesEqual(new Uint8Array(await crypto.subtle.digest(alg, assertion.rawBoxPayload)), ref.hash)) return C2paStatusCode.ASSERTION_HASHEDURI_MISMATCH;
876
+ return null;
877
+ });
878
+ const results = await Promise.all(hashChecks);
879
+ for (const result of results) if (result) codes.push(result);
880
+ return codes;
881
+ }
882
+
883
+ //#endregion
884
+ //#region src/cose/constants.ts
885
+ const ECDSA_ALGORITHM = "ECDSA";
886
+ const ED25519_ALGORITHM = "Ed25519";
887
+ const RSA_PSS_ALGORITHM = "RSA-PSS";
888
+ const CURVE_P256 = "P-256";
889
+ const CURVE_P384 = "P-384";
890
+ const CURVE_P521 = "P-521";
891
+ const HASH_SHA256 = "SHA-256";
892
+ const HASH_SHA384 = "SHA-384";
893
+ const HASH_SHA512 = "SHA-512";
894
+
895
+ //#endregion
896
+ //#region src/cose/resolveAlgorithmFromCoseAlg.ts
897
+ const COSE_ALGORITHM_MAP = new Map([
898
+ [-7, {
899
+ name: ECDSA_ALGORITHM,
900
+ namedCurve: CURVE_P256
901
+ }],
902
+ [-35, {
903
+ name: ECDSA_ALGORITHM,
904
+ namedCurve: CURVE_P384
905
+ }],
906
+ [-36, {
907
+ name: ECDSA_ALGORITHM,
908
+ namedCurve: CURVE_P521
909
+ }],
910
+ [-8, { name: ED25519_ALGORITHM }],
911
+ [-37, {
912
+ name: RSA_PSS_ALGORITHM,
913
+ hash: { name: HASH_SHA256 }
914
+ }],
915
+ [-38, {
916
+ name: RSA_PSS_ALGORITHM,
917
+ hash: { name: HASH_SHA384 }
918
+ }],
919
+ [-39, {
920
+ name: RSA_PSS_ALGORITHM,
921
+ hash: { name: HASH_SHA512 }
922
+ }]
923
+ ]);
924
+ /**
925
+ * Maps a COSE algorithm number (from a COSE_Sign1 protected header) to the
926
+ * WebCrypto algorithm identifier needed for `crypto.subtle.importKey('spki', ...)`.
927
+ *
928
+ * @param coseAlg - COSE algorithm identifier (e.g. -7 for ES256)
929
+ * @returns WebCrypto algorithm identifier
930
+ * @throws If the COSE algorithm is not supported
931
+ *
932
+ * @internal
933
+ */
934
+ function resolveAlgorithmFromCoseAlg(coseAlg) {
935
+ const algorithm = COSE_ALGORITHM_MAP.get(coseAlg);
936
+ if (!algorithm) throw new Error(`Unsupported COSE algorithm: ${coseAlg}`);
937
+ return algorithm;
938
+ }
939
+
940
+ //#endregion
941
+ //#region src/cose/buildSigStructure.ts
942
+ const CBOR_ARRAY_FOUR_ITEMS = 132;
943
+ const CBOR_TEXT_MAJOR_TYPE_BASE = 96;
944
+ const CBOR_BYTES_MAJOR_TYPE_BASE = 64;
945
+ const CBOR_UINT8_LENGTH_INDICATOR = 88;
946
+ const CBOR_UINT16_LENGTH_INDICATOR = 89;
947
+ const CBOR_UINT32_LENGTH_INDICATOR = 90;
948
+ const CBOR_INLINE_MAX = 23;
949
+ const CBOR_UINT8_MAX = 255;
950
+ const CBOR_UINT16_MAX = 65535;
951
+ const COSE_SIG1_CONTEXT_BYTES = new TextEncoder().encode("Signature1");
952
+ function byteStringHeaderSize(length) {
953
+ if (length <= CBOR_INLINE_MAX) return 1;
954
+ if (length <= CBOR_UINT8_MAX) return 2;
955
+ if (length <= CBOR_UINT16_MAX) return 3;
956
+ return 5;
957
+ }
958
+ function writeByteStringHeader(output, offset, length) {
959
+ if (length <= CBOR_INLINE_MAX) output[offset++] = CBOR_BYTES_MAJOR_TYPE_BASE + length;
960
+ else if (length <= CBOR_UINT8_MAX) {
961
+ output[offset++] = CBOR_UINT8_LENGTH_INDICATOR;
962
+ output[offset++] = length;
963
+ } else if (length <= CBOR_UINT16_MAX) {
964
+ output[offset++] = CBOR_UINT16_LENGTH_INDICATOR;
965
+ output[offset++] = length >> 8 & 255;
966
+ output[offset++] = length & 255;
967
+ } else {
968
+ output[offset++] = CBOR_UINT32_LENGTH_INDICATOR;
969
+ output[offset++] = length >>> 24 & 255;
970
+ output[offset++] = length >>> 16 & 255;
971
+ output[offset++] = length >>> 8 & 255;
972
+ output[offset++] = length & 255;
973
+ }
974
+ return offset;
975
+ }
976
+ /**
977
+ * Builds the COSE `Sig_Structure` (ToBeSigned) bytes for a `COSE_Sign1` structure (RFC 9052 §4.4).
978
+ *
979
+ * The resulting bytes are the direct input to signature creation or verification via WebCrypto.
980
+ *
981
+ * @param protectedBytes - Serialized protected header (CBOR-encoded byte string)
982
+ * @param payload - Payload bytes
983
+ * @param externalAad - External additional authenticated data (default: empty)
984
+ * @returns CBOR-encoded `Sig_Structure` array ready for signing or verification
985
+ *
986
+ * @example
987
+ * {@includeCode ../../test/cose/buildSigStructure.test.ts#example}
988
+ *
989
+ * @public
990
+ */
991
+ function buildSigStructure(protectedBytes, payload, externalAad = new Uint8Array(0)) {
992
+ const totalLength = 2 + COSE_SIG1_CONTEXT_BYTES.length + byteStringHeaderSize(protectedBytes.length) + protectedBytes.length + byteStringHeaderSize(externalAad.length) + externalAad.length + byteStringHeaderSize(payload.length) + payload.length;
993
+ const result = new Uint8Array(totalLength);
994
+ let offset = 0;
995
+ result[offset++] = CBOR_ARRAY_FOUR_ITEMS;
996
+ result[offset++] = CBOR_TEXT_MAJOR_TYPE_BASE + COSE_SIG1_CONTEXT_BYTES.length;
997
+ result.set(COSE_SIG1_CONTEXT_BYTES, offset);
998
+ offset += COSE_SIG1_CONTEXT_BYTES.length;
999
+ offset = writeByteStringHeader(result, offset, protectedBytes.length);
1000
+ result.set(protectedBytes, offset);
1001
+ offset += protectedBytes.length;
1002
+ offset = writeByteStringHeader(result, offset, externalAad.length);
1003
+ result.set(externalAad, offset);
1004
+ offset += externalAad.length;
1005
+ offset = writeByteStringHeader(result, offset, payload.length);
1006
+ result.set(payload, offset);
1007
+ return result;
1008
+ }
1009
+
1010
+ //#endregion
1011
+ //#region src/cose/verifyCoseSign1.ts
1012
+ const DER_SEQUENCE_TAG = 48;
1013
+ const DER_INTEGER_TAG = 2;
1014
+ const CURVE_COMPONENT_BYTES = {
1015
+ [CURVE_P256]: 32,
1016
+ [CURVE_P384]: 48,
1017
+ [CURVE_P521]: 66
1018
+ };
1019
+ const RSA_PSS_SALT_LENGTH = {
1020
+ [HASH_SHA256]: 32,
1021
+ [HASH_SHA384]: 48,
1022
+ [HASH_SHA512]: 64
1023
+ };
1024
+ function getComponentSize(publicKey) {
1025
+ const curve = publicKey.algorithm.namedCurve;
1026
+ const size = CURVE_COMPONENT_BYTES[curve];
1027
+ if (!size) throw new Error(`Unsupported EC curve: ${curve}`);
1028
+ return size;
1029
+ }
1030
+ function isDerEncoded(signature, expectedRawLength) {
1031
+ return signature.length !== expectedRawLength && signature.length > 2 && signature[0] === DER_SEQUENCE_TAG;
1032
+ }
1033
+ function parseDerLength(der, offset) {
1034
+ if (offset >= der.length) throw new Error("DER signature: truncated length");
1035
+ const firstByte = der[offset];
1036
+ if ((firstByte & 128) === 0) return {
1037
+ length: firstByte,
1038
+ nextOffset: offset + 1
1039
+ };
1040
+ const numBytes = firstByte & 127;
1041
+ if (numBytes === 0 || offset + numBytes >= der.length) throw new Error("DER signature: invalid long-form length");
1042
+ let length = 0;
1043
+ for (let i = 1; i <= numBytes; i++) length = length << 8 | der[offset + i];
1044
+ return {
1045
+ length,
1046
+ nextOffset: offset + 1 + numBytes
1047
+ };
1048
+ }
1049
+ function extractDerInteger(der, offset) {
1050
+ if (der[offset] !== DER_INTEGER_TAG) throw new Error("DER signature: expected INTEGER tag");
1051
+ const { length, nextOffset: valueStart } = parseDerLength(der, offset + 1);
1052
+ let start = valueStart;
1053
+ let remaining = length;
1054
+ if (remaining > 0 && der[start] === 0) {
1055
+ start++;
1056
+ remaining--;
1057
+ }
1058
+ return {
1059
+ value: der.slice(start, start + remaining),
1060
+ nextOffset: valueStart + length
1061
+ };
1062
+ }
1063
+ function derToRawEcdsaSignature(der, componentSize) {
1064
+ if (der[0] !== DER_SEQUENCE_TAG) throw new Error("DER signature: expected SEQUENCE tag");
1065
+ const { nextOffset: contentStart } = parseDerLength(der, 1);
1066
+ const rResult = extractDerInteger(der, contentStart);
1067
+ const sResult = extractDerInteger(der, rResult.nextOffset);
1068
+ const rawLength = componentSize * 2;
1069
+ const raw = new Uint8Array(rawLength);
1070
+ raw.set(rResult.value, componentSize - rResult.value.length);
1071
+ raw.set(sResult.value, rawLength - sResult.value.length);
1072
+ return raw;
1073
+ }
1074
+ function resolveVerifyAlgorithm(publicKey) {
1075
+ const { name } = publicKey.algorithm;
1076
+ if (name === ED25519_ALGORITHM) return { name: ED25519_ALGORITHM };
1077
+ if (name === ECDSA_ALGORITHM) {
1078
+ const curve = publicKey.algorithm.namedCurve;
1079
+ return {
1080
+ name: ECDSA_ALGORITHM,
1081
+ hash: { name: curve === CURVE_P384 ? HASH_SHA384 : curve === CURVE_P521 ? HASH_SHA512 : HASH_SHA256 }
1082
+ };
1083
+ }
1084
+ if (name === RSA_PSS_ALGORITHM) return {
1085
+ name: RSA_PSS_ALGORITHM,
1086
+ saltLength: RSA_PSS_SALT_LENGTH[publicKey.algorithm.hash.name] ?? 32
1087
+ };
1088
+ throw new Error(`Unsupported public key algorithm: ${name}`);
1089
+ }
1090
+ function normalizeSignature(signature, publicKey) {
1091
+ if (publicKey.algorithm.name !== ECDSA_ALGORITHM) return signature;
1092
+ const componentSize = getComponentSize(publicKey);
1093
+ if (isDerEncoded(signature, componentSize * 2)) return derToRawEcdsaSignature(signature, componentSize);
1094
+ return signature;
1095
+ }
1096
+ /**
1097
+ * Verifies a `COSE_Sign1` signature using a provided `CryptoKey` (RFC 9052 §4.4).
1098
+ *
1099
+ * Builds the `Sig_Structure` from the protected header and payload, normalizes
1100
+ * DER-encoded ECDSA signatures to raw format if needed, and delegates
1101
+ * verification to the WebCrypto API.
1102
+ *
1103
+ * Supports `ECDSA` (P-256, P-384, P-521), `Ed25519`, and `RSA-PSS` (PS256, PS384, PS512) keys.
1104
+ *
1105
+ * @param coseSign1 - Decoded COSE_Sign1 structure (from {@link decodeCoseSign1})
1106
+ * @param payload - Payload bytes to verify. May differ from `coseSign1.payload` for detached payloads.
1107
+ * @param publicKey - Imported public key for verification
1108
+ * @returns `true` if the signature is valid
1109
+ * @throws If the key algorithm is not supported
1110
+ *
1111
+ * @example
1112
+ * {@includeCode ../../test/cose/verifyCoseSign1.test.ts#example}
1113
+ *
1114
+ * @public
1115
+ */
1116
+ async function verifyCoseSign1(coseSign1, payload, publicKey) {
1117
+ const sigStructure = buildSigStructure(coseSign1.protectedBytes, payload);
1118
+ const algorithm = resolveVerifyAlgorithm(publicKey);
1119
+ const signature = normalizeSignature(coseSign1.signature, publicKey);
1120
+ return crypto.subtle.verify(algorithm, publicKey, signature, sigStructure);
1121
+ }
1122
+
1123
+ //#endregion
1124
+ //#region src/x509/extractCertificateSpki.ts
1125
+ /**
1126
+ * Extracts the SubjectPublicKeyInfo (SPKI) DER bytes from an X.509 DER certificate.
1127
+ *
1128
+ * Navigates the TBSCertificate structure to reach the 7th field
1129
+ * (SubjectPublicKeyInfo), skipping version, serialNumber, signatureAlgorithm,
1130
+ * issuer, validity, and subject.
1131
+ *
1132
+ * The returned bytes can be imported directly via
1133
+ * `crypto.subtle.importKey('spki', ...)`.
1134
+ *
1135
+ * @param certDER - DER-encoded X.509 certificate bytes
1136
+ * @returns Full DER encoding of the SubjectPublicKeyInfo, or `null` on parse failure
1137
+ *
1138
+ * @internal
1139
+ */
1140
+ function extractCertificateSpki(certDER) {
1141
+ try {
1142
+ const cert = readElement(certDER, 0);
1143
+ if (!cert || cert.tag !== ASN1_TAG_SEQUENCE) return null;
1144
+ const tbs = readElement(cert.value, 0);
1145
+ if (!tbs || tbs.tag !== ASN1_TAG_SEQUENCE) return null;
1146
+ let offset = 0;
1147
+ let el = readElement(tbs.value, offset);
1148
+ if (!el) return null;
1149
+ if (el.tag === ASN1_TAG_CONTEXT_0) {
1150
+ offset += el.totalSize;
1151
+ el = readElement(tbs.value, offset);
1152
+ if (!el) return null;
1153
+ }
1154
+ if (el.tag !== ASN1_TAG_INTEGER) return null;
1155
+ offset += el.totalSize;
1156
+ el = readElement(tbs.value, offset);
1157
+ if (!el || el.tag !== ASN1_TAG_SEQUENCE) return null;
1158
+ offset += el.totalSize;
1159
+ el = readElement(tbs.value, offset);
1160
+ if (!el || el.tag !== ASN1_TAG_SEQUENCE) return null;
1161
+ offset += el.totalSize;
1162
+ el = readElement(tbs.value, offset);
1163
+ if (!el || el.tag !== ASN1_TAG_SEQUENCE) return null;
1164
+ offset += el.totalSize;
1165
+ el = readElement(tbs.value, offset);
1166
+ if (!el || el.tag !== ASN1_TAG_SEQUENCE) return null;
1167
+ offset += el.totalSize;
1168
+ return readElementRaw(tbs.value, offset);
1169
+ } catch {
1170
+ return null;
1171
+ }
1172
+ }
1173
+
1174
+ //#endregion
1175
+ //#region src/x509/normalizeRsaPssSpki.ts
1176
+ const OID_RSASSA_PSS = new Uint8Array([
1177
+ 6,
1178
+ 9,
1179
+ 42,
1180
+ 134,
1181
+ 72,
1182
+ 134,
1183
+ 247,
1184
+ 13,
1185
+ 1,
1186
+ 1,
1187
+ 10
1188
+ ]);
1189
+ const RSA_ENCRYPTION_ALGORITHM_ID = new Uint8Array([
1190
+ 48,
1191
+ 13,
1192
+ 6,
1193
+ 9,
1194
+ 42,
1195
+ 134,
1196
+ 72,
1197
+ 134,
1198
+ 247,
1199
+ 13,
1200
+ 1,
1201
+ 1,
1202
+ 1,
1203
+ 5,
1204
+ 0
1205
+ ]);
1206
+ function containsRsaPssOid(bytes) {
1207
+ if (bytes.length < OID_RSASSA_PSS.length) return false;
1208
+ outer: for (let i = 0; i <= bytes.length - OID_RSASSA_PSS.length; i++) {
1209
+ for (let j = 0; j < OID_RSASSA_PSS.length; j++) if (bytes[i + j] !== OID_RSASSA_PSS[j]) continue outer;
1210
+ return true;
1211
+ }
1212
+ return false;
1213
+ }
1214
+ /**
1215
+ * Normalizes an SPKI that uses the `rsassaPss` OID (1.2.840.113549.1.1.10) to
1216
+ * use the generic `rsaEncryption` OID (1.2.840.113549.1.1.1) instead.
1217
+ *
1218
+ * WebCrypto's `importKey('spki', ...)` with `{ name: 'RSA-PSS' }` expects the
1219
+ * SPKI to use the generic `rsaEncryption` OID. Certificates signed with
1220
+ * RSASSA-PSS often embed the PSS-specific OID and parameters, which causes
1221
+ * `DataError` on import.
1222
+ *
1223
+ * If the SPKI does not use `rsassaPss`, it is returned unchanged.
1224
+ *
1225
+ * @internal
1226
+ */
1227
+ function normalizeRsaPssSpki(spkiBytes) {
1228
+ if (!containsRsaPssOid(spkiBytes)) return spkiBytes;
1229
+ const outer = readElement(spkiBytes, 0);
1230
+ if (!outer || outer.tag !== ASN1_TAG_SEQUENCE) return spkiBytes;
1231
+ const algId = readElement(outer.value, 0);
1232
+ if (!algId || algId.tag !== ASN1_TAG_SEQUENCE) return spkiBytes;
1233
+ const publicKeyBitString = outer.value.subarray(algId.totalSize);
1234
+ const innerLength = RSA_ENCRYPTION_ALGORITHM_ID.length + publicKeyBitString.length;
1235
+ const headerBytes = encodeDerLength(innerLength);
1236
+ const result = new Uint8Array(1 + headerBytes.length + innerLength);
1237
+ let offset = 0;
1238
+ result[offset++] = ASN1_TAG_SEQUENCE;
1239
+ result.set(headerBytes, offset);
1240
+ offset += headerBytes.length;
1241
+ result.set(RSA_ENCRYPTION_ALGORITHM_ID, offset);
1242
+ offset += RSA_ENCRYPTION_ALGORITHM_ID.length;
1243
+ result.set(publicKeyBitString, offset);
1244
+ return result;
1245
+ }
1246
+ function encodeDerLength(length) {
1247
+ if (length < 128) return new Uint8Array([length]);
1248
+ if (length <= 255) return new Uint8Array([129, length]);
1249
+ if (length <= 65535) return new Uint8Array([
1250
+ 130,
1251
+ length >> 8 & 255,
1252
+ length & 255
1253
+ ]);
1254
+ return new Uint8Array([
1255
+ 131,
1256
+ length >> 16 & 255,
1257
+ length >> 8 & 255,
1258
+ length & 255
1259
+ ]);
1260
+ }
1261
+
1262
+ //#endregion
1263
+ //#region src/claim/verifyClaimSignature.ts
1264
+ /**
1265
+ * Verifies the claim signature of a C2PA manifest per §15.7.
1266
+ *
1267
+ * The `c2pa.signature` JUMBF box contains a `COSE_Sign1` structure whose payload
1268
+ * is the claim CBOR bytes. This function verifies the signature against the
1269
+ * public key extracted from the end-entity certificate in the `x5chain` header.
1270
+ *
1271
+ * @param signatureBytes - Raw COSE_Sign1 bytes from the `c2pa.signature` box
1272
+ * @param claimCborBytes - Raw CBOR bytes of the claim content box
1273
+ * @param certificateDER - DER-encoded end-entity X.509 certificate
1274
+ * @returns `true` if the signature is valid
1275
+ *
1276
+ * @internal
1277
+ */
1278
+ async function verifyClaimSignature(signatureBytes, claimCborBytes, certificateDER) {
1279
+ try {
1280
+ const coseSign1 = decodeCoseSign1(signatureBytes);
1281
+ const rawSpki = extractCertificateSpki(certificateDER);
1282
+ if (!rawSpki) return false;
1283
+ const spkiBytes = normalizeRsaPssSpki(rawSpki);
1284
+ if (coseSign1.alg == null) return false;
1285
+ const importAlgorithm = resolveAlgorithmFromCoseAlg(coseSign1.alg);
1286
+ return verifyCoseSign1(coseSign1, claimCborBytes, await crypto.subtle.importKey("spki", new Uint8Array(spkiBytes), importAlgorithm, true, ["verify"]));
1287
+ } catch {
1288
+ return false;
1289
+ }
1290
+ }
1291
+
1292
+ //#endregion
1293
+ //#region src/claim/validateManifestIntegrity.ts
1294
+ /**
1295
+ * Validates the integrity of a C2PA manifest per Chapter 15 and Chapter 18.
1296
+ *
1297
+ * Runs four validation checks in parallel where possible:
1298
+ * 1. Assertion hash verification (§15.10.3.1)
1299
+ * 2. Missing assertion detection (§15.10.3.1)
1300
+ * 3. Action ingredient validation (§18.15.4.7)
1301
+ * 4. Claim signature verification (§15.7)
1302
+ *
1303
+ * @param internal - Enriched manifest data with raw assertion bytes and claim references
1304
+ * @param certificateDER - DER-encoded end-entity certificate, or `null` to skip signature check
1305
+ * @returns Array of C2PA status codes for any failures found (empty if all pass)
1306
+ *
1307
+ * @internal
1308
+ */
1309
+ async function validateManifestIntegrity(internal, certificateDER) {
1310
+ const { signatureBytes, claimCborBytes } = internal;
1311
+ const [assertionHashCodes, signatureValid] = await Promise.all([validateAssertionHashes(internal.claimAssertionRefs, internal.assertions), signatureBytes && claimCborBytes && certificateDER ? verifyClaimSignature(signatureBytes, claimCborBytes, certificateDER) : Promise.resolve(true)]);
1312
+ const actionCodes = validateActionIngredients(internal.assertions);
1313
+ const codes = [...assertionHashCodes, ...actionCodes];
1314
+ if (!signatureValid) codes.push(C2paStatusCode.CLAIM_SIGNATURE_MISMATCH);
1315
+ return codes;
1316
+ }
1317
+
1318
+ //#endregion
1319
+ //#region src/cose/convertCoseKeyToJwk.ts
1320
+ const OKP_KEY_TYPE = 1;
1321
+ const EC2_KEY_TYPE = 2;
1322
+ const EC_CURVE_NAMES = {
1323
+ 1: "P-256",
1324
+ 2: "P-384",
1325
+ 3: "P-521"
1326
+ };
1327
+ const OKP_CURVE_NAMES = {
1328
+ 4: "X25519",
1329
+ 5: "X448",
1330
+ 6: "Ed25519",
1331
+ 7: "Ed448"
1332
+ };
1333
+ function coseGet(key, intKey) {
1334
+ if (key instanceof Map) return key.get(intKey);
1335
+ return key[intKey];
1336
+ }
1337
+ function toBase64Url(value) {
1338
+ const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
1339
+ let binary = "";
1340
+ for (const byte of bytes) binary += String.fromCharCode(byte);
1341
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
1342
+ }
1343
+ /**
1344
+ * Converts a COSE public key (RFC 9052 / IANA COSE Key registry) to JWK format.
1345
+ *
1346
+ * Supports EC2 keys (P-256, P-384, P-521) and OKP keys (Ed25519, Ed448, X25519, X448).
1347
+ * Input may be a `Map\<number, unknown\>` (from CBOR decoders like cbor-x) or a plain
1348
+ * object with integer keys.
1349
+ *
1350
+ * @param coseKey - COSE key structure as decoded from a C2PA `c2pa.session-keys` assertion
1351
+ * @returns JWK representation of the public key
1352
+ * @throws If the key type or curve is not supported
1353
+ *
1354
+ * @example
1355
+ * {@includeCode ../../test/cose/convertCoseKeyToJwk.test.ts#example}
1356
+ *
1357
+ * @public
1358
+ */
1359
+ function convertCoseKeyToJwk(coseKey) {
1360
+ const key = coseKey;
1361
+ const kty = coseGet(key, 1);
1362
+ const crv = coseGet(key, -1);
1363
+ const x = coseGet(key, -2);
1364
+ if (kty === EC2_KEY_TYPE) {
1365
+ const curveName = EC_CURVE_NAMES[crv];
1366
+ if (!curveName) throw new Error(`Unsupported EC curve: ${crv}`);
1367
+ const y = coseGet(key, -3);
1368
+ if (!(x instanceof Uint8Array)) throw new Error("EC2 key missing or invalid x coordinate");
1369
+ if (!(y instanceof Uint8Array)) throw new Error("EC2 key missing or invalid y coordinate");
1370
+ return {
1371
+ kty: "EC",
1372
+ crv: curveName,
1373
+ x: toBase64Url(x),
1374
+ y: toBase64Url(y)
1375
+ };
1376
+ }
1377
+ if (kty === OKP_KEY_TYPE) {
1378
+ const curveName = OKP_CURVE_NAMES[crv];
1379
+ if (!curveName) throw new Error(`Unsupported OKP curve: ${crv}`);
1380
+ return {
1381
+ kty: "OKP",
1382
+ crv: curveName,
1383
+ x: toBase64Url(x)
1384
+ };
1385
+ }
1386
+ throw new Error(`Unsupported COSE key type: ${kty}`);
1387
+ }
1388
+
1389
+ //#endregion
1390
+ //#region src/cose/resolveImportAlgorithm.ts
1391
+ const KEY_TYPE_OKP = "OKP";
1392
+ function resolveImportAlgorithm(jwk) {
1393
+ return jwk.kty === KEY_TYPE_OKP ? { name: jwk.crv } : {
1394
+ name: ECDSA_ALGORITHM,
1395
+ namedCurve: jwk.crv
1396
+ };
1397
+ }
1398
+
1399
+ //#endregion
1400
+ //#region src/cose/verifySignerBinding.ts
1401
+ /**
1402
+ * Verifies a C2PA signer binding — a `COSE_Sign1` structure that proves
1403
+ * a session key was authorized by the content signer (C2PA spec §18.25.2).
1404
+ *
1405
+ * The `Sig_Structure` payload is `CBOR(bstr(signerCertBytes))` per the C2PA spec.
1406
+ * DER-encoded ECDSA signatures are automatically normalized to raw format.
1407
+ *
1408
+ * @param signerBindingBytes - Raw `COSE_Sign1` bytes of the signer binding
1409
+ * @param sessionCoseKey - COSE public key from the `c2pa.session-keys` assertion
1410
+ * @param signerCertBytes - DER-encoded end-entity certificate (from `x5chain`)
1411
+ * @returns `true` if the signer binding signature is valid
1412
+ * @throws If the COSE key type is not supported or decoding fails
1413
+ *
1414
+ * @example
1415
+ * {@includeCode ../../test/cose/verifySignerBinding.test.ts#example}
1416
+ *
1417
+ * @public
1418
+ */
1419
+ async function verifySignerBinding(signerBindingBytes, sessionCoseKey, signerCertBytes) {
1420
+ const coseSign1 = decodeCoseSign1(signerBindingBytes);
1421
+ const jwk = convertCoseKeyToJwk(sessionCoseKey);
1422
+ const publicKey = await crypto.subtle.importKey("jwk", jwk, resolveImportAlgorithm(jwk), false, ["verify"]);
1423
+ return verifyCoseSign1(coseSign1, encode(signerCertBytes), publicKey);
1424
+ }
1425
+
1426
+ //#endregion
1427
+ //#region src/init/validateC2paInitSegment.ts
1428
+ const BMFF_HASH_ASSERTION_LABEL$1 = "c2pa.hash.bmff.v3";
1429
+ const SESSION_KEYS_ASSERTION_LABEL = "c2pa.session-keys";
1430
+ const COSE_KEY_ID_LABEL = 2;
1431
+ const CBOR_TAGGED_KEY = "@@TAGGED@@";
1432
+ function extractCborTaggedValue(value) {
1433
+ if (typeof value !== "object" || value === null) return null;
1434
+ const obj = value;
1435
+ if (typeof obj["tag"] === "number" && "value" in obj) return obj["value"];
1436
+ const tagged = obj[CBOR_TAGGED_KEY];
1437
+ if (Array.isArray(tagged) && tagged.length === 2) return tagged[1];
1438
+ return null;
1439
+ }
1440
+ function normalizeToUint8Array(value) {
1441
+ if (value instanceof Uint8Array) return value;
1442
+ if (Array.isArray(value)) return new Uint8Array(value);
1443
+ if (extractCborTaggedValue(value) !== null) return encode(value);
1444
+ throw new Error("Cannot convert value to Uint8Array");
1445
+ }
1446
+ function ensureDecodedCbor(value) {
1447
+ if (value instanceof Uint8Array) return decode(value);
1448
+ if (Array.isArray(value) && value.length > 0 && typeof value[0] === "number") return decode(new Uint8Array(value));
1449
+ return value;
1450
+ }
1451
+ function parseCreatedAt(value) {
1452
+ const resolved = extractCborTaggedValue(value) ?? value;
1453
+ if (typeof resolved === "string") return resolved;
1454
+ if (resolved instanceof Date) return resolved.toISOString();
1455
+ return null;
1456
+ }
1457
+ function extractKidHex(keyData, coseKey) {
1458
+ const kid = keyData["kid"];
1459
+ if (kid instanceof Uint8Array) return bytesToHex(kid);
1460
+ if (typeof kid === "string") return kid;
1461
+ if (Array.isArray(kid) && kid.length > 0) return bytesToHex(new Uint8Array(kid));
1462
+ const coseKeyLike = coseKey;
1463
+ const coseKid = coseKeyLike instanceof Map ? coseKeyLike.get(COSE_KEY_ID_LABEL) : coseKeyLike[COSE_KEY_ID_LABEL];
1464
+ if (coseKid instanceof Uint8Array) return bytesToHex(coseKid);
1465
+ if (Array.isArray(coseKid) && coseKid.length > 0) return bytesToHex(new Uint8Array(coseKid));
1466
+ return null;
1467
+ }
1468
+ function extractKeyArray(data) {
1469
+ if (Array.isArray(data)) return data;
1470
+ if (typeof data === "object" && data !== null) {
1471
+ const obj = data;
1472
+ const keys = obj["keys"] ?? obj["sessionKeys"];
1473
+ if (Array.isArray(keys)) return keys;
1474
+ if (typeof keys === "object" && keys !== null) {
1475
+ const nested = keys["keys"];
1476
+ if (Array.isArray(nested)) return nested;
1477
+ }
1478
+ }
1479
+ return [];
1480
+ }
1481
+ async function validateBmffHashAssertion(bytes, assertion) {
1482
+ if (!assertion) return true;
1483
+ const data = assertion.data;
1484
+ const rawHash = data["hash"] ?? data["value"];
1485
+ if (!rawHash) return true;
1486
+ const expectedHash = rawHash instanceof Uint8Array ? rawHash : new Uint8Array(rawHash);
1487
+ const alg = normalizeAlgorithmName(data["alg"]);
1488
+ return validateBmffHash(bytes, expectedHash, {
1489
+ exclusions: data["exclusions"] ?? [],
1490
+ alg
1491
+ });
1492
+ }
1493
+ function extractSessionKeyFields(entry) {
1494
+ const keyData = entry;
1495
+ const minSequenceNumber = keyData["minSequenceNumber"];
1496
+ const validityPeriod = keyData["validityPeriod"];
1497
+ const createdAt = parseCreatedAt(keyData["createdAt"]);
1498
+ if (minSequenceNumber == null || validityPeriod == null || !createdAt) return null;
1499
+ if (/* @__PURE__ */ new Date() < new Date(createdAt) || isKeyExpired(createdAt, Number(validityPeriod))) return null;
1500
+ const coseKey = ensureDecodedCbor(keyData["key"]);
1501
+ const kid = extractKidHex(keyData, coseKey);
1502
+ if (!kid) return null;
1503
+ const signerBindingRaw = keyData["signerBinding"];
1504
+ if (!signerBindingRaw) return null;
1505
+ try {
1506
+ return {
1507
+ minSequenceNumber: Number(minSequenceNumber),
1508
+ validityPeriod: Number(validityPeriod),
1509
+ createdAt,
1510
+ kid,
1511
+ coseKey,
1512
+ signerBindingBytes: normalizeToUint8Array(signerBindingRaw)
1513
+ };
1514
+ } catch {
1515
+ return null;
1516
+ }
1517
+ }
1518
+ async function verifyAndConvertKey(fields, certificate) {
1519
+ if (!await verifySignerBinding(fields.signerBindingBytes, fields.coseKey, certificate)) return null;
1520
+ try {
1521
+ const jwk = convertCoseKeyToJwk(fields.coseKey);
1522
+ return {
1523
+ kid: fields.kid,
1524
+ jwk,
1525
+ minSequenceNumber: fields.minSequenceNumber,
1526
+ validityPeriod: fields.validityPeriod,
1527
+ createdAt: fields.createdAt
1528
+ };
1529
+ } catch {
1530
+ return null;
1531
+ }
1532
+ }
1533
+ async function validateSingleSessionKey(entry, certificate) {
1534
+ const fields = extractSessionKeyFields(entry);
1535
+ if (!fields) return null;
1536
+ return verifyAndConvertKey(fields, certificate);
1537
+ }
1538
+ async function validateSessionKeys(assertion, certificate) {
1539
+ const keyEntries = extractKeyArray(ensureDecodedCbor(assertion.data));
1540
+ return (await Promise.all(keyEntries.map((entry) => validateSingleSessionKey(entry, certificate)))).filter((key) => key !== null);
1541
+ }
1542
+ /**
1543
+ * Validates a C2PA init segment: parses the manifest, extracts and verifies
1544
+ * the certificate, validates the BMFF hard binding hash, verifies all
1545
+ * session keys from the `c2pa.session-keys` assertion, and performs
1546
+ * manifest integrity checks (assertion hashes, missing assertions,
1547
+ * action ingredients, and claim signature verification).
1548
+ *
1549
+ * Only session keys with a valid signer binding and an unexpired validity period
1550
+ * are included in the result.
1551
+ *
1552
+ * @param bytes - Raw init segment bytes
1553
+ * @returns Structured validation result (with `INIT_INVALID` error code if `mdat` box is present)
1554
+ * @throws If no C2PA UUID box is found
1555
+ *
1556
+ * @example
1557
+ * {@includeCode ../../test/init/validateC2paInitSegment.test.ts#example}
1558
+ *
1559
+ * @public
1560
+ */
1561
+ async function validateC2paInitSegment(bytes) {
1562
+ const boxes = readIsoBoxes(bytes);
1563
+ if (findIsoBox(boxes, (box) => box.type === "mdat")) return {
1564
+ manifest: null,
1565
+ certificate: null,
1566
+ manifestId: null,
1567
+ sessionKeys: [],
1568
+ isValid: false,
1569
+ errorCodes: [LiveVideoStatusCode.INIT_INVALID]
1570
+ };
1571
+ const internalData = readC2paManifest(bytes, boxes);
1572
+ const { manifest } = internalData;
1573
+ const certificate = internalData.signatureBytes ? extractCertificateFromSignatureBytes(internalData.signatureBytes) : null;
1574
+ const bmffHashValid = await validateBmffHashAssertion(bytes, manifest.assertions.find((a) => a.label === BMFF_HASH_ASSERTION_LABEL$1) ?? null);
1575
+ const sessionKeysAssertion = manifest.assertions.find((a) => a.label === SESSION_KEYS_ASSERTION_LABEL);
1576
+ const sessionKeys = sessionKeysAssertion && certificate ? await validateSessionKeys(sessionKeysAssertion, certificate) : [];
1577
+ const integrityCodes = await validateManifestIntegrity(internalData, certificate);
1578
+ const codes = /* @__PURE__ */ new Set();
1579
+ if (!bmffHashValid) codes.add(LiveVideoStatusCode.INIT_INVALID);
1580
+ if (sessionKeys.length === 0) codes.add(LiveVideoStatusCode.SESSIONKEY_INVALID);
1581
+ for (const code of integrityCodes) codes.add(code);
1582
+ const errorCodes = [...codes];
1583
+ return {
1584
+ manifest,
1585
+ certificate,
1586
+ manifestId: manifest.instanceId,
1587
+ sessionKeys,
1588
+ isValid: errorCodes.length === 0,
1589
+ errorCodes
1590
+ };
1591
+ }
1592
+
1593
+ //#endregion
1594
+ //#region src/emsg/extractVsiEmsgBox.ts
1595
+ const EMSG_BOX_TYPE = "emsg";
1596
+ const VSI_SCHEME_URI = "urn:c2pa:verifiable-segment-info";
1597
+ const EMSG_READER_CONFIG = { readers: { emsg: readEmsg } };
1598
+ /**
1599
+ * Finds and parses the first C2PA Verifiable Segment Info (VSI) EMSG box
1600
+ * in a DASH segment, identified by scheme URI `urn:c2pa:verifiable-segment-info`.
1601
+ *
1602
+ * @param segmentBytes - Raw DASH segment bytes
1603
+ * @returns The parsed VSI EMSG box, or `null` if not found
1604
+ *
1605
+ * @example
1606
+ * {@includeCode ../../test/emsg/extractVsiEmsgBox.test.ts#example}
1607
+ *
1608
+ * @internal
1609
+ */
1610
+ function extractVsiEmsgBox(segmentBytes) {
1611
+ for (const box of readIsoBoxes(segmentBytes, EMSG_READER_CONFIG)) {
1612
+ if (box.type !== EMSG_BOX_TYPE) continue;
1613
+ const emsg = box;
1614
+ if (emsg.schemeIdUri === VSI_SCHEME_URI) return emsg;
1615
+ }
1616
+ return null;
1617
+ }
1618
+
1619
+ //#endregion
1620
+ //#region src/vsi/decodeVsiMap.ts
1621
+ /**
1622
+ * Decodes a C2PA Verifiable Segment Info (VSI) CBOR map from raw bytes.
1623
+ *
1624
+ * Normalizes hash algorithm names to WebCrypto format (e.g. `sha256` → `SHA-256`).
1625
+ *
1626
+ * @param vsiCborBytes - Raw CBOR-encoded VSI map bytes from the EMSG `messageData`
1627
+ * @returns The decoded VSI map
1628
+ * @throws If the bytes are not a valid VSI CBOR map
1629
+ *
1630
+ * @example
1631
+ * {@includeCode ../../test/vsi/decodeVsiMap.test.ts#example}
1632
+ *
1633
+ * @internal
1634
+ */
1635
+ function decodeVsiMap(vsiCborBytes) {
1636
+ const raw = decode(vsiCborBytes);
1637
+ if (typeof raw !== "object" || raw === null) throw new Error("VSI map must be a CBOR map");
1638
+ const sequenceNumber = raw["sequenceNumber"];
1639
+ if (typeof sequenceNumber !== "number") throw new Error("VSI map missing or invalid sequenceNumber");
1640
+ const bmffHashRaw = raw["bmffHash"];
1641
+ if (!bmffHashRaw || typeof bmffHashRaw !== "object") throw new Error("VSI map missing bmffHash");
1642
+ const hash = bmffHashRaw["hash"];
1643
+ if (!(hash instanceof Uint8Array)) throw new Error("VSI map bmffHash.hash must be a Uint8Array");
1644
+ const exclusions = bmffHashRaw["exclusions"];
1645
+ if (exclusions !== void 0 && !Array.isArray(exclusions)) throw new Error("VSI map bmffHash.exclusions must be an array");
1646
+ const manifestId = raw["manifestId"];
1647
+ if (typeof manifestId !== "string") throw new Error("VSI map missing or invalid manifestId");
1648
+ return {
1649
+ sequenceNumber,
1650
+ bmffHash: {
1651
+ hash,
1652
+ alg: normalizeAlgorithmName(bmffHashRaw["alg"]),
1653
+ exclusions: exclusions ?? []
1654
+ },
1655
+ manifestId
1656
+ };
1657
+ }
1658
+
1659
+ //#endregion
1660
+ //#region src/vsi/createSequenceState.ts
1661
+ /**
1662
+ * Creates the initial (empty) sequence state for a new stream.
1663
+ *
1664
+ * @returns A {@link SequenceState} with no history
1665
+ *
1666
+ * @example
1667
+ * {@includeCode ../../test/vsi/validateSequenceNumber.test.ts#example}
1668
+ *
1669
+ * @internal
1670
+ */
1671
+ function createSequenceState() {
1672
+ return {
1673
+ lastSequenceNumber: null,
1674
+ seenSequences: /* @__PURE__ */ new Set()
1675
+ };
1676
+ }
1677
+
1678
+ //#endregion
1679
+ //#region src/vsi/SequenceState.ts
1680
+ /**
1681
+ * Reason codes for sequence number validation outcomes.
1682
+ *
1683
+ * @see {@link SequenceValidationResult}
1684
+ *
1685
+ * @enum
1686
+ *
1687
+ * @public
1688
+ */
1689
+ const SequenceValidationReason = {
1690
+ VALID: "valid",
1691
+ DUPLICATE: "duplicate",
1692
+ GAP_DETECTED: "gap_detected",
1693
+ OUT_OF_ORDER: "out_of_order",
1694
+ SEQUENCE_NUMBER_BELOW_MINIMUM: "sequence_number_below_minimum"
1695
+ };
1696
+
1697
+ //#endregion
1698
+ //#region src/vsi/validateSequenceNumber.ts
1699
+ const SEEN_SEQUENCES_WINDOW_SIZE = 32;
1700
+ function pruneSeenSequences(seen, lastSequenceNumber) {
1701
+ if (seen.size <= SEEN_SEQUENCES_WINDOW_SIZE) return seen;
1702
+ const threshold = lastSequenceNumber - SEEN_SEQUENCES_WINDOW_SIZE;
1703
+ const pruned = /* @__PURE__ */ new Set();
1704
+ for (const seq of seen) if (seq >= threshold) pruned.add(seq);
1705
+ return pruned;
1706
+ }
1707
+ /**
1708
+ * Validates a segment's sequence number against the current stream state
1709
+ * per C2PA Live Streaming Specification §18.4.
1710
+ *
1711
+ * This function is **pure and stateless** — it returns both the validation
1712
+ * result and the next state. The caller is responsible for persisting
1713
+ * `nextState` between calls.
1714
+ *
1715
+ * Detects: `duplicate`, `out_of_order`, `gap_detected`, and
1716
+ * `sequence_number_below_minimum`.
1717
+ *
1718
+ * Internally uses a sliding window of the last 32 sequence numbers
1719
+ * to bound memory usage during long-running streams.
1720
+ *
1721
+ * @param state - Current stream state (from {@link createSequenceState} or previous `nextState`)
1722
+ * @param sequenceNumber - Sequence number from the segment's VSI map
1723
+ * @param minSequenceNumber - Minimum accepted sequence number (from the session key, default 0)
1724
+ * @returns Validation result and the updated state to persist
1725
+ *
1726
+ * @example
1727
+ * {@includeCode ../../test/vsi/validateSequenceNumber.test.ts#example}
1728
+ *
1729
+ * @internal
1730
+ */
1731
+ function validateSequenceNumber(state, sequenceNumber, minSequenceNumber) {
1732
+ if (sequenceNumber < minSequenceNumber) return {
1733
+ result: {
1734
+ isValid: false,
1735
+ reason: SequenceValidationReason.SEQUENCE_NUMBER_BELOW_MINIMUM
1736
+ },
1737
+ nextState: state
1738
+ };
1739
+ if (state.seenSequences.has(sequenceNumber)) return {
1740
+ result: {
1741
+ isValid: false,
1742
+ reason: SequenceValidationReason.DUPLICATE
1743
+ },
1744
+ nextState: state
1745
+ };
1746
+ const nextSeenSequences = new Set(state.seenSequences);
1747
+ nextSeenSequences.add(sequenceNumber);
1748
+ if (state.lastSequenceNumber !== null && sequenceNumber < state.lastSequenceNumber) return {
1749
+ result: {
1750
+ isValid: false,
1751
+ reason: SequenceValidationReason.OUT_OF_ORDER
1752
+ },
1753
+ nextState: {
1754
+ lastSequenceNumber: state.lastSequenceNumber,
1755
+ seenSequences: pruneSeenSequences(nextSeenSequences, state.lastSequenceNumber)
1756
+ }
1757
+ };
1758
+ if (state.lastSequenceNumber !== null && sequenceNumber > state.lastSequenceNumber + 1) {
1759
+ const missingFrom = state.lastSequenceNumber + 1;
1760
+ const missingTo = sequenceNumber - 1;
1761
+ return {
1762
+ result: {
1763
+ isValid: false,
1764
+ reason: SequenceValidationReason.GAP_DETECTED,
1765
+ missingFrom,
1766
+ missingTo
1767
+ },
1768
+ nextState: {
1769
+ lastSequenceNumber: sequenceNumber,
1770
+ seenSequences: pruneSeenSequences(nextSeenSequences, sequenceNumber)
1771
+ }
1772
+ };
1773
+ }
1774
+ return {
1775
+ result: {
1776
+ isValid: true,
1777
+ reason: SequenceValidationReason.VALID
1778
+ },
1779
+ nextState: {
1780
+ lastSequenceNumber: sequenceNumber,
1781
+ seenSequences: pruneSeenSequences(nextSeenSequences, sequenceNumber)
1782
+ }
1783
+ };
1784
+ }
1785
+
1786
+ //#endregion
1787
+ //#region src/segment/validateC2paSegment.ts
1788
+ function findSessionKey(sessionKeys, kidHex) {
1789
+ if (!kidHex || sessionKeys.length === 0) return null;
1790
+ return sessionKeys.find((k) => k.kid === kidHex) ?? null;
1791
+ }
1792
+ /**
1793
+ * Validates a C2PA live stream segment using the VSI/EMSG method (§19.7.3).
1794
+ *
1795
+ * Extracts the EMSG box, decodes the COSE_Sign1 and VSI map, matches the
1796
+ * session key by kid, then performs all cryptographic checks: signature
1797
+ * verification, BMFF content hash, sequence number floor, and key validity.
1798
+ *
1799
+ * Returns `null` if the segment does not contain a C2PA EMSG box.
1800
+ *
1801
+ * @param segmentBytes - Raw segment bytes
1802
+ * @param sessionKeys - Available session keys from the init segment
1803
+ * @param sequenceState - Current sequence state for this stream
1804
+ * @returns Validation result and updated sequence state, or `null` if no C2PA EMSG box
1805
+ *
1806
+ * @example
1807
+ * {@includeCode ../../test/segment/validateC2paSegment.test.ts#example}
1808
+ *
1809
+ * @public
1810
+ */
1811
+ async function validateC2paSegment(segmentBytes, sessionKeys, sequenceState = createSequenceState()) {
1812
+ const emsgBox = extractVsiEmsgBox(segmentBytes);
1813
+ if (!emsgBox) return null;
1814
+ const coseSign1 = decodeCoseSign1(emsgBox.messageData);
1815
+ const { payload } = coseSign1;
1816
+ if (!payload) throw new Error("COSE_Sign1 payload is empty — cannot decode VSI map");
1817
+ const vsi = decodeVsiMap(payload);
1818
+ const kidHex = coseSign1.kid ? bytesToHex(coseSign1.kid) : null;
1819
+ const sessionKey = findSessionKey(sessionKeys, kidHex);
1820
+ const bmffHashHex = bytesToHex(vsi.bmffHash.hash);
1821
+ const minSequenceNumber = sessionKey?.minSequenceNumber ?? 0;
1822
+ const { result: sequenceResult, nextState: nextSequenceState } = validateSequenceNumber(sequenceState, vsi.sequenceNumber, minSequenceNumber);
1823
+ const baseFields = {
1824
+ sequenceNumber: vsi.sequenceNumber,
1825
+ manifestId: vsi.manifestId,
1826
+ bmffHashHex,
1827
+ kidHex,
1828
+ sequenceResult
1829
+ };
1830
+ if (!sessionKey) return {
1831
+ result: {
1832
+ ...baseFields,
1833
+ isValid: false,
1834
+ errorCodes: [LiveVideoStatusCode.SEGMENT_INVALID]
1835
+ },
1836
+ nextSequenceState
1837
+ };
1838
+ const algorithm = resolveImportAlgorithm(sessionKey.jwk);
1839
+ const publicKey = await crypto.subtle.importKey("jwk", sessionKey.jwk, algorithm, false, ["verify"]);
1840
+ const [signatureValid, hashValid] = await Promise.all([verifyCoseSign1(coseSign1, payload, publicKey), validateBmffHash(segmentBytes, vsi.bmffHash.hash, {
1841
+ exclusions: vsi.bmffHash.exclusions,
1842
+ alg: vsi.bmffHash.alg
1843
+ })]);
1844
+ const sequenceAboveMin = vsi.sequenceNumber >= sessionKey.minSequenceNumber;
1845
+ const keyExpired = isKeyExpired(sessionKey.createdAt, sessionKey.validityPeriod);
1846
+ const codes = /* @__PURE__ */ new Set();
1847
+ if (!signatureValid || !hashValid || !sequenceAboveMin) codes.add(LiveVideoStatusCode.SEGMENT_INVALID);
1848
+ if (!sequenceResult.isValid) codes.add(LiveVideoStatusCode.ASSERTION_INVALID);
1849
+ if (keyExpired) codes.add(LiveVideoStatusCode.SESSIONKEY_INVALID);
1850
+ const errorCodes = [...codes];
1851
+ return {
1852
+ result: {
1853
+ ...baseFields,
1854
+ isValid: errorCodes.length === 0,
1855
+ errorCodes
1856
+ },
1857
+ nextSequenceState
1858
+ };
1859
+ }
1860
+
1861
+ //#endregion
1862
+ //#region src/manifestbox/validateC2paManifestBoxSegment.ts
1863
+ const LIVE_VIDEO_ASSERTION_LABEL = "c2pa.livevideo.segment";
1864
+ const BMFF_HASH_ASSERTION_LABEL = "c2pa.hash.bmff.v3";
1865
+ const MANIFEST_ID_PREFIX_PATTERN = /^(xmp:iid:|urn:uuid:)/i;
1866
+ const CONTINUITY_METHOD_MANIFEST_ID = "c2pa.manifestId";
1867
+ const SUPPORTED_CONTINUITY_METHODS = new Set([CONTINUITY_METHOD_MANIFEST_ID]);
1868
+ function normalizeManifestId(id) {
1869
+ if (!id) return null;
1870
+ return id.replace(MANIFEST_ID_PREFIX_PATTERN, "").toLowerCase();
1871
+ }
1872
+ function extractAssertionData(data) {
1873
+ if (data !== null && typeof data === "object" && !Array.isArray(data)) return data;
1874
+ return null;
1875
+ }
1876
+ function toUint8Array(value) {
1877
+ if (value instanceof Uint8Array) return value;
1878
+ if (Array.isArray(value)) return new Uint8Array(value);
1879
+ return null;
1880
+ }
1881
+ function parseLiveVideoAssertion(assertions) {
1882
+ const assertion = assertions.find((a) => a.label === LIVE_VIDEO_ASSERTION_LABEL);
1883
+ if (!assertion) return null;
1884
+ const data = extractAssertionData(assertion.data);
1885
+ const rawSeq = data?.["sequenceNumber"];
1886
+ const rawPrev = data?.["previousManifestId"];
1887
+ const rawStreamId = data?.["streamId"];
1888
+ const rawContinuity = data?.["continuityMethod"];
1889
+ return {
1890
+ sequenceNumber: typeof rawSeq === "number" ? rawSeq : null,
1891
+ previousManifestId: typeof rawPrev === "string" ? rawPrev : null,
1892
+ streamId: typeof rawStreamId === "string" ? rawStreamId : null,
1893
+ continuityMethod: typeof rawContinuity === "string" ? rawContinuity : null
1894
+ };
1895
+ }
1896
+ const EMPTY_BMFF_HASH = {
1897
+ hashBytes: null,
1898
+ hashHex: null,
1899
+ exclusions: [],
1900
+ alg: null
1901
+ };
1902
+ function parseConstraints(rawConstraints) {
1903
+ if (!Array.isArray(rawConstraints)) return [];
1904
+ const constraints = [];
1905
+ for (const c of rawConstraints) {
1906
+ if (!c || typeof c !== "object") continue;
1907
+ const record = c;
1908
+ if (typeof record["offset"] !== "number") continue;
1909
+ const value = toUint8Array(record["value"]);
1910
+ if (value) constraints.push({
1911
+ offset: record["offset"],
1912
+ value
1913
+ });
1914
+ }
1915
+ return constraints;
1916
+ }
1917
+ function parseExclusions(rawExclusions) {
1918
+ if (!Array.isArray(rawExclusions)) return [];
1919
+ const exclusions = [];
1920
+ for (const exc of rawExclusions) {
1921
+ if (!exc || typeof exc !== "object") continue;
1922
+ const record = exc;
1923
+ if (typeof record["xpath"] !== "string") continue;
1924
+ const constraints = parseConstraints(record["data"]);
1925
+ exclusions.push(constraints.length > 0 ? {
1926
+ xpath: record["xpath"],
1927
+ data: constraints
1928
+ } : { xpath: record["xpath"] });
1929
+ }
1930
+ return exclusions;
1931
+ }
1932
+ function parseBmffHashAssertion(assertions) {
1933
+ const assertion = assertions.find((a) => a.label === BMFF_HASH_ASSERTION_LABEL);
1934
+ if (!assertion) return EMPTY_BMFF_HASH;
1935
+ const data = extractAssertionData(assertion.data);
1936
+ if (!data) return EMPTY_BMFF_HASH;
1937
+ const hashBytes = toUint8Array(data["hash"] ?? data["value"]);
1938
+ return {
1939
+ hashBytes,
1940
+ hashHex: hashBytes ? bytesToHex(hashBytes) : null,
1941
+ exclusions: parseExclusions(data["exclusions"]),
1942
+ alg: typeof data["alg"] === "string" ? normalizeAlgorithmName(data["alg"]) : null
1943
+ };
1944
+ }
1945
+ function parseManifest(bytes) {
1946
+ try {
1947
+ const { manifest } = readC2paManifest(bytes);
1948
+ if (!manifest) return {
1949
+ manifest: null,
1950
+ issuer: null,
1951
+ liveVideo: null,
1952
+ bmff: EMPTY_BMFF_HASH
1953
+ };
1954
+ return {
1955
+ manifest,
1956
+ issuer: manifest.signatureInfo?.issuer ?? null,
1957
+ liveVideo: parseLiveVideoAssertion(manifest.assertions),
1958
+ bmff: parseBmffHashAssertion(manifest.assertions)
1959
+ };
1960
+ } catch {
1961
+ return {
1962
+ manifest: null,
1963
+ issuer: null,
1964
+ liveVideo: null,
1965
+ bmff: EMPTY_BMFF_HASH
1966
+ };
1967
+ }
1968
+ }
1969
+ function collectErrorCodes(hasManifest, hasLiveVideo, streamIdValid, sequenceNumberValid, bmffHashMatches, continuityMethod, previousManifestId, lastManifestId) {
1970
+ const codes = /* @__PURE__ */ new Set();
1971
+ if (!hasManifest) codes.add(LiveVideoStatusCode.MANIFEST_INVALID);
1972
+ if (!hasLiveVideo) codes.add(LiveVideoStatusCode.ASSERTION_INVALID);
1973
+ if (!streamIdValid) codes.add(LiveVideoStatusCode.ASSERTION_INVALID);
1974
+ if (!sequenceNumberValid) codes.add(LiveVideoStatusCode.ASSERTION_INVALID);
1975
+ if (!bmffHashMatches) codes.add(LiveVideoStatusCode.SEGMENT_INVALID);
1976
+ if (!continuityMethod) codes.add(LiveVideoStatusCode.CONTINUITY_METHOD_INVALID);
1977
+ else if (!SUPPORTED_CONTINUITY_METHODS.has(continuityMethod)) codes.add(LiveVideoStatusCode.CONTINUITY_METHOD_INVALID);
1978
+ else if (continuityMethod === CONTINUITY_METHOD_MANIFEST_ID) {
1979
+ if (!previousManifestId) codes.add(LiveVideoStatusCode.CONTINUITY_METHOD_INVALID);
1980
+ else if (lastManifestId && normalizeManifestId(previousManifestId) !== normalizeManifestId(lastManifestId)) codes.add(LiveVideoStatusCode.SEGMENT_INVALID);
1981
+ }
1982
+ return [...codes];
1983
+ }
1984
+ /**
1985
+ * Validates a C2PA manifest-box live stream segment.
1986
+ *
1987
+ * Parses the C2PA manifest embedded in the segment and validates per §19.7.1 and §19.7.2.
1988
+ * Recomputes the `c2pa.hash.bmff.v3` content hash from the raw segment bytes and compares
1989
+ * it against the expected hash in the manifest assertion. Checks live-video assertions
1990
+ * (sequenceNumber, streamId, continuityMethod) and manifest-ID chain continuity.
1991
+ *
1992
+ * This function is **pure** — it does not access any external state. The
1993
+ * caller is responsible for persisting `nextManifestId` and `nextState`
1994
+ * between calls.
1995
+ *
1996
+ * @param bytes - Raw segment bytes
1997
+ * @param lastManifestId - Manifest ID from the previous segment, or null for the first segment
1998
+ * @param state - Optional state from the previous segment for streamId/sequenceNumber checks
1999
+ * @returns Validation result, the manifest ID, and state to persist for the next call
2000
+ *
2001
+ * @example
2002
+ * {@includeCode ../../test/manifestbox/validateC2paManifestBoxSegment.test.ts#example}
2003
+ *
2004
+ * @public
2005
+ */
2006
+ async function validateC2paManifestBoxSegment(bytes, lastManifestId, state) {
2007
+ const { manifest, issuer, liveVideo, bmff } = parseManifest(bytes);
2008
+ const sequenceNumber = liveVideo?.sequenceNumber ?? null;
2009
+ const previousManifestId = liveVideo?.previousManifestId ?? null;
2010
+ const streamId = liveVideo?.streamId ?? null;
2011
+ const continuityMethod = liveVideo?.continuityMethod ?? null;
2012
+ const streamIdValid = state?.lastStreamId == null || streamId === state.lastStreamId;
2013
+ const sequenceNumberValid = state?.lastSequenceNumber == null || sequenceNumber !== null && sequenceNumber > state.lastSequenceNumber;
2014
+ let bmffHashMatches = true;
2015
+ if (bmff.hashBytes !== null) bmffHashMatches = await validateBmffHash(bytes, bmff.hashBytes, {
2016
+ exclusions: bmff.exclusions,
2017
+ alg: bmff.alg ?? void 0
2018
+ });
2019
+ const errorCodes = [...collectErrorCodes(manifest !== null, liveVideo !== null, streamIdValid, sequenceNumberValid, bmffHashMatches, continuityMethod, previousManifestId, lastManifestId)];
2020
+ const currentManifestId = manifest?.instanceId ?? null;
2021
+ return {
2022
+ result: {
2023
+ manifest: manifest ?? null,
2024
+ issuer,
2025
+ sequenceNumber,
2026
+ previousManifestId,
2027
+ streamId,
2028
+ continuityMethod,
2029
+ bmffHashHex: bmff.hashHex,
2030
+ isValid: errorCodes.length === 0,
2031
+ errorCodes
2032
+ },
2033
+ nextManifestId: currentManifestId ?? lastManifestId,
2034
+ nextState: {
2035
+ lastStreamId: streamId ?? state?.lastStreamId,
2036
+ lastSequenceNumber: sequenceNumber ?? state?.lastSequenceNumber
2037
+ }
2038
+ };
2039
+ }
2040
+
2041
+ //#endregion
2042
+ export { C2paStatusCode, LiveVideoStatusCode, SequenceValidationReason, validateC2paInitSegment, validateC2paManifestBoxSegment, validateC2paSegment };
2043
+ //# sourceMappingURL=index.js.map