@svta/cml-c2pa 1.3.0 → 1.4.0

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