8000 A certificate with no organisation signs manifests nothing can read, and the warning calls it cosmetic · Issue #179 · ynishi/asterism · GitHub
[go: up one dir, main page]

Skip to content

A certificate with no organisation signs manifests nothing can read, and the warning calls it cosmetic #179

Open

Description

@ynishi

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_certificate warning 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_pair an organizationName, so the suite signs against a certificate shaped like a real one, and pin the O=-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

  1. ynishi commented on Sep 20, 2026

    @ynishi
    OwnerAuthor

    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 c2pa is 0.90.22; there is no 0.91.0 and no pre-release of one. git compare c2pa-v0.90.22...bc0194b reports diverged, 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 tag c2pa-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 (commit 298f16e) 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.

    certificate failures issuer
    issued_shaped_pair (organisation present) ["signingCredential.untrusted"] named
    throwaway_identity (no organisation) ["signingCredential.untrusted"] none

    The repair does not reclassify claimSignature.mismatch into some other code — it stops emitting it. The certificate carrying an organisation is unaffected. So integrity_of in asterism-core reads the repaired version's output correctly with no change at all, and the only edit what_the_sdk_reports_for_this_builds_own_signatures needs is dropping that one string from its second assertion.

    One consequence worth recording for the day the pin moves: signer_named stops 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 as Mark::Undetermined would 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-core passed 630 of 630. asterism-infra passed 660 of 661, and the single failure is the characterisation test named above, which exists to fail on exactly this.

    What it takes:

    1. cargo update base64@0.23.0. 0.91 requires base64 ^0.23.1 where the lockfile holds 0.23.0 through rmcp, and without this resolution fails before anything compiles.
    2. Two test expectations, described below and in the measurement above.
    3. just licences, for the lockfile rows that move (base64, c2pa, c2pa_cbor, and the new c2pa_raw_crypto and pem entries).

    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. SigningAlg moving out of c2pa::crypto::raw_signature was a call-site problem, fixed independently on fix/c2pa-callsite-hygiene by 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::definition writes 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 with c2pa.created or c2pa.opened. Measured under both versions through the writer:

    • 0.90.12 signs it. The half reports Written, and the read-back fails assertion.action.malformed: first action must be created or opened. This build ships a manifest no validator accepts.
    • 0.91.0-rc.3 verifies as it signs and refuses. The half reports Failed with 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::NothingToDisclose would 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:

    • The requirement candidate is already done — issued_shaped_pair carries an organisation and what_the_sdk_reports_for_this_builds_own_signatures pins both certificates' answers.
    • The Verification clause asked for the defect to report as broken on the integrity axis or be refused before signing. The tree answers by a third route that reached main earlier: integrity_of's signer_named returns Mark::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.
    • The scope candidate'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 is Valid and only the trust code remains.
    • The incidental item at the end of the issue is answered above, and reaches apply in 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, where c2pa reads 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 carrying O="" 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 attribute c2pa ignores.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      0