Skip to content

v0.5.0 — reject a signature that does not cover the claim

Latest

Choose a tag to compare

@richardwooding richardwooding released this 28 Aug 15:25
1f6adb4

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 unchecked

Real 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. FuzzBMFFParse and FuzzBMFFHash were 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