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]>
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, rolecawg.creator),creator-and-publisher(two identity assertions,cawg.identityandcawg.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 apad2), andgathered-reference-admitted(the file ofgathered-reference, read by a verifier that admitsgathered_assertions). - Refused, each a manifest the SDK signed around one defect:
carried-from-another-manifestandgathered-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 ordersig_type,role,referenced_assertions, which is neither serialization) andsigned-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-expiredandcredential-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-aloneandsdk-builder-writtenits state isTrusted, it reports no failure code, and for each identity assertion it reportscawg.x509.credential.trusted,cawg.x509.signature.validatedandcawg.identity.well-formed; - for
creator-deterministicit reportscawg.x509.signature.mismatchand nothing else, with the credential trusted. Identity Assertion 1.3 §8.2.1 has the holder sign thesigner_payloadin 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 forsigned-by-another-key,signed-third-orderandsigned-other-members(cawg.x509.signature.mismatch, each);cawg.x509.signature.outside_validitywithcawg.x509.credential.untrustedforcredential-expired;assertion.required.missingforpad1-missing; andcawg.x509.credential.invalidforalgorithm-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 forgathered-reference(it looks for a referenced assertion ingathered_assertionsas 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.