Skip to content

feat(c2pa): add native C2PA §19.3/§19.4 provenance validation for live streams - #164

Open
valentinamgiusti wants to merge 44 commits into
developmentfrom
feature/c2pa-cml-validation
Open

feat(c2pa): add native C2PA §19.3/§19.4 provenance validation for live streams#164
valentinamgiusti wants to merge 44 commits into
developmentfrom
feature/c2pa-cml-validation

Conversation

@valentinamgiusti

Copy link
Copy Markdown
Collaborator

Summary

  • Adds native, opt-in per-segment C2PA provenance validation for live CMAF/DASH
    streams, covering both live signing methods from C2PA 2.3 §19:
    §19.3 Manifest Box (per-segment uuid box, c2pa.hash.bmff.v3) and
    §19.4 VSI (emsg COSE_Sign1, session keys in the init segment)
  • Off by default: while disabled, no segment is parsed and the validation
    engine (@svta/cml-c2pa) is never imported (dynamic import, code-split
    out of both default bundles)
  • Classifies each track from its init segment, validates each media segment
    as it's fetched (without touching the fetch-to-MSE path), and emits a
    result record per segment through the public event system
  • In auto mode, classifies segments independently by their ISO-BMFF boxes
    when the init segment was missed or ambiguous
player.updateSettings({
    streaming: {
        c2pa: { enabled: true, method: 'auto' }  // 'auto' | '19.3' | '19.4'
    }
});

New files

  • src/streaming/c2pa/C2paController.js — owns the scanning lifecycle,
    created during MediaPlayer.initialize()
  • src/streaming/c2pa/C2paScanner.js — thin adapter over the existing
    response interceptor API
  • src/streaming/c2pa/C2paValidationCoordinator.js — per-track state,
    classification and validation orchestration
  • src/streaming/c2pa/C2paOptions.js — settings normalization/validation
  • src/streaming/c2pa/C2paEvents.js — event/payload typedefs
  • src/streaming/c2pa/detection/BoxParsingDetector.js,
    detection/C2paDetector.js — swappable auto-mode detection strategy
  • src/streaming/c2pa/README.md — architecture, settings, events and
    continuity semantics
  • samples/c2pa/index.html — per-segment ✅ / ❌ / ⚠ provenance grid
  • Unit tests for all of the above under
    test/unit/test/streaming/streaming.c2pa.*

Modified files

  • Settings.js / CoreEvents.jsstreaming.c2pa settings
    (enabled/method/mediaTypes) and runtime-toggle event
  • MediaPlayer.js / MediaPlayerEvents.js — wiring and the three public
    events (C2PA_INIT_PROCESSED, C2PA_SEGMENT_VALIDATED, C2PA_ERROR)
  • IsoBox.js — expose the uuid box's usertype for C2PA detection
  • samples/samples.json — registers the new sample
  • index.d.ts — TypeScript definitions
  • package.json / package-lock.json — adds @svta/cml-c2pa (optional
    dependency)

N1Knight and others added 30 commits July 10, 2026 15:40
…for ManifestBox segments

An unrecognized continuityMethod and a broken manifest-id chain both
surfaced as continuityInvalid, even though the CML engine always pairs
CONTINUITY_METHOD_UNSUPPORTED with CONTINUITY_METHOD_INVALID for the
former. Split them so the sample can render unsupported as a distinct
warning instead of an error.
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

valentinamgiusti and others added 2 commits August 4, 2026 11:06
…essage

- Empty URL field by default, with two live example buttons (§19.4 VSI /
  §19.3 Manifest Box) that fill the URL and load immediately
- Match the repo's sample-side-panel convention instead of custom CSS
- Clarify the continuityUnsupported details message

@N1Knight N1Knight left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed against the dash.js contribution rules (CONTRIBUTING.md, AGENTS.md, FactoryMaker/context DI, Settings.js + index.d.ts parity, samples.json registration, BSD-3 headers) and against clean-code/SOLID criteria. I did not review the C2PA cryptography itself — that is correctly delegated to @svta/cml-c2pa.

What is already right: every new file carries the BSD-3 header, all modules follow the closure-factory + context pattern, the settings are added to both Settings.js and index.d.ts, the sample is registered in samples.json, the test files follow the streaming.c2pa.* mirror naming, npx eslint src/streaming/c2pa test/unit/test/streaming/streaming.c2pa.*.js is clean, and the 58 new unit tests pass. Nice use of the public response-interceptor API instead of patching the fetch-to-MSE path, and the C2paDetector strategy seam is the right call.

Detailed comments inline. Grouping them by weight:

Must fix before merge

  1. _checkSequence builds an unbounded array from a sequence number taken from a segment that may have failed validation — a forged sequenceNumber freezes the tab. This is the one finding I'd call a blocker, because it is triggerable by exactly the attacker this feature exists to detect.
  2. C2PA state is never reset on source change, and trackKey is derived from the URL filename, so two streams whose segments are both named chunk-stream0-*.m4s share state (stale session keys, stale lastManifestId, stale sequence number).
  3. Two docstrings describe wiring that does not exist (reset() "on source change", resetSequenceForTrack() "on seek / period change").

Should fix
4. Silent failure modes: a failed engine import, a swallowed interceptor error and a swallowed box-parse error all produce zero diagnostics, and none of the five new modules uses the dash.js Debug/logger that 14 of 20 streaming/controllers/ modules use.
5. optionalDependencies + a literal specifier in import() — please confirm npm ci --omit=optional && npm run build still succeeds.
6. Dead code: C2paEvents.js has no importers anywhere, and C2paOptions' normalization API has no production callers (only tests), while its stated job is duplicated inline in two modules.

Design / nits
7. C2paValidationCoordinator at 614 lines carries six responsibilities; extracting a sequence tracker and a record factory would also remove five near-duplicate record literals and the parameter mutation.
8. Segment bytes are copied two to three times per media segment.
9. ADR-0002 and 12 AC#NN references point at documents that are not in this repo.

None of this is structural rework — the module boundaries are sound.

Comment thread src/streaming/c2pa/C2paValidationCoordinator.js Outdated
Comment thread src/streaming/c2pa/C2paValidationCoordinator.js Outdated
Comment thread src/streaming/c2pa/C2paValidationCoordinator.js
Comment thread src/streaming/c2pa/C2paValidationCoordinator.js
Comment thread src/streaming/c2pa/C2paValidationCoordinator.js
Comment thread src/streaming/MediaPlayer.js
Comment thread package.json Outdated
Comment thread samples/c2pa/index.html
Comment thread samples/c2pa/index.html
Comment thread src/streaming/vo/IsoBox.js
Must-fix:
- Cap the sequence gap loop and only sequence-check valid records, so a
  segment that fails validation can't claim a forged sequenceNumber and
  trigger an unbounded loop/OOM
- Stop mutating the emitted record in place
- Reset per-track C2PA state on attachSource() via a new
  C2paController.resetForNewSource(), and clear initPromises in reset()
  (two different streams can otherwise share a filename-derived trackKey)
- Wire PLAYBACK_SEEKED/PERIOD_SWITCH_COMPLETED to reset active tracks'
  sequence state, matching what the docstrings already claimed

Should-fix:
- Add Debug/logger to the three previously-silent catches
- Emit C2PA_ERROR (c2pa.engineUnavailable) once per session if the
  validation engine fails to load, instead of degrading silently forever
- Document as a known limitation that a malformed init segment reads the
  same as a never-signed one (the engine has no stable, documented way to
  tell them apart, only an unstable exception message)
- Move @svta/cml-c2pa from optionalDependencies to dependencies: the
  dynamic import() uses a literal specifier, so webpack resolves it at
  build time regardless, and npm ci --omit=optional broke the build
- Wire normalizeC2paOptions as the actual single normalization point
  instead of three ad-hoc reimplementations; drop the now-dead
  isValidC2paMethod/getDefaultC2paOptions non-usage; trim C2paEvents.js
  to its typedefs (the runtime object had no importers)

Nits:
- Fix _copySegmentBytes and BoxParsingDetector._toArrayBuffer to copy the
  segment bytes once instead of two or three times
- Remove dangling AC#NN / ADR-0002 references to documents not in this repo
- Move SEGMENT_KIND_INIT/MEDIA to C2paOptions.js so the scanner and
  coordinator share one definition of their contract
- Drop defensive typeof settings.get checks not used anywhere else in the
  codebase; use Constants.VIDEO/AUDIO instead of raw string literals
- Fix _resolveMediaMethod: a track already classified from its init now
  trusts that classification instead of re-parsing every media segment
  through the detector, matching the documented "auto" behavior
A chip is appended per segment and never pruned; on a long-running live
stream that's thousands of nodes each holding a closure over its record.
…ilding

The coordinator owned six responsibilities in one 614-line closure; pull
out the two pieces that are genuinely independent of validation:

- C2paSequenceTracker: the signed-sequence-number bookkeeping (replay /
  reorder / gap detection), with its own test file. This is where the
  DoS fix lives, so it's worth testing directly rather than only through
  the coordinator.
- _createSegmentRecord: a single default SegmentRecord shape used by all
  five record builders instead of each repeating the same 11 fields, with
  an injectable clock (config.now) so tests can assert timestamps.
Matches protectionController/metricsReportingController, which are both
reset and nulled; c2paController was only reset. Harmless in practice
(it's a singleton re-fetched on the next initialize()), but the
asymmetry was worth removing.
Video and audio chips were interleaved in one row with the media type
only in the tooltip, so a track validating both read as a jumbled
sequence.
@valentinamgiusti

valentinamgiusti commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed all three must-fix items, all three should-fix items, and all the design/nits, across several commits.

Must fix

  • Blocker (unbounded gap loop / OOM): fixed. Sequence-checking now only runs on a valid record, and the reported gap is capped at 100 (a larger gap emits one bounded record with missingCount instead of enumerating it). Added a regression test with a forged sequenceNumber: 1_000_000_000 on a failed record.
  • Record mutation / status precedence: fixed. No longer mutates the record in place, and since sequence-checking now only runs on valid records, invalid can no longer be silently overwritten by replayed/reordered.
  • State leak on attachSource() / trackKey collision: fixed. Added C2paController.resetForNewSource(), wired into MediaPlayer._resetPlaybackControllers(). Also fixed initPromises not being cleared in reset().

Should fix

  • Stale docstrings: fixed for real. PLAYBACK_SEEKED/PERIOD_SWITCH_COMPLETED are now wired to reset active tracks' sequence state, matching what the docstrings already claimed.
  • Silent failures: added Debug/logger to the three previously-silent catches, and a one-time C2PA_ERROR (c2pa.engineUnavailable) when the engine fails to load.
  • Absent vs malformed init: confirmed the engine throws different messages for each case but exposes no stable/documented way to tell them apart. Documented it as a known threat-model limitation in README.md instead of matching an undocumented exception string.
  • optionalDependencies build break: confirmed it for real (removed the package from node_modules, ran the build, got Module not found). Moved @svta/cml-c2pa to dependencies.
  • Dead code: wired normalizeC2paOptions as the actual single normalization point. Trimmed C2paEvents.js to its typedefs.

Design / nits

  • Extracted C2paSequenceTracker out of the coordinator into its own module with its own test file (9 tests): it's where the DoS fix lives, so it's worth testing in isolation.
  • Consolidated the five duplicated record literals into one _createSegmentRecord helper with an injectable clock.
  • Fixed the double/triple segment-byte copy in the scanner and detector.
  • Removed the dangling AC#NN/ADR-0002 references.
  • Moved SEGMENT_KIND_INIT/MEDIA to C2paOptions.js so both sides share one definition.
  • Dropped the typeof settings.get guards and switched to Constants.VIDEO/AUDIO.
  • Fixed _resolveMediaMethod: it was always consulting the detector even with a known init classification, contradicting its own docstring and the PR description. Now trusts the init classification first.
  • protectionController/c2paController null-symmetry in MediaPlayer.js: nulled it too, matching the other two controllers.
  • Sample: capped the chip grid at 300 nodes and split it into separate video/audio rows so an interleaved track reads clearly. Kept the §19.3 bucket URL button; it's live (segments near the live edge respond 200; my earlier read of it as dead was from testing stale, already-rotated-out segment numbers).

On the IsoBox.js comment (no test verifying usertype survives the real parse path): I don't think this needs a new test. test/unit/test/streaming/streaming.c2pa.detection.BoxParsingDetector.js already uses the real BoxParser (not stubbed) and builds a real uuid box byte-by-byte; the assertion that detect() classifies it as §19.3 only passes if usertype survived that real parse. Happy to add a dedicated IsoBox.js test too if you'd still like one, but the seam itself is exercised today.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants