@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/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 COSE_SIGN1_TAG_SINGLE_BYTE = 210;
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
- if (Array.isArray(value)) return new Uint8Array(value);
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 `COSE_Sign1` structure from raw bytes (RFC 9052).
350
+ * Decodes a `COSE_Sign1_Tagged` structure from raw bytes (RFC 9052 section 4.2).
50
351
  *
51
- * Handles CBOR tag 18 in both single-byte (0xD2) and two-byte (0xD8 0x12) form.
352
+ * The bytes must carry CBOR tag 18 in any encoding length (C2PA section 14).
52
353
  *
53
- * @param coseBytes - Raw COSE_Sign1 bytes, optionally prefixed with CBOR tag 18
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 valid COSE_Sign1 structure
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 coseArray = decode(stripCoseTag(coseBytes));
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 = decode(protectedBytes);
372
+ if (protectedBytes.length > 0) protectedHeader = decodeCbor(protectedBytes);
70
373
  const unprotectedHeader = unprotectedRaw ?? {};
71
- const kidRaw = coseGet$1(protectedHeader, COSE_KEY_KID) ?? coseGet$1(unprotectedHeader, COSE_KEY_KID) ?? null;
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 = coseGet$1(protectedHeader, COSE_KEY_ALG) ?? coseGet$1(unprotectedHeader, COSE_KEY_ALG) ?? null;
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 < JUMD_UUID_SIZE + 1) return null;
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
- if (value instanceof Uint8Array) return value;
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 === ASN1_TAG_UTC_TIME) {
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 === 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`;
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 !== ASN1_TAG_OBJECT_IDENTIFIER) return null;
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 !== ASN1_TAG_SET) break;
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 !== ASN1_TAG_SEQUENCE) break;
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 !== ASN1_TAG_SEQUENCE) return null;
704
+ if (!cert || cert.tag !== 48) return null;
418
705
  const tbs = readElement(cert.value, 0);
419
- if (!tbs || tbs.tag !== ASN1_TAG_SEQUENCE) return null;
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 === ASN1_TAG_CONTEXT_0) {
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 !== ASN1_TAG_INTEGER) return null;
715
+ if (el.tag !== 2) return null;
429
716
  offset += el.totalSize;
430
717
  el = readElement(tbs.value, offset);
431
- if (!el || el.tag !== ASN1_TAG_SEQUENCE) return null;
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 !== ASN1_TAG_SEQUENCE) return null;
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 === ASN1_TAG_SEQUENCE) {
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
- if (contentBox) if (contentBox.type === "cbor") try {
491
- data = decode(contentBox.data);
492
- } catch {
493
- data = contentBox.data;
494
- }
495
- else if (contentBox.type === "json") try {
496
- data = JSON.parse(TEXT_DECODER.decode(contentBox.data));
497
- } catch {
498
- data = contentBox.data;
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 || !hash) continue;
863
+ if (!url || hash == null) continue;
554
864
  refs.push({
555
865
  url,
556
- hash: hash instanceof Uint8Array ? hash : new Uint8Array(hash),
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
- try {
607
- claimData = decode(contentBox.data);
608
- } catch {}
921
+ const claim = readClaim(contentBox.data);
922
+ claimData = claim.data;
923
+ claimCode = claim.code;
609
924
  }
610
- } else if (label === "c2pa.assertions") internalAssertions = parseAssertionsInternal(inner);
611
- else if (label === "c2pa.signature") {
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/C2paStatusCode.ts
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
- * Standard C2PA validation status codes for manifest integrity checks,
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
- * @enum
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
- * @public
1097
+ * @internal
759
1098
  */
760
- const C2paStatusCode = {
761
- ASSERTION_HASHEDURI_MISMATCH: "assertion.hashedURI.mismatch",
762
- ASSERTION_MISSING: "assertion.missing",
763
- ASSERTION_ACTION_INGREDIENT_MISMATCH: "assertion.action.ingredientMismatch",
764
- CLAIM_SIGNATURE_MISMATCH: "claimSignature.mismatch",
765
- CLAIM_SIGNATURE_MISSING: "claimSignature.missing",
766
- CLAIM_MISSING: "claim.missing",
767
- ASSERTION_BMFFHASH_MALFORMED: "assertion.bmffHash.malformed",
768
- ASSERTION_BMFFHASH_MISMATCH: "assertion.bmffHash.mismatch"
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
- * - Checks that the assertion exists in the assertion store (`assertion.missing`)
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 Array of C2PA status codes for any failures found
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
- const assertionsByLabel = /* @__PURE__ */ new Map();
863
- for (const assertion of assertions) assertionsByLabel.set(assertion.label, assertion);
864
- const codes = [];
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 assertion = assertionsByLabel.get(label);
868
- if (!assertion) return C2paStatusCode.ASSERTION_MISSING;
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 C2paStatusCode.ASSERTION_HASHEDURI_MISMATCH;
871
- return null;
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
- for (const result of results) if (result) codes.push(result);
875
- return codes;
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
- [CURVE_P256]: 32,
1011
- [CURVE_P384]: 48,
1012
- [CURVE_P521]: 66
1396
+ "P-256": 32,
1397
+ "P-384": 48,
1398
+ "P-521": 66
1013
1399
  };
1014
1400
  const RSA_PSS_SALT_LENGTH = {
1015
- [HASH_SHA256]: 32,
1016
- [HASH_SHA384]: 48,
1017
- [HASH_SHA512]: 64
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 === ED25519_ALGORITHM) return { name: ED25519_ALGORITHM };
1072
- if (name === ECDSA_ALGORITHM) {
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 === CURVE_P384 ? HASH_SHA384 : curve === CURVE_P521 ? HASH_SHA512 : HASH_SHA256 }
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 !== ECDSA_ALGORITHM) return signature;
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 !== ASN1_TAG_SEQUENCE) return null;
1526
+ if (!cert || cert.tag !== 48) return null;
1139
1527
  const tbs = readElement(cert.value, 0);
1140
- if (!tbs || tbs.tag !== ASN1_TAG_SEQUENCE) return null;
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 === ASN1_TAG_CONTEXT_0) {
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 !== ASN1_TAG_INTEGER) return null;
1537
+ if (el.tag !== 2) return null;
1150
1538
  offset += el.totalSize;
1151
1539
  el = readElement(tbs.value, offset);
1152
- if (!el || el.tag !== ASN1_TAG_SEQUENCE) return null;
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 !== ASN1_TAG_SEQUENCE) return null;
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 !== ASN1_TAG_SEQUENCE) return null;
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 !== ASN1_TAG_SEQUENCE) return null;
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 !== ASN1_TAG_SEQUENCE) return spkiBytes;
1612
+ if (!outer || outer.tag !== 48) return spkiBytes;
1226
1613
  const algId = readElement(outer.value, 0);
1227
- if (!algId || algId.tag !== ASN1_TAG_SEQUENCE) return spkiBytes;
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++] = ASN1_TAG_SEQUENCE;
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 four validation checks in parallel where possible:
1317
- * 1. Assertion hash verification (§15.10.3.1)
1318
- * 2. Missing assertion detection (§15.10.3.1)
1319
- * 3. Action ingredient validation (§18.15.4.7)
1320
- * 4. Claim signature verification (§15.7)
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, and the certificate of the claim signature
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 [assertionHashCodes, signature] = await Promise.all([validateAssertionHashes(internal.claimAssertionRefs, internal.assertions), validateClaimSignature(internal)]);
1332
- const codes = [...assertionHashCodes, ...validateActionIngredients(internal.assertions)];
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 coseGet(key, intKey) {
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
- * Input may be a `Map\<number, unknown\>` (from CBOR decoders like cbor-x) or a plain
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 = coseGet(key, 1);
1384
- const crv = coseGet(key, -1);
1385
- const x = coseGet(key, -2);
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 = coseGet(key, -3);
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 — a `COSE_Sign1` structure that proves
1425
- * a session key was authorized by the content signer (C2PA spec §18.25.2).
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 `CBOR(bstr(signerCertBytes))` per the C2PA spec.
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 signature is valid
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
- return verifyCoseSign1(coseSign1, encode(signerCertBytes), publicKey);
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 (Array.isArray(value)) return new Uint8Array(value);
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
- if (value instanceof Uint8Array) return decode(value);
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
- const resolved = extractCborTaggedValue(value) ?? value;
1586
- if (typeof resolved === "string") return resolved;
1587
- if (resolved instanceof Date) return resolved.toISOString();
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(keyData, coseKey) {
1591
- const kid = keyData["kid"];
1592
- if (kid instanceof Uint8Array) return bytesToHex(kid);
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
- const expectedHash = rawHash instanceof Uint8Array ? rawHash : new Uint8Array(rawHash);
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: data["exclusions"] ?? [],
1977
+ exclusions,
1623
1978
  alg,
1624
1979
  offsetPrefixSize: 8
1625
- }), expectedHash);
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 == null || validityPeriod == null || !createdAt) return null;
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(keyData, coseKey);
1989
+ const kid = extractKidHex(coseKey);
1636
1990
  if (!kid) return null;
1637
1991
  const signerBindingRaw = keyData["signerBinding"];
1638
1992
  if (!signerBindingRaw) return null;
1639
- try {
1640
- return {
1641
- minSequenceNumber: Number(minSequenceNumber),
1642
- validityPeriod: Number(validityPeriod),
1643
- createdAt,
1644
- kid,
1645
- coseKey,
1646
- signerBindingBytes: normalizeToUint8Array(signerBindingRaw)
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 jwk = convertCoseKeyToJwk(fields.coseKey);
1656
- return {
1657
- kid: fields.kid,
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
- async function validateSingleSessionKey(entry, certificate) {
1668
- const fields = extractSessionKeyFields(entry);
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(ensureDecodedCbor(assertion.data));
1674
- return (await Promise.all(keyEntries.map((entry) => validateSingleSessionKey(entry, certificate)))).filter((key) => key !== null);
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, missing assertions,
1681
- * action ingredients, and claim signature verification).
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 = manifest.assertions.find((a) => a.label === BMFF_HASH_ASSERTION_LABEL$1) ?? null;
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 (sessionKeys.length === 0 && merkleMaps === null) codes.add(LiveVideoStatusCode.SESSIONKEY_INVALID);
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: 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 = decode(vsiCborBytes);
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 sequenceNumber = raw["sequenceNumber"];
1776
- if (typeof sequenceNumber !== "number") throw new Error("VSI map missing or invalid sequenceNumber");
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 (exclusions !== void 0 && !Array.isArray(exclusions)) throw new Error("VSI map bmffHash.exclusions must be an array");
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: 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 updated sequence state, or `null` if no C2PA EMSG box
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 (!signatureValid || !hashValid || !sequenceAboveMin) codes.add(LiveVideoStatusCode.SEGMENT_INVALID);
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: typeof rawSeq === "number" ? rawSeq : null,
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 hashBytes = toUint8Array(data["hash"] ?? data["value"]);
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: hashBytes ? bytesToHex(hashBytes) : null,
2073
- exclusions: parseExclusions(data["exclusions"]),
2074
- alg: typeof data["alg"] === "string" ? normalizeAlgorithmName(data["alg"]) : null
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(parsed, lastManifestId, options) {
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 previous segment, or null for the first segment
2143
- * @param state - Optional state from the previous segment for streamId/sequenceNumber checks
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, liveVideo, bmff, internalData } = parseManifest(bytes);
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
- manifest,
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
- decodeMultiple(payload, (value) => {
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 uniqueId = asInteger(readMapField(decoded, "uniqueId"));
2240
- const localId = asInteger(readMapField(decoded, "localId"));
2241
- const location = asInteger(readMapField(decoded, "location"));
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 = readMapField(decoded, "hashes");
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