diff --git a/.github/workflows/wai-conformance.yml b/.github/workflows/wai-conformance.yml index de429a1be..423bb966c 100644 --- a/.github/workflows/wai-conformance.yml +++ b/.github/workflows/wai-conformance.yml @@ -106,6 +106,10 @@ on: # reports, live_ads_verify.py reads that table, and the registry's wire identifiers test # reads its sections. - 'wai/extensions/live-ads.md' + # tests/live_provenance.rs requires this extension's §12 to list the codes and the C2PA + # codes the reference reports, c2pa_live_verify.py reads that table, and the registry's + # wire identifiers test reads its sections. + - 'wai/extensions/live-provenance.md' - 'wai/wai-web/**' # The audio provenance check below reads the corpora above with this tool. - 'wai/tools/wai_audio_provenance_check.py' @@ -776,7 +780,8 @@ jobs: # live_metering_verify.py (the live-metering corpus), energy_steering_verify.py (the # energy-steering corpus), and live_moderation_verify.py (the live-moderation corpus), and # e2ee_verify.py (the end-to-end encrypted rooms corpus), live_watermark_verify.py (the - # forensic-watermark corpus) and live_ads_verify.py (the live-ads corpus). + # forensic-watermark corpus), live_ads_verify.py (the live-ads corpus) and + # c2pa_live_verify.py (the live-provenance corpus). # Each exits non-zero on any failure or any difference, and the step fails with it. # One platform: what they check is the reading of the text, not the architecture. independent-verifiers: @@ -1091,6 +1096,27 @@ jobs: (cd wai-rs && cargo test --no-default-features --features live_ads --lib wai_video_ssai::) (cd wai-rs && cargo test --no-default-features --features live_ads --test live_ads) python3 -I live-conformance/live_ads_verify.py live-conformance/ads + # wai.live.provenance (extensions/live-provenance.md): the reference module's unit tests + # (boxes, chunks, emsg and the BMFF hash; the JUMBF store; COSE_Sign1 and the OKP key; + # certificates written and read, their profile and chain; identifiers, track renderings, + # dates and the compositor's source type) and tests/live_provenance.rs (every file of the + # committed corpus regenerates byte for byte from its fixed seeds and the packaging + # corpus's AV1 floor, and no other file is there; wai-rs gives every vector its stated + # outcome; every reason of §12 refuses a vector; §12 lists exactly the reference's codes + # with the two C2PA codes it reports); then the corpus's second implementation, + # c2pa_live_verify.py, written from the extension's text and sharing no code with + # wai-rs, which reads CBOR, ISO BMFF, JUMBF, COSE and X.509 DER itself, verifies every + # signature with WAI's strict Ed25519 rule and every receipt with BLAKE3 from + # claim-conformance, reads the extension's reason table, and requires every mutated copy + # of a good vector to be refused; standard library only, run isolated (-I). + - name: live provenance · wai-rs and c2pa_live_verify.py + working-directory: wai + shell: bash + run: | + set -euo pipefail + (cd wai-rs && cargo test --no-default-features --features live_provenance --lib live_provenance::) + (cd wai-rs && cargo test --no-default-features --features live_provenance --test live_provenance) + python3 -I live-conformance/c2pa_live_verify.py live-conformance/c2pa-live # wai.live.e2ee (extensions/confidentiality.md): the corpus's second implementation, # e2ee_verify.py, written from the extension's text and the documents it reads and # sharing no code with the reference (joule-moq's secure_objects and joule-moq-mls, diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 4784bd07e..4cd3c90bd 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -6,6 +6,72 @@ wire format may change and such changes are recorded here. ## v0.4.0 — current +### `wai.live.provenance`: C2PA 2.4 live video provenance for room tracks and takes, tied to the JWP receipts + +- **The extension** ([live-provenance](extensions/live-provenance.md)) registers the record + `wai.live.provenance`: C2PA 2.4 §19's live video provenance, written exactly as §19 defines + it, for a room's CMAF tracks and a device's CMAF take. A room track packaged as CMAF travels + under `draft-ietf-moq-cmsf-01`, each MoQ group one segment and each object one CMAF chunk. + A segment is its C2PA boxes followed by its group's objects and nothing else, so the + segment's `c2pa.hash.bmff.v3` and the group's JWP receipt commit to the same chunks; the + segment's sequence number is its first `mfhd`'s, following from the receipts' leaf counts. +- **Both methods.** Per-segment C2PA Manifest Boxes: each manifest holds the hard binding, the + actions, `c2pa.livevideo.segment` (the stream identifier and the manifest before it) and a + group assertion naming its receipt by link hash and root. Verifiable Segment Info: an `emsg` + of version 0 per segment carrying a COSE_Sign1 by a session key over the segment-info map; + the session keys, OKP Ed25519 keys bound to the claim signer's certificate, are delivered in + the initialization manifest and rotated by a manifest in a segment, and the session key also + signs the group's receipts. A take uses both at once, its device's key the session key, and + its stream identifier its take id's, as capture-binding §11 had them. +- **The profile.** One manifest per store; EdDSA throughout, deterministic as C2PA §13.2.1 + recommends for streaming; an X.509 chain of version 3 Ed25519 certificates with + `c2pa-kp-claimSigning`; four whole-box exclusions in every hash; every manifest identifier + derived from the stream, the manifest's role and the segment's sequence number. Two readings + of C2PA 2.4 are stated: the VSI box's exclusion offset is 12, where `scheme_id_uri` begins, + since the 0 §19.4.1 prints compares a box's size; and a `signerBinding` is detached with a + `nil` payload, as §13.2.3 requires, where §18.25's example prints `h''`. +- **AI disclosure.** Every manifest carries the IPTC digital source type and one + `c2pa.ai-disclosure` per model that the stream's label gives, as timed-text §10.3 gives them + for captions; a compositor's output is `compositeCapture`, `composite` or + `compositeSynthetic` from its inputs' labels and carries their models' disclosures. A stream + labelled machine-made without the marking is refused. This is the machine-readable marking + Regulation (EU) 2024/1689 Article 50(2) describes, stated as an engineering requirement. +- **Dynamic and redundant packaging.** C2PA 2.4 treats content packaged on request as live + (§19.2.1) and recommends deterministic signatures for it, naming ISO/IEC 23009-9 (§13.2.1). + With derived identifiers, deterministic CBOR and Ed25519, every byte of a segment is a + function of its group's objects, the stream and the claim signer's key: packagers that share + a key write the same segment, and packagers that do not chain across a failover, their + segments' objects under the publisher's one receipt. Checking two encoders' outputs by + re-encoding under a `replicate` capability is stated as not in this revision. +- **Repository receipts** (C2PA 2.4 §18.27). A registered initialization segment keeps its + manifest box as `original` and ends with an `update` box: an update manifest whose one + ingredient is the parent, with the parent's validation results, and whose + `c2pa.repository-receipt` carries, as the repository's proof, an RFC 9942 receipt of + inclusion of the parent's manifest hash in the repository's RFC 9162 log, verified against + the repository's key. +- **Carriage.** Over HTTP, the segment files. Over MoQ, each segment's C2PA boxes ride a + provenance track, `_c2pa` (`packaging` `wai-c2pa`, named in `wai_c2pa_track`), whose + group *g* is segment *g*'s C2PA boxes, so the media objects stay the chunks a CMSF player plays + and the receipts commit to; the initialization segment rides the catalog's inline data. +- **Thirty-three reasons**, from `segment_malformed` to `segment_missing`, each with the C2PA + 2.4 §15 and §19.7 codes a C2PA validator reports for it, where C2PA checks it. +- **Reference and corpus.** `wai-rs` feature `live_provenance` (`src/live_provenance.rs` and + its `bmff`, `jumbf`, `cose`, `x509` and `corpus` modules), with no C2PA library and no new + dependency. `live-conformance/c2pa-live/`, written from fixed seeds by + `wai_live_provenance_vectors` over the packaging corpus's AV1 floor: a camera track under each + method (the session key rotated at its third segment), a track labelled machine-made, a + compositor's output, a take, a failover between two packagers and a registered stream; and + one negative per reason, among them a segment whose bytes + changed, a missing segment and a reordered one, a manifest bound to the wrong receipt, a + stream identifier that differs, an expired and a time-stamping end entity, and a machine-made + track without its disclosure. Checked by `tests/live_provenance.rs` and by + `live-conformance/c2pa_live_verify.py`, a second implementation that shares no code with + `wai-rs` and requires every mutated copy of a good vector to be refused. The corpus's + initialization segments, the registered one with its update manifest included, also validate + in the C2PA command-line tool; the tool opens no media segment (`c2pa-live/README.md`). The + registry lists the record and the catalog values `wai-c2pa` and `wai_c2pa_track`. +- **capture-binding §11** now points here for the C2PA side of a take, and is implemented. + ### `wai-ops/1-i32` on GPUs, and the tier where a run showed it - **`wai/wai-ops-gpu`**, an operator implementation of `wai-ops/1-i32` (integer-operators §9.1) in WGSL compute kernels through wgpu: Metal, Vulkan and DX12 natively, and a browser's WebGPU. WGSL has no 64-bit integer, so the kernels use 32-bit words only: diff --git a/wai/REGISTRY.md b/wai/REGISTRY.md index 5e28e271c..7066284de 100644 --- a/wai/REGISTRY.md +++ b/wai/REGISTRY.md @@ -42,7 +42,8 @@ Every registered string has one kind. `wai.video.manifest` and its siblings; the live records `wai.live.events`, `wai.live.room`, `wai.capture.take`, `wai.live.compose`, `wai.live.recording`, `wai.live.metering`, `wai.live.steering`, `wai.live.moderation`, `wai.live.e2ee`, - `wai.live.watermark` and `wai.live.ads`; timed text, `wai.text.timed`). + `wai.live.watermark`, `wai.live.ads` and `wai.live.provenance`; timed text, + `wai.text.timed`). It names no payload format. A derivation or record string is not a payload capability, so it is not @@ -202,6 +203,7 @@ and a role are fixed once registered. | `wai.live.events` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content | | `wai.live.metering` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content | | `wai.live.moderation` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content | +| `wai.live.provenance` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content | | `wai.live.recording` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content | | `wai.live.room` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content | | `wai.live.steering` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content | @@ -356,6 +358,7 @@ and a role are fixed once registered. | `urn:wai:event:1` | scheme | defined | live-events §8.4 | | `urn:wai:event:1` | catalog | defined | live-events §8.1 | | `wai` | catalog | defined | moq-streaming-format §3 | +| `wai-c2pa` | catalog | defined | live-provenance §10.2 | | `wai-key-status` | document-kind | defined | jwp-receipts §7.9 | | `wai-module-grant` | document-kind | defined | jwp-receipts §7.9 | | `wai-module-grants` | document-kind | defined | http-delivery §8.4 | @@ -366,6 +369,7 @@ and a role are fixed once registered. | `wai2` | codecs-prefix | defined | http-delivery §4.1 | | `wai2` | fourcc | provisional | http-delivery Appendix B | | `waiC` | fourcc | provisional | http-delivery Appendix B | +| `wai_c2pa_track` | catalog | defined | live-provenance §10.2 | | `wai_capabilities` | catalog | defined | moq-streaming-format §4 | | `wai_delivery_timeout_ms` | catalog | defined | packaging §8 | | `wai_determinism` | catalog | defined | moq-streaming-format §4 | diff --git a/wai/SPEC.md b/wai/SPEC.md index 2ba3f0bb1..14d644d45 100644 --- a/wai/SPEC.md +++ b/wai/SPEC.md @@ -594,8 +594,8 @@ kind: `wai.video.package` and `wai.video.channel`; and the live records `wai.live.events`, `wai.live.room`, `wai.capture.take`, `wai.live.compose`, `wai.live.recording`, `wai.live.metering`, `wai.live.steering`, `wai.live.moderation`, `wai.live.e2ee`, - `wai.live.watermark` and `wai.live.ads`, and timed text, `wai.text.timed`), which names no - payload format. + `wai.live.watermark`, `wai.live.ads` and `wai.live.provenance`, and timed text, + `wai.text.timed`), which names no payload format. Only payload, companion and refinement capabilities have a media class, a fidelity tier, a replicate class and a role. A derivation or record string is not a payload @@ -1071,6 +1071,28 @@ recomputes the fill and the asset list, refuses a second receipt of one session and holds the spend of every receipt together to the decider grant's ceiling: [`extensions/live-ads.md`](extensions/live-ads.md). +And for the **provenance** of a room's CMAF tracks and a device's CMAF take — +`wai.live.provenance`: the live video provenance of the C2PA Technical Specification, version +2.4, §19, written as it defines it. A room track packaged as CMAF travels under +`draft-ietf-moq-cmsf-01`, each MoQ group one segment and each object one chunk, and a segment +is its C2PA boxes followed by its group's objects and nothing else, so the segment's C2PA hard +binding and the group's JWP receipt commit to the same chunks. Under the per-segment C2PA +Manifest Box method each segment's manifest holds a `c2pa.hash.bmff.v3` over the segment, the +live video segment assertion naming the stream and the manifest before it, and the link hash +and root of its group's receipt; under the Verifiable Segment Info method each segment carries +an `emsg` with a segment-info map signed by a session key delivered in a manifest, rotated in a +segment's manifest, and the same key signs the group's receipts. The claim signer's X.509 chain +and its profile, the session keys' binding to it, and every object's receipt are checked +together. A stream labelled machine-made carries C2PA's digital source type and AI disclosure +for its model in every manifest, as timed text does for a caption model, and a compositor's +output names its inputs' labels. Manifest identifiers are derived from the stream and the +segment and every signature is deterministic, so packagers sharing a key write the same +segment and packagers that do not chain across a failover, as C2PA 2.4 has dynamically +packaged content; and a registered initialization manifest carries C2PA's repository receipt, +whose proof is an RFC 9942 receipt of inclusion in the repository's RFC 9162 log. The C2PA +boxes of each segment ride a provenance track beside the media track: +[`extensions/live-provenance.md`](extensions/live-provenance.md). + ### Payloads of the WAI-native integer codecs This revision pins a payload format to each WAI-native integer codec whose row named diff --git a/wai/extensions/capture-binding.md b/wai/extensions/capture-binding.md index b88c6cece..362fe9737 100644 --- a/wai/extensions/capture-binding.md +++ b/wai/extensions/capture-binding.md @@ -13,7 +13,8 @@ > its verdict: `single-take`, `aligned`, `complete` and `publishable` for each purpose. > The reference case is a lesson or an audition, where the live call is low quality and > the master is made on the performer's own device. No new transport: the commitments are -> events, and the mapping onto C2PA live video output is field semantics (§11). The +> events, and its C2PA live video output is a take stream of +> [live-provenance](live-provenance.md) (§11). The > reference implementation is the `capture_take` module of `wai-rs` (feature > `capture_take`); the corpus is [`live-conformance/takes/`](../live-conformance/takes/), > and its second implementation is `live-conformance/capture_take_verify.py`. @@ -535,14 +536,14 @@ capture instant on the room clock maps that instant. ## 11. C2PA live video -This section maps a take, and a CMAF output of a room, onto the C2PA Technical -Specification's live video provenance (§19 there). It is field semantics, and this -revision implements none of it beyond the stream id: `wai-rs` has no CBOR or COSE -encoder, and the C2PA signer `c2pa_emit` uses writes whole-asset manifests and fragmented -BMFF manifests (one manifest in the initialization segment, a Merkle tree over the -fragments), with no live video segment assertion, no session keys assertion and no -Verifiable Segment Info. The take's commitments are the WAI side's witness; the C2PA -output is what a C2PA validator reads, and each is checked on its own. +This section maps a take onto the C2PA Technical Specification's live video provenance (§19 +there). A take's CMAF master is a stream of kind `take` of [live-provenance](live-provenance.md) +(§3.1 there), which writes and verifies the C2PA structures; this section states what a take +puts in them. The reference implementation is `live-provenance`'s: the `live_provenance` +module of `wai-rs` writes a take's initialization segment and segments under both of §19's +methods and verifies them, and its corpus holds a take (`live-conformance/c2pa-live/take-both`). +The take's commitments are the WAI side's witness, which §9 verifies; the C2PA output is what +a C2PA validator reads, and each is checked on its own. ### 11.1 What maps to what @@ -550,29 +551,32 @@ output is what a C2PA validator reads, and each is checked on its own. id's first 16 bytes as an RFC 9562 UUID of version 8: byte 6's high four bits set to `1000`, byte 8's high two bits to `10`, written in the 8-4-4-4-12 lowercase form. The corpus states it for each take. -- **The segments.** Where the master is CMAF, chunk *i* holds the segment whose movie - fragment the device numbers *i* + 1 in its `mfhd` `sequence_number` - (ISO/IEC 14496-12), and that is the segment's `sequenceNumber` in both methods. Chunk 0 - holds the initialization segment too (§4.1), with the C2PA Manifest Box §19.2.3 - requires. +- **The segments.** Where the master is CMAF, chunk *i* holds segment *i*, and chunk 0 holds + the initialization segment before it (§4.1), with the C2PA Manifest Box §19.2.3 requires. + The segment's `sequenceNumber`, in both methods, is the `mfhd` `sequence_number` + (ISO/IEC 14496-12) of its first movie fragment, the device numbering its movie fragments + from 1 across the take (live-provenance §3.3): *i* + 1 where each segment is one movie + fragment. - **The hashes.** A chunk's `hash` covers every byte of the chunk, its C2PA boxes included; the segment's `c2pa.hash.bmff.v3` excludes them by §19's exclusions. The two are different digests of different spans. ### 11.2 Per-segment manifests (§19.3) -Each segment carries a C2PA Manifest Box in a `uuid` box, whose manifest holds a -`c2pa.hash.bmff.v3` over the segment and the assertion `c2pa.livevideo.segment` with -`sequenceNumber` as above, `streamId` the take's, `continuityMethod` `c2pa.manifestId`, -and `previousManifestId` the manifest identifier of chunk *i* − 1's segment. The -initialization segment's manifest carries `c2pa.actions.v2` whose first action is -`c2pa.created` with the IPTC digital source type `digitalCapture` (§18.15.2). +Each segment carries a C2PA Manifest Box whose manifest holds, as live-provenance §6 lists +them, a `c2pa.hash.bmff.v3` over the segment, `c2pa.actions.v2` (each segment's manifest is a +standard manifest, which §15.10.1.2 there requires to hold one) whose one action is +`c2pa.created` with the IPTC digital source type `digitalCapture` (§18.15.2), and the +assertion `c2pa.livevideo.segment` with `sequenceNumber` as above, `streamId` the take's, +`continuityMethod` `c2pa.manifestId`, and `previousManifestId` the manifest identifier of +the segment before it, or of the initialization segment's manifest for segment 0. -Each segment's manifest **MAY** also carry the assertion `science.transaction.wai.take`, -a map of `take` (the take id), `room` (the room id), `device` (the device's key) and -`commit` (the digest of the device's last `take_commit` before the segment was written, -absent before the first), each a byte string, so that a C2PA validator finds the room -events that witness the take. +Every manifest of the take holds the assertion `science.transaction.wai.take` +(live-provenance §5.3), a map of `take` (the take id), `room` (the room id) and `device` (the +device's key), each a byte string; a segment's also holds `commit`, the digest of the device's +last `take_commit` before the segment was written, and the initialization segment's `epoch`, +the room-clock instant of the master's media time 0 (§10), so that a C2PA validator finds the +room events that witness the take. ### 11.3 Verifiable Segment Info (§19.4) @@ -588,27 +592,25 @@ events that witness the take. `timescale` and `event_duration` covering the segment, `id` the `sequenceNumber`, and `message_data` a COSE_Sign1_Tagged by the session key whose payload holds `sequenceNumber`, `bmffHash` (the segment's, excluding its C2PA `emsg` boxes, with no - `merkle` field) and `manifestId` (§19.4.1, §19.4.2). + `merkle` field) and `manifestId` (§19.4.1, §19.4.2), as live-provenance §7.2 writes it. - A COSE Sig_structure begins with the context string `Signature1`, and every message WAI signs begins with a `wai:` domain, so no signature of one is a signature of the other. ### 11.4 A room's CMAF output -A CMAF rendition a packager makes of a room's live tracks maps the same way, as its own -live video session: its own `streamId`, its own session key, its own claim signer. That -is the packager's provenance of its output, not the device's of its take. +A CMAF rendition of a room's live tracks is a stream of kind `track` of live-provenance, as +its own live video session: its own `streamId`, its own session key, its own claim signer, +each segment tied to the JWP receipt of its group (§8 there). That is the publisher's or the +packager's provenance of its output, not the device's of its take. ### 11.5 AI disclosure and sustainability - **AI disclosure** (§18.28, `c2pa.ai-disclosure`). A take's manifest has no field for a trained model, and its C2PA output carries no `c2pa.ai-disclosure`: its source type is - a digital capture. A CMAF output that carries captions a model produced (live-events - §4.10, `origin` 2 or 3) discloses them: `modelIdentifier` is `sha256:` followed by the - caption's `model` digest in lowercase hexadecimal; `contentProfile` - `humanOversightLevel` is `fully_autonomous` for `origin` 2 and `human_validated` for 3; - `modelType` is required and an event does not carry it, so an emitter writes the - assertion only where it knows the model's type (§18.28's Table 12). + a digital capture. A room track or a compositor's output that is labelled machine-made + discloses its models as live-provenance §9 has it, and captions a model produced as + [timed-text](timed-text.md) §10.3 has it. - **Sustainability** (§18.26, `c2pa.environmental-sustainability`). An emitter writes `energy_kwh` only from a measured figure with its class, under [energy-binding](energy-binding.md)'s rules, and never `carbon_kgco2e` or diff --git a/wai/extensions/live-provenance.md b/wai/extensions/live-provenance.md new file mode 100644 index 000000000..e9b0b90b6 --- /dev/null +++ b/wai/extensions/live-provenance.md @@ -0,0 +1,924 @@ +# WAI Extension: Live Provenance + +> Status: Draft. Registers the record `wai.live.provenance`: the live video provenance of +> C2PA 2.4 §19 for a room's CMAF tracks and for a device's CMAF take, written exactly as §19 +> defines it and tied to WAI's own per-object commitments. A room track packaged as CMAF is +> carried over MoQ under `draft-ietf-moq-cmsf-01`: each MoQ group is one CMAF segment and each +> object one CMAF chunk. Its publisher signs a JWP group receipt for each group +> ([jwp-receipts](jwp-receipts.md)), and this extension makes the segment's C2PA hard binding +> and that receipt's Merkle root commit to the same bytes: the segment, less its C2PA boxes, +> **is** the group's objects in order. The per-segment C2PA Manifest Box method (§19.3 there) +> names the group's receipt inside the signed manifest; the Verifiable Segment Info method +> (§19.4 there) is signed by a session key that is also the key that signs the group's +> receipts. One verification then establishes the C2PA provenance of every segment and the +> receipt of every object. The C2PA boxes of each segment ride a **provenance track** beside +> the media track, as the receipts ride the receipts track. The machine-readable marking of +> machine-made content, the IPTC digital source type and the `c2pa.ai-disclosure` assertion, +> follows the stream's disclosure label, as [timed-text](timed-text.md) §10.3 has it for +> captions, and for a compositor's output, its inputs' labels. Every manifest identifier is +> derived from the stream and the segment, and every signature is Ed25519, so packagers that +> share a signing key write the same segment and packagers that do not still chain across a +> failover: the dynamically packaged content C2PA 2.4 §19.2.1 treats as live. The +> initialization manifest may carry C2PA's repository receipt (§18.27 there), whose proof is an +> RFC 9942 receipt of inclusion in a repository's RFC 9162 log. No new transport. The reference +> implementation is the `live_provenance` module of `wai-rs` (feature `live_provenance`); the +> corpus is [`live-conformance/c2pa-live/`](../live-conformance/c2pa-live/), and its second +> implementation is `live-conformance/c2pa_live_verify.py`. +> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174. +> +> Texts read for this revision, each pinned to the version named: the C2PA Technical +> Specification, version 2.4 (April 2026), §5.3.1, §6.4, §8.1, §8.4.2, §10.2, §11.1, §11.2, +> §13.1, §13.2, §14.2, §14.4.1, §14.5, §15.5 to §15.12, §18.6, §18.15, §18.16, §18.25, +> §18.27, §18.28, §19 and Appendix A.5; RFC 9052 (COSE structures), RFC 9053 (EdDSA and OKP +> keys), RFC 9360 (`x5chain`), RFC 9942 (COSE Receipts) §2, §4 and §5, RFC 9162 §2.1, +> RFC 8949 §4.2.1, RFC 5280, RFC 8410, RFC 7093, RFC 9562 and RFC 4648 §5; +> `draft-ietf-moq-cmsf-01` §3; the Event Message box syntax of ISO/IEC 23009-1 §5.10.3.3.3 as +> AOM id3-emsg v1.0.0 §2.2.2 reproduces it; the IPTC Digital Source Type vocabulary as released +> on 2024-10-23; and Regulation (EU) 2024/1689, Article 50. ISO/IEC 14496-12, ISO/IEC 23000-19, +> ISO/IEC 19566-5:2023 (JUMBF) and ISO/IEC 23009-9:2025 (REaP) were not read for this +> revision: the box layouts below are the ones C2PA 2.4 and [packaging](packaging.md) §7 +> (paragraph 7.6) state, REaP is named only as C2PA 2.4 §13.2.1 and §19.4.1 name it, and the +> corpus's README says what an independent C2PA validator reports for the corpus. A section +> number in this text is C2PA 2.4's unless it names another document. + +## 1. Scope + +In scope: what a stream is, how its segments correspond to a room track's groups or to a +take's chunks, its sequence numbers, its stream identifier and its presentation time (§3); +the C2PA structures this extension writes, which are a profile of C2PA 2.4: the manifest +store, the claim and its derived identifier, its signature and the signer's certificates, the +hard binding, the actions, and the repository receipt in an update manifest (§4); the WAI +assertions (§5); the per-segment C2PA Manifest Box method (§6); the Verifiable Segment Info +method, its session keys and their rotation (§7); the correspondence +with the group receipts and the object receipts (§8); the digital source type and AI +disclosure (§9); carriage over HTTP and over MoQ, and dynamic and redundant packaging (§10); +verification (§11); and the reasons (§12). + +Out of scope: a transport; the media codecs; MPEG transport streams, which §19.1 excludes; a +track packaged as WAI envelopes (`packaging` `"wai"`, [moq-streaming-format](moq-streaming-format.md) +§3), which is not ISO BMFF and so has no §19 provenance; time-stamps (§10.3.2.5) and +revocation information (§14.5.2), which this revision neither writes nor reads; ingredients +other than an update manifest's parent, update manifests other than §4.7's, compressed +manifests, external manifests and the `manifestUri` of a segment-info map; the Merkle form of +the BMFF hash (Appendix A.5.4), which §19 does not use; consistency proofs between a +repository's tree heads (RFC 9942 §5.3); the segment indexing of ISO/IEC 23009-9 §6.2, which +§19.4.1 offers as an alternative to `mfhd` numbering; re-encoding a source to check that two +encoders' outputs agree (§10.3); and what a party does with a verdict. + +Relationship to the core spec: `wai.live.provenance` is a **record** ([SPEC](../SPEC.md) §5): +a signed record a WAI tool emits and verifies. It names no payload format and is not +dispatchable. + +Relationship to [capture-binding](capture-binding.md): §11 there maps a take onto C2PA live +video. A take is a stream of this extension of kind `take` (§3.1), and this extension is +where that mapping is written and verified. + +## 2. Terms + +- **Room**, **principal**, **track**, **Full Track Name**: as in [live-rooms](live-rooms.md) + §2 and §6.1. **Group receipt**, **object receipt**, **link hash**: as in + [jwp-receipts](jwp-receipts.md) §2.4 and §3 step 3. +- **Stream**: one C2PA live video session (§19): an initialization segment and media + segments, of one CMAF track. +- **Chunk**: one `moof` box and the `mdat` box after it. +- **Segment**: a media segment of the stream: its **C2PA boxes** (§3.2), then one or more + chunks. +- **C2PA Manifest Box**: the `uuid` box of Appendix A.5.1, with `box_purpose` `manifest`. +- **VSI box**: an `emsg` box carrying a verifiable-segment-info (§19.4.2). +- **Claim signer**: the holder of the X.509 end-entity certificate that signs a manifest's + claim. **Session key**: a key of §19.4.4, which signs segment-info maps. +- **Label**: the stream's disclosure label (§5.1): its `origin` (1 human, 2 machine, 3 + machine-reviewed, as [timed-text](timed-text.md) §3.1 has them) and a model digest for 2 + and 3, or, for a compositor's output, its inputs' labels. +- *T*: the instant a verification is made, in microseconds since 1970-01-01T00:00:00Z. + +Integers in boxes are big-endian. A digest is SHA-256 (FIPS 180-4), 32 bytes, unless it is +named BLAKE3. CBOR is written in RFC 8949 §4.2.1's core deterministic encoding. + +## 3. The stream + +### 3.1 Kinds + +A stream is of one of two kinds: + +- **track**: a room track (live-rooms §6.1) of media kind `audio`, `video` or `screen`, + packaged as CMAF and published under `draft-ietf-moq-cmsf-01` (`packaging` `"cmaf"`). Each + MoQ group of the track is one segment and each object one chunk; `draft-ietf-moq-cmsf-01` + §3.3 lets an object hold more than one chunk, and this extension requires exactly one. The + publisher signs a group receipt for every group (jwp-receipts §2.4.1) and an object receipt + for every object (§2.4.2 there), and §8 ties them to the segments. +- **take**: the CMAF master of a take ([capture-binding](capture-binding.md) §3), whose chunk + *i* is the segment *i* of the stream, chunk 0 holding the initialization segment before it + (capture-binding §4.1, §11.1). The take's commitments in the room's events are its WAI + witness, and capture-binding §9 verifies them; this extension verifies the C2PA side and + the identity a take's manifests carry (§5.3). + +### 3.2 Layout + +The initialization segment is exactly three top-level boxes, in this order: `ftyp`, a C2PA +Manifest Box, and `moov`, holding one track (Appendix A.5.3; §19.2.3); or, once registered, +those three followed by the update box of §4.7. It holds no `mdat` (§19.7.1). + +A segment is, in this order: + +1. its C2PA boxes: a C2PA Manifest Box, a VSI box, or the first then the second; +2. one or more chunks. + +It holds no other top-level box. A chunk's `moof` holds an `mfhd`, then one `traf` holding a +`tfhd` with default-base-is-moof and the track's `track_ID`, a `tfdt` of version 1 and a `trun` +of version 0 with a data offset and each sample's duration, size and flags, as packaging §7 +(paragraph 7.6) writes a fragment; the data offset points at the `mdat`'s payload, and the +sizes fill it. The C2PA Manifest Box precedes the first `mdat` (Appendix A.5.3) and the VSI box +precedes the first `moof`, as an Event Message box precedes the `moof` of its chunk in CMAF. + +**The bytes of a segment.** A segment is its C2PA boxes followed by its chunks, concatenated, +and nothing else. For a track, the chunks are the group's objects in object order, each +exactly as it travelled (§10.2), so a segment *is* `P ‖ O₀ ‖ … ‖ Oₙ₋₁`, *P* its C2PA boxes and +*O*ᵢ object *i*. + +### 3.3 Sequence numbers + +Every `moof` of the stream carries a `sequence_number` in its `mfhd`: 1 in the first chunk of +the stream's first segment, and one more in each chunk after it, across segments. A segment's +**sequence number** is the `sequence_number` of its first `moof`, and it is the +`sequenceNumber` of its live video segment assertion (§19.3.2) and of its segment-info map +(§19.4.1), each of which §19 requires to match `mfhd.sequence_number`. For a track, the +segment of group *g* therefore has the sequence number + + *s*(*g*) = 1 + Σ *n*(*h*), over the groups *h* of the stream before *g*, + +where *n*(*h*) is the `leaf_count` of group *h*'s receipt. + +Where a segment holds more than one chunk, the sequence numbers of consecutive segments +differ by the number of chunks of the earlier one. §19.4.1's note reads them as one apart, +which is the case of one chunk per segment; its normative text, that the number matches +`mfhd.sequence_number` and never decreases or repeats, holds either way. + +### 3.4 The stream identifier + +A stream's `streamId` (§19.3.2) is an RFC 9562 UUID of version 8, written in the 8-4-4-4-12 +lowercase form. Its 16 bytes are, before byte 6's high four bits are set to `1000` and byte +8's high two bits to `10`: + +- for a track: the first 16 bytes of SHA-256(`"wai:live-provenance:stream\x01"` ‖ `room` ‖ + u32(len(`track`)) ‖ `track` ‖ u64(`epoch`)), with the fields of the stream assertion + (§5.1); +- for a take: the take id's first 16 bytes (capture-binding §11.1). + +### 3.5 Presentation time + +The track's timescale is the `timescale` of its `mdhd`. A segment's presentation time is the +`baseMediaDecodeTime` of its first `tfdt`, in that timescale. For a track its instant is +`epoch` + ⌊`baseMediaDecodeTime` × 1 000 000 / `timescale`⌋ microseconds, with the `epoch` of +the stream assertion (§5.1) or, for a take, of the take assertion (§5.3). + +## 4. The C2PA structures + +Every C2PA structure this extension writes is the one C2PA 2.4 defines; this section fixes the +choices it leaves open, so that every conforming writer of a stream produces the same bytes +from the same inputs and keys. + +### 4.1 The manifest store + +A C2PA Manifest Box is a `uuid` box with extended type +`D8FEC3D6-1B0E-483C-9297-5828877EC481`, version 0 and flags 0, then `box_purpose` `manifest` +and a zero byte, then 8 zero bytes (the offset of the first merkle box, of which a stream has +none), then the manifest store (Appendix A.5.1 to A.5.3). It carries no padding. + +The store is a JUMBF superbox (`jumb`, holding a description box `jumd` and then its content +boxes) of a single manifest, the active one. Each description box is the JUMBF type UUID (16 +bytes), the toggles byte `0x03` (requestable, label present; §11.1.4.1.2), and the label in +UTF-8 followed by a zero byte. The boxes, with their type and label: + +| box | JUMBF type UUID | label | content | +|---|---|---|---| +| the store | `63327061-0011-0010-8000-00AA00389B71` (c2pa) | `c2pa` | the manifest | +| the manifest | `63326D61-0011-0010-8000-00AA00389B71` (c2ma) | its identifier (§4.2) | the assertion store, the claim, the claim signature | +| the assertion store | `63326173-0011-0010-8000-00AA00389B71` (c2as) | `c2pa.assertions` | one superbox per assertion, in the claim's order | +| an assertion | `63626F72-0011-0010-8000-00AA00389B71` (cbor) | the assertion's label | one `cbor` box of its CBOR | +| the claim | `6332636C-0011-0010-8000-00AA00389B71` (c2cl) | `c2pa.claim.v2` | one `cbor` box of the claim | +| the claim signature | `63326373-0011-0010-8000-00AA00389B71` (c2cs) | `c2pa.signature` | one `cbor` box of the COSE structure | + +Every box header is a 4-byte size, which includes the header, and a 4-byte type. + +### 4.2 The manifest identifier and the claim + +A manifest's identifier is `urn:c2pa:` followed by an RFC 9562 UUID of version 4 in the +8-4-4-4-12 lowercase form, with no claim generator identifier (§8.1). Its 16 bytes are, before +byte 6's high four bits are set to `0100` and byte 8's high two bits to `10`, the first 16 +bytes of + + SHA-256(`"wai:live-provenance:manifest\x01"` ‖ u32(len(`streamId`)) ‖ `streamId` ‖ u8(*role*) ‖ u64(*s*)), + +`streamId` in its text form (§3.4), *role* 0 for the initialization segment's manifest, 1 for a +per-segment manifest (§6), 2 for a manifest that delivers session keys in a segment (§7.3) and +3 for the update manifest of §4.7, and *s* the segment's sequence number (§3.3), 0 for roles 0 +and 3. The identifier is a function of the stream and the segment, not of the packager: every +packager of a stream writes the same identifier for the same manifest (§10.3). No two +manifests of a stream share one, and the 122 bits SHA-256 leaves are distributed as §8.1's +random ones are; the identifier is not a secret, and a signature, not the identifier, binds a +manifest. + +The claim is a `claim-map-v2` (§10.2.1) with exactly these fields: + +- `instanceID`: `xmp:iid:` followed by the manifest identifier's UUID; +- `claim_generator_info`: `{"name": "wai", "specVersion": "2.4.0"}`; +- `signature`: `self#jumbf=/c2pa/` ‖ the manifest identifier ‖ `/c2pa.signature`; +- `created_assertions`: one hashed URI (§8.4.2.1) per assertion, in the order of §6 and §7: + `url` `self#jumbf=/c2pa/` ‖ the manifest identifier ‖ `/c2pa.assertions/` ‖ the label, and + `hash` the SHA-256 of the assertion's superbox less its 8-byte header (§8.4.2.3); +- `alg`: `sha256`. + +### 4.3 The claim signature + +The claim signature is a `COSE_Sign1_Tagged` (CBOR tag 18; RFC 9052 §4.2) whose payload is +detached (`nil`) and whose Sig_structure payload is the claim's CBOR (§10.3.2.4, §13.2.2): + +- protected header: the map `{1: -8, 33: [EE, CA]}`: the algorithm EdDSA, the Ed25519 instance + (§13.2.1; RFC 9053 §2.2), and `x5chain` under its integer label (§14.5; RFC 9360) holding the + claim signer's end-entity certificate and then every intermediate certificate, the trust + anchor left out; +- unprotected header: the empty map; +- Sig_structure: `["Signature1", protected, h'', payload]` (§13.2.3); +- signature: the 64-byte Ed25519 signature (RFC 8032) of the claim signer over the + Sig_structure's CBOR. + +Ed25519 is deterministic, which §13.2.1 recommends for streaming so that equal segments +remain equal. Every signature this extension writes is Ed25519. + +### 4.4 Certificates + +The claim signer's chain meets §14.5.1.1 and RFC 5280: X.509 version 3; signature algorithm +`id-Ed25519` (RFC 8410 §3), no parameters; Ed25519 subject public keys; no unique +identifiers; a Basic Constraints extension, critical, asserting `cA` in a certificate +authority's certificate and absent `cA` in the end-entity's; a Key Usage extension, critical, +with `keyCertSign` (and `cRLSign`) in a certificate authority's and `digitalSignature` alone +in the end-entity's; a Subject Key Identifier in every certificate, the leftmost 160 bits of +the SHA-256 of the subject public key (RFC 7093 §2, method 1); an Authority Key Identifier, +its key identifier, in every certificate not self-signed; and, in the end-entity's, an +Extended Key Usage extension holding `c2pa-kp-claimSigning` (1.3.6.1.4.1.62558.2.1) and +`id-kp-documentSigning` (1.3.6.1.5.5.7.3.36), which §14.4.1 recommends for older validators. +Every name is an organization (`id-at-organizationName`) and a common name, each a +UTF8String. Validity is written as UTCTime through 2049 and GeneralizedTime from 2050 (RFC 5280 +§4.1.2.5). + +A verifier of this profile accepts a chain under §14.5.1 and §15.7 (§11.2). + +### 4.5 The hard binding + +Every manifest holds exactly one hard binding, a `c2pa.hash.bmff.v3` assertion (§18.6), the +map: + +- `exclusions`: exactly these four, in this order: + 1. `{"xpath": "/uuid", "data": [{"offset": 8, "value": h'D8FEC3D61B0E483C92975828877EC481'}]}` + (Appendix A.5.6); + 2. `{"xpath": "/ftyp"}`; + 3. `{"xpath": "/mfra"}`; + 4. `{"xpath": "/emsg", "data": [{"offset": 12, "value": 'urn:c2pa:verifiable-segment-info' ‖ h'00'}]}` + (§19.4.1); +- `alg`: `sha256`; +- `hash`: the hash below; +- `sequenceNumber`: in a segment's manifest, the segment's sequence number (§3.3); absent in + the initialization segment's. + +**The hash** (§18.6.2, Example 6) is SHA-256 over, for each top-level box of the segment that +no exclusion matches, in order, the box's offset from the segment's first byte as a u64 +followed by every byte of the box. An exclusion matches a box when the box's type is the +xpath's and, at each `data` entry's offset from the box's first byte, the box holds the +entry's value. Every box the profile excludes is excluded whole, so no `subset` applies. In +the initialization segment the hash covers the `moov`, which §19.2.3 requires. + +The fourth exclusion is the VSI box. §19.4.1 writes its `data` offset as 0, where every box +holds its size; offsets count from a box's first byte (§18.6.2, as the first exclusion's 8 +shows), and the `scheme_id_uri` of an `emsg` of version 0 begins at 12. This extension writes +12, the entry that excludes the box. The same list is written in every manifest and every +segment-info map of a stream, whether or not the segment holds a VSI box, so a segment that +carries both methods has one hash under both. + +### 4.6 The actions + +Every manifest holds a `c2pa.actions.v2` assertion (§18.15.2) whose `actions` is one action: +`{"action": "c2pa.created", "digitalSourceType": DST}`, DST the IPTC term of §9 as a URI under +`http://cv.iptc.org/newscodes/digitalsourcetype/`. Each segment's manifest is a standard +manifest and so holds its own (§15.10.1.2). + +### 4.7 The repository receipt + +A publisher that registers the initialization segment's manifest with a C2PA Manifest +Repository records the repository's proof of ingestion in a `c2pa.repository-receipt` +assertion (§18.27), which C2PA allows only in an update manifest (§11.2.3): + +- **The layout.** The initialization segment keeps its three boxes, its C2PA Manifest Box's + `box_purpose` becoming `original`, and a fourth, a C2PA `uuid` box of `box_purpose` `update`, + ends the file (Appendix A.5.3). Both purposes are eight bytes, so no offset moves and the + hard binding is unchanged; both boxes are C2PA `uuid` boxes, which the hash excludes. +- **The update store** holds one update manifest (JUMBF type `6332756D-0011-0010-8000-00AA00389B71`, + c2um), of role 3 (§4.2), whose claim (§4.2) and signature (§4.3) are as a standard + manifest's, and which holds, in this order and nothing else: + 1. `c2pa.ingredient.v3` (§18.16), the map `{"relationship": "parentOf", "validationResults": + V, "activeManifest": {"url": "self#jumbf=/c2pa/" ‖ P, "hash": H}, "claimSignature": {"url": + "self#jumbf=/c2pa/" ‖ P ‖ "/c2pa.signature", "hash": G}}`, `P` the parent's identifier, `H` + and `G` the hashes of its manifest superbox and its claim signature superbox (§8.4.2.3), + as §11.2.3 requires of an update manifest's one ingredient. `V` is the result of validating + the parent, which §18.16.12.4.3 requires with an `activeManifest`: `{"activeManifest": + {"success": S, "informational": [], "failure": []}, "ingredientDeltas": [], "specVersion": + "2.4.0"}`, `S` the status maps `{"code": C, "url": U}` of `claimSignature.validated`, + `signingCredential.trusted` and `claimSignature.insideValidity` with `U` the parent's + signature URI, then `assertion.hashedURI.match` for each of the parent's assertions in its + claim's order with `U` that assertion's URI (`self#jumbf=/c2pa/` ‖ P ‖ + `/c2pa.assertions/` ‖ its label), then `assertion.bmffHash.match` with the hard binding's + URI. A parent that does not verify is not registered, so `S` is the one list a valid + parent gives, and a verifier recomputes it; + 2. `c2pa.repository-receipt`, a JSON content superbox (JUMBF type + `6A736F6E-0011-0010-8000-00AA00389B71`, one `json` box), the object of §18.27.2: + `{"@context": {"c2pa": "https://c2pa.org/ns/", "receipt": "https://c2pa.org/ns/repository-receipt#"}, + "@type": "org.c2pa.repository-receipt", "repository": {"uri": R, "manifestId": P}, + "anchor": {"uri": A, "proof": {"profile": "urn:ietf:rfc:9942", "entry": E, "receipt": C}}}`, + written compactly in this member order. `R` is the repository's base URI and `A` where the + proof may be retrieved. +- **The proof.** §18.27 leaves the `proof` object to the repository; this extension's is a + receipt of inclusion. The repository keeps an RFC 9162 §2.1 log over SHA-256 whose entries + are manifests by `H`; `E` is `H` in lowercase hexadecimal, and `C` is, in base64url without + padding (RFC 4648 §5), an RFC 9942 §5.2.1 receipt of inclusion: a `COSE_Sign1_Tagged` with + protected header `{1: -8, 395: 1}` (EdDSA, `vds` `RFC9162_SHA256`), unprotected header + `{396: {-1: [I]}}` with one inclusion proof `I`, the CBOR of `[tree_size, leaf_index, + inclusion_path]` in a byte string, and a detached payload. Its signature is the repository's + Ed25519 key's over the Sig_structure whose payload is the tree's root. + +A verifier holds the repositories it accepts, each its base URI and key. The receipt then +establishes that the repository's log held the manifest at that tree size: the leaf is +SHA-256(0x00 ‖ `H`), the inclusion path leads from it to a root (RFC 9162 §2.1.3.2), and the +repository signed that root. It does not establish the manifest's content or its signer, +which §11.2 checks, and it is a witness of registration, not a trust anchor +([transparency](transparency.md) §6). + +## 5. The WAI assertions + +The assertions in this section are CBOR maps under C2PA's entity-specific namespace (§6.2.1). + +### 5.1 The stream assertion + +A track's initialization manifest, and every manifest of a segment that holds no live video +segment assertion (§7.3), holds `science.transaction.wai.stream`: + +| key | value | +|---|---| +| `room` | the room id, 32 bytes | +| `track` | the track's Full Track Name in MOQT §8.8's rendering, as live-rooms §6.1 writes it: `----` | +| `trackAlias` | the `wai_receipt_alias` the track's receipts sign ([moq-streaming-format](moq-streaming-format.md) §4) | +| `epoch` | the instant of media time 0, in microseconds since 1970-01-01T00:00:00Z | +| `firstGroup` | the group id of the stream's first segment | +| `label` | `{"origin": O}` with `O` 1; `{"origin": O, "model": M}` with `O` 2 or 3 and `M` the model's 32-byte digest; or, for a compositor's output, `{"inputs": [I, …]}`, each `I` `{"streamId": S, "track": N, "origin": O}` or the same with `"model": M` | + +The `track`'s first field is `room` in lowercase hexadecimal, and its kind is `audio`, `video` +or `screen`. A stream assertion in a later manifest equals the initialization manifest's. + +### 5.2 The group assertion + +A track's per-segment manifest (§6) holds `science.transaction.wai.group`: + +| key | value | +|---|---| +| `groupId` | the group id of the segment's group | +| `receipt` | the link hash of the group's receipt (jwp-receipts §3 step 3), 32 bytes | +| `root` | the group receipt's `root_hash`, 32 bytes | +| `objects` | the group receipt's `leaf_count` | + +### 5.3 The take assertion + +Every manifest of a take holds `science.transaction.wai.take`, as capture-binding §11.2 has +it: `take` (the take id), `room` (the room id) and `device` (the device's key), each 32 bytes, +and `commit` (32 bytes), the digest of the device's last `take_commit` before the segment was +written, absent in the initialization manifest. The initialization manifest's also holds +`epoch`: the instant of the master's media time 0 on the room clock, in microseconds since +1970-01-01T00:00:00Z (capture-binding §10), which no later manifest holds. `take`, `room` and +`device` are the same in every manifest of the take. + +## 6. The per-segment C2PA Manifest Box method + +Under §19.3, every segment holds a C2PA Manifest Box whose manifest holds, in this order: + +1. `c2pa.hash.bmff.v3` over the segment (§4.5); +2. `c2pa.actions.v2` (§4.6); +3. `c2pa.livevideo.segment` (§19.3.2): `{"sequenceNumber": s, "streamId": S, "continuityMethod": + "c2pa.manifestId", "previousManifestId": P}`, `s` the segment's sequence number (§3.3), `S` + the stream's identifier (§3.4), and `P` the identifier of the manifest of the segment + before it, which for the first segment is the initialization segment's; +4. the AI disclosure assertions of §9, if any; +5. `science.transaction.wai.group` (§5.2) for a track, or `science.transaction.wai.take` + (§5.3) for a take. + +The initialization segment's manifest holds `c2pa.hash.bmff.v3` over it, `c2pa.actions.v2`, +`c2pa.session-keys` where §7 applies, the AI disclosure assertions, and the stream assertion +(§5.1) for a track or the take assertion for a take. + +The continuity chain makes each segment's manifest name the one before it. A segment that is +lost, inserted or moved breaks it at the next segment (§19.7.2). + +## 7. The Verifiable Segment Info method + +### 7.1 Session keys + +Under §19.4, the initialization manifest holds `c2pa.session-keys` (§18.25): `{"keys": [K, …]}`, +each `K`: + +- `key`: an OKP COSE_Key (RFC 9053 §7.2): `{1: 1, 2: kid, -1: 6, -2: x}`, the key type OKP, + its `kid`, the curve Ed25519 and the 32-byte public key `x`. `kid` is `x` itself, so a key + names itself; +- `minSequenceNumber`: the first sequence number it signs; +- `createdAt`: CBOR tag 0 over an RFC 3339 date-time in UTC with no fraction, + `YYYY-MM-DDTHH:MM:SSZ`; +- `validityPeriod`: seconds from `createdAt`; +- `signerBinding`: a `COSE_Sign1_Tagged` with protected header `{1: -8}`, an empty + unprotected header and a detached payload (`nil`), whose Sig_structure payload is the claim + signer's end-entity certificate, the first certificate of the manifest's `x5chain`. §18.25's + example shows the payload as `h''`; §13.2.3 reserves `nil` for detached content, and the + text calls the signature detached, so this extension writes `nil`. + +A key is valid from `createdAt` to `createdAt` + `validityPeriod` (excluded). For a track, the +session key that signs a segment's segment-info map is also the `signer_pubkey` of that +segment's group receipt (§8.3). For a take, the session key is the device's key +(capture-binding §11.3). + +### 7.2 The VSI box + +Every segment holds a VSI box (§19.4.2): an `emsg` of version 0 and flags 0 (ISO/IEC 23009-1 +§5.10.3.3.3) with: + +| field | value | +|---|---| +| `scheme_id_uri` | `urn:c2pa:verifiable-segment-info`, then a zero byte | +| `value` | `fseg`, then a zero byte | +| `timescale` | the track's timescale (§3.5) | +| `presentation_time_delta` | 0 | +| `event_duration` | the sum of the segment's sample durations, in the timescale | +| `id` | the segment's sequence number | +| `message_data` | the verifiable-segment-info | + +The verifiable-segment-info is a `COSE_Sign1_Tagged` with protected header `{1: -8}`, +unprotected header `{4: kid}`, the session key's `kid`, and the payload present: the CBOR of +the segment-info map `{"sequenceNumber": s, "bmffHash": H, "manifestId": M}`. `s` is the +segment's sequence number; `H` is the segment's hash as §4.5 writes it, with its +`sequenceNumber` and no `merkle`; `M` is the identifier of the active manifest: the manifest +of the segment's own C2PA Manifest Box where it has one, and otherwise the last manifest the +stream carried before it. The signature is the session key's over the Sig_structure +`["Signature1", protected, h'', payload]`. + +### 7.3 Rotation + +A new session key is delivered in a manifest carried in a segment's C2PA Manifest Box +(§19.4.3, §18.25), whose minimum sequence number is that segment's. Appendix A.5.3 requires +every initialization segment of a stream to carry the same manifest store, so this extension +rotates keys in a segment, not in a repeated initialization segment. That manifest holds, in +this order, the hard binding over its segment, the actions, `c2pa.session-keys` with the new +keys, the AI disclosure assertions, and the stream or take assertion; it holds no live video +segment assertion unless the stream also uses §6, and it becomes the active manifest. Its +segment's segment-info map names it and is signed by the new key. + +A segment is signed by the **current key** for its sequence number *s*: among the keys the +verifier holds, the one with the greatest `minSequenceNumber` not above *s*. A key's +`minSequenceNumber` is at least the sequence number of the segment that delivers it. + +## 8. The correspondence with WAI's receipts + +### 8.1 The same bytes + +For a track, the segment of group *g* and the group's receipts commit to the same objects: + +- the segment's chunks are the group's objects, one chunk an object, in object order (§3.2); +- object *i*'s `content_hash` is BLAKE3(`"moq-jwp:object\x01"` ‖ *O*ᵢ) (jwp-receipts §2.1), + over the chunk's bytes, its `moof` and its `mdat`, as the object travelled; +- the group receipt's `root_hash` is the Merkle root of jwp-receipts §2.2 over the objects' + leaves, and its `leaf_count` is the number of chunks; +- the segment's hard binding covers the same chunks at their offsets in the segment (§4.5). + +So no byte of an object can change without changing both the root the publisher signed and the +hash the claim signer or the session key signed, and each covers exactly the objects: the +C2PA boxes are what C2PA adds, and the group's objects are everything else. The objects carry +no energy label in this profile, so each leaf takes jwp-receipts §2.2's first form. + +### 8.2 The per-segment method + +A per-segment manifest names its group's receipt in its group assertion (§5.2): `groupId` is +the receipt's `group_id`, `receipt` its link hash, `root` its `root_hash` and `objects` its +`leaf_count`. The claim signer thereby signs which receipt the segment is, and the receipt's +signer signs which objects it holds. + +### 8.3 The Verifiable Segment Info method + +The session key that signs a segment's segment-info map is the `signer_pubkey` of the group's +receipt: one key signs both, over different structures. A Sig_structure's CBOR begins with +the byte `0x84` and a group signing payload with `moq-jwp:` (jwp-receipts §2.3), so no message +of one is a message of the other. A rotation (§7.3) changes the receipt key at the same group, +and the receipts of the new key start a chain of their own, whose first `parent_receipt_hash` +is `null` (jwp-receipts §2.4.1). + +### 8.4 Receipts + +The group receipt of every group of the stream, from `firstGroup` on, verifies under +jwp-receipts §3 steps 1 and 2, has the stream's `trackAlias`, and links to the receipt before +it under the same key (§3 step 3 there). Every object receipt of the group is accepted under +jwp-receipts §3 steps 1 to 5 against the group receipt and its chunk's bytes. The sequence +numbers of §3.3 follow from the receipts' `leaf_count`s. + +## 9. Digital source type and AI disclosure + +### 9.1 A stream's own label + +| label | digital source type | AI disclosure | +|---|---|---| +| `origin` 1, kind `audio` or `video` | `digitalCapture` | none | +| `origin` 1, kind `screen` | `screenCapture` | none | +| `origin` 2, model *M* | `trainedAlgorithmicMedia` | one, *M*, `fully_autonomous` | +| `origin` 3, model *M* | `compositeWithTrainedAlgorithmicMedia` | one, *M*, `human_validated` | +| a take | `digitalCapture` | none | + +An AI disclosure is a `c2pa.ai-disclosure` assertion (§18.28): `{"modelType": +"c2pa.types.model", "modelIdentifier": "tag:transaction.science,2026:wai/model/" ‖ M, +"contentProfile": {"humanOversightLevel": H}}`, `M` the model's digest in lowercase +hexadecimal, exactly as [timed-text](timed-text.md) §10.3 writes it for a caption model. +`c2pa.types.model` is Table 12's generic model type: an event names its model by digest and not +by type. Several are labelled `c2pa.ai-disclosure`, `c2pa.ai-disclosure__1`, … (§6.4). + +### 9.2 A compositor's output + +The output of a server compositor ([live-compose](live-compose.md)) is a stream whose label +lists its inputs. Its digital source type is `compositeCapture` when every input is of +`origin` 1 and kind `audio` or `video`; `compositeSynthetic` when any input is of `origin` 2 +or 3; and `composite` otherwise. It carries one AI disclosure per model of its inputs, in the +order models first appear among them, `fully_autonomous` when any input of that model is of +`origin` 2 and `human_validated` otherwise. This is timed-text §10.3's rule for a set of cues +of more than one origin, applied to inputs. + +### 9.3 What is checked + +Every manifest of a stream holds the digital source type and exactly the AI disclosures its +label gives, in that order; a verifier refuses a manifest whose `c2pa.created` action names +another source type (`source_type_mismatch`) or that lacks, adds or changes a disclosure +(`ai_disclosure_missing`). The check is against the label the stream states. Whether content +is machine-made is not decidable from its bytes: the label is the publisher's statement, +signed with the stream's other assertions, as a caption's `origin` is its producer's +(timed-text §14). What the check establishes is that a stream labelled machine-made carries +C2PA's machine-readable marking of it, in every manifest, and a stream labelled human carries +none. + +Regulation (EU) 2024/1689, Article 50(2), requires the outputs of AI systems that generate +synthetic audio, image, video or text to be marked in a machine-readable format and detectable +as artificially generated or manipulated; Article 50(4) requires a deployer to disclose a deep +fake. The digital source type and the AI disclosure assertion are such a marking, carried in +every segment's provenance. Writing them is an engineering requirement of this extension, and +meeting it is not a statement about the Regulation. + +## 10. Carriage + +### 10.1 HTTP + +Over HTTP the initialization segment and each segment are files as §3.2 lays them out, and a +playlist lists them as for any CMAF track ([live-recording](live-recording.md) §7). + +### 10.2 MoQ: the provenance track + +Over MoQ the track's objects are its chunks, exactly as `draft-ietf-moq-cmsf-01` §3.3 has +them, and the initialization segment, its C2PA Manifest Box included, is the catalog's inline +initialization data (§3.1 there). Each segment's C2PA boxes ride a **provenance track**: + +- it is in the media track's namespace and its Track Name is the media track's followed by + `_c2pa`, as `0_c2pa` (live-rooms §6.1's characters); +- its group *g* holds one object, the C2PA boxes of the media track's group *g*, exactly as + they lead the segment; +- the media track's catalog entry names it in `wai_c2pa_track`, and its own catalog entry has + `packaging` `"wai-c2pa"`; +- a relay forwards it as it forwards any track, unaltered. + +A subscriber that verifies reassembles the segment of group *g* as the provenance object of +group *g* followed by the media objects of group *g* in object order. + +**Why a track, not an object property.** A MOQT object property (MOQT §8.4, as +[moq-streaming-format](moq-streaming-format.md) §5 uses one) rides the header of every copy +of the object it is on, and a property of the first object would make every forwarded copy of +that object kilobytes larger, since a manifest holds a certificate chain. A track keeps the +media objects exactly the CMAF chunks a CMSF player plays and the receipts commit to (§8.1), +lets a subscriber that does not verify leave it out, lets one that does fetch a past group's +provenance by its group number (live-recording §8), and has the receipts track's form +(live-rooms §6.1), with which it shares group numbers. + +### 10.3 Dynamic and redundant packaging + +C2PA 2.4 treats dynamically packaged content, packaged just in time on request, as live video +(§19.2.1), and recommends deterministic signature algorithms for streaming so that segments +packaged apart remain equal and cacheable, naming ISO/IEC 23009-9 (REaP) among the workflows +that need it (§13.2.1). In this profile every byte of a segment is a function of the group's +objects, the stream, the segment's sequence number and the claim signer's key: + +- the chunks are the objects as they travelled (§3.2), and the layout and every box are fixed + (§4); +- the manifest identifiers are derived (§4.2), not drawn; +- CBOR is deterministic (§2), and Ed25519 signatures are deterministic (RFC 8032), as are the + session keys' and the receipts'. + +So two packagers that hold one claim signer's key and package the same group write the same +segment, byte for byte: a segment from either is interchangeable with the other's and caches +as one. Two packagers with different keys write segments whose manifests differ in their +signatures and certificates but share their identifiers, so the continuity chain of §6 holds +across a failover from one to the other, and both segments' chunks are the objects one group +receipt commits to (§8.1). Under the Verifiable Segment Info method a session key also signs +the receipts (§8.3), so a packager that is not the publisher uses the per-segment method. + +A packager that changes what a segment holds, by re-segmenting a stream or writing its objects +into another layout or codec, makes a new stream, derived from the first: its manifests are its +own, under its own stream identifier, and where it holds the source segments' manifests, its +output's manifest names each as a `parentOf` ingredient, with `c2pa.repackaged` for a +repackaging and `c2pa.transcoded` for a re-encoding, as deriving-edge §6.1 has a deriving edge +name its inputs' manifest stores. Packaging a group's objects into this profile's segment, which +this section is about, changes no object: there is no derivation, and the objects stay under +the publisher's receipt. + +What a verifier checks of a repackaged segment is its own: the identifiers (§11.4 step 3), the +manifest under its own signer, and the objects under the publisher's receipt, so it needs to +trust neither packager for the objects. Checking that two **encoders** produced the same +objects, by re-encoding the source under a WAI capability whose decode is `replicate` +([determinism-tiers](determinism-tiers.md)), is not in this revision: it needs the source and +an encoder that is itself a pure function of it. + +## 11. Verification + +### 11.1 Inputs + +A verifier holds: the trust anchors it accepts for `c2pa-kp-claimSigning` (§14.4.1); the +manifest repositories it accepts, each its base URI and Ed25519 key (§4.7); the instant *T*; +the stream's kind; its initialization segment; its segments in the order it received them, +each with its group id for a track (the MoQ group it came in, or the playlist's); and, for a +track, the group receipts and object receipts of the stream's groups. It verifies in the order +below and stops at the first refusal, which it reports with the reason, where it was raised +(`init`, `segment` *k* for the *k*-th segment received counting from 0, or `group` *g*), and +the two C2PA codes §12 gives it. A stream every step accepts is accepted. + +### 11.2 A manifest + +A C2PA Manifest Box of a segment (or of the initialization segment) is verified against that +segment's bytes, in C2PA's order (§15.6 to §15.12): + +1. The store, the manifest and its three parts read as §4.1 lays them out, the manifest + identifier as §4.2 has it, the claim as well-formed deterministic CBOR with the fields of + §4.2, and the claim signature as a `COSE_Sign1_Tagged` with an EdDSA algorithm, a detached + payload and exactly one `x5chain`, in the protected header (§14.2): otherwise + `manifest_malformed`. +2. The end-entity certificate and the intermediates meet §4.4's profile and §14.5.1.1, and the + end-entity's Extended Key Usage holds `c2pa-kp-claimSigning` and neither `id-kp-timeStamping` + nor `id-kp-OCSPSigning`: otherwise `credential_invalid` (`signingCredential.invalid`). +3. A chain is built from the end-entity through the intermediates to a trust anchor, each + certificate's issuer name equal to the next's subject, its Authority Key Identifier to the + next's Subject Key Identifier, and its signature verifying under the next's key: otherwise + `credential_untrusted` (`signingCredential.untrusted`). +4. The signature verifies under the end-entity's key over the claim (§4.3), with the strict + Ed25519 rule of jwp-receipts §3.1 step 3: otherwise `claim_signature_mismatch` + (`claimSignature.mismatch`). +5. *T* is within the validity of every certificate of the chain, the anchor's included; with + no time-stamp, §15.8 checks the current time: otherwise `credential_expired` + (`claimSignature.outsideValidity`). +6. Every hashed URI of `created_assertions` resolves to an assertion of the store whose hash it + holds: otherwise `assertion_hash_mismatch` (`assertion.hashedURI.mismatch`), or, for a URI + that resolves to nothing, `manifest_malformed`. +7. The assertions are the ones §6 or §7 lists for the manifest, in that order and no others, + the AI disclosures in any number at their place (§9.3 checks them), each readable with the + fields it defines: exactly one hard binding (§15.10.1.2), with §4.5's exclusions and `alg`; + the actions of §4.6, one `c2pa.created` action with a source type of the IPTC vocabulary; + and the rest as their sections give them: otherwise `manifest_malformed`. Which list + applies follows from where the manifest is and what it holds: the initialization segment's + (§6), a segment's with a live video segment assertion (§6), or a segment's without one + (§7.3). +8. The hard binding's `hash` is the segment's hash (§4.5): otherwise `hard_binding_mismatch` + (`assertion.bmffHash.mismatch`). + +A refusal at steps 1 to 8 in any segment, the initialization segment included, carries §19.7's +`livevideo.manifest.invalid`. + +### 11.3 The initialization segment + +1. Its boxes read: otherwise `segment_malformed`. It holds an `mdat`, or is not `ftyp`, a C2PA + `uuid` box and `moov` with one track whose `tkhd`, `mdhd` and `trex` read, then optionally + a second C2PA `uuid` box: otherwise `init_invalid`. +2. Its manifest verifies (§11.2): the store of the first C2PA box, of purpose `manifest`, or + `original` where a second box follows. +3. For a track, the stream assertion reads as §5.1 has it; for a take, the take assertion as + §5.3 has it: otherwise `stream_assertion_invalid`. The stream identifier follows from it + (§3.4), and the manifest's identifier is §4.2's of role 0: otherwise `manifest_id_mismatch`. +4. The digital source type and the AI disclosures are the label's (§9.3). +5. Every session key reads as §7.1 has it, its `kid` is its `x`, and its `signerBinding` + verifies under it over the claim signer's end-entity certificate, and no two keys share an + `x`: otherwise `session_key_invalid`. For a take, a session key is the take assertion's + `device`. +6. Where a second C2PA box follows, of purpose `update` (§4.7): its store reads as one update + manifest, whose claim and signature verify as §11.2 steps 1 to 6 have them, a failure of + form being `update_manifest_invalid`; its identifier is §4.2's of role 3 + (`manifest_id_mismatch`); it holds exactly the ingredient and the repository receipt of + §4.7, the ingredient naming the initialization manifest and its claim signature by their + hashes, and the receipt's JSON reading as §4.7 writes it, naming that manifest and its hash + (`update_manifest_invalid`); the repository is one the verifier accepts, and the receipt of + inclusion verifies under its key for the entry `H` (`repository_receipt_invalid`). + +### 11.4 Each segment + +For the segment *k* received, of group *g* for a track: + +1. Its boxes read, and after a leading C2PA Manifest Box and then a VSI box, each where present, + they are one or more chunks as §3.2 lays them out: otherwise `segment_malformed`. It holds + neither C2PA box: `provenance_missing`. Its `moof`s carry consecutive `sequence_number`s and + the track's `track_ID`: otherwise `segment_malformed`. +2. Its sequence number exceeds the previous segment's, and, for a track, *g* exceeds the + previous segment's group: otherwise `sequence_not_increasing`. +3. Where it holds a C2PA Manifest Box, the manifest verifies (§11.2), its identifier is §4.2's, + of role 1 where it holds a live video segment assertion and 2 where it does not, for the + segment's sequence number (`manifest_id_mismatch`), its hard binding's `sequenceNumber` is + the segment's sequence number (`sequence_mismatch`), and then: + - with a live video segment assertion (§6): its `sequenceNumber` is the segment's sequence + number (`sequence_mismatch`); its `streamId` is the stream's (`stream_id_mismatch`); + `continuityMethod` is `c2pa.manifestId` and `previousManifestId` is present + (`continuity_invalid`); and `previousManifestId` is the identifier of the previous + segment's manifest, or the initialization segment's for the first (`continuity_broken`). + For a track the group assertion reads (§11.2 step 7) and is checked at step 6; for a take + the take assertion's `take`, `room` and `device` are the initialization manifest's + (`stream_assertion_invalid`); + - without one (§7.3): the segment holds a VSI box, as the method that delivers keys signs + its segments (`provenance_missing`); the manifest holds the stream or take assertion, + equal to the initialization manifest's (`stream_assertion_invalid`), and new session + keys, each read as §11.3 step 5 reads one, none with the `x` of a key already held, and + each with a `minSequenceNumber` not below the segment's sequence number + (`session_key_invalid`); the keys are added to those held, and the manifest becomes the + active one; + - its digital source type and AI disclosures are the label's (§9.3). +4. Where it holds a VSI box: + - the box has §7.2's fields, the verifiable-segment-info reads as a `COSE_Sign1_Tagged` + with an EdDSA algorithm, a `kid` and a payload present, and the payload as a segment-info + map with §4.5's exclusions and `alg`: otherwise `segment_info_invalid`; + - the verifier holds a session key of that `kid`: otherwise `session_key_unknown`; + - the signature verifies under it, by the strict rule: otherwise + `segment_info_signature_failed`; + - the key is the current key for the segment (§7.3), its `minSequenceNumber` is not above + the segment's sequence number, and the segment's presentation instant (§3.5) is within + its validity: otherwise `session_key_out_of_range`; + - the map's `sequenceNumber`, and its `bmffHash`'s, is the segment's sequence number: + otherwise `sequence_mismatch`; + - its `manifestId` is the active manifest's (§7.2): otherwise `segment_info_invalid`; + - its `bmffHash.hash` is the segment's hash (§4.5): otherwise `segment_hash_mismatch`. +5. For a track: the group receipt of every group from `firstGroup` to *g* is held + (`receipt_missing`), and only one of group *g* (`receipt_invalid`), and the sequence number + is *s*(*g*) (§3.3). For a take: the sequence + number is one more than the last `sequence_number` of the previous segment, or 1 for the + first. Otherwise `sequence_mismatch`. +6. For a track, of group *g*'s receipt (§8): it is of the legacy form (jwp-receipts §2.3; its + total carries no class of its own, §8.1), it verifies, has the stream's `trackAlias` and + `group_id` *g*, and links to the held receipt of the group before it under the same key, or + has a `null` parent where that receipt is under another key or *g* is `firstGroup` + (`receipt_invalid`); its `leaf_count` is the number of chunks and its `root_hash` the root + over the chunks' leaves, and, for a per-segment manifest, the group assertion names it + (`receipt_mismatch`); for a segment with a VSI box, its `signer_pubkey` is the session key + that signed the segment-info map (`receipt_signer_mismatch`); and the group's object + receipts are one per chunk, each accepted against its chunk (`object_receipt_invalid`). + +### 11.5 The end of the stream + +For a track, every group from `firstGroup` to the last of which the verifier holds a receipt +has a segment: otherwise `segment_missing`, at the first group without one. + +### 11.6 What an accepted stream states + +The stream identifier; its digital source type; the initialization manifest's registration, +where it carries one: the repository's base URI, the tree size and the leaf index; and, for +each segment in order, its sequence +number, its group (a track) or chunk index (a take), the methods it carries (`manifest`, +`vsi` or both), the identifier of its active manifest, its number of chunks, and the root of +its group (a track) or the SHA-256 of its take chunk (a take: chunk 0 is the initialization +segment followed by segment 0). + +## 12. Reasons + +| reason | raised at | C2PA 2.4 §15 code | §19.7 code | +|---|---|---|---| +| `segment_malformed` | §11.3 step 1, §11.4 step 1 | — | `livevideo.segment.invalid` | +| `init_invalid` | §11.3 step 1 | — | `livevideo.init.invalid` | +| `provenance_missing` | §11.4 steps 1, 3 | — | `livevideo.segment.invalid` | +| `manifest_malformed` | §11.2 steps 1, 6, 7; §11.4 step 3 | `claim.malformed` | `livevideo.manifest.invalid` | +| `credential_invalid` | §11.2 step 2 | `signingCredential.invalid` | `livevideo.manifest.invalid` | +| `credential_untrusted` | §11.2 step 3 | `signingCredential.untrusted` | `livevideo.manifest.invalid` | +| `claim_signature_mismatch` | §11.2 step 4 | `claimSignature.mismatch` | `livevideo.manifest.invalid` | +| `credential_expired` | §11.2 step 5 | `claimSignature.outsideValidity` | `livevideo.manifest.invalid` | +| `assertion_hash_mismatch` | §11.2 step 6 | `assertion.hashedURI.mismatch` | `livevideo.manifest.invalid` | +| `hard_binding_mismatch` | §11.2 step 8 | `assertion.bmffHash.mismatch` | `livevideo.manifest.invalid` | +| `manifest_id_mismatch` | §4.2, §11.3 steps 3, 6, §11.4 step 3 | — | — | +| `stream_assertion_invalid` | §11.3 step 3, §11.4 step 3 | — | — | +| `source_type_mismatch` | §9.3, §11.3 step 4, §11.4 step 3 | — | — | +| `ai_disclosure_missing` | §9.3, §11.3 step 4, §11.4 step 3 | — | — | +| `update_manifest_invalid` | §4.7, §11.3 step 6 | `manifest.update.invalid` | `livevideo.manifest.invalid` | +| `repository_receipt_invalid` | §4.7, §11.3 step 6 | — | — | +| `session_key_invalid` | §11.3 step 5, §11.4 step 3 | — | `livevideo.sessionkey.invalid` | +| `sequence_not_increasing` | §11.4 step 2 | — | `livevideo.assertion.invalid` | +| `sequence_mismatch` | §11.4 steps 3, 4, 5 | — | — | +| `stream_id_mismatch` | §11.4 step 3 | — | `livevideo.assertion.invalid` | +| `continuity_invalid` | §11.4 step 3 | — | `livevideo.continuityMethod.invalid` | +| `continuity_broken` | §11.4 step 3 | — | `livevideo.segment.invalid` | +| `segment_info_invalid` | §11.4 step 4 | — | `livevideo.segment.invalid` | +| `session_key_unknown` | §11.4 step 4 | — | `livevideo.segment.invalid` | +| `segment_info_signature_failed` | §11.4 step 4 | — | `livevideo.segment.invalid` | +| `session_key_out_of_range` | §11.4 step 4 | — | `livevideo.segment.invalid` | +| `segment_hash_mismatch` | §11.4 step 4 | `assertion.bmffHash.mismatch` | `livevideo.segment.invalid` | +| `receipt_missing` | §11.4 step 5 | — | — | +| `receipt_invalid` | §11.4 step 6 | — | — | +| `receipt_mismatch` | §11.4 step 6 | — | — | +| `receipt_signer_mismatch` | §11.4 step 6 | — | — | +| `object_receipt_invalid` | §11.4 step 6 | — | — | +| `segment_missing` | §11.5 | — | — | + +The two code columns are what a C2PA validator reports for the same defect where C2PA checks +it: §15's failure code for a check of a manifest, and §19.7's for a check of live video, which +for a manifest's own failure in a segment is `livevideo.manifest.invalid` (§19.7.1). A refusal +reports both. `—` marks a check C2PA does not make: the rules of this extension; the sequence +numbers' match with `mfhd.sequence_number`, which §19.4.1 requires of a writer and §19.7 does +not check; the identifiers' derivation; and the repository's proof, whose form §18.27 leaves +to the repository. `update_manifest_invalid` stands for every failure of the update manifest's +form, which §15.10.1.3 reports as `manifest.update.invalid` or `manifest.update.wrongParents`; +it reports the first. `manifest_malformed` stands for every failure of a manifest's form, whose +§15 codes are many (`claim.malformed`, `claim.cbor.invalid`, `claimSignature.missing`, +`algorithm.unsupported`, `assertion.missing`, `claim.hardBindings.missing`, +`assertion.bmffHash.malformed`, …); it reports `claim.malformed`. Over the Verifiable Segment +Info method alone, §19.7.3 states no order check, and `sequence_not_increasing` there is this +extension's rule, reported with §19.7.2's code. + +## 13. Conformance + +[`live-conformance/c2pa-live/`](../live-conformance/c2pa-live/) holds the corpus, written from +fixed seeds by `wai_live_provenance_vectors gen`: `vectors.json`, the trust anchor, and for +each vector its initialization segment, its segments and, for a track, its receipts, each a +file of its own. Every key and certificate is derived from the seeds, and none is a key anyone +holds; every manifest identifier is its stream's (§4.2). The media are the AV1 floor of the +packaging corpus's `video-base-01` (packaging §7, paragraph 7.6), its samples cut into chunks +of at most 15 frames, each group one of its pieces, so every segment begins at a key frame. + +- **Good streams**: a camera track under the per-segment method; the same track under the + Verifiable Segment Info method, with the session key rotated at its third segment by a + manifest in that segment, the receipts' chain restarting under the new key; a track + labelled machine-made, with its AI disclosure; a compositor's output of a camera and that + track; a take under both methods at once, the device's key as its session key; the camera + track with a second packager, under its own claim signer, taking over at the third segment + (§10.3); and the camera track with its initialization manifest registered, its update + manifest and repository receipt after it (§4.7). +- **Negatives**, each a control of one defect: a segment whose bytes changed under each + method; a missing segment under each; a reordered segment; a manifest bound to the wrong + receipt; a stream identifier that differs; an end-entity certificate that has expired, one + whose Extended Key Usage is time-stamping, and one under an anchor the verifier does not + hold; a machine-made track without its AI disclosure, and a compositor's output that names + a source type of captures; a manifest whose identifier is not derived; a repository receipt + signed by a key that is not the repository's, and an update manifest whose ingredient names + another parent; and every other reason of §12. + +A conforming verifier gives every vector the outcome, the reason, the place, the two C2PA +codes and, for an accepted stream, what §11.6 states, as the corpus states them. +`wai_live_provenance_vectors verify` and the test `tests/live_provenance.rs` check that every +file regenerates byte for byte, that no other file is there, that `wai-rs` gives every vector +its outcome, that every reason of §12 refuses a vector, and that §12 lists exactly the +reference's codes. `live-conformance/c2pa_live_verify.py` is the second implementation, +written from this text: it shares no code with `wai-rs`; it reads CBOR, COSE, JUMBF, ISO BMFF +and X.509 DER itself, verifies signatures with WAI's strict rule written from RFC 8032 +(`claim-conformance/_ed25519.py`) and hashes with BLAKE3 from `claim-conformance/_blake3.py`; +gives every vector the same outcome; and mutates copies of the good vectors, one defect each, +and requires every copy to be refused. The README of the corpus records what an independent C2PA validator reports for the +corpus's files. + +## 14. Security and privacy considerations + +- **What is bound to what.** The claim signer's certificate names who stands behind a + manifest; a session key is bound to it by the `signerBinding` and, for a track, signs the + group receipts too, so the C2PA signer, the session key and the receipts' signer are one + chain of statements. Which principal of the room the receipts' key belongs to is the room's + (jwp-receipts §7, live-rooms §4) and is not checked here. +- **Signed is not trusted.** A chain to an anchor the verifier holds is a check of the chain, + not of the anchor: the corpus's anchor is a test anchor, and §14.4's trust lists are the + relying party's choice. +- **Order and loss.** Sequence numbers, the continuity chain and the receipts' chain each + detect a segment lost, repeated or moved; under the Verifiable Segment Info method alone a + segment-info map does not name the one before it, so a lost segment is found by the + receipts (§11.5), which a relay forwards for objects it did not forward (jwp-receipts §4.4). +- **Session keys.** A session key signs every segment of its range. Its validity bounds how + long a stolen key signs; rotation (§7.3) shortens it. §19.7.3 recommends that a verifier + of a very long session check the claim signer's certificate again; this revision checks it + once, at *T*. +- **Registration is a witness.** A repository receipt (§4.7) shows that the repository's log + held the manifest at a tree size under the repository's key; it says nothing of the + manifest's truth, and a verifier that accepts a repository chooses whom it believes about + registration, as it chooses anchors. Consistency between two tree heads, which would show + the log did not fork, is not checked in this revision. +- **Derived identifiers.** Anyone can compute the identifier of a future segment's manifest + (§4.2). That forges nothing: a manifest is bound by its signature and its hard binding, and + a verifier checks both before it reads the identifier. What derivation buys is that a + packager cannot choose an identifier, so two packagers agree on it. +- **No time-stamps.** Without a time-stamp a manifest is valid only while the signer's + certificate is (§15.8): a recording verified after the certificate expires is refused + `credential_expired`. A publisher that keeps recordings SHOULD time-stamp them, which a later + revision adds. +- **The label is a statement.** §9.3 checks that a stream labelled machine-made is marked as + C2PA marks it; it cannot detect a machine-made stream labelled human. The label is signed + with the stream, so a false one is attributable to its signer. +- **Privacy.** A manifest carries the claim signer's certificate and, for a take, the + device's key and the room's id; the stream assertion carries the room id and the publisher's + key in the track name. A stream is public provenance of its publisher; a take's manifests + link the device to the room, as capture-binding §14 says of a take. diff --git a/wai/live-conformance/README.md b/wai/live-conformance/README.md index eff884d3d..1a2e20d09 100644 --- a/wai/live-conformance/README.md +++ b/wai/live-conformance/README.md @@ -17,6 +17,7 @@ and the independent verifier of each, beside it. | [`ads/`](ads/) | [live-ads](../extensions/live-ads.md) (`wai.live.ads`) | `live_ads_verify.py` | | [`e2ee/`](e2ee/) | [confidentiality](../extensions/confidentiality.md) (`wai.live.e2ee`) | `e2ee_verify.py` | | [`watermark/`](watermark/) | [forensic-watermark](../extensions/forensic-watermark.md) (`wai.live.watermark`) | `live_watermark_verify.py` | +| [`c2pa-live/`](c2pa-live/) | [live-provenance](../extensions/live-provenance.md) (`wai.live.provenance`) | `c2pa_live_verify.py` | **Provenance.** Generated here: `wai_live_events_vectors gen` writes `events/`, `wai_live_room_vectors gen` writes `rooms/`, `wai_capture_take_vectors gen` writes @@ -24,11 +25,13 @@ and the independent verifier of each, beside it. writes `recordings/`, `wai_timed_text_vectors gen` writes `captions/` and `wai_live_metering_vectors gen` writes `metering/`, `wai_energy_steering_vectors gen` writes `steering/`, `wai_live_moderation_vectors gen` writes `moderation/`, -`wai_live_watermark_vectors gen` writes `watermark/` and `wai_live_ads_vectors gen` writes -`ads/`, each from records authored in code, from fixed seeds (`events/README.md`, +`wai_live_watermark_vectors gen` writes `watermark/`, `wai_live_ads_vectors gen` writes +`ads/` and `wai_live_provenance_vectors gen` writes `c2pa-live/`, each from records authored +in code, from fixed seeds (`events/README.md`, `rooms/README.md`, `takes/README.md`, `compose/README.md`, `recordings/README.md`, `captions/README.md`, `metering/README.md`, `steering/README.md`, `moderation/README.md`, -`watermark/README.md`, `ads/README.md`). `joule_moq_e2ee_vectors gen` (in +`watermark/README.md`, `ads/README.md`, `c2pa-live/README.md`); `c2pa-live/` cuts its media +from the packaging corpus's AV1 floor. `joule_moq_e2ee_vectors gen` (in `joule-code/joule-code-rs/crates/joule-moq-mls`) writes `e2ee/vectors.json` from fixed seeds, and `e2ee/` also holds the test vectors RFC 9605 and `draft-ietf-moq-secure-objects-01` publish, as printed (`e2ee/README.md`). One recorded record: `metering/relay-record.wmh` and @@ -51,4 +54,5 @@ python3 live_moderation_verify.py moderation python3 live_ads_verify.py ads python3 e2ee_verify.py e2ee python3 live_watermark_verify.py watermark +python3 c2pa_live_verify.py c2pa-live ``` diff --git a/wai/live-conformance/c2pa-live/README.md b/wai/live-conformance/c2pa-live/README.md new file mode 100644 index 000000000..6b6e96c5c --- /dev/null +++ b/wai/live-conformance/c2pa-live/README.md @@ -0,0 +1,83 @@ +# live-conformance/c2pa-live — live provenance + +The corpus of [`extensions/live-provenance.md`](../../extensions/live-provenance.md) §13: C2PA +2.4 §19 live video streams, each an initialization segment and its segments in the order a +verifier receives them, with, for a room track, the JWP group and object receipts of its +groups; the trust anchor every vector is verified under; and what a verifier states of each. + +**Files.** `vectors.json` lists the vectors, each its `name`, `kind` (`track` or `take`), +`init`, `segments` (each a `file` and, for a track, the `group` it came in), `receipts` (a JSON +file of `groups` and `objects` in jwp-receipts §2.4's forms, or `null` for a take) and +`expect`. A file whose bytes another vector already wrote is named by that vector's path, so a +negative that changes one segment holds one file of its own. `anchor.der` is the trust anchor +for `c2pa-kp-claimSigning`, and `time` the instant *T* (§11.1). + +**Equivalence.** A conforming verifier given a vector's files, the anchor and *T* reaches the +outcome `expect` states (§11): + +- `refuse`: the `reason` of §12, `at` (`init`, `segment k` counting received segments from 0, + or `group g`), and the two C2PA codes §12 gives the reason, `c2pa` (§15's) and `livevideo` + (§19.7's), each `null` where §12 gives none; +- `accept`: the `streamId`, the `sourceType`, the `registration` (the `repository`, the + `treeSize` and the `leafIndex` of the receipt of inclusion) or `null`, and for each segment + (§11.6) its + `sequenceNumber`, its `group` (a track) or `chunk` (a take), its `methods` (`manifest`, + `vsi` or `manifest+vsi`), the active `manifestId`, its number of `chunks`, and its group's + `root` or its take chunk's `chunkHash`, in lowercase hexadecimal. + +**The vectors.** + +- Good: `camera-manifests` (the per-segment method), `camera-vsi` (the Verifiable Segment + Info method from group 10, the session key rotated at the third segment by a manifest in + it, the receipts' chain restarting under the new key), `avatar-manifests` (a track labelled + machine-made, with its AI disclosure), `composite-manifests` (a compositor's output of the + camera and the avatar: `compositeSynthetic` and the avatar's disclosure), `take-both` (a + take under both methods at once, the device's key as its session key), `camera-failover` + (the camera track, a second packager under its own claim signer taking over at the third + segment, the continuity chain holding across it because the identifiers are derived) and + `camera-registered` (the camera track's initialization segment with its update manifest + and repository receipt; `repositories` in `vectors.json` names the repository and its key). +- One negative for each reason of §12, and for some reasons more than one; each name says its + defect. + +**Provenance.** `wai_live_provenance_vectors gen` writes every file but this one from fixed +seeds: every key is SHA-256 of a label and no key is one anyone holds; the certificates are +issued under the corpus's own test anchor; the media are the AV1 floor of +`pack-conformance/packs/video-base-01`, its samples cut into chunks of at most 15 frames, each +group one of the floor's pieces, renumbered and retimed across the stream. + +**Checked by.** `wai_live_provenance_vectors verify` and `tests/live_provenance.rs` in +`wai-rs` (every file regenerates byte for byte and no other is here; every vector gets its +outcome; every reason refuses one; §12 lists the reference's codes), and +`../c2pa_live_verify.py`, the second implementation, written from the extension's text, which +also requires every mutated copy of a good vector to be refused. + +**An independent C2PA validator.** The corpus's files were also read with the C2PA +command-line tool, version 0.27.22, given `anchor.der` as its trust anchor, on 2026-10-21: + +- every good vector's initialization segment validates: state `Trusted`, with + `assertion.bmffHash.match`, `assertion.hashedURI.match`, `claimSignature.validated`, + `claimSignature.insideValidity` and `signingCredential.trusted`. The manifest store, the + claim, its signature, the certificate chain and the BMFF hash over the `moov` are therefore + read the same way by an implementation that shares nothing with this one; +- `camera-registered/init.mp4` validates too, state `Trusted`, its active manifest the update + manifest: the tool reads the ingredient (`ingredient.manifest.validated`) and its validation + results. Before the ingredient carried them it reported `assertion.ingredient.malformed`, + which C2PA 2.4 §18.16.12.4.3 requires; `update-ingredient/init.mp4` is reported + `ingredient.manifest.mismatch`, where this corpus states `update_manifest_invalid`; + `repository-receipt-forged/init.mp4` is reported `Trusted`: the receipt's proof is the + repository's form (§18.27), which the tool does not check; +- `cert-untrusted/init.mp4` is reported `signingCredential.untrusted`, as here; +- `cert-expired/init.mp4` is reported `signingCredential.expired`, a code C2PA 2.4 does not + list; §15.8 names this failure `claimSignature.outsideValidity`, which this corpus states; +- `cert-wrong-eku/init.mp4`, whose end entity is valid for `id-kp-timeStamping` alone, is + reported `Trusted`: the tool's default list of accepted extended key usages includes + time-stamping and OCSP signing. C2PA 2.4 §14.5.1.1 makes a certificate valid for either of + those purposes valid for no other, and §14.5.1.2 requires a claim signer's to hold an EKU of + the validator's configuration, so this corpus refuses it `credential_invalid`; +- the tool reads no media segment: a file that does not begin with `ftyp` is not one it + opens, so it does not verify §19 per-segment manifests, segment-info maps or session keys. + The segments are checked by the two implementations above alone. + +The tool reports a claim signature as failed when the end-entity certificate's subject holds +no organization name, which C2PA does not require; the corpus's names all hold one (§4.4). diff --git a/wai/live-conformance/c2pa-live/ai-disclosure-missing/init.mp4 b/wai/live-conformance/c2pa-live/ai-disclosure-missing/init.mp4 new file mode 100644 index 000000000..022d5d090 Binary files /dev/null and b/wai/live-conformance/c2pa-live/ai-disclosure-missing/init.mp4 differ diff --git a/wai/live-conformance/c2pa-live/ai-disclosure-missing/seg-00.m4s b/wai/live-conformance/c2pa-live/ai-disclosure-missing/seg-00.m4s new file mode 100644 index 000000000..f1b867103 Binary files /dev/null and b/wai/live-conformance/c2pa-live/ai-disclosure-missing/seg-00.m4s differ diff --git a/wai/live-conformance/c2pa-live/ai-disclosure-missing/seg-01.m4s b/wai/live-conformance/c2pa-live/ai-disclosure-missing/seg-01.m4s new file mode 100644 index 000000000..b644c106c Binary files /dev/null and b/wai/live-conformance/c2pa-live/ai-disclosure-missing/seg-01.m4s differ diff --git a/wai/live-conformance/c2pa-live/ai-disclosure-missing/seg-02.m4s b/wai/live-conformance/c2pa-live/ai-disclosure-missing/seg-02.m4s new file mode 100644 index 000000000..01c021e01 Binary files /dev/null and b/wai/live-conformance/c2pa-live/ai-disclosure-missing/seg-02.m4s differ diff --git a/wai/live-conformance/c2pa-live/anchor.der b/wai/live-conformance/c2pa-live/anchor.der new file mode 100644 index 000000000..4f20e745f Binary files /dev/null and b/wai/live-conformance/c2pa-live/anchor.der differ diff --git a/wai/live-conformance/c2pa-live/assertion-altered/seg-01.m4s b/wai/live-conformance/c2pa-live/assertion-altered/seg-01.m4s new file mode 100644 index 000000000..fcd7ad78c Binary files /dev/null and b/wai/live-conformance/c2pa-live/assertion-altered/seg-01.m4s differ diff --git a/wai/live-conformance/c2pa-live/avatar-manifests/init.mp4 b/wai/live-conformance/c2pa-live/avatar-manifests/init.mp4 new file mode 100644 index 000000000..eb286dd5e Binary files /dev/null and b/wai/live-conformance/c2pa-live/avatar-manifests/init.mp4 differ diff --git a/wai/live-conformance/c2pa-live/avatar-manifests/receipts.json b/wai/live-conformance/c2pa-live/avatar-manifests/receipts.json new file mode 100644 index 000000000..8c6d7934f --- /dev/null +++ b/wai/live-conformance/c2pa-live/avatar-manifests/receipts.json @@ -0,0 +1,163 @@ +{ + "groups": [ + { + "kind": "group", + "track_alias": 2, + "group_id": 0, + "root_hash": "99c1ee93a49d44a71d04407c59b1f688f1ccd160c388ea2a94a4ebfe0ee162e0", + "leaf_count": 2, + "joules_total": 0, + "group_start": "2026-10-20T11:00:00.000000Z", + "group_end": "2026-10-20T11:00:01.000000Z", + "signer_pubkey": "cb1e7aca7605dd3f10a7e215063c7bca869b83ba11c259cb6bc3a815836f67f5", + "signer_id": "corpus publisher", + "sig": "57f56e2e571f43062b6be26642e369d80633de7f813502420868a2ce2d750e085dfde6501fb064c1c34387558fea8e793f96ab1fb0f17410281a31ef7533970f", + "parent_receipt_hash": null + }, + { + "kind": "group", + "track_alias": 2, + "group_id": 1, + "root_hash": "3e5165b1371e3693e542dca31f1aeba5c362bff06c7c64b39237913101faedb9", + "leaf_count": 2, + "joules_total": 0, + "group_start": "2026-10-20T11:00:01.000000Z", + "group_end": "2026-10-20T11:00:02.000000Z", + "signer_pubkey": "cb1e7aca7605dd3f10a7e215063c7bca869b83ba11c259cb6bc3a815836f67f5", + "signer_id": "corpus publisher", + "sig": "abd8b03222d2c7c9f0f0068d6d48f258f12d8d1d2f0cc8bab92d9bfe5c721a435671217e4cdc1b9b6eabb219c548affd069e4277f13ba819e884d850611a3c09", + "parent_receipt_hash": "2d8f6b9fb2434236acce8ad994a84cd4160971e82c60e239fb25e5c7cb03dcae" + }, + { + "kind": "group", + "track_alias": 2, + "group_id": 2, + "root_hash": "c605d0c6ba8546635e3783f9a130355667c68085e44f043ae18df8ba974ce6b3", + "leaf_count": 3, + "joules_total": 0, + "group_start": "2026-10-20T11:00:02.000000Z", + "group_end": "2026-10-20T11:00:03.600000Z", + "signer_pubkey": "cb1e7aca7605dd3f10a7e215063c7bca869b83ba11c259cb6bc3a815836f67f5", + "signer_id": "corpus publisher", + "sig": "432ee5edc46ea0eef4d35b8b0751dfb13668691919eac0b59bbeaa09c7c418dd3586df1cd698d818d8eed5ec469fb354325bf1d6630ad0773c8a0afe08c5eb02", + "parent_receipt_hash": "16fe87519f983c0484d60e031f0e6227854697ed783c90ee10a5845b478b95e2" + } + ], + "objects": [ + { + "kind": "object", + "track_alias": 2, + "group_id": 0, + "object_id": 0, + "content_hash": "5a942f3a7fd658d2c4ee8b4db9dd11c605a56c6dc3718f869b049faa8850ec7d", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "b2659df6816d8da90543f3169c36eccd5b78a0a4294232f38e5811c4b13a34ac" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 2, + "group_id": 0, + "object_id": 1, + "content_hash": "99ec5ab9416df8a59ef5750c22e0e4f18b4eda8fda7172fa248831592b35aa00", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "38d035a1e790abbca8af2f04899b88f8dad09fdc08e9c52b83348a2be4e85f39" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 2, + "group_id": 1, + "object_id": 0, + "content_hash": "463b72614a6be56062a09fe4b7d32eac97fbbdc236adf689c9cabfa8c7c89863", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "68bf7ad2e58084a8cab1a2ea0796e6ed8d3d269efc010991998f73be81b1c517" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 2, + "group_id": 1, + "object_id": 1, + "content_hash": "2cb216ed938cdccf0951b31101fb568ca2e239bb104a48e9814eb9d029ea75a3", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "961a306577f65e0f6acc69587c643b14eded1b0a8f4ee0956b0a6a6fad548871" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 2, + "group_id": 2, + "object_id": 0, + "content_hash": "a5997aaab20bc8bcc8cdac4fc3c786b92352433b0b8b22e2214fbe628efab6d8", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "2e78c4feba0e79ef8596f688bc7826dfbb3868b21af78bb679b44ff4583e83b2", + "7e95960d0d4d629b92965a7ca6f5260167330632c6f0ac3ebe6e21712766b982" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 2, + "group_id": 2, + "object_id": 1, + "content_hash": "3ffb00ddeae6286fb93e015f05a0f13ef8491ec1fe36c7238ba0842296ebf58d", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "9bb3706bca48afff149104cd93664ee02c35cc7b1ef79bf6d9ded3e1af11fe2d", + "7e95960d0d4d629b92965a7ca6f5260167330632c6f0ac3ebe6e21712766b982" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 2, + "group_id": 2, + "object_id": 2, + "content_hash": "d1bc55a8c7ebfa6bab73ecdf2c8abed915c9b4f033be93acf2664bd7be2b7738", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "3edb959cd3da90f4a7bdb2238b1a3a938caf77445245b23a73918b48d9068d26", + "9e9a526eb74468568b1c429128744208f640c7e06b2879b5209ca3478b999579" + ], + "direction_bits": 2, + "leaf_index": 2 + } + } + ] +} diff --git a/wai/live-conformance/c2pa-live/avatar-manifests/seg-00.m4s b/wai/live-conformance/c2pa-live/avatar-manifests/seg-00.m4s new file mode 100644 index 000000000..d95a4a264 Binary files /dev/null and b/wai/live-conformance/c2pa-live/avatar-manifests/seg-00.m4s differ diff --git a/wai/live-conformance/c2pa-live/avatar-manifests/seg-01.m4s b/wai/live-conformance/c2pa-live/avatar-manifests/seg-01.m4s new file mode 100644 index 000000000..bcc2c9b3d Binary files /dev/null and b/wai/live-conformance/c2pa-live/avatar-manifests/seg-01.m4s differ diff --git a/wai/live-conformance/c2pa-live/avatar-manifests/seg-02.m4s b/wai/live-conformance/c2pa-live/avatar-manifests/seg-02.m4s new file mode 100644 index 000000000..bdf4d2f2b Binary files /dev/null and b/wai/live-conformance/c2pa-live/avatar-manifests/seg-02.m4s differ diff --git a/wai/live-conformance/c2pa-live/bytes-changed-manifests/seg-02.m4s b/wai/live-conformance/c2pa-live/bytes-changed-manifests/seg-02.m4s new file mode 100644 index 000000000..bba887640 Binary files /dev/null and b/wai/live-conformance/c2pa-live/bytes-changed-manifests/seg-02.m4s differ diff --git a/wai/live-conformance/c2pa-live/bytes-changed-vsi/seg-01.m4s b/wai/live-conformance/c2pa-live/bytes-changed-vsi/seg-01.m4s new file mode 100644 index 000000000..8722495f8 Binary files /dev/null and b/wai/live-conformance/c2pa-live/bytes-changed-vsi/seg-01.m4s differ diff --git a/wai/live-conformance/c2pa-live/camera-failover/receipts.json b/wai/live-conformance/c2pa-live/camera-failover/receipts.json new file mode 100644 index 000000000..e2de4c348 --- /dev/null +++ b/wai/live-conformance/c2pa-live/camera-failover/receipts.json @@ -0,0 +1,262 @@ +{ + "groups": [ + { + "kind": "group", + "track_alias": 1, + "group_id": 0, + "root_hash": "d7c41cd63b26b976cc556f9f5a5ecb1151421210aa5979e124958e951e7ed309", + "leaf_count": 4, + "joules_total": 0, + "group_start": "2026-10-20T11:00:00.000000Z", + "group_end": "2026-10-20T11:00:02.400000Z", + "signer_pubkey": "b5b033e2c5d98007d13e75aeefe0abd2d89c0e186a1484f3dae6d6e43ca312f0", + "signer_id": "corpus publisher", + "sig": "02644ca827c8ea3daa2e72a421bb949bb67b263b6feec2c02bd59427e42abec4648cdd9b8706b57982982d32cdb48f4dd8225847d6e7ee6a14e24d3ae7c30b0b", + "parent_receipt_hash": null + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 1, + "root_hash": "73ff462e0454b7c6d37af29e979c3a7dcad4d128e15c15035f601c3819d12c71", + "leaf_count": 2, + "joules_total": 0, + "group_start": "2026-10-20T11:00:02.400000Z", + "group_end": "2026-10-20T11:00:03.400000Z", + "signer_pubkey": "b5b033e2c5d98007d13e75aeefe0abd2d89c0e186a1484f3dae6d6e43ca312f0", + "signer_id": "corpus publisher", + "sig": "0f906851a29aacaab43f606e74f0983047a329b36e8999ea91638e7f30691a9c9e566cf12071a169ce352675a69505b3460df9abdf86f7335a1ec703cadae70c", + "parent_receipt_hash": "77806cf33682f4544f7fea3101005e9bfdd290ecebe78dd7b0e24b570f52d521" + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 2, + "root_hash": "7db0b6d4458309c8be8df8080ce08452bdccc79aed00d85cec614ce5adc83009", + "leaf_count": 4, + "joules_total": 0, + "group_start": "2026-10-20T11:00:03.400000Z", + "group_end": "2026-10-20T11:00:05.400000Z", + "signer_pubkey": "b5b033e2c5d98007d13e75aeefe0abd2d89c0e186a1484f3dae6d6e43ca312f0", + "signer_id": "corpus publisher", + "sig": "421706dcece46963a6283d15f0269b711e2125ffee005507d18981f8858d7102d4898efed0778ae997fadf06da29d31e583662b69ad0eb70c654ddaa538dca01", + "parent_receipt_hash": "7e45ba5b32c2baeef1b55d2473a0b8096b6d0e156f478f051e859403337c4dbb" + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 3, + "root_hash": "293931ddebcd8e3cca482475e3b1a93cc2f225427538a3e5da476bf5eae3b4d0", + "leaf_count": 2, + "joules_total": 0, + "group_start": "2026-10-20T11:00:05.400000Z", + "group_end": "2026-10-20T11:00:06.400000Z", + "signer_pubkey": "b5b033e2c5d98007d13e75aeefe0abd2d89c0e186a1484f3dae6d6e43ca312f0", + "signer_id": "corpus publisher", + "sig": "876f172a3c59e83575552bd9082e2252c01441100d75bbd65d8ea0f5cf3cfbb04a6f61c00f8898e2e6bfba2e947ab1efdf9122520faba0a6e4437d74d9293d0f", + "parent_receipt_hash": "e47d3c48a3688255bd2f89fa71a5af8d044742eae7742d18f2c862654d9b2968" + } + ], + "objects": [ + { + "kind": "object", + "track_alias": 1, + "group_id": 0, + "object_id": 0, + "content_hash": "ed3e285f8b0590025be3a310fd384642973f7ab4cc2531ffb3767dc65decbc87", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "84bd9285e1a79ab4d31fc47abcc3d4d7385c4ee38be89090f67b3d0da066fe03", + "a3f56e2717b3cae3def088678e67d4b8266e38cecd14b6a29f03653ef1d01996" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 0, + "object_id": 1, + "content_hash": "c3c0621e9c77910ba45e7c7d6060ca3baa48454877a3c2947c1327f0b5042960", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "476483f91e3ae73abe1fa7137178680a3350d30a86d619ccde04ce7b9d4672f1", + "a3f56e2717b3cae3def088678e67d4b8266e38cecd14b6a29f03653ef1d01996" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 0, + "object_id": 2, + "content_hash": "313e889ebc1aaa08f347a248bae963fdb3ce7cb1ebffd13a0ea862d4833184de", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "2212db8269bac3d91fd34abfda366d918d848c5d0b1f5b900b1421d22cbe0225", + "a954156f25c6e4a22d745879b4e028f3a1039875a5c784423c876399b4e4e2fb" + ], + "direction_bits": 2, + "leaf_index": 2 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 0, + "object_id": 3, + "content_hash": "c9b87a65ceabd21a394a62f0b27677207fa40002294635bc4c19235dfdae2b35", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "b760b6cef18489c4db7a83675f09424498848a4e93f112700ac075dbb1ff05df", + "a954156f25c6e4a22d745879b4e028f3a1039875a5c784423c876399b4e4e2fb" + ], + "direction_bits": 3, + "leaf_index": 3 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 1, + "object_id": 0, + "content_hash": "d26598db3232eabc5dd6f8e733f3c684a4ec2b7974b185d88ac3bcd3c020e167", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "0aae05deca890dcf94db748d801465bb0b62247cf070cd34016ed810b7b5e3b8" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 1, + "object_id": 1, + "content_hash": "56421ee48deb82f5692315209c0be565c75fde7531c924e68ee0159515de135b", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "d22a0c40cbd68fa52e6afb9ecb920e80fabb689963214e037059b2c45ecfe151" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 2, + "object_id": 0, + "content_hash": "7032e9f88dc8b7f736553b7a9c7236669833b4e633ca24d28ed5c839b692b716", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "5e4b0d6ce028d04f28ab23761ce1f3b0f3f5e4d014f4b4f9b2fdfbd9a6994a00", + "e4e9b40d3ba168108dc827586994c75940b4168078970493d41828b60c816605" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 2, + "object_id": 1, + "content_hash": "07508165a8199e0362b74c498999eea3b023cb67f89b663250ab0677dcb47966", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "df44809333587e5f04510b40dd099829f3214d783044dd56a5683f56a4a5dafd", + "e4e9b40d3ba168108dc827586994c75940b4168078970493d41828b60c816605" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 2, + "object_id": 2, + "content_hash": "b20995ab22a57bc11cfdc93867ba34e004a038fa39b8517e7b09354f9097e1cc", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "afcffc4feb8366e461d33125a05a371ffeeb6d52e8a66f823cd7682788ccd6fc", + "4f8188470c70525b9004625ecb9e384b61df9fb5c4c7cad082e408ea3754ccec" + ], + "direction_bits": 2, + "leaf_index": 2 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 2, + "object_id": 3, + "content_hash": "c4156696922509b57ff04f290a8f880b2c7f9c567d72ac7ef8aa1dd4a3f9702d", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "f0be86f54fbf3a67c5ba7b56d3efb346e7efc1dcd75eec634be99ae33afd52bc", + "4f8188470c70525b9004625ecb9e384b61df9fb5c4c7cad082e408ea3754ccec" + ], + "direction_bits": 3, + "leaf_index": 3 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 3, + "object_id": 0, + "content_hash": "14400b38229ea3c7ba3a9df42a9ad39cc3a9d9a26cf086d1474237e3aaa4f0a3", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "1dd9e51b1130f7abaaa90a130e278f4a85e62f423e0ae95cb122a6b77cccd9f3" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 3, + "object_id": 1, + "content_hash": "70dee7a8e6962e9ea7e80b4b8591bf565799fa0b835ad5e4933fc6ffcf83a81a", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "8fe3347c01ade8ed1f58502398c60ab6b8cc6b68017b2632bde4fd3631edb286" + ], + "direction_bits": 1, + "leaf_index": 1 + } + } + ] +} diff --git a/wai/live-conformance/c2pa-live/camera-failover/seg-00.m4s b/wai/live-conformance/c2pa-live/camera-failover/seg-00.m4s new file mode 100644 index 000000000..e4a669792 Binary files /dev/null and b/wai/live-conformance/c2pa-live/camera-failover/seg-00.m4s differ diff --git a/wai/live-conformance/c2pa-live/camera-failover/seg-01.m4s b/wai/live-conformance/c2pa-live/camera-failover/seg-01.m4s new file mode 100644 index 000000000..9d18d1de0 Binary files /dev/null and b/wai/live-conformance/c2pa-live/camera-failover/seg-01.m4s differ diff --git a/wai/live-conformance/c2pa-live/camera-failover/seg-02.m4s b/wai/live-conformance/c2pa-live/camera-failover/seg-02.m4s new file mode 100644 index 000000000..44d12d57e Binary files /dev/null and b/wai/live-conformance/c2pa-live/camera-failover/seg-02.m4s differ diff --git a/wai/live-conformance/c2pa-live/camera-failover/seg-03.m4s b/wai/live-conformance/c2pa-live/camera-failover/seg-03.m4s new file mode 100644 index 000000000..d0a752085 Binary files /dev/null and b/wai/live-conformance/c2pa-live/camera-failover/seg-03.m4s differ diff --git a/wai/live-conformance/c2pa-live/camera-manifests/init.mp4 b/wai/live-conformance/c2pa-live/camera-manifests/init.mp4 new file mode 100644 index 000000000..869b8c63f Binary files /dev/null and b/wai/live-conformance/c2pa-live/camera-manifests/init.mp4 differ diff --git a/wai/live-conformance/c2pa-live/camera-manifests/receipts.json b/wai/live-conformance/c2pa-live/camera-manifests/receipts.json new file mode 100644 index 000000000..8f7060d7c --- /dev/null +++ b/wai/live-conformance/c2pa-live/camera-manifests/receipts.json @@ -0,0 +1,262 @@ +{ + "groups": [ + { + "kind": "group", + "track_alias": 1, + "group_id": 0, + "root_hash": "d7c41cd63b26b976cc556f9f5a5ecb1151421210aa5979e124958e951e7ed309", + "leaf_count": 4, + "joules_total": 0, + "group_start": "2026-10-20T11:00:00.000000Z", + "group_end": "2026-10-20T11:00:02.400000Z", + "signer_pubkey": "7772ae8959f2a940a9f4e895d8ba7cf958632cb3ad7f98625c25b5a86fb21d6b", + "signer_id": "corpus publisher", + "sig": "f6e30f65fceac9b6ada8dcac2cc2570dc0f995a47510f1f8a9b059df4c8078cd22f9dcadcfe9b6fe63e09cfb66456ee52ec95d888eb9ee97da9d7d9401ffd502", + "parent_receipt_hash": null + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 1, + "root_hash": "73ff462e0454b7c6d37af29e979c3a7dcad4d128e15c15035f601c3819d12c71", + "leaf_count": 2, + "joules_total": 0, + "group_start": "2026-10-20T11:00:02.400000Z", + "group_end": "2026-10-20T11:00:03.400000Z", + "signer_pubkey": "7772ae8959f2a940a9f4e895d8ba7cf958632cb3ad7f98625c25b5a86fb21d6b", + "signer_id": "corpus publisher", + "sig": "726b6d73a220c9145b0dc85ef8b8f165d1dda249b61dac2f4e4182da953c5d7da1d05fccfbacdf4f16831898a019386175216a80f0bb61f65e1f66f8799bc80a", + "parent_receipt_hash": "351b15632022ea1de17b0625f29e1b5da5fd1f8062f08e2ac71592a3a06ed278" + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 2, + "root_hash": "7db0b6d4458309c8be8df8080ce08452bdccc79aed00d85cec614ce5adc83009", + "leaf_count": 4, + "joules_total": 0, + "group_start": "2026-10-20T11:00:03.400000Z", + "group_end": "2026-10-20T11:00:05.400000Z", + "signer_pubkey": "7772ae8959f2a940a9f4e895d8ba7cf958632cb3ad7f98625c25b5a86fb21d6b", + "signer_id": "corpus publisher", + "sig": "efb642334c4f1bd9e314cfa40b7ebad09b0a1bcf2755f9f53aff673d05c9e6b29447da8fd211ef0f7688f5fd66dc124b4a46f9ca6b41c92d04577c7239066004", + "parent_receipt_hash": "34fc9a08f1f5be49823e319385542696b4d388088d0117b8823a58ed6aed811f" + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 3, + "root_hash": "293931ddebcd8e3cca482475e3b1a93cc2f225427538a3e5da476bf5eae3b4d0", + "leaf_count": 2, + "joules_total": 0, + "group_start": "2026-10-20T11:00:05.400000Z", + "group_end": "2026-10-20T11:00:06.400000Z", + "signer_pubkey": "7772ae8959f2a940a9f4e895d8ba7cf958632cb3ad7f98625c25b5a86fb21d6b", + "signer_id": "corpus publisher", + "sig": "0652205c00a64d57cf0e33d358c22319c3bb39e37d3d3a6186241bd9a35a46e040024ec8c5c7c17fa4c0ee3135bb229e7a46708651cf4b225266ded20b21bb09", + "parent_receipt_hash": "6bea20c763d58218a72402e7d6d75ef9db460b3c62258b26e1a49762800c9516" + } + ], + "objects": [ + { + "kind": "object", + "track_alias": 1, + "group_id": 0, + "object_id": 0, + "content_hash": "ed3e285f8b0590025be3a310fd384642973f7ab4cc2531ffb3767dc65decbc87", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "84bd9285e1a79ab4d31fc47abcc3d4d7385c4ee38be89090f67b3d0da066fe03", + "a3f56e2717b3cae3def088678e67d4b8266e38cecd14b6a29f03653ef1d01996" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 0, + "object_id": 1, + "content_hash": "c3c0621e9c77910ba45e7c7d6060ca3baa48454877a3c2947c1327f0b5042960", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "476483f91e3ae73abe1fa7137178680a3350d30a86d619ccde04ce7b9d4672f1", + "a3f56e2717b3cae3def088678e67d4b8266e38cecd14b6a29f03653ef1d01996" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 0, + "object_id": 2, + "content_hash": "313e889ebc1aaa08f347a248bae963fdb3ce7cb1ebffd13a0ea862d4833184de", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "2212db8269bac3d91fd34abfda366d918d848c5d0b1f5b900b1421d22cbe0225", + "a954156f25c6e4a22d745879b4e028f3a1039875a5c784423c876399b4e4e2fb" + ], + "direction_bits": 2, + "leaf_index": 2 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 0, + "object_id": 3, + "content_hash": "c9b87a65ceabd21a394a62f0b27677207fa40002294635bc4c19235dfdae2b35", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "b760b6cef18489c4db7a83675f09424498848a4e93f112700ac075dbb1ff05df", + "a954156f25c6e4a22d745879b4e028f3a1039875a5c784423c876399b4e4e2fb" + ], + "direction_bits": 3, + "leaf_index": 3 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 1, + "object_id": 0, + "content_hash": "d26598db3232eabc5dd6f8e733f3c684a4ec2b7974b185d88ac3bcd3c020e167", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "0aae05deca890dcf94db748d801465bb0b62247cf070cd34016ed810b7b5e3b8" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 1, + "object_id": 1, + "content_hash": "56421ee48deb82f5692315209c0be565c75fde7531c924e68ee0159515de135b", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "d22a0c40cbd68fa52e6afb9ecb920e80fabb689963214e037059b2c45ecfe151" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 2, + "object_id": 0, + "content_hash": "7032e9f88dc8b7f736553b7a9c7236669833b4e633ca24d28ed5c839b692b716", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "5e4b0d6ce028d04f28ab23761ce1f3b0f3f5e4d014f4b4f9b2fdfbd9a6994a00", + "e4e9b40d3ba168108dc827586994c75940b4168078970493d41828b60c816605" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 2, + "object_id": 1, + "content_hash": "07508165a8199e0362b74c498999eea3b023cb67f89b663250ab0677dcb47966", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "df44809333587e5f04510b40dd099829f3214d783044dd56a5683f56a4a5dafd", + "e4e9b40d3ba168108dc827586994c75940b4168078970493d41828b60c816605" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 2, + "object_id": 2, + "content_hash": "b20995ab22a57bc11cfdc93867ba34e004a038fa39b8517e7b09354f9097e1cc", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "afcffc4feb8366e461d33125a05a371ffeeb6d52e8a66f823cd7682788ccd6fc", + "4f8188470c70525b9004625ecb9e384b61df9fb5c4c7cad082e408ea3754ccec" + ], + "direction_bits": 2, + "leaf_index": 2 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 2, + "object_id": 3, + "content_hash": "c4156696922509b57ff04f290a8f880b2c7f9c567d72ac7ef8aa1dd4a3f9702d", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "f0be86f54fbf3a67c5ba7b56d3efb346e7efc1dcd75eec634be99ae33afd52bc", + "4f8188470c70525b9004625ecb9e384b61df9fb5c4c7cad082e408ea3754ccec" + ], + "direction_bits": 3, + "leaf_index": 3 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 3, + "object_id": 0, + "content_hash": "14400b38229ea3c7ba3a9df42a9ad39cc3a9d9a26cf086d1474237e3aaa4f0a3", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "1dd9e51b1130f7abaaa90a130e278f4a85e62f423e0ae95cb122a6b77cccd9f3" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 3, + "object_id": 1, + "content_hash": "70dee7a8e6962e9ea7e80b4b8591bf565799fa0b835ad5e4933fc6ffcf83a81a", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "8fe3347c01ade8ed1f58502398c60ab6b8cc6b68017b2632bde4fd3631edb286" + ], + "direction_bits": 1, + "leaf_index": 1 + } + } + ] +} diff --git a/wai/live-conformance/c2pa-live/camera-manifests/seg-00.m4s b/wai/live-conformance/c2pa-live/camera-manifests/seg-00.m4s new file mode 100644 index 000000000..759ff96a9 Binary files /dev/null and b/wai/live-conformance/c2pa-live/camera-manifests/seg-00.m4s differ diff --git a/wai/live-conformance/c2pa-live/camera-manifests/seg-01.m4s b/wai/live-conformance/c2pa-live/camera-manifests/seg-01.m4s new file mode 100644 index 000000000..33fc6c6c7 Binary files /dev/null and b/wai/live-conformance/c2pa-live/camera-manifests/seg-01.m4s differ diff --git a/wai/live-conformance/c2pa-live/camera-manifests/seg-02.m4s b/wai/live-conformance/c2pa-live/camera-manifests/seg-02.m4s new file mode 100644 index 000000000..ab34dbf55 Binary files /dev/null and b/wai/live-conformance/c2pa-live/camera-manifests/seg-02.m4s differ diff --git a/wai/live-conformance/c2pa-live/camera-manifests/seg-03.m4s b/wai/live-conformance/c2pa-live/camera-manifests/seg-03.m4s new file mode 100644 index 000000000..18ceb4dbe Binary files /dev/null and b/wai/live-conformance/c2pa-live/camera-manifests/seg-03.m4s differ diff --git a/wai/live-conformance/c2pa-live/camera-registered/init.mp4 b/wai/live-conformance/c2pa-live/camera-registered/init.mp4 new file mode 100644 index 000000000..e8af8af50 Binary files /dev/null and b/wai/live-conformance/c2pa-live/camera-registered/init.mp4 differ diff --git a/wai/live-conformance/c2pa-live/camera-vsi/init.mp4 b/wai/live-conformance/c2pa-live/camera-vsi/init.mp4 new file mode 100644 index 000000000..d24b3a13c Binary files /dev/null and b/wai/live-conformance/c2pa-live/camera-vsi/init.mp4 differ diff --git a/wai/live-conformance/c2pa-live/camera-vsi/receipts.json b/wai/live-conformance/c2pa-live/camera-vsi/receipts.json new file mode 100644 index 000000000..ce8de043d --- /dev/null +++ b/wai/live-conformance/c2pa-live/camera-vsi/receipts.json @@ -0,0 +1,327 @@ +{ + "groups": [ + { + "kind": "group", + "track_alias": 1, + "group_id": 10, + "root_hash": "d7c41cd63b26b976cc556f9f5a5ecb1151421210aa5979e124958e951e7ed309", + "leaf_count": 4, + "joules_total": 0, + "group_start": "2026-10-20T11:00:00.000000Z", + "group_end": "2026-10-20T11:00:02.400000Z", + "signer_pubkey": "4ae5f950c862c6f33f7bb93849fcfb198d87421660fa5ab5e71c0548abf7983e", + "signer_id": "corpus publisher", + "sig": "84df449899ed40a12e7d03c9ec84a37d3e9102d53ddeed19ebf8d9eecad4edb89427f7eb4a34cffda0417a2ffabef2617dd626be82eb3a02fa92623bda3e240d", + "parent_receipt_hash": null + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 11, + "root_hash": "73ff462e0454b7c6d37af29e979c3a7dcad4d128e15c15035f601c3819d12c71", + "leaf_count": 2, + "joules_total": 0, + "group_start": "2026-10-20T11:00:02.400000Z", + "group_end": "2026-10-20T11:00:03.400000Z", + "signer_pubkey": "4ae5f950c862c6f33f7bb93849fcfb198d87421660fa5ab5e71c0548abf7983e", + "signer_id": "corpus publisher", + "sig": "30769cdf9cc26ae3054323d0ba4d16ad9c381c86f05f458c084a707a060bdde8849883f4f8ebcf7c34a1314da2f86e6711e791f1d9fc2c3d8fb9c4598abbba0b", + "parent_receipt_hash": "8c02df8dba110bfcf3a8e9e8849fc74c4afcd5fa033b9002efc460964225c11e" + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 12, + "root_hash": "7db0b6d4458309c8be8df8080ce08452bdccc79aed00d85cec614ce5adc83009", + "leaf_count": 4, + "joules_total": 0, + "group_start": "2026-10-20T11:00:03.400000Z", + "group_end": "2026-10-20T11:00:05.400000Z", + "signer_pubkey": "9e5e5a3320bc06913b2cd5b938c59fe79ad4d89015467b16638a41a464719557", + "signer_id": "corpus publisher", + "sig": "02b221bcdcefb2977d2c5d6db6acf971aa1137524976d84f6674af4d2a8ff044cf2eb905d0d730a0422d5a0c53db6af8aa556567e1ebdb022ed22620112fcd0c", + "parent_receipt_hash": null + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 13, + "root_hash": "293931ddebcd8e3cca482475e3b1a93cc2f225427538a3e5da476bf5eae3b4d0", + "leaf_count": 2, + "joules_total": 0, + "group_start": "2026-10-20T11:00:05.400000Z", + "group_end": "2026-10-20T11:00:06.400000Z", + "signer_pubkey": "9e5e5a3320bc06913b2cd5b938c59fe79ad4d89015467b16638a41a464719557", + "signer_id": "corpus publisher", + "sig": "dc2f2c66935d91c8b4dc1f3922670cd0966fec281852bc1d31161b1365bff8a714f9785e17cff3db17e51652b125a95685eff0777052cd924ea9faf64c3d9501", + "parent_receipt_hash": "3899aa43e23adcafb061e18f011c724107eb054da674322abf859f51cbcf9e8f" + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 14, + "root_hash": "54a6deeba8e5e8e4a3b6e5d53f6156408652c1f9fbdb822ab22fe0b8ecb959c0", + "leaf_count": 3, + "joules_total": 0, + "group_start": "2026-10-20T11:00:06.400000Z", + "group_end": "2026-10-20T11:00:08.000000Z", + "signer_pubkey": "9e5e5a3320bc06913b2cd5b938c59fe79ad4d89015467b16638a41a464719557", + "signer_id": "corpus publisher", + "sig": "e39cb9411567ec6d234350a11db255c55ed9b177032cd9b1538dec5290df58e206bcc43137cc9b33532aeed7fbc45d416f8a5f569ef2c5610c6a95552d3a6301", + "parent_receipt_hash": "b331689118cb8ea170eb38a414fa9fee995adc47e50fce629a809d264a0c1c54" + } + ], + "objects": [ + { + "kind": "object", + "track_alias": 1, + "group_id": 10, + "object_id": 0, + "content_hash": "ed3e285f8b0590025be3a310fd384642973f7ab4cc2531ffb3767dc65decbc87", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "84bd9285e1a79ab4d31fc47abcc3d4d7385c4ee38be89090f67b3d0da066fe03", + "a3f56e2717b3cae3def088678e67d4b8266e38cecd14b6a29f03653ef1d01996" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 10, + "object_id": 1, + "content_hash": "c3c0621e9c77910ba45e7c7d6060ca3baa48454877a3c2947c1327f0b5042960", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "476483f91e3ae73abe1fa7137178680a3350d30a86d619ccde04ce7b9d4672f1", + "a3f56e2717b3cae3def088678e67d4b8266e38cecd14b6a29f03653ef1d01996" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 10, + "object_id": 2, + "content_hash": "313e889ebc1aaa08f347a248bae963fdb3ce7cb1ebffd13a0ea862d4833184de", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "2212db8269bac3d91fd34abfda366d918d848c5d0b1f5b900b1421d22cbe0225", + "a954156f25c6e4a22d745879b4e028f3a1039875a5c784423c876399b4e4e2fb" + ], + "direction_bits": 2, + "leaf_index": 2 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 10, + "object_id": 3, + "content_hash": "c9b87a65ceabd21a394a62f0b27677207fa40002294635bc4c19235dfdae2b35", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "b760b6cef18489c4db7a83675f09424498848a4e93f112700ac075dbb1ff05df", + "a954156f25c6e4a22d745879b4e028f3a1039875a5c784423c876399b4e4e2fb" + ], + "direction_bits": 3, + "leaf_index": 3 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 11, + "object_id": 0, + "content_hash": "d26598db3232eabc5dd6f8e733f3c684a4ec2b7974b185d88ac3bcd3c020e167", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "0aae05deca890dcf94db748d801465bb0b62247cf070cd34016ed810b7b5e3b8" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 11, + "object_id": 1, + "content_hash": "56421ee48deb82f5692315209c0be565c75fde7531c924e68ee0159515de135b", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "d22a0c40cbd68fa52e6afb9ecb920e80fabb689963214e037059b2c45ecfe151" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 12, + "object_id": 0, + "content_hash": "7032e9f88dc8b7f736553b7a9c7236669833b4e633ca24d28ed5c839b692b716", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "5e4b0d6ce028d04f28ab23761ce1f3b0f3f5e4d014f4b4f9b2fdfbd9a6994a00", + "e4e9b40d3ba168108dc827586994c75940b4168078970493d41828b60c816605" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 12, + "object_id": 1, + "content_hash": "07508165a8199e0362b74c498999eea3b023cb67f89b663250ab0677dcb47966", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "df44809333587e5f04510b40dd099829f3214d783044dd56a5683f56a4a5dafd", + "e4e9b40d3ba168108dc827586994c75940b4168078970493d41828b60c816605" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 12, + "object_id": 2, + "content_hash": "b20995ab22a57bc11cfdc93867ba34e004a038fa39b8517e7b09354f9097e1cc", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "afcffc4feb8366e461d33125a05a371ffeeb6d52e8a66f823cd7682788ccd6fc", + "4f8188470c70525b9004625ecb9e384b61df9fb5c4c7cad082e408ea3754ccec" + ], + "direction_bits": 2, + "leaf_index": 2 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 12, + "object_id": 3, + "content_hash": "c4156696922509b57ff04f290a8f880b2c7f9c567d72ac7ef8aa1dd4a3f9702d", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "f0be86f54fbf3a67c5ba7b56d3efb346e7efc1dcd75eec634be99ae33afd52bc", + "4f8188470c70525b9004625ecb9e384b61df9fb5c4c7cad082e408ea3754ccec" + ], + "direction_bits": 3, + "leaf_index": 3 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 13, + "object_id": 0, + "content_hash": "14400b38229ea3c7ba3a9df42a9ad39cc3a9d9a26cf086d1474237e3aaa4f0a3", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "1dd9e51b1130f7abaaa90a130e278f4a85e62f423e0ae95cb122a6b77cccd9f3" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 13, + "object_id": 1, + "content_hash": "70dee7a8e6962e9ea7e80b4b8591bf565799fa0b835ad5e4933fc6ffcf83a81a", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "8fe3347c01ade8ed1f58502398c60ab6b8cc6b68017b2632bde4fd3631edb286" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 14, + "object_id": 0, + "content_hash": "9513817d3f50ea722639343dcc52b1919677ebbf90a269b4202af216e63a2d80", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "4b215fc705ff8f7283d9d8a278bdcb14465d952c871e22748edfa35ace8f7993", + "2ec5cd1f3d701bd4dd6ddb31e8cb3a27dd0ff54ec3a66b91d2e9bb996b6ca883" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 14, + "object_id": 1, + "content_hash": "dd2ff276ca81c40d2152c7bd76e03c28e9d356f20157f1f1af28248aeab6e261", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "6a3fdf96c6c86ba1e7c5b97ec0b966b1b221dcf2f4569589b1c0d927955b3f26", + "2ec5cd1f3d701bd4dd6ddb31e8cb3a27dd0ff54ec3a66b91d2e9bb996b6ca883" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 14, + "object_id": 2, + "content_hash": "287cf5dc07c0ddbbca276149a450e84da9b01d4f26daad93c9061ffbaf93e61d", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "2f713e57cb5e7d69741ceeb374bebd00450d9f233b95f00e3c7ad935e15478a2", + "389e3783984727b17412c2965c5e97ef01b9225dbee3b3200922e2ae9e83ff56" + ], + "direction_bits": 2, + "leaf_index": 2 + } + } + ] +} diff --git a/wai/live-conformance/c2pa-live/camera-vsi/seg-00.m4s b/wai/live-conformance/c2pa-live/camera-vsi/seg-00.m4s new file mode 100644 index 000000000..062d605d5 Binary files /dev/null and b/wai/live-conformance/c2pa-live/camera-vsi/seg-00.m4s differ diff --git a/wai/live-conformance/c2pa-live/camera-vsi/seg-01.m4s b/wai/live-conformance/c2pa-live/camera-vsi/seg-01.m4s new file mode 100644 index 000000000..9a9d6e645 Binary files /dev/null and b/wai/live-conformance/c2pa-live/camera-vsi/seg-01.m4s differ diff --git a/wai/live-conformance/c2pa-live/camera-vsi/seg-02.m4s b/wai/live-conformance/c2pa-live/camera-vsi/seg-02.m4s new file mode 100644 index 000000000..8ec8cf578 Binary files /dev/null and b/wai/live-conformance/c2pa-live/camera-vsi/seg-02.m4s differ diff --git a/wai/live-conformance/c2pa-live/camera-vsi/seg-03.m4s b/wai/live-conformance/c2pa-live/camera-vsi/seg-03.m4s new file mode 100644 index 000000000..ee4892dec Binary files /dev/null and b/wai/live-conformance/c2pa-live/camera-vsi/seg-03.m4s differ diff --git a/wai/live-conformance/c2pa-live/camera-vsi/seg-04.m4s b/wai/live-conformance/c2pa-live/camera-vsi/seg-04.m4s new file mode 100644 index 000000000..b635cfb90 Binary files /dev/null and b/wai/live-conformance/c2pa-live/camera-vsi/seg-04.m4s differ diff --git a/wai/live-conformance/c2pa-live/cert-expired/init.mp4 b/wai/live-conformance/c2pa-live/cert-expired/init.mp4 new file mode 100644 index 000000000..e82353173 Binary files /dev/null and b/wai/live-conformance/c2pa-live/cert-expired/init.mp4 differ diff --git a/wai/live-conformance/c2pa-live/cert-expired/seg-00.m4s b/wai/live-conformance/c2pa-live/cert-expired/seg-00.m4s new file mode 100644 index 000000000..65bfaef6c Binary files /dev/null and b/wai/live-conformance/c2pa-live/cert-expired/seg-00.m4s differ diff --git a/wai/live-conformance/c2pa-live/cert-expired/seg-01.m4s b/wai/live-conformance/c2pa-live/cert-expired/seg-01.m4s new file mode 100644 index 000000000..7e1a61be1 Binary files /dev/null and b/wai/live-conformance/c2pa-live/cert-expired/seg-01.m4s differ diff --git a/wai/live-conformance/c2pa-live/cert-expired/seg-02.m4s b/wai/live-conformance/c2pa-live/cert-expired/seg-02.m4s new file mode 100644 index 000000000..59f58ea4f Binary files /dev/null and b/wai/live-conformance/c2pa-live/cert-expired/seg-02.m4s differ diff --git a/wai/live-conformance/c2pa-live/cert-expired/seg-03.m4s b/wai/live-conformance/c2pa-live/cert-expired/seg-03.m4s new file mode 100644 index 000000000..a9a23ad1b Binary files /dev/null and b/wai/live-conformance/c2pa-live/cert-expired/seg-03.m4s differ diff --git a/wai/live-conformance/c2pa-live/cert-untrusted/init.mp4 b/wai/live-conformance/c2pa-live/cert-untrusted/init.mp4 new file mode 100644 index 000000000..c2dd74574 Binary files /dev/null and b/wai/live-conformance/c2pa-live/cert-untrusted/init.mp4 differ diff --git a/wai/live-conformance/c2pa-live/cert-untrusted/seg-00.m4s b/wai/live-conformance/c2pa-live/cert-untrusted/seg-00.m4s new file mode 100644 index 000000000..eddc73d25 Binary files /dev/null and b/wai/live-conformance/c2pa-live/cert-untrusted/seg-00.m4s differ diff --git a/wai/live-conformance/c2pa-live/cert-untrusted/seg-01.m4s b/wai/live-conformance/c2pa-live/cert-untrusted/seg-01.m4s new file mode 100644 index 000000000..938242b71 Binary files /dev/null and b/wai/live-conformance/c2pa-live/cert-untrusted/seg-01.m4s differ diff --git a/wai/live-conformance/c2pa-live/cert-untrusted/seg-02.m4s b/wai/live-conformance/c2pa-live/cert-untrusted/seg-02.m4s new file mode 100644 index 000000000..4d40b9ab8 Binary files /dev/null and b/wai/live-conformance/c2pa-live/cert-untrusted/seg-02.m4s differ diff --git a/wai/live-conformance/c2pa-live/cert-untrusted/seg-03.m4s b/wai/live-conformance/c2pa-live/cert-untrusted/seg-03.m4s new file mode 100644 index 000000000..da504d107 Binary files /dev/null and b/wai/live-conformance/c2pa-live/cert-untrusted/seg-03.m4s differ diff --git a/wai/live-conformance/c2pa-live/cert-wrong-eku/init.mp4 b/wai/live-conformance/c2pa-live/cert-wrong-eku/init.mp4 new file mode 100644 index 000000000..4171f6129 Binary files /dev/null and b/wai/live-conformance/c2pa-live/cert-wrong-eku/init.mp4 differ diff --git a/wai/live-conformance/c2pa-live/cert-wrong-eku/seg-00.m4s b/wai/live-conformance/c2pa-live/cert-wrong-eku/seg-00.m4s new file mode 100644 index 000000000..d20a3a59e Binary files /dev/null and b/wai/live-conformance/c2pa-live/cert-wrong-eku/seg-00.m4s differ diff --git a/wai/live-conformance/c2pa-live/cert-wrong-eku/seg-01.m4s b/wai/live-conformance/c2pa-live/cert-wrong-eku/seg-01.m4s new file mode 100644 index 000000000..a0438cf62 Binary files /dev/null and b/wai/live-conformance/c2pa-live/cert-wrong-eku/seg-01.m4s differ diff --git a/wai/live-conformance/c2pa-live/cert-wrong-eku/seg-02.m4s b/wai/live-conformance/c2pa-live/cert-wrong-eku/seg-02.m4s new file mode 100644 index 000000000..30bc234cc Binary files /dev/null and b/wai/live-conformance/c2pa-live/cert-wrong-eku/seg-02.m4s differ diff --git a/wai/live-conformance/c2pa-live/cert-wrong-eku/seg-03.m4s b/wai/live-conformance/c2pa-live/cert-wrong-eku/seg-03.m4s new file mode 100644 index 000000000..05f918cf4 Binary files /dev/null and b/wai/live-conformance/c2pa-live/cert-wrong-eku/seg-03.m4s differ diff --git a/wai/live-conformance/c2pa-live/claim-signature/seg-01.m4s b/wai/live-conformance/c2pa-live/claim-signature/seg-01.m4s new file mode 100644 index 000000000..8490a5df6 Binary files /dev/null and b/wai/live-conformance/c2pa-live/claim-signature/seg-01.m4s differ diff --git a/wai/live-conformance/c2pa-live/composite-manifests/init.mp4 b/wai/live-conformance/c2pa-live/composite-manifests/init.mp4 new file mode 100644 index 000000000..e2192cef6 Binary files /dev/null and b/wai/live-conformance/c2pa-live/composite-manifests/init.mp4 differ diff --git a/wai/live-conformance/c2pa-live/composite-manifests/receipts.json b/wai/live-conformance/c2pa-live/composite-manifests/receipts.json new file mode 100644 index 000000000..17fd44f9f --- /dev/null +++ b/wai/live-conformance/c2pa-live/composite-manifests/receipts.json @@ -0,0 +1,235 @@ +{ + "groups": [ + { + "kind": "group", + "track_alias": 3, + "group_id": 0, + "root_hash": "e1a83ba6bfd481ac0f831d04edbccf3d2ff94b2e447ba3add4ab9b481220d8e1", + "leaf_count": 3, + "joules_total": 0, + "group_start": "2026-10-20T11:00:00.000000Z", + "group_end": "2026-10-20T11:00:01.600000Z", + "signer_pubkey": "c6d973538beef5386cc061f48a81c581312b56ee754bd1ca5345938052e7c1b0", + "signer_id": "corpus publisher", + "sig": "87331eda06f11cc3e6117722508a22b3ecaee3e718089f8ef78156b76ecd9e6c5f677a6395ff66b5269e5b09ac5955ee6251b9ebbf6fe1312b62c3fdb767b408", + "parent_receipt_hash": null + }, + { + "kind": "group", + "track_alias": 3, + "group_id": 1, + "root_hash": "db7bfcbeb3cc6749cac67629bcf4b80ea89576de4ab35e26c01cf9cec5e89a2b", + "leaf_count": 4, + "joules_total": 0, + "group_start": "2026-10-20T11:00:01.600000Z", + "group_end": "2026-10-20T11:00:04.000000Z", + "signer_pubkey": "c6d973538beef5386cc061f48a81c581312b56ee754bd1ca5345938052e7c1b0", + "signer_id": "corpus publisher", + "sig": "29b4dab9052d0ba13f6af8fb1297c5ed01a3e1a763602344e032d8fd388cb47cb1c0c1b0c7c1e440234d7a1433fb7ef1d3c5bbcab9228927b6bb92440af1950f", + "parent_receipt_hash": "c075cc5af1e63d18079a5da68c927639cd98d760747cb94bbb42a8e5181b1e85" + }, + { + "kind": "group", + "track_alias": 3, + "group_id": 2, + "root_hash": "197d569450354354a4c0334579bbcf74930804b3231ac5eeb14c396dfbbd8e47", + "leaf_count": 4, + "joules_total": 0, + "group_start": "2026-10-20T11:00:04.000000Z", + "group_end": "2026-10-20T11:00:06.000000Z", + "signer_pubkey": "c6d973538beef5386cc061f48a81c581312b56ee754bd1ca5345938052e7c1b0", + "signer_id": "corpus publisher", + "sig": "4f2b2ffeeadc6e4bcb1c0869b8461a40157c8ee36107783b13b0d95def100f6d42b686f031a07975748d95c35c4913cc6bc21c593623ab4018d219c1e24aa305", + "parent_receipt_hash": "c86f619afaddf1c01831563ee04cf587daa845b933e178264b3e640466a9b659" + } + ], + "objects": [ + { + "kind": "object", + "track_alias": 3, + "group_id": 0, + "object_id": 0, + "content_hash": "cccf0f7cf804fde5d752a6b594f7d2bb436d82cf8185dafcdbe08c10bb3dfd0a", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "8b5f9ae72ca1383edc1797ec81312e393d80e2487713b36376e9d9ea2f48f591", + "c6923246eb07c837ec211840187a1e0a03e02c53bc0d4cf63064f0ebae8ad695" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 3, + "group_id": 0, + "object_id": 1, + "content_hash": "48048f298f38270ff99c343912a55c15714d1d95a747438d33361e70ac0b6e41", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "8b2955c8060f6760cdbdd3d0faa690994e3485fec0591bb1f23254ef9ef0f6f4", + "c6923246eb07c837ec211840187a1e0a03e02c53bc0d4cf63064f0ebae8ad695" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 3, + "group_id": 0, + "object_id": 2, + "content_hash": "782a9615578ad1b6dfce2688dff7fe112da7c8de2f8d44128a8adbcbcc096919", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "ddcf3f22cbb5dac9540a3e38ecfc70cc037062142faaaccbfc12f0dc7cb33db3", + "d6837f1b58faee67f8fadbb83949888c8695b7ba458e09855ba042184469d4ca" + ], + "direction_bits": 2, + "leaf_index": 2 + } + }, + { + "kind": "object", + "track_alias": 3, + "group_id": 1, + "object_id": 0, + "content_hash": "96e5ab63f8b0786fe8134df7dbbf766f874b00327111a68200bd96ce0da2ab7a", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "b89d6e096ba98f5bf3b9e06bf9a9bfe4d0910f0a48c39de1fa7238e23a798504", + "cde9b502fb70313628c3e90426d3eba0b02b954f4278aee205881387360a90ce" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 3, + "group_id": 1, + "object_id": 1, + "content_hash": "b6138e08ce5070082dbf3cfd65640cc3715fd688088902f22446c53256c53c91", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "05997be1dfdbed39dfb9751c086dcb6eddceab89c4839a4803d7dd763cc96057", + "cde9b502fb70313628c3e90426d3eba0b02b954f4278aee205881387360a90ce" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 3, + "group_id": 1, + "object_id": 2, + "content_hash": "b235d04615e6f10f8484dc24b53d399de4182e3db66e9dd51fc13160b9faf84c", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "6152066b69fa966442153bf4f8820a862a071e0daba93cd1dbbac949751fe55d", + "131f4882b5990ca31505c5a5402009a499628914d215bebc93f8af5827d9bdcd" + ], + "direction_bits": 2, + "leaf_index": 2 + } + }, + { + "kind": "object", + "track_alias": 3, + "group_id": 1, + "object_id": 3, + "content_hash": "ae978bdaa19dd90c35546ee87cd5871e93948cb977674c72d53538ec5ec92fea", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "e11ca7d076239e6293344e7eee2c2c4b1d10354c1f794e67c0fe58140f257ba4", + "131f4882b5990ca31505c5a5402009a499628914d215bebc93f8af5827d9bdcd" + ], + "direction_bits": 3, + "leaf_index": 3 + } + }, + { + "kind": "object", + "track_alias": 3, + "group_id": 2, + "object_id": 0, + "content_hash": "c69a9e0afd679fcf5d9743807b38a55d951b59a7f1b1920059e1c272af1638d2", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "5396302ece948169569bb6671300709fa00c3c479f014177a23997f855956cfa", + "e288602bf44a8efc15f36e6dabdd165eea2e71c993f9ca8273694290a9ec83d1" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 3, + "group_id": 2, + "object_id": 1, + "content_hash": "3da88edb3c6793f3bf206b74db210618d716c28a075d9d2e9b3142f4aeb46013", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "d5094da95008c6d0bf238d854873a0b7696dfd80082cb8481890bbd699b15398", + "e288602bf44a8efc15f36e6dabdd165eea2e71c993f9ca8273694290a9ec83d1" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 3, + "group_id": 2, + "object_id": 2, + "content_hash": "f32df7ab37e364edc2d22b3fab7a7cefaccb3357a1e51396200460e77b02d305", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "a76d58e41afb10f34613eb62755419bd56c3f96685385fdfbce8d1d5f95e23ca", + "c63fb69cd61d929eb5360c97c168490c0bc57bb2318f8ded79e67edffe168d1a" + ], + "direction_bits": 2, + "leaf_index": 2 + } + }, + { + "kind": "object", + "track_alias": 3, + "group_id": 2, + "object_id": 3, + "content_hash": "a132892d5bb477f409493bea4699e8d335027d30aa66c82d69aaa89cc15f1a9c", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "6be22c59f2b8aeeacae5713c9d74e612d1943412c3932c71ac6f280d238649c8", + "c63fb69cd61d929eb5360c97c168490c0bc57bb2318f8ded79e67edffe168d1a" + ], + "direction_bits": 3, + "leaf_index": 3 + } + } + ] +} diff --git a/wai/live-conformance/c2pa-live/composite-manifests/seg-00.m4s b/wai/live-conformance/c2pa-live/composite-manifests/seg-00.m4s new file mode 100644 index 000000000..7a76a620f Binary files /dev/null and b/wai/live-conformance/c2pa-live/composite-manifests/seg-00.m4s differ diff --git a/wai/live-conformance/c2pa-live/composite-manifests/seg-01.m4s b/wai/live-conformance/c2pa-live/composite-manifests/seg-01.m4s new file mode 100644 index 000000000..44814579d Binary files /dev/null and b/wai/live-conformance/c2pa-live/composite-manifests/seg-01.m4s differ diff --git a/wai/live-conformance/c2pa-live/composite-manifests/seg-02.m4s b/wai/live-conformance/c2pa-live/composite-manifests/seg-02.m4s new file mode 100644 index 000000000..fc1cea8d8 Binary files /dev/null and b/wai/live-conformance/c2pa-live/composite-manifests/seg-02.m4s differ diff --git a/wai/live-conformance/c2pa-live/composite-source-type/init.mp4 b/wai/live-conformance/c2pa-live/composite-source-type/init.mp4 new file mode 100644 index 000000000..d078288f7 Binary files /dev/null and b/wai/live-conformance/c2pa-live/composite-source-type/init.mp4 differ diff --git a/wai/live-conformance/c2pa-live/composite-source-type/seg-00.m4s b/wai/live-conformance/c2pa-live/composite-source-type/seg-00.m4s new file mode 100644 index 000000000..4015c2724 Binary files /dev/null and b/wai/live-conformance/c2pa-live/composite-source-type/seg-00.m4s differ diff --git a/wai/live-conformance/c2pa-live/composite-source-type/seg-01.m4s b/wai/live-conformance/c2pa-live/composite-source-type/seg-01.m4s new file mode 100644 index 000000000..f23f032d1 Binary files /dev/null and b/wai/live-conformance/c2pa-live/composite-source-type/seg-01.m4s differ diff --git a/wai/live-conformance/c2pa-live/composite-source-type/seg-02.m4s b/wai/live-conformance/c2pa-live/composite-source-type/seg-02.m4s new file mode 100644 index 000000000..74f300da9 Binary files /dev/null and b/wai/live-conformance/c2pa-live/composite-source-type/seg-02.m4s differ diff --git a/wai/live-conformance/c2pa-live/continuity-method/seg-01.m4s b/wai/live-conformance/c2pa-live/continuity-method/seg-01.m4s new file mode 100644 index 000000000..e0768c6a4 Binary files /dev/null and b/wai/live-conformance/c2pa-live/continuity-method/seg-01.m4s differ diff --git a/wai/live-conformance/c2pa-live/init-mdat/init.mp4 b/wai/live-conformance/c2pa-live/init-mdat/init.mp4 new file mode 100644 index 000000000..80697dbe4 Binary files /dev/null and b/wai/live-conformance/c2pa-live/init-mdat/init.mp4 differ diff --git a/wai/live-conformance/c2pa-live/manifest-id-not-derived/seg-01.m4s b/wai/live-conformance/c2pa-live/manifest-id-not-derived/seg-01.m4s new file mode 100644 index 000000000..b6b3b61a9 Binary files /dev/null and b/wai/live-conformance/c2pa-live/manifest-id-not-derived/seg-01.m4s differ diff --git a/wai/live-conformance/c2pa-live/manifest-id-not-derived/seg-02.m4s b/wai/live-conformance/c2pa-live/manifest-id-not-derived/seg-02.m4s new file mode 100644 index 000000000..7ec2972c8 Binary files /dev/null and b/wai/live-conformance/c2pa-live/manifest-id-not-derived/seg-02.m4s differ diff --git a/wai/live-conformance/c2pa-live/manifest-malformed/init.mp4 b/wai/live-conformance/c2pa-live/manifest-malformed/init.mp4 new file mode 100644 index 000000000..f17864351 Binary files /dev/null and b/wai/live-conformance/c2pa-live/manifest-malformed/init.mp4 differ diff --git a/wai/live-conformance/c2pa-live/object-receipt/receipts.json b/wai/live-conformance/c2pa-live/object-receipt/receipts.json new file mode 100644 index 000000000..7d0e0e970 --- /dev/null +++ b/wai/live-conformance/c2pa-live/object-receipt/receipts.json @@ -0,0 +1,262 @@ +{ + "groups": [ + { + "kind": "group", + "track_alias": 1, + "group_id": 0, + "root_hash": "d7c41cd63b26b976cc556f9f5a5ecb1151421210aa5979e124958e951e7ed309", + "leaf_count": 4, + "joules_total": 0, + "group_start": "2026-10-20T11:00:00.000000Z", + "group_end": "2026-10-20T11:00:02.400000Z", + "signer_pubkey": "7772ae8959f2a940a9f4e895d8ba7cf958632cb3ad7f98625c25b5a86fb21d6b", + "signer_id": "corpus publisher", + "sig": "f6e30f65fceac9b6ada8dcac2cc2570dc0f995a47510f1f8a9b059df4c8078cd22f9dcadcfe9b6fe63e09cfb66456ee52ec95d888eb9ee97da9d7d9401ffd502", + "parent_receipt_hash": null + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 1, + "root_hash": "73ff462e0454b7c6d37af29e979c3a7dcad4d128e15c15035f601c3819d12c71", + "leaf_count": 2, + "joules_total": 0, + "group_start": "2026-10-20T11:00:02.400000Z", + "group_end": "2026-10-20T11:00:03.400000Z", + "signer_pubkey": "7772ae8959f2a940a9f4e895d8ba7cf958632cb3ad7f98625c25b5a86fb21d6b", + "signer_id": "corpus publisher", + "sig": "726b6d73a220c9145b0dc85ef8b8f165d1dda249b61dac2f4e4182da953c5d7da1d05fccfbacdf4f16831898a019386175216a80f0bb61f65e1f66f8799bc80a", + "parent_receipt_hash": "351b15632022ea1de17b0625f29e1b5da5fd1f8062f08e2ac71592a3a06ed278" + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 2, + "root_hash": "7db0b6d4458309c8be8df8080ce08452bdccc79aed00d85cec614ce5adc83009", + "leaf_count": 4, + "joules_total": 0, + "group_start": "2026-10-20T11:00:03.400000Z", + "group_end": "2026-10-20T11:00:05.400000Z", + "signer_pubkey": "7772ae8959f2a940a9f4e895d8ba7cf958632cb3ad7f98625c25b5a86fb21d6b", + "signer_id": "corpus publisher", + "sig": "efb642334c4f1bd9e314cfa40b7ebad09b0a1bcf2755f9f53aff673d05c9e6b29447da8fd211ef0f7688f5fd66dc124b4a46f9ca6b41c92d04577c7239066004", + "parent_receipt_hash": "34fc9a08f1f5be49823e319385542696b4d388088d0117b8823a58ed6aed811f" + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 3, + "root_hash": "293931ddebcd8e3cca482475e3b1a93cc2f225427538a3e5da476bf5eae3b4d0", + "leaf_count": 2, + "joules_total": 0, + "group_start": "2026-10-20T11:00:05.400000Z", + "group_end": "2026-10-20T11:00:06.400000Z", + "signer_pubkey": "7772ae8959f2a940a9f4e895d8ba7cf958632cb3ad7f98625c25b5a86fb21d6b", + "signer_id": "corpus publisher", + "sig": "0652205c00a64d57cf0e33d358c22319c3bb39e37d3d3a6186241bd9a35a46e040024ec8c5c7c17fa4c0ee3135bb229e7a46708651cf4b225266ded20b21bb09", + "parent_receipt_hash": "6bea20c763d58218a72402e7d6d75ef9db460b3c62258b26e1a49762800c9516" + } + ], + "objects": [ + { + "kind": "object", + "track_alias": 1, + "group_id": 0, + "object_id": 0, + "content_hash": "ed3e285f8b0590025be3a310fd384642973f7ab4cc2531ffb3767dc65decbc87", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "84bd9285e1a79ab4d31fc47abcc3d4d7385c4ee38be89090f67b3d0da066fe03", + "a3f56e2717b3cae3def088678e67d4b8266e38cecd14b6a29f03653ef1d01996" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 0, + "object_id": 1, + "content_hash": "c3c0621e9c77910ba45e7c7d6060ca3baa48454877a3c2947c1327f0b5042960", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "476483f91e3ae73abe1fa7137178680a3350d30a86d619ccde04ce7b9d4672f1", + "a3f56e2717b3cae3def088678e67d4b8266e38cecd14b6a29f03653ef1d01996" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 0, + "object_id": 2, + "content_hash": "313e889ebc1aaa08f347a248bae963fdb3ce7cb1ebffd13a0ea862d4833184de", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "2212db8269bac3d91fd34abfda366d918d848c5d0b1f5b900b1421d22cbe0225", + "a954156f25c6e4a22d745879b4e028f3a1039875a5c784423c876399b4e4e2fb" + ], + "direction_bits": 2, + "leaf_index": 2 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 0, + "object_id": 3, + "content_hash": "c9b87a65ceabd21a394a62f0b27677207fa40002294635bc4c19235dfdae2b35", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "b760b6cef18489c4db7a83675f09424498848a4e93f112700ac075dbb1ff05df", + "a954156f25c6e4a22d745879b4e028f3a1039875a5c784423c876399b4e4e2fb" + ], + "direction_bits": 3, + "leaf_index": 3 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 1, + "object_id": 0, + "content_hash": "d26598db3232eabc5dd6f8e733f3c684a4ec2b7974b185d88ac3bcd3c020e167", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "0bae05deca890dcf94db748d801465bb0b62247cf070cd34016ed810b7b5e3b8" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 1, + "object_id": 1, + "content_hash": "56421ee48deb82f5692315209c0be565c75fde7531c924e68ee0159515de135b", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "d22a0c40cbd68fa52e6afb9ecb920e80fabb689963214e037059b2c45ecfe151" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 2, + "object_id": 0, + "content_hash": "7032e9f88dc8b7f736553b7a9c7236669833b4e633ca24d28ed5c839b692b716", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "5e4b0d6ce028d04f28ab23761ce1f3b0f3f5e4d014f4b4f9b2fdfbd9a6994a00", + "e4e9b40d3ba168108dc827586994c75940b4168078970493d41828b60c816605" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 2, + "object_id": 1, + "content_hash": "07508165a8199e0362b74c498999eea3b023cb67f89b663250ab0677dcb47966", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "df44809333587e5f04510b40dd099829f3214d783044dd56a5683f56a4a5dafd", + "e4e9b40d3ba168108dc827586994c75940b4168078970493d41828b60c816605" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 2, + "object_id": 2, + "content_hash": "b20995ab22a57bc11cfdc93867ba34e004a038fa39b8517e7b09354f9097e1cc", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "afcffc4feb8366e461d33125a05a371ffeeb6d52e8a66f823cd7682788ccd6fc", + "4f8188470c70525b9004625ecb9e384b61df9fb5c4c7cad082e408ea3754ccec" + ], + "direction_bits": 2, + "leaf_index": 2 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 2, + "object_id": 3, + "content_hash": "c4156696922509b57ff04f290a8f880b2c7f9c567d72ac7ef8aa1dd4a3f9702d", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "f0be86f54fbf3a67c5ba7b56d3efb346e7efc1dcd75eec634be99ae33afd52bc", + "4f8188470c70525b9004625ecb9e384b61df9fb5c4c7cad082e408ea3754ccec" + ], + "direction_bits": 3, + "leaf_index": 3 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 3, + "object_id": 0, + "content_hash": "14400b38229ea3c7ba3a9df42a9ad39cc3a9d9a26cf086d1474237e3aaa4f0a3", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "1dd9e51b1130f7abaaa90a130e278f4a85e62f423e0ae95cb122a6b77cccd9f3" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 3, + "object_id": 1, + "content_hash": "70dee7a8e6962e9ea7e80b4b8591bf565799fa0b835ad5e4933fc6ffcf83a81a", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "8fe3347c01ade8ed1f58502398c60ab6b8cc6b68017b2632bde4fd3631edb286" + ], + "direction_bits": 1, + "leaf_index": 1 + } + } + ] +} diff --git a/wai/live-conformance/c2pa-live/provenance-missing/seg-01.m4s b/wai/live-conformance/c2pa-live/provenance-missing/seg-01.m4s new file mode 100644 index 000000000..865e9e4ad Binary files /dev/null and b/wai/live-conformance/c2pa-live/provenance-missing/seg-01.m4s differ diff --git a/wai/live-conformance/c2pa-live/receipt-forged/receipts.json b/wai/live-conformance/c2pa-live/receipt-forged/receipts.json new file mode 100644 index 000000000..1347e3051 --- /dev/null +++ b/wai/live-conformance/c2pa-live/receipt-forged/receipts.json @@ -0,0 +1,262 @@ +{ + "groups": [ + { + "kind": "group", + "track_alias": 1, + "group_id": 0, + "root_hash": "d7c41cd63b26b976cc556f9f5a5ecb1151421210aa5979e124958e951e7ed309", + "leaf_count": 4, + "joules_total": 0, + "group_start": "2026-10-20T11:00:00.000000Z", + "group_end": "2026-10-20T11:00:02.400000Z", + "signer_pubkey": "7772ae8959f2a940a9f4e895d8ba7cf958632cb3ad7f98625c25b5a86fb21d6b", + "signer_id": "corpus publisher", + "sig": "f6e30f65fceac9b6ada8dcac2cc2570dc0f995a47510f1f8a9b059df4c8078cd22f9dcadcfe9b6fe63e09cfb66456ee52ec95d888eb9ee97da9d7d9401ffd502", + "parent_receipt_hash": null + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 1, + "root_hash": "73ff462e0454b7c6d37af29e979c3a7dcad4d128e15c15035f601c3819d12c71", + "leaf_count": 2, + "joules_total": 0, + "group_start": "2026-10-20T11:00:02.400000Z", + "group_end": "2026-10-20T11:00:03.400000Z", + "signer_pubkey": "7772ae8959f2a940a9f4e895d8ba7cf958632cb3ad7f98625c25b5a86fb21d6b", + "signer_id": "corpus publisher", + "sig": "736b6d73a220c9145b0dc85ef8b8f165d1dda249b61dac2f4e4182da953c5d7da1d05fccfbacdf4f16831898a019386175216a80f0bb61f65e1f66f8799bc80a", + "parent_receipt_hash": "351b15632022ea1de17b0625f29e1b5da5fd1f8062f08e2ac71592a3a06ed278" + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 2, + "root_hash": "7db0b6d4458309c8be8df8080ce08452bdccc79aed00d85cec614ce5adc83009", + "leaf_count": 4, + "joules_total": 0, + "group_start": "2026-10-20T11:00:03.400000Z", + "group_end": "2026-10-20T11:00:05.400000Z", + "signer_pubkey": "7772ae8959f2a940a9f4e895d8ba7cf958632cb3ad7f98625c25b5a86fb21d6b", + "signer_id": "corpus publisher", + "sig": "efb642334c4f1bd9e314cfa40b7ebad09b0a1bcf2755f9f53aff673d05c9e6b29447da8fd211ef0f7688f5fd66dc124b4a46f9ca6b41c92d04577c7239066004", + "parent_receipt_hash": "34fc9a08f1f5be49823e319385542696b4d388088d0117b8823a58ed6aed811f" + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 3, + "root_hash": "293931ddebcd8e3cca482475e3b1a93cc2f225427538a3e5da476bf5eae3b4d0", + "leaf_count": 2, + "joules_total": 0, + "group_start": "2026-10-20T11:00:05.400000Z", + "group_end": "2026-10-20T11:00:06.400000Z", + "signer_pubkey": "7772ae8959f2a940a9f4e895d8ba7cf958632cb3ad7f98625c25b5a86fb21d6b", + "signer_id": "corpus publisher", + "sig": "0652205c00a64d57cf0e33d358c22319c3bb39e37d3d3a6186241bd9a35a46e040024ec8c5c7c17fa4c0ee3135bb229e7a46708651cf4b225266ded20b21bb09", + "parent_receipt_hash": "6bea20c763d58218a72402e7d6d75ef9db460b3c62258b26e1a49762800c9516" + } + ], + "objects": [ + { + "kind": "object", + "track_alias": 1, + "group_id": 0, + "object_id": 0, + "content_hash": "ed3e285f8b0590025be3a310fd384642973f7ab4cc2531ffb3767dc65decbc87", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "84bd9285e1a79ab4d31fc47abcc3d4d7385c4ee38be89090f67b3d0da066fe03", + "a3f56e2717b3cae3def088678e67d4b8266e38cecd14b6a29f03653ef1d01996" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 0, + "object_id": 1, + "content_hash": "c3c0621e9c77910ba45e7c7d6060ca3baa48454877a3c2947c1327f0b5042960", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "476483f91e3ae73abe1fa7137178680a3350d30a86d619ccde04ce7b9d4672f1", + "a3f56e2717b3cae3def088678e67d4b8266e38cecd14b6a29f03653ef1d01996" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 0, + "object_id": 2, + "content_hash": "313e889ebc1aaa08f347a248bae963fdb3ce7cb1ebffd13a0ea862d4833184de", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "2212db8269bac3d91fd34abfda366d918d848c5d0b1f5b900b1421d22cbe0225", + "a954156f25c6e4a22d745879b4e028f3a1039875a5c784423c876399b4e4e2fb" + ], + "direction_bits": 2, + "leaf_index": 2 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 0, + "object_id": 3, + "content_hash": "c9b87a65ceabd21a394a62f0b27677207fa40002294635bc4c19235dfdae2b35", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "b760b6cef18489c4db7a83675f09424498848a4e93f112700ac075dbb1ff05df", + "a954156f25c6e4a22d745879b4e028f3a1039875a5c784423c876399b4e4e2fb" + ], + "direction_bits": 3, + "leaf_index": 3 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 1, + "object_id": 0, + "content_hash": "d26598db3232eabc5dd6f8e733f3c684a4ec2b7974b185d88ac3bcd3c020e167", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "0aae05deca890dcf94db748d801465bb0b62247cf070cd34016ed810b7b5e3b8" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 1, + "object_id": 1, + "content_hash": "56421ee48deb82f5692315209c0be565c75fde7531c924e68ee0159515de135b", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "d22a0c40cbd68fa52e6afb9ecb920e80fabb689963214e037059b2c45ecfe151" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 2, + "object_id": 0, + "content_hash": "7032e9f88dc8b7f736553b7a9c7236669833b4e633ca24d28ed5c839b692b716", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "5e4b0d6ce028d04f28ab23761ce1f3b0f3f5e4d014f4b4f9b2fdfbd9a6994a00", + "e4e9b40d3ba168108dc827586994c75940b4168078970493d41828b60c816605" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 2, + "object_id": 1, + "content_hash": "07508165a8199e0362b74c498999eea3b023cb67f89b663250ab0677dcb47966", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "df44809333587e5f04510b40dd099829f3214d783044dd56a5683f56a4a5dafd", + "e4e9b40d3ba168108dc827586994c75940b4168078970493d41828b60c816605" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 2, + "object_id": 2, + "content_hash": "b20995ab22a57bc11cfdc93867ba34e004a038fa39b8517e7b09354f9097e1cc", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "afcffc4feb8366e461d33125a05a371ffeeb6d52e8a66f823cd7682788ccd6fc", + "4f8188470c70525b9004625ecb9e384b61df9fb5c4c7cad082e408ea3754ccec" + ], + "direction_bits": 2, + "leaf_index": 2 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 2, + "object_id": 3, + "content_hash": "c4156696922509b57ff04f290a8f880b2c7f9c567d72ac7ef8aa1dd4a3f9702d", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "f0be86f54fbf3a67c5ba7b56d3efb346e7efc1dcd75eec634be99ae33afd52bc", + "4f8188470c70525b9004625ecb9e384b61df9fb5c4c7cad082e408ea3754ccec" + ], + "direction_bits": 3, + "leaf_index": 3 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 3, + "object_id": 0, + "content_hash": "14400b38229ea3c7ba3a9df42a9ad39cc3a9d9a26cf086d1474237e3aaa4f0a3", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "1dd9e51b1130f7abaaa90a130e278f4a85e62f423e0ae95cb122a6b77cccd9f3" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 3, + "object_id": 1, + "content_hash": "70dee7a8e6962e9ea7e80b4b8591bf565799fa0b835ad5e4933fc6ffcf83a81a", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "8fe3347c01ade8ed1f58502398c60ab6b8cc6b68017b2632bde4fd3631edb286" + ], + "direction_bits": 1, + "leaf_index": 1 + } + } + ] +} diff --git a/wai/live-conformance/c2pa-live/receipt-missing/receipts.json b/wai/live-conformance/c2pa-live/receipt-missing/receipts.json new file mode 100644 index 000000000..6b7b61a91 --- /dev/null +++ b/wai/live-conformance/c2pa-live/receipt-missing/receipts.json @@ -0,0 +1,248 @@ +{ + "groups": [ + { + "kind": "group", + "track_alias": 1, + "group_id": 0, + "root_hash": "d7c41cd63b26b976cc556f9f5a5ecb1151421210aa5979e124958e951e7ed309", + "leaf_count": 4, + "joules_total": 0, + "group_start": "2026-10-20T11:00:00.000000Z", + "group_end": "2026-10-20T11:00:02.400000Z", + "signer_pubkey": "7772ae8959f2a940a9f4e895d8ba7cf958632cb3ad7f98625c25b5a86fb21d6b", + "signer_id": "corpus publisher", + "sig": "f6e30f65fceac9b6ada8dcac2cc2570dc0f995a47510f1f8a9b059df4c8078cd22f9dcadcfe9b6fe63e09cfb66456ee52ec95d888eb9ee97da9d7d9401ffd502", + "parent_receipt_hash": null + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 2, + "root_hash": "7db0b6d4458309c8be8df8080ce08452bdccc79aed00d85cec614ce5adc83009", + "leaf_count": 4, + "joules_total": 0, + "group_start": "2026-10-20T11:00:03.400000Z", + "group_end": "2026-10-20T11:00:05.400000Z", + "signer_pubkey": "7772ae8959f2a940a9f4e895d8ba7cf958632cb3ad7f98625c25b5a86fb21d6b", + "signer_id": "corpus publisher", + "sig": "efb642334c4f1bd9e314cfa40b7ebad09b0a1bcf2755f9f53aff673d05c9e6b29447da8fd211ef0f7688f5fd66dc124b4a46f9ca6b41c92d04577c7239066004", + "parent_receipt_hash": "34fc9a08f1f5be49823e319385542696b4d388088d0117b8823a58ed6aed811f" + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 3, + "root_hash": "293931ddebcd8e3cca482475e3b1a93cc2f225427538a3e5da476bf5eae3b4d0", + "leaf_count": 2, + "joules_total": 0, + "group_start": "2026-10-20T11:00:05.400000Z", + "group_end": "2026-10-20T11:00:06.400000Z", + "signer_pubkey": "7772ae8959f2a940a9f4e895d8ba7cf958632cb3ad7f98625c25b5a86fb21d6b", + "signer_id": "corpus publisher", + "sig": "0652205c00a64d57cf0e33d358c22319c3bb39e37d3d3a6186241bd9a35a46e040024ec8c5c7c17fa4c0ee3135bb229e7a46708651cf4b225266ded20b21bb09", + "parent_receipt_hash": "6bea20c763d58218a72402e7d6d75ef9db460b3c62258b26e1a49762800c9516" + } + ], + "objects": [ + { + "kind": "object", + "track_alias": 1, + "group_id": 0, + "object_id": 0, + "content_hash": "ed3e285f8b0590025be3a310fd384642973f7ab4cc2531ffb3767dc65decbc87", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "84bd9285e1a79ab4d31fc47abcc3d4d7385c4ee38be89090f67b3d0da066fe03", + "a3f56e2717b3cae3def088678e67d4b8266e38cecd14b6a29f03653ef1d01996" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 0, + "object_id": 1, + "content_hash": "c3c0621e9c77910ba45e7c7d6060ca3baa48454877a3c2947c1327f0b5042960", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "476483f91e3ae73abe1fa7137178680a3350d30a86d619ccde04ce7b9d4672f1", + "a3f56e2717b3cae3def088678e67d4b8266e38cecd14b6a29f03653ef1d01996" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 0, + "object_id": 2, + "content_hash": "313e889ebc1aaa08f347a248bae963fdb3ce7cb1ebffd13a0ea862d4833184de", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "2212db8269bac3d91fd34abfda366d918d848c5d0b1f5b900b1421d22cbe0225", + "a954156f25c6e4a22d745879b4e028f3a1039875a5c784423c876399b4e4e2fb" + ], + "direction_bits": 2, + "leaf_index": 2 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 0, + "object_id": 3, + "content_hash": "c9b87a65ceabd21a394a62f0b27677207fa40002294635bc4c19235dfdae2b35", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "b760b6cef18489c4db7a83675f09424498848a4e93f112700ac075dbb1ff05df", + "a954156f25c6e4a22d745879b4e028f3a1039875a5c784423c876399b4e4e2fb" + ], + "direction_bits": 3, + "leaf_index": 3 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 1, + "object_id": 0, + "content_hash": "d26598db3232eabc5dd6f8e733f3c684a4ec2b7974b185d88ac3bcd3c020e167", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "0aae05deca890dcf94db748d801465bb0b62247cf070cd34016ed810b7b5e3b8" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 1, + "object_id": 1, + "content_hash": "56421ee48deb82f5692315209c0be565c75fde7531c924e68ee0159515de135b", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "d22a0c40cbd68fa52e6afb9ecb920e80fabb689963214e037059b2c45ecfe151" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 2, + "object_id": 0, + "content_hash": "7032e9f88dc8b7f736553b7a9c7236669833b4e633ca24d28ed5c839b692b716", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "5e4b0d6ce028d04f28ab23761ce1f3b0f3f5e4d014f4b4f9b2fdfbd9a6994a00", + "e4e9b40d3ba168108dc827586994c75940b4168078970493d41828b60c816605" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 2, + "object_id": 1, + "content_hash": "07508165a8199e0362b74c498999eea3b023cb67f89b663250ab0677dcb47966", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "df44809333587e5f04510b40dd099829f3214d783044dd56a5683f56a4a5dafd", + "e4e9b40d3ba168108dc827586994c75940b4168078970493d41828b60c816605" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 2, + "object_id": 2, + "content_hash": "b20995ab22a57bc11cfdc93867ba34e004a038fa39b8517e7b09354f9097e1cc", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "afcffc4feb8366e461d33125a05a371ffeeb6d52e8a66f823cd7682788ccd6fc", + "4f8188470c70525b9004625ecb9e384b61df9fb5c4c7cad082e408ea3754ccec" + ], + "direction_bits": 2, + "leaf_index": 2 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 2, + "object_id": 3, + "content_hash": "c4156696922509b57ff04f290a8f880b2c7f9c567d72ac7ef8aa1dd4a3f9702d", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "f0be86f54fbf3a67c5ba7b56d3efb346e7efc1dcd75eec634be99ae33afd52bc", + "4f8188470c70525b9004625ecb9e384b61df9fb5c4c7cad082e408ea3754ccec" + ], + "direction_bits": 3, + "leaf_index": 3 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 3, + "object_id": 0, + "content_hash": "14400b38229ea3c7ba3a9df42a9ad39cc3a9d9a26cf086d1474237e3aaa4f0a3", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "1dd9e51b1130f7abaaa90a130e278f4a85e62f423e0ae95cb122a6b77cccd9f3" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 3, + "object_id": 1, + "content_hash": "70dee7a8e6962e9ea7e80b4b8591bf565799fa0b835ad5e4933fc6ffcf83a81a", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "8fe3347c01ade8ed1f58502398c60ab6b8cc6b68017b2632bde4fd3631edb286" + ], + "direction_bits": 1, + "leaf_index": 1 + } + } + ] +} diff --git a/wai/live-conformance/c2pa-live/receipt-other-objects/receipts.json b/wai/live-conformance/c2pa-live/receipt-other-objects/receipts.json new file mode 100644 index 000000000..7ab606fd5 --- /dev/null +++ b/wai/live-conformance/c2pa-live/receipt-other-objects/receipts.json @@ -0,0 +1,327 @@ +{ + "groups": [ + { + "kind": "group", + "track_alias": 1, + "group_id": 10, + "root_hash": "d7c41cd63b26b976cc556f9f5a5ecb1151421210aa5979e124958e951e7ed309", + "leaf_count": 4, + "joules_total": 0, + "group_start": "2026-10-20T11:00:00.000000Z", + "group_end": "2026-10-20T11:00:02.400000Z", + "signer_pubkey": "4ae5f950c862c6f33f7bb93849fcfb198d87421660fa5ab5e71c0548abf7983e", + "signer_id": "corpus publisher", + "sig": "84df449899ed40a12e7d03c9ec84a37d3e9102d53ddeed19ebf8d9eecad4edb89427f7eb4a34cffda0417a2ffabef2617dd626be82eb3a02fa92623bda3e240d", + "parent_receipt_hash": null + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 11, + "root_hash": "9c8cbf9446de2268f89df7d65f94e8ee8429fbe8506435355878cd8a29024b8a", + "leaf_count": 2, + "joules_total": 0, + "group_start": "2026-10-20T11:00:02.400000Z", + "group_end": "2026-10-20T11:00:03.400000Z", + "signer_pubkey": "4ae5f950c862c6f33f7bb93849fcfb198d87421660fa5ab5e71c0548abf7983e", + "signer_id": "corpus publisher", + "sig": "39b23a7bb5c1ef3db475be398dcb34b433dbb344c1478c8dfddd2a7a19fdf8f279ea1f67bbfcce1a6fa0863a813b4bc28fb7754f86a6735b2462c79fb758360f", + "parent_receipt_hash": "8c02df8dba110bfcf3a8e9e8849fc74c4afcd5fa033b9002efc460964225c11e" + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 12, + "root_hash": "7db0b6d4458309c8be8df8080ce08452bdccc79aed00d85cec614ce5adc83009", + "leaf_count": 4, + "joules_total": 0, + "group_start": "2026-10-20T11:00:03.400000Z", + "group_end": "2026-10-20T11:00:05.400000Z", + "signer_pubkey": "9e5e5a3320bc06913b2cd5b938c59fe79ad4d89015467b16638a41a464719557", + "signer_id": "corpus publisher", + "sig": "02b221bcdcefb2977d2c5d6db6acf971aa1137524976d84f6674af4d2a8ff044cf2eb905d0d730a0422d5a0c53db6af8aa556567e1ebdb022ed22620112fcd0c", + "parent_receipt_hash": null + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 13, + "root_hash": "293931ddebcd8e3cca482475e3b1a93cc2f225427538a3e5da476bf5eae3b4d0", + "leaf_count": 2, + "joules_total": 0, + "group_start": "2026-10-20T11:00:05.400000Z", + "group_end": "2026-10-20T11:00:06.400000Z", + "signer_pubkey": "9e5e5a3320bc06913b2cd5b938c59fe79ad4d89015467b16638a41a464719557", + "signer_id": "corpus publisher", + "sig": "dc2f2c66935d91c8b4dc1f3922670cd0966fec281852bc1d31161b1365bff8a714f9785e17cff3db17e51652b125a95685eff0777052cd924ea9faf64c3d9501", + "parent_receipt_hash": "3899aa43e23adcafb061e18f011c724107eb054da674322abf859f51cbcf9e8f" + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 14, + "root_hash": "54a6deeba8e5e8e4a3b6e5d53f6156408652c1f9fbdb822ab22fe0b8ecb959c0", + "leaf_count": 3, + "joules_total": 0, + "group_start": "2026-10-20T11:00:06.400000Z", + "group_end": "2026-10-20T11:00:08.000000Z", + "signer_pubkey": "9e5e5a3320bc06913b2cd5b938c59fe79ad4d89015467b16638a41a464719557", + "signer_id": "corpus publisher", + "sig": "e39cb9411567ec6d234350a11db255c55ed9b177032cd9b1538dec5290df58e206bcc43137cc9b33532aeed7fbc45d416f8a5f569ef2c5610c6a95552d3a6301", + "parent_receipt_hash": "b331689118cb8ea170eb38a414fa9fee995adc47e50fce629a809d264a0c1c54" + } + ], + "objects": [ + { + "kind": "object", + "track_alias": 1, + "group_id": 10, + "object_id": 0, + "content_hash": "ed3e285f8b0590025be3a310fd384642973f7ab4cc2531ffb3767dc65decbc87", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "84bd9285e1a79ab4d31fc47abcc3d4d7385c4ee38be89090f67b3d0da066fe03", + "a3f56e2717b3cae3def088678e67d4b8266e38cecd14b6a29f03653ef1d01996" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 10, + "object_id": 1, + "content_hash": "c3c0621e9c77910ba45e7c7d6060ca3baa48454877a3c2947c1327f0b5042960", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "476483f91e3ae73abe1fa7137178680a3350d30a86d619ccde04ce7b9d4672f1", + "a3f56e2717b3cae3def088678e67d4b8266e38cecd14b6a29f03653ef1d01996" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 10, + "object_id": 2, + "content_hash": "313e889ebc1aaa08f347a248bae963fdb3ce7cb1ebffd13a0ea862d4833184de", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "2212db8269bac3d91fd34abfda366d918d848c5d0b1f5b900b1421d22cbe0225", + "a954156f25c6e4a22d745879b4e028f3a1039875a5c784423c876399b4e4e2fb" + ], + "direction_bits": 2, + "leaf_index": 2 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 10, + "object_id": 3, + "content_hash": "c9b87a65ceabd21a394a62f0b27677207fa40002294635bc4c19235dfdae2b35", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "b760b6cef18489c4db7a83675f09424498848a4e93f112700ac075dbb1ff05df", + "a954156f25c6e4a22d745879b4e028f3a1039875a5c784423c876399b4e4e2fb" + ], + "direction_bits": 3, + "leaf_index": 3 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 11, + "object_id": 0, + "content_hash": "9cf4550fd7e78c9a34fb59234db253b7d254d4d29e6aab037d072f6f3965dd69", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "0aae05deca890dcf94db748d801465bb0b62247cf070cd34016ed810b7b5e3b8" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 11, + "object_id": 1, + "content_hash": "56421ee48deb82f5692315209c0be565c75fde7531c924e68ee0159515de135b", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "8a86dca33eb5ed4e7c9db15a2d55c6f788bc73a0dae2cd42f50500115bdb2b89" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 12, + "object_id": 0, + "content_hash": "7032e9f88dc8b7f736553b7a9c7236669833b4e633ca24d28ed5c839b692b716", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "5e4b0d6ce028d04f28ab23761ce1f3b0f3f5e4d014f4b4f9b2fdfbd9a6994a00", + "e4e9b40d3ba168108dc827586994c75940b4168078970493d41828b60c816605" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 12, + "object_id": 1, + "content_hash": "07508165a8199e0362b74c498999eea3b023cb67f89b663250ab0677dcb47966", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "df44809333587e5f04510b40dd099829f3214d783044dd56a5683f56a4a5dafd", + "e4e9b40d3ba168108dc827586994c75940b4168078970493d41828b60c816605" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 12, + "object_id": 2, + "content_hash": "b20995ab22a57bc11cfdc93867ba34e004a038fa39b8517e7b09354f9097e1cc", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "afcffc4feb8366e461d33125a05a371ffeeb6d52e8a66f823cd7682788ccd6fc", + "4f8188470c70525b9004625ecb9e384b61df9fb5c4c7cad082e408ea3754ccec" + ], + "direction_bits": 2, + "leaf_index": 2 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 12, + "object_id": 3, + "content_hash": "c4156696922509b57ff04f290a8f880b2c7f9c567d72ac7ef8aa1dd4a3f9702d", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "f0be86f54fbf3a67c5ba7b56d3efb346e7efc1dcd75eec634be99ae33afd52bc", + "4f8188470c70525b9004625ecb9e384b61df9fb5c4c7cad082e408ea3754ccec" + ], + "direction_bits": 3, + "leaf_index": 3 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 13, + "object_id": 0, + "content_hash": "14400b38229ea3c7ba3a9df42a9ad39cc3a9d9a26cf086d1474237e3aaa4f0a3", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "1dd9e51b1130f7abaaa90a130e278f4a85e62f423e0ae95cb122a6b77cccd9f3" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 13, + "object_id": 1, + "content_hash": "70dee7a8e6962e9ea7e80b4b8591bf565799fa0b835ad5e4933fc6ffcf83a81a", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "8fe3347c01ade8ed1f58502398c60ab6b8cc6b68017b2632bde4fd3631edb286" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 14, + "object_id": 0, + "content_hash": "9513817d3f50ea722639343dcc52b1919677ebbf90a269b4202af216e63a2d80", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "4b215fc705ff8f7283d9d8a278bdcb14465d952c871e22748edfa35ace8f7993", + "2ec5cd1f3d701bd4dd6ddb31e8cb3a27dd0ff54ec3a66b91d2e9bb996b6ca883" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 14, + "object_id": 1, + "content_hash": "dd2ff276ca81c40d2152c7bd76e03c28e9d356f20157f1f1af28248aeab6e261", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "6a3fdf96c6c86ba1e7c5b97ec0b966b1b221dcf2f4569589b1c0d927955b3f26", + "2ec5cd1f3d701bd4dd6ddb31e8cb3a27dd0ff54ec3a66b91d2e9bb996b6ca883" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 14, + "object_id": 2, + "content_hash": "287cf5dc07c0ddbbca276149a450e84da9b01d4f26daad93c9061ffbaf93e61d", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "2f713e57cb5e7d69741ceeb374bebd00450d9f233b95f00e3c7ad935e15478a2", + "389e3783984727b17412c2965c5e97ef01b9225dbee3b3200922e2ae9e83ff56" + ], + "direction_bits": 2, + "leaf_index": 2 + } + } + ] +} diff --git a/wai/live-conformance/c2pa-live/receipt-signer/receipts.json b/wai/live-conformance/c2pa-live/receipt-signer/receipts.json new file mode 100644 index 000000000..71013d2bc --- /dev/null +++ b/wai/live-conformance/c2pa-live/receipt-signer/receipts.json @@ -0,0 +1,327 @@ +{ + "groups": [ + { + "kind": "group", + "track_alias": 1, + "group_id": 10, + "root_hash": "d7c41cd63b26b976cc556f9f5a5ecb1151421210aa5979e124958e951e7ed309", + "leaf_count": 4, + "joules_total": 0, + "group_start": "2026-10-20T11:00:00.000000Z", + "group_end": "2026-10-20T11:00:02.400000Z", + "signer_pubkey": "4ae5f950c862c6f33f7bb93849fcfb198d87421660fa5ab5e71c0548abf7983e", + "signer_id": "corpus publisher", + "sig": "84df449899ed40a12e7d03c9ec84a37d3e9102d53ddeed19ebf8d9eecad4edb89427f7eb4a34cffda0417a2ffabef2617dd626be82eb3a02fa92623bda3e240d", + "parent_receipt_hash": null + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 11, + "root_hash": "73ff462e0454b7c6d37af29e979c3a7dcad4d128e15c15035f601c3819d12c71", + "leaf_count": 2, + "joules_total": 0, + "group_start": "2026-10-20T11:00:02.400000Z", + "group_end": "2026-10-20T11:00:03.400000Z", + "signer_pubkey": "a8c27d61a29c34ff447794561bbbd5eb9ff4c6cd7f75989b12e9e97e34645520", + "signer_id": "corpus publisher", + "sig": "e5ee49fee1fd989c5e1c65ff294dbe0711cccbba89e381bd4af7066f4779385e32fd235f63199c9f31dcd994bdd8bfdba4f7c80cc248f95904a5823091adea01", + "parent_receipt_hash": null + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 12, + "root_hash": "7db0b6d4458309c8be8df8080ce08452bdccc79aed00d85cec614ce5adc83009", + "leaf_count": 4, + "joules_total": 0, + "group_start": "2026-10-20T11:00:03.400000Z", + "group_end": "2026-10-20T11:00:05.400000Z", + "signer_pubkey": "9e5e5a3320bc06913b2cd5b938c59fe79ad4d89015467b16638a41a464719557", + "signer_id": "corpus publisher", + "sig": "02b221bcdcefb2977d2c5d6db6acf971aa1137524976d84f6674af4d2a8ff044cf2eb905d0d730a0422d5a0c53db6af8aa556567e1ebdb022ed22620112fcd0c", + "parent_receipt_hash": null + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 13, + "root_hash": "293931ddebcd8e3cca482475e3b1a93cc2f225427538a3e5da476bf5eae3b4d0", + "leaf_count": 2, + "joules_total": 0, + "group_start": "2026-10-20T11:00:05.400000Z", + "group_end": "2026-10-20T11:00:06.400000Z", + "signer_pubkey": "9e5e5a3320bc06913b2cd5b938c59fe79ad4d89015467b16638a41a464719557", + "signer_id": "corpus publisher", + "sig": "dc2f2c66935d91c8b4dc1f3922670cd0966fec281852bc1d31161b1365bff8a714f9785e17cff3db17e51652b125a95685eff0777052cd924ea9faf64c3d9501", + "parent_receipt_hash": "3899aa43e23adcafb061e18f011c724107eb054da674322abf859f51cbcf9e8f" + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 14, + "root_hash": "54a6deeba8e5e8e4a3b6e5d53f6156408652c1f9fbdb822ab22fe0b8ecb959c0", + "leaf_count": 3, + "joules_total": 0, + "group_start": "2026-10-20T11:00:06.400000Z", + "group_end": "2026-10-20T11:00:08.000000Z", + "signer_pubkey": "9e5e5a3320bc06913b2cd5b938c59fe79ad4d89015467b16638a41a464719557", + "signer_id": "corpus publisher", + "sig": "e39cb9411567ec6d234350a11db255c55ed9b177032cd9b1538dec5290df58e206bcc43137cc9b33532aeed7fbc45d416f8a5f569ef2c5610c6a95552d3a6301", + "parent_receipt_hash": "b331689118cb8ea170eb38a414fa9fee995adc47e50fce629a809d264a0c1c54" + } + ], + "objects": [ + { + "kind": "object", + "track_alias": 1, + "group_id": 10, + "object_id": 0, + "content_hash": "ed3e285f8b0590025be3a310fd384642973f7ab4cc2531ffb3767dc65decbc87", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "84bd9285e1a79ab4d31fc47abcc3d4d7385c4ee38be89090f67b3d0da066fe03", + "a3f56e2717b3cae3def088678e67d4b8266e38cecd14b6a29f03653ef1d01996" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 10, + "object_id": 1, + "content_hash": "c3c0621e9c77910ba45e7c7d6060ca3baa48454877a3c2947c1327f0b5042960", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "476483f91e3ae73abe1fa7137178680a3350d30a86d619ccde04ce7b9d4672f1", + "a3f56e2717b3cae3def088678e67d4b8266e38cecd14b6a29f03653ef1d01996" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 10, + "object_id": 2, + "content_hash": "313e889ebc1aaa08f347a248bae963fdb3ce7cb1ebffd13a0ea862d4833184de", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "2212db8269bac3d91fd34abfda366d918d848c5d0b1f5b900b1421d22cbe0225", + "a954156f25c6e4a22d745879b4e028f3a1039875a5c784423c876399b4e4e2fb" + ], + "direction_bits": 2, + "leaf_index": 2 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 10, + "object_id": 3, + "content_hash": "c9b87a65ceabd21a394a62f0b27677207fa40002294635bc4c19235dfdae2b35", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "b760b6cef18489c4db7a83675f09424498848a4e93f112700ac075dbb1ff05df", + "a954156f25c6e4a22d745879b4e028f3a1039875a5c784423c876399b4e4e2fb" + ], + "direction_bits": 3, + "leaf_index": 3 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 11, + "object_id": 0, + "content_hash": "d26598db3232eabc5dd6f8e733f3c684a4ec2b7974b185d88ac3bcd3c020e167", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "0aae05deca890dcf94db748d801465bb0b62247cf070cd34016ed810b7b5e3b8" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 11, + "object_id": 1, + "content_hash": "56421ee48deb82f5692315209c0be565c75fde7531c924e68ee0159515de135b", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "d22a0c40cbd68fa52e6afb9ecb920e80fabb689963214e037059b2c45ecfe151" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 12, + "object_id": 0, + "content_hash": "7032e9f88dc8b7f736553b7a9c7236669833b4e633ca24d28ed5c839b692b716", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "5e4b0d6ce028d04f28ab23761ce1f3b0f3f5e4d014f4b4f9b2fdfbd9a6994a00", + "e4e9b40d3ba168108dc827586994c75940b4168078970493d41828b60c816605" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 12, + "object_id": 1, + "content_hash": "07508165a8199e0362b74c498999eea3b023cb67f89b663250ab0677dcb47966", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "df44809333587e5f04510b40dd099829f3214d783044dd56a5683f56a4a5dafd", + "e4e9b40d3ba168108dc827586994c75940b4168078970493d41828b60c816605" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 12, + "object_id": 2, + "content_hash": "b20995ab22a57bc11cfdc93867ba34e004a038fa39b8517e7b09354f9097e1cc", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "afcffc4feb8366e461d33125a05a371ffeeb6d52e8a66f823cd7682788ccd6fc", + "4f8188470c70525b9004625ecb9e384b61df9fb5c4c7cad082e408ea3754ccec" + ], + "direction_bits": 2, + "leaf_index": 2 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 12, + "object_id": 3, + "content_hash": "c4156696922509b57ff04f290a8f880b2c7f9c567d72ac7ef8aa1dd4a3f9702d", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "f0be86f54fbf3a67c5ba7b56d3efb346e7efc1dcd75eec634be99ae33afd52bc", + "4f8188470c70525b9004625ecb9e384b61df9fb5c4c7cad082e408ea3754ccec" + ], + "direction_bits": 3, + "leaf_index": 3 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 13, + "object_id": 0, + "content_hash": "14400b38229ea3c7ba3a9df42a9ad39cc3a9d9a26cf086d1474237e3aaa4f0a3", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "1dd9e51b1130f7abaaa90a130e278f4a85e62f423e0ae95cb122a6b77cccd9f3" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 13, + "object_id": 1, + "content_hash": "70dee7a8e6962e9ea7e80b4b8591bf565799fa0b835ad5e4933fc6ffcf83a81a", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "8fe3347c01ade8ed1f58502398c60ab6b8cc6b68017b2632bde4fd3631edb286" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 14, + "object_id": 0, + "content_hash": "9513817d3f50ea722639343dcc52b1919677ebbf90a269b4202af216e63a2d80", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "4b215fc705ff8f7283d9d8a278bdcb14465d952c871e22748edfa35ace8f7993", + "2ec5cd1f3d701bd4dd6ddb31e8cb3a27dd0ff54ec3a66b91d2e9bb996b6ca883" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 14, + "object_id": 1, + "content_hash": "dd2ff276ca81c40d2152c7bd76e03c28e9d356f20157f1f1af28248aeab6e261", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "6a3fdf96c6c86ba1e7c5b97ec0b966b1b221dcf2f4569589b1c0d927955b3f26", + "2ec5cd1f3d701bd4dd6ddb31e8cb3a27dd0ff54ec3a66b91d2e9bb996b6ca883" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 14, + "object_id": 2, + "content_hash": "287cf5dc07c0ddbbca276149a450e84da9b01d4f26daad93c9061ffbaf93e61d", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "2f713e57cb5e7d69741ceeb374bebd00450d9f233b95f00e3c7ad935e15478a2", + "389e3783984727b17412c2965c5e97ef01b9225dbee3b3200922e2ae9e83ff56" + ], + "direction_bits": 2, + "leaf_index": 2 + } + } + ] +} diff --git a/wai/live-conformance/c2pa-live/repository-receipt-forged/init.mp4 b/wai/live-conformance/c2pa-live/repository-receipt-forged/init.mp4 new file mode 100644 index 000000000..f95eef117 Binary files /dev/null and b/wai/live-conformance/c2pa-live/repository-receipt-forged/init.mp4 differ diff --git a/wai/live-conformance/c2pa-live/segment-truncated/seg-02.m4s b/wai/live-conformance/c2pa-live/segment-truncated/seg-02.m4s new file mode 100644 index 000000000..177536686 Binary files /dev/null and b/wai/live-conformance/c2pa-live/segment-truncated/seg-02.m4s differ diff --git a/wai/live-conformance/c2pa-live/sequence-mismatch/seg-02.m4s b/wai/live-conformance/c2pa-live/sequence-mismatch/seg-02.m4s new file mode 100644 index 000000000..0efb2116e Binary files /dev/null and b/wai/live-conformance/c2pa-live/sequence-mismatch/seg-02.m4s differ diff --git a/wai/live-conformance/c2pa-live/session-key-binding/init.mp4 b/wai/live-conformance/c2pa-live/session-key-binding/init.mp4 new file mode 100644 index 000000000..570ad9430 Binary files /dev/null and b/wai/live-conformance/c2pa-live/session-key-binding/init.mp4 differ diff --git a/wai/live-conformance/c2pa-live/session-key-binding/seg-02.m4s b/wai/live-conformance/c2pa-live/session-key-binding/seg-02.m4s new file mode 100644 index 000000000..fb750f879 Binary files /dev/null and b/wai/live-conformance/c2pa-live/session-key-binding/seg-02.m4s differ diff --git a/wai/live-conformance/c2pa-live/session-key-superseded/receipts.json b/wai/live-conformance/c2pa-live/session-key-superseded/receipts.json new file mode 100644 index 000000000..cf8f91ef8 --- /dev/null +++ b/wai/live-conformance/c2pa-live/session-key-superseded/receipts.json @@ -0,0 +1,327 @@ +{ + "groups": [ + { + "kind": "group", + "track_alias": 1, + "group_id": 10, + "root_hash": "d7c41cd63b26b976cc556f9f5a5ecb1151421210aa5979e124958e951e7ed309", + "leaf_count": 4, + "joules_total": 0, + "group_start": "2026-10-20T11:00:00.000000Z", + "group_end": "2026-10-20T11:00:02.400000Z", + "signer_pubkey": "4ae5f950c862c6f33f7bb93849fcfb198d87421660fa5ab5e71c0548abf7983e", + "signer_id": "corpus publisher", + "sig": "84df449899ed40a12e7d03c9ec84a37d3e9102d53ddeed19ebf8d9eecad4edb89427f7eb4a34cffda0417a2ffabef2617dd626be82eb3a02fa92623bda3e240d", + "parent_receipt_hash": null + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 11, + "root_hash": "73ff462e0454b7c6d37af29e979c3a7dcad4d128e15c15035f601c3819d12c71", + "leaf_count": 2, + "joules_total": 0, + "group_start": "2026-10-20T11:00:02.400000Z", + "group_end": "2026-10-20T11:00:03.400000Z", + "signer_pubkey": "4ae5f950c862c6f33f7bb93849fcfb198d87421660fa5ab5e71c0548abf7983e", + "signer_id": "corpus publisher", + "sig": "30769cdf9cc26ae3054323d0ba4d16ad9c381c86f05f458c084a707a060bdde8849883f4f8ebcf7c34a1314da2f86e6711e791f1d9fc2c3d8fb9c4598abbba0b", + "parent_receipt_hash": "8c02df8dba110bfcf3a8e9e8849fc74c4afcd5fa033b9002efc460964225c11e" + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 12, + "root_hash": "7db0b6d4458309c8be8df8080ce08452bdccc79aed00d85cec614ce5adc83009", + "leaf_count": 4, + "joules_total": 0, + "group_start": "2026-10-20T11:00:03.400000Z", + "group_end": "2026-10-20T11:00:05.400000Z", + "signer_pubkey": "9e5e5a3320bc06913b2cd5b938c59fe79ad4d89015467b16638a41a464719557", + "signer_id": "corpus publisher", + "sig": "02b221bcdcefb2977d2c5d6db6acf971aa1137524976d84f6674af4d2a8ff044cf2eb905d0d730a0422d5a0c53db6af8aa556567e1ebdb022ed22620112fcd0c", + "parent_receipt_hash": null + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 13, + "root_hash": "293931ddebcd8e3cca482475e3b1a93cc2f225427538a3e5da476bf5eae3b4d0", + "leaf_count": 2, + "joules_total": 0, + "group_start": "2026-10-20T11:00:05.400000Z", + "group_end": "2026-10-20T11:00:06.400000Z", + "signer_pubkey": "4ae5f950c862c6f33f7bb93849fcfb198d87421660fa5ab5e71c0548abf7983e", + "signer_id": "corpus publisher", + "sig": "99ad73ac72a6b82438857655e4919171449152d2857246f4a413ff79d521c2e3ab79ae713a66b5fcfb64872fd401cb9882b46928a9bff5727824ca12c5ef0b00", + "parent_receipt_hash": null + }, + { + "kind": "group", + "track_alias": 1, + "group_id": 14, + "root_hash": "54a6deeba8e5e8e4a3b6e5d53f6156408652c1f9fbdb822ab22fe0b8ecb959c0", + "leaf_count": 3, + "joules_total": 0, + "group_start": "2026-10-20T11:00:06.400000Z", + "group_end": "2026-10-20T11:00:08.000000Z", + "signer_pubkey": "9e5e5a3320bc06913b2cd5b938c59fe79ad4d89015467b16638a41a464719557", + "signer_id": "corpus publisher", + "sig": "e39cb9411567ec6d234350a11db255c55ed9b177032cd9b1538dec5290df58e206bcc43137cc9b33532aeed7fbc45d416f8a5f569ef2c5610c6a95552d3a6301", + "parent_receipt_hash": null + } + ], + "objects": [ + { + "kind": "object", + "track_alias": 1, + "group_id": 10, + "object_id": 0, + "content_hash": "ed3e285f8b0590025be3a310fd384642973f7ab4cc2531ffb3767dc65decbc87", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "84bd9285e1a79ab4d31fc47abcc3d4d7385c4ee38be89090f67b3d0da066fe03", + "a3f56e2717b3cae3def088678e67d4b8266e38cecd14b6a29f03653ef1d01996" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 10, + "object_id": 1, + "content_hash": "c3c0621e9c77910ba45e7c7d6060ca3baa48454877a3c2947c1327f0b5042960", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "476483f91e3ae73abe1fa7137178680a3350d30a86d619ccde04ce7b9d4672f1", + "a3f56e2717b3cae3def088678e67d4b8266e38cecd14b6a29f03653ef1d01996" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 10, + "object_id": 2, + "content_hash": "313e889ebc1aaa08f347a248bae963fdb3ce7cb1ebffd13a0ea862d4833184de", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "2212db8269bac3d91fd34abfda366d918d848c5d0b1f5b900b1421d22cbe0225", + "a954156f25c6e4a22d745879b4e028f3a1039875a5c784423c876399b4e4e2fb" + ], + "direction_bits": 2, + "leaf_index": 2 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 10, + "object_id": 3, + "content_hash": "c9b87a65ceabd21a394a62f0b27677207fa40002294635bc4c19235dfdae2b35", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "b760b6cef18489c4db7a83675f09424498848a4e93f112700ac075dbb1ff05df", + "a954156f25c6e4a22d745879b4e028f3a1039875a5c784423c876399b4e4e2fb" + ], + "direction_bits": 3, + "leaf_index": 3 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 11, + "object_id": 0, + "content_hash": "d26598db3232eabc5dd6f8e733f3c684a4ec2b7974b185d88ac3bcd3c020e167", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "0aae05deca890dcf94db748d801465bb0b62247cf070cd34016ed810b7b5e3b8" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 11, + "object_id": 1, + "content_hash": "56421ee48deb82f5692315209c0be565c75fde7531c924e68ee0159515de135b", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "d22a0c40cbd68fa52e6afb9ecb920e80fabb689963214e037059b2c45ecfe151" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 12, + "object_id": 0, + "content_hash": "7032e9f88dc8b7f736553b7a9c7236669833b4e633ca24d28ed5c839b692b716", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "5e4b0d6ce028d04f28ab23761ce1f3b0f3f5e4d014f4b4f9b2fdfbd9a6994a00", + "e4e9b40d3ba168108dc827586994c75940b4168078970493d41828b60c816605" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 12, + "object_id": 1, + "content_hash": "07508165a8199e0362b74c498999eea3b023cb67f89b663250ab0677dcb47966", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "df44809333587e5f04510b40dd099829f3214d783044dd56a5683f56a4a5dafd", + "e4e9b40d3ba168108dc827586994c75940b4168078970493d41828b60c816605" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 12, + "object_id": 2, + "content_hash": "b20995ab22a57bc11cfdc93867ba34e004a038fa39b8517e7b09354f9097e1cc", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "afcffc4feb8366e461d33125a05a371ffeeb6d52e8a66f823cd7682788ccd6fc", + "4f8188470c70525b9004625ecb9e384b61df9fb5c4c7cad082e408ea3754ccec" + ], + "direction_bits": 2, + "leaf_index": 2 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 12, + "object_id": 3, + "content_hash": "c4156696922509b57ff04f290a8f880b2c7f9c567d72ac7ef8aa1dd4a3f9702d", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "f0be86f54fbf3a67c5ba7b56d3efb346e7efc1dcd75eec634be99ae33afd52bc", + "4f8188470c70525b9004625ecb9e384b61df9fb5c4c7cad082e408ea3754ccec" + ], + "direction_bits": 3, + "leaf_index": 3 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 13, + "object_id": 0, + "content_hash": "14400b38229ea3c7ba3a9df42a9ad39cc3a9d9a26cf086d1474237e3aaa4f0a3", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "1dd9e51b1130f7abaaa90a130e278f4a85e62f423e0ae95cb122a6b77cccd9f3" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 13, + "object_id": 1, + "content_hash": "70dee7a8e6962e9ea7e80b4b8591bf565799fa0b835ad5e4933fc6ffcf83a81a", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "8fe3347c01ade8ed1f58502398c60ab6b8cc6b68017b2632bde4fd3631edb286" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 14, + "object_id": 0, + "content_hash": "9513817d3f50ea722639343dcc52b1919677ebbf90a269b4202af216e63a2d80", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "4b215fc705ff8f7283d9d8a278bdcb14465d952c871e22748edfa35ace8f7993", + "2ec5cd1f3d701bd4dd6ddb31e8cb3a27dd0ff54ec3a66b91d2e9bb996b6ca883" + ], + "direction_bits": 0, + "leaf_index": 0 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 14, + "object_id": 1, + "content_hash": "dd2ff276ca81c40d2152c7bd76e03c28e9d356f20157f1f1af28248aeab6e261", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "6a3fdf96c6c86ba1e7c5b97ec0b966b1b221dcf2f4569589b1c0d927955b3f26", + "2ec5cd1f3d701bd4dd6ddb31e8cb3a27dd0ff54ec3a66b91d2e9bb996b6ca883" + ], + "direction_bits": 1, + "leaf_index": 1 + } + }, + { + "kind": "object", + "track_alias": 1, + "group_id": 14, + "object_id": 2, + "content_hash": "287cf5dc07c0ddbbca276149a450e84da9b01d4f26daad93c9061ffbaf93e61d", + "joules_micro": 0, + "origin": "corpus", + "merkle_proof": { + "siblings": [ + "2f713e57cb5e7d69741ceeb374bebd00450d9f233b95f00e3c7ad935e15478a2", + "389e3783984727b17412c2965c5e97ef01b9225dbee3b3200922e2ae9e83ff56" + ], + "direction_bits": 2, + "leaf_index": 2 + } + } + ] +} diff --git a/wai/live-conformance/c2pa-live/session-key-superseded/seg-03.m4s b/wai/live-conformance/c2pa-live/session-key-superseded/seg-03.m4s new file mode 100644 index 000000000..539381e9a Binary files /dev/null and b/wai/live-conformance/c2pa-live/session-key-superseded/seg-03.m4s differ diff --git a/wai/live-conformance/c2pa-live/session-key-unknown/seg-01.m4s b/wai/live-conformance/c2pa-live/session-key-unknown/seg-01.m4s new file mode 100644 index 000000000..98d3018fd Binary files /dev/null and b/wai/live-conformance/c2pa-live/session-key-unknown/seg-01.m4s differ diff --git a/wai/live-conformance/c2pa-live/stream-assertion-room/init.mp4 b/wai/live-conformance/c2pa-live/stream-assertion-room/init.mp4 new file mode 100644 index 000000000..4a357348f Binary files /dev/null and b/wai/live-conformance/c2pa-live/stream-assertion-room/init.mp4 differ diff --git a/wai/live-conformance/c2pa-live/stream-id-mismatch/seg-02.m4s b/wai/live-conformance/c2pa-live/stream-id-mismatch/seg-02.m4s new file mode 100644 index 000000000..605f94247 Binary files /dev/null and b/wai/live-conformance/c2pa-live/stream-id-mismatch/seg-02.m4s differ diff --git a/wai/live-conformance/c2pa-live/take-both/init.mp4 b/wai/live-conformance/c2pa-live/take-both/init.mp4 new file mode 100644 index 000000000..4fa159462 Binary files /dev/null and b/wai/live-conformance/c2pa-live/take-both/init.mp4 differ diff --git a/wai/live-conformance/c2pa-live/take-both/seg-00.m4s b/wai/live-conformance/c2pa-live/take-both/seg-00.m4s new file mode 100644 index 000000000..b7009a0af Binary files /dev/null and b/wai/live-conformance/c2pa-live/take-both/seg-00.m4s differ diff --git a/wai/live-conformance/c2pa-live/take-both/seg-01.m4s b/wai/live-conformance/c2pa-live/take-both/seg-01.m4s new file mode 100644 index 000000000..e7f0eb60d Binary files /dev/null and b/wai/live-conformance/c2pa-live/take-both/seg-01.m4s differ diff --git a/wai/live-conformance/c2pa-live/take-both/seg-02.m4s b/wai/live-conformance/c2pa-live/take-both/seg-02.m4s new file mode 100644 index 000000000..31a80e7d4 Binary files /dev/null and b/wai/live-conformance/c2pa-live/take-both/seg-02.m4s differ diff --git a/wai/live-conformance/c2pa-live/update-ingredient/init.mp4 b/wai/live-conformance/c2pa-live/update-ingredient/init.mp4 new file mode 100644 index 000000000..a7b486d1b Binary files /dev/null and b/wai/live-conformance/c2pa-live/update-ingredient/init.mp4 differ diff --git a/wai/live-conformance/c2pa-live/vectors.json b/wai/live-conformance/c2pa-live/vectors.json new file mode 100644 index 000000000..af9d60a72 --- /dev/null +++ b/wai/live-conformance/c2pa-live/vectors.json @@ -0,0 +1,1522 @@ +{ + "extension": "live-provenance", + "time": "2026-10-20T12:00:00Z", + "anchors": [ + "anchor.der" + ], + "repositories": [ + { + "uri": "https://repository.example/", + "key": "e16db63840b1128fc9e750611c4c3bc8bd722f335370e25ea6d9dfb75c05bc4b" + } + ], + "vectors": [ + { + "name": "avatar-manifests", + "kind": "track", + "init": "avatar-manifests/init.mp4", + "segments": [ + { + "file": "avatar-manifests/seg-00.m4s", + "group": 0 + }, + { + "file": "avatar-manifests/seg-01.m4s", + "group": 1 + }, + { + "file": "avatar-manifests/seg-02.m4s", + "group": 2 + } + ], + "receipts": "avatar-manifests/receipts.json", + "expect": { + "outcome": "accept", + "streamId": "0504592f-2332-8bcd-98de-c4b9c976105b", + "sourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia", + "registration": null, + "segments": [ + { + "sequenceNumber": 1, + "group": 0, + "methods": "manifest", + "manifestId": "urn:c2pa:608ac18c-2c3e-44da-9650-23624d1876f5", + "chunks": 2, + "root": "99c1ee93a49d44a71d04407c59b1f688f1ccd160c388ea2a94a4ebfe0ee162e0" + }, + { + "sequenceNumber": 3, + "group": 1, + "methods": "manifest", + "manifestId": "urn:c2pa:9dc14fbd-001b-45d4-8287-f538401130a6", + "chunks": 2, + "root": "3e5165b1371e3693e542dca31f1aeba5c362bff06c7c64b39237913101faedb9" + }, + { + "sequenceNumber": 5, + "group": 2, + "methods": "manifest", + "manifestId": "urn:c2pa:2103dfe4-39bd-48ec-878e-647b12b8a4a6", + "chunks": 3, + "root": "c605d0c6ba8546635e3783f9a130355667c68085e44f043ae18df8ba974ce6b3" + } + ] + } + }, + { + "name": "camera-manifests", + "kind": "track", + "init": "camera-manifests/init.mp4", + "segments": [ + { + "file": "camera-manifests/seg-00.m4s", + "group": 0 + }, + { + "file": "camera-manifests/seg-01.m4s", + "group": 1 + }, + { + "file": "camera-manifests/seg-02.m4s", + "group": 2 + }, + { + "file": "camera-manifests/seg-03.m4s", + "group": 3 + } + ], + "receipts": "camera-manifests/receipts.json", + "expect": { + "outcome": "accept", + "streamId": "b5c52adf-4778-8fba-870f-a8f43ec11a21", + "sourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture", + "registration": null, + "segments": [ + { + "sequenceNumber": 1, + "group": 0, + "methods": "manifest", + "manifestId": "urn:c2pa:5a3c760d-2dd2-45a1-840b-62c5137c20f7", + "chunks": 4, + "root": "d7c41cd63b26b976cc556f9f5a5ecb1151421210aa5979e124958e951e7ed309" + }, + { + "sequenceNumber": 5, + "group": 1, + "methods": "manifest", + "manifestId": "urn:c2pa:15daceef-ff56-4d9f-9bbc-41516a0ed1cb", + "chunks": 2, + "root": "73ff462e0454b7c6d37af29e979c3a7dcad4d128e15c15035f601c3819d12c71" + }, + { + "sequenceNumber": 7, + "group": 2, + "methods": "manifest", + "manifestId": "urn:c2pa:ccbccd38-a042-4ee4-92cd-3f8b1a50bd63", + "chunks": 4, + "root": "7db0b6d4458309c8be8df8080ce08452bdccc79aed00d85cec614ce5adc83009" + }, + { + "sequenceNumber": 11, + "group": 3, + "methods": "manifest", + "manifestId": "urn:c2pa:f925eca9-ed21-49ba-8624-276c59d2f1dd", + "chunks": 2, + "root": "293931ddebcd8e3cca482475e3b1a93cc2f225427538a3e5da476bf5eae3b4d0" + } + ] + } + }, + { + "name": "camera-vsi", + "kind": "track", + "init": "camera-vsi/init.mp4", + "segments": [ + { + "file": "camera-vsi/seg-00.m4s", + "group": 10 + }, + { + "file": "camera-vsi/seg-01.m4s", + "group": 11 + }, + { + "file": "camera-vsi/seg-02.m4s", + "group": 12 + }, + { + "file": "camera-vsi/seg-03.m4s", + "group": 13 + }, + { + "file": "camera-vsi/seg-04.m4s", + "group": 14 + } + ], + "receipts": "camera-vsi/receipts.json", + "expect": { + "outcome": "accept", + "streamId": "b5c52adf-4778-8fba-870f-a8f43ec11a21", + "sourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture", + "registration": null, + "segments": [ + { + "sequenceNumber": 1, + "group": 10, + "methods": "vsi", + "manifestId": "urn:c2pa:d01c313c-0490-47b6-a7a4-19dc9cea455a", + "chunks": 4, + "root": "d7c41cd63b26b976cc556f9f5a5ecb1151421210aa5979e124958e951e7ed309" + }, + { + "sequenceNumber": 5, + "group": 11, + "methods": "vsi", + "manifestId": "urn:c2pa:d01c313c-0490-47b6-a7a4-19dc9cea455a", + "chunks": 2, + "root": "73ff462e0454b7c6d37af29e979c3a7dcad4d128e15c15035f601c3819d12c71" + }, + { + "sequenceNumber": 7, + "group": 12, + "methods": "vsi", + "manifestId": "urn:c2pa:7846a908-7da5-41c5-86d1-378b265103c0", + "chunks": 4, + "root": "7db0b6d4458309c8be8df8080ce08452bdccc79aed00d85cec614ce5adc83009" + }, + { + "sequenceNumber": 11, + "group": 13, + "methods": "vsi", + "manifestId": "urn:c2pa:7846a908-7da5-41c5-86d1-378b265103c0", + "chunks": 2, + "root": "293931ddebcd8e3cca482475e3b1a93cc2f225427538a3e5da476bf5eae3b4d0" + }, + { + "sequenceNumber": 13, + "group": 14, + "methods": "vsi", + "manifestId": "urn:c2pa:7846a908-7da5-41c5-86d1-378b265103c0", + "chunks": 3, + "root": "54a6deeba8e5e8e4a3b6e5d53f6156408652c1f9fbdb822ab22fe0b8ecb959c0" + } + ] + } + }, + { + "name": "composite-manifests", + "kind": "track", + "init": "composite-manifests/init.mp4", + "segments": [ + { + "file": "composite-manifests/seg-00.m4s", + "group": 0 + }, + { + "file": "composite-manifests/seg-01.m4s", + "group": 1 + }, + { + "file": "composite-manifests/seg-02.m4s", + "group": 2 + } + ], + "receipts": "composite-manifests/receipts.json", + "expect": { + "outcome": "accept", + "streamId": "fc7ed304-c2e5-8da2-9381-58538a50569c", + "sourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/compositeSynthetic", + "registration": null, + "segments": [ + { + "sequenceNumber": 1, + "group": 0, + "methods": "manifest", + "manifestId": "urn:c2pa:1877cd3e-e550-4175-8bcd-a7e2ac153229", + "chunks": 3, + "root": "e1a83ba6bfd481ac0f831d04edbccf3d2ff94b2e447ba3add4ab9b481220d8e1" + }, + { + "sequenceNumber": 4, + "group": 1, + "methods": "manifest", + "manifestId": "urn:c2pa:ba49aaea-a4ac-42a8-b123-a72d3047e25b", + "chunks": 4, + "root": "db7bfcbeb3cc6749cac67629bcf4b80ea89576de4ab35e26c01cf9cec5e89a2b" + }, + { + "sequenceNumber": 8, + "group": 2, + "methods": "manifest", + "manifestId": "urn:c2pa:4b9c1e70-94ae-4ed9-b5c5-7c100bdd6885", + "chunks": 4, + "root": "197d569450354354a4c0334579bbcf74930804b3231ac5eeb14c396dfbbd8e47" + } + ] + } + }, + { + "name": "take-both", + "kind": "take", + "init": "take-both/init.mp4", + "segments": [ + { + "file": "take-both/seg-00.m4s" + }, + { + "file": "take-both/seg-01.m4s" + }, + { + "file": "take-both/seg-02.m4s" + } + ], + "receipts": null, + "expect": { + "outcome": "accept", + "streamId": "b9b2466e-c02d-89ff-8064-b0b1946f3970", + "sourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture", + "registration": null, + "segments": [ + { + "sequenceNumber": 1, + "chunk": 0, + "methods": "manifest+vsi", + "manifestId": "urn:c2pa:46640c73-2c1c-44b2-ad04-030f5fdcf53c", + "chunks": 4, + "chunkHash": "18a4fd303e96075c2b1b708543e348e4a1e81198f9c1dc81392eefbecd6fdbf6" + }, + { + "sequenceNumber": 5, + "chunk": 1, + "methods": "manifest+vsi", + "manifestId": "urn:c2pa:8a1b8075-9922-410c-8992-d5e8c5578f5f", + "chunks": 2, + "chunkHash": "0999bbd7989d6596ce764046fec03476a6b8ea09d4fc8af71ee5e3d4f00d669f" + }, + { + "sequenceNumber": 7, + "chunk": 2, + "methods": "manifest+vsi", + "manifestId": "urn:c2pa:83351869-5224-40bb-8b66-a8bbdf9b6512", + "chunks": 4, + "chunkHash": "a2f02f3d6d8ee0713d21f93d00fe170597d2d01e56d48bfd91929b14256582e0" + } + ] + } + }, + { + "name": "cert-wrong-eku", + "kind": "track", + "init": "cert-wrong-eku/init.mp4", + "segments": [ + { + "file": "cert-wrong-eku/seg-00.m4s", + "group": 0 + }, + { + "file": "cert-wrong-eku/seg-01.m4s", + "group": 1 + }, + { + "file": "cert-wrong-eku/seg-02.m4s", + "group": 2 + }, + { + "file": "cert-wrong-eku/seg-03.m4s", + "group": 3 + } + ], + "receipts": "camera-manifests/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "credential_invalid", + "at": "init", + "c2pa": "signingCredential.invalid", + "livevideo": "livevideo.manifest.invalid" + } + }, + { + "name": "cert-untrusted", + "kind": "track", + "init": "cert-untrusted/init.mp4", + "segments": [ + { + "file": "cert-untrusted/seg-00.m4s", + "group": 0 + }, + { + "file": "cert-untrusted/seg-01.m4s", + "group": 1 + }, + { + "file": "cert-untrusted/seg-02.m4s", + "group": 2 + }, + { + "file": "cert-untrusted/seg-03.m4s", + "group": 3 + } + ], + "receipts": "camera-manifests/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "credential_untrusted", + "at": "init", + "c2pa": "signingCredential.untrusted", + "livevideo": "livevideo.manifest.invalid" + } + }, + { + "name": "cert-expired", + "kind": "track", + "init": "cert-expired/init.mp4", + "segments": [ + { + "file": "cert-expired/seg-00.m4s", + "group": 0 + }, + { + "file": "cert-expired/seg-01.m4s", + "group": 1 + }, + { + "file": "cert-expired/seg-02.m4s", + "group": 2 + }, + { + "file": "cert-expired/seg-03.m4s", + "group": 3 + } + ], + "receipts": "camera-manifests/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "credential_expired", + "at": "init", + "c2pa": "claimSignature.outsideValidity", + "livevideo": "livevideo.manifest.invalid" + } + }, + { + "name": "stream-assertion-room", + "kind": "track", + "init": "stream-assertion-room/init.mp4", + "segments": [ + { + "file": "camera-manifests/seg-00.m4s", + "group": 0 + }, + { + "file": "camera-manifests/seg-01.m4s", + "group": 1 + }, + { + "file": "camera-manifests/seg-02.m4s", + "group": 2 + }, + { + "file": "camera-manifests/seg-03.m4s", + "group": 3 + } + ], + "receipts": "camera-manifests/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "stream_assertion_invalid", + "at": "init", + "c2pa": null, + "livevideo": null + } + }, + { + "name": "ai-disclosure-missing", + "kind": "track", + "init": "ai-disclosure-missing/init.mp4", + "segments": [ + { + "file": "ai-disclosure-missing/seg-00.m4s", + "group": 0 + }, + { + "file": "ai-disclosure-missing/seg-01.m4s", + "group": 1 + }, + { + "file": "ai-disclosure-missing/seg-02.m4s", + "group": 2 + } + ], + "receipts": "avatar-manifests/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "ai_disclosure_missing", + "at": "init", + "c2pa": null, + "livevideo": null + } + }, + { + "name": "composite-source-type", + "kind": "track", + "init": "composite-source-type/init.mp4", + "segments": [ + { + "file": "composite-source-type/seg-00.m4s", + "group": 0 + }, + { + "file": "composite-source-type/seg-01.m4s", + "group": 1 + }, + { + "file": "composite-source-type/seg-02.m4s", + "group": 2 + } + ], + "receipts": "composite-manifests/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "source_type_mismatch", + "at": "init", + "c2pa": null, + "livevideo": null + } + }, + { + "name": "sequence-mismatch", + "kind": "track", + "init": "camera-manifests/init.mp4", + "segments": [ + { + "file": "camera-manifests/seg-00.m4s", + "group": 0 + }, + { + "file": "camera-manifests/seg-01.m4s", + "group": 1 + }, + { + "file": "sequence-mismatch/seg-02.m4s", + "group": 2 + }, + { + "file": "camera-manifests/seg-03.m4s", + "group": 3 + } + ], + "receipts": "camera-manifests/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "sequence_mismatch", + "at": "segment 2", + "c2pa": null, + "livevideo": null + } + }, + { + "name": "stream-id-mismatch", + "kind": "track", + "init": "camera-manifests/init.mp4", + "segments": [ + { + "file": "camera-manifests/seg-00.m4s", + "group": 0 + }, + { + "file": "camera-manifests/seg-01.m4s", + "group": 1 + }, + { + "file": "stream-id-mismatch/seg-02.m4s", + "group": 2 + }, + { + "file": "camera-manifests/seg-03.m4s", + "group": 3 + } + ], + "receipts": "camera-manifests/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "stream_id_mismatch", + "at": "segment 2", + "c2pa": null, + "livevideo": "livevideo.assertion.invalid" + } + }, + { + "name": "continuity-method", + "kind": "track", + "init": "camera-manifests/init.mp4", + "segments": [ + { + "file": "camera-manifests/seg-00.m4s", + "group": 0 + }, + { + "file": "continuity-method/seg-01.m4s", + "group": 1 + }, + { + "file": "camera-manifests/seg-02.m4s", + "group": 2 + }, + { + "file": "camera-manifests/seg-03.m4s", + "group": 3 + } + ], + "receipts": "camera-manifests/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "continuity_invalid", + "at": "segment 1", + "c2pa": null, + "livevideo": "livevideo.continuityMethod.invalid" + } + }, + { + "name": "wrong-receipt", + "kind": "track", + "init": "camera-manifests/init.mp4", + "segments": [ + { + "file": "camera-manifests/seg-00.m4s", + "group": 0 + }, + { + "file": "wrong-receipt/seg-01.m4s", + "group": 1 + }, + { + "file": "camera-manifests/seg-02.m4s", + "group": 2 + }, + { + "file": "camera-manifests/seg-03.m4s", + "group": 3 + } + ], + "receipts": "camera-manifests/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "receipt_mismatch", + "at": "group 1", + "c2pa": null, + "livevideo": null + } + }, + { + "name": "vsi-manifest-id", + "kind": "track", + "init": "camera-vsi/init.mp4", + "segments": [ + { + "file": "camera-vsi/seg-00.m4s", + "group": 10 + }, + { + "file": "camera-vsi/seg-01.m4s", + "group": 11 + }, + { + "file": "camera-vsi/seg-02.m4s", + "group": 12 + }, + { + "file": "vsi-manifest-id/seg-03.m4s", + "group": 13 + }, + { + "file": "camera-vsi/seg-04.m4s", + "group": 14 + } + ], + "receipts": "camera-vsi/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "segment_info_invalid", + "at": "segment 3", + "c2pa": null, + "livevideo": "livevideo.segment.invalid" + } + }, + { + "name": "session-key-unknown", + "kind": "track", + "init": "camera-vsi/init.mp4", + "segments": [ + { + "file": "camera-vsi/seg-00.m4s", + "group": 10 + }, + { + "file": "session-key-unknown/seg-01.m4s", + "group": 11 + }, + { + "file": "camera-vsi/seg-02.m4s", + "group": 12 + }, + { + "file": "camera-vsi/seg-03.m4s", + "group": 13 + }, + { + "file": "camera-vsi/seg-04.m4s", + "group": 14 + } + ], + "receipts": "camera-vsi/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "session_key_unknown", + "at": "segment 1", + "c2pa": null, + "livevideo": "livevideo.segment.invalid" + } + }, + { + "name": "session-key-superseded", + "kind": "track", + "init": "camera-vsi/init.mp4", + "segments": [ + { + "file": "camera-vsi/seg-00.m4s", + "group": 10 + }, + { + "file": "camera-vsi/seg-01.m4s", + "group": 11 + }, + { + "file": "camera-vsi/seg-02.m4s", + "group": 12 + }, + { + "file": "session-key-superseded/seg-03.m4s", + "group": 13 + }, + { + "file": "camera-vsi/seg-04.m4s", + "group": 14 + } + ], + "receipts": "session-key-superseded/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "session_key_out_of_range", + "at": "segment 3", + "c2pa": null, + "livevideo": "livevideo.segment.invalid" + } + }, + { + "name": "session-key-binding", + "kind": "track", + "init": "session-key-binding/init.mp4", + "segments": [ + { + "file": "camera-vsi/seg-00.m4s", + "group": 10 + }, + { + "file": "camera-vsi/seg-01.m4s", + "group": 11 + }, + { + "file": "session-key-binding/seg-02.m4s", + "group": 12 + }, + { + "file": "camera-vsi/seg-03.m4s", + "group": 13 + }, + { + "file": "camera-vsi/seg-04.m4s", + "group": 14 + } + ], + "receipts": "camera-vsi/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "session_key_invalid", + "at": "init", + "c2pa": null, + "livevideo": "livevideo.sessionkey.invalid" + } + }, + { + "name": "receipt-signer", + "kind": "track", + "init": "camera-vsi/init.mp4", + "segments": [ + { + "file": "camera-vsi/seg-00.m4s", + "group": 10 + }, + { + "file": "camera-vsi/seg-01.m4s", + "group": 11 + }, + { + "file": "camera-vsi/seg-02.m4s", + "group": 12 + }, + { + "file": "camera-vsi/seg-03.m4s", + "group": 13 + }, + { + "file": "camera-vsi/seg-04.m4s", + "group": 14 + } + ], + "receipts": "receipt-signer/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "receipt_signer_mismatch", + "at": "group 11", + "c2pa": null, + "livevideo": null + } + }, + { + "name": "receipt-other-objects", + "kind": "track", + "init": "camera-vsi/init.mp4", + "segments": [ + { + "file": "camera-vsi/seg-00.m4s", + "group": 10 + }, + { + "file": "camera-vsi/seg-01.m4s", + "group": 11 + }, + { + "file": "camera-vsi/seg-02.m4s", + "group": 12 + }, + { + "file": "camera-vsi/seg-03.m4s", + "group": 13 + }, + { + "file": "camera-vsi/seg-04.m4s", + "group": 14 + } + ], + "receipts": "receipt-other-objects/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "receipt_mismatch", + "at": "group 11", + "c2pa": null, + "livevideo": null + } + }, + { + "name": "manifest-id-not-derived", + "kind": "track", + "init": "camera-manifests/init.mp4", + "segments": [ + { + "file": "camera-manifests/seg-00.m4s", + "group": 0 + }, + { + "file": "manifest-id-not-derived/seg-01.m4s", + "group": 1 + }, + { + "file": "manifest-id-not-derived/seg-02.m4s", + "group": 2 + }, + { + "file": "camera-manifests/seg-03.m4s", + "group": 3 + } + ], + "receipts": "camera-manifests/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "manifest_id_mismatch", + "at": "segment 1", + "c2pa": null, + "livevideo": null + } + }, + { + "name": "camera-failover", + "kind": "track", + "init": "camera-manifests/init.mp4", + "segments": [ + { + "file": "camera-failover/seg-00.m4s", + "group": 0 + }, + { + "file": "camera-failover/seg-01.m4s", + "group": 1 + }, + { + "file": "camera-failover/seg-02.m4s", + "group": 2 + }, + { + "file": "camera-failover/seg-03.m4s", + "group": 3 + } + ], + "receipts": "camera-failover/receipts.json", + "expect": { + "outcome": "accept", + "streamId": "b5c52adf-4778-8fba-870f-a8f43ec11a21", + "sourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture", + "registration": null, + "segments": [ + { + "sequenceNumber": 1, + "group": 0, + "methods": "manifest", + "manifestId": "urn:c2pa:5a3c760d-2dd2-45a1-840b-62c5137c20f7", + "chunks": 4, + "root": "d7c41cd63b26b976cc556f9f5a5ecb1151421210aa5979e124958e951e7ed309" + }, + { + "sequenceNumber": 5, + "group": 1, + "methods": "manifest", + "manifestId": "urn:c2pa:15daceef-ff56-4d9f-9bbc-41516a0ed1cb", + "chunks": 2, + "root": "73ff462e0454b7c6d37af29e979c3a7dcad4d128e15c15035f601c3819d12c71" + }, + { + "sequenceNumber": 7, + "group": 2, + "methods": "manifest", + "manifestId": "urn:c2pa:ccbccd38-a042-4ee4-92cd-3f8b1a50bd63", + "chunks": 4, + "root": "7db0b6d4458309c8be8df8080ce08452bdccc79aed00d85cec614ce5adc83009" + }, + { + "sequenceNumber": 11, + "group": 3, + "methods": "manifest", + "manifestId": "urn:c2pa:f925eca9-ed21-49ba-8624-276c59d2f1dd", + "chunks": 2, + "root": "293931ddebcd8e3cca482475e3b1a93cc2f225427538a3e5da476bf5eae3b4d0" + } + ] + } + }, + { + "name": "camera-registered", + "kind": "track", + "init": "camera-registered/init.mp4", + "segments": [ + { + "file": "camera-manifests/seg-00.m4s", + "group": 0 + }, + { + "file": "camera-manifests/seg-01.m4s", + "group": 1 + }, + { + "file": "camera-manifests/seg-02.m4s", + "group": 2 + }, + { + "file": "camera-manifests/seg-03.m4s", + "group": 3 + } + ], + "receipts": "camera-manifests/receipts.json", + "expect": { + "outcome": "accept", + "streamId": "b5c52adf-4778-8fba-870f-a8f43ec11a21", + "sourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture", + "registration": { + "repository": "https://repository.example/", + "treeSize": 7, + "leafIndex": 4 + }, + "segments": [ + { + "sequenceNumber": 1, + "group": 0, + "methods": "manifest", + "manifestId": "urn:c2pa:5a3c760d-2dd2-45a1-840b-62c5137c20f7", + "chunks": 4, + "root": "d7c41cd63b26b976cc556f9f5a5ecb1151421210aa5979e124958e951e7ed309" + }, + { + "sequenceNumber": 5, + "group": 1, + "methods": "manifest", + "manifestId": "urn:c2pa:15daceef-ff56-4d9f-9bbc-41516a0ed1cb", + "chunks": 2, + "root": "73ff462e0454b7c6d37af29e979c3a7dcad4d128e15c15035f601c3819d12c71" + }, + { + "sequenceNumber": 7, + "group": 2, + "methods": "manifest", + "manifestId": "urn:c2pa:ccbccd38-a042-4ee4-92cd-3f8b1a50bd63", + "chunks": 4, + "root": "7db0b6d4458309c8be8df8080ce08452bdccc79aed00d85cec614ce5adc83009" + }, + { + "sequenceNumber": 11, + "group": 3, + "methods": "manifest", + "manifestId": "urn:c2pa:f925eca9-ed21-49ba-8624-276c59d2f1dd", + "chunks": 2, + "root": "293931ddebcd8e3cca482475e3b1a93cc2f225427538a3e5da476bf5eae3b4d0" + } + ] + } + }, + { + "name": "repository-receipt-forged", + "kind": "track", + "init": "repository-receipt-forged/init.mp4", + "segments": [ + { + "file": "camera-manifests/seg-00.m4s", + "group": 0 + }, + { + "file": "camera-manifests/seg-01.m4s", + "group": 1 + }, + { + "file": "camera-manifests/seg-02.m4s", + "group": 2 + }, + { + "file": "camera-manifests/seg-03.m4s", + "group": 3 + } + ], + "receipts": "camera-manifests/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "repository_receipt_invalid", + "at": "init", + "c2pa": null, + "livevideo": null + } + }, + { + "name": "update-ingredient", + "kind": "track", + "init": "update-ingredient/init.mp4", + "segments": [ + { + "file": "camera-manifests/seg-00.m4s", + "group": 0 + }, + { + "file": "camera-manifests/seg-01.m4s", + "group": 1 + }, + { + "file": "camera-manifests/seg-02.m4s", + "group": 2 + }, + { + "file": "camera-manifests/seg-03.m4s", + "group": 3 + } + ], + "receipts": "camera-manifests/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "update_manifest_invalid", + "at": "init", + "c2pa": "manifest.update.invalid", + "livevideo": "livevideo.manifest.invalid" + } + }, + { + "name": "bytes-changed-manifests", + "kind": "track", + "init": "camera-manifests/init.mp4", + "segments": [ + { + "file": "camera-manifests/seg-00.m4s", + "group": 0 + }, + { + "file": "camera-manifests/seg-01.m4s", + "group": 1 + }, + { + "file": "bytes-changed-manifests/seg-02.m4s", + "group": 2 + }, + { + "file": "camera-manifests/seg-03.m4s", + "group": 3 + } + ], + "receipts": "camera-manifests/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "hard_binding_mismatch", + "at": "segment 2", + "c2pa": "assertion.bmffHash.mismatch", + "livevideo": "livevideo.manifest.invalid" + } + }, + { + "name": "bytes-changed-vsi", + "kind": "track", + "init": "camera-vsi/init.mp4", + "segments": [ + { + "file": "camera-vsi/seg-00.m4s", + "group": 10 + }, + { + "file": "bytes-changed-vsi/seg-01.m4s", + "group": 11 + }, + { + "file": "camera-vsi/seg-02.m4s", + "group": 12 + }, + { + "file": "camera-vsi/seg-03.m4s", + "group": 13 + }, + { + "file": "camera-vsi/seg-04.m4s", + "group": 14 + } + ], + "receipts": "camera-vsi/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "segment_hash_mismatch", + "at": "segment 1", + "c2pa": "assertion.bmffHash.mismatch", + "livevideo": "livevideo.segment.invalid" + } + }, + { + "name": "segment-missing-manifests", + "kind": "track", + "init": "camera-manifests/init.mp4", + "segments": [ + { + "file": "camera-manifests/seg-00.m4s", + "group": 0 + }, + { + "file": "camera-manifests/seg-01.m4s", + "group": 1 + }, + { + "file": "camera-manifests/seg-03.m4s", + "group": 3 + } + ], + "receipts": "camera-manifests/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "continuity_broken", + "at": "segment 2", + "c2pa": null, + "livevideo": "livevideo.segment.invalid" + } + }, + { + "name": "segment-missing-vsi", + "kind": "track", + "init": "camera-vsi/init.mp4", + "segments": [ + { + "file": "camera-vsi/seg-00.m4s", + "group": 10 + }, + { + "file": "camera-vsi/seg-02.m4s", + "group": 12 + }, + { + "file": "camera-vsi/seg-03.m4s", + "group": 13 + }, + { + "file": "camera-vsi/seg-04.m4s", + "group": 14 + } + ], + "receipts": "camera-vsi/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "segment_missing", + "at": "group 11", + "c2pa": null, + "livevideo": null + } + }, + { + "name": "segment-reordered", + "kind": "track", + "init": "camera-vsi/init.mp4", + "segments": [ + { + "file": "camera-vsi/seg-00.m4s", + "group": 10 + }, + { + "file": "camera-vsi/seg-02.m4s", + "group": 12 + }, + { + "file": "camera-vsi/seg-01.m4s", + "group": 11 + }, + { + "file": "camera-vsi/seg-03.m4s", + "group": 13 + }, + { + "file": "camera-vsi/seg-04.m4s", + "group": 14 + } + ], + "receipts": "camera-vsi/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "sequence_not_increasing", + "at": "segment 2", + "c2pa": null, + "livevideo": "livevideo.assertion.invalid" + } + }, + { + "name": "segment-truncated", + "kind": "track", + "init": "camera-manifests/init.mp4", + "segments": [ + { + "file": "camera-manifests/seg-00.m4s", + "group": 0 + }, + { + "file": "camera-manifests/seg-01.m4s", + "group": 1 + }, + { + "file": "segment-truncated/seg-02.m4s", + "group": 2 + }, + { + "file": "camera-manifests/seg-03.m4s", + "group": 3 + } + ], + "receipts": "camera-manifests/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "segment_malformed", + "at": "segment 2", + "c2pa": null, + "livevideo": "livevideo.segment.invalid" + } + }, + { + "name": "init-mdat", + "kind": "track", + "init": "init-mdat/init.mp4", + "segments": [ + { + "file": "camera-manifests/seg-00.m4s", + "group": 0 + }, + { + "file": "camera-manifests/seg-01.m4s", + "group": 1 + }, + { + "file": "camera-manifests/seg-02.m4s", + "group": 2 + }, + { + "file": "camera-manifests/seg-03.m4s", + "group": 3 + } + ], + "receipts": "camera-manifests/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "init_invalid", + "at": "init", + "c2pa": null, + "livevideo": "livevideo.init.invalid" + } + }, + { + "name": "provenance-missing", + "kind": "track", + "init": "camera-vsi/init.mp4", + "segments": [ + { + "file": "camera-vsi/seg-00.m4s", + "group": 10 + }, + { + "file": "provenance-missing/seg-01.m4s", + "group": 11 + }, + { + "file": "camera-vsi/seg-02.m4s", + "group": 12 + }, + { + "file": "camera-vsi/seg-03.m4s", + "group": 13 + }, + { + "file": "camera-vsi/seg-04.m4s", + "group": 14 + } + ], + "receipts": "camera-vsi/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "provenance_missing", + "at": "segment 1", + "c2pa": null, + "livevideo": "livevideo.segment.invalid" + } + }, + { + "name": "manifest-malformed", + "kind": "track", + "init": "manifest-malformed/init.mp4", + "segments": [ + { + "file": "camera-manifests/seg-00.m4s", + "group": 0 + }, + { + "file": "camera-manifests/seg-01.m4s", + "group": 1 + }, + { + "file": "camera-manifests/seg-02.m4s", + "group": 2 + }, + { + "file": "camera-manifests/seg-03.m4s", + "group": 3 + } + ], + "receipts": "camera-manifests/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "manifest_malformed", + "at": "init", + "c2pa": "claim.malformed", + "livevideo": "livevideo.manifest.invalid" + } + }, + { + "name": "claim-signature", + "kind": "track", + "init": "camera-manifests/init.mp4", + "segments": [ + { + "file": "camera-manifests/seg-00.m4s", + "group": 0 + }, + { + "file": "claim-signature/seg-01.m4s", + "group": 1 + }, + { + "file": "camera-manifests/seg-02.m4s", + "group": 2 + }, + { + "file": "camera-manifests/seg-03.m4s", + "group": 3 + } + ], + "receipts": "camera-manifests/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "claim_signature_mismatch", + "at": "segment 1", + "c2pa": "claimSignature.mismatch", + "livevideo": "livevideo.manifest.invalid" + } + }, + { + "name": "assertion-altered", + "kind": "track", + "init": "camera-manifests/init.mp4", + "segments": [ + { + "file": "camera-manifests/seg-00.m4s", + "group": 0 + }, + { + "file": "assertion-altered/seg-01.m4s", + "group": 1 + }, + { + "file": "camera-manifests/seg-02.m4s", + "group": 2 + }, + { + "file": "camera-manifests/seg-03.m4s", + "group": 3 + } + ], + "receipts": "camera-manifests/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "assertion_hash_mismatch", + "at": "segment 1", + "c2pa": "assertion.hashedURI.mismatch", + "livevideo": "livevideo.manifest.invalid" + } + }, + { + "name": "vsi-signature", + "kind": "track", + "init": "camera-vsi/init.mp4", + "segments": [ + { + "file": "camera-vsi/seg-00.m4s", + "group": 10 + }, + { + "file": "camera-vsi/seg-01.m4s", + "group": 11 + }, + { + "file": "vsi-signature/seg-02.m4s", + "group": 12 + }, + { + "file": "camera-vsi/seg-03.m4s", + "group": 13 + }, + { + "file": "camera-vsi/seg-04.m4s", + "group": 14 + } + ], + "receipts": "camera-vsi/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "segment_info_signature_failed", + "at": "segment 2", + "c2pa": null, + "livevideo": "livevideo.segment.invalid" + } + }, + { + "name": "receipt-missing", + "kind": "track", + "init": "camera-manifests/init.mp4", + "segments": [ + { + "file": "camera-manifests/seg-00.m4s", + "group": 0 + }, + { + "file": "camera-manifests/seg-01.m4s", + "group": 1 + }, + { + "file": "camera-manifests/seg-02.m4s", + "group": 2 + }, + { + "file": "camera-manifests/seg-03.m4s", + "group": 3 + } + ], + "receipts": "receipt-missing/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "receipt_missing", + "at": "group 1", + "c2pa": null, + "livevideo": null + } + }, + { + "name": "receipt-forged", + "kind": "track", + "init": "camera-manifests/init.mp4", + "segments": [ + { + "file": "camera-manifests/seg-00.m4s", + "group": 0 + }, + { + "file": "camera-manifests/seg-01.m4s", + "group": 1 + }, + { + "file": "camera-manifests/seg-02.m4s", + "group": 2 + }, + { + "file": "camera-manifests/seg-03.m4s", + "group": 3 + } + ], + "receipts": "receipt-forged/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "receipt_invalid", + "at": "group 1", + "c2pa": null, + "livevideo": null + } + }, + { + "name": "object-receipt", + "kind": "track", + "init": "camera-manifests/init.mp4", + "segments": [ + { + "file": "camera-manifests/seg-00.m4s", + "group": 0 + }, + { + "file": "camera-manifests/seg-01.m4s", + "group": 1 + }, + { + "file": "camera-manifests/seg-02.m4s", + "group": 2 + }, + { + "file": "camera-manifests/seg-03.m4s", + "group": 3 + } + ], + "receipts": "object-receipt/receipts.json", + "expect": { + "outcome": "refuse", + "reason": "object_receipt_invalid", + "at": "group 1", + "c2pa": null, + "livevideo": null + } + } + ] +} diff --git a/wai/live-conformance/c2pa-live/vsi-manifest-id/seg-03.m4s b/wai/live-conformance/c2pa-live/vsi-manifest-id/seg-03.m4s new file mode 100644 index 000000000..c51f92a2e Binary files /dev/null and b/wai/live-conformance/c2pa-live/vsi-manifest-id/seg-03.m4s differ diff --git a/wai/live-conformance/c2pa-live/vsi-signature/seg-02.m4s b/wai/live-conformance/c2pa-live/vsi-signature/seg-02.m4s new file mode 100644 index 000000000..7f188105f Binary files /dev/null and b/wai/live-conformance/c2pa-live/vsi-signature/seg-02.m4s differ diff --git a/wai/live-conformance/c2pa-live/wrong-receipt/seg-01.m4s b/wai/live-conformance/c2pa-live/wrong-receipt/seg-01.m4s new file mode 100644 index 000000000..60a7a6a58 Binary files /dev/null and b/wai/live-conformance/c2pa-live/wrong-receipt/seg-01.m4s differ diff --git a/wai/live-conformance/c2pa_live_verify.py b/wai/live-conformance/c2pa_live_verify.py new file mode 100644 index 000000000..5ee606c7c --- /dev/null +++ b/wai/live-conformance/c2pa_live_verify.py @@ -0,0 +1,1691 @@ +#!/usr/bin/env python3 +"""Independent verifier of the live-provenance corpus (extensions/live-provenance.md). + +Written from the extension's text and the texts it reads (C2PA 2.4 §19 and what it relies on, +RFC 9052, RFC 9053, RFC 9360, RFC 5280, RFC 8410, jwp-receipts §2 and §3). It shares no code +with wai-rs, which generates the corpus, and never runs it. It reads, with the standard library +alone: + +- CBOR in RFC 8949 §4.2.1's core deterministic encoding; +- ISO BMFF boxes: a segment's layout (§3.2), its chunks, the initialization segment's `moov`, + the C2PA Manifest Box and the VSI `emsg` box, and the BMFF hash over whole-box exclusions + (§4.5); +- the JUMBF manifest store (§4.1), the claim (§4.2) and its `COSE_Sign1_Tagged` (§4.3); +- X.509 DER: the claim signer's certificates, their profile (§4.4), their chain to the trust + anchor and their validity (§11.2); +- the session keys, their `signerBinding` and rotation (§7), and each segment-info map; +- the JWP group and object receipts of each group (§8; jwp-receipts §2.1 to §2.4 and §3). + +Signatures are verified with ../claim-conformance/_ed25519.py, WAI's strict rule written from +RFC 8032, and BLAKE3 is ../claim-conformance/_blake3.py. The codes a refusal reports are read +from the extension's §12 table. + +For every vector it requires the outcome, the reason, the place and the two C2PA codes, and for +an accepted stream what §11.6 states; of the corpus, that every reason of §12 refuses a vector +and that no other file is there. Then it mutates copies of the good vectors, one defect each, +and requires every copy to be refused. + + python3 c2pa_live_verify.py + +Exit status 0 only if every vector verifies and every mutated copy is refused. Python 3.8 or +later; standard library only. +""" + +import base64 +import calendar +import copy +import hashlib +import json +import os +import re +import struct +import sys +import time + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, os.path.join(HERE, "..", "claim-conformance")) +import _blake3 as B3 # noqa: E402 +import _ed25519 as ED # noqa: E402 + +EXTENSION = os.path.join(HERE, "..", "extensions", "live-provenance.md") + +C2PA_UUID = bytes.fromhex("D8FEC3D61B0E483C92975828877EC481") # Appendix A.5.1 +VSI_SCHEME = "urn:c2pa:verifiable-segment-info" # §19.4.2 +STREAM_DOMAIN = b"wai:live-provenance:stream\x01" # §3.4 +MANIFEST_DOMAIN = b"wai:live-provenance:manifest\x01" # §4.2 +ROLE_INIT, ROLE_SEGMENT, ROLE_ROTATION, ROLE_UPDATE = 0, 1, 2, 3 # §4.2 +L_INGREDIENT, L_REPOSITORY = "c2pa.ingredient.v3", "c2pa.repository-receipt" # §4.7 +DST_PREFIX = "http://cv.iptc.org/newscodes/digitalsourcetype/" +MODEL_URI = "tag:transaction.science,2026:wai/model/" # timed-text §10.3 +L_HASH, L_ACTIONS, L_LIVE, L_KEYS, L_AI = ( + "c2pa.hash.bmff.v3", "c2pa.actions.v2", "c2pa.livevideo.segment", "c2pa.session-keys", "c2pa.ai-disclosure") +L_STREAM, L_GROUP, L_TAKE = "science.transaction.wai.stream", "science.transaction.wai.group", "science.transaction.wai.take" + + +def jtype(prefix): + """A JUMBF type UUID: four characters, then 0011-0010-8000-00AA00389B71 (§4.1).""" + return prefix + bytes.fromhex("00110010800000AA00389B71") + + +T_STORE, T_MANIFEST, T_UPDATE, T_ASSERTIONS, T_CBOR, T_JSON, T_CLAIM, T_SIG = ( + jtype(b"c2pa"), jtype(b"c2ma"), jtype(b"c2um"), jtype(b"c2as"), jtype(b"cbor"), jtype(b"json"), jtype(b"c2cl"), + jtype(b"c2cs")) + +# X.509 object identifiers, as content octets. +def oid(dotted): + arcs = [int(x) for x in dotted.split(".")] + out = bytearray() + for v in [arcs[0] * 40 + arcs[1]] + arcs[2:]: + tmp = [v & 0x7F] + v >>= 7 + while v: + tmp.append(0x80 | (v & 0x7F)) + v >>= 7 + out += bytes(reversed(tmp)) + return bytes(out) + + +OID_ED25519 = oid("1.3.101.112") +OID_BC, OID_KU, OID_EKU, OID_SKI, OID_AKI = (oid("2.5.29.19"), oid("2.5.29.15"), oid("2.5.29.37"), + oid("2.5.29.14"), oid("2.5.29.35")) +EKU_CLAIM, EKU_TSA, EKU_OCSP, EKU_ANY = (oid("1.3.6.1.4.1.62558.2.1"), oid("1.3.6.1.5.5.7.3.8"), + oid("1.3.6.1.5.5.7.3.9"), oid("2.5.29.37.0")) +ED25519_ALG = b"\x30\x05\x06\x03" + OID_ED25519 + + +class Refuse(Exception): + def __init__(self, reason, at): + super().__init__(reason) + self.reason, self.at = reason, at + + +def reason_table(): + """§12: each reason and its two C2PA codes, from the extension's own table.""" + text = open(EXTENSION, encoding="utf-8").read() + sec = text[text.index("\n## 12. Reasons"):] + sec = sec[:sec.index("\n## 13.")] + out = {} + for line in sec.splitlines(): + if line.startswith("| `"): + cells = [c.strip().strip("`") for c in line.split("|")] + out[cells[1]] = (None if cells[3] == "—" else cells[3], None if cells[4] == "—" else cells[4]) + return out + + +REASONS = reason_table() + +# ───────────────────────────── CBOR (RFC 8949 §4.2.1) ───────────────────────────── + + +class Tag: + def __init__(self, tag, value): + self.tag, self.value = tag, value + + def __eq__(self, o): + return isinstance(o, Tag) and (self.tag, self.value) == (o.tag, o.value) + + +class CborError(Exception): + pass + + +def cbor_read(b): + """One data item in deterministic encoding, with nothing after it.""" + pos = [0] + + def take(n): + if len(b) - pos[0] < n: + raise CborError("short") + s = b[pos[0]:pos[0] + n] + pos[0] += n + return s + + def arg(ai): + if ai < 24: + return ai + if ai > 27: + raise CborError("indefinite or reserved") + n = {24: 1, 25: 2, 26: 4, 27: 8}[ai] + v = int.from_bytes(take(n), "big") + if v < {1: 24, 2: 256, 4: 65536, 8: 1 << 32}[n]: + raise CborError("not the shortest head") + return v + + def item(depth): + if depth > 16: + raise CborError("too deep") + ib = take(1)[0] + major, ai = ib >> 5, ib & 31 + if major == 7: + if ai == 20: + return False + if ai == 21: + return True + if ai == 22: + return None + raise CborError("unsupported simple or float") + v = arg(ai) + if major == 0: + return v + if major == 1: + return -1 - v + if major == 2: + return bytes(take(v)) + if major == 3: + try: + return take(v).decode("utf-8") + except UnicodeDecodeError: + raise CborError("text") + if major == 4: + return [item(depth + 1) for _ in range(v)] + if major == 5: + m, prev = {}, None + for _ in range(v): + s = pos[0] + k = item(depth + 1) + kb = b[s:pos[0]] + if prev is not None and not prev < kb: + raise CborError("keys out of order") + prev = kb + if isinstance(k, (list, dict)): + raise CborError("key type") + m[k] = item(depth + 1) + return m + return Tag(v, item(depth + 1)) + + v = item(1) + if pos[0] != len(b): + raise CborError("trailing bytes") + return v + + +def head(major, v): + if v < 24: + return bytes([major << 5 | v]) + for n, ai in ((1, 24), (2, 25), (4, 26), (8, 27)): + if v < 1 << (8 * n): + return bytes([major << 5 | ai]) + v.to_bytes(n, "big") + + +def sig_structure(protected, payload): + """RFC 9052 §4.4: ["Signature1", protected, h'', payload], encoded.""" + return (b"\x84" + head(3, 10) + b"Signature1" + head(2, len(protected)) + protected + head(2, 0) + + head(2, len(payload)) + payload) + + +def is_map(m, required, optional=()): + return (isinstance(m, dict) and all(isinstance(k, str) and (k in required or k in optional) for k in m) + and all(k in m for k in required)) + + +def uint(v): + return isinstance(v, int) and not isinstance(v, bool) and v >= 0 + + +def b32(v): + return isinstance(v, bytes) and len(v) == 32 + + +# ───────────────────────────── BMFF ───────────────────────────── + + +def boxes(b, start, end): + out, p = [], start + while p < end: + if end - p < 8: + return None + size = struct.unpack(">I", b[p:p + 4])[0] + if size < 8 or end - p < size: + return None + out.append((b[p + 4:p + 8], p, p + size)) + p += size + return out + + +def full(b, r): + """(version, flags) of a full box.""" + if r[2] - r[1] < 12: + return None + v = struct.unpack(">I", b[r[1] + 8:r[1] + 12])[0] + return v >> 24, v & 0xFFFFFF + + +def is_c2pa_uuid(b, r): + return r[0] == b"uuid" and b[r[1] + 8:r[1] + 24] == C2PA_UUID + + +def manifest_store_bytes(b, r, purpose=b"manifest"): + """The store a C2PA uuid box of `purpose` holds (§4.1, §4.7; Appendix A.5.3).""" + body = b[r[1] + 8:r[2]] + pre = C2PA_UUID + bytes(4) + purpose + b"\x00" + bytes(8) + return body[len(pre):] if body.startswith(pre) else None + + +VSI_MARK = VSI_SCHEME.encode() + b"\x00" + + +def is_vsi(b, r): + return r[0] == b"emsg" and b[r[1] + 12:r[1] + 12 + len(VSI_MARK)] == VSI_MARK + + +def read_emsg(b, r): + if r[0] != b"emsg" or full(b, r) != (0, 0): + return None + body = b[r[1] + 12:r[2]] + z1 = body.find(b"\x00") + if z1 < 0: + return None + z2 = body.find(b"\x00", z1 + 1) + if z2 < 0: + return None + rest = body[z2 + 1:] + if len(rest) < 16: + return None + try: + scheme, value = body[:z1].decode(), body[z1 + 1:z2].decode() + except UnicodeDecodeError: + return None + ts, ptd, dur, eid = struct.unpack(">IIII", rest[:16]) + return dict(scheme=scheme, value=value, timescale=ts, ptd=ptd, duration=dur, id=eid, data=rest[16:]) + + +PROFILE_EXCLUSIONS = [ + {"xpath": "/uuid", "data": [{"offset": 8, "value": C2PA_UUID}]}, + {"xpath": "/ftyp"}, + {"xpath": "/mfra"}, + {"xpath": "/emsg", "data": [{"offset": 12, "value": VSI_MARK}]}, +] + + +def bmff_hash(b, top): + """§4.5: SHA-256 over each top-level box no exclusion matches, as its u64 offset, then its bytes.""" + h = hashlib.sha256() + for ty, s, e in top: + excluded = False + for x in PROFILE_EXCLUSIONS: + if x["xpath"] != "/" + ty.decode("latin-1"): + continue + if all(s + d["offset"] + len(d["value"]) <= e and b[s + d["offset"]:s + d["offset"] + len(d["value"])] == d["value"] + for d in x.get("data", [])): + excluded = True + if not excluded: + h.update(struct.pack(">Q", s)) + h.update(b[s:e]) + return h.digest() + + +def children(b, r, skip=0): + if r[2] - r[1] < 8 + skip: + return None + return boxes(b, r[1] + 8 + skip, r[2]) + + +def read_chunk(b, moof, mdat): + """§3.2's chunk: moof (mfhd, traf: tfhd, tfdt, trun) then mdat; the trun's data at the mdat's payload.""" + if moof[0] != b"moof" or mdat[0] != b"mdat" or mdat[1] != moof[2]: + return None + mc = children(b, moof) + if mc is None or [c[0] for c in mc] != [b"mfhd", b"traf"]: + return None + mfhd, traf = mc + if full(b, mfhd) != (0, 0) or mfhd[2] - mfhd[1] != 16: + return None + seq = struct.unpack(">I", b[mfhd[1] + 12:mfhd[1] + 16])[0] + tc = children(b, traf) + if tc is None or [c[0] for c in tc] != [b"tfhd", b"tfdt", b"trun"]: + return None + tfhd, tfdt, trun = tc + if full(b, tfhd) != (0, 0x020000) or tfhd[2] - tfhd[1] != 16: + return None + track_id = struct.unpack(">I", b[tfhd[1] + 12:tfhd[1] + 16])[0] + if full(b, tfdt) != (1, 0) or tfdt[2] - tfdt[1] != 20: + return None + bmdt = struct.unpack(">Q", b[tfdt[1] + 12:tfdt[1] + 20])[0] + if full(b, trun) != (0, 0x000701) or trun[2] - trun[1] < 20: + return None + count = struct.unpack(">I", b[trun[1] + 12:trun[1] + 16])[0] + if trun[2] - trun[1] != 20 + 12 * count: + return None + offset = struct.unpack(">i", b[trun[1] + 16:trun[1] + 20])[0] + if offset != moof[2] - moof[1] + 8: + return None + dur = size = 0 + for i in range(count): + d, s, _ = struct.unpack(">III", b[trun[1] + 20 + 12 * i:trun[1] + 32 + 12 * i]) + dur, size = dur + d, size + s + if size != mdat[2] - mdat[1] - 8: + return None + return dict(seq=seq, track_id=track_id, bmdt=bmdt, duration=dur) + + +def read_moov(b, moov): + mc = children(b, moov) + if mc is None: + return None + traks = [c for c in mc if c[0] == b"trak"] + mvex = [c for c in mc if c[0] == b"mvex"] + if len(traks) != 1 or not mvex: + return None + mv = children(b, mvex[0]) + if mv is None or not any(c[0] == b"trex" for c in mv): + return None + tc = children(b, traks[0]) + if tc is None: + return None + tkhd = [c for c in tc if c[0] == b"tkhd"] + mdia = [c for c in tc if c[0] == b"mdia"] + if not tkhd or not mdia: + return None + md = children(b, mdia[0]) + mdhd = [c for c in (md or []) if c[0] == b"mdhd"] + if not mdhd: + return None + + def field(r): + vf = full(b, r) + if vf is None or vf[0] not in (0, 1): + return None + at = 20 if vf[0] == 0 else 28 + if r[2] - r[1] < at + 4: + return None + return struct.unpack(">I", b[r[1] + at:r[1] + at + 4])[0] + + track_id, timescale = field(tkhd[0]), field(mdhd[0]) + if track_id is None or not timescale: + return None + return dict(track_id=track_id, timescale=timescale) + + +# ───────────────────────────── JUMBF (§4.1) ───────────────────────────── + + +def read_super(b, r): + if r[0] != b"jumb": + return None + inner = boxes(b, r[1] + 8, r[2]) + if not inner or inner[0][0] != b"jumd": + return None + d = b[inner[0][1] + 8:inner[0][2]] + if len(d) < 18 or d[16] != 0x03 or d[-1] != 0: + return None + label = d[17:-1] + if not label or 0 in label: + return None + try: + label = label.decode("utf-8") + except UnicodeDecodeError: + return None + return dict(type=d[:16], label=label, content=inner[1:]) + + +def one_cbor(b, s, ty=b"cbor"): + if len(s["content"]) != 1 or s["content"][0][0] != ty: + return None + c = s["content"][0] + return b[c[1] + 8:c[2]] + + +def read_store(b): + top = boxes(b, 0, len(b)) + if not top or len(top) != 1: + return None + store = read_super(b, top[0]) + if not store or store["type"] != T_STORE or store["label"] != "c2pa" or len(store["content"]) != 1: + return None + mbox = store["content"][0] + man = read_super(b, mbox) + if not man or man["type"] not in (T_MANIFEST, T_UPDATE) or len(man["content"]) != 3: + return None + parts = [read_super(b, r) for r in man["content"]] + want = [(T_ASSERTIONS, "c2pa.assertions"), (T_CLAIM, "c2pa.claim.v2"), (T_SIG, "c2pa.signature")] + if any(p is None or (p["type"], p["label"]) != w for p, w in zip(parts, want)): + return None + claim, sig = one_cbor(b, parts[1]), one_cbor(b, parts[2]) + if claim is None or sig is None: + return None + assertions = [] + for r in parts[0]["content"]: + a = read_super(b, r) + if not a or a["type"] not in (T_CBOR, T_JSON): + return None + form = "cbor" if a["type"] == T_CBOR else "json" + c = one_cbor(b, a, form.encode()) + if c is None: + return None + assertions.append(dict(label=a["label"], form=form, cbor=c, hash=hashlib.sha256(b[r[1] + 8:r[2]]).digest())) + sbox = man["content"][2] + return dict(type=man["type"], id=man["label"], assertions=assertions, claim=claim, signature=sig, + manifest_hash=hashlib.sha256(b[mbox[1] + 8:mbox[2]]).digest(), + signature_hash=hashlib.sha256(b[sbox[1] + 8:sbox[2]]).digest()) + + +# ───────────────────────────── COSE (§4.3, §7) ───────────────────────────── + + +def read_sign1(item): + """A COSE_Sign1_Tagged item: protected map with alg EdDSA (-8), unprotected map, payload or nil, 64-byte signature.""" + if not isinstance(item, Tag) or item.tag != 18 or not isinstance(item.value, list) or len(item.value) != 4: + return None + p, u, payload, sig = item.value + if not isinstance(p, bytes) or not isinstance(u, dict) or not isinstance(sig, bytes) or len(sig) != 64: + return None + if not (payload is None or isinstance(payload, bytes)): + return None + try: + ph = cbor_read(p) + except CborError: + return None + if not isinstance(ph, dict) or ph.get(1) != -8 or 1 in u: + return None + return dict(pbytes=p, protected=ph, unprotected=u, payload=payload, sig=sig) + + +def cose_verifies(s, key, signed): + return ED.verify(key, sig_structure(s["pbytes"], signed), s["sig"]) is None + + +def read_okp(k): + """§7.1's OKP COSE_Key: {1: 1, 2: kid, -1: 6, -2: x}, kid equal to x.""" + if not isinstance(k, dict) or len(k) != 4 or set(k) != {1, 2, -1, -2}: + return None + if k[1] != 1 or k[-1] != 6 or not b32(k[-2]) or k[2] != k[-2]: + return None + return k[-2] + + +# ───────────────────────────── X.509 (§4.4, §11.2) ───────────────────────────── + + +def der(b): + """(tag, content, whole, rest) of the first element, DER lengths only.""" + if len(b) < 2 or b[0] & 0x1F == 0x1F: + return None + l0 = b[1] + if l0 < 0x80: + n, hdr = l0, 2 + else: + k = l0 & 0x7F + if k == 0 or k > 4 or len(b) < 2 + k or b[2] == 0: + return None + n = int.from_bytes(b[2:2 + k], "big") + if n < 0x80: + return None + hdr = 2 + k + if len(b) < hdr + n: + return None + return b[0], b[hdr:hdr + n], b[:hdr + n], b[hdr + n:] + + +def der_items(b): + out = [] + while b: + e = der(b) + if e is None: + return None + out.append(e) + b = e[3] + return out + + +def der_only(b, tag): + e = der(b) + if e is None or e[3] or e[0] != tag: + return None + return e + + +def read_time(e): + try: + s = e[1].decode("ascii") + except UnicodeDecodeError: + return None + if e[0] == 0x17 and len(s) == 13: + yy = int(s[:2]) if s[:2].isdigit() else -1 + if yy < 0: + return None + y, rest = (1900 + yy if yy >= 50 else 2000 + yy), s[2:] + elif e[0] == 0x18 and len(s) == 15: + if not s[:4].isdigit(): + return None + y, rest = int(s[:4]), s[4:] + else: + return None + if not rest.endswith("Z") or not rest[:-1].isdigit(): + return None + mo, d, h, mi, se = (int(rest[i:i + 2]) for i in range(0, 10, 2)) + if not (1 <= mo <= 12 and 1 <= d <= 31 and h <= 23 and mi <= 59 and se <= 59): + return None + return unix(y, mo, d, h, mi, se) + + +def unix(y, mo, d, h, mi, se): + # Days from the civil date, proleptic Gregorian, as calendar.timegm counts them without range limits. + yy = y - 1 if mo <= 2 else y + era = yy // 400 + yoe = yy - era * 400 + doy = (153 * (mo - 3 if mo > 2 else mo + 9) + 2) // 5 + d - 1 + doe = yoe * 365 + yoe // 4 - yoe // 100 + doy + return (era * 146097 + doe - 719468) * 86400 + h * 3600 + mi * 60 + se + + +def read_cert(b): + top = der_only(b, 0x30) + if top is None: + return None + parts = der_items(top[1]) + if parts is None or len(parts) != 3: + return None + tbs, alg, bits = parts + if tbs[0] != 0x30 or alg[0] != 0x30 or bits[0] != 0x03: + return None + t = der_items(tbs[1]) + if not t: + return None + version, i = 0, 0 + if t[0][0] == 0xA0: + v = der_only(t[0][1], 0x02) + if v is None: + return None + version, i = int.from_bytes(v[1], "big"), 1 + if len(t) < i + 6: + return None + serial, tbs_alg, issuer, validity, subject, spki = t[i:i + 6] + if [x[0] for x in (serial, tbs_alg, issuer, validity, subject, spki)] != [0x02, 0x30, 0x30, 0x30, 0x30, 0x30]: + return None + v = der_items(validity[1]) + sp = der_items(spki[1]) + if not v or len(v) != 2 or not sp or len(sp) != 2: + return None + if sp[0][2] != ED25519_ALG or sp[1][0] != 0x03 or len(sp[1][1]) != 33 or sp[1][1][0] != 0: + return None + unique, exts = False, [] + for e in t[i + 6:]: + if e[0] in (0x81, 0x82): + unique = True + elif e[0] == 0xA3: + s = der_only(e[1], 0x30) + xs = der_items(s[1]) if s else None + if xs is None: + return None + for x in xs: + if x[0] != 0x30: + return None + f = der_items(x[1]) + if f and len(f) == 2: + o, crit, val = f[0], False, f[1] + elif f and len(f) == 3 and f[1][0] == 0x01 and f[1][1] == b"\xff": + o, crit, val = f[0], True, f[2] + else: + return None + if o[0] != 0x06 or val[0] != 0x04: + return None + exts.append((o[1], crit, val[1])) + else: + return None + if len(bits[1]) != 65 or bits[1][0] != 0: + return None + nb, na = read_time(v[0]), read_time(v[1]) + if nb is None or na is None: + return None + return dict(tbs=tbs[2], version=version, issuer=issuer[2], subject=subject[2], nb=nb, na=na, + key=sp[1][1][1:], unique=unique, ed=tbs_alg[2] == ED25519_ALG and alg[2] == ED25519_ALG, + exts=exts, sig=bits[1][1:]) + + +def ext(c, o): + for x in c["exts"]: + if x[0] == o: + return x + return None + + +def ski(c): + x = ext(c, OID_SKI) + e = der_only(x[2], 0x04) if x else None + return e[1] if e else None + + +def aki(c): + x = ext(c, OID_AKI) + s = der_only(x[2], 0x30) if x else None + f = der_items(s[1]) if s else None + if f is None: + return None + for e in f: + if e[0] == 0x80: + return e[1] + return None + + +def profile_ok(c, ca): + """§4.4 and C2PA 2.4 §14.5.1.1, for the end entity (ca False) or an intermediate.""" + known = {OID_BC, OID_KU, OID_EKU, OID_SKI, OID_AKI} + seen = set() + for o, crit, _ in c["exts"]: + if o in seen or (crit and o not in known): + return False + seen.add(o) + if c["version"] != 2 or not c["ed"] or c["unique"] or aki(c) is None: + return False + bc = ext(c, OID_BC) + is_ca = False + if bc: + s = der_only(bc[2], 0x30) + if s is None: + return False + f = der_items(s[1]) + if f is None: + return False + is_ca = bool(f) and f[0][0] == 0x01 and f[0][1] == b"\xff" + ku = ext(c, OID_KU) + kb = der_only(ku[2], 0x03) if ku else None + if kb is None or not kb[1] or len(kb[1]) > 3 or kb[1][0] > 7: + return False + first = kb[1][1] if len(kb[1]) > 1 else 0 + digital_signature, key_cert_sign = bool(first & 0x80), bool(first & 0x04) + if ca: + return is_ca and key_cert_sign and ski(c) is not None + if is_ca or not digital_signature or key_cert_sign: + return False + e = ext(c, OID_EKU) + s = der_only(e[2], 0x30) if e else None + f = der_items(s[1]) if s else None + if f is None or any(x[0] != 0x06 for x in f): + return False + ekus = {x[1] for x in f} + return bool(ekus) and EKU_ANY not in ekus and EKU_CLAIM in ekus and EKU_TSA not in ekus and EKU_OCSP not in ekus + + +def issued_by(c, issuer): + a = aki(c) + s = ski(issuer) + return (c["issuer"] == issuer["subject"] and a is not None and (s is None or s == a) + and ED.verify(issuer["key"], c["tbs"], c["sig"]) is None) + + +# ───────────────────────────── receipts (jwp-receipts §2, §3) ───────────────────────────── + + +def blake3(*parts): + return B3.blake3(b"".join(parts)) + + +GROUP_FIELDS = ["kind", "track_alias", "group_id", "root_hash", "leaf_count", "joules_total", "group_start", + "group_end", "signer_pubkey", "signer_id", "sig", "parent_receipt_hash"] + + +def group_payload(g): + """jwp-receipts §2.3's legacy signing payload.""" + p = b"moq-jwp:group-sign\x01" + struct.pack(">QQ", g["track_alias"], g["group_id"]) + bytes.fromhex(g["root_hash"]) + p += struct.pack(">QQ", g["leaf_count"], g["joules_total"]) + for s in (g["group_start"], g["group_end"]): + s = s.encode() + p += struct.pack(">I", len(s)) + s + sid = g["signer_id"].encode() + return p + bytes.fromhex(g["signer_pubkey"]) + struct.pack(">I", len(sid)) + sid + + +def link_hash(g): + return blake3(b"moq-jwp:receipt-link\x01", group_payload(g), bytes.fromhex(g["sig"])) + + +def group_ok(g): + """§3 steps 1 and 2 for a group receipt of the legacy form, which this profile uses.""" + if set(g) != set(GROUP_FIELDS) or g["kind"] != "group": + return False + return ED.verify(bytes.fromhex(g["signer_pubkey"]), group_payload(g), bytes.fromhex(g["sig"])) is None + + +def leaf(content): + return blake3(b"moq-jwp:merkle-leaf\x01", content) + + +def node(left, right): + return blake3(b"moq-jwp:merkle-node\x01", left, right) + + +def merkle_root(leaves): + if not leaves: + return bytes(32) + level = list(leaves) + while len(level) > 1: + if len(level) % 2: + level.append(level[-1]) + level = [node(level[i], level[i + 1]) for i in range(0, len(level), 2)] + return level[0] + + +def object_accepts(o, g, chunk): + """jwp-receipts §3 steps 1 to 5: the group verifies, the chunk hashes to the content hash, the + object stands at its own position, and its leaf walks to the root.""" + if not group_ok(g) or o.get("kind") != "object" or "leaf_version" in o: + return False + if bytes.fromhex(o["content_hash"]) != blake3(b"moq-jwp:object\x01", chunk): + return False + p = o["merkle_proof"] + n = g["leaf_count"] + depth = 0 if n <= 1 else (n - 1).bit_length() + if (o["track_alias"], o["group_id"]) != (g["track_alias"], g["group_id"]) or o["object_id"] != p["leaf_index"]: + return False + if not (p["leaf_index"] < n and len(p["siblings"]) == depth and p["direction_bits"] == p["leaf_index"]): + return False + cur = leaf(bytes.fromhex(o["content_hash"])) + for i, s in enumerate(p["siblings"]): + s = bytes.fromhex(s) + cur = node(s, cur) if (p["direction_bits"] >> i) & 1 else node(cur, s) + return cur == bytes.fromhex(g["root_hash"]) + + +# ───────────────────────────── the assertions ───────────────────────────── + + +def hexs(b): + return b.hex() + + +def uuid_text(b): + h = b.hex() + return "%s-%s-%s-%s-%s" % (h[:8], h[8:12], h[12:16], h[16:20], h[20:]) + + +def uuid_v8(b16): + b = bytearray(b16) + b[6] = (b[6] & 0x0F) | 0x80 + b[8] = (b[8] & 0x3F) | 0x80 + return uuid_text(bytes(b)) + + +def manifest_uuid(s): + if not isinstance(s, str) or not s.startswith("urn:c2pa:"): + return None + u = s[len("urn:c2pa:"):] + if not re.fullmatch(r"[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}", u): + return None + return u + + +def track_kind(track, room): + """The media kind of `----` (live-rooms §6.1) whose room is `room`.""" + if not isinstance(track, str) or "--" not in track: + return None + ns, layer = track.split("--", 1) + if not re.fullmatch(r"0|[1-9][0-9]*", layer): + return None + f = ns.split("-") + if len(f) != 3 or f[0] != room.hex(): + return None + if not (re.fullmatch(r"[0-9a-f]{64}", f[1]) or re.fullmatch(r"_[a-z0-9_]+", f[1])): + return None + return f[2] if f[2] in ("audio", "video", "screen") else None + + +def read_origin(m): + o = m.get("origin") + if not uint(o): + return None + model = m.get("model") + if "model" in m and not b32(model): + return None + if o == 1 and model is None: + return (1, None) + if o in (2, 3) and model is not None: + return (o, model) + return None + + +def read_label(m): + if is_map(m, ["inputs"]): + v = m["inputs"] + if not isinstance(v, list) or not v: + return None + out = [] + for i in v: + if not is_map(i, ["streamId", "track", "origin"], ["model"]): + return None + om = read_origin(i) + if om is None or not isinstance(i["streamId"], str) or not isinstance(i["track"], str): + return None + out.append(dict(streamId=i["streamId"], track=i["track"], origin=om[0], model=om[1])) + return ("inputs", out) + if not is_map(m, ["origin"], ["model"]): + return None + om = read_origin(m) + return None if om is None else ("own", om) + + +def read_stream(m): + """§5.1's stream assertion.""" + if not is_map(m, ["room", "track", "trackAlias", "epoch", "firstGroup", "label"]): + return None + if not (b32(m["room"]) and isinstance(m["track"], str) and uint(m["trackAlias"]) and uint(m["epoch"]) + and uint(m["firstGroup"])): + return None + label = read_label(m["label"]) + if label is None or m["trackAlias"] >= 1 << 53 or track_kind(m["track"], m["room"]) is None: + return None + if label[0] == "inputs" and any(track_kind(i["track"], m["room"]) is None for i in label[1]): + return None + return dict(m, label=label) + + +def stream_id_of(sa): + """§3.4.""" + t = sa["track"].encode() + h = hashlib.sha256(STREAM_DOMAIN + sa["room"] + struct.pack(">I", len(t)) + t + struct.pack(">Q", sa["epoch"])).digest() + return uuid_v8(h[:16]) + + +def read_take(m, init): + """§5.3's take assertion: epoch and no commit in the initialization manifest, commit and no epoch later.""" + need = ["take", "room", "device", "epoch"] if init else ["take", "room", "device", "commit"] + if not is_map(m, need): + return None + if not all(b32(m[k]) for k in ("take", "room", "device")): + return None + if init and not uint(m["epoch"]): + return None + if not init and not b32(m["commit"]): + return None + return m + + +def disclosure(kind, ident): + """§9.1 and §9.2: the IPTC term and the AI disclosures (model, fully autonomous).""" + if kind == "take": + return "digitalCapture", [] + + def models(pairs): + out = [] + for o, m in pairs: + if m is None: + continue + for x in out: + if x[0] == m: + x[1] = x[1] or o == 2 + break + else: + out.append([m, o == 2]) + return [tuple(x) for x in out] + + label = ident["label"] + if label[0] == "own": + o, m = label[1] + k = track_kind(ident["track"], ident["room"]) + term = ({1: "screenCapture" if k == "screen" else "digitalCapture", 2: "trainedAlgorithmicMedia"} + .get(o, "compositeWithTrainedAlgorithmicMedia")) + return term, models([(o, m)]) + inputs = label[1] + if all(i["origin"] == 1 and track_kind(i["track"], ident["room"]) in ("audio", "video") for i in inputs): + term = "compositeCapture" + elif any(i["origin"] != 1 for i in inputs): + term = "compositeSynthetic" + else: + term = "composite" + return term, models([(i["origin"], i["model"]) for i in inputs]) + + +def ai_label(i): + return L_AI if i == 0 else "%s__%d" % (L_AI, i) + + +def read_actions(m): + if not is_map(m, ["actions"]) or not isinstance(m["actions"], list) or len(m["actions"]) != 1: + return None + a = m["actions"][0] + if not is_map(a, ["action", "digitalSourceType"]) or a["action"] != "c2pa.created": + return None + d = a["digitalSourceType"] + if not isinstance(d, str) or not d.startswith(DST_PREFIX): + return None + return d[len(DST_PREFIX):] + + +def read_ai(m): + if not is_map(m, ["modelType", "modelIdentifier", "contentProfile"]) or m["modelType"] != "c2pa.types.model": + return None + cp = m["contentProfile"] + if not is_map(cp, ["humanOversightLevel"]): + return None + if not isinstance(m["modelIdentifier"], str) or not isinstance(cp["humanOversightLevel"], str): + return None + return (m["modelIdentifier"], cp["humanOversightLevel"]) + + +def read_hash_item(m, segment): + keys = ["exclusions", "alg", "hash"] + (["sequenceNumber"] if segment else []) + if not is_map(m, keys) or m["alg"] != "sha256" or m["exclusions"] != PROFILE_EXCLUSIONS or not b32(m["hash"]): + return None + if segment and not uint(m["sequenceNumber"]): + return None + return m["hash"], (m["sequenceNumber"] if segment else None) + + +def read_live(m): + if not is_map(m, ["sequenceNumber", "streamId"], ["continuityMethod", "previousManifestId"]): + return None + if not uint(m["sequenceNumber"]) or not isinstance(m["streamId"], str): + return None + for k in ("continuityMethod", "previousManifestId"): + if k in m and not isinstance(m[k], str): + return None + return m + + +def read_group(m): + if not is_map(m, ["groupId", "receipt", "root", "objects"]): + return None + if not (uint(m["groupId"]) and b32(m["receipt"]) and b32(m["root"]) and uint(m["objects"])): + return None + return m + + +def shape_of(labels, init, kind): + """§11.2 step 7: which list a manifest is held to.""" + if len(labels) < 3 or labels[0] != L_HASH or labels[1] != L_ACTIONS: + return None + rest = labels[2:] + if init: + shape = "init" + if rest[0] == L_KEYS: + rest = rest[1:] + elif rest[0] == L_LIVE: + shape, rest = "per-segment", rest[1:] + elif rest[0] == L_KEYS: + shape, rest = "rotation", rest[1:] + else: + return None + if not rest: + return None + ai, last = rest[:-1], rest[-1] + if any(l != ai_label(i) for i, l in enumerate(ai)): + return None + want = L_GROUP if (shape == "per-segment" and kind == "track") else (L_TAKE if kind == "take" else L_STREAM) + return shape if last == want else None + + +# ───────────────────────────── §11.2: a manifest ───────────────────────────── + + +def verify_claim(store, anchors, t_us): + """§11.2 steps 1 (the claim and its signature's form) to 6. The labels in the claim's order + and the end-entity certificate; raises the reason as a string.""" + mid = store["id"] + u = manifest_uuid(mid) + if u is None: + raise ValueError("manifest_malformed") + try: + claim = cbor_read(store["claim"]) + except CborError: + raise ValueError("manifest_malformed") + if (not is_map(claim, ["instanceID", "claim_generator_info", "signature", "created_assertions", "alg"]) + or claim["instanceID"] != "xmp:iid:" + u or claim["signature"] != "self#jumbf=/c2pa/%s/c2pa.signature" % mid + or claim["alg"] != "sha256"): + raise ValueError("manifest_malformed") + cgi = claim["claim_generator_info"] + if not is_map(cgi, ["name", "specVersion"]) or not isinstance(cgi["name"], str) or not isinstance(cgi["specVersion"], str): + raise ValueError("manifest_malformed") + refs = claim["created_assertions"] + if not isinstance(refs, list): + raise ValueError("manifest_malformed") + try: + s = read_sign1(cbor_read(store["signature"])) + except CborError: + s = None + if s is None or s["payload"] is not None or s["unprotected"] != {} or len(s["protected"]) != 2: + raise ValueError("manifest_malformed") + x5 = s["protected"].get(33) + if isinstance(x5, list) and x5 and all(isinstance(c, bytes) for c in x5): + chain_der = x5 + elif isinstance(x5, bytes): + chain_der = [x5] + else: + raise ValueError("manifest_malformed") + chain = [read_cert(d) for d in chain_der] + if any(c is None for c in chain) or not profile_ok(chain[0], False) or not all(profile_ok(c, True) for c in chain[1:]): + raise ValueError("credential_invalid") + if any(not issued_by(chain[i], chain[i + 1]) for i in range(len(chain) - 1)): + raise ValueError("credential_untrusted") + anchor = next((a for a in anchors if a is not None and issued_by(chain[-1], a)), None) + if anchor is None: + raise ValueError("credential_untrusted") + if not cose_verifies(s, chain[0]["key"], store["claim"]): + raise ValueError("claim_signature_mismatch") + if not all(c["nb"] * 1000000 <= t_us <= c["na"] * 1000000 for c in chain + [anchor]): + raise ValueError("credential_expired") + prefix = "self#jumbf=/c2pa/%s/c2pa.assertions/" % mid + labels = [] + for i, rf in enumerate(refs): + if not is_map(rf, ["url", "hash"]) or not isinstance(rf["url"], str) or not rf["url"].startswith(prefix) or not b32(rf["hash"]): + raise ValueError("manifest_malformed") + label = rf["url"][len(prefix):] + held = [a for a in store["assertions"] if a["label"] == label] + if len(held) != 1 or i >= len(store["assertions"]) or store["assertions"][i]["label"] != label: + raise ValueError("manifest_malformed") + if held[0]["hash"] != rf["hash"]: + raise ValueError("assertion_hash_mismatch") + labels.append(label) + if len(labels) != len(store["assertions"]): + raise ValueError("manifest_malformed") + return labels, chain_der[0] + + +def verify_manifest(asset, top, store_bytes, init, kind, anchors, t_us): + """§11.2 for a standard manifest. Raises the reason as a string; returns the manifest's + identifier, shape, assertions, end-entity cert and store.""" + store = read_store(store_bytes or b"") + if store is None or store["type"] != T_MANIFEST or any(a["form"] != "cbor" for a in store["assertions"]): + raise ValueError("manifest_malformed") + labels, ee = verify_claim(store, anchors, t_us) + shape = shape_of(labels, init, kind) + if shape is None: + raise ValueError("manifest_malformed") + items = [] + for a in store["assertions"]: + try: + items.append((a["label"], cbor_read(a["cbor"]))) + except CborError: + raise ValueError("manifest_malformed") + hb = read_hash_item(items[0][1], not init) + if hb is None or read_actions(items[1][1]) is None: + raise ValueError("manifest_malformed") + for label, item in items[2:]: + if label == L_LIVE: + ok = read_live(item) is not None + elif label == L_KEYS: + ok = is_map(item, ["keys"]) and isinstance(item["keys"], list) and bool(item["keys"]) + elif label == L_GROUP: + ok = read_group(item) is not None + elif label in (L_STREAM, L_TAKE): + ok = isinstance(item, dict) + else: + ok = read_ai(item) is not None + if not ok: + raise ValueError("manifest_malformed") + if bmff_hash(asset, top) != hb[0]: + raise ValueError("hard_binding_mismatch") + return dict(id=store["id"], shape=shape, items=dict(items), order=items, ee=ee, seq=hb[1], store=store) + + +def derived_id(stream_id, role, seq): + """§4.2: the identifier of the manifest of `role` for sequence number `seq`.""" + sid = stream_id.encode() + h = bytearray(hashlib.sha256(MANIFEST_DOMAIN + struct.pack(">I", len(sid)) + sid + bytes([role]) + + struct.pack(">Q", seq)).digest()[:16]) + h[6] = (h[6] & 0x0F) | 0x40 + h[8] = (h[8] & 0x3F) | 0x80 + return "urn:c2pa:" + uuid_text(bytes(h)) + + +# ───────────────────────────── the repository receipt (§4.7) ───────────────────────────── + + +def log_leaf(entry): + return hashlib.sha256(b"\x00" + entry).digest() + + +def log_node(left, right): + return hashlib.sha256(b"\x01" + left + right).digest() + + +def log_root(entry, index, size, path): + """RFC 9162 §2.1.3.2.""" + if index >= size: + return None + fn, sn, r = index, size - 1, log_leaf(entry) + for p in path: + if sn == 0: + return None + if fn & 1 or fn == sn: + r = log_node(p, r) + while not fn & 1 and fn != 0: + fn >>= 1 + sn >>= 1 + else: + r = log_node(r, p) + fn >>= 1 + sn >>= 1 + return r if sn == 0 else None + + +def verify_inclusion_receipt(receipt, entry, key): + """RFC 9942 §5.2.1: protected {1: -8, 395: 1}; unprotected {396: {-1: [proof]}}; detached + payload; the proof leads from the entry to a root under which the key's signature + verifies. The tree size and the leaf index, or None.""" + try: + s = read_sign1(cbor_read(receipt)) + except CborError: + return None + if s is None or s["protected"] != {1: -8, 395: 1} or s["payload"] is not None or set(s["unprotected"]) != {396}: + return None + vdp = s["unprotected"][396] + if not isinstance(vdp, dict) or set(vdp) != {-1} or not isinstance(vdp[-1], list) or len(vdp[-1]) != 1: + return None + if not isinstance(vdp[-1][0], bytes): + return None + try: + c = cbor_read(vdp[-1][0]) + except CborError: + return None + if not isinstance(c, list) or len(c) != 3 or not uint(c[0]) or not uint(c[1]) or not isinstance(c[2], list) or not c[2]: + return None + if not all(b32(h) for h in c[2]): + return None + root = log_root(entry, c[1], c[0], c[2]) + if root is None or not cose_verifies(s, key, root): + return None + return c[0], c[1] + + +def verify_update(store_bytes, original, stream_id, anchors, repositories, t_us): + """§11.3 step 6: the update manifest over `original` and its repository receipt.""" + store = read_store(store_bytes or b"") + if store is None or store["type"] != T_UPDATE: + raise ValueError("update_manifest_invalid") + try: + labels, _ = verify_claim(store, anchors, t_us) + except ValueError as e: + raise ValueError("update_manifest_invalid" if str(e) == "manifest_malformed" else str(e)) + if store["id"] != derived_id(stream_id, ROLE_UPDATE, 0): + raise ValueError("manifest_id_mismatch") + if labels != [L_INGREDIENT, L_REPOSITORY] or [a["form"] for a in store["assertions"]] != ["cbor", "json"]: + raise ValueError("update_manifest_invalid") + sig = "self#jumbf=/c2pa/%s/c2pa.signature" % original["id"] + labels0 = [a["label"] for a in original["assertions"]] + success = [{"code": c, "url": sig} for c in ("claimSignature.validated", "signingCredential.trusted", "claimSignature.insideValidity")] + success += [{"code": "assertion.hashedURI.match", "url": "self#jumbf=/c2pa/%s/c2pa.assertions/%s" % (original["id"], l)} for l in labels0] + success += [{"code": "assertion.bmffHash.match", "url": "self#jumbf=/c2pa/%s/c2pa.assertions/%s" % (original["id"], labels0[0])}] + want = { + "relationship": "parentOf", + "validationResults": {"activeManifest": {"success": success, "informational": [], "failure": []}, + "ingredientDeltas": [], "specVersion": "2.4.0"}, + "activeManifest": {"url": "self#jumbf=/c2pa/" + original["id"], "hash": original["manifest_hash"]}, + "claimSignature": {"url": "self#jumbf=/c2pa/%s/c2pa.signature" % original["id"], "hash": original["signature_hash"]}, + } + try: + if cbor_read(store["assertions"][0]["cbor"]) != want: + raise ValueError("update_manifest_invalid") + v = json.loads(store["assertions"][1]["cbor"]) + except (CborError, ValueError): + raise ValueError("update_manifest_invalid") + + def jkeys(o, required, optional=()): + return isinstance(o, dict) and all(k in required or k in optional for k in o) and all(k in o for k in required) + + ok = (jkeys(v, ["@context", "@type", "repository", "anchor"]) and isinstance(v["@context"], (dict, str)) + and v["@type"] == "org.c2pa.repository-receipt" and jkeys(v["repository"], ["uri", "manifestId"]) + and jkeys(v["anchor"], ["uri", "proof"], ["parameters"]) and jkeys(v["anchor"]["proof"], ["profile", "entry", "receipt"]) + and isinstance(v["anchor"]["uri"], str) and v["repository"]["manifestId"] == original["id"] + and v["anchor"]["proof"]["profile"] == "urn:ietf:rfc:9942" + and v["anchor"]["proof"]["entry"] == original["manifest_hash"].hex() + and isinstance(v["repository"]["uri"], str) and isinstance(v["anchor"]["proof"]["receipt"], str)) + if not ok: + raise ValueError("update_manifest_invalid") + r64 = v["anchor"]["proof"]["receipt"] + if not re.fullmatch(r"[A-Za-z0-9_-]*", r64) or len(r64) % 4 == 1: + raise ValueError("update_manifest_invalid") + receipt = base64.urlsafe_b64decode(r64 + "=" * (-len(r64) % 4)) + key = repositories.get(v["repository"]["uri"]) + if key is None: + raise ValueError("repository_receipt_invalid") + got = verify_inclusion_receipt(receipt, original["manifest_hash"], key) + if got is None: + raise ValueError("repository_receipt_invalid") + return {"repository": v["repository"]["uri"], "treeSize": got[0], "leafIndex": got[1]} + + +def check_label(m, kind, ident): + """§9.3.""" + term, models = disclosure(kind, ident) + if read_actions(m["items"][L_ACTIONS]) != term: + raise ValueError("source_type_mismatch") + held = [read_ai(i) for l, i in m["order"] if l.startswith(L_AI)] + want = [(MODEL_URI + mm.hex(), "fully_autonomous" if auto else "human_validated") for mm, auto in models] + if held != want: + raise ValueError("ai_disclosure_missing") + + +def rfc3339(s): + if not isinstance(s, str) or not re.fullmatch(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z", s): + return None + y, mo, d, h, mi, se = int(s[0:4]), int(s[5:7]), int(s[8:10]), int(s[11:13]), int(s[14:16]), int(s[17:19]) + if not (1 <= mo <= 12 and 1 <= d <= 31 and h <= 23 and mi <= 59 and se <= 59): + return None + try: + t = calendar.timegm((y, mo, d, h, mi, se, 0, 0, 0)) + except (OverflowError, ValueError): + return None + back = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(t)) + return t if back == s and t >= 0 else None + + +def read_keys(m, device): + """§11.3 step 5: the session keys a manifest delivers.""" + item = m["items"].get(L_KEYS) + if item is None: + return [] + out = [] + for k in item["keys"]: + if not is_map(k, ["key", "minSequenceNumber", "createdAt", "validityPeriod", "signerBinding"]): + raise ValueError("session_key_invalid") + x = read_okp(k["key"]) + ca = k["createdAt"] + created = rfc3339(ca.value) if isinstance(ca, Tag) and ca.tag == 0 else None + if x is None or not uint(k["minSequenceNumber"]) or created is None or not uint(k["validityPeriod"]): + raise ValueError("session_key_invalid") + b = read_sign1(k["signerBinding"]) + if (b is None or len(b["protected"]) != 1 or b["payload"] is not None or b["unprotected"] != {} + or not cose_verifies(b, x, m["ee"])): + raise ValueError("session_key_invalid") + if (device is not None and device != x) or any(h["x"] == x for h in out): + raise ValueError("session_key_invalid") + out.append(dict(x=x, min=k["minSequenceNumber"], frm=created * 1000000, + to=(created + k["validityPeriod"]) * 1000000)) + return out + + +# ───────────────────────────── §11: a stream ───────────────────────────── + + +def verify_stream(kind, init, segments, groups, objects, anchors, repositories, t_us): + """Returns §11.6's statement, or raises Refuse.""" + def refuse(reason, at): + raise Refuse(reason, at) + + top = boxes(init, 0, len(init)) + if top is None: + refuse("segment_malformed", "init") + if any(r[0] == b"mdat" for r in top): + refuse("init_invalid", "init") + if (len(top) not in (3, 4) or top[0][0] != b"ftyp" or not is_c2pa_uuid(init, top[1]) or top[2][0] != b"moov" + or (len(top) == 4 and not is_c2pa_uuid(init, top[3]))): + refuse("init_invalid", "init") + update = top[3] if len(top) == 4 else None + track = read_moov(init, top[2]) + if track is None: + refuse("init_invalid", "init") + store0 = manifest_store_bytes(init, top[1], b"original" if update else b"manifest") + if store0 is None: + refuse("manifest_malformed", "init") + try: + m0 = verify_manifest(init, top, store0, True, kind, anchors, t_us) + except ValueError as e: + refuse(str(e), "init") + if kind == "track": + ident = read_stream(m0["items"][L_STREAM]) + if ident is None: + refuse("stream_assertion_invalid", "init") + stream_id, epoch, first_group, device = stream_id_of(ident), ident["epoch"], ident["firstGroup"], None + else: + ident = read_take(m0["items"][L_TAKE], True) + if ident is None: + refuse("stream_assertion_invalid", "init") + stream_id, epoch, first_group, device = uuid_v8(ident["take"][:16]), ident["epoch"], 0, ident["device"] + if m0["id"] != derived_id(stream_id, ROLE_INIT, 0): + refuse("manifest_id_mismatch", "init") + registration = None + try: + check_label(m0, kind, ident) + held_keys = read_keys(m0, device) + if update: + ustore = manifest_store_bytes(init, update, b"update") + if ustore is None: + raise ValueError("update_manifest_invalid") + registration = verify_update(ustore, m0["store"], stream_id, anchors, repositories, t_us) + except ValueError as e: + refuse(str(e), "init") + + prev_seq = prev_group = None + prev_last = 0 + prev_manifest = active = m0["id"] + seen, out = [], [] + for k, (b, g) in enumerate(segments): + at = "segment %d" % k + top = boxes(b, 0, len(b)) + if top is None: + refuse("segment_malformed", at) + i = 0 + mbox = top[0] if top and is_c2pa_uuid(b, top[0]) else None + if mbox: + i += 1 + vbox = top[i] if len(top) > i and is_vsi(b, top[i]) else None + if vbox: + i += 1 + rest = top[i:] + if not rest or len(rest) % 2: + refuse("segment_malformed", at) + chunks, infos = [], [] + for j in range(0, len(rest), 2): + info = read_chunk(b, rest[j], rest[j + 1]) + if info is None: + refuse("segment_malformed", at) + infos.append(info) + chunks.append(b[rest[j][1]:rest[j + 1][2]]) + if i == 0: + refuse("provenance_missing", at) + if any(x["track_id"] != track["track_id"] for x in infos) or any( + infos[j + 1]["seq"] != infos[j]["seq"] + 1 for j in range(len(infos) - 1)): + refuse("segment_malformed", at) + seq, last, n = infos[0]["seq"], infos[-1]["seq"], len(chunks) + if (prev_seq is not None and seq <= prev_seq) or ( + kind == "track" and prev_group is not None and (g is None or g <= prev_group)): + refuse("sequence_not_increasing", at) + # Step 3. + per = None + if mbox: + try: + m = verify_manifest(b, top, manifest_store_bytes(b, mbox), False, kind, anchors, t_us) + except ValueError as e: + refuse(str(e), at) + role = ROLE_SEGMENT if m["shape"] == "per-segment" else ROLE_ROTATION + if m["id"] != derived_id(stream_id, role, seq): + refuse("manifest_id_mismatch", at) + if m["seq"] != seq: + refuse("sequence_mismatch", at) + if m["shape"] == "per-segment": + lv = m["items"][L_LIVE] + if lv["sequenceNumber"] != seq: + refuse("sequence_mismatch", at) + if lv["streamId"] != stream_id: + refuse("stream_id_mismatch", at) + if lv.get("continuityMethod") != "c2pa.manifestId" or "previousManifestId" not in lv: + refuse("continuity_invalid", at) + if lv["previousManifestId"] != prev_manifest: + refuse("continuity_broken", at) + if kind == "take": + t = read_take(m["items"][L_TAKE], False) + if t is None or (t["take"], t["room"], t["device"]) != (ident["take"], ident["room"], ident["device"]): + refuse("stream_assertion_invalid", at) + else: + if vbox is None: + refuse("provenance_missing", at) + if kind == "track": + same = read_stream(m["items"][L_STREAM]) == ident + else: + same = read_take(m["items"][L_TAKE], True) == ident + if not same: + refuse("stream_assertion_invalid", at) + try: + new = read_keys(m, device) + except ValueError as e: + refuse(str(e), at) + if any(h["min"] < seq or any(o["x"] == h["x"] for o in held_keys) for h in new): + refuse("session_key_invalid", at) + held_keys = held_keys + new + try: + check_label(m, kind, ident) + except ValueError as e: + refuse(str(e), at) + prev_manifest = active = m["id"] + if m["shape"] == "per-segment": + per = m + methods = {(True, True): "manifest+vsi", (True, False): "manifest"}.get((per is not None, vbox is not None), "vsi") + # Step 4. + signer = None + if vbox: + e = read_emsg(b, vbox) + dur = sum(x["duration"] for x in infos) + if (e is None or e["scheme"] != VSI_SCHEME or e["value"] != "fseg" or e["timescale"] != track["timescale"] + or e["ptd"] != 0 or e["duration"] != dur or e["id"] != seq): + refuse("segment_info_invalid", at) + try: + cs = read_sign1(cbor_read(e["data"])) + except CborError: + cs = None + if (cs is None or len(cs["protected"]) != 1 or len(cs["unprotected"]) != 1 + or not isinstance(cs["unprotected"].get(4), bytes) or cs["payload"] is None): + refuse("segment_info_invalid", at) + try: + info = cbor_read(cs["payload"]) + except CborError: + refuse("segment_info_invalid", at) + if not is_map(info, ["sequenceNumber", "bmffHash", "manifestId"]): + refuse("segment_info_invalid", at) + vh = read_hash_item(info["bmffHash"], True) + if vh is None or not uint(info["sequenceNumber"]) or not isinstance(info["manifestId"], str): + refuse("segment_info_invalid", at) + key = next((h for h in held_keys if h["x"] == cs["unprotected"][4]), None) + if key is None: + refuse("session_key_unknown", at) + if not cose_verifies(cs, key["x"], cs["payload"]): + refuse("segment_info_signature_failed", at) + pres = epoch + infos[0]["bmdt"] * 1000000 // track["timescale"] + superseded = any(key["min"] < o["min"] <= seq for o in held_keys) + if key["min"] > seq or superseded or not key["frm"] <= pres < key["to"]: + refuse("session_key_out_of_range", at) + if info["sequenceNumber"] != seq or vh[1] != seq: + refuse("sequence_mismatch", at) + if info["manifestId"] != active: + refuse("segment_info_invalid", at) + if bmff_hash(b, top) != vh[0]: + refuse("segment_hash_mismatch", at) + signer = key["x"] + # Steps 5 and 6. + if kind == "track": + if g is None: + refuse("segment_malformed", at) + expect = 1 + for h in range(first_group, g): + rh = [r for r in groups if r["group_id"] == h] + if not rh: + refuse("receipt_missing", "group %d" % h) + expect += rh[0]["leaf_count"] + held = [r for r in groups if r["group_id"] == g] + if len(held) != 1: + refuse("receipt_missing" if not held else "receipt_invalid", "group %d" % g) + rg = held[0] + if seq != expect: + refuse("sequence_mismatch", at) + atg = "group %d" % g + prev = next((r for r in groups if r["group_id"] == g - 1), None) if g > first_group else None + parent = rg["parent_receipt_hash"] + if prev is not None and prev["signer_pubkey"] == rg["signer_pubkey"]: + parent_ok = parent == link_hash(prev).hex() + else: + parent_ok = parent is None + if g < first_group or not group_ok(rg) or rg["track_alias"] != ident["trackAlias"] or not parent_ok: + refuse("receipt_invalid", atg) + leaves = [leaf(blake3(b"moq-jwp:object\x01", c)) for c in chunks] + named = True + if per is not None: + ga = per["items"][L_GROUP] + named = (ga["groupId"] == g and ga["receipt"] == link_hash(rg) and ga["root"].hex() == rg["root_hash"] + and ga["objects"] == rg["leaf_count"]) + if rg["leaf_count"] != n or rg["root_hash"] != merkle_root(leaves).hex() or not named: + refuse("receipt_mismatch", atg) + if signer is not None and signer.hex() != rg["signer_pubkey"]: + refuse("receipt_signer_mismatch", atg) + objs = [o for o in objects if o["group_id"] == g] + if len(objs) != n: + refuse("object_receipt_invalid", atg) + for j, c in enumerate(chunks): + mine = [o for o in objs if o["object_id"] == j] + if len(mine) != 1 or not object_accepts(mine[0], rg, c): + refuse("object_receipt_invalid", atg) + seen.append(g) + prev_group = g + index, digest, dkey, ikey = g, rg["root_hash"], "root", "group" + else: + if seq != prev_last + 1: + refuse("sequence_mismatch", at) + index = k + digest = hashlib.sha256((init + b) if k == 0 else b).hexdigest() + dkey, ikey = "chunkHash", "chunk" + prev_seq, prev_last = seq, last + out.append({"sequenceNumber": seq, ikey: index, "methods": methods, "manifestId": active, "chunks": n, dkey: digest}) + if kind == "track": + gs = [r["group_id"] for r in groups if r["group_id"] >= first_group] + if gs: + for h in range(first_group, max(gs) + 1): + if h not in seen: + refuse("segment_missing", "group %d" % h) + return {"outcome": "accept", "streamId": stream_id, "sourceType": DST_PREFIX + disclosure(kind, ident)[0], + "registration": registration, "segments": out} + + +# ───────────────────────────── the corpus ───────────────────────────── + + +def run(v, doc, files): + anchors = [read_cert(files[a]) for a in doc["anchors"]] + t_us = rfc3339(doc["time"]) * 1000000 + segs = [(files[s["file"]], s.get("group")) for s in v["segments"]] + groups, objects = [], [] + if v["receipts"] is not None: + r = json.loads(files[v["receipts"]]) + groups, objects = r["groups"], r["objects"] + repositories = {r["uri"]: bytes.fromhex(r["key"]) for r in doc["repositories"]} + try: + return verify_stream(v["kind"], files[v["init"]], segs, groups, objects, anchors, repositories, t_us) + except Refuse as e: + c2pa, live = REASONS[e.reason] + return {"outcome": "refuse", "reason": e.reason, "at": e.at, "c2pa": c2pa, "livevideo": live} + + +def load(root): + files = {} + for dirpath, _, names in os.walk(root): + for n in names: + p = os.path.join(dirpath, n) + rel = os.path.relpath(p, root).replace(os.sep, "/") + if rel != "README.md": + files[rel] = open(p, "rb").read() + return files + + +def flip_last_mdat(b): + top = boxes(b, 0, len(b)) + m = [r for r in top if r[0] == b"mdat"][-1] + return b[:m[2] - 1] + bytes([b[m[2] - 1] ^ 1]) + b[m[2]:] + + +def mutations(doc, files): + """One defect each, on copies of the good vectors; every copy must be refused.""" + out = [] + for v in doc["vectors"]: + if v["expect"]["outcome"] != "accept": + continue + mid = len(v["segments"]) // 2 + + def segment_bytes(fs, vv, j, f): + path = "mutated/" + vv["segments"][j]["file"] + fs[path] = f(fs[vv["segments"][j]["file"]]) + vv["segments"][j]["file"] = path + + def m_mdat(vv, fs): + segment_bytes(fs, vv, mid, flip_last_mdat) + + def m_drop(vv, fs): + del vv["segments"][mid] + + def m_swap(vv, fs): + vv["segments"][0], vv["segments"][1] = vv["segments"][1], vv["segments"][0] + + def m_moov(vv, fs): + b = fs[vv["init"]] + top = boxes(b, 0, len(b)) + at = top[2][2] - 1 + fs["mutated/" + vv["init"]] = b[:at] + bytes([b[at] ^ 1]) + b[at + 1:] + vv["init"] = "mutated/" + vv["init"] + + def m_claim_sig(vv, fs): + b = fs[vv["init"]] + top = boxes(b, 0, len(b)) + sig = read_store(manifest_store_bytes(b, top[1]) or manifest_store_bytes(b, top[1], b"original"))["signature"][-64:] + at = b.index(sig) + 10 + fs["mutated/" + vv["init"]] = b[:at] + bytes([b[at] ^ 1]) + b[at + 1:] + vv["init"] = "mutated/" + vv["init"] + + def m_last_byte_of_c2pa_box(vv, fs): + def f(b): + top = boxes(b, 0, len(b)) + r = top[0] + return b[:r[2] - 1] + bytes([b[r[2] - 1] ^ 1]) + b[r[2]:] + segment_bytes(fs, vv, mid, f) + + def m_receipt(vv, fs): + r = json.loads(fs[vv["receipts"]]) + g = r["groups"][mid] + g["sig"] = ("0" if g["sig"][0] != "0" else "1") + g["sig"][1:] + fs["mutated/receipts.json"] = json.dumps(r).encode() + vv["receipts"] = "mutated/receipts.json" + + def m_object(vv, fs): + r = json.loads(fs[vv["receipts"]]) + o = [x for x in r["objects"] if x["group_id"] == vv["segments"][mid]["group"]][0] + o["content_hash"] = ("0" if o["content_hash"][0] != "0" else "1") + o["content_hash"][1:] + fs["mutated/receipts.json"] = json.dumps(r).encode() + vv["receipts"] = "mutated/receipts.json" + + def m_time(vv, fs, d): + d["time"] = "2027-06-01T00:00:00Z" + + ms = [("a byte of the middle segment's media", m_mdat), ("the middle segment dropped", m_drop), + ("the first two segments swapped", m_swap), ("a byte of the initialization segment's moov", m_moov), + ("a byte of the initialization manifest's claim signature", m_claim_sig), + ("the last byte of the middle segment's first C2PA box", m_last_byte_of_c2pa_box)] + def m_update(vv, fs): + b = fs[vv["init"]] + at = len(b) - 1 + fs["mutated/" + vv["init"]] = b[:at] + bytes([b[at] ^ 1]) + vv["init"] = "mutated/" + vv["init"] + + if len(boxes(files[v["init"]], 0, len(files[v["init"]]))) == 4: + ms.append(("the last byte of the update manifest's store", m_update)) + if v["kind"] == "track": + ms += [("a byte of the middle group's receipt signature", m_receipt), + ("a byte of an object receipt's content hash", m_object)] + for label, f in ms: + vv, fs = copy.deepcopy(v), dict(files) + f(vv, fs) + out.append(("%s: %s" % (v["name"], label), vv, doc, fs)) + vv, fs, dd = copy.deepcopy(v), dict(files), copy.deepcopy(doc) + m_time(vv, fs, dd) + out.append(("%s: verified after the end-entity certificate expired" % v["name"], vv, dd, fs)) + return out + + +def main(root): + files = load(root) + doc = json.loads(files["vectors.json"]) + fails, reasons = [], set() + used = {"vectors.json"} | set(doc["anchors"]) + for v in doc["vectors"]: + used |= {v["init"]} | {s["file"] for s in v["segments"]} | ({v["receipts"]} if v["receipts"] else set()) + try: + got = run(v, doc, files) + except Exception as e: # a crash is a failure + got = {"error": repr(e)} + if got.get("outcome") == "refuse": + reasons.add(got["reason"]) + if got != v["expect"]: + fails.append("%s: expected %s, got %s" % (v["name"], json.dumps(v["expect"]), json.dumps(got))) + else: + print("PASS %s: %s%s" % (v["name"], got["outcome"], (" " + got["reason"] + " at " + got["at"]) if got["outcome"] == "refuse" else "")) + for r in REASONS: + if r not in reasons: + fails.append("no vector is refused %s" % r) + extra = sorted(set(files) - used) + if extra: + fails.append("files no vector names: %s" % extra) + caught = 0 + for label, vv, dd, fs in mutations(doc, files): + try: + got = run(vv, dd, fs) + except Exception as e: # a crash on a mutated copy is a failure to read it, which is a refusal + got = {"outcome": "refuse", "reason": "crash: %r" % e} + if got.get("outcome") == "accept": + fails.append("mutation not caught: " + label) + else: + caught += 1 + print("%d mutated copies, %d refused" % (caught + sum(1 for f in fails if f.startswith("mutation")), caught)) + for f in fails: + print("FAIL " + f) + print("PASS" if not fails else "FAIL", "every vector of", root) + return 1 if fails else 0 + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("usage: c2pa_live_verify.py ", file=sys.stderr) + sys.exit(2) + sys.exit(main(sys.argv[1])) diff --git a/wai/registry/capabilities.json b/wai/registry/capabilities.json index f3a940393..0e28a7a91 100644 --- a/wai/registry/capabilities.json +++ b/wai/registry/capabilities.json @@ -686,6 +686,20 @@ "edition": null, "registered": "2026-10-19" }, + { + "capability": "wai.live.provenance", + "kind": "record", + "status": "registered", + "media": null, + "role": null, + "tier": null, + "licence": null, + "replicate": null, + "sets": [], + "spec_section": "§5 Interactive, temporal & computed content", + "edition": null, + "registered": "2026-10-21" + }, { "capability": "wai.live.recording", "kind": "record", @@ -2277,6 +2291,13 @@ "section": "§3", "status": "defined" }, + { + "name": "wai-c2pa", + "kind": "catalog", + "document": "extensions/live-provenance.md", + "section": "§10.2", + "status": "defined" + }, { "name": "wai-key-status", "kind": "document-kind", @@ -2347,6 +2368,13 @@ "section": "Appendix B", "status": "provisional" }, + { + "name": "wai_c2pa_track", + "kind": "catalog", + "document": "extensions/live-provenance.md", + "section": "§10.2", + "status": "defined" + }, { "name": "wai_capabilities", "kind": "catalog", diff --git a/wai/vocab/v2.ttl b/wai/vocab/v2.ttl index 7dc643ce1..093c3babe 100644 --- a/wai/vocab/v2.ttl +++ b/wai/vocab/v2.ttl @@ -576,6 +576,13 @@ wai:wai.live.moderation a wai:Capability ; wai:specSection "§5 Interactive, temporal & computed content" ; wai:registeredOn "2026-10-19"^^xsd:date . +wai:wai.live.provenance a wai:Capability ; + rdfs:label "wai.live.provenance" ; + wai:kind wai:kind-record ; + wai:status wai:status-registered ; + wai:specSection "§5 Interactive, temporal & computed content" ; + wai:registeredOn "2026-10-21"^^xsd:date . + wai:wai.live.recording a wai:Capability ; rdfs:label "wai.live.recording" ; wai:kind wai:kind-record ; diff --git a/wai/wai-rs/Cargo.toml b/wai/wai-rs/Cargo.toml index 7c146371e..4eacec8be 100644 --- a/wai/wai-rs/Cargo.toml +++ b/wai/wai-rs/Cargo.toml @@ -264,6 +264,16 @@ live_watermark = ["live_room"] # wai.video.ssai's receipt: the decision recomputed, the asset list bound, the spend held to the # decider grant's ceiling. SHA-256, BLAKE3, Ed25519 and the key log only; no new dependency. live_ads = ["live_room", "video_ssai"] +# Live provenance (extensions/live-provenance): `wai.live.provenance` — the live video provenance +# of C2PA 2.4 §19 for a room's CMAF tracks and a device's CMAF take, written and verified with no +# C2PA library: ISO BMFF segments and the BMFF hash over whole-box exclusions, the JUMBF manifest +# store, COSE_Sign1 with EdDSA and the OKP key, the claim signer's X.509 chain (DER written and +# read here), per-segment manifests chained by manifest identifier, Verifiable Segment Info under +# rotating session keys, and each segment tied to the JWP group receipt and object receipts of the +# same chunks. CBOR through live_watermark's deterministic reader; the disclosure label and its +# IPTC and AI-disclosure mapping through timed_text. SHA-256, BLAKE3 and Ed25519 only; no new +# dependency. +live_provenance = ["live_watermark", "timed_text"] # Timed text and live captions (extensions/timed-text): `wai.text.timed` — the cue model, # live caption revisions (`caption` events of wai.live.events with the WTX1 annotation), # and their exact mappings onto WebVTT documents and segments, the IMSC1 Text Profile, @@ -728,6 +738,11 @@ name = "wai_live_ads_vectors" path = "src/bin/wai_live_ads_vectors.rs" required-features = ["live_ads"] +[[bin]] +name = "wai_live_provenance_vectors" +path = "src/bin/wai_live_provenance_vectors.rs" +required-features = ["live_provenance"] + [[bin]] name = "wai_timed_text_vectors" path = "src/bin/wai_timed_text_vectors.rs" @@ -1428,6 +1443,11 @@ name = "live_ads" path = "tests/live_ads.rs" required-features = ["live_ads"] +[[test]] +name = "live_provenance" +path = "tests/live_provenance.rs" +required-features = ["live_provenance"] + [[test]] name = "timed_text" path = "tests/timed_text.rs" diff --git a/wai/wai-rs/src/bin/wai_live_provenance_vectors.rs b/wai/wai-rs/src/bin/wai_live_provenance_vectors.rs new file mode 100644 index 000000000..cbe80860b --- /dev/null +++ b/wai/wai-rs/src/bin/wai_live_provenance_vectors.rs @@ -0,0 +1,93 @@ +//! Conformance corpus for live provenance (`live-conformance/c2pa-live/`, +//! extensions/live-provenance.md §13). +//! +//! wai_live_provenance_vectors gen write vectors.json, the trust anchor and +//! every vector's segments and receipts, +//! from fixed seeds +//! wai_live_provenance_vectors verify every file regenerates byte for byte, no +//! other file but README.md is there, and +//! every vector gets the outcome it states +//! +//! The media are the AV1 floor of the packaging corpus (`pack-conformance/packs/video-base-01`), +//! read from the `wai` directory two levels above the corpus directory. The generator and this +//! verifier are one implementation; the corpus's second implementation is +//! `live-conformance/c2pa_live_verify.py`, written from the extension's text and sharing no +//! code with this binary. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use wai::live_provenance::corpus::{self, Media}; + +fn media(dir: &Path) -> Media { + // The corpus directory need not exist yet, so its parents are taken from the path. + let wai = dir.parent().and_then(Path::parent).map_or_else(|| dir.join("../.."), Path::to_path_buf); + match Media::load(&wai) { + Some(m) => m, + None => { + eprintln!("the floor under {} does not read", wai.join(corpus::FLOOR_DIR).display()); + std::process::exit(1); + } + } +} + +fn main() { + let a: Vec = std::env::args().collect(); + match (a.get(1).map(String::as_str), a.get(2)) { + (Some("gen"), Some(d)) => { + let dir = PathBuf::from(d); + let m = media(&dir); + for (name, body) in corpus::generate(&m) { + let p = dir.join(&name); + if let Some(parent) = p.parent() + && let Err(e) = std::fs::create_dir_all(parent) + { + eprintln!("create {}: {e}", parent.display()); + std::process::exit(1); + } + if let Err(e) = std::fs::write(&p, body) { + eprintln!("write {}: {e}", p.display()); + std::process::exit(1); + } + } + println!("wrote {d}"); + } + (Some("verify"), Some(d)) => std::process::exit(verify(Path::new(d))), + _ => { + eprintln!("usage: wai_live_provenance_vectors gen|verify "); + std::process::exit(2); + } + } +} + +fn read_tree(root: &Path, dir: &Path, out: &mut BTreeMap>) -> std::io::Result<()> { + for e in std::fs::read_dir(dir)? { + let e = e?; + let p = e.path(); + if p.is_dir() { + read_tree(root, &p, out)?; + } else { + let rel = p.strip_prefix(root).expect("under the root").to_string_lossy().replace('\\', "/"); + if rel != "README.md" { + out.insert(rel, std::fs::read(&p)?); + } + } + } + Ok(()) +} + +fn verify(dir: &Path) -> i32 { + let mut files = BTreeMap::new(); + if let Err(e) = read_tree(dir, dir, &mut files) { + eprintln!("read {}: {e}", dir.display()); + return 1; + } + let generated: BTreeMap> = corpus::generate(&media(dir)).into_iter().collect(); + let same = files == generated; + println!("{} every file regenerates byte for byte from its seeds, and no other file is there", if same { "PASS" } else { "FAIL" }); + let fails = corpus::verify_files(&files); + for f in &fails { + println!("FAIL {f}"); + } + println!("{} every vector gets the outcome the corpus states", if fails.is_empty() { "PASS" } else { "FAIL" }); + (!same || !fails.is_empty()) as i32 +} diff --git a/wai/wai-rs/src/codecs/registry.rs b/wai/wai-rs/src/codecs/registry.rs index 4cd0e0c41..3deae314e 100644 --- a/wai/wai-rs/src/codecs/registry.rs +++ b/wai/wai-rs/src/codecs/registry.rs @@ -221,6 +221,7 @@ pub const CAP_LIVE_MODERATION: &str = "wai.live.moderation"; pub const CAP_LIVE_E2EE: &str = "wai.live.e2ee"; pub const CAP_LIVE_WATERMARK: &str = "wai.live.watermark"; pub const CAP_LIVE_ADS: &str = "wai.live.ads"; +pub const CAP_LIVE_PROVENANCE: &str = "wai.live.provenance"; /// One row per string SPEC §5 registers that is a payload, companion or record: /// `(string, SPEC section, edition, registered)`. @@ -310,6 +311,7 @@ const ROWS: &[(&str, &str, Option<&str>, &str)] = &[ (CAP_LIVE_E2EE, S_INTERACTIVE, None, "2026-10-19"), (CAP_LIVE_WATERMARK, S_INTERACTIVE, None, "2026-10-20"), (CAP_LIVE_ADS, S_INTERACTIVE, None, "2026-10-20"), + (CAP_LIVE_PROVENANCE, S_INTERACTIVE, None, "2026-10-21"), (CAP_AUDIO_IAMF, S_IMMERSIVE, None, "2026-06-17"), (CAP_AUDIO_AC4, S_IMMERSIVE, None, "2026-06-17"), (CAP_AUDIO_IVAS, S_IMMERSIVE, None, "2026-06-17"), @@ -362,6 +364,7 @@ const RECORDS: &[&str] = &[ CAP_LIVE_METERING, CAP_LIVE_STEERING, CAP_TEXT_TIMED, CAP_LIVE_MODERATION, CAP_LIVE_E2EE, CAP_LIVE_WATERMARK, CAP_LIVE_ADS, + CAP_LIVE_PROVENANCE, ]; /// A named identifier SPEC uses that has the shape of a capability string but is @@ -518,6 +521,7 @@ const LIVE_MODERATION: &str = "extensions/live-moderation.md"; const CONFIDENTIALITY: &str = "extensions/confidentiality.md"; const FORENSIC_WATERMARK: &str = "extensions/forensic-watermark.md"; const LIVE_ADS: &str = "extensions/live-ads.md"; +const LIVE_PROVENANCE: &str = "extensions/live-provenance.md"; const fn wire(name: &'static str, kind: WireKind, document: &'static str, section: &'static str, status: WireStatus) -> WireIdentifier { @@ -528,8 +532,8 @@ const fn wire(name: &'static str, kind: WireKind, document: &'static str, sectio /// (http-delivery, moq-streaming-format), of the key discovery those bindings serve /// (jwp-receipts §7), of packaging, of live events, of live rooms, of capture binding, of /// live composition, of live recording, of timed text, of live metering, of energy steering, -/// of live moderation, of confidentiality, of forensic watermark sessions and of live ads, in -/// document order. The export lists them sorted by name and kind. +/// of live moderation, of confidentiality, of forensic watermark sessions, of live ads and of +/// live provenance, in document order. The export lists them sorted by name and kind. pub const WIRE_IDENTIFIERS: &[WireIdentifier] = &[ wire("WAI1", WireKind::Magic, "SPEC.md", "§2", WireStatus::Defined), wire("WAI2", WireKind::Magic, "SPEC.md", "§7.1", WireStatus::Defined), @@ -615,6 +619,8 @@ pub const WIRE_IDENTIFIERS: &[WireIdentifier] = &[ wire("WWD1", WireKind::Magic, FORENSIC_WATERMARK, "§8.1", WireStatus::Defined), wire("WAP1", WireKind::Magic, LIVE_ADS, "§3.1", WireStatus::Defined), wire("WAB1", WireKind::Magic, LIVE_ADS, "§4.2", WireStatus::Defined), + wire("wai_c2pa_track", WireKind::Catalog, LIVE_PROVENANCE, "§10.2", WireStatus::Defined), + wire("wai-c2pa", WireKind::Catalog, LIVE_PROVENANCE, "§10.2", WireStatus::Defined), ]; /// A string with the shape of a capability that the reference implementation uses diff --git a/wai/wai-rs/src/lib.rs b/wai/wai-rs/src/lib.rs index 189e83f18..b1e066f70 100644 --- a/wai/wai-rs/src/lib.rs +++ b/wai/wai-rs/src/lib.rs @@ -477,6 +477,15 @@ pub mod live_watermark; /// cbindgen:ignore #[cfg(feature = "live_ads")] pub mod live_ads; +// Live provenance (extensions/live-provenance.md): `wai.live.provenance` — the live video +// provenance of C2PA 2.4 §19 for a room's CMAF tracks and a device's CMAF take: per-segment +// manifests chained by manifest identifier, Verifiable Segment Info under rotating session keys, +// and each segment's hard binding tied to the JWP group receipt and object receipts of the same +// chunks. Rust-only, like the other live modules: its constants must not become macros in +// include/wai.h. +/// cbindgen:ignore +#[cfg(feature = "live_provenance")] +pub mod live_provenance; // Timed text and live captions (extensions/timed-text.md): `wai.text.timed` — the cue model // both WebVTT and the IMSC1 Text Profile express, live caption revisions as `caption` // events of `wai.live.events` with the WTX1 annotation, and the exact mappings onto WebVTT diff --git a/wai/wai-rs/src/live_provenance.rs b/wai/wai-rs/src/live_provenance.rs new file mode 100644 index 000000000..8ac1b7d67 --- /dev/null +++ b/wai/wai-rs/src/live_provenance.rs @@ -0,0 +1,1837 @@ +//! Live provenance — `wai.live.provenance` ([extensions/live-provenance.md]). +//! +//! The live video provenance of C2PA 2.4 §19 for a room's CMAF tracks and a device's CMAF +//! take, written and verified exactly as the extension profiles it, with no C2PA library: +//! +//! - [`bmff`]: the segment layout (§3.2), the C2PA Manifest Box and the VSI `emsg` box, the +//! chunks and the `moov`, and the BMFF hash over whole-box exclusions (§4.5); +//! - [`jumbf`]: the manifest store (§4.1); +//! - [`cose`]: `COSE_Sign1_Tagged` with EdDSA and the OKP key (§4.3, §7); +//! - [`x509`]: the claim signer's certificates, their profile and chain (§4.4, §11.2); +//! - this module: the assertions (§4.5 to §9), the writer of initialization segments and +//! segments under either method or both, and the verifier of §11; +//! - [`corpus`]: the conformance corpus (§13). +//! +//! The segment of a room track's group *is* the group's objects behind its C2PA boxes, so +//! the verifier checks the C2PA hard binding, the group receipt's Merkle root and each +//! object receipt over the same chunks (§8), through [`crate::jwp_delivery`]. +//! +//! [extensions/live-provenance.md]: https://git.transaction.science/Transaction-Science/open-standards/src/branch/main/wai/extensions/live-provenance.md + +pub mod bmff; +pub mod corpus; +pub mod cose; +pub mod jumbf; +pub mod log; +pub mod x509; + +use crate::jwp_delivery::{leaf_legacy, root, GroupReceipt, ObjectReceipt}; +use crate::jwp_hash::content_hash; +use crate::live_watermark::cbor::{encode, read_one, Item}; +use crate::timed_text::{origin, DST_PREFIX, MODEL_URI}; +use bmff::{BoxRef, Emsg}; +use ed25519_dalek::SigningKey; +use sha2::{Digest, Sha256}; + +/// The record string (SPEC §5). +pub const CAPABILITY: &str = "wai.live.provenance"; +/// §3.4: the domain of a track's stream identifier. +pub const STREAM_DOMAIN: &[u8] = b"wai:live-provenance:stream\x01"; +/// §4.2: the domain of a manifest identifier. +pub const MANIFEST_DOMAIN: &[u8] = b"wai:live-provenance:manifest\x01"; +/// §4.2: the role of the initialization segment's manifest. +pub const ROLE_INIT: u8 = 0; +/// §4.2: the role of a per-segment manifest (§6). +pub const ROLE_SEGMENT: u8 = 1; +/// §4.2: the role of a manifest that delivers session keys in a segment (§7.3). +pub const ROLE_ROTATION: u8 = 2; +/// §4.2: the role of the update manifest that carries a repository receipt (§4.7). +pub const ROLE_UPDATE: u8 = 3; +/// §4.7: the ingredient assertion's label. +pub const L_INGREDIENT: &str = "c2pa.ingredient.v3"; +/// §4.7: the repository receipt assertion's label (C2PA 2.4 §18.27). +pub const L_REPOSITORY: &str = "c2pa.repository-receipt"; +/// §4.7: the registration profile of the repository's proof. +pub const RECEIPT_PROFILE: &str = "urn:ietf:rfc:9942"; +/// §10.2: the suffix of a provenance track's name. +pub const PROVENANCE_SUFFIX: &str = "_c2pa"; +/// §10.2: the provenance track's `packaging`. +pub const PROVENANCE_PACKAGING: &str = "wai-c2pa"; +/// §10.2: the catalog field that names the provenance track. +pub const CATALOG_FIELD: &str = "wai_c2pa_track"; + +/// The hard binding's label (§4.5). +pub const L_HASH: &str = "c2pa.hash.bmff.v3"; +/// The actions' label (§4.6). +pub const L_ACTIONS: &str = "c2pa.actions.v2"; +/// The live video segment assertion's label (§6). +pub const L_LIVE: &str = "c2pa.livevideo.segment"; +/// The session keys assertion's label (§7.1). +pub const L_KEYS: &str = "c2pa.session-keys"; +/// The AI disclosure's label (§9.1); later instances add `__1`, `__2`, …. +pub const L_AI: &str = "c2pa.ai-disclosure"; +/// The stream assertion (§5.1). +pub const L_STREAM: &str = "science.transaction.wai.stream"; +/// The group assertion (§5.2). +pub const L_GROUP: &str = "science.transaction.wai.group"; +/// The take assertion (§5.3). +pub const L_TAKE: &str = "science.transaction.wai.take"; + +/// The claim generator's name (§4.2). +pub const GENERATOR: &str = "wai"; +/// The specification version the claim generator names (§4.2). +pub const SPEC_VERSION: &str = "2.4.0"; +/// The continuity method (§6). +pub const CONTINUITY: &str = "c2pa.manifestId"; +/// The generic model type of C2PA 2.4 Table 12 (§9.1). +pub const MODEL_TYPE: &str = "c2pa.types.model"; + +// ───────────────────────────── reasons ───────────────────────────── + +/// A reason of §12. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Reason { + SegmentMalformed, + InitInvalid, + ProvenanceMissing, + ManifestMalformed, + CredentialInvalid, + CredentialUntrusted, + ClaimSignatureMismatch, + CredentialExpired, + AssertionHashMismatch, + HardBindingMismatch, + ManifestIdMismatch, + StreamAssertionInvalid, + SourceTypeMismatch, + AiDisclosureMissing, + UpdateManifestInvalid, + RepositoryReceiptInvalid, + SessionKeyInvalid, + SequenceNotIncreasing, + SequenceMismatch, + StreamIdMismatch, + ContinuityInvalid, + ContinuityBroken, + SegmentInfoInvalid, + SessionKeyUnknown, + SegmentInfoSignatureFailed, + SessionKeyOutOfRange, + SegmentHashMismatch, + ReceiptMissing, + ReceiptInvalid, + ReceiptMismatch, + ReceiptSignerMismatch, + ObjectReceiptInvalid, + SegmentMissing, +} + +impl Reason { + /// Every reason, in §12's order. + pub const ALL: [Reason; 33] = [ + Reason::SegmentMalformed, + Reason::InitInvalid, + Reason::ProvenanceMissing, + Reason::ManifestMalformed, + Reason::CredentialInvalid, + Reason::CredentialUntrusted, + Reason::ClaimSignatureMismatch, + Reason::CredentialExpired, + Reason::AssertionHashMismatch, + Reason::HardBindingMismatch, + Reason::ManifestIdMismatch, + Reason::StreamAssertionInvalid, + Reason::SourceTypeMismatch, + Reason::AiDisclosureMissing, + Reason::UpdateManifestInvalid, + Reason::RepositoryReceiptInvalid, + Reason::SessionKeyInvalid, + Reason::SequenceNotIncreasing, + Reason::SequenceMismatch, + Reason::StreamIdMismatch, + Reason::ContinuityInvalid, + Reason::ContinuityBroken, + Reason::SegmentInfoInvalid, + Reason::SessionKeyUnknown, + Reason::SegmentInfoSignatureFailed, + Reason::SessionKeyOutOfRange, + Reason::SegmentHashMismatch, + Reason::ReceiptMissing, + Reason::ReceiptInvalid, + Reason::ReceiptMismatch, + Reason::ReceiptSignerMismatch, + Reason::ObjectReceiptInvalid, + Reason::SegmentMissing, + ]; + + /// The code §12 names. + pub fn code(self) -> &'static str { + match self { + Reason::SegmentMalformed => "segment_malformed", + Reason::InitInvalid => "init_invalid", + Reason::ProvenanceMissing => "provenance_missing", + Reason::ManifestMalformed => "manifest_malformed", + Reason::CredentialInvalid => "credential_invalid", + Reason::CredentialUntrusted => "credential_untrusted", + Reason::ClaimSignatureMismatch => "claim_signature_mismatch", + Reason::CredentialExpired => "credential_expired", + Reason::AssertionHashMismatch => "assertion_hash_mismatch", + Reason::HardBindingMismatch => "hard_binding_mismatch", + Reason::ManifestIdMismatch => "manifest_id_mismatch", + Reason::UpdateManifestInvalid => "update_manifest_invalid", + Reason::RepositoryReceiptInvalid => "repository_receipt_invalid", + Reason::StreamAssertionInvalid => "stream_assertion_invalid", + Reason::SourceTypeMismatch => "source_type_mismatch", + Reason::AiDisclosureMissing => "ai_disclosure_missing", + Reason::SessionKeyInvalid => "session_key_invalid", + Reason::SequenceNotIncreasing => "sequence_not_increasing", + Reason::SequenceMismatch => "sequence_mismatch", + Reason::StreamIdMismatch => "stream_id_mismatch", + Reason::ContinuityInvalid => "continuity_invalid", + Reason::ContinuityBroken => "continuity_broken", + Reason::SegmentInfoInvalid => "segment_info_invalid", + Reason::SessionKeyUnknown => "session_key_unknown", + Reason::SegmentInfoSignatureFailed => "segment_info_signature_failed", + Reason::SessionKeyOutOfRange => "session_key_out_of_range", + Reason::SegmentHashMismatch => "segment_hash_mismatch", + Reason::ReceiptMissing => "receipt_missing", + Reason::ReceiptInvalid => "receipt_invalid", + Reason::ReceiptMismatch => "receipt_mismatch", + Reason::ReceiptSignerMismatch => "receipt_signer_mismatch", + Reason::ObjectReceiptInvalid => "object_receipt_invalid", + Reason::SegmentMissing => "segment_missing", + } + } + + /// The C2PA 2.4 §15 code §12 gives the reason. + pub fn c2pa(self) -> Option<&'static str> { + Some(match self { + Reason::ManifestMalformed => "claim.malformed", + Reason::CredentialInvalid => "signingCredential.invalid", + Reason::CredentialUntrusted => "signingCredential.untrusted", + Reason::ClaimSignatureMismatch => "claimSignature.mismatch", + Reason::CredentialExpired => "claimSignature.outsideValidity", + Reason::AssertionHashMismatch => "assertion.hashedURI.mismatch", + Reason::HardBindingMismatch | Reason::SegmentHashMismatch => "assertion.bmffHash.mismatch", + Reason::UpdateManifestInvalid => "manifest.update.invalid", + _ => return None, + }) + } + + /// The §19.7 code §12 gives the reason. + pub fn livevideo(self) -> Option<&'static str> { + Some(match self { + Reason::SegmentMalformed + | Reason::ProvenanceMissing + | Reason::ContinuityBroken + | Reason::SegmentInfoInvalid + | Reason::SessionKeyUnknown + | Reason::SegmentInfoSignatureFailed + | Reason::SessionKeyOutOfRange + | Reason::SegmentHashMismatch => "livevideo.segment.invalid", + Reason::InitInvalid => "livevideo.init.invalid", + Reason::ManifestMalformed + | Reason::CredentialInvalid + | Reason::CredentialUntrusted + | Reason::ClaimSignatureMismatch + | Reason::CredentialExpired + | Reason::AssertionHashMismatch + | Reason::HardBindingMismatch + | Reason::UpdateManifestInvalid => "livevideo.manifest.invalid", + Reason::SessionKeyInvalid => "livevideo.sessionkey.invalid", + Reason::SequenceNotIncreasing | Reason::StreamIdMismatch => "livevideo.assertion.invalid", + Reason::ContinuityInvalid => "livevideo.continuityMethod.invalid", + _ => return None, + }) + } + + /// The reason of a code. + pub fn from_code(c: &str) -> Option { + Reason::ALL.iter().copied().find(|r| r.code() == c) + } +} + +/// Where a refusal was raised (§11.1). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Place { + Init, + /// The *k*-th segment received, counting from 0. + Segment(usize), + Group(u64), +} + +impl Place { + pub fn label(&self) -> String { + match self { + Place::Init => "init".into(), + Place::Segment(k) => format!("segment {k}"), + Place::Group(g) => format!("group {g}"), + } + } +} + +/// A refusal: the reason and where. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Refusal { + pub reason: Reason, + pub at: Place, +} + +// ───────────────────────────── CBOR helpers ───────────────────────────── + +fn text(s: &str) -> Item { + Item::Text(s.into()) +} + +fn bytes(b: &[u8]) -> Item { + Item::Bytes(b.to_vec()) +} + +fn map(entries: Vec<(&str, Item)>) -> Item { + Item::Map(entries.into_iter().map(|(k, v)| (text(k), v)).collect()) +} + +/// The value of map `m` under the text key `k`. +fn get<'a>(m: &'a Item, k: &str) -> Option<&'a Item> { + match m { + Item::Map(e) => e.iter().find(|(key, _)| matches!(key, Item::Text(t) if t == k)).map(|(_, v)| v), + _ => None, + } +} + +/// Whether `m` is a map whose keys are text, all of `required` among them, and no other but +/// `optional`'s. +fn keys(m: &Item, required: &[&str], optional: &[&str]) -> bool { + let Item::Map(e) = m else { return false }; + e.iter().all(|(k, _)| matches!(k, Item::Text(t) if required.contains(&t.as_str()) || optional.contains(&t.as_str()))) + && required.iter().all(|r| get(m, r).is_some()) +} + +fn get_text<'a>(m: &'a Item, k: &str) -> Option<&'a str> { + match get(m, k)? { + Item::Text(t) => Some(t), + _ => None, + } +} + +fn get_uint(m: &Item, k: &str) -> Option { + match get(m, k)? { + Item::Uint(v) => Some(*v), + _ => None, + } +} + +fn get_b32(m: &Item, k: &str) -> Option<[u8; 32]> { + match get(m, k)? { + Item::Bytes(b) => b.as_slice().try_into().ok(), + _ => None, + } +} + +/// Lowercase hexadecimal. +pub fn hex(b: &[u8]) -> String { + b.iter().map(|x| format!("{x:02x}")).collect() +} + +fn uuid_string(b: &[u8; 16]) -> String { + let h = hex(b); + format!("{}-{}-{}-{}-{}", &h[..8], &h[8..12], &h[12..16], &h[16..20], &h[20..]) +} + +// ───────────────────────────── identities ───────────────────────────── + +/// A version-8 UUID of 16 bytes (§3.4). +pub fn uuid_v8(mut b: [u8; 16]) -> String { + b[6] = (b[6] & 0x0F) | 0x80; + b[8] = (b[8] & 0x3F) | 0x80; + uuid_string(&b) +} + +/// A manifest identifier (§4.2): `urn:c2pa:` and a version-4 UUID of the 16 bytes. +pub fn manifest_id(mut b: [u8; 16]) -> String { + b[6] = (b[6] & 0x0F) | 0x40; + b[8] = (b[8] & 0x3F) | 0x80; + format!("urn:c2pa:{}", uuid_string(&b)) +} + +/// §4.2: the identifier of the manifest of `role` for sequence number `seq` (0 for the +/// initialization segment's and its update) of the stream `stream_id`. +pub fn derived_manifest_id(stream_id: &str, role: u8, seq: u64) -> String { + let mut h = Sha256::new(); + h.update(MANIFEST_DOMAIN); + h.update((stream_id.len() as u32).to_be_bytes()); + h.update(stream_id.as_bytes()); + h.update([role]); + h.update(seq.to_be_bytes()); + manifest_id(h.finalize()[..16].try_into().expect("sixteen bytes")) +} + +/// Whether `s` is a manifest identifier as §4.2 has it, and its UUID. +fn manifest_uuid(s: &str) -> Option<&str> { + let u = s.strip_prefix("urn:c2pa:")?; + let ok = u.len() == 36 + && u.char_indices().all(|(i, c)| if [8, 13, 18, 23].contains(&i) { c == '-' } else { c.is_ascii_digit() || ('a'..='f').contains(&c) }) + && &u[14..15] == "4" + && "89ab".contains(&u[19..20]); + ok.then_some(u) +} + +/// A media kind of live-rooms §6.1. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MediaKind { + Audio, + Video, + Screen, +} + +/// The media kind of a track's rendering `----` (§5.1), whose +/// room field is `room`. +pub fn track_kind(track: &str, room: &[u8; 32]) -> Option { + let (ns, layer) = track.split_once("--")?; + let layer_ok = !layer.is_empty() && layer.bytes().all(|c| c.is_ascii_digit()) && (layer == "0" || !layer.starts_with('0')); + let f: Vec<&str> = ns.split('-').collect(); + let [r, owner, kind] = f.as_slice() else { return None }; + let lower_hex = |s: &str| s.len() == 64 && s.bytes().all(|c| c.is_ascii_digit() || (b'a'..=b'f').contains(&c)); + let slot = |s: &str| s.len() > 1 && s.starts_with('_') && s.bytes().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'_'); + if !layer_ok || *r != hex(room) || !(lower_hex(owner) || slot(owner)) { + return None; + } + match *kind { + "audio" => Some(MediaKind::Audio), + "video" => Some(MediaKind::Video), + "screen" => Some(MediaKind::Screen), + _ => None, + } +} + +/// One input of a compositor's output (§5.1). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Input { + pub stream_id: String, + pub track: String, + pub origin: u8, + pub model: Option<[u8; 32]>, +} + +/// A stream's disclosure label (§5.1). +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Label { + Own { origin: u8, model: Option<[u8; 32]> }, + Inputs(Vec), +} + +fn origin_model_ok(origin: u8, model: &Option<[u8; 32]>) -> bool { + match origin { + origin::HUMAN => model.is_none(), + origin::MACHINE | origin::REVIEWED => model.is_some(), + _ => false, + } +} + +fn origin_item(entries: &mut Vec<(&str, Item)>, o: u8, m: &Option<[u8; 32]>) { + entries.push(("origin", Item::Uint(o as u64))); + if let Some(m) = m { + entries.push(("model", bytes(m))); + } +} + +fn read_origin(m: &Item) -> Option<(u8, Option<[u8; 32]>)> { + let o = u8::try_from(get_uint(m, "origin")?).ok()?; + let model = match get(m, "model") { + None => None, + Some(_) => Some(get_b32(m, "model")?), + }; + origin_model_ok(o, &model).then_some((o, model)) +} + +impl Label { + pub fn to_item(&self) -> Item { + match self { + Label::Own { origin, model } => { + let mut e = Vec::new(); + origin_item(&mut e, *origin, model); + map(e) + } + Label::Inputs(v) => map(vec![( + "inputs", + Item::Array( + v.iter() + .map(|i| { + let mut e = vec![("streamId", text(&i.stream_id)), ("track", text(&i.track))]; + origin_item(&mut e, i.origin, &i.model); + map(e) + }) + .collect(), + ), + )]), + } + } + + fn from_item(m: &Item) -> Option