open-standards/wai/extensions/c2pa-binding.md
Transaction Science 1a63a8d66e
Some checks failed
wai-audit collector and player integration / does this change touch it (push) Successful in 15s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 20s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 22s
wai-fleet service / does this change touch it (push) Successful in 20s
wai-sink-android native sink library / does this change touch it (push) Successful in 9s
WAI Studio / does this change touch it (push) Successful in 13s
wai-sink-swift native sink package / does this change touch it (push) Successful in 14s
wai-app and Create / wai-app · does this change touch it (push) Successful in 2m16s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 10s
WAI examples / WAI examples · does this change touch them (push) Successful in 8s
wai-quantum for JavaScript / does this change touch it (push) Successful in 3m41s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 3m44s
wai-ledger service / does this change touch it (push) Successful in 3m44s
WAI operation catalogue / does this change touch it (push) Successful in 3m45s
wai-quantum studio and desktop / does this change touch it (push) Successful in 3m48s
joule-moq-http live HTTP egress / joule-moq-http · does this change touch it (push) Successful in 3m58s
wai-quantum studio and desktop / desktop app · windows (push) Waiting to run
wai-pack / wai-pack · does this change touch it (push) Successful in 3m53s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m39s
wai-audit collector and player integration / collector · linux (push) Successful in 3m21s
wai-fleet service / service · linux (push) Successful in 3m14s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 1m3s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 2m30s
wai-ledger service / service · linux (push) Successful in 2m37s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 7m31s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 2m28s
wai-pack / wai-pack · linux (push) Successful in 5m27s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 4m26s
wai-quantum for JavaScript / javascript · linux (push) Successful in 10m50s
wai-quantum studio and desktop / studio · browser (push) Successful in 9m7s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 10m45s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 9m41s
WAI Studio / Studio in WebKit · linux (push) Successful in 26m7s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Failing after 13m17s
WAI examples / WAI examples · portable · linux (push) Successful in 22m54s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 2m0s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 52s
wai-fleet service / service · macos (push) Successful in 44s
joule-moq-http live HTTP egress / joule-moq-http, every target (push) Successful in 3m28s
wai-pack / wai-pack · linux-arm64 (push) Successful in 4m36s
wai-pack / wai-pack · the floor through a media prober (push) Successful in 1m49s
wai-pack / wai-pack · dependency licences (push) Successful in 2m5s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 9m44s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 10m57s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 14m29s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 13m11s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 10m22s
WAI examples / WAI examples · android-app · linux-arm64 (push) Successful in 11m25s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 8m51s
wai-ledger service / service · macos (push) Successful in 1m8s
wai-pack / wai-pack · independent verifier (Python) (push) Successful in 21m13s
wai-pack / wai-pack · macos (push) Successful in 3m53s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 5m53s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (push) Successful in 22m0s
Identity assertions validate in the C2PA SDK's validator: the holder signs the schema's order by default, a verifier accepts either serialization
Every identity assertion WAI wrote was reported cawg.x509.signature.mismatch by the C2PA SDK's
validator. Read at their sources: Identity Assertion 1.0 to 1.3 §8.2.1 (the payload is serialized
in RFC 8949 §4.2.1's core deterministic encoding and those bytes are signed; no later version or
erratum), and the c2pa crate at 0.91.7 and 0.91.11, its newest release, which signs and verifies
its own serialization of the payload's members in the schema's order (referenced_assertions,
sig_type, role; url, alg, hash) with the writer that keeps declaration order. The bytes signed
were the whole difference: the COSE structure, protected header, sig_type, hashed URIs and chain
of a WAI assertion and of one the SDK's builder writes are the same.

c2pa-binding §4.10, §4.11, §6.2 step 12, §6.3, §9.2, §9.3 and decisions D6 (revised): a writer
signs the schema's order by default and may be asked for the core deterministic encoding
(identity::Signed); the stored assertion stays deterministic either way. The verifier accepts a
signature over either serialization of the payload's members, states which, and refuses one over
a third order, over other members, or by another key. On the SDK's path an identity assertion is
read in any well-formed encoding (one the SDK's builder wrote is accepted) and a map with a key
twice is refused.

Tests run the SDK's validator, configured with the corpora's anchors, on manifests signed
through the SDK and on the store writer's stores beside their envelopes: trusted, no failure
code, cawg.x509.credential.trusted, cawg.x509.signature.validated and cawg.identity.well-formed
for each identity assertion. The strict form gets exactly cawg.x509.signature.mismatch, pinned.
c2pa-sdk-identity/vectors.json pins the validator's report of every file at the SDK's version.

Both corpora regenerated with their tools (c2pa-identity byte for byte on x86_64 and aarch64);
new vectors for the strict form, the SDK builder's assertion, a signature over a third order and
over other members. Both Python verifiers compute the two serializations themselves and are
caught without either and when they take the members in any order. The wire break and the
three API changes are acknowledged in the change note.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-11 09:58:01 -05:00

862 lines
59 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# WAI Extension: C2PA Binding of a Stored Envelope
> Status: Draft. Registers the record `wai.asset.c2pa`: a C2PA Manifest Store bound to one
> stored `WAI1` or `WAI2` envelope ([SPEC](../SPEC.md) §2, §7.1), the envelope treated as a
> C2PA asset and left byte for byte as it is. The store is a sidecar served beside the object.
> Its hard binding is a `c2pa.hash.data` over every byte of the envelope, and its WAI assertion
> names the envelope's content hash ([jwp-receipts](jwp-receipts.md) §2.1), the capabilities it
> carries, and the JWP group receipt the envelope was delivered under, by that receipt's link
> hash and its publisher key. One verification then establishes the store's signer and
> assertions, the envelope's bytes, and the receipt that delivered them. The store is the
> profile [live-provenance](live-provenance.md) §4 writes: its JUMBF layout, its claim, its
> EdDSA claim signature and its claim signer's certificates are that extension's. No envelope
> byte, no manifest member and no transport changes. The reference implementation is the
> `c2pa_envelope` module of `wai-rs` (feature `c2pa_envelope`); the corpus is
> [`live-conformance/c2pa-envelope/`](../live-conformance/c2pa-envelope/), and its second
> implementation is `live-conformance/c2pa_live_verify.py`. After its three assertions a store
> may carry soft bindings of WAI's picture watermark, each naming the robustness claim that
> states what the mark survives (§4.8); the content's ISCC, as the soft binding `io.iscc.v0` of
> IEP-0020 and as an identifier (§4.9); and the Creator
> Assertions Working Group's identity assertions, by which a credential holder signs the
> assertions it stands behind (§4.10). A verifier may hold its trust anchors as trust lists in
> the form the C2PA Trust List is published in, each pinned by the digest of its bytes (§6.4).
> Their corpus is [`live-conformance/c2pa-identity/`](../live-conformance/c2pa-identity/), with
> the same second implementation.
> A WAI tool that signs through a C2PA SDK writes the same identity assertion into the manifest
> the SDK lays out, in the placeholder that path needs, and verifies it with the same checks
> (§4.11); its corpus is [`live-conformance/c2pa-sdk-identity/`](../live-conformance/c2pa-sdk-identity/).
> 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), §11.4 (external manifests and their media type),
> §15.5.3.1 and §15.5.3.2 (locating a store by reference and by `Link` header), §15.5.5,
> §15.12.1 (validating a data hash) and §18.5 (the data hash assertion), with the sections
> [live-provenance](live-provenance.md) lists for the store, the claim, its signature and the
> certificates it shares with this extension; RFC 8288 (`Link`); and RFC 9562. For §4.8 to §4.10,
> §6.4 and §9.2: the C2PA Technical Specification, version 2.5 (October 2026), §12.2.3, §13.4,
> §13.5.1, §14.2.2, §14.7, §16.3.7 and §16.5.13.4.3; the Creator Assertions Working Group's
> Identity Assertion, version 1.3 (ratified 17 August 2026), §5, §6, §7 and §8.2; ETSI TS 119 602
> V1.1.1 (2025-11), §6.3.2, §6.3.14, §6.3.15, §6.6.3 to §6.6.5 and §6.8; the C2PA Trust List as
> the C2PA published it on 4 November 2026 (sequence number 4), for its form; and the C2PA
> soft-binding algorithm list as fetched on 10 November 2026, for its entries' form; and, for
> §4.9, IEP-0020 (*ISCC C2PA Conformance*, a draft updated 2026-10-28), §4.3 and §5. A section
> number in this text is C2PA 2.4's unless it names another document or version.
## 1. Scope
In scope: which bytes a store binds (§3); the store, its manifest identifier, its hard binding,
its WAI assertion and its actions (§4); the soft bindings, the ISCC identifier and the identity
assertions a store may carry after them (§4.8 to §4.10); where a store is served beside its
envelope (§5); the verification that ties the store, the envelope and the envelope's receipts
together (§6), and the trust lists a verifier holds its anchors in (§6.4); its reasons (§7);
and what a writer of a store does (§8).
Also in scope: the identity assertion of §4.10 in a manifest a C2PA SDK lays out (§4.11).
Out of scope: a transport; a store embedded in an envelope (§4.6); ingredients, update manifests,
time-stamps and the manifest repositories of live-provenance §4.7; an identity credential other
than an X.509 certificate (the identity claims aggregation of Identity Assertion 1.3 §8.1);
revocation (C2PA 2.5 §13.5.2); a lookup of a store by its soft binding (C2PA 2.5 §16.3.7.5); a store about one component of
a `WAI2` fetched alone (a component's exploded envelope, SPEC §7.1, is a `WAI1` and takes a store
of its own); the carriage of a store over MoQ, where an object is a group's member and a stream
of objects takes [live-provenance](live-provenance.md)'s methods; and what a party does with a
verdict.
Relationship to the core spec: `wai.asset.c2pa` is a **record** ([SPEC](../SPEC.md) §5): a
signed record a WAI tool writes and verifies. It names no payload format and is not
dispatchable, so it is never a component of an envelope in any role (SPEC §7.1, *Component
identity*).
Relationship to [provenance](provenance.md): `wai.asset.provenance` signs a WAI lineage of an
asset's steps under a WAI key. This extension states an envelope's provenance in C2PA's own
form, so that a C2PA validator reads it, and binds it to the envelope's delivery receipt.
Relationship to [deriving-edge](deriving-edge.md): a store bound to an input as this extension
binds one is a store "carried beside it" there (§6.1), named by its SHA-256 and carried forward.
## 2. Terms
- **Envelope**: one `WAI1` or `WAI2` envelope, as SPEC §2 and §7.1 frame it.
- **Content hash**, **cid**: the envelope's [jwp-receipts](jwp-receipts.md) §2.1 content hash,
`BLAKE3("moq-jwp:object\x01" ‖ envelope)`, 32 bytes.
- **Store**: a C2PA Manifest Store, a JUMBF superbox (live-provenance §4.1).
- **Group receipt**, **object receipt**, **link hash**: as in [jwp-receipts](jwp-receipts.md)
§2.4.1, §2.4.2 and §3 step 3. The **publisher key** is the group receipt's `signer_pubkey`.
- **Claim signer**: the holder of the X.509 end-entity certificate that signs the store's claim
(live-provenance §2).
- *T*: the instant a verification is made.
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 envelope
The asset a store binds is the envelope's bytes exactly: for a `WAI1`, `12 + man_len +
payload_len` bytes; for a `WAI2`, its head and its payloads, `10 + man_len + 8·n + Σ lengths`
bytes; nothing before the magic and nothing after the last payload byte, as
[http-delivery](http-delivery.md) §5.1 requires of an object. Its manifest reads as SPEC §3 (a
`WAI1`) or §7.1 (a `WAI2`) has it. Its **capabilities** are, for a `WAI1`, its
`model_requirement.capability`, and for a `WAI2`, the `capability` of each entry of
`renditions`, in table order, companions and refinements included.
A store binds the envelope whole. A `WAI2` client that fetched some of its components by range
(http-delivery §7) holds the envelope's cid from its head proof, not its bytes, and verifies the
store once it holds the envelope whole.
## 4. The manifest store
### 4.1 The store
The store is the one live-provenance §4.1 lays out: a JUMBF superbox (`c2pa`) of one standard
manifest (`c2ma`), the active one, whose content is its assertion store, its claim and its claim
signature, each assertion a CBOR content superbox. Its claim is live-provenance §4.2's
`claim-map-v2` with exactly the fields that section lists, its `created_assertions` naming the
assertions below in this order; its claim signature is live-provenance §4.3's
`COSE_Sign1_Tagged` with EdDSA and the claim signer's `x5chain`; and its claim signer's
certificates meet live-provenance §4.4. The manifest holds, in this order:
1. `c2pa.hash.data`, the hard binding (§4.3);
2. `c2pa.actions.v2`, the actions (§4.5);
3. `science.transaction.wai.envelope`, the WAI assertion (§4.4);
and after them, each optional, in this order and nothing else:
4. soft bindings, labelled `c2pa.soft-binding`, `c2pa.soft-binding__1`, … (C2PA 2.4 §6.4): those
of WAI's watermark (§4.8), then at most one of ISCC (§4.9), the last of them;
5. at most one ISCC identifier, `science.transaction.wai.iscc` (§4.9);
6. identity assertions (§4.10), labelled `cawg.identity`, `cawg.identity__1`, ….
A store holds no detection verdict ([detection](detection.md) §6): a verdict is a detector's claim
of its own, not a statement of the claim signer's about how the content came to exist.
### 4.2 The manifest identifier
The manifest's identifier is `urn:c2pa:` followed by an RFC 9562 UUID of version 4 in the
8-4-4-4-12 lowercase form, as live-provenance §4.2 writes one, whose 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:c2pa-binding:manifest\x01"` ‖ *cid* ‖ *receipt* ‖ SHA-256(*EE*)),
*cid* and *receipt* the WAI assertion's `contentHash` and `receipt` (§4.4) and *EE* the DER of
the claim signer's end-entity certificate. The identifier is a function of the envelope, the
receipt and the signer: two signers of one envelope write two identifiers, and one signer that
signs one envelope under one receipt again writes the same one.
### 4.3 The hard binding
The hard binding is a `c2pa.hash.data` assertion (§18.5), the map:
- `alg`: `sha256`;
- `hash`: the SHA-256 of every byte of the envelope (§3);
- `pad`: the empty byte string.
It holds no `exclusions`, `name`, `pad2` or `url`. A store beside its asset is in none of the
asset's bytes, so nothing is excluded: the data hash covers the envelope whole, and §15.12.1's
validation of it is the comparison of one SHA-256. A C2PA validator given the envelope and its
store (§15.5.3) reports `assertion.dataHash.match` or `assertion.dataHash.mismatch` for it.
### 4.4 The WAI assertion
`science.transaction.wai.envelope`, a CBOR map under C2PA's entity-specific namespace (§6.2.1),
with exactly these keys:
| key | value |
|---|---|
| `contentHash` | the envelope's cid, 32 bytes |
| `capabilities` | the envelope's capabilities (§3), an array of one or more text strings |
| `receipt` | the link hash of the group receipt the envelope was delivered under, 32 bytes |
| `publisher` | that group receipt's `signer_pubkey`, 32 bytes |
The group receipt is in the labelled payload (jwp-receipts §2.3, `sign_version` 2), or, for a
verifier that implements [pq-signatures](pq-signatures.md), its hybrid form: a group in the
legacy payload has a link hash that does not cover its parent, so a store would not name one
chain ([http-delivery](http-delivery.md) §8.1 refuses it as a receipt file for the same reason).
`receipt` is also where the receipt is served over HTTP, `rcpt/<receipt>.json` (http-delivery
§8.1), so the store itself names the file a client fetches to check it.
### 4.5 The actions
`c2pa.actions.v2` (§18.15.2), the map `{"actions": [{"action": "c2pa.created",
"digitalSourceType": DST}]}`, as live-provenance §4.6 writes it: DST is a term of the IPTC
digital source type vocabulary, one or more ASCII letters, as a URI under
`http://cv.iptc.org/newscodes/digitalsourcetype/`. The claim signer states it; this extension
does not check it against the content. A verifier reports it (§6.3).
### 4.6 Beside the envelope, not in it
The store is never carried inside the envelope it binds. An envelope that held its own store
would have other bytes and so another cid, and the receipts already signed for the envelope
would no longer name it; and the store names the cid, which an envelope cannot carry about
itself. A `WAI2` component is applied with or instead of a rendition and names a payload format
(SPEC §7.1); a store describes the whole envelope and is a record, which is no component. Beside
the envelope, the store leaves the object, its cid, its receipts and its head proof as they
were, and a store can be added to an object published before it, replaced when its signer's
certificate is renewed, or joined by another signer's store, without a new object.
### 4.7 The envelope's picture
The **picture** of an envelope is its first `wai.image.png` payload, a `WAI1`'s own or, for a
`WAI2`, that of the first entry of `renditions` whose `capability` is `wai.image.png`, decoded to
its 8-bit samples (an 8-bit greyscale or RGB PNG without interlace, as [packaging](packaging.md)
§5, paragraph 5.7, has a still read), a grey sample taken as R = G = B. An envelope has none where no payload is one or where it
does not decode. §4.8 and §4.9 read it.
### 4.8 Soft bindings
A soft binding of a watermark is a C2PA soft-binding assertion (C2PA 2.5 §16.3.7) for a WAI
watermark marked in the envelope's picture, the map:
- `alg`: the scheme's identifier in the soft-binding algorithm list's form (§16.3.7.4 there):
`science.transaction.wai.tent64` for `wai-mark:tent64` ([synthesis](synthesis.md) §7.5), the
scheme this revision binds by;
- `blocks`: one block, `{"scope": {}, "value": V}`: the empty scope, the whole picture, and `V` the
8-byte payload marked in it (synthesis §7.2), a byte string;
- `bindingMetadata`: `{"science.transaction.wai.robustnessClaim": L}`, `L` the link hash of the
robustness claim ([robustness-evidence](robustness-evidence.md) §8) that states what the mark
survives, a field of an entity-specific name (§16.3.7.3.1 there).
It holds no other key: no `pad`, `pad2`, `name`, `alg-params`, `url` or `metadata`. C2PA matches a
soft binding by `alg` and `value` (§16.3.7.5.1 there), and a validator does not use
`bindingMetadata` to validate one; this profile reads it to find the claim the binding rests on
(§6.2 step 10). A soft binding names the content it is in: its value is the mark the envelope's
picture carries, so a store cannot carry the mark of other content, and the claim it names is held
to the records it cites, so it cannot claim more than was measured.
The entries these identifiers take in the C2PA soft-binding algorithm list are written in
[`c2pa-soft-binding-entries.json`](c2pa-soft-binding-entries.json) in the list's form (§16.3.7.4
there), for `wai-mark:tent64` and `wai-mark:band64` (synthesis §7.5, §7.10), the schemes whose
robustness records the corpora hold, each naming, under an entity-specific member, the link
hashes of its robustness claim and of the records the claim cites, which robustness-evidence §11
maps onto the proposed `robustnessEvidence` object. An entry's `identifier` is the C2PA's to
assign, and its `contact` and the date it is entered the submitter's to give, so none is written;
submitting the entries is not this text's.
### 4.9 The ISCC soft binding and the ISCC identifier
A store carries the envelope's ISCC ([iscc](iscc.md), ISO 24138's codes) in two assertions, each
optional.
**The ISCC soft binding** is a C2PA soft-binding assertion of the algorithm `io.iscc.v0`, entry 3
of the C2PA soft-binding algorithm list, as IEP-0020 §4.3 has one, the map:
- `alg`: `io.iscc.v0`;
- `blocks`: one block, `{"scope": {}, "value": V}`: the empty scope, and `V` a byte string, the
ISCC-SEQ (iscc §3.3) of the **binding's units**. A value is never the sequence's text, and this
profile does not write IEP-0020's other form, an array of integers;
- `bindingMetadata`: `{"informationalUrl": "https://ieps.iscc.codes/iep-0020/",
"io.iscc.generator": {"name": N, "version": R}}`, `N` and `R` text strings that are not empty,
the software that computed the units and its version (IEP-0020 §4.3). The reference writer's
are `wai-rs` and `iscc-1`, the revision of iscc §4's computation, which changes when a code it
computes of the same content changes.
It holds no other key. The binding's units are the ones IEP-0020 §5 has a claim generator compute
from the source, which for a store beside its asset is the envelope's bytes (§3), each of 256
bits, in this order:
1. the Image-Code of the envelope's picture (iscc §4.2, after iscc §4.3's pre-processing), where
the envelope has one and the code is not featureless (iscc §5; IEP-0020 §5 has no binding
include a featureless Image-Code);
2. the Data-Code of the envelope's bytes (iscc §4.6);
3. the Instance-Code of the envelope's bytes (iscc §4.1).
A soft binding of a watermark and the ISCC soft binding are two kinds, and neither is read as
the other. A watermark's value is a payload its detector reads out of the picture, and its
`bindingMetadata` names the robustness claim that states what the mark survives. An ISCC
binding's value is computed from the envelope and marks nothing: no detector reads it, and no
robustness record of this revision measures `io.iscc.v0`, so it names no robustness claim. A
verifier holds it to the units computed again from the envelope (§6.2 step 11), each equal: a
binding that iscc §5 would match by similarity, its Data-Code or its Image-Code near or equal
and its Instance-Code another's, is refused. Matching by similarity is what a party does with a
binding and an asset that has no store (iscc §5); in a store, the envelope is at hand.
IEP-0020 §4.3 has a validator ignore what it does not know of such an assertion (a block of
another scope, `alg-params`, a `bindingMetadata` it cannot read) and accept the array form. This
profile is the form §8's writer writes, and a verifier refuses a store whose ISCC binding is in
another (`manifest_malformed`): every binding this profile accepts is one an IEP-0020 validator
reads whole.
**The ISCC identifier** is `science.transaction.wai.iscc`, a CBOR map under C2PA's
entity-specific namespace of exactly one key, `iscc`: a byte string holding the ISCC-SEQ (iscc
§3.3) of, in this order, the Image-Code of the envelope's picture, where the envelope has one,
featureless or not, and the Instance-Code of the envelope's bytes, each of 256 bits. It
identifies the content by ISO 24138's codes: the Instance-Code its bytes exactly, the Image-Code
its picture by similarity, which survives a re-encoding the hard binding does not. Both are
functions of the envelope, so a verifier recomputes them (§6.2 step 11). It is an identifier under
WAI's own label and not a soft-binding assertion: a C2PA validator that resolves soft bindings
reads the ISCC soft binding, and a WAI tool that held stores before this revision reads the
identifier, which is unchanged.
### 4.10 Identity assertions
An identity assertion is the Creator Assertions Working Group's (Identity Assertion 1.3 §5), with
an X.509 credential and a COSE signature (§8.2 there, `sig_type` `cawg.x509.cose`), by which a
credential holder, the **named actor**, signs the assertions it stands behind:
- the map `{"signer_payload": P, "signature": S, "pad1": h''}`, with no `pad2`;
- `P`, the `signer-payload-map` (§5.2 there): `{"referenced_assertions": R, "sig_type":
"cawg.x509.cose"}`, with `"role": [...]` where the holder states its relationship to the content
(§5.1.2 there, `cawg.creator`, `cawg.publisher`, …) and only then; `R` holds a hashed URI, `url`
and `hash` as the claim's `created_assertions` writes them, for each assertion the holder
chooses, in the order it chooses, each once, the hard binding among them (§5.1.1 there), and
none written after the identity assertion itself;
- `S`, a `COSE_Sign1_Tagged` written as the claim signature is (live-provenance §4.3) with the claim
replaced by the bytes of `P` the holder signs, which are stated below: the
protected header `{1: -8, 33: [EE, CA…]}` with the holder's end-entity certificate and its
intermediates, the unprotected header empty, the payload detached;
- the holder's end-entity certificate meets live-provenance §4.4's profile with
`id-kp-documentSigning` (1.3.6.1.5.5.7.3.36), the EKU §8.2.4 there accepts, in the place of
`c2pa-kp-claimSigning`.
Every signature is Ed25519 and every encoding deterministic, so the store's writer knows the hash
of each assertion an identity references before the holder signs, and no placeholder (§6.3 there)
is needed: `pad1` is empty. Several identity assertions describe several actors; one may reference
another written before it (§5.1.1 there).
**The bytes the holder signs.** The assertion a store holds, `P` in it, is in the core
deterministic encoding like every assertion of §4.1. The bytes `S` is over are one of two
serializations of `P`'s members:
- **the schema's order** (`schema`): a map of `P`'s members `referenced_assertions`, `sig_type`
and `role`, in that order, each where `P` holds it, a hashed URI's members in the order `url`,
`alg`, `hash`, each where it holds it; no other member of `P` or of a hashed URI is among the
bytes; every key and every value under those is encoded as the core deterministic encoding
encodes it. This is the order in which the schema lists the members (§5.2 there, and the
hashed URI of C2PA 2.4);
- **the core deterministic encoding** (`deterministic`): RFC 8949 §4.2.1's encoding of the whole
of `P`, whose keys are in the bytewise order of their encodings (`role`, `sig_type`,
`referenced_assertions`; in a hashed URI `alg`, `url`, `hash`).
With or without a role the two differ, so a signature over one does not verify over the other.
A writer signs the schema's order unless it is asked for the core deterministic encoding, and a
verifier accepts a signature that verifies over either, states which (§6.3), and refuses one over
any other bytes (§6.2 step 12). Under the schema's order a member the schema does not define is
not signed; step 12 does not consider such a member, so nothing a verifier states rests on one.
*Why two.* Identity Assertion 1.3 §8.2.1 states that the `signer_payload` "MUST be serialized as
described in Section 4.2.1, 'Core Deterministic Encoding,' of RFC 8949" and that "the resulting
byte string is presented to the credential holder for signature"; versions 1.0, 1.1 and 1.2
state the same in the same place, and on 11 November 2026 no later version or erratum had been
published. The validators deployed depart from that clause. The C2PA SDK through which the
ecosystem's tools validate reads an identity assertion into the schema's members and verifies
the signature over its own serialization of them, in the schema's order, whatever order the
store holds them in; its identity builder signs those bytes, so no assertion it writes verifies
over §8.2.1's encoding, and of an assertion signed as §8.2.1 states it reports
`cawg.x509.signature.mismatch`. A store this profile writes is to validate in those validators,
so the schema's order is what a writer signs by default; §8.2.1's encoding stays a form a writer
may ask for and a verifier accepts, for a validator that follows the clause. When the validators
deployed verify §8.2.1's bytes, the default follows them ([decisions](../decisions.md) D6); §9.3
pins what such a validator reports of each form.
### 4.11 Identity assertions in a manifest a C2PA SDK lays out
A WAI tool that signs an asset through a C2PA SDK, as [energy-binding](energy-binding.md) §6 has
one emit a manifest, does not lay the manifest out: the SDK draws its identifier, salts each
assertion, writes the claim and fixes the hard binding last. Such a manifest is not a store of
§4.1 and is not verified by §6.2. The identity assertion it carries is §4.10's all the same: the
same map, the same `signer-payload-map`, the same `COSE_Sign1_Tagged` over the same bytes of the
payload (the schema's order, unless the writer is asked for the core deterministic encoding),
written by the same writer and held to §6.2 step 12's checks by the same
verifier. Three things are the path's and none is the assertion's form.
**The placeholder.** With a data hash the file's layout is fixed before the hard binding's hash
is known, and the layout needs the assertion's size, which is not known until its holder has
signed. The writer reserves room for the assertion before the layout (Identity Assertion 1.3
§6.3) and, once the holder has signed, writes `pad1` as the zero bytes that bring the assertion's
encoding to exactly the room reserved (§6.2 there). A byte string of 256 to 65 535 bytes has a
head of three bytes, so every room that leaves `pad1` a length in that range is filled by `pad1`
alone; the writer reserves such a room and writes no `pad2`. One that would leave `pad1` another
length writes nothing.
**The referenced assertions.** The holder signs the hashed URIs the claim holds when the SDK asks
for the assertion's content, each `url` and `hash` as the claim writes it: the hard binding first,
which is the claim's one assertion whose label begins `c2pa.hash.`, then the assertions the
holder chose, in its order. A label the claim does not list, a label named twice, or the label
of an identity assertion stops the signing. An identity assertion on this path references no
other identity assertion: the other's content is still its placeholder when the payload is
signed, so its hash in the payload would name bytes the manifest does not hold.
**The claim's list.** The SDK lists an assertion its signer supplies while signing in the claim's
`gathered_assertions`, where a store of §4.1 lists every assertion in `created_assertions`. The
identity assertion is the named actor's statement and not the claim signer's, and which list
names it changes nothing step 12 checks.
**Verification.** A verifier holds the asset, its manifest store, the identity trust anchors it
accepts and *T*. A C2PA validator first validates the manifest against the asset (C2PA 2.4 §15:
the claim signature, the hashed URIs and the hard binding); this section does not repeat that.
The verifier then reads the store itself, in this order:
1. The store is one JUMBF superbox labelled `c2pa` of one or more manifests, and its last
manifest, the active one, holds one assertion store and one claim labelled `c2pa.claim.v2`,
a CBOR map with `created_assertions`, an array of hashed URIs (`url`, a text string, `hash`,
32 bytes, and `alg`, where present, `sha256`), and `gathered_assertions`, where present,
another: otherwise `manifest_malformed`. The claim is read in any well-formed definite-length
encoding with no map key twice, since an SDK's claim is not in the core deterministic one.
2. Every hashed URI of the two lists names, under one prefix that ends `c2pa.assertions/`,
exactly one assertion of that assertion store (`manifest_malformed`) whose superbox, less its
8-byte header, has the SHA-256 the URI states: otherwise `assertion_hash_mismatch`.
3. `created_assertions` names exactly one hard binding: otherwise `manifest_malformed`.
4. Each assertion labelled `cawg.identity`, `cawg.identity__1`, …, in the assertion store's
order, is named by one of the two lists, and no other assertion's label begins
`cawg.identity`: otherwise `manifest_malformed`. Its content is one CBOR box, read as the
claim is: otherwise `identity_malformed`. A writer of this section writes it in the core
deterministic encoding; an SDK's own identity builder writes its members in the schema's
order, with a `pad2`, and its assertion is read and verified all the same. It then passes
§6.2 step 12's checks
in their order, with their reasons and codes (§7.2), the hard binding being step 3's and the
claim's assertions being those of `created_assertions`.
A verifier MAY be configured to look for a referenced assertion in `gathered_assertions` as well,
as Identity Assertion 1.3 §7.1 admits; it states that it did. §6.2 step 12 holds a store of
§4.1, which has no second list, to `created_assertions`, and that is this section's rule unless
the verifier is configured otherwise. It never looks in an ingredient's manifest.
It states, for each identity assertion, its label, its roles, the named actor (the SHA-256 of its
end-entity certificate), the labels of the assertions it references, the SHA-256 of the anchor
its chain reached, and which of §4.10's two serializations its signature is over. It establishes what §10 says of an identity assertion and nothing about the
claim signer, which the C2PA validator reports.
## 5. Carriage
### 5.1 HTTP
An origin that serves an envelope at `<cid>.wai` ([http-delivery](http-delivery.md) §5.2) serves
its publisher's store, where it has one, at the same path with the filename extension `.c2pa`:
`<cid>.c2pa`, where §15.5.3.1 has a validator look for one. It is served:
```
Content-Type: application/c2pa (§11.4)
Cache-Control: max-age=300, no-transform
Access-Control-Allow-Origin: *
```
and not as immutable, since a store may be replaced (§4.6). A response for the object MAY carry
`Link: <<cid>.c2pa>; rel="c2pa-manifest"` (RFC 8288; §15.5.3.2), which names the store for a
client that holds the object under an alias. A client reads at most 1 048 576 bytes of a store
and refuses a longer one (`manifest_malformed`): a store of this profile is three assertions and
a certificate chain, a few kilobytes. A name at which nothing is served leaves the object
without a store (`store_missing`).
The receipts the verification needs are where http-delivery §8 serves them: the group receipt at
`rcpt/<receipt>.json` (§8.1 there), `receipt` being the WAI assertion's, and the object receipt
in the object's receipt index, `<cid>.wai.receipts` (§8.2 there).
### 5.2 Other stores
Another party's store for the same envelope binds it in the same way wherever it is served, and
a verifier given it verifies it as §6 states. Each store is verified on its own; one store's
verdict says nothing of another's.
## 6. Verification
### 6.1 Inputs
A verifier holds: the trust anchors it accepts for `c2pa-kp-claimSigning` (§14.4.1), and the
trust lists of claim signers it is configured with (§6.4); the identity trust anchors it accepts
for `id-kp-documentSigning`, and the trust lists of identity credentials it is configured with
(§6.4); the instant *T*; the envelope; the store served beside it, or none; the group receipt it
was given for the store's `receipt`, or none; the envelope's object receipt under that group, or
none, each receipt in its JSON form (jwp-receipts §2.4.1, §2.4.2); and the robustness records and
claims it was given (robustness-evidence §9.1), in order. It verifies in the order below and stops
at the first refusal, which it reports with the reason and the code §7 gives it.
### 6.2 Steps
0. Every trust list the verifier is configured with is the bytes it pinned, reads, and is in force
at *T* (§6.4): otherwise `trust_list_mismatch`, `trust_list_malformed` or `trust_list_stale`.
The anchors each gives at *T* join the verifier's for their purpose.
1. The envelope reads as §3 has it: otherwise `envelope_malformed`. Its cid and capabilities
follow.
2. A store is held: otherwise `store_missing`. It is at most 1 048 576 bytes and reads as §4.1
lays it out, one standard manifest whose assertions are each a CBOR content superbox:
otherwise `manifest_malformed`.
3. Live-provenance §11.2 steps 1 to 6 over the store: its claim and signature's form
(`manifest_malformed`), the claim signer's certificates (`credential_invalid`), their chain to
a trust anchor (`credential_untrusted`), the claim signature (`claim_signature_mismatch`),
their validity at *T* (`credential_expired`), and the hashed URIs
(`assertion_hash_mismatch`).
4. An assertion labelled `science.transaction.wai.detection` is a detection verdict offered as
the store's provenance: `detection_as_provenance` ([detection](detection.md) §6). The
assertions are the three of §4.1, in that order, followed only by what §4.1 admits after them,
in its order, each readable as §4.3, §4.4, §4.5, §4.8 and §4.9 write it (a soft binding of
the algorithm `io.iscc.v0` the last of the soft bindings and read as §4.9 writes one, every
other as §4.8 writes one; an ISCC soft binding's value and an ISCC identifier's bytes each an
ISCC-SEQ, iscc §3.3): otherwise `manifest_malformed`.
5. The manifest's identifier is §4.2's for the WAI assertion's `contentHash` and `receipt` and
the claim signer's end-entity certificate: otherwise `manifest_id_mismatch`.
6. The hard binding's `hash` is the SHA-256 of the envelope: otherwise `hard_binding_mismatch`.
7. The WAI assertion's `contentHash` is the envelope's cid (`content_hash_mismatch`), and its
`capabilities` are the envelope's, in order (`capabilities_mismatch`).
8. A group receipt is held: otherwise `receipt_missing`. It reads as jwp-receipts §3 step 1
reads one, in the labelled payload (§4.4): otherwise `receipt_invalid`. Its link hash is the
WAI assertion's `receipt`: otherwise `receipt_mismatch`. It verifies under jwp-receipts §3
step 2: otherwise `receipt_invalid`. Its `signer_pubkey` is the WAI assertion's `publisher`:
otherwise `receipt_signer_mismatch`.
9. An object receipt is held, reads as jwp-receipts §2.4.2 has it, and passes jwp-receipts §3
steps 4 and 5 against the group receipt over the envelope: its `content_hash` is the cid, it
stands at its own position in the group, and its leaf walks to the group's root. Otherwise
`object_receipt_invalid`.
10. Each watermark's soft binding's value is the payload the scheme's detector (synthesis §7.5.5)
reads from the envelope's picture: otherwise, and for an envelope with no picture,
`soft_binding_mismatch`. Then the robustness records and claims the verifier was given verify
in order as robustness-evidence §9.2 has it, under the policy `stated`, and each of those
soft bindings' `robustnessClaim` is the link hash of a claim among them about the binding's
scheme: otherwise `soft_binding_unevidenced`. An ISCC soft binding whose `bindingMetadata`
names a `robustnessClaim` is `soft_binding_unevidenced` whatever the verifier was given: no
claim is about `io.iscc.v0` (§4.9).
11. The ISCC soft binding's units are the binding's units §4.9 computes from the envelope, each
equal and in its order, and the ISCC identifier's units are the ones §4.9 computes for it:
otherwise `iscc_mismatch`.
12. Each identity assertion, in order, by Identity Assertion 1.3 §7.1 and §8.2.2.1, its checks in
this order:
- it holds `signer_payload`, a map, `signature` and `pad1`, byte strings, and `pad2` a byte
string where present; its payload holds `referenced_assertions`, a non-empty array of
hashed URIs (each a map with `url`, a text string, `hash`, a byte string, and `alg`, a text
string, where present), `sig_type`, a non-empty text string, and `role` where present a
non-empty array of non-empty text strings; members neither table defines are not
considered (§7.1 there): otherwise `identity_malformed`;
- each referenced assertion is in the claim's `created_assertions` with the same `url` and
`hash` (and an `alg`, where given, of `sha256`): `identity_assertion_mismatch`; none is
named twice: `identity_assertion_duplicate`; the hard binding is among them:
`identity_hard_binding_missing`;
- `sig_type` is `cawg.x509.cose`: otherwise `identity_sig_type_unknown`; `pad1` and `pad2`
hold only zero bytes: otherwise `identity_pad_invalid`;
- the signature reads as a `COSE_Sign1_Tagged` whose protected header holds an `alg`:
otherwise `identity_malformed`; that `alg` is EdDSA: otherwise
`identity_algorithm_unsupported`; and it reads as §4.10 writes it, a detached payload, an
empty unprotected header and an `x5chain` in the protected one: otherwise
`identity_malformed`;
- the end-entity certificate and the intermediates meet §4.10's profile:
`identity_credential_invalid`; a chain leads from them to an identity trust anchor, as
live-provenance §11.2 step 3 builds one: `identity_credential_untrusted`; the signature
verifies under the end-entity's key, by the strict rule, over the payload's members in the
schema's order or over the payload's core deterministic encoding (§4.10), and over no
other bytes: `identity_signature_mismatch`; *T* is within the validity of every certificate of the
chain, the anchor's included: `identity_credential_expired`.
A binding every step accepts is accepted.
### 6.3 What an accepted binding states
- the envelope's form (`WAI1` or `WAI2`), its cid and its capabilities;
- the manifest's identifier, and the source type the claim signer states (§4.5), as its URI;
- the claim signer, as the SHA-256 of its end-entity certificate;
- the receipt: its link hash, its publisher key, its `track_alias` and `group_id`, and the
object receipt's `object_id`;
- where the store or the verifier has them: the trust lists of claim signers that held the
chain's anchor, each by its URI, its type, its sequence number as its `identifier` and its
digest (C2PA 2.5 §14.2.2's `trust-list-uri-map` and its digest); each watermark's soft
binding's algorithm, scheme, value and robustness claim; the ISCC soft binding's algorithm, its
sequence in its canonical form (iscc §3.2) and its generator's name and version; the ISCC
identifier, in its canonical form; and each identity assertion's label, roles, named actor (the SHA-256 of its
end-entity certificate), the labels of the assertions it references, which serialization of
its payload its signature is over (`schema` or `deterministic`, §4.10), and the identity trust
list that held its anchor, where a list did.
### 6.4 Trust lists
A verifier MAY hold the anchors of a purpose (`c2pa-kp-claimSigning` for claim signers,
`id-kp-documentSigning` for identity credentials) as a **trust list**: a list of trusted entities
in the JSON form the C2PA Trust List is published in (ETSI TS 119 602's data model, the list's
`LoTE` object), configured by a URI, a type (`c2pa.trustList` for claim signers, C2PA 2.5
§14.2.2; `science.transaction.wai.identityTrustList`, an entity-specific type, for identity
credentials) and the **digest** of its bytes, the SHA-256 the verifier pinned when it chose the
list. At *T* (§6.2 step 0):
1. the bytes it holds are the pinned ones: otherwise `trust_list_mismatch`. A list is used as
the bytes that were chosen, never as whatever a URI serves at *T*: an anchor added to a list
after it was pinned is in other bytes;
2. it reads: a JSON object whose `LoTE` holds `ListAndSchemeInformation`, with an integer
`LoTESequenceNumber` of at least 1 (§6.3.2 there) and the date-times `ListIssueDateTime` and
`NextUpdate` (§6.3.14, §6.3.15), and `TrustedEntitiesList`, an array of trusted entities, each
with `TrustedEntityServices`, an array of services, each with `ServiceInformation` holding
`ServiceDigitalIdentity.X509Certificates`, an array of `{"val": B}`, `B` a certificate's DER in
base64 with padding (RFC 4648 §4), `ServiceStatus`, a URI, and `StatusStartingTime`, a
date-time (§6.6.3 to §6.6.5); every other member is left as it is: otherwise
`trust_list_malformed`;
3. *T* is from its `ListIssueDateTime` to its `NextUpdate`: otherwise `trust_list_stale`.
Its anchors at *T* are the certificates of each service whose `ServiceStatus` is
`http://c2pa.org/conformance/trust-list/trusted`, the status the C2PA Trust List gives an entity
it trusts, from a `StatusStartingTime` not after *T* (C2PA 2.5 §13.4.1's `notBefore`), that this
profile reads (live-provenance §4.4: an Ed25519 certificate). A certificate of another key type
anchors nothing for this profile and does not make the list malformed: the C2PA Trust List's
anchors are of other key types today, so a verifier of this profile pinned to it trusts none of
them, and a claim signer of this profile is trusted through a list that holds its anchor. A signer
whose chain leads to no anchor of its purpose, a withdrawn entity's or one whose trust has not yet
started among them, is `credential_untrusted` (C2PA 2.5 §14.7's `signingCredential.untrusted`), and
an identity credential's `identity_credential_untrusted`.
A list is signed by its operator (§6.8 there); this profile does not read the signature, and the
pinned digest is what binds the verifier to the list it chose. A verifier that updates its list
pins the new one's digest, and reports the sequence number its verdicts were made under.
## 7. Reasons
### 7.1 The envelope, the store and the receipts
| reason | raised at | C2PA 2.4 §15 code |
|---|---|---|
| `envelope_malformed` | §6.2 step 1 | — |
| `store_missing` | §6.2 step 2 | — |
| `manifest_malformed` | §6.2 steps 2, 3, 4 | `claim.malformed` |
| `credential_invalid` | §6.2 step 3 | `signingCredential.invalid` |
| `credential_untrusted` | §6.2 step 3 | `signingCredential.untrusted` |
| `claim_signature_mismatch` | §6.2 step 3 | `claimSignature.mismatch` |
| `credential_expired` | §6.2 step 3 | `claimSignature.outsideValidity` |
| `assertion_hash_mismatch` | §6.2 step 3 | `assertion.hashedURI.mismatch` |
| `manifest_id_mismatch` | §6.2 step 5 | — |
| `hard_binding_mismatch` | §6.2 step 6 | `assertion.dataHash.mismatch` |
| `content_hash_mismatch` | §6.2 step 7 | — |
| `capabilities_mismatch` | §6.2 step 7 | — |
| `receipt_missing` | §6.2 step 8 | — |
| `receipt_invalid` | §6.2 step 8 | — |
| `receipt_mismatch` | §6.2 step 8 | — |
| `receipt_signer_mismatch` | §6.2 step 8 | — |
| `object_receipt_invalid` | §6.2 step 9 | — |
C2PA 2.4 gives no code for an asset at which no store is located (§15.5.3), so `store_missing`
has none.
### 7.2 Trust lists, soft bindings, the identifier and identities
The code is the Identity Assertion's (1.3 §7.2.2, §8.2.2.4) for an identity assertion's refusal;
C2PA 2.5 gives none for the others, which are this profile's checks. A signer outside every
trust list it was configured with is §7.1's `credential_untrusted`.
| reason | raised at | code |
|---|---|---|
| `trust_list_mismatch` | §6.2 step 0, §6.4 | — |
| `trust_list_malformed` | §6.2 step 0, §6.4 | — |
| `trust_list_stale` | §6.2 step 0, §6.4 | — |
| `detection_as_provenance` | §6.2 step 4 | — |
| `soft_binding_mismatch` | §6.2 step 10 | — |
| `soft_binding_unevidenced` | §6.2 step 10 | — |
| `iscc_mismatch` | §6.2 step 11 | — |
| `identity_malformed` | §6.2 step 12 | `cawg.identity.cbor.invalid` |
| `identity_assertion_mismatch` | §6.2 step 12 | `cawg.identity.assertion.mismatch` |
| `identity_assertion_duplicate` | §6.2 step 12 | `cawg.identity.assertion.duplicate` |
| `identity_hard_binding_missing` | §6.2 step 12 | `cawg.identity.hard_binding_missing` |
| `identity_sig_type_unknown` | §6.2 step 12 | `cawg.identity.sig_type.unknown` |
| `identity_pad_invalid` | §6.2 step 12 | `cawg.identity.pad.invalid` |
| `identity_algorithm_unsupported` | §6.2 step 12 | `cawg.x509.algorithm.unsupported` |
| `identity_credential_invalid` | §6.2 step 12 | `cawg.x509.credential.untrusted` |
| `identity_credential_untrusted` | §6.2 step 12 | `cawg.x509.credential.untrusted` |
| `identity_signature_mismatch` | §6.2 step 12 | `cawg.x509.signature.mismatch` |
| `identity_credential_expired` | §6.2 step 12 | `cawg.x509.signature.outside_validity` |
The Identity Assertion has no code for a credential outside its profile; its trust model accepts
a certificate for `id-kp-documentSigning` (§8.2.4 there), so one without that purpose is not
trusted, and `identity_credential_invalid` reports `cawg.x509.credential.untrusted`. An algorithm
this profile does not verify is reported as the Identity Assertion reports one off C2PA's lists;
EdDSA is the only one this profile writes or reads.
## 8. Writing a store
A writer holds the envelope, the group receipt it was delivered under, the source type, and the
claim signer's key and certificates. It **MUST** write the store §4 lays out over the envelope as
§3 reads it, and **MUST NOT** write one for an envelope that does not read, for a group receipt in
the legacy payload, or with a source type that is not a term (§4.5). Every signature is Ed25519
and every encoding deterministic, so one writer given the same inputs writes the same store byte
for byte. The reference writer is `c2pa_envelope::sign`.
A writer that adds §4.8 to §4.10's assertions holds, beside those: for each soft binding, the
payload it marked into the envelope's picture and the link hash of the robustness claim that
states what the mark survives; and for each identity assertion, the credential holder's signature
over the payload it was shown, in the schema's order unless the holder is asked for the core
deterministic encoding (§4.10), with the labels it chose and the roles it states. It **MUST**
write a watermark's soft binding only with the payload the picture carries, the ISCC soft binding
and the ISCC identifier only as §4.9 computes them from the envelope it binds, the binding with
the name and the version of the software that computed its units, and an identity assertion only over assertions written before it, the hard binding
among them. The reference writer is `c2pa_envelope::sign_with`.
A writer that signs through a C2PA SDK (§4.11) holds the credential holder's key and
certificates, the labels it chose and the roles it states. It **MUST** reserve the assertion's
room before the SDK fixes the hard binding, **MUST** write the assertion §4.10 writes with
`pad1` filling that room and no `pad2`, and **MUST NOT** write one that references an assertion
the claim does not list or another identity assertion. The reference writer is
`c2pa_emit::identity::IdentitySigner`, a claim signer that hands the SDK the assertions to place;
the reference verifier is `c2pa_emit::identity::verify_store`.
## 9. Conformance
### 9.1 The envelopes and the receipts
[`live-conformance/c2pa-envelope/`](../live-conformance/c2pa-envelope/) holds the corpus, written
from fixed seeds by `wai_c2pa_envelope_vectors gen`: `vectors.json`, the trust anchor, and for
each vector its envelope, its store and its receipts, each a file of its own, the good envelopes
and their stores named as an origin serves them (`objects/<cid>.wai` and `objects/<cid>.c2pa`).
Every key and certificate is derived from the seeds, and none is a key anyone holds. The
envelopes carry pictures the generator draws: a `WAI1` of one `wai.image.png`, and a `WAI2` of the
same picture at two sizes, each entry with its `id` and `sha256`. Both are objects of one labelled
group receipt.
- **Good bindings**: a store bound to the `WAI1`, and one bound to the `WAI2`.
- **Negatives**, each a control of one defect: a changed byte of the `WAI1` and of the `WAI2`'s
second rendition; an envelope truncated by one byte and one with a byte after it; the store
stripped, and truncated by one byte; the `WAI1`'s store beside the `WAI2`; a store without the
WAI assertion, one with its actions and WAI assertion in each other's place, one whose data
hash excludes four bytes of the envelope, and one whose WAI assertion has a key §4.4 does not
define; an end-entity certificate valid for time-stamping alone, one under an anchor the
verifier does not hold, and one that has expired; a changed byte of the claim signature and of
the WAI assertion; an identifier that is not derived; a WAI assertion that names another
envelope's cid, and one that names other capabilities; the group receipt withheld, swapped for
the group after it, forged, and in the legacy payload; a WAI assertion that names another
publisher; and the object receipt of another object of the group, withheld, and with a changed
proof. Every reason of §7.1 refuses one.
A conforming verifier gives every vector the outcome, the reason, the C2PA code and, for an
accepted binding, what §6.3 states, as the corpus states them. `wai_c2pa_envelope_vectors
verify` and the test `tests/c2pa_envelope.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
§7.1 refuses a vector, and that §7 lists exactly the reference's codes with the code it reports. `live-conformance/c2pa_live_verify.py` is the second implementation, written from this
text: it shares no code with `wai-rs`; it reads the envelopes, CBOR, COSE, JUMBF 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; mutates copies of the good vectors, one defect each, and
requires every copy to be refused; and drops each of its own checks in turn and requires the
corpus to catch it. The corpus's README records what an independent C2PA validator reports for
its stores.
### 9.2 Trust lists, soft bindings, the identifier and identities
[`live-conformance/c2pa-identity/`](../live-conformance/c2pa-identity/) holds the corpus of §4.8 to
§4.10 and §6.4, written from fixed seeds by `wai_c2pa_identity_vectors gen`: `vectors.json`; the
trust lists, each a file in the C2PA Trust List's form, which each vector names with the URI, the
type and the digest its verifier pins; the robustness record and claims; and for each vector its
envelope, its store and its receipts. Every key and certificate is derived from the seeds, under
four test anchors of the corpus's own (claim signers, another of claim signers, identity
credentials, another of identity credentials), and none is a key anyone holds. The envelopes are
three `WAI1`s of one 128 × 128 picture each, drawn by the generator, the first two marked with
`wai-mark:tent64`, all three objects of one labelled group receipt, and a fourth of a picture of
one grey, the object of the group after it. The robustness record measures
`wai-mark:tent64` over the first two pictures under five attacks, its table the harness's
(robustness-evidence §6) and so reproducible by anyone who holds the pictures; the claim is every
attack the record shows survived.
- **Good bindings**: a store with a soft binding, the ISCC identifier and the creator's identity
assertion; one with two identity assertions, the publisher's referencing the creator's; one
with an identity assertion alone, over the unmarked picture; the same with the identity
assertion signed over the core deterministic encoding; one with a watermark's soft
binding, the ISCC soft binding, the ISCC identifier and the creator's identity assertion over
both soft bindings; one with the ISCC soft binding alone; and one over the picture of one grey,
whose ISCC soft binding holds no Image-Code and whose identifier holds the featureless one.
- **Negatives**, each a control of one defect: a trust list with an anchor added after it was
pinned, one past its next update, and one without a sequence number; a claim signer under an
anchor no list holds, one under an anchor its list withdrew, and one whose anchor's trust has
not yet started; a store holding a detection verdict; a soft binding of the other picture's
mark, one over the unmarked picture, one whose claim names an attack no record measured, and
one whose claim the verifier was not given; an ISCC identifier whose Instance-Code was computed
over altered bytes, and one whose Image-Code is the other picture's; an ISCC soft binding
computed from another envelope, one whose Data-Code is the envelope's and whose Instance-Code
is that of the envelope with one bit changed (a candidate match of iscc §5, and refused), one
without its Instance-Code, one of 64-bit units, one holding a featureless Image-Code, one whose
sequence is cut by a byte, one whose value is the sequence's text, one without its generator,
a second one, one written before the watermark's, and one that names the watermark's
robustness claim as its own; and an identity assertion
carried from another store, one whose certificates are another actor's under the first actor's
signature, one signed over the payload's members in a third order, one signed over a payload
with another role than the one it holds, one under an anchor no list holds, one whose credential expired, one credentialed
for claim signing alone, one with a non-zero pad, one of an unknown signature type, one naming
an assertion twice, one without the hard binding, one without `pad1`, and one signed under an
algorithm C2PA does not admit for a claim. Every reason of §7.2 refuses one.
A conforming verifier gives every vector the outcome, the reason, the code and, for an accepted
binding, what §6.3 states, as the corpus states them. `wai_c2pa_identity_vectors verify` and the
test `tests/c2pa_identity.rs` check that every file regenerates byte for byte, that no other file
is there, that `wai-rs` gives every vector its outcome, and that every reason of §7.2 refuses a
vector. `c2pa_live_verify.py` verifies this corpus too, from this text: it reads the trust lists
and the identity assertions itself, detects the mark with the synthesis corpus's second
implementation (`_wai_mark.py`), verifies the robustness evidence with robustness-evidence's
(`robustness_verify.py`) and recomputes the ISCC units with iscc's (`iscc_verify.py`); it gives
every vector the same outcome, requires every mutated copy of a good vector to be refused, and
drops each of its own checks in turn and requires the corpus to catch it.
### 9.3 Identity assertions in manifests a C2PA SDK signed
[`live-conformance/c2pa-sdk-identity/`](../live-conformance/c2pa-sdk-identity/) holds the corpus
of §4.11: `vectors.json` and, for each vector, a PNG a C2PA SDK signed with the manifest
energy-binding §6 emits, one assertion more left in `gathered_assertions`, and the vector's
identity assertions. Every key and certificate is derived from fixed seeds, and none is a key
anyone holds. The SDK draws a manifest's identifier, a salt for each assertion and an
ingredient's instance identifier at random, so its output is not a function of its inputs and
this corpus does not regenerate byte for byte: its files were written once by
`wai_c2pa_sdk_identity_vectors gen`, and `vectors.json` pins each by its SHA-256.
- **Good**: the creator's identity assertion over the hard binding, the actions and the energy
binding, with a role; two identity assertions, the creator's and the publisher's; one over the
hard binding alone, with no role; the creator's signed over the core deterministic encoding;
the creator's written and signed by the SDK's own identity builder and not by this profile's
writer; and one over a gathered assertion, read by a verifier configured to admit
`gathered_assertions`.
- **Negatives**, each a manifest the SDK signed around an identity assertion with one defect: an
identity assertion signed for another manifest and carried into this one; one over a gathered
assertion, held to `created_assertions`; one without the hard binding; one naming an
assertion twice; one whose `pad1` ends in a non-zero byte; one without `pad1`; one of an
unknown signature type; one signed under an algorithm this profile does not verify; the
creator's certificates over another key's signature; the creator's signature over the
payload's members in a third order, which is neither serialization of §4.10; the creator's
signature over a payload with another role than the one the assertion holds; a credential
under an anchor the verifier does not hold; an expired one; and one credentialed for claim
signing alone. Every reason §7.2 gives an identity assertion refuses one.
A conforming verifier gives every vector the outcome, the reason, the code and, for an accepted
manifest, what §4.11 states, as the corpus states them. `wai_c2pa_sdk_identity_vectors verify`
and the tests of `c2pa_emit::identity_corpus` check that every file is the one pinned and no
other is there, that the SDK validates every file's claim signature and hard binding, that
`wai-rs` gives every vector its outcome, that every reason refuses one, and that manifests
signed again, which differ from the pinned ones in their random members, get the same outcomes.
`live-conformance/c2pa_sdk_identity_verify.py` is the second implementation: it reads the PNG,
the store and the claim itself, verifies each identity assertion with the verifier
`c2pa_live_verify.py` holds WAI's own stores to, gives every vector the same outcome, requires
every mutated copy of a good vector to be refused, and drops each of its checks in turn and
requires a vector or a mutated copy to catch it; among the checks dropped are each of the two
serializations a signature may be over, and the rule that it is over no other.
**The SDK's validator on what this profile writes.** `vectors.json` names the SDK and its
version and pins, for every vector, what that SDK's validator reports of the file when it is
configured with the corpus's claim anchor and identity anchor: its validation state and its
failure codes. The tool and the tests hold the validator to those pins, and to this:
- of every manifest this profile accepts whose identity assertions are signed over the schema's
order, which is every manifest a writer of §4.10 writes by default, the validator's state is
trusted, it reports no failure code, and for each identity assertion it reports
`cawg.x509.credential.trusted`, `cawg.x509.signature.validated` and
`cawg.identity.well-formed`;
- the same holds of WAI's own stores (§4.1) given to that validator beside their envelopes: the
test `the_sdks_validator_accepts_the_stores_the_store_writer_writes` runs it over every good
store of §9.2's corpus that carries an identity assertion;
- of an identity assertion signed over the core deterministic encoding the validator reports
`cawg.x509.signature.mismatch` and nothing else: its credential is trusted and only the bytes
verified differ. The test
`the_sdks_validator_departs_from_8_2_1_and_reports_a_mismatch_of_its_bytes` pins that
departure from Identity Assertion 1.3 §8.2.1 at the SDK's version, and fails when a validator
verifies other bytes than that one does; §4.10's default and its paragraph are then
reconsidered ([decisions](../decisions.md) D6);
- an identity assertion the SDK's own builder wrote is accepted by this profile's verifier, its
signature over the schema's order.
The corpus's README records what the validator reports of each negative.
## 10. What an accepted binding establishes
That the claim signer, whose certificate chains to an anchor the verifier holds, signed a C2PA
manifest over exactly these envelope bytes; that the same signer named the receipt the envelope
was delivered under and its publisher key; and that the publisher key signed a group that holds
the envelope at its stated position. It does not establish that the claim signer holds the
publisher key: the two are tied by the claim signer's statement, and a verifier reports both
(§6.3). It does not establish the source type, which the claim signer states (§4.5), nor
anything about a store other than the one verified (§5.2).
Where the store carries them: that the envelope's picture carries each soft binding's mark, and
that the robustness claim each names is held to the records it cites, which says what the mark
survived under the battery measured and no more (robustness-evidence §14); that the ISCC soft
binding and the ISCC identifier are the envelope's, computed again from its bytes and its
picture, which says nothing of what the codes survive and nothing of the generator the binding
names, which the claim signer states; and that each named actor, whose credential chains to an identity
anchor the verifier holds, signed the assertions it references, the hard binding among them, and
so these envelope bytes. An identity assertion establishes the actor's signature over those
assertions, not the roles it states, and not the assertions it does not reference: a claim
generator can sign the actor's assertion into a manifest of other assertions (Identity Assertion
1.3 §9.4.7), and a verifier reports which assertions each actor referenced. A trust list
establishes which anchors the verifier chose, by the digest it pinned; it does not establish that
the list's operator vets anyone in particular.