Skip to content

feat: Implement live video section 19.4 — Verifiable Segment Info#2

Open
N1Knight wants to merge 38 commits into
feat/live-video-section-19-3from
feat/live-video-section-19-4
Open

feat: Implement live video section 19.4 — Verifiable Segment Info#2
N1Knight wants to merge 38 commits into
feat/live-video-section-19-3from
feat/live-video-section-19-4

Conversation

@N1Knight

@N1Knight N1Knight commented Mar 10, 2026

Copy link
Copy Markdown

Summary

⚠️ Depends on the §19.3 PR — please merge that one first. This PR adds only the section 19.4 work on top.

Implements C2PA Live Video §19.4 — Verifiable Segment Info (VSI), the second validation method for live video streams. Instead of embedding a full C2PA manifest in each segment, the init segment carries a c2pa.session-keys assertion with one or more session keys (COSE_Key / RFC 9052), and each media segment carries a COSE_Sign1_Tagged signature in an ISO BMFF emsg box.

This PR also wires up full VSI signing support using caller‑provided Ed25519 session keys, including incremental signing across process runs via resume_from_segment and CLI flags --session-key and --previous-segment.

Also implements §19.7.3 signerBinding verification: each session key's COSE_Sign1_Tagged signerBinding is now cryptographically verified against the manifest signer's end-entity X.509 certificate, ensuring session keys were issued by the same entity that signed the manifest.

High-Level Changes

  • VSI & Session Key Validation (sdk/src/live_video/): Core logic to validate VSI fields (sequenceNumber, bmffHash, signerBinding), enforce sequence bounds, and verify session keys (including mandatory kid checks).

  • Cryptographic Bindings (session_key_validation.rs): Implements signerBinding verification. Extracts the SPKI from the end-entity certificate (via x509-parser) and verifies the COSE_Sign1_Tagged signature produced by the session key.

  • New Assertions (sdk/src/assertions/session_keys.rs): Adds typed structs and serde support for the c2pa.session-keys assertion.

  • VSI signing from explicit session keys (sdk/src/live_video/vsi_signing.rs): Replaces the ephemeral key mode with LiveVideoVsiSigner::from_signing_key, which takes a caller‑provided Ed25519 session key and builds a spec‑compliant c2pa.session-keys assertion (including signerBinding) into the init segment.

  • Incremental VSI signing (resume_from_segment): Adds LiveVideoVsiSigner::resume_from_segment, which parses a previously signed VSI emsg box, reads the sequenceNumber, and sets next_sequence_number = sequenceNumber + 1 so segments can be signed incrementally across separate process runs.

  • CLI wiring for VSI (cli/src/live_video_sign.rs, cli/src/main.rs): The live-video-sign subcommand now requires --session-key when --method vsi, always re‑signs the init segment for VSI, and supports --previous-segment to resume the VSI sequence number from an earlier segment using the same session key.

Key design decisions

  • COSE_Key integer map keys: The c2pa.session-keys assertion stores keys as raw c2pa_cbor::Value with integer CBOR keys (RFC 9052 §7.1). The SDK's manifest read path transcodes CBOR through JSON, converting integer keys to text strings and byte values to integer arrays. cose_key.rs handles both representations transparently.

  • Session key resolution: validate_verifiable_segment_info finds the matching session key by comparing the kid in the COSE_Sign1 unprotected header against the keys registered via validate_session_keys.

  • signerBinding bytes extraction: The internal CBOR→JSON→CBOR roundtrip (via ManifestAssertion::as_json_object) loses the CBOR Bytes type. extract_signer_binding_bytes handles all resulting representations (Value::Bytes, Value::Text base64, Value::Array of integers) to support both direct CBOR access and the current JSON-transcoded path.

  • Certificate from manifest: The end-entity certificate for signerBinding verification is extracted from the manifest's own signature info (Manifest::signature_info().cert_chain()), requiring no additional CLI arguments.

  • bmffHash verification: The bmffHash field in the VSI SegmentInfoMap is deserialized into the existing BmffHash struct via c2pa_cbor::value::from_value. Verification reuses BmffHash::verify_in_memory_hash, which internally calls bmff_to_jumbf_exclusions to resolve /emsg xpath exclusions into byte ranges, then hashes the segment data excluding those ranges. When bmffHash is Null, verification is skipped for backward compatibility.

Test

  • cargo test --features="file_io" -p c2pa -- live_video

End to end test

The caller provides an Ed25519 session key (--session-key, raw 32-byte seed). The init segment carries a c2pa.session-keys assertion; each media segment receives a COSE_Sign1 in an emsg box signed with that session key.

--init and --session-key are both required for VSI.

Sign all segments at once

cargo run --bin c2patool -- live_video_demo/input live-video-sign \
  --segments_glob "seg_*.m4s" \
  --output live_video_demo/signed_vsi \
  --manifest live_video_demo/manifest.json \
  --init init.mp4 \
  --method vsi \
  --session-key live_video_demo/ed25519_session.key

Sign segments one at a time (simulating a live ingest pipeline)

First segment:

cargo run --bin c2patool -- live_video_demo/input live-video-sign \
  --segments_glob "seg_001.m4s" \
  --output live_video_demo/signed_vsi \
  --manifest live_video_demo/manifest.json \
  --init init.mp4 \
  --method vsi \
  --session-key live_video_demo/ed25519_session.key

Subsequent segments (use --previous-segment to resume the sequence number):

cargo run --bin c2patool -- live_video_demo/input live-video-sign \
  --segments_glob "seg_002.m4s" \
  --output live_video_demo/signed_vsi \
  --manifest live_video_demo/manifest.json \
  --init init.mp4 \
  --method vsi \
  --session-key live_video_demo/ed25519_session.key \
  --previous-segment live_video_demo/signed_vsi/seg_001.m4s

Verify all segments

The validator auto-detects the method from the init segment manifest
(c2pa.session-keys assertion):

cargo run --bin c2patool -- live_video_demo/signed_vsi/init.mp4 live-video \
  --segments_glob "seg_*.m4s"

Verify a single segment

cargo run --bin c2patool -- live_video_demo/signed_vsi/init.mp4 live-video \
  --segments_glob "seg_001.m4s"

Spec references

Section Title
§18.25 c2pa.session-keys assertion
§19.4 Verifiable Segment Info
§19.7.3 Verifiable Segment Info Validation
RFC 9052 §7.1 COSE_Key map parameter labels

N1Knight added 16 commits March 10, 2026 11:20
…pture

- Populate `bmffHash` in `SegmentInfoMap` using SHA-256 over the segment
  data, excluding VSI emsg boxes (offset=12 per BMFF FullBox layout)
- Capture `manifestId` from the signed init segment via `Reader::from_stream`
  and embed it in subsequent media segments per §19.4
- Add regression tests: bmffHash non-null, manifestId empty without init,
  manifestId populated after signing init, and c2pa.hash.bmff.v3 label guard
Add `restore_manifest_id_from_signed_init` to `LiveVideoVsiSigner` and
update the CLI to skip init re-signing when `--previous-segment` is
provided, preserving a consistent `manifestId` across all segments per §19.4.
Per RFC 9052 §4.4 and C2PA §18.25.2, the signer's EE certificate is the
detached payload of the signerBinding COSE_Sign1, not the AAD. Replace
tbs_data(cert) with tbs_detached_data(cert, b"") in both build_signer_binding
and verify_signer_binding so the Sig_Structure is spec-compliant.
…_Key

- Serialize signerBinding as CBOR tag 18 (COSE_Sign1_Tagged) instead of
  wrapping the tagged bytes in a bstr, matching the C2PA spec §18.25.2
  and Unified Streaming's implementation.
- Add field 3 (alg = -8, EdDSA) to the Ed25519 COSE_Key per RFC 9052.
- Add Builder.add_assertion_cbor() to bypass the to_value() pipeline
  which drops CBOR tags through ValueSerializer.
- Add tests for tag 18 wire format, detached payload, protected header
  algorithm, COSE_Key alg field, and wrong EE cert rejection.
N1Knight pushed a commit that referenced this pull request Apr 1, 2026
* FLAC support

* Moved Architecture.md to docs folder

* Update links in Architecture.md to correct paths

* Add the correct flac sample

* Mark test-only methods as test-only

Move the flac asset-handler documentation into docs/formats folder.

* refactor: Extract shared ID3v2 logic into `id3_helper` module (#2)

* Refactor common code between mp3 and flac

* Add more tests for mp3_io

* Rename id3_audio to id3_helper

---------

Co-authored-by: Gopalakrishna Sharma <1406458+mpgopala@users.noreply.github.com>

* Update flac.md with id3_helper details

* More refactoring

* Fix clippy and fmt errors

* Fix clippy errors (Attempt 2)

* Fix fmt errors (Attempt 2)

* Review comments

1. Remove dependency on metaflac as it was validating the entire flac file. Validating only the initial header is enough.
2. Remove Architecture.md. Will add it in a separate PR.
3. Error handling.

* Review comments

1. Remove dependency on metaflac as it was validating the entire flac file. Validating only the initial header is enough.
2. Remove Architecture.md. Will add it in a separate PR.
3. Error handling.

* Format fixes

* Return OK if the stream has TooManyManifestStores from add_required_frame

* Update rustls-webpki version to 0.103.10 to fix vulnerability audit failure

* A proper upgrade for rustls-webpki

* Removing testing details

Removing testing details as it already exists in a single file (flac_io.rs).
crandmck and others added 13 commits April 1, 2026 16:26
…tentauth#1899)

* docs: Combine docs on Context and Settings to reduce duplication

* Remove file that was deleted in main

* Add links to REDME

* polish edits

* Comments from Nick

* Remove toml examples, other small edits
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](actions/download-artifact@v7...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
dependabot Bot and others added 9 commits April 1, 2026 16:30
…ntentauth#1913)

Bumps [svenstaro/upload-release-action](https://github.com/svenstaro/upload-release-action) from 2.11.3 to 2.11.4.
- [Release notes](https://github.com/svenstaro/upload-release-action/releases)
- [Changelog](https://github.com/svenstaro/upload-release-action/blob/master/CHANGELOG.md)
- [Commits](svenstaro/upload-release-action@2.11.3...2.11.4)

---
updated-dependencies:
- dependency-name: svenstaro/upload-release-action
  dependency-version: 2.11.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…pture

- Populate `bmffHash` in `SegmentInfoMap` using SHA-256 over the segment
  data, excluding VSI emsg boxes (offset=12 per BMFF FullBox layout)
- Capture `manifestId` from the signed init segment via `Reader::from_stream`
  and embed it in subsequent media segments per §19.4
- Add regression tests: bmffHash non-null, manifestId empty without init,
  manifestId populated after signing init, and c2pa.hash.bmff.v3 label guard
Add `restore_manifest_id_from_signed_init` to `LiveVideoVsiSigner` and
update the CLI to skip init re-signing when `--previous-segment` is
provided, preserving a consistent `manifestId` across all segments per §19.4.
Per RFC 9052 §4.4 and C2PA §18.25.2, the signer's EE certificate is the
detached payload of the signerBinding COSE_Sign1, not the AAD. Replace
tbs_data(cert) with tbs_detached_data(cert, b"") in both build_signer_binding
and verify_signer_binding so the Sig_Structure is spec-compliant.
…_Key

- Serialize signerBinding as CBOR tag 18 (COSE_Sign1_Tagged) instead of
  wrapping the tagged bytes in a bstr, matching the C2PA spec §18.25.2
  and Unified Streaming's implementation.
- Add field 3 (alg = -8, EdDSA) to the Ed25519 COSE_Key per RFC 9052.
- Add Builder.add_assertion_cbor() to bypass the to_value() pipeline
  which drops CBOR tags through ValueSerializer.
- Add tests for tag 18 wire format, detached payload, protected header
  algorithm, COSE_Key alg field, and wrong EE cert rejection.
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.

3 participants