Repository navigation
Description
A certificate whose subject carries no organizationName produces manifests that c2pa reads as ValidationState::Invalid — every one of them, on every container. The build warns about that certificate, and what the warning describes is a cosmetic consequence: inspect_certificate (crates/asterism-infra/src/disclosure.rs:1104-1115) says "its subject names no organisation, and a validator that displays a signer's name reads that field". The measured consequence is that nothing the certificate signs validates at all.
Under Strictness::Strict the warning is promoted to a refusal, so a strict deployment is protected. Under Strictness::Permissive — which the module docs point a non-publishing installation at — the deployment is told something mild and ships unreadable manifests.
Why the attribute matters at all, which is the part worth writing down
It does not, to the specification. The C2PA Technical Specification 2.1 and 2.2 contain no occurrence of organizationName, commonName, Distinguished Name or 2.5.4.10; §14.5.1's certificate profile states shall requirements for the signature algorithm, key parameters, version, uniqueIDs, basic constraints, AKI/SKI, key usage and EKU, and says nothing about subject DN attributes. c2pa's own certificate_profile.rs checks none either.
The failure comes from somewhere else. crypto/cose/verifier.rs:159-166 in c2pa 0.90.12 fetches the subject's organisation after validator.validate(...) at line 156 has already verified the signature cryptographically, and turns its absence into MissingSigningCertificateChain. claim.rs:3087-3096 then folds every error out of verify_cose into a single code, so a display-metadata lookup that found nothing arrives as claimSignature.mismatch with the explanation "claim signature is not valid".
Upstream calls it a bug: contentauth/c2pa-rs#2262, open and labelled accepted, with #2497 as a duplicate and PR #2540 unmerged. The reporter's trigger is an individual creator's certificate, where the CA/Browser Forum's individual-validated S/MIME profile forbids an O=. So this is not only a fixture's problem: a real person signing with a certificate issued to a person rather than to an organisation hits it, and the C2PA Certificate Policy's own §7.1.2 requirement of C, O, CN applies to certificates the Conformance Program issues — not to every certificate a deployment might hold.
Measured
A standalone probe against c2pa =0.90.12 with the workspace's own feature selection (default-features = false, rust_native_crypto, file_io), varying one attribute:
| Certificate | File | validation_state |
Failures |
|---|---|---|---|
with O= |
untouched | Valid |
none; successes include claimSignature.validated |
without O= |
untouched | Invalid |
claimSignature.mismatch — "claim signature is not valid" |
Through this repository's own writer, both halves written, the same split holds on PNG, MP4 and MOV. The XMP half is not implicated: skipping it entirely leaves the result unchanged either way. Neither is the algorithm (es256 and es384 behave identically) nor the signature encoding (the DER-versus-P1363 path at crypto/cose/verifier.rs:87-99 raises signingCredential.invalid, which never appeared).
The test fixture generates exactly this certificate
self_signed_pair (crates/asterism-infra/src/disclosure.rs:2082-2119) sets is_ca, the key usage, the extended key usage and the AKI extension, and never pushes DnType::OrganizationName; rcgen's default subject is CN=rcgen self signed cert. So every signed artefact the suite produces carries the upstream bug, and a test that observes it reads as a statement about the SDK rather than about the fixture.
That is what the_sdk_reports_a_clean_signature_as_mismatched_whatever_the_trust_setting does today: its name and its comment say c2pa "calls its claim signature mismatched anyway" for a file "signed by this build and untouched since". Stated that generally it is false. The SDK does not do this to a clean signature; it does it to a certificate missing one attribute, and the fixture is where that attribute is missing.
What is not wrong
The write path. With an O=-carrying certificate, this repository's writer produces ValidationState::Valid with an empty failure list on PNG, MP4 and MOV, both halves written. Nothing about the manifest, the ordering of the two halves, or the hard binding is implicated, and README.md's statement that a self-issued certificate validates as signingCredential.untrusted while the validation state stays Valid is true — of a certificate that carries an organisation. It would be false if demonstrated with the fixture this repository currently ships, which is worth knowing before anyone reaches for that fixture to demonstrate it.
Candidates
Not decided here.
- patch — Rewrite the
inspect_certificatewarning to state the measured consequence rather than the cosmetic one, citing the upstream issue and the version it was measured against, and leave the strict/permissive split alone. - requirement — Give
self_signed_pairanorganizationName, so the suite signs against a certificate shaped like a real one, and pin theO=-less case as its own named test that says what it is about. The test named above is retargeted at what was actually established. - scope — Carry it as a known upstream limitation: track #2262 and #2540, change nothing, and document that a certificate without an organisation cannot produce a readable manifest with this SDK version.
The three are not exclusive, and the second is the one that decides whether anything else in the tree is reasoning from the fixture's behaviour as though it were the SDK's.
Verification
A certificate without an organizationName produces a manifest this build's own reader reports as broken on the integrity axis, or the deployment is refused before it signs — either is honest, and the present arrangement is neither. A certificate with one produces ValidationState::Valid with no failures, which is already true and should stay pinned.
Incidental, unverified
A record disclosing nothing (DisclosureRecord::for_asset alone) still gets a manifest written, and that manifest fails assertion.action.malformed — "first action must be created or opened" — even under a good certificate. Whether such a record reaches apply in production was not checked.
Activity
ynishi commentedon Sep 20, 2026
Upstream state, 2026-09-21
contentauth/c2pa-rs#2262 is closed, labelled
status: done. The repair merged on 2026-09-04 as contentauth/c2pa-rs#2540 (bc0194b, "fix: x.509 cert with missing org should not be a reporting failure").No published version carries it. crates.io's newest
c2pais 0.90.22; there is no 0.91.0 and no pre-release of one.git compare c2pa-v0.90.22...bc0194breportsdiverged, behind by 94, so the 0.90 line the pin resolves within does not contain the commit. The only release carrying it is the GitHub tagc2pa-rc-v0.91.0-rc.3, which is not on crates.io. So the merge is easy to mistake for this tree having the repair, and it does not — the characterisation test went on passing after 2026-09-04 for exactly that reason.What the repaired version answers
Measured by pointing the workspace dependency at
c2pa-rc-v0.91.0-rc.3(commit298f16e) and running this repository's own fixtures through its own writer, then restoring the pin. Nothing below was inferred from reading the SDK's source.issued_shaped_pair(organisation present)["signingCredential.untrusted"]throwaway_identity(no organisation)["signingCredential.untrusted"]The repair does not reclassify
claimSignature.mismatchinto some other code — it stops emitting it. The certificate carrying an organisation is unaffected. Sointegrity_ofinasterism-corereads the repaired version's output correctly with no change at all, and the only editwhat_the_sdk_reports_for_this_builds_own_signaturesneeds is dropping that one string from its second assertion.One consequence worth recording for the day the pin moves:
signer_namedstops carrying the question it exists for. Once the defect no longer produces the code, a mismatch under an unnamed signer is an ordinary forgery, and reporting it asMark::Undeterminedwould weaken the answer rather than protect it. Removing that input belongs to the pin move.What the move costs
Run against 0.91.0-rc.3: every member of the workspace compiled, all targets, with no error and no warning.
asterism-corepassed 630 of 630.asterism-infrapassed 660 of 661, and the single failure is the characterisation test named above, which exists to fail on exactly this.What it takes:
cargo update base64@0.23.0. 0.91 requiresbase64 ^0.23.1where the lockfile holds 0.23.0 throughrmcp, and without this resolution fails before anything compiles.just licences, for the lockfile rows that move (base64,c2pa,c2pa_cbor, and the newc2pa_raw_cryptoandpementries).Three things that look like costs are not. Stable rustc passed 0.91's 1.96 requirement long ago — a local toolchain sitting at 1.95 is the only reason it appeared.
SigningAlgmoving out ofc2pa::crypto::raw_signaturewas a call-site problem, fixed independently onfix/c2pa-callsite-hygieneby naming it from the crate root, which both versions carry. And the third is a behaviour improvement rather than a cost, below.A record that established no digital source type
asterism_disclosure_format::manifest::definitionwrites the actions assertion only when a digital source type was established, so such a record's manifest carries no action at all, and a C2PA claim has to open withc2pa.createdorc2pa.opened. Measured under both versions through the writer:Written, and the read-back failsassertion.action.malformed: first action must be created or opened. This build ships a manifest no validator accepts.Failedwith that same message, and the file is left untouched.The second is the better answer, so the pin move improves this path rather than regressing it. Whether the writer should instead decline before signing, with a skip reason of its own, is a separate question: the record may still disclose a system or a prompt, so
Skipped::NothingToDisclosewould be a false reason, and a new variant reaches the release row's SQL and the release view.What is still open, which is this issue
None of the above changes what a certificate without an organisation produces today. Under the pinned version its manifests still come back with
claimSignature.mismatch, and this issue's subject stands until the pin moves.What has been settled since the issue was written:
requirementcandidate is already done —issued_shaped_paircarries an organisation andwhat_the_sdk_reports_for_this_builds_own_signaturespins both certificates' answers.mainearlier:integrity_of'ssigner_namedreturnsMark::Undetermined, which says the question cannot be settled from the report alone. That is neither of the two and is the more honest of the three.scopecandidate's premise is wrong. It proposed documenting that a certificate without an organisation cannot produce a readable manifest with this SDK. After the repair it can: the validation state isValidand only the trust code remains.applyin production whenever a record establishes no digital source type.Separately corrected on
fix/c2pa-callsite-hygiene, because it is true regardless of which version is pinned: the organisation check read the first attribute of the subject and warned when its value was empty, wherec2pareads the last and fails when the value will not decode (crypto/cose/verifier.rs:158-165, unchanged between 0.90.12 and 0.90.22). A certificate carryingO=""was warned about and signs and reads back with nothing but the trust code; one carrying a non-UTF-8 organisation was passed in silence and is the value that fails; one carrying two was judged on the attributec2paignores.