open-standards/wai/live-conformance/c2pa-sdk-identity
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Transaction Science f58b3b0bb1 c2pa-sdk-identity: signed again at the SDK the lockfile holds, c2pa 0.91.11
wai-conformance's C2PA job failed `the_sdk_version_pinned_is_the_lockfiles`
(run 30917): 55d4f1ca6 moved wai-rs's lockfile from the c2pa crate 0.91.7
to 0.91.11, and SDK_VERSION, the corpus's vectors.json and its README still
named 0.91.7. Every claim the SDK signs names its version in
claim_generator_info, so each of the corpus's files named 0.91.7 too.

Signed again with 0.91.11 by `wai_c2pa_sdk_identity_vectors gen`.
SDK_VERSION is 0.91.11. Old and new vectors.json differ only in sdk.version
and each vector's sha256 (all 20 new); every expect and every pinned report
of the SDK's validator is the same, and every PNG names 0.91.11 in its
claim. Before the regeneration, 0.91.11's validator already reported of
the files 0.91.7 signed exactly what was pinned (the committed-corpus test
and `verify` passed at 0.91.11 in CI and here). So 0.91.11 changed nothing
a verdict reads: D6's reading of the two releases holds (recorded there),
the validator accepts whole every default identity assertion of both
corpora, and reports exactly cawg.x509.signature.mismatch of the strict
form. c2pa-binding §9.3, the corpus's README and the conformance table say
the corpus is written with the SDK the lockfile holds, and again only when
that version moves.

Reproduced on an aarch64 build host at d0b303206: the job's identity step
failed only that test (13 passed, 1 failed). After, on a macOS arm64 build
host, every step of the C2PA job: no system crypto in the graph; c2pa_emit
9 passed; the session receipt 2 passed; c2pa_emit::identity 14 passed and
`verify` PASS on both counts; provenance builds without c2pa. Also
c2pa_sdk_identity_verify.py (57 mutated copies refused, 16 of 16 weakened
verifiers caught), clippy --all-targets --no-default-features --features
c2pa_emit,c2pa_envelope -D warnings clean, and `wai breaking` against
main with its 21 findings (the 20 files and SDK_VERSION) each acknowledged in the change note. Proved by
the next wai-conformance run on main (C2PA manifest emission).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 08:41:10 -05:00
..
algorithm-unsupported.png c2pa-sdk-identity: signed again at the SDK the lockfile holds, c2pa 0.91.11 2026-11-12 08:41:10 -05:00
assertion-twice.png c2pa-sdk-identity: signed again at the SDK the lockfile holds, c2pa 0.91.11 2026-11-12 08:41:10 -05:00
carried-from-another-manifest.png c2pa-sdk-identity: signed again at the SDK the lockfile holds, c2pa 0.91.11 2026-11-12 08:41:10 -05:00
creator-and-publisher.png c2pa-sdk-identity: signed again at the SDK the lockfile holds, c2pa 0.91.11 2026-11-12 08:41:10 -05:00
creator-deterministic.png c2pa-sdk-identity: signed again at the SDK the lockfile holds, c2pa 0.91.11 2026-11-12 08:41:10 -05:00
creator.png c2pa-sdk-identity: signed again at the SDK the lockfile holds, c2pa 0.91.11 2026-11-12 08:41:10 -05:00
credential-expired.png c2pa-sdk-identity: signed again at the SDK the lockfile holds, c2pa 0.91.11 2026-11-12 08:41:10 -05:00
credential-untrusted.png c2pa-sdk-identity: signed again at the SDK the lockfile holds, c2pa 0.91.11 2026-11-12 08:41:10 -05:00
credential-wrong-purpose.png c2pa-sdk-identity: signed again at the SDK the lockfile holds, c2pa 0.91.11 2026-11-12 08:41:10 -05:00
gathered-reference.png c2pa-sdk-identity: signed again at the SDK the lockfile holds, c2pa 0.91.11 2026-11-12 08:41:10 -05:00
hard-binding-alone.png c2pa-sdk-identity: signed again at the SDK the lockfile holds, c2pa 0.91.11 2026-11-12 08:41:10 -05:00
hard-binding-missing.png c2pa-sdk-identity: signed again at the SDK the lockfile holds, c2pa 0.91.11 2026-11-12 08:41:10 -05:00
pad-nonzero.png c2pa-sdk-identity: signed again at the SDK the lockfile holds, c2pa 0.91.11 2026-11-12 08:41:10 -05:00
pad1-missing.png c2pa-sdk-identity: signed again at the SDK the lockfile holds, c2pa 0.91.11 2026-11-12 08:41:10 -05:00
README.md c2pa-sdk-identity: signed again at the SDK the lockfile holds, c2pa 0.91.11 2026-11-12 08:41:10 -05:00
sdk-builder-written.png c2pa-sdk-identity: signed again at the SDK the lockfile holds, c2pa 0.91.11 2026-11-12 08:41:10 -05:00
sig-type-unknown.png c2pa-sdk-identity: signed again at the SDK the lockfile holds, c2pa 0.91.11 2026-11-12 08:41:10 -05:00
signed-by-another-key.png c2pa-sdk-identity: signed again at the SDK the lockfile holds, c2pa 0.91.11 2026-11-12 08:41:10 -05:00
signed-other-members.png c2pa-sdk-identity: signed again at the SDK the lockfile holds, c2pa 0.91.11 2026-11-12 08:41:10 -05:00
signed-third-order.png c2pa-sdk-identity: signed again at the SDK the lockfile holds, c2pa 0.91.11 2026-11-12 08:41:10 -05:00
vectors.json c2pa-sdk-identity: signed again at the SDK the lockfile holds, c2pa 0.91.11 2026-11-12 08:41:10 -05:00

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.91.11, 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. 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-12, at the version vectors.json names (0.91.7 reported the same of every vector of the files it signed, 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.