Releases: richardwooding/c2pa
Release list
v0.5.0 — reject a signature that does not cover the claim
A security fix to Validate, and the test infrastructure that found it.
Fixed — a signature could cover bytes that were not the claim
verifyCOSE injected the claim box's bytes only when the COSE payload was detached, and never compared an attached payload against the claim box:
if msg.Payload == nil {
msg.Payload = m.claimBytes
}
// ...the else branch was uncheckedReal C2PA signers always detach the payload, so nothing legitimate reached the other branch. But a forged manifest could attach a payload it had signed and ship an entirely different c2pa.claim box. The signature verified over the attacker's bytes, while the claim that was parsed, reported through Info, and hash-checked came from the substituted box — yielding claimSignature.validated, signingCredential.trusted, and Valid == true for a manifest whose signature covered none of its stated provenance.
An attached payload is now required to equal the claim box; otherwise Validate records claimSignature.mismatch.
Impact. Only files that attach a COSE payload are affected, which real signers do not produce — so no legitimate asset changes verdict. If you consume Validate for trust decisions, take this release.
Added — a generated test corpus
The suite previously had five binary fixtures and synthesised failure paths by byte-mutating them, which cannot express an expired certificate, an ES256 signature, or a timestamp that fails one specific way. There is now a generator — JUMBF superbox/jumd writer, assertion store, 1.x and 2.x claims, COSE_Sign1, JPEG APP11 / PNG caBX framing, and a hand-rolled RFC 3161 / CMS token writer — that builds valid assets from scratch and then applies named mutations.
It is entirely test-only: no new fixtures are committed, no new dependencies, still no cgo, and the package stays flat.
- 16 positive cases across 2 containers x 4 signature algorithms x 2 claim shapes. ES256, ES384 and EdDSA had never been exercised — only PS256.
- 33 negative cases pinned to exact status codes, covering the certificate profile checks, hard-binding dispatch, assertion hashes, and every reachable
timeStamp.*failure. - Two new fuzz targets seeded from generated assets.
FuzzBMFFParseandFuzzBMFFHashwere also added to the nightly matrix, which had been running 7 of 9 targets since BMFF support landed.
Statement coverage rose from 78.9% to 82.8%, concentrated where it was weakest: timestamp.go 74.9% → 82.2%, chain.go 70.3% → 86.9%.
Notes
No public API change: Read, Validate, Info, the options and the status codes are all unchanged. This is a minor rather than a patch because it changes validation outcomes for a verifier library.
Four status codes remain declared but unreachable — claim.multiple, timeStamp.outsideValidity, and assertion.boxesHash.match/.mismatch (the latter two dead by design while c2pa.hash.boxes reports general.unsupported).
Full changelog: v0.4.0...v0.5.0
v0.4.0 — read the C2PA 2.x shapes real files use
Reads the C2PA 2.x shapes that current writers — OpenAI's and Google's signers among them — actually put in a file, and that the library previously read back empty or as malformed.
Fixed — timestamps in a bare CMS ContentInfo
sigTst and sigTst2 may hold either an RFC 3161 TimeStampResp or a bare CMS ContentInfo. Both descents probed with a two-RawValue struct, which matches any two-element SEQUENCE — including a ContentInfo — so a ContentInfo token was descended into as a response, failed, and was rejected. Read returned a zero SignedAt and Validate recorded timeStamp.mismatch ("malformed timestamp token") for every timestamp of that shape, which is what Google- and OpenAI-signed images carry.
The two shapes are now told apart by their first element (an OID for a ContentInfo, a SEQUENCE for a PKIStatusInfo) in one shared helper used by both call sites.
signingTime also looked only at sigTst, while Validate's extraction already handled sigTst2. A c2pa.claim.v2 signature carries its timestamp only in sigTst2, so Read's SignedAt stayed zero for exactly those files. It now reads either container.
Fixed — claim_generator_info
An array of entries in 1.x, a single entry in 2.x. A c2pa.claim.v2 carries only the 2.x shape and no flat claim_generator, so Info.ClaimGenerator came back empty for every such file. Both shapes are now read.
Added — Info.SoftwareAgent
softwareAgent names the model rather than the signing service (e.g. gpt-image 2.0 on OpenAI's c2pa.created action) and had no field. It is read as the 1.x string, the 2.x inline entry, or a 2.x softwareAgentIndex into the assertion's softwareAgents array, and is assigned per manifest the way ClaimGenerator is, so the two fields always describe the same manifest.
Tests
New fixture testdata/c2pa_2x_openai.png — an OpenAI-generated PNG carrying all three 2.x shapes; reverting the fixes under TestC2PA2xOpenAIFixture reproduces all three symptoms. The timestamp tests unwrap the existing signed fixture's own token to get the bare ContentInfo shape and assert it reaches timeStamp.validated; the remaining cases synthesise CBOR.
No API removals or behaviour changes beyond the above.
Full changelog: v0.3.0...v0.4.0
v0.3.0 — BMFF (MP4/MOV/HEIC/HEIF/AVIF) support
Backfilled release notes — the v0.3.0 tag was pushed 2026-08 without a GitHub release entry.
Adds BMFF containers (MP4, MOV, HEIC, HEIF, AVIF) to the pure-Go (no cgo) toolkit, alongside the existing JPEG and PNG support.
Added — BMFF Container
The manifest is read from the top-level C2PA uuid box: purpose-aware, merkle-offset tolerant, padding-trimmed. Read and Validate both accept it.
Added — c2pa.hash.bmff.v2 / .v3 hard-binding verification
Validate verifies the BMFF hard binding per spec §18.6: exclusion xpath / length / data / subset / version / flags predicates, then a single ascending pass hashing each non-excluded top-level box as an 8-byte big-endian offset marker plus its bytes minus exclusions.
New status codes: assertion.bmffHash.match, assertion.bmffHash.mismatch, assertion.bmffHash.malformed.
Changed
- BEHAVIOR CHANGE — a BMFF hard binding on a non-BMFF container is now
hardBinding.missinginstead of an informational "unsupported". The old status let a bmff-only manifest validate with nothing actually checked. - v1
c2pa.hash.bmffis ignored per spec §18.6.1; a manifest whose only binding is v1 reportshardBinding.missing. - Fragmented / Merkle BMFF assets are reported as informational unsupported.
- Ingredient manifests no longer run hard-binding checks against the active asset's bytes — their binding refers to the ingredient's original bytes, so the result is informational instead of a spurious mismatch.
- The COSE
x5chainlookup also accepts the pre-1.3 c2pa-rs text key"x5chain"in either header.
Tests
Fixtures from c2pa-rs (Apache-2.0 / MIT): a signed MP4 that serves as the hash oracle and validates end to end, its unsigned twin, and a v1-legacy MP4. Two new fuzz targets (FuzzBMFFParse, FuzzBMFFHash); FuzzRead / FuzzValidate extended to the new container.
Full changelog: v0.2.0...v0.3.0
v0.2.0 — full pure-Go C2PA validation
First release with cryptographic validation. The library is now a two-mode pure-Go (no cgo) C2PA / Content Credentials toolkit for JPEG & PNG.
Added — Validate
A full, opt-in verifier alongside the existing fast, unverified Read:
r := c2pa.Validate(ctx, c2pa.JPEG, reader, opts...)
// r.Valid, r.Statuses (C2PA §15 codes), r.SignerChain, r.SignedAt, r.InfoIt performs the complete C2PA validation algorithm in pure Go:
- COSE signature — detached-payload
COSE_Sign1verification (ES256/384/512, PS256/384/512, EdDSA). - Certificate chain + C2PA profile — chains the signer to the trust list and enforces the C2PA cert profile (EKU, key usage, no weak algorithms), pinned to the verified signing time.
- Hash bindings —
c2pa.hash.data(asset content hash, with exclusions) and per-assertionhashed_uriintegrity. - RFC 3161 timestamp — full CMS signature verification, TSA chain, and message-imprint binding to the signature.
- Revocation — OCSP/CRL, opt-in (
WithOnlineRevocation), soft-fail. - Ingredients — recursive nested-manifest validation with depth cap and cycle detection.
Trust anchors are the official C2PA conformance lists, embedded via go:embed and overridable with WithSigningTrust / WithTimestampTrust. Options: WithOnlineRevocation, WithClock, WithMaxIngredientDepth, WithMaxScan, WithHTTPClient.
Read is unchanged — same fast, never-fail, unverified behaviour.
Changed
- Go 1.25+ is now required (the floor is set by
golang.org/x/crypto, pulled in for OCSP). - New dependency:
golang.org/x/crypto. Still no cgo.
Full changelog: v0.1.0...v0.2.0
v0.1.0 — initial release: pure-Go read-only C2PA reader
Backfilled release notes — the v0.1.0 tag was pushed 2026-06-11 without a GitHub release entry.
Initial release: a small, pure-Go, read-only reader for C2PA / Content Credentials provenance manifests embedded in JPEG and PNG files.
Extracted from file-search-on, where it powers the is_c2pa / c2pa_* search attributes.
What it does
Surfaces what a file claims about its provenance — creating tool, title, declared format, whether it declares AI-generated content, and the claimed signer identity + signing time — by parsing the embedded JUMBF manifest (ISO 19566-5), CBOR-decoding the active manifest's claim and c2pa.actions assertion, and decoding the COSE_Sign1 signature envelope.
info := c2pa.Read(context.Background(), c2pa.JPEG, f) // or c2pa.PNG
info.Present, info.ClaimGenerator, info.Title, info.Format,
info.AIGenerated, info.SignedBy, info.SignedAtRead is best-effort and never returns an error: a missing or malformed manifest yields Info{Present: false}. It reads at most c2pa.MaxScan (16 MiB) and honours the context, surrendering promptly mid-scan on cancel.
⚠️ Unverified — read, not validate
At this version the library is the equivalent of reading EXIF, or an email From: header: it reports the file's claims and does not authenticate them. No COSE signature verification, no certificate-chain check against the C2PA trust list — SignedBy is who the file claims signed it. Useful for search, indexing, triage and inventory; not for trust decisions.
(Cryptographic validation arrived later, in v0.2.0's Validate.)
Public API
Read(ctx, container, r) Info; WalkBoxes(ctx, jumbf, fn) for assertions Read doesn't model (depth-capped against adversarial nesting); Info; MaxScan.
Requirements
Go 1.23+. Pure Go, no cgo — depends only on fxamacker/cbor and veraison/go-cose. Context-aware and fuzzed (FuzzRead, FuzzWalkBoxes).