Release 4.5.6
C2PA trust anchor validation for c2pa-python >= 0.29.0
Background
c2pa-python==0.29.0 (released March 17 2026) changed c2pa-rs so that trust anchor checking is always enforced by default. Any certificate not chaining to a root CA in c2pa-rs's built-in store fires signingCredential.untrusted and hard-fails validation.
We had pinned c2pa-python>=0.25.0,<0.29.0 as a short-term workaround. This restores previous behavior but leaves a cert-forgery gap: our code only string-matched on the issuer field, which an attacker could fake with a self-signed cert set to "Stability AI Ltd".
What this PR does
1. Extracts and saves trust anchor PEM files
Scanned miner-submitted media in /workspace/.cache/sn34 (88k entries, source_type = 'miner') to find one sample file per AI provider. Extracted the full DER certificate chains embedded in each file's C2PA JUMBF box and saved them to gas/verification/trust_anchors/.
All major providers turned out to use private or non-CAI root CAs that are not in c2pa-rs's built-in store:
| Provider | Cert chain root | File |
|---|---|---|
| Stability AI | GlobalSign Root CA - R6 (via GlobalSign GCC R6 SMIME CA 2023) | stability_ai.pem |
| Runway (pre-Gen4) | GlobalSign Root CA - R6 (same intermediate) | runway.pem |
| Runway Gen4 | CN=Stability AI, O=Stability AI, C=US (private self-signed) |
runway_gen4.pem |
| Black Forest Labs | GlobalSign Root CA - R6 (same intermediate) | black_forest_labs.pem |
| Microsoft | CN=Microsoft Supply Chain RSA Root CA 2022 (private self-signed) |
microsoft.pem |
| OpenAI (via Truepic) | CN=Truepic WebClaimSigningCA chain |
openai_truepic.pem |
| Adobe | CN=Adobe Product Services G4 chain |
adobe.pem |
CN=Google C2PA Root CA G3 (fetched from pki.goog/c2pa/root-g3.crt) |
google_c2pa.pem |
GlobalSign Root CA - R6 was sourced from the certifi bundle. Google's C2PA Root CA G3 was fetched directly from Google's PKI at http://pki.goog/c2pa/root-g3.crt.
2. Updates c2pa_verification.py
Three additions at the top of the file, zero changes to existing verification logic:
_load_trust_anchors()— concatenates all PEM files at import time into a single string_TRUST_ANCHORS_PEM/_C2PA_HAS_CONTEXT_API— cached module-level constants (no per-call file I/O)_open_c2pa_reader(file_path)— transparent wrapper aroundc2pa.Reader:- On >= 0.29.0: builds a
Settingsobject withuser_anchors(additive — preserves the built-in store) and wraps the reader in aContext - On < 0.29.0: returns a bare
c2pa.Reader, identical to previous behaviour
- On >= 0.29.0: builds a
The one-line change in verify_c2pa():
# before
with c2pa.Reader(file_path) as reader:
# after
with _open_c2pa_reader(file_path) as reader:3. Bumps the version pin
# pyproject.toml
- "c2pa-python>=0.25.0",
+ "c2pa-python>=0.29.0",Verification
Ran gascli g verify-c2pa against one sample file per provider on c2pa-python 0.29.0:
Adobe (Adobe Inc.)
Black Forest Labs (Black Forest Labs Inc.)
Runway Gen4 (Runway — private Stability AI root)
Google (Google LLC — Google C2PA Root CA G3)
Google (Google C2PA Core Generator Library)
Microsoft (Microsoft Corporation — video)
Microsoft (Microsoft Corporation — image)
OpenAI (Sora / Truepic chain)
OpenAI (OpenAI-API)
Runway (RUNWAY AI, INC. — pre-Gen4)
One Stability AI sample showed assertion.dataHash.mismatch — the cached file was modified after signing, which is correct behaviour (hash mismatch ≠ trust failure).
Security improvement
With c2pa-python>=0.25.0,<0.29.0, c2pa-rs skipped the trust chain check entirely. Our code fell back to string-matching on issuer, meaning an attacker could submit media signed with their own self-signed cert and set issuer = "Stability AI Ltd" to pass validation.
With this PR, c2pa-rs performs full cryptographic chain verification against the extracted root certs. Forging a cert that chains to CN=Stability AI, O=Stability AI (Runway Gen4's root) or GlobalSign Root CA - R6 requires breaking RSA-2048/PSS — the issuer string alone is no longer sufficient.