open-standards/wai/live-conformance/c2pa-sdk-identity/README.md
Transaction Science 59feeeff98
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
wai-rs: jxl_encode and the C2PA identity signer on jpegxl-rs 0.16 and c2pa 0.92, and the SDK identity corpus signed again at 0.92.1
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]>
2026-11-14 07:02:48 -05:00

8.7 KiB

live-conformance/c2pa-sdk-identity — identity assertions in manifests a C2PA SDK signed

The corpus of extensions/c2pa-binding.md §9.3: PNG files a C2PA SDK signed, each with the manifest energy-binding §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 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.