feat(c2pa): VOD Merkle validation for fragmented MP4#53
Open
valentinamgiusti wants to merge 31 commits into
Open
feat(c2pa): VOD Merkle validation for fragmented MP4#53valentinamgiusti wants to merge 31 commits into
valentinamgiusti wants to merge 31 commits into
Conversation
valentinamgiusti
added a commit
to qualabs/c2pa-live-video-toolkit
that referenced
this pull request
Jul 13, 2026
CI installs @svta/cml-c2pa from npm, where the latest release (1.0.1) predates the VOD Merkle API, so the integration tests validated nothing and failed. Vendor the build of qualabs/common-media-library#53 as a tarball and install it after npm ci. Delete the step and vendor/ once upstream publishes 1.1.0.
valentinamgiusti
added a commit
to qualabs/c2pa-live-video-toolkit
that referenced
this pull request
Jul 13, 2026
CI installs @svta/cml-c2pa from npm, where the latest release (1.0.1) predates the VOD Merkle API, so the integration tests validated nothing and failed. Vendor the build of qualabs/common-media-library#53 as a tarball and install it after npm ci. Delete the step and vendor/ once upstream publishes 1.1.0.
valentinamgiusti
force-pushed
the
c2pa-merkle-validation
branch
from
July 16, 2026 14:29
671c700 to
a728de9
Compare
Move parseExclusions to bmff/, and toUint8Array, asInteger, matchesUuid, JUMBF_UUID to utils so upcoming Merkle validation can reuse them. Signed-off-by: Valentina <valentina.giusti@qualabs.com>
validateC2paMerkleSegment verifies fragmented MP4 media segments against the init manifest merkle maps (C2PA §15.12.2 / §18.6): parses the auxiliary uuid box (spec 2.4 and c2pa-rs formats), recomputes the leaf hash, walks the Merkle proof, and enforces location continuity via caller-held state. Adds status codes assertion.bmffHash.malformed / .mismatch. Signed-off-by: Valentina <valentina.giusti@qualabs.com>
validateC2paInitSegment reads the merkle field of the c2pa.hash.bmff.v3 assertion, validates each entry's initHash binding against the raw init bytes, and returns the maps as merkleMaps. VOD Merkle streams no longer report SESSIONKEY_INVALID. Signed-off-by: Valentina <valentina.giusti@qualabs.com>
Changelog, version bump, and API report for VOD Merkle validation. Signed-off-by: Valentina <valentina.giusti@qualabs.com>
Signed-off-by: Valentina <valentina.giusti@qualabs.com>
Signed-off-by: Valentina <valentina.giusti@qualabs.com>
Signed-off-by: Valentina <valentina.giusti@qualabs.com>
valentinamgiusti
force-pushed
the
c2pa-merkle-validation
branch
from
July 17, 2026 15:41
6acec11 to
5fd7854
Compare
…consumed hashes Bound the proof path against the committed manifest row instead of the full tree depth, and reject inputs that leave unconsumed proof hashes. Addresses review feedback on streaming-video-technology-alliance#378/streaming-video-technology-alliance#379. Signed-off-by: Valentina <valentina.giusti@qualabs.com>
Only advance per-track location state when the location continuity check also passes, not just the Merkle proof. Addresses review feedback on streaming-video-technology-alliance#379. Signed-off-by: Valentina <valentina.giusti@qualabs.com>
valentinamgiusti
force-pushed
the
c2pa-merkle-validation
branch
from
July 20, 2026 14:27
31a2ac5 to
838effc
Compare
Per §A.5.1.2/A.5.4.1.4, the auxiliary 'uuid' C2PA box for merkle data is the same ContentProvenanceBox used for the manifest box (version/ flags + null-terminated box_purpose + raw data), not a second box type with a bare CBOR map. Remove the fabricated MERKLE_AUX_UUID and its bare-CBOR-map parser, and the invented integer-key path in bmff-merkle-map (the CDDL only defines string keys). Addresses review feedback on streaming-video-technology-alliance#378. Signed-off-by: Valentina <valentina.giusti@qualabs.com>
§A.5.4.1.4 requires padding auxiliary uuid boxes to a fixed size when a Merkle tree has more than one. cbor-x's decode() throws on trailing bytes, so a spec-conforming padded box was flagged malformed. Decode via decodeMultiple and stop after the first item instead. Addresses review feedback on streaming-video-technology-alliance#378. Signed-off-by: Valentina <valentina.giusti@qualabs.com>
…ssing Per the merkle-map CDDL, initHash is required for fragmented assets (the only case validateMerkleMaps handles), and alg has no default when absent from both the entry and the enclosing assertion. Both were previously tolerated silently: a missing initHash left the init segment unbound, and a missing alg defaulted to SHA-256. Addresses review feedback on streaming-video-technology-alliance#378. Signed-off-by: Valentina <valentina.giusti@qualabs.com>
BmffHashExclusion/BmffHashConstraint were already @public but not exported from the barrel, so MerkleMap.exclusions duplicated their shape inline. Export them and reuse the type to avoid drift. No runtime or shape change. Addresses review feedback on streaming-video-technology-alliance#378. Signed-off-by: Valentina <valentina.giusti@qualabs.com>
Rename MerkleSegmentState.lastLocation to lastLocations (it's a map of per-track locations) and document the uniqueId:localId key format and the leniency on the first location seen per track. Also document that VOD Merkle reuses livevideo.assertion.invalid for per-track location discontinuity, which its §19.7.2 doc comment didn't cover. Addresses review feedback on streaming-video-technology-alliance#378. Signed-off-by: Valentina <valentina.giusti@qualabs.com>
C2PA 2.3, which this package targets, has no Annex D — the failure code table lives at §15.2.2.3. Fix the citation on both BMFF hash codes, add them to results-and-error-codes.md, and note the SESSIONKEY_INVALID behavior change for VOD Merkle streams. Addresses review feedback on streaming-video-technology-alliance#378. Signed-off-by: Valentina <valentina.giusti@qualabs.com>
The only caller of this validator (SegmentRouter in the toolkit) already gates on merkleMaps.length > 0 before invoking it, so the null return for the empty case was dead weight at every call site. Document the precondition instead. Addresses review feedback on streaming-video-technology-alliance#378. BREAKING CHANGE: validateC2paMerkleSegment no longer returns null. Callers that branched on a null result should remove that check. Signed-off-by: Valentina <valentina.giusti@qualabs.com>
Freezing the location baseline on any discontinuity (to stop a replay from laundering itself as the new baseline) had a worse side effect: one out-of-order or dropped segment left every later segment flagged relative to a stale baseline for the rest of the session, even when those later segments were sequential among themselves. Advance the baseline on any segment whose Merkle proof verifies, discontinuous or not — it's genuinely signed content, so a forward gap resyncs immediately instead of cascading. A replayed/reordered segment is still flagged invalid at the point it occurs. Signed-off-by: Valentina <valentina.giusti@qualabs.com>
…reaming-video-technology-alliance#380) C2PA 2.4 §18.6.2 defines the v2/v3 flat hash input as offset || data for every non-excluded root box; the offset is only omitted for Merkle tree hashes when the bmff-hash-map carries both hash and merkle. c2pa-rs verifies strictly this way, and both repo fixtures were signed with prefixed hashes, so the unprefixed fallback in the init path only admitted non-compliant signers and dropped the box-position binding the spec calls out. validateBmffHashAssertion now computes the hash with offsetPrefixSize: 8 instead of trying both modes. The dual-mode validateBmffHash helper is unchanged for the VSI (§19.7.3) and manifest-box (§19.7.2) live paths, where the draft live workflow is the documented reason for the leniency. Signed-off-by: Casey Occhialini <1508707+littlespex@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…hash (streaming-video-technology-alliance#381) validateC2paManifestBoxSegment verified the flat c2pa.hash.bmff.v3 assertion hash of a §19.7.2 live segment through the lenient dual-mode validateBmffHash helper, which accepts either the 8-byte box-offset prefix or no prefix. C2PA 2.4 §18.6.2 mandates the offset prefix for flat v2/v3 assertion hashes; only Merkle tree hashes may omit it. Compute the hash strictly with offsetPrefixSize: 8 and compare, matching c2pa-rs and the init-segment change from PR streaming-video-technology-alliance#380. The dual-mode helper's interoperability rationale applies to VSI-map-sourced hashes (§19.7.3), not §18.6.2 assertion hashes, so the VSI path keeps dual-mode validation and the helper doc now scopes the leniency accordingly. Regression tests mirror PR streaming-video-technology-alliance#380: prefixed flat hash accepted, unprefixed flat hash rejected (fails on the previous code), and the real signed test-segment.m4s fixture end-to-end, which confirms the fixture hash is offset-prefixed. Signed-off-by: Casey Occhialini <1508707+littlespex@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Resolves conflicts with the flat v3 offset-prefix fixes (streaming-video-technology-alliance#380, streaming-video-technology-alliance#381): strict offsetPrefixSize 8 flat-hash validation is kept alongside the merkle integration, and the two Fixed entries are folded into the 1.1.0 changelog section this PR cuts. Signed-off-by: Casey Occhialini <1508707+littlespex@users.noreply.github.com>
…tion readAuxPayload can only return a payload or 'other' since 5046af2 removed the bare-CBOR-map parser that used to signal 'malformed'; the flag was always false. Simplify extractMerkleAuxBoxes to return just the payloads array, and note the unplumbed claim-level alg fallback in validateMerkleMaps. Addresses review feedback on streaming-video-technology-alliance#378. Signed-off-by: Valentina <valentina.giusti@qualabs.com>
…o-technology-alliance#383) Extending a bare web platform global (TransformStream, ReadableStream) in a class heritage clause reads it at module evaluation. Bundlers treat unknown global reads as potential side effects, so consumers importing only unrelated exports still shipped the class (Rollup default treeshake.unknownGlobalSideEffects retains it; TransformStream is not in Rollup's known-globals list). After downstream transpile + minify the retained class collapses to a bare 'TransformStream;' statement that throws ReferenceError at script evaluation on runtimes without the global: Chrome < 67, Safari < 14.1, Firefox < 102, Node < 18. Resolve the base classes through guarded local bindings instead: typeof on an undeclared identifier never throws, and a /* @__PURE__ */ IIFE lets bundlers drop the statement entirely when the stream class is unused. The guard deliberately avoids globalThis, which is itself missing on Chrome < 71. On runtimes without the Web Streams API, module import now succeeds and instantiation throws a descriptive error. Subclass semantics and the declared type surface are unchanged. Apply the same treatment to the other module-scope platform-global reads found by a monorepo sweep (bare-import Rollup probe per package): - webvtt: WebVttTransformStream extends TransformStream (tree-shaking break and legacy import crash; reported by a downstream consumer) - iso-bmff: IsoBoxReadableStream extends ReadableStream (legacy import crash; ReadableStream happens to be in Rollup's known-globals list) - c2pa: top-level new TextDecoder() x3 and new TextEncoder().encode() retained in consumer bundles (annotation only) - drm: top-level guarded MediaKeys.isTypeSupported probe retained (annotation only) Verified with Rollup 4.52.4 at default treeshake settings: importing only parseWebVtt from the built webvtt dist retains zero TransformStream references (previously the full class chain), and a bare import of the webvtt and iso-bmff bundles retains nothing. New environment tests cover import and instantiation without the streams globals. Fixes streaming-video-technology-alliance#382 Signed-off-by: Casey Occhialini <1508707+littlespex@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…reaming-video-technology-alliance#384) * fix(cmcd): mark module-scope key tables side-effect free Top-level `new Set(...)`/`new Map(...)` with arguments and the CmcdReporter state-field tables (`Array.from` + `new Map(...map())`) run at module evaluation. Bundlers cannot prove argful collection constructors pure (Rollup's default treeshake only models the argless forms), so a consumer importing anything from the package retained ~4.9KB of eagerly-built key tables plus the constants they reference. Annotate the initializers with /* @__PURE__ */ so bundlers drop them when the functions using them are unused. STATE_FIELDS_BY_EVENT's inner `.map()` call needs its own annotation: a pure annotation covers the call itself, not argument evaluation, and the unannotated argument anchored the whole state-field chain in the bundle. Verified with Rollup 4.52.5 at default treeshake settings: a bare import of the built dist now retains nothing beyond bare external import lines (previously 4,882 bytes). All 341 cmcd tests pass. Follow-up to the module-scope side-effect audit in streaming-video-technology-alliance#382. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Casey Occhialini <1508707+littlespex@users.noreply.github.com> * fix(c2pa): mark bytesToHex's hex lookup table side-effect free The module-scope HEX_TABLE (`Array.from({ length: 256 }, ...)`) runs at module evaluation, so bundlers retained the call in every consumer bundle regardless of whether bytesToHex is used. Annotate the initializer with /* @__PURE__ */ so it is dropped when unused. Verified with Rollup 4.52.5 at default treeshake settings: a bare import of the built dist no longer retains the Array.from call. The remaining TextDecoder/TextEncoder retentions in that probe are addressed separately by streaming-video-technology-alliance#383. All 127 c2pa tests pass. Follow-up to the module-scope side-effect audit in streaming-video-technology-alliance#382. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Casey Occhialini <1508707+littlespex@users.noreply.github.com> * fix(drm): build sample-entry tables without import-time side effects ensureEncryptedInit built its reader/writer tables with module-scope forEach loops that call the @svta/cml-iso-bmff sample-entry factories at import time. Bundlers must retain calls into an external module, so every drm consumer shipped the audio/visual sample-entry factory chain even when ensureEncryptedInit was unused. createAudioSampleEntryReader and createVisualSampleEntryReader are pure factories (each closes over the type and returns a reader; no shared registry mutation), so the tables can be built inside /* @__PURE__ */ IIFEs instead: same objects and iteration order, but droppable when unused. Verified with Rollup 4.52.5 at default treeshake settings: a bare import of the built dist retains no ensureEncryptedInit code and no named iso-bmff factory imports. The remaining MediaKeys probe retention is addressed separately by streaming-video-technology-alliance#383. All 47 drm tests pass. Follow-up to the module-scope side-effect audit in streaming-video-technology-alliance#382. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Casey Occhialini <1508707+littlespex@users.noreply.github.com> * docs(drm): scope changelog entry to bundler-facing effect The entry implied importing the package no longer builds the sample-entry tables. The IIFEs still execute at module evaluation for non-bundled consumers; the improvement is that bundlers can now drop the pure-annotated initializers (and their iso-bmff factory imports) when ensureEncryptedInit is unused. Say only that. Addresses PR streaming-video-technology-alliance#384 review feedback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Casey Occhialini <1508707+littlespex@users.noreply.github.com> --------- Signed-off-by: Casey Occhialini <1508707+littlespex@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Removed unreleased version section and added details for version 1.1.0. Signed-off-by: Casey Occhialini <1508707+littlespex@users.noreply.github.com>
Version bumps go through a separate release script, not feature PRs. Addresses review feedback on streaming-video-technology-alliance#378/streaming-video-technology-alliance#379. Signed-off-by: Valentina <valentina.giusti@qualabs.com>
…n permissions (streaming-video-technology-alliance#386) Signed-off-by: Casey Occhialini <1508707+littlespex@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
The README still described only the two segment-level validation methods and didn't link a dedicated guide for VOD Merkle, unlike VSI and Manifest Box. Add docs/merkle-validation.md following the same structure, and update the README's method list and doc links. Addresses review feedback on streaming-video-technology-alliance#378. Signed-off-by: Valentina <valentina.giusti@qualabs.com>
…echnology-alliance#387) Bump @microsoft/api-extractor 7.53.1 -> 7.58.12 (drops lodash entirely, pins patched minimatch 10.2.3) and eslint-plugin-tsdoc 0.4.0 -> 0.5.2 (moves to @microsoft/tsdoc-config 0.18.1 with patched ajv). All other fixes are lockfile-only updates within existing semver ranges. Fixes Dependabot alerts (all development-scope, package-lock.json): - js-yaml 4.1.0 -> 4.3.0 (streaming-video-technology-alliance#72 high, streaming-video-technology-alliance#69 medium, #31 medium) - brace-expansion 1.1.12 -> 1.1.16, 2.0.2 -> 2.1.2 (streaming-video-technology-alliance#70, streaming-video-technology-alliance#71 high) - ws 7.5.10 -> 7.5.13 (streaming-video-technology-alliance#66 high) - markdown-it 14.1.0 -> 14.3.0 (streaming-video-technology-alliance#65 medium) - lodash removed from tree (streaming-video-technology-alliance#63 high, streaming-video-technology-alliance#62 medium, #34 medium) - defu 6.1.4 -> 6.1.7 (streaming-video-technology-alliance#61 high) - picomatch 2.3.1 -> 2.3.2, 4.0.3 -> 4.0.5 (streaming-video-technology-alliance#58, streaming-video-technology-alliance#56 medium) - flatted 3.3.3 -> 3.4.2 (#54 high) - minimatch 3.1.2 -> 3.1.5, 9.0.5 -> 9.0.9, 10.0.3 -> 10.2.3 (#51, #49, #46 high) - rollup 4.52.5 -> 4.62.2 (#48 high) - koa 2.16.3 -> 2.16.4 (#47 high) - min-document 2.19.0 -> 2.19.2 (#29 low) Also clears remaining npm audit findings not yet surfaced as alerts: ajv ReDoS (GHSA-2g4f-4pwh-qvx6), diff DoS (GHSA-73rr-hh4g-fpgx), yaml stack overflow (GHSA-48c2-rrv3-qjmp). npm audit now reports 0 vulnerabilities. Signed-off-by: Casey Occhialini <1508707+littlespex@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
valentinamgiusti
added a commit
to qualabs/c2pa-live-video-toolkit
that referenced
this pull request
Jul 22, 2026
@svta/cml-c2pa 1.1.0 is now published upstream, so the CI workaround from qualabs/common-media-library#53 is no longer needed. Bump the dependency floor to ^1.1.0 in the three packages that need it, remove the vendored tarball and its CI install step, and drop the now-unused .gitignore exception. Signed-off-by: Valentina <valentina.giusti@qualabs.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds VOD Merkle validation (C2PA 2.4 §15.12.2.2 / §18.6) to
@svta/cml-c2pa, released as 1.1.0:validateC2paMerkleSegment— verifies fragmented MP4 media segments against the merkle maps committed in the init manifest: parses the auxiliary uuid box (both the spec §A.5.4 format and the c2pa-rs ContentProvenanceBox variant), recomputes the offset-prefixed leaf hash, walks the Merkle proof up to the committed row, and enforceslocationcontinuity via caller-held state.validateC2paInitSegmentnow extractsmerkleMapsfrom thec2pa.hash.bmff.v3assertion and validates each entry'sinitHashbinding against the raw init bytes. VOD Merkle streams no longer reportSESSIONKEY_INVALID.assertion.bmffHash.malformed/assertion.bmffHash.mismatch.parseExclusions, uuid/byte utils) with no behavior change for the existing VSI/PSM paths.Structured as 4 commits (helpers refactor → merkle module → init integration → release chore); each commit passes the full test suite on its own.
Testing
npm test --workspace=libs/c2pa), including new unit + proof-path suites undertest/merkle/.