WAI delivery bindings: secure-objects AAD in receipts (MOQT 22/18/16), CMSD read, steering named to CDNs, live C2PA signing #458

Closed
dcharlot wants to merge 16 commits from wai/delivery-bindings into main

2026-10-24

Transaction Science
delivery-telemetry §6.2, §6.7: science.transaction.wai-op is a String, as CTA-5006 §4 item 1 has a custom key
Some checks failed
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 3m0s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 11m28s
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 1m40s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 29s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 5m17s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Failing after 4m5s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 13s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 29s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 2m19s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 32s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 1m57s
standards workspaces build + test / macOS energy backends (pull_request) Failing after 6m16s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 11m12s
wai-pack / wai-pack · linux (pull_request) Successful in 1m33s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 20m34s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 15m39s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 4m1s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Failing after 22m54s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 2m54s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 1m29s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 7m21s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 2m17s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 7m9s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 23m48s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 29s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 1m34s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 7m47s
wai-pack / wai-pack · macos (pull_request) Successful in 1m21s
wai-quantum studio and desktop / desktop app · macos (pull_request) Successful in 2m33s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 1h2m11s
CTA-5006 (November 2022) §4, item 1: custom keys "MUST only utilize the String value
type". §6.2 wrote the operation token as a Token; it now writes a String holding the
token, and §6.7 reads only that form. A Token for the key is a value not of its type, so
a reader ignores it and states so (`type`, §6.8; CTA-5006 §8.1 item 4). The §6.9 note
that kept the former form is removed.

- The emitter (CmsdStatic::encode) writes the String; the reader takes a String holding a
  token and ignores a Token; the CML differential encodes a String and its Table-1 check
  no longer takes a Token for the key; the unit test's expected field follows.
- telemetry-conformance: the one value of expected/cmsd.json that held the key changes,
  `science.transaction.wai-op=crop` to `science.transaction.wai-op="crop"`;
  cmsd-read/wai-keys writes the String; new cmsd-read/wai-op-a-token: the Token ignored,
  `type`, and `ot` read beside it.
- CHANGELOG notes it.

Falsified: a reader that takes the Token again fails wai_telemetry verify on
wai-op-a-token; an emitter that writes the Token again fails verify on expected/cmsd.json
and the unit test; a differential whose check takes the Token fails on wai-op-a-token.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:45:08 -04:00
Transaction Science
wai SPEC §5, CHANGELOG, live-conformance README: the delivery bindings
Some checks failed
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 4m51s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Failing after 2m25s
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 1m55s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 19s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 4m27s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 10s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 27s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 2m13s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 32s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 2m59s
wai-pack / wai-pack · linux (pull_request) Successful in 1m44s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
wai-pack / wai-pack · macos (pull_request) Has been cancelled
wai-pack / wai-pack · independent verifier (Python) (pull_request) Has been cancelled
wai-pack / wai-pack · the floor through a media prober (pull_request) Has been cancelled
wai-pack / wai-pack · dependency licences (pull_request) Has been cancelled
wai-quantum studio and desktop / studio · browser (pull_request) Has been cancelled
wai-quantum studio and desktop / desktop app · macos (pull_request) Has been cancelled
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
wai-pack / wai-pack · linux-arm64 (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
SPEC §5: wai.live.e2ee's receipts may bind each object's identity, the AAD it is sealed
under, formed as MOQT -22 encodes it on any path; wai.live.steering's decision and hops
named in CMSD and CMCD for CDNs, none of it evidence; wai.live.provenance signed and
verified segment by segment as the stream runs. CHANGELOG v0.4.0: one entry for the
bindings. live-conformance README: joule_moq_e2ee_vectors also writes identity.json.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:22:15 -04:00
Transaction Science
c2pa-live README: what independent C2PA implementations report, 2026-10-22
The corpus read by the C2PA command-line tool 0.28.2 (built on the C2PA
Rust library 0.91.2) and by the library 0.91.4 through an uncommitted
harness, each under the corpus's anchor:

- given an anchor and no EKU list, the library clears its accepted EKUs
  and keeps e-mail, time-stamping and OCSP by name: every claim-signing
  certificate is signingCredential.invalid and the time-stamping one
  Trusted; with its own default list as trust_config, the good
  initialization segments are Trusted, as with 0.27.22;
- each per-segment manifest of every good stream, read against its own
  segment, is Trusted with assertion.bmffHash.match, and the manifest
  negatives get the §15 codes §12 gives them;
- neither implements §19's validation (no source names the live video
  segment assertion, the segment-info scheme or session keys); their
  fragment path is Appendix A.5.4's Merkle form, which refuses every
  stream here; a segment alone is not read.

live-provenance §13 and its header name implementations, plural.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:22:15 -04:00
Transaction Science
live-provenance §10.4, §11.7: signing as the stream runs, verifying as it arrives
§10.4: a packager signs a segment once its last chunk closes (the hard
binding covers every chunk, C2PA 2.4 §19.2.1, §19.2.2); what it holds
between segments, all public but its keys, so another packager continues
from it (§10.3); what it checks before it releases a segment, each defect
with §12's reason; session keys delivered ahead of their first segment
(§19.4.3, §19.4.4.1, §19.6.2). A player that presents parts of a segment
before its C2PA boxes presents them unverified: this revision signs
whole segments.

§11.7: a verifier checks each segment once the receipts of its group and
of the groups before it are held, holds what does not grow with the
stream, and stops at the first refusal. §13 names the new checks;
capture-binding §11 names the live signer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:22:15 -04:00
Transaction Science
live_provenance::live: sign each segment as its last chunk closes
LiveSigner writes the initialization segment, then a track's segment from
its chunks as they travelled and its group receipt, or a take's from its
chunks and the device's last commitment, under the per-segment C2PA
Manifest Box method (C2PA 2.4 §19.3), the Verifiable Segment Info method
in emsg (§19.4), or both, delivering later session keys in a segment's
manifest (§19.4.3). Before it writes a segment it checks what a verifier
will (§11.4 steps 1, 2, 4, 5, 6) and refuses with the verifier's Reason;
a refusal changes nothing it holds. LiveState is what it holds between
segments, all of it public, and resume() continues a stream from it.

The corpus checks, in tests/live_provenance.rs and
wai_live_provenance_vectors verify: the live signer writes every good
stream byte for byte one segment at a time, the failover's second
packager resumed from the first's state at its third segment; it refuses
one defect of each kind with its reason, keeps its state, and the stream
then continues as the corpus's; and every vector, verified segment by
segment as it would arrive, gets the outcome the whole stream gets. No
corpus file changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:22:15 -04:00
Transaction Science
live_provenance: a verifier that receives the stream as it arrives
verify() becomes LiveVerifier given a whole stream at once. LiveVerifier
starts on the initialization segment (§11.3), takes each segment in the
order received once the receipts of its group and of the groups before it
are held (§11.4), and checks the end (§11.5). What it holds does not grow
with the stream: the running count of the receipts' objects replaces the
walk from firstGroup at every segment, receipts of groups passed are let
go, and §11.5 keeps the first group a segment passed over. After a refusal
every call gives that refusal again.

Every vector of live-conformance/c2pa-live keeps its outcome through the
wrapper: tests/live_provenance.rs is unchanged and passes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:22:15 -04:00
Transaction Science
energy_steering_verify.py: §8.5 from the text
The second implementation reads cdn.json beside vectors.json: it writes the steering
response's CMSD-Static field and the hops' CMSD-Dynamic entries, checks each response
against its manifest, adds the decision's CMCD key, and attributes each response, with a
List parser added to its own RFC 9651 parser and the CMSD reading §8.5 needs (field lines
joined, a later version ignoring the data set, custom keys as Strings). It requires every
case's outcome, reason and entry, that §8.5's table lists exactly the reasons it raises,
and that each of fourteen mutated copies of cdn.json fails.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:22:15 -04:00
Transaction Science
energy-steering §8.5: a decision and its hops named in CMSD and CMCD, for CDNs
Steering already decides from the hops' signed per-hop records (live-metering §3) and
writes the steering manifest players read; nothing a CDN parses named the decision or the
hops. §8.5 binds them to the fields CDNs and players already parse:

- a steering response SHOULD carry CMSD-Static `n`, `ot=m` and the custom String key
  science.transaction.wai-sd, the decision's digest (CTA-5006 §3, §4 item 1, §7 Table 1);
  a client checks it against the manifest's member (§8.4): bound, absent, or
  cmsd_decision_mismatch, keeping to the manifest either way;
- a client applying a manifest SHOULD add science.transaction.wai-sd to its media
  requests' CMCD (CMCD-Session shard or the CMCD query), inserted in bytewise order with
  every other member unchanged, so a CDN's CMCD logs name the decision behind each
  request; the observation schema and the telemetry corpus are unchanged;
- a hop SHOULD add its own CMSD-Dynamic entry with science.transaction.wai-hop, the key
  that signs its per-hop records (CTA-5006 §4 item 5, §5); it reads nothing else of the
  records, so it holds for any later per-hop record or receipt signed by that key;
- "served by": a client's or auditor's check of which policy hops a response's
  CMSD-Dynamic names on the pathway it applied, with pathway_unknown, served_malformed,
  served_unknown_hop, served_off_pathway and served_out_of_order.

The decision rule reads no CMSD value and no CMCD key but §4.4's (CTA-5006 §8.1 item 7,
§9.2); WSP1, WSD1 and vectors.json keep their bytes. §8.5 has its own reasons table, so
§10 and Reason::ALL are unchanged. SVTA Open Caching is named out of scope: its texts
were not read for this revision.

Code: energy_steering::cdn. Corpus: live-conformance/steering/cdn.json, written by
`wai_energy_steering_vectors gen` from vectors.json and checked by `verify` and
tests/energy_steering.rs (regenerates byte for byte; every case its outcome; §8.5 lists
exactly cdn::Reason::ALL; every reason refuses a case).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:22:15 -04:00
Transaction Science
telemetry differential: CMSD read against the SVTA common-media-library
cml/differential.mjs now decodes every cmsd-read/ case with the library's CMSD decoders
(@svta/cml-cmsd 1.1.1 over @svta/cml-structured-field-values 1.1.6, the locked versions)
and requires: the library's value for every key the reader keeps; the library's failure
exactly where the reader ignores a field whole; and, for every key the reader ignores for
its type or calls unknown, that CTA-5006 §7 Table 1 and §4 item 1, written here in
JavaScript and applied to the library's own Structured Field decoding, agree. Each
remaining difference must be of a named kind and is counted: the library applies none of
§8.1's reading rules (type, unknown, version); its decodeCmsdStatic returns a Token as a
string, so `n=origin` and `n="origin"` decode alike; and its parser takes RFC 9651 Dates
but not Display Strings. The README states each kind.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:22:15 -04:00
Transaction Science
delivery-telemetry §6.6–§6.10: CMSD read as CTA-5006 §8.1 has a client read it
CMSD was written (§6.1–§6.5) and never read. The reader (telemetry::cmsd::read) takes a
response's CMSD-Static and CMSD-Dynamic field lines, joins each field's lines (RFC 9110
§5.3) and parses it whole (RFC 9651 §4.2), and then, as CTA-5006 (November 2022) §8.1
has it: a field that does not parse is ignored whole (item 1); a `v` above 1 ignores the
data set (item 8); a value not of its key's Table 1 type is ignored alone (item 4); a key
the header does not reserve and that is not a custom key is ignored (item 3), `tl` among
them; custom keys are read only as Strings (§4 item 1). CMSD-Dynamic entries keep their
order, origin first (§4). Everything ignored is stated with its field, entry, key and why.

Table 1's full key set is read, `n`, `br` and `v` included; `sf` as a Token or an Inner
List of Tokens, since Table 1 and Annex A differ; `nor` and `nrr` cut at `|`, a range
element empty or start-end. The emitter now refuses `st=ll`, which Table 1 does not have
(no corpus byte changes: no CMSD case used it). §6.9 states where CTA-5006 is read one
way, among them that §6.2 writes `wai-op` as a Token, which §4 item 1 does not allow for a
CMSD custom key; the corpus keeps §6.2's bytes and the reader takes a Token or a String.

The corpus gains cmsd-read/ (29 cases, nine in the shape of CTA-5006 Annex A's examples,
written with the corpus's own values, since CTA-5006 carries no reproduction licence) and
expected/cmsd-read.json; `wai_telemetry verify` reads every case and requires each field
the crate writes to read back as the description it came from. Every existing file of
telemetry-conformance keeps its bytes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:22:15 -04:00
Transaction Science
wai-rs telemetry: RFC 9651 parsed whole, for readers of fields others write
The whole-grammar Structured Field parser energy steering wrote for a client's CMCD
(Dates, Display Strings, Byte Sequences checked as base64, parameters on members) moves
to telemetry::sf::full, with a List parser beside its Dictionary parser and the RFC 9110
§5.3 joining of field lines. energy_steering re-exports it as `sf`, so its callers and
tests are unchanged; the CMSD reader of delivery-telemetry §6.6 uses the same parser.
The subset parser the reports' own round trips use stays as it was.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:22:15 -04:00
Transaction Science
confidentiality §8.4, §11.6, §11.7: receipts bound to each object's identity, and the transport's revisions
- §8.4: an object's identity is the AAD secure objects §3.5 forms for it, as MOQT -22
  encodes its fields; an identity-bound object receipt (leaf_version 3) binds it beside the
  labelled leaf's fields; whoever holds the object and its receipt forms the identity as
  the object arrived and refuses identity_mismatch, naming group_object,
  publisher_priority or immutable_properties; identity_malformed for a receipt whose
  identity is not its own. A publisher of a sealed track SHOULD sign in this form (§8.1).
  The reasons are §8.4's own, so §13 and vectors.json keep their codes.
- §11.6: -18 carries -22's integers and pairs and keeps Immutable Properties' bytes
  (-18 §1.4.1, §1.4.3, §12.7); -16 writes QUIC's integer (-16 §1.4, RFC 9000 §16), so a
  -16 endpoint or bridge re-encodes the pairs of Immutable Extensions (-16 §11.2) as -22
  writes them before it forms the AAD; a value past 2^62 - 1 does not cross -16.
- §11.7: the working group's milestone "Request publication of Publication and
  Subscription Protocol to IESG", due 31 December 2026, and what the next revision of this
  text re-reads when it is made; the Key ID stays at 0x3A4A until secure objects and the
  registry agree.
- §4.4, §8.1, §8.2 corrected: WAI_INNER_DIGEST is BLAKE3 over the payload alone, and a
  receipt's content hash is jwp-receipts §2.1's (domain moq-jwp:object\x01); the text said
  they were the same hash. The corpus's bytes are unchanged; its README says which is which.
- Texts read: MOQT -18 and -16 at the sections named, RFC 9000 §16, the MoQ working
  group's milestones as listed on 22 October 2026.
- joule-moq and joule-moq-relay READMEs: the identity-bound receipt, the door's check, the
  -16 re-encoding row, and the runs of 2026-10-22 through joule-moq-relay at every draft
  pair and through another implementation's open-source relay built from source at -16
  and at -18, on loopback: every sealed object's identity matched its receipt and every
  object opened; every tamper was refused.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:22:15 -04:00
Transaction Science
e2ee corpus: identity.json, identity-bound receipts over sealed objects, and its second check
joule_moq_e2ee_vectors gen now also writes live-conformance/e2ee/identity.json (WAI
confidentiality §8.4, §11.6), from the corpus's seeds: a sealed video track's group whose
object receipts bind each object's identity, in the WJR1 form a receipts track carries,
with each sealed payload, content hash, identity and leaf; four copies with one defect
each (an identity of another object: identity_malformed; an identity dropped and a
priority restated inside one: unverified; an identity length below 13: malformed); and
thirteen arrivals of those objects at a hop, at -22, -18 and -16, each checked against its
receipt: pass, or identity_mismatch naming group_object, publisher_priority or
immutable_properties. The -16 arrivals state the pairs as -16 carries them and as -22
encodes them, and one that takes -16's bytes for -22's fails, which is why §11.6 has a
-16 hop re-encode them. vectors.json regenerates byte for byte; verify checks both files.

e2ee_verify.py, the second implementation, reads the bundle in jwp-receipts §2.7's binary
form with the third leaf form, checks the group's signature under the publisher's key
(strict Ed25519), rebuilds each leaf from the sealed payload's content hash and the
identity and walks it to the signed root, re-encodes -16's pairs (QUIC's integer) as -22
encodes them, and names the part that differs; seven mutated copies of identity.json
must each fail. Falsified: a v3 leaf without the identity fails every group; skipping
the -16 re-encoding fails draft16-converted.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:22:15 -04:00
Transaction Science
joule-moq-interop: wai-sealed, every object a secure object and every receipt identity-bound
The wai-receipts scenario with each media object sealed as an MoQ secure object
(draft-ietf-moq-secure-objects-01 on MOQT -22's objects, as WAI confidentiality §4 places
it: suite 0x0004, the Key ID inside Immutable Properties, no receipt property) and each
object receipt bound to its identity (§8.4). The subscriber forms each object's identity
from what arrived, checks it against its receipt, and opens the object under that same
AAD. It shows whether a relay carries the bytes secure objects authenticates unchanged.
`--tamper identity` sends one object with one more pair inside its Immutable Properties
than its seal and receipt bound; scripts/local.sh runs wai-sealed at each draft and
across drafts, and requires that tamper to fail.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:22:15 -04:00
Transaction Science
joule-moq: receipts bound to the object's identity, and a door that holds objects to it
A third leaf form, the identity-bound one of WAI confidentiality §8.4 (leaf_version 3):
the labelled leaf's fields under the domain moq-jwp:merkle-leaf\x03, then the object's
identity, the SECURE_OBJECT_AAD of draft-ietf-moq-secure-objects-01 §3.5 as MOQT -22
encodes it: Group ID (8 bytes), Object ID (4), Publisher Priority (1) and the value of
its Immutable Properties pair. The receipt carries the identity (u32 length, then the
bytes, after the class), so a hop checks what it forwards in the clear against what the
publisher signed, with no key: the priority, the Key ID and every other immutable pair.

- receipt: object_identity, identity_as_received (one Immutable Properties pair or none;
  an object read from -16 through the bridge has -22's encoding), check_identity with
  the part that differs (identity_difference: group_object, publisher_priority,
  immutable_properties, length), ReceiptBuilder::add_identity_object and identity_for;
  the label requires a class beside an identity and an identity of the receipt's own
  Group ID and Object ID; the binary form reads leaf_version 3 and refuses an identity
  shorter than 13 bytes or longer than 13 + 2^16 - 1.
- joule-moq-relay: the door forms each object's identity as it arrived (the header's
  priority, or the track's DEFAULT_PUBLISHER_PRIORITY it inherits, as fan-out uses) and
  refuses identity_mismatch; the sealed-objects test's receipts are identity-bound and
  each subscriber checks them, and a new test refuses at the door a sealed object whose
  Immutable Properties changed after its receipt.

Every receipt in the legacy and labelled forms keeps its bytes: the new form is opt-in,
and ObjectReceipt's new field is absent from the JSON and binary forms when unset.
Falsified: check_identity passing anything fails the unit test and the door test (group
1 delivered); a leaf that drops the identity fails the unit test; an identity that
ignores Immutable Properties fails the as-received unit test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 20:22:15 -04:00