All checks were successful
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Successful in 7s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 6s
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 6s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 2m42s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 4m36s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 1m25s
WAI conformance / does this change touch it (pull_request) Successful in 48s
WAI energy receipts / does this change touch it (pull_request) Successful in 50s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 2m1s
wai-quantum studio and desktop / does this change touch it (pull_request) Successful in 1m47s
wai-web wasm32 / does this change touch it (pull_request) Successful in 1m35s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 7m53s
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 2m59s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 12m59s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 2m21s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 31s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 9m13s
wai-pack / wai-pack · linux (pull_request) Successful in 1m21s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 3m49s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 2m35s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 15s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 11s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 29s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 18m48s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 1m32s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 4m18s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 12m19s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m28s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 17m57s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 18s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 1m22s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 4m50s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 25m35s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 8m40s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 15m15s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 20s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 45s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 5m35s
wai-pack / wai-pack · macos (pull_request) Successful in 1m5s
wai-quantum studio and desktop / desktop app · macos (pull_request) Successful in 2m5s
Re-segmenting a stream, or writing its objects into another layout or codec, makes a new stream that names the source segments' manifests as parentOf ingredients, as deriving-edge §6.1 has an edge name its inputs' stores; packaging the same objects into this profile's segment derives nothing. Co-Authored-By: Claude Opus 5.5 <[email protected]>
924 lines
59 KiB
Markdown
924 lines
59 KiB
Markdown
# 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: `<room>-<owner>-<kind>--<layer>` |
|
||
| `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.
|