@svta/cml-c2pa 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -6
- package/dist/index.d.ts +121 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +404 -100
- package/dist/index.js.map +1 -1
- package/dist/tsdoc-metadata.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decode } from "cbor-x/decode";
|
|
1
|
+
import { decode, decodeMultiple } from "cbor-x/decode";
|
|
2
2
|
import { encode } from "cbor-x/encode";
|
|
3
3
|
import { findIsoBox, readEmsg, readIsoBoxes } from "@svta/cml-iso-bmff";
|
|
4
4
|
|
|
@@ -19,6 +19,7 @@ const LiveVideoStatusCode = {
|
|
|
19
19
|
SEGMENT_INVALID: "livevideo.segment.invalid",
|
|
20
20
|
ASSERTION_INVALID: "livevideo.assertion.invalid",
|
|
21
21
|
CONTINUITY_METHOD_INVALID: "livevideo.continuityMethod.invalid",
|
|
22
|
+
CONTINUITY_METHOD_UNSUPPORTED: "livevideo.continuityMethod.unsupported",
|
|
22
23
|
SESSIONKEY_INVALID: "livevideo.sessionkey.invalid"
|
|
23
24
|
};
|
|
24
25
|
|
|
@@ -109,7 +110,7 @@ function parseJumbfBoxes(bytes) {
|
|
|
109
110
|
|
|
110
111
|
//#endregion
|
|
111
112
|
//#region src/jumbf/parseJumbfLabel.ts
|
|
112
|
-
const TEXT_DECODER$
|
|
113
|
+
const TEXT_DECODER$3 = /* @__PURE__ */ new TextDecoder();
|
|
113
114
|
const JUMD_UUID_SIZE = 16;
|
|
114
115
|
const JUMD_TOGGLES_OFFSET = 16;
|
|
115
116
|
const JUMD_LABEL_START = 17;
|
|
@@ -135,7 +136,7 @@ function parseJumbfLabel(jumdData) {
|
|
|
135
136
|
let end = JUMD_LABEL_START;
|
|
136
137
|
while (end < jumdData.length && jumdData[end] !== 0) end++;
|
|
137
138
|
if (end >= jumdData.length) return null;
|
|
138
|
-
return TEXT_DECODER$
|
|
139
|
+
return TEXT_DECODER$3.decode(jumdData.subarray(JUMD_LABEL_START, end));
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
//#endregion
|
|
@@ -152,7 +153,7 @@ const SHA_ALGORITHM_PATTERN = /^sha(\d+)$/i;
|
|
|
152
153
|
function normalizeAlgorithmName(rawAlg) {
|
|
153
154
|
return (rawAlg ?? "SHA-256").replace(SHA_ALGORITHM_PATTERN, "SHA-$1");
|
|
154
155
|
}
|
|
155
|
-
const HEX_TABLE = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
|
|
156
|
+
const HEX_TABLE = /* @__PURE__ */ Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
|
|
156
157
|
/**
|
|
157
158
|
* Converts a Uint8Array to a lowercase hex string.
|
|
158
159
|
*
|
|
@@ -208,6 +209,7 @@ const C2PA_MANIFEST_UUID = [
|
|
|
208
209
|
16,
|
|
209
210
|
234
|
|
210
211
|
];
|
|
212
|
+
/** @internal */
|
|
211
213
|
const JUMBF_UUID = [
|
|
212
214
|
216,
|
|
213
215
|
254,
|
|
@@ -226,9 +228,20 @@ const JUMBF_UUID = [
|
|
|
226
228
|
196,
|
|
227
229
|
129
|
|
228
230
|
];
|
|
231
|
+
/** @internal */
|
|
229
232
|
function matchesUuid(usertype, expected) {
|
|
230
233
|
return usertype.length === expected.length && expected.every((b, i) => b === usertype[i]);
|
|
231
234
|
}
|
|
235
|
+
/** @internal */
|
|
236
|
+
function toUint8Array(value) {
|
|
237
|
+
if (value instanceof Uint8Array) return value;
|
|
238
|
+
if (Array.isArray(value)) return new Uint8Array(value);
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
/** @internal */
|
|
242
|
+
function asInteger(value) {
|
|
243
|
+
return typeof value === "number" && Number.isInteger(value) ? value : null;
|
|
244
|
+
}
|
|
232
245
|
function isC2paUuid(usertype) {
|
|
233
246
|
return matchesUuid(usertype, C2PA_MANIFEST_UUID) || matchesUuid(usertype, JUMBF_UUID);
|
|
234
247
|
}
|
|
@@ -245,6 +258,18 @@ function findC2paUuidBox(boxes) {
|
|
|
245
258
|
}
|
|
246
259
|
const FULLBOX_HEADER_SIZE = 4;
|
|
247
260
|
const AUX_UUID_OFFSET_SIZE = 8;
|
|
261
|
+
const TEXT_DECODER$2 = new TextDecoder();
|
|
262
|
+
/** @internal */
|
|
263
|
+
function readUuidBoxPurpose(payload) {
|
|
264
|
+
if (payload.length < FULLBOX_HEADER_SIZE) return null;
|
|
265
|
+
let offset = FULLBOX_HEADER_SIZE;
|
|
266
|
+
while (offset < payload.length && payload[offset] !== 0) offset++;
|
|
267
|
+
if (offset >= payload.length) return null;
|
|
268
|
+
return {
|
|
269
|
+
purpose: TEXT_DECODER$2.decode(payload.subarray(FULLBOX_HEADER_SIZE, offset)),
|
|
270
|
+
rest: payload.subarray(offset + 1)
|
|
271
|
+
};
|
|
272
|
+
}
|
|
248
273
|
/**
|
|
249
274
|
* Strips the JUMBF UUID box prefix (fullbox header, purpose string, aux offset)
|
|
250
275
|
* to return only the JUMBF manifest data.
|
|
@@ -257,14 +282,9 @@ const AUX_UUID_OFFSET_SIZE = 8;
|
|
|
257
282
|
* @internal
|
|
258
283
|
*/
|
|
259
284
|
function stripJumbfUuidPrefix(payload) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (offset >= payload.length) return null;
|
|
264
|
-
offset++;
|
|
265
|
-
offset += AUX_UUID_OFFSET_SIZE;
|
|
266
|
-
if (offset > payload.length) return null;
|
|
267
|
-
return payload.subarray(offset);
|
|
285
|
+
const prefix = readUuidBoxPurpose(payload);
|
|
286
|
+
if (!prefix || prefix.rest.length < AUX_UUID_OFFSET_SIZE) return null;
|
|
287
|
+
return prefix.rest.subarray(AUX_UUID_OFFSET_SIZE);
|
|
268
288
|
}
|
|
269
289
|
|
|
270
290
|
//#endregion
|
|
@@ -327,7 +347,7 @@ function readElementRaw(bytes, offset) {
|
|
|
327
347
|
|
|
328
348
|
//#endregion
|
|
329
349
|
//#region src/x509/extractCertificateInfo.ts
|
|
330
|
-
const TEXT_DECODER$1 = new TextDecoder();
|
|
350
|
+
const TEXT_DECODER$1 = /* @__PURE__ */ new TextDecoder();
|
|
331
351
|
const OID_COMMON_NAME = new Uint8Array([
|
|
332
352
|
85,
|
|
333
353
|
4,
|
|
@@ -431,7 +451,7 @@ function extractCertificateInfo(certDER) {
|
|
|
431
451
|
|
|
432
452
|
//#endregion
|
|
433
453
|
//#region src/readC2paManifest.ts
|
|
434
|
-
const TEXT_DECODER = new TextDecoder();
|
|
454
|
+
const TEXT_DECODER = /* @__PURE__ */ new TextDecoder();
|
|
435
455
|
const MAX_JUMBF_NESTING_DEPTH = 20;
|
|
436
456
|
const X5CHAIN_HEADER_LABEL = 33;
|
|
437
457
|
function resolveManifestBoxes(jumbfBoxes, depth = 0) {
|
|
@@ -745,35 +765,6 @@ async function computeBmffHash(segmentBytes, options) {
|
|
|
745
765
|
return new Uint8Array(hashBuffer);
|
|
746
766
|
}
|
|
747
767
|
|
|
748
|
-
//#endregion
|
|
749
|
-
//#region src/bmff/validateBmffHash.ts
|
|
750
|
-
const OFFSET_PREFIX_SIZES_TO_TRY = [8, 0];
|
|
751
|
-
/**
|
|
752
|
-
* Validates the C2PA BMFF content hash (`c2pa.hash.bmff.v3`) for a DASH segment.
|
|
753
|
-
*
|
|
754
|
-
* Tries both offset-prefix modes — 8-byte file offset prefix and no prefix — to handle
|
|
755
|
-
* inter-signer interoperability until a standard signaling mechanism is defined in the spec.
|
|
756
|
-
*
|
|
757
|
-
* Use {@link computeBmffHash} directly when the offset prefix size is known.
|
|
758
|
-
*
|
|
759
|
-
* @param segmentBytes - Raw segment bytes to validate
|
|
760
|
-
* @param expectedHash - Expected hash bytes from the VSI CBOR map
|
|
761
|
-
* @param options - Validation options (exclusions, algorithm)
|
|
762
|
-
* @returns `true` if the computed hash matches the expected hash in either offset mode
|
|
763
|
-
*
|
|
764
|
-
* @example
|
|
765
|
-
* {@includeCode ../../test/bmff/validateBmffHash.test.ts#example}
|
|
766
|
-
*
|
|
767
|
-
* @public
|
|
768
|
-
*/
|
|
769
|
-
async function validateBmffHash(segmentBytes, expectedHash, options) {
|
|
770
|
-
for (const offsetPrefixSize of OFFSET_PREFIX_SIZES_TO_TRY) if (hashesEqual(await computeBmffHash(segmentBytes, {
|
|
771
|
-
...options,
|
|
772
|
-
offsetPrefixSize
|
|
773
|
-
}), expectedHash)) return true;
|
|
774
|
-
return false;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
768
|
//#endregion
|
|
778
769
|
//#region src/C2paStatusCode.ts
|
|
779
770
|
/**
|
|
@@ -790,7 +781,9 @@ const C2paStatusCode = {
|
|
|
790
781
|
ASSERTION_HASHEDURI_MISMATCH: "assertion.hashedURI.mismatch",
|
|
791
782
|
ASSERTION_MISSING: "assertion.missing",
|
|
792
783
|
ASSERTION_ACTION_INGREDIENT_MISMATCH: "assertion.action.ingredientMismatch",
|
|
793
|
-
CLAIM_SIGNATURE_MISMATCH: "claim.signature.mismatch"
|
|
784
|
+
CLAIM_SIGNATURE_MISMATCH: "claim.signature.mismatch",
|
|
785
|
+
ASSERTION_BMFFHASH_MALFORMED: "assertion.bmffHash.malformed",
|
|
786
|
+
ASSERTION_BMFFHASH_MISMATCH: "assertion.bmffHash.mismatch"
|
|
794
787
|
};
|
|
795
788
|
|
|
796
789
|
//#endregion
|
|
@@ -948,7 +941,7 @@ const CBOR_UINT32_LENGTH_INDICATOR = 90;
|
|
|
948
941
|
const CBOR_INLINE_MAX = 23;
|
|
949
942
|
const CBOR_UINT8_MAX = 255;
|
|
950
943
|
const CBOR_UINT16_MAX = 65535;
|
|
951
|
-
const COSE_SIG1_CONTEXT_BYTES = new TextEncoder().encode("Signature1");
|
|
944
|
+
const COSE_SIG1_CONTEXT_BYTES = /* @__PURE__ */ new TextEncoder().encode("Signature1");
|
|
952
945
|
function byteStringHeaderSize(length) {
|
|
953
946
|
if (length <= CBOR_INLINE_MAX) return 1;
|
|
954
947
|
if (length <= CBOR_UINT8_MAX) return 2;
|
|
@@ -1423,6 +1416,117 @@ async function verifySignerBinding(signerBindingBytes, sessionCoseKey, signerCer
|
|
|
1423
1416
|
return verifyCoseSign1(coseSign1, encode(signerCertBytes), publicKey);
|
|
1424
1417
|
}
|
|
1425
1418
|
|
|
1419
|
+
//#endregion
|
|
1420
|
+
//#region src/bmff/parseExclusions.ts
|
|
1421
|
+
function parseConstraints(rawConstraints) {
|
|
1422
|
+
if (!Array.isArray(rawConstraints)) return [];
|
|
1423
|
+
const constraints = [];
|
|
1424
|
+
for (const c of rawConstraints) {
|
|
1425
|
+
if (!c || typeof c !== "object") continue;
|
|
1426
|
+
const record = c;
|
|
1427
|
+
if (typeof record["offset"] !== "number") continue;
|
|
1428
|
+
const value = toUint8Array(record["value"]);
|
|
1429
|
+
if (value) constraints.push({
|
|
1430
|
+
offset: record["offset"],
|
|
1431
|
+
value
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
return constraints;
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Parses the `exclusions` array of a `c2pa.hash.bmff.v3` assertion.
|
|
1438
|
+
*
|
|
1439
|
+
* @internal
|
|
1440
|
+
*/
|
|
1441
|
+
function parseExclusions(rawExclusions) {
|
|
1442
|
+
if (!Array.isArray(rawExclusions)) return [];
|
|
1443
|
+
const exclusions = [];
|
|
1444
|
+
for (const exc of rawExclusions) {
|
|
1445
|
+
if (!exc || typeof exc !== "object") continue;
|
|
1446
|
+
const record = exc;
|
|
1447
|
+
if (typeof record["xpath"] !== "string") continue;
|
|
1448
|
+
const constraints = parseConstraints(record["data"]);
|
|
1449
|
+
exclusions.push(constraints.length > 0 ? {
|
|
1450
|
+
xpath: record["xpath"],
|
|
1451
|
+
data: constraints
|
|
1452
|
+
} : { xpath: record["xpath"] });
|
|
1453
|
+
}
|
|
1454
|
+
return exclusions;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
//#endregion
|
|
1458
|
+
//#region src/merkle/validateMerkleMaps.ts
|
|
1459
|
+
function parseMerkleRow(rawHashes) {
|
|
1460
|
+
if (!Array.isArray(rawHashes) || rawHashes.length === 0) return null;
|
|
1461
|
+
const row = [];
|
|
1462
|
+
for (const entry of rawHashes) {
|
|
1463
|
+
const bytes = toUint8Array(entry);
|
|
1464
|
+
if (!bytes) return null;
|
|
1465
|
+
row.push(bytes);
|
|
1466
|
+
}
|
|
1467
|
+
return row;
|
|
1468
|
+
}
|
|
1469
|
+
function extractMerkleMaps(bmffHashAssertionData) {
|
|
1470
|
+
const rawMerkle = bmffHashAssertionData["merkle"];
|
|
1471
|
+
if (!Array.isArray(rawMerkle) || rawMerkle.length === 0) return null;
|
|
1472
|
+
const exclusions = parseExclusions(bmffHashAssertionData["exclusions"]);
|
|
1473
|
+
const assertionAlg = bmffHashAssertionData["alg"];
|
|
1474
|
+
const offsetPrefixSize = bmffHashAssertionData["hash"] == null ? 8 : 0;
|
|
1475
|
+
const maps = [];
|
|
1476
|
+
for (const entry of rawMerkle) {
|
|
1477
|
+
if (!entry || typeof entry !== "object") return null;
|
|
1478
|
+
const record = entry;
|
|
1479
|
+
const uniqueId = asInteger(record["uniqueId"]);
|
|
1480
|
+
const localId = asInteger(record["localId"]);
|
|
1481
|
+
const count = asInteger(record["count"]);
|
|
1482
|
+
const hashes = parseMerkleRow(record["hashes"]);
|
|
1483
|
+
if (uniqueId === null || localId === null || count === null || !hashes) return null;
|
|
1484
|
+
const initHash = toUint8Array(record["initHash"]);
|
|
1485
|
+
if (!initHash) return null;
|
|
1486
|
+
const rawAlg = record["alg"] ?? assertionAlg;
|
|
1487
|
+
if (typeof rawAlg !== "string") return null;
|
|
1488
|
+
maps.push({
|
|
1489
|
+
uniqueId,
|
|
1490
|
+
localId,
|
|
1491
|
+
count,
|
|
1492
|
+
hashes,
|
|
1493
|
+
initHash,
|
|
1494
|
+
alg: normalizeAlgorithmName(rawAlg),
|
|
1495
|
+
exclusions,
|
|
1496
|
+
offsetPrefixSize
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
return maps;
|
|
1500
|
+
}
|
|
1501
|
+
async function validateMerkleMaps(bytes, assertion, codes) {
|
|
1502
|
+
const data = assertion?.data;
|
|
1503
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) return null;
|
|
1504
|
+
if (data["merkle"] === void 0) return null;
|
|
1505
|
+
const merkleMaps = extractMerkleMaps(data);
|
|
1506
|
+
if (!merkleMaps) {
|
|
1507
|
+
codes.add(C2paStatusCode.ASSERTION_BMFFHASH_MALFORMED);
|
|
1508
|
+
return [];
|
|
1509
|
+
}
|
|
1510
|
+
const initHashByAlg = /* @__PURE__ */ new Map();
|
|
1511
|
+
for (const merkleMap of merkleMaps) {
|
|
1512
|
+
if (!merkleMap.initHash) continue;
|
|
1513
|
+
let computed = initHashByAlg.get(merkleMap.alg);
|
|
1514
|
+
if (!computed) {
|
|
1515
|
+
computed = await computeBmffHash(bytes, {
|
|
1516
|
+
offsetPrefixSize: merkleMap.offsetPrefixSize,
|
|
1517
|
+
exclusions: merkleMap.exclusions,
|
|
1518
|
+
alg: merkleMap.alg ?? void 0
|
|
1519
|
+
});
|
|
1520
|
+
initHashByAlg.set(merkleMap.alg, computed);
|
|
1521
|
+
}
|
|
1522
|
+
if (!hashesEqual(computed, merkleMap.initHash)) {
|
|
1523
|
+
codes.add(LiveVideoStatusCode.INIT_INVALID);
|
|
1524
|
+
codes.add(C2paStatusCode.ASSERTION_BMFFHASH_MISMATCH);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
return merkleMaps;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1426
1530
|
//#endregion
|
|
1427
1531
|
//#region src/init/validateC2paInitSegment.ts
|
|
1428
1532
|
const BMFF_HASH_ASSERTION_LABEL$1 = "c2pa.hash.bmff.v3";
|
|
@@ -1485,10 +1589,11 @@ async function validateBmffHashAssertion(bytes, assertion) {
|
|
|
1485
1589
|
if (!rawHash) return true;
|
|
1486
1590
|
const expectedHash = rawHash instanceof Uint8Array ? rawHash : new Uint8Array(rawHash);
|
|
1487
1591
|
const alg = normalizeAlgorithmName(data["alg"]);
|
|
1488
|
-
return
|
|
1592
|
+
return hashesEqual(await computeBmffHash(bytes, {
|
|
1489
1593
|
exclusions: data["exclusions"] ?? [],
|
|
1490
|
-
alg
|
|
1491
|
-
|
|
1594
|
+
alg,
|
|
1595
|
+
offsetPrefixSize: 8
|
|
1596
|
+
}), expectedHash);
|
|
1492
1597
|
}
|
|
1493
1598
|
function extractSessionKeyFields(entry) {
|
|
1494
1599
|
const keyData = entry;
|
|
@@ -1565,19 +1670,22 @@ async function validateC2paInitSegment(bytes) {
|
|
|
1565
1670
|
certificate: null,
|
|
1566
1671
|
manifestId: null,
|
|
1567
1672
|
sessionKeys: [],
|
|
1673
|
+
merkleMaps: [],
|
|
1568
1674
|
isValid: false,
|
|
1569
1675
|
errorCodes: [LiveVideoStatusCode.INIT_INVALID]
|
|
1570
1676
|
};
|
|
1571
1677
|
const internalData = readC2paManifest(bytes, boxes);
|
|
1572
1678
|
const { manifest } = internalData;
|
|
1573
1679
|
const certificate = internalData.signatureBytes ? extractCertificateFromSignatureBytes(internalData.signatureBytes) : null;
|
|
1574
|
-
const
|
|
1680
|
+
const bmffHashAssertion = manifest.assertions.find((a) => a.label === BMFF_HASH_ASSERTION_LABEL$1) ?? null;
|
|
1681
|
+
const bmffHashValid = await validateBmffHashAssertion(bytes, bmffHashAssertion);
|
|
1575
1682
|
const sessionKeysAssertion = manifest.assertions.find((a) => a.label === SESSION_KEYS_ASSERTION_LABEL);
|
|
1576
1683
|
const sessionKeys = sessionKeysAssertion && certificate ? await validateSessionKeys(sessionKeysAssertion, certificate) : [];
|
|
1577
1684
|
const integrityCodes = await validateManifestIntegrity(internalData, certificate);
|
|
1578
1685
|
const codes = /* @__PURE__ */ new Set();
|
|
1686
|
+
const merkleMaps = await validateMerkleMaps(bytes, bmffHashAssertion, codes);
|
|
1579
1687
|
if (!bmffHashValid) codes.add(LiveVideoStatusCode.INIT_INVALID);
|
|
1580
|
-
if (sessionKeys.length === 0) codes.add(LiveVideoStatusCode.SESSIONKEY_INVALID);
|
|
1688
|
+
if (sessionKeys.length === 0 && merkleMaps === null) codes.add(LiveVideoStatusCode.SESSIONKEY_INVALID);
|
|
1581
1689
|
for (const code of integrityCodes) codes.add(code);
|
|
1582
1690
|
const errorCodes = [...codes];
|
|
1583
1691
|
return {
|
|
@@ -1585,6 +1693,7 @@ async function validateC2paInitSegment(bytes) {
|
|
|
1585
1693
|
certificate,
|
|
1586
1694
|
manifestId: manifest.instanceId,
|
|
1587
1695
|
sessionKeys,
|
|
1696
|
+
merkleMaps: merkleMaps ?? [],
|
|
1588
1697
|
isValid: errorCodes.length === 0,
|
|
1589
1698
|
errorCodes
|
|
1590
1699
|
};
|
|
@@ -1783,6 +1892,36 @@ function validateSequenceNumber(state, sequenceNumber, minSequenceNumber) {
|
|
|
1783
1892
|
};
|
|
1784
1893
|
}
|
|
1785
1894
|
|
|
1895
|
+
//#endregion
|
|
1896
|
+
//#region src/bmff/validateBmffHash.ts
|
|
1897
|
+
const OFFSET_PREFIX_SIZES_TO_TRY = [8, 0];
|
|
1898
|
+
/**
|
|
1899
|
+
* Validates the C2PA BMFF content hash (`c2pa.hash.bmff.v3`) for a DASH segment.
|
|
1900
|
+
*
|
|
1901
|
+
* Tries both offset-prefix modes — 8-byte file offset prefix and no prefix — to handle
|
|
1902
|
+
* inter-signer interoperability for hashes carried in the VSI CBOR map (§19.7.3), where
|
|
1903
|
+
* the spec does not signal the prefix mode. Flat hashes from a `c2pa.hash.bmff.v3`
|
|
1904
|
+
* assertion always use the 8-byte prefix (§18.6.2); verify those with
|
|
1905
|
+
* {@link computeBmffHash} and `offsetPrefixSize: 8` instead.
|
|
1906
|
+
*
|
|
1907
|
+
* @param segmentBytes - Raw segment bytes to validate
|
|
1908
|
+
* @param expectedHash - Expected hash bytes from the VSI CBOR map
|
|
1909
|
+
* @param options - Validation options (exclusions, algorithm)
|
|
1910
|
+
* @returns `true` if the computed hash matches the expected hash in either offset mode
|
|
1911
|
+
*
|
|
1912
|
+
* @example
|
|
1913
|
+
* {@includeCode ../../test/bmff/validateBmffHash.test.ts#example}
|
|
1914
|
+
*
|
|
1915
|
+
* @public
|
|
1916
|
+
*/
|
|
1917
|
+
async function validateBmffHash(segmentBytes, expectedHash, options) {
|
|
1918
|
+
for (const offsetPrefixSize of OFFSET_PREFIX_SIZES_TO_TRY) if (hashesEqual(await computeBmffHash(segmentBytes, {
|
|
1919
|
+
...options,
|
|
1920
|
+
offsetPrefixSize
|
|
1921
|
+
}), expectedHash)) return true;
|
|
1922
|
+
return false;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1786
1925
|
//#endregion
|
|
1787
1926
|
//#region src/segment/validateC2paSegment.ts
|
|
1788
1927
|
function findSessionKey(sessionKeys, kidHex) {
|
|
@@ -1864,7 +2003,6 @@ const LIVE_VIDEO_ASSERTION_LABEL = "c2pa.livevideo.segment";
|
|
|
1864
2003
|
const BMFF_HASH_ASSERTION_LABEL = "c2pa.hash.bmff.v3";
|
|
1865
2004
|
const MANIFEST_ID_PREFIX_PATTERN = /^(xmp:iid:|urn:uuid:)/i;
|
|
1866
2005
|
const CONTINUITY_METHOD_MANIFEST_ID = "c2pa.manifestId";
|
|
1867
|
-
const SUPPORTED_CONTINUITY_METHODS = new Set([CONTINUITY_METHOD_MANIFEST_ID]);
|
|
1868
2006
|
function normalizeManifestId(id) {
|
|
1869
2007
|
if (!id) return null;
|
|
1870
2008
|
return id.replace(MANIFEST_ID_PREFIX_PATTERN, "").toLowerCase();
|
|
@@ -1873,11 +2011,6 @@ function extractAssertionData(data) {
|
|
|
1873
2011
|
if (data !== null && typeof data === "object" && !Array.isArray(data)) return data;
|
|
1874
2012
|
return null;
|
|
1875
2013
|
}
|
|
1876
|
-
function toUint8Array(value) {
|
|
1877
|
-
if (value instanceof Uint8Array) return value;
|
|
1878
|
-
if (Array.isArray(value)) return new Uint8Array(value);
|
|
1879
|
-
return null;
|
|
1880
|
-
}
|
|
1881
2014
|
function parseLiveVideoAssertion(assertions) {
|
|
1882
2015
|
const assertion = assertions.find((a) => a.label === LIVE_VIDEO_ASSERTION_LABEL);
|
|
1883
2016
|
if (!assertion) return null;
|
|
@@ -1890,7 +2023,8 @@ function parseLiveVideoAssertion(assertions) {
|
|
|
1890
2023
|
sequenceNumber: typeof rawSeq === "number" ? rawSeq : null,
|
|
1891
2024
|
previousManifestId: typeof rawPrev === "string" ? rawPrev : null,
|
|
1892
2025
|
streamId: typeof rawStreamId === "string" ? rawStreamId : null,
|
|
1893
|
-
continuityMethod: typeof rawContinuity === "string" ? rawContinuity : null
|
|
2026
|
+
continuityMethod: typeof rawContinuity === "string" ? rawContinuity : null,
|
|
2027
|
+
data: data ?? {}
|
|
1894
2028
|
};
|
|
1895
2029
|
}
|
|
1896
2030
|
const EMPTY_BMFF_HASH = {
|
|
@@ -1899,36 +2033,6 @@ const EMPTY_BMFF_HASH = {
|
|
|
1899
2033
|
exclusions: [],
|
|
1900
2034
|
alg: null
|
|
1901
2035
|
};
|
|
1902
|
-
function parseConstraints(rawConstraints) {
|
|
1903
|
-
if (!Array.isArray(rawConstraints)) return [];
|
|
1904
|
-
const constraints = [];
|
|
1905
|
-
for (const c of rawConstraints) {
|
|
1906
|
-
if (!c || typeof c !== "object") continue;
|
|
1907
|
-
const record = c;
|
|
1908
|
-
if (typeof record["offset"] !== "number") continue;
|
|
1909
|
-
const value = toUint8Array(record["value"]);
|
|
1910
|
-
if (value) constraints.push({
|
|
1911
|
-
offset: record["offset"],
|
|
1912
|
-
value
|
|
1913
|
-
});
|
|
1914
|
-
}
|
|
1915
|
-
return constraints;
|
|
1916
|
-
}
|
|
1917
|
-
function parseExclusions(rawExclusions) {
|
|
1918
|
-
if (!Array.isArray(rawExclusions)) return [];
|
|
1919
|
-
const exclusions = [];
|
|
1920
|
-
for (const exc of rawExclusions) {
|
|
1921
|
-
if (!exc || typeof exc !== "object") continue;
|
|
1922
|
-
const record = exc;
|
|
1923
|
-
if (typeof record["xpath"] !== "string") continue;
|
|
1924
|
-
const constraints = parseConstraints(record["data"]);
|
|
1925
|
-
exclusions.push(constraints.length > 0 ? {
|
|
1926
|
-
xpath: record["xpath"],
|
|
1927
|
-
data: constraints
|
|
1928
|
-
} : { xpath: record["xpath"] });
|
|
1929
|
-
}
|
|
1930
|
-
return exclusions;
|
|
1931
|
-
}
|
|
1932
2036
|
function parseBmffHashAssertion(assertions) {
|
|
1933
2037
|
const assertion = assertions.find((a) => a.label === BMFF_HASH_ASSERTION_LABEL);
|
|
1934
2038
|
if (!assertion) return EMPTY_BMFF_HASH;
|
|
@@ -1970,21 +2074,28 @@ function parseManifest(bytes) {
|
|
|
1970
2074
|
};
|
|
1971
2075
|
}
|
|
1972
2076
|
}
|
|
1973
|
-
function collectErrorCodes(hasManifest, hasLiveVideo, streamIdValid, sequenceNumberValid, bmffHashMatches
|
|
2077
|
+
function collectErrorCodes(hasManifest, hasLiveVideo, streamIdValid, sequenceNumberValid, bmffHashMatches) {
|
|
1974
2078
|
const codes = /* @__PURE__ */ new Set();
|
|
1975
2079
|
if (!hasManifest) codes.add(LiveVideoStatusCode.MANIFEST_INVALID);
|
|
1976
2080
|
if (!hasLiveVideo) codes.add(LiveVideoStatusCode.ASSERTION_INVALID);
|
|
1977
2081
|
if (!streamIdValid) codes.add(LiveVideoStatusCode.ASSERTION_INVALID);
|
|
1978
2082
|
if (!sequenceNumberValid) codes.add(LiveVideoStatusCode.ASSERTION_INVALID);
|
|
1979
2083
|
if (!bmffHashMatches) codes.add(LiveVideoStatusCode.SEGMENT_INVALID);
|
|
1980
|
-
if (!continuityMethod) codes.add(LiveVideoStatusCode.CONTINUITY_METHOD_INVALID);
|
|
1981
|
-
else if (!SUPPORTED_CONTINUITY_METHODS.has(continuityMethod)) codes.add(LiveVideoStatusCode.CONTINUITY_METHOD_INVALID);
|
|
1982
|
-
else if (continuityMethod === CONTINUITY_METHOD_MANIFEST_ID) {
|
|
1983
|
-
if (!previousManifestId) codes.add(LiveVideoStatusCode.CONTINUITY_METHOD_INVALID);
|
|
1984
|
-
else if (lastManifestId && normalizeManifestId(previousManifestId) !== normalizeManifestId(lastManifestId)) codes.add(LiveVideoStatusCode.SEGMENT_INVALID);
|
|
1985
|
-
}
|
|
1986
2084
|
return [...codes];
|
|
1987
2085
|
}
|
|
2086
|
+
async function validateContinuity(parsed, lastManifestId, options) {
|
|
2087
|
+
const { manifest, liveVideo } = parsed;
|
|
2088
|
+
const method = liveVideo?.continuityMethod ?? null;
|
|
2089
|
+
if (!method) return [LiveVideoStatusCode.CONTINUITY_METHOD_INVALID];
|
|
2090
|
+
if (method === CONTINUITY_METHOD_MANIFEST_ID) {
|
|
2091
|
+
const prev = liveVideo?.previousManifestId ?? null;
|
|
2092
|
+
if (!prev) return [LiveVideoStatusCode.CONTINUITY_METHOD_INVALID];
|
|
2093
|
+
return !!lastManifestId && normalizeManifestId(prev) !== normalizeManifestId(lastManifestId) ? [LiveVideoStatusCode.SEGMENT_INVALID] : [];
|
|
2094
|
+
}
|
|
2095
|
+
const custom = options?.continuityValidator;
|
|
2096
|
+
if (custom?.method !== method || !manifest || !liveVideo) return [LiveVideoStatusCode.CONTINUITY_METHOD_INVALID, LiveVideoStatusCode.CONTINUITY_METHOD_UNSUPPORTED];
|
|
2097
|
+
return await Promise.resolve().then(() => custom.validate(liveVideo.data, manifest)).catch(() => false) ? [] : [LiveVideoStatusCode.SEGMENT_INVALID];
|
|
2098
|
+
}
|
|
1988
2099
|
/**
|
|
1989
2100
|
* Validates a C2PA manifest-box live stream segment.
|
|
1990
2101
|
*
|
|
@@ -1992,6 +2103,8 @@ function collectErrorCodes(hasManifest, hasLiveVideo, streamIdValid, sequenceNum
|
|
|
1992
2103
|
* Recomputes the `c2pa.hash.bmff.v3` content hash from the raw segment bytes and compares
|
|
1993
2104
|
* it against the expected hash in the manifest assertion. Checks live-video assertions
|
|
1994
2105
|
* (sequenceNumber, streamId, continuityMethod) and manifest-ID chain continuity.
|
|
2106
|
+
* A validator for an implementer-defined continuity method can be registered
|
|
2107
|
+
* via {@link ManifestBoxValidationOptions}.
|
|
1995
2108
|
*
|
|
1996
2109
|
* This function is **pure** — it does not access any external state. The
|
|
1997
2110
|
* caller is responsible for persisting `nextManifestId` and `nextState`
|
|
@@ -2000,6 +2113,7 @@ function collectErrorCodes(hasManifest, hasLiveVideo, streamIdValid, sequenceNum
|
|
|
2000
2113
|
* @param bytes - Raw segment bytes
|
|
2001
2114
|
* @param lastManifestId - Manifest ID from the previous segment, or null for the first segment
|
|
2002
2115
|
* @param state - Optional state from the previous segment for streamId/sequenceNumber checks
|
|
2116
|
+
* @param options - Optional custom continuity method validator
|
|
2003
2117
|
* @returns Validation result, the manifest ID, and state to persist for the next call
|
|
2004
2118
|
*
|
|
2005
2119
|
* @example
|
|
@@ -2007,7 +2121,7 @@ function collectErrorCodes(hasManifest, hasLiveVideo, streamIdValid, sequenceNum
|
|
|
2007
2121
|
*
|
|
2008
2122
|
* @public
|
|
2009
2123
|
*/
|
|
2010
|
-
async function validateC2paManifestBoxSegment(bytes, lastManifestId, state) {
|
|
2124
|
+
async function validateC2paManifestBoxSegment(bytes, lastManifestId, state, options) {
|
|
2011
2125
|
const { manifest, issuer, liveVideo, bmff, internalData } = parseManifest(bytes);
|
|
2012
2126
|
const sequenceNumber = liveVideo?.sequenceNumber ?? null;
|
|
2013
2127
|
const previousManifestId = liveVideo?.previousManifestId ?? null;
|
|
@@ -2016,11 +2130,19 @@ async function validateC2paManifestBoxSegment(bytes, lastManifestId, state) {
|
|
|
2016
2130
|
const streamIdValid = state?.lastStreamId == null || streamId === state.lastStreamId;
|
|
2017
2131
|
const sequenceNumberValid = state?.lastSequenceNumber == null || sequenceNumber !== null && sequenceNumber > state.lastSequenceNumber;
|
|
2018
2132
|
let bmffHashMatches = true;
|
|
2019
|
-
if (bmff.hashBytes !== null) bmffHashMatches = await
|
|
2133
|
+
if (bmff.hashBytes !== null) bmffHashMatches = hashesEqual(await computeBmffHash(bytes, {
|
|
2020
2134
|
exclusions: bmff.exclusions,
|
|
2021
|
-
alg: bmff.alg ?? void 0
|
|
2022
|
-
|
|
2023
|
-
|
|
2135
|
+
alg: bmff.alg ?? void 0,
|
|
2136
|
+
offsetPrefixSize: 8
|
|
2137
|
+
}), bmff.hashBytes);
|
|
2138
|
+
const continuityCodes = await validateContinuity({
|
|
2139
|
+
manifest,
|
|
2140
|
+
issuer,
|
|
2141
|
+
liveVideo,
|
|
2142
|
+
bmff,
|
|
2143
|
+
internalData
|
|
2144
|
+
}, lastManifestId, options);
|
|
2145
|
+
const liveVideoCodes = [...new Set([...collectErrorCodes(manifest !== null, liveVideo !== null, streamIdValid, sequenceNumberValid, bmffHashMatches), ...continuityCodes])];
|
|
2024
2146
|
const integrityCodes = internalData ? await validateManifestIntegrity(internalData, internalData.signatureBytes ? extractCertificateFromSignatureBytes(internalData.signatureBytes) : null) : [];
|
|
2025
2147
|
const errorCodes = [...liveVideoCodes, ...integrityCodes];
|
|
2026
2148
|
const currentManifestId = manifest?.instanceId ?? null;
|
|
@@ -2045,5 +2167,187 @@ async function validateC2paManifestBoxSegment(bytes, lastManifestId, state) {
|
|
|
2045
2167
|
}
|
|
2046
2168
|
|
|
2047
2169
|
//#endregion
|
|
2048
|
-
|
|
2170
|
+
//#region src/merkle/validateC2paMerkleSegment.ts
|
|
2171
|
+
const MERKLE_BOX_PURPOSE = "merkle";
|
|
2172
|
+
function readAuxPayload(rawPayload) {
|
|
2173
|
+
const prefix = readUuidBoxPurpose(rawPayload);
|
|
2174
|
+
if (!prefix || prefix.purpose !== MERKLE_BOX_PURPOSE) return "other";
|
|
2175
|
+
return prefix.rest;
|
|
2176
|
+
}
|
|
2177
|
+
function extractMerkleAuxBoxes(segmentBytes) {
|
|
2178
|
+
const payloads = [];
|
|
2179
|
+
for (const box of readIsoBoxes(segmentBytes)) {
|
|
2180
|
+
const uuidBox = box;
|
|
2181
|
+
if (uuidBox.type !== "uuid") continue;
|
|
2182
|
+
if (!matchesUuid(uuidBox.usertype ?? [], JUMBF_UUID)) continue;
|
|
2183
|
+
const payload = readAuxPayload(uuidBox.view.readData(uuidBox.view.bytesRemaining));
|
|
2184
|
+
if (payload !== "other") payloads.push(payload);
|
|
2185
|
+
}
|
|
2186
|
+
return payloads;
|
|
2187
|
+
}
|
|
2188
|
+
function readMapField(map, name) {
|
|
2189
|
+
if (map instanceof Map) return map.get(name);
|
|
2190
|
+
return map[name];
|
|
2191
|
+
}
|
|
2192
|
+
function decodeFirstCbor(payload) {
|
|
2193
|
+
let first;
|
|
2194
|
+
let found = false;
|
|
2195
|
+
try {
|
|
2196
|
+
decodeMultiple(payload, (value) => {
|
|
2197
|
+
first = value;
|
|
2198
|
+
found = true;
|
|
2199
|
+
return false;
|
|
2200
|
+
});
|
|
2201
|
+
} catch {
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
return found ? first : void 0;
|
|
2205
|
+
}
|
|
2206
|
+
function parseBmffMerkleMap(payload) {
|
|
2207
|
+
const decoded = decodeFirstCbor(payload);
|
|
2208
|
+
if (decoded === null || decoded === void 0 || typeof decoded !== "object") return null;
|
|
2209
|
+
const uniqueId = asInteger(readMapField(decoded, "uniqueId"));
|
|
2210
|
+
const localId = asInteger(readMapField(decoded, "localId"));
|
|
2211
|
+
const location = asInteger(readMapField(decoded, "location"));
|
|
2212
|
+
if (uniqueId === null || localId === null || location === null) return null;
|
|
2213
|
+
const rawHashes = readMapField(decoded, "hashes");
|
|
2214
|
+
if (rawHashes == null) return {
|
|
2215
|
+
uniqueId,
|
|
2216
|
+
localId,
|
|
2217
|
+
location,
|
|
2218
|
+
hashes: null
|
|
2219
|
+
};
|
|
2220
|
+
if (!Array.isArray(rawHashes)) return null;
|
|
2221
|
+
const hashes = [];
|
|
2222
|
+
for (const entry of rawHashes) {
|
|
2223
|
+
if (entry == null) {
|
|
2224
|
+
hashes.push(null);
|
|
2225
|
+
continue;
|
|
2226
|
+
}
|
|
2227
|
+
const bytes = toUint8Array(entry);
|
|
2228
|
+
if (!bytes) return null;
|
|
2229
|
+
hashes.push(bytes);
|
|
2230
|
+
}
|
|
2231
|
+
return {
|
|
2232
|
+
uniqueId,
|
|
2233
|
+
localId,
|
|
2234
|
+
location,
|
|
2235
|
+
hashes
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
async function hashPair(left, right, alg) {
|
|
2239
|
+
const input = new Uint8Array(left.length + right.length);
|
|
2240
|
+
input.set(left, 0);
|
|
2241
|
+
input.set(right, left.length);
|
|
2242
|
+
return new Uint8Array(await crypto.subtle.digest(alg, input));
|
|
2243
|
+
}
|
|
2244
|
+
function treeLayout(leafCount) {
|
|
2245
|
+
const layers = [leafCount];
|
|
2246
|
+
while (layers[layers.length - 1] > 1) layers.push(Math.ceil(layers[layers.length - 1] / 2));
|
|
2247
|
+
return layers;
|
|
2248
|
+
}
|
|
2249
|
+
async function verifyMerkleProof(leafHash, location, proofPath, merkleMap) {
|
|
2250
|
+
const { count, hashes: manifestRow } = merkleMap;
|
|
2251
|
+
if (location < 0 || location >= count) return C2paStatusCode.ASSERTION_BMFFHASH_MALFORMED;
|
|
2252
|
+
const layers = treeLayout(count);
|
|
2253
|
+
const committedLevel = layers.indexOf(manifestRow.length);
|
|
2254
|
+
if (committedLevel === -1) return C2paStatusCode.ASSERTION_BMFFHASH_MALFORMED;
|
|
2255
|
+
const rawPath = proofPath ?? [];
|
|
2256
|
+
if (rawPath.length > committedLevel) return C2paStatusCode.ASSERTION_BMFFHASH_MALFORMED;
|
|
2257
|
+
const path = rawPath.filter((node) => node !== null);
|
|
2258
|
+
const alg = normalizeAlgorithmName(merkleMap.alg ?? void 0);
|
|
2259
|
+
let currentHash = leafHash;
|
|
2260
|
+
let currentIndex = location;
|
|
2261
|
+
let proofIndex = 0;
|
|
2262
|
+
for (const layer of layers) {
|
|
2263
|
+
if (layer === manifestRow.length) break;
|
|
2264
|
+
const siblingIndex = currentIndex % 2 === 0 ? currentIndex + 1 : currentIndex - 1;
|
|
2265
|
+
if (siblingIndex >= 0 && siblingIndex < layer) {
|
|
2266
|
+
const sibling = path[proofIndex++];
|
|
2267
|
+
if (!sibling) return C2paStatusCode.ASSERTION_BMFFHASH_MALFORMED;
|
|
2268
|
+
currentHash = currentIndex % 2 === 0 ? await hashPair(currentHash, sibling, alg) : await hashPair(sibling, currentHash, alg);
|
|
2269
|
+
}
|
|
2270
|
+
currentIndex = Math.floor(currentIndex / 2);
|
|
2271
|
+
}
|
|
2272
|
+
if (proofIndex !== path.length) return C2paStatusCode.ASSERTION_BMFFHASH_MALFORMED;
|
|
2273
|
+
const expected = manifestRow[currentIndex];
|
|
2274
|
+
if (!expected) return C2paStatusCode.ASSERTION_BMFFHASH_MALFORMED;
|
|
2275
|
+
return hashesEqual(currentHash, expected) ? "ok" : C2paStatusCode.ASSERTION_BMFFHASH_MISMATCH;
|
|
2276
|
+
}
|
|
2277
|
+
function findMerkleMap(merkleMaps, uniqueId, localId) {
|
|
2278
|
+
return merkleMaps.find((m) => m.uniqueId === uniqueId && m.localId === localId) ?? null;
|
|
2279
|
+
}
|
|
2280
|
+
async function computeLeafHash(segmentBytes, merkleMap, cache) {
|
|
2281
|
+
const cached = cache.find((entry) => entry.alg === merkleMap.alg && entry.offsetPrefixSize === merkleMap.offsetPrefixSize && entry.exclusions === merkleMap.exclusions);
|
|
2282
|
+
if (cached) return cached.hash;
|
|
2283
|
+
const hash = await computeBmffHash(segmentBytes, {
|
|
2284
|
+
offsetPrefixSize: merkleMap.offsetPrefixSize,
|
|
2285
|
+
exclusions: merkleMap.exclusions,
|
|
2286
|
+
alg: merkleMap.alg ?? void 0
|
|
2287
|
+
});
|
|
2288
|
+
cache.push({
|
|
2289
|
+
alg: merkleMap.alg,
|
|
2290
|
+
offsetPrefixSize: merkleMap.offsetPrefixSize,
|
|
2291
|
+
exclusions: merkleMap.exclusions,
|
|
2292
|
+
hash
|
|
2293
|
+
});
|
|
2294
|
+
return hash;
|
|
2295
|
+
}
|
|
2296
|
+
/**
|
|
2297
|
+
* Validates a fragmented MP4 VOD media segment against the init manifest's
|
|
2298
|
+
* merkle maps (§15.12.2.2). Pure: the caller persists `nextState` and resets
|
|
2299
|
+
* `state` after a seek.
|
|
2300
|
+
*
|
|
2301
|
+
* `merkleMaps` must be non-empty; the caller is responsible for only calling
|
|
2302
|
+
* this in VOD Merkle mode (i.e. after `InitSegmentValidation.merkleMaps` came
|
|
2303
|
+
* back non-empty).
|
|
2304
|
+
*
|
|
2305
|
+
* @example
|
|
2306
|
+
* {@includeCode ../../test/merkle/validateC2paMerkleSegment.test.ts#example}
|
|
2307
|
+
*
|
|
2308
|
+
* @public
|
|
2309
|
+
*/
|
|
2310
|
+
async function validateC2paMerkleSegment(segmentBytes, merkleMaps, state) {
|
|
2311
|
+
const previousLocations = state?.lastLocations ?? /* @__PURE__ */ new Map();
|
|
2312
|
+
const nextLocations = new Map(previousLocations);
|
|
2313
|
+
const codes = /* @__PURE__ */ new Set();
|
|
2314
|
+
const leafHashCache = [];
|
|
2315
|
+
let location = null;
|
|
2316
|
+
let bmffHashHex = null;
|
|
2317
|
+
const payloads = extractMerkleAuxBoxes(segmentBytes);
|
|
2318
|
+
if (payloads.length === 0) codes.add(C2paStatusCode.ASSERTION_BMFFHASH_MALFORMED);
|
|
2319
|
+
for (const payload of payloads) {
|
|
2320
|
+
const segmentMap = parseBmffMerkleMap(payload);
|
|
2321
|
+
if (!segmentMap) {
|
|
2322
|
+
codes.add(C2paStatusCode.ASSERTION_BMFFHASH_MALFORMED);
|
|
2323
|
+
continue;
|
|
2324
|
+
}
|
|
2325
|
+
const merkleMap = findMerkleMap(merkleMaps, segmentMap.uniqueId, segmentMap.localId);
|
|
2326
|
+
if (!merkleMap) {
|
|
2327
|
+
codes.add(C2paStatusCode.ASSERTION_BMFFHASH_MALFORMED);
|
|
2328
|
+
continue;
|
|
2329
|
+
}
|
|
2330
|
+
if (location === null) location = segmentMap.location;
|
|
2331
|
+
const trackKey = `${segmentMap.uniqueId}:${segmentMap.localId}`;
|
|
2332
|
+
const lastLocation = previousLocations.get(trackKey);
|
|
2333
|
+
if (!(lastLocation === void 0 || segmentMap.location === lastLocation + 1)) codes.add(LiveVideoStatusCode.ASSERTION_INVALID);
|
|
2334
|
+
const leafHash = await computeLeafHash(segmentBytes, merkleMap, leafHashCache);
|
|
2335
|
+
if (bmffHashHex === null) bmffHashHex = bytesToHex(leafHash);
|
|
2336
|
+
const proofResult = await verifyMerkleProof(leafHash, segmentMap.location, segmentMap.hashes, merkleMap);
|
|
2337
|
+
if (proofResult === "ok") nextLocations.set(trackKey, segmentMap.location);
|
|
2338
|
+
else codes.add(proofResult);
|
|
2339
|
+
}
|
|
2340
|
+
return {
|
|
2341
|
+
result: {
|
|
2342
|
+
location,
|
|
2343
|
+
bmffHashHex,
|
|
2344
|
+
isValid: codes.size === 0,
|
|
2345
|
+
errorCodes: [...codes]
|
|
2346
|
+
},
|
|
2347
|
+
nextState: { lastLocations: nextLocations }
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
//#endregion
|
|
2352
|
+
export { C2paStatusCode, LiveVideoStatusCode, SequenceValidationReason, validateC2paInitSegment, validateC2paManifestBoxSegment, validateC2paMerkleSegment, validateC2paSegment };
|
|
2049
2353
|
//# sourceMappingURL=index.js.map
|