Some checks failed
WAI conformance / does this change touch it (push) Has been skipped
WAI conformance / cargo-class corpora · linux (push) Has been skipped
WAI conformance / cargo-class corpora · linux-arm64 (push) Has been skipped
WAI conformance / cargo-class corpora · macos (push) Has been skipped
WAI conformance / cargo-class corpora · windows (push) Has been skipped
WAI conformance / fast integer kernels, every x86_64 kernel set (AVX-512 silicon) (push) Has been skipped
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (push) Has been skipped
WAI conformance / faces from keypoints under wasm32-wasip2 (wasmtime) (push) Has been skipped
WAI conformance / v2 vocabulary · manifests validate as stated (push) Has been skipped
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (push) Has been skipped
WAI conformance / independent verifiers · every verifier step, each corpus's Rust side (push) Has been skipped
WAI conformance / each feature of the independent verifiers' steps compiles alone (push) Has been skipped
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (push) Has been skipped
WAI conformance / cross-arch byte-equality · linux (push) Has been skipped
WAI conformance / cross-arch byte-equality · linux-arm64 (push) Has been skipped
WAI conformance / cross-arch byte-equality · macos (push) Has been skipped
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (push) Has been skipped
WAI conformance / C2PA manifest emission · signed, read back, validated (push) Has been skipped
WAI conformance / independent verifiers · each needs= is what a run without packages shows (nightly) (push) Has been skipped
wai-audit collector and player integration / does this change touch it (push) Successful in 1m23s
wai-pack / wai-pack · does this change touch it (push) Successful in 1m26s
wai-fleet service / does this change touch it (push) Successful in 1m28s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 1m29s
WAI operation catalogue / does this change touch it (push) Successful in 1m29s
wai-quantum for JavaScript / does this change touch it (push) Successful in 1m28s
joule-moq-http live HTTP egress / joule-moq-http · does this change touch it (push) Successful in 1m34s
wai-ledger service / does this change touch it (push) Successful in 1m30s
wai-quantum studio and desktop / does this change touch it (push) Successful in 45s
wai-sink-swift native sink package / does this change touch it (push) Successful in 43s
wai-sink-android native sink library / does this change touch it (push) Successful in 45s
WAI Studio / does this change touch it (push) Successful in 43s
wai-app and Create / wai-app · does this change touch it (push) Successful in 5m4s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 2m10s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 5m35s
joule-moq-agent AI participants / joule-moq-agent · does this change touch it (push) Successful in 6m6s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 5m54s
wai-fleet service / service · macos (push) Successful in 42s
wai-pack / wai-pack · the floor through a media prober (push) Successful in 1m12s
wai-ledger service / service · macos (push) Successful in 1m24s
wai-pack / wai-pack · linux-arm64 (push) Successful in 4m46s
wai-pack / wai-pack · dependency licences (push) Successful in 31s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 5m25s
joule-moq-http live HTTP egress / joule-moq-http, every target (push) Successful in 4m11s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 3m2s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 11m35s
joule-moq-agent AI participants / joule-moq-agent, every target (push) Successful in 3m57s
wai-pack / wai-pack · macos (push) Successful in 10m52s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 14s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 3m34s
WAI examples / WAI examples · does this change touch them (push) Successful in 21s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 10m18s
WAI conformance / a push to main · corpus provenance and sidecars, signer ids, the registry export (push) Successful in 1m11s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 8m51s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 14m44s
wai-fleet service / service · linux (push) Successful in 4m14s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 52s
wai-pack / wai-pack · linux (push) Successful in 5m17s
wai-audit collector and player integration / collector · linux (push) Successful in 6m1s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 20m38s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 17m33s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 2m15s
wai-ledger service / service · linux (push) Successful in 3m58s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 15m28s
wai-pack / wai-pack · independent verifier (Python) (push) Successful in 25m9s
wai-quantum for JavaScript / javascript · linux (push) Successful in 9m42s
wai-quantum studio and desktop / studio · browser (push) Successful in 9m44s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 13m26s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 12m14s
WAI examples / WAI examples · android-app · linux-arm64 (push) Successful in 13m17s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (push) Successful in 18m24s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 4m23s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 37m2s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 12m43s
WAI examples / WAI examples · portable · linux (push) Successful in 18m37s
wai-quantum studio and desktop / desktop app · windows (push) Successful in 12m41s
WAI Studio / Studio in WebKit · linux (push) Failing after 2h30m4s
The 2026-11-14 wai-conformance nightly (run 35860) passed every job but
"codec wrappers" and "C2PA manifest emission", both compile errors after
526554191 moved jpegxl-rs 0.14->0.16 and c2pa 0.91->0.92 in paths no
per-push job builds:
- codecs::image::jxl_encode and tests/media_decode_bounds.rs: jpegxl-rs 0.16's
encode takes the pixel type alone and returns the bytes.
- c2pa_emit::identity's signer wrapper: c2pa 0.92's Signer passes a
&Context to send_timestamp_request; the wrapper passes it on.
- live-conformance/c2pa-sdk-identity signed again with 0.92.1, as its README
says it is when the lockfile moves the SDK: all 20 vectors keep 0.91.11's
validator reports, instant and anchors; SDK_VERSION, the README and D6 say
0.92.1. The 21 corpus records and SDK_VERSION are acknowledged breaks.
Checked on build hosts: the codec wrappers' five test binaries, the image
codec unit tests, the semantic path, wai_prov and clippy (macOS arm64);
the emitter, session and identity tests, the SDK identity vectors and
clippy (Linux aarch64); wai breaking: no unacknowledged break.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
118 lines
8.7 KiB
Markdown
118 lines
8.7 KiB
Markdown
# live-conformance/c2pa-sdk-identity — identity assertions in manifests a C2PA SDK signed
|
|
|
|
The corpus of [`extensions/c2pa-binding.md`](../../extensions/c2pa-binding.md) §9.3: PNG files a
|
|
C2PA SDK signed, each with the manifest [energy-binding](../../extensions/energy-binding.md) §6
|
|
emits and the Creator Assertions Working Group's identity assertions as §4.11 has a WAI tool
|
|
write them through the SDK: the assertion of §4.10, written by the writer of WAI's own stores, in
|
|
the placeholder the SDK reserves.
|
|
|
|
**Pinned, not regenerated.** The SDK draws a manifest's identifier (`urn:c2pa:` and a version 4
|
|
UUID), a 16-byte salt for every assertion and the parent ingredient's `instanceID` from the
|
|
system's random source. Every hash over an assertion, the actions' reference to the ingredient,
|
|
the identity assertion's payload and both signatures follow from those, so two signings of one
|
|
input differ in bytes and agree in length and in everything a verdict reads. The files here were
|
|
written by `wai_c2pa_sdk_identity_vectors gen` with the SDK at the version `wai-rs`'s lockfile
|
|
holds (the `c2pa` crate, version 0.92.1, which every claim names in its `claim_generator_info`),
|
|
and `vectors.json` pins each by its SHA-256. They are written again when the lockfile moves the
|
|
SDK, and only then: on 2026-11-12, from 0.91.7 to 0.91.11, and on 2026-11-14, from 0.91.11 to
|
|
0.92.1. Running `gen` again writes another
|
|
corpus with the same outcomes, and is not how this one is checked.
|
|
|
|
**Files.** `vectors.json` holds `time` (the instant *T*), `identityAnchors` (the identity trust
|
|
anchors, each a certificate's DER in base64), `claimAnchors` (the claim signer's anchor, which
|
|
only a C2PA validator needs), `sdk` (the SDK whose validator's reports are pinned, and its
|
|
version) and the vectors: each its `name`, `about`, `asset` (the signed PNG), `sha256` (its
|
|
pin), `referenced` (`created`, or `created_or_gathered` for a verifier configured to admit the
|
|
claim's gathered assertions), `expect`, and `sdk` (what that SDK's validator reports of the
|
|
file when it holds both anchors: its validation `state` and its `failure` codes, sorted). Every
|
|
key and certificate is derived from fixed seeds, and none is a key anyone holds. The claim
|
|
signer is an Ed25519 certificate for `c2pa-kp-claimSigning` and `id-kp-emailProtection` under
|
|
the corpus's claim anchor; a validator that does not hold that anchor reports
|
|
`signingCredential.untrusted` for every file, and one that does not hold the identity anchor
|
|
`cawg.x509.credential.untrusted` for every identity assertion.
|
|
|
|
**Equivalence.** A conforming verifier given a vector's asset, the anchors and *T* reaches the
|
|
outcome `expect` states (§4.11): for `refuse`, the `reason` of §7 and the code §7 gives it
|
|
(`c2pa`); for `accept`, each identity assertion's `label`, `roles`, `actor` (the SHA-256 of its
|
|
end-entity certificate), `referenced` (the labels of the assertions it references, in order),
|
|
`anchor` (the SHA-256 of the anchor its chain reached) and `signed` (which serialization of its
|
|
payload the signature is over: `schema`, the members in the schema's order, which a writer
|
|
signs by default, or `deterministic`, the core deterministic encoding; §4.10).
|
|
|
|
**The vectors.**
|
|
|
|
- Good: `creator` (over the hard binding, the actions and the energy binding, role
|
|
`cawg.creator`), `creator-and-publisher` (two identity assertions, `cawg.identity` and
|
|
`cawg.identity__1`), `hard-binding-alone` (no role), `creator-deterministic` (the creator's
|
|
assertion signed over the core deterministic encoding, the form a writer may ask for),
|
|
`sdk-builder-written` (the creator's assertion written and signed by the SDK's own identity
|
|
builder, nothing of it this profile's: its members are in the schema's order in the store and
|
|
it holds a `pad2`), and `gathered-reference-admitted` (the file of `gathered-reference`, read
|
|
by a verifier that admits `gathered_assertions`).
|
|
- Refused, each a manifest the SDK signed around one defect: `carried-from-another-manifest`
|
|
and `gathered-reference` (`identity_assertion_mismatch`), `hard-binding-missing`,
|
|
`assertion-twice`, `pad-nonzero`, `pad1-missing` (`identity_malformed`), `sig-type-unknown`,
|
|
`algorithm-unsupported`, `signed-by-another-key`, `signed-third-order` (the creator's
|
|
signature over the payload's members in the order `sig_type`, `role`,
|
|
`referenced_assertions`, which is neither serialization) and `signed-other-members` (the
|
|
creator's signature over a payload with the publisher's role, beside a payload that states
|
|
the creator's) (`identity_signature_mismatch`, each), `credential-untrusted`, `credential-expired` and `credential-wrong-purpose`
|
|
(`identity_credential_invalid`). Every reason §7.2 gives an identity assertion refuses one.
|
|
|
|
**Checking it.**
|
|
|
|
```
|
|
cargo run --no-default-features --features c2pa_emit,c2pa_envelope --bin wai_c2pa_sdk_identity_vectors -- verify live-conformance/c2pa-sdk-identity
|
|
python3 -I live-conformance/c2pa_sdk_identity_verify.py live-conformance/c2pa-sdk-identity
|
|
```
|
|
|
|
The first holds every file to its pin and its outcome, has the SDK validate every file's claim
|
|
signature and hard binding (`claimSignature.validated`, `assertion.dataHash.match`, and no
|
|
failure of the claim, its hashed URIs or its data hash), holds the SDK's validator, configured
|
|
with the corpus's two anchors, to the report `vectors.json` pins for each file and to accepting
|
|
whole every manifest whose identity assertions are signed by default, and signs every vector
|
|
again to the same outcomes and the same reports. The second is the second implementation, written from the extension's text: 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, 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.
|
|
|
|
**What the SDK's own validator reports.** The SDK reads identity assertions itself. Of the
|
|
corpus's files, on 2026-11-14, at the version `vectors.json` names (0.91.11 reported the same of
|
|
every vector of the files it signed, on 2026-11-12, and 0.91.7 of its own on 2026-11-11), configured with the
|
|
corpus's claim anchor and identity anchor (its `manifest` and `cawg` trust kinds), as each
|
|
vector's `sdk` pins it:
|
|
|
|
- **the manifests this profile writes by default are accepted whole**: for `creator`,
|
|
`creator-and-publisher`, `hard-binding-alone` and `sdk-builder-written` its 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`;
|
|
- for `creator-deterministic` it reports `cawg.x509.signature.mismatch` and nothing else, with
|
|
the credential trusted. Identity Assertion 1.3 §8.2.1 has the holder sign the
|
|
`signer_payload` in RFC 8949 §4.2.1's core deterministic encoding, whose map keys are in the
|
|
bytewise order of their encodings (`role`, `sig_type`, `referenced_assertions`). The SDK does
|
|
not verify those bytes: it verifies a signature over its own serialization of the payload's
|
|
members in the order the schema lists them (`referenced_assertions`, `sig_type`, `role`),
|
|
and its builder signs the same. That is the clause the deployed validators depart from, and
|
|
it is why §4.10 signs the schema's order by default and keeps §8.2.1's encoding as a form a
|
|
writer may ask for ([decisions](../../decisions.md) D6). This profile's verifier accepts
|
|
both and states which;
|
|
- it reports the code this profile does for `carried-from-another-manifest`
|
|
(`cawg.identity.assertion.mismatch`), `hard-binding-missing`, `assertion-twice`,
|
|
`pad-nonzero`, `sig-type-unknown`, `credential-untrusted`, and for `signed-by-another-key`,
|
|
`signed-third-order` and `signed-other-members` (`cawg.x509.signature.mismatch`, each);
|
|
`cawg.x509.signature.outside_validity` with `cawg.x509.credential.untrusted` for
|
|
`credential-expired`; `assertion.required.missing` for `pad1-missing`; and
|
|
`cawg.x509.credential.invalid` for `algorithm-unsupported`;
|
|
- it reports no failure for `credential-wrong-purpose` (it accepts as an identity credential a
|
|
certificate whose only purpose is claim signing, which §4.10's profile refuses,
|
|
`identity_credential_invalid`) or for `gathered-reference` (it looks for a referenced
|
|
assertion in `gathered_assertions` as well, which this profile does only where its verifier
|
|
is configured to).
|
|
|
|
The tests `the_sdks_validator_accepts_the_identity_assertions_this_profile_writes`,
|
|
`the_sdks_validator_accepts_the_stores_the_store_writer_writes` (the stores of `c2pa-identity`,
|
|
beside their envelopes) and `the_sdks_validator_departs_from_8_2_1_and_reports_a_mismatch_of_its_bytes`
|
|
of `c2pa_emit::identity_corpus` hold the first two points, and every vector's `sdk` pin the
|
|
rest, so an SDK whose validator reports otherwise is noticed.
|