c2pa

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 11, 2026 License: MIT Imports: 24 Imported by: 0

README

c2pa

Go Reference CI Go Report Card

A small, pure-Go (no cgo) library for C2PA / Content Credentials provenance manifests embedded in JPEG and PNG files, with two modes:

  • Read — a fast, unverified reader. Surfaces what a file claims (creating tool, title, format, AI-generated flag, signer identity, signing time) like EXIF or an email From: header.
  • Validate — a full, opt-in verifier. Checks the COSE signature, the certificate chain against the C2PA trust list, assertion and hard-binding hashes, the RFC 3161 timestamp, revocation, and ingredients — reporting C2PA status codes. Pure Go, no c2pa-rs/CGO.
go get github.com/richardwooding/c2pa

Read — fast, unverified triage

Read reports the file's claims; it does not authenticate them. Treat every field like EXIF: accurate-as-recorded, not proven. SignedBy is who the file claims signed it. Use it for search, indexing, triage, and inventory ("find images with Content Credentials", "find AI-generated assets") — not for trust decisions.

f, _ := os.Open("photo.jpg")
defer f.Close()

info := c2pa.Read(context.Background(), c2pa.JPEG, f) // or c2pa.PNG
if !info.Present {
    return // no Content Credentials embedded
}

fmt.Println(info.ClaimGenerator) // e.g. "Adobe Firefly"
fmt.Println(info.Title)          // claim dc:title
fmt.Println(info.Format)         // claim dc:format
fmt.Println(info.AIGenerated)    // declared AI-generated?
fmt.Println(info.SignedBy)       // CLAIMED signer cert CN (unverified)
fmt.Println(info.SignedAt)       // RFC 3161 signing time (unverified)

Read 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) from the reader and honours the context — a cancelled call surrenders promptly mid-scan.

Info field Meaning
Present a C2PA manifest was found and parsed
ClaimGenerator the tool that created/edited the asset
Title claim dc:title
Format claim dc:format (declared media type)
AIGenerated a c2pa.actions digitalSourceType declares trainedAlgorithmicMedia / compositeWithTrainedAlgorithmicMedia
SignedBy COSE signer leaf-cert common name — unverified
SignedAt RFC 3161 signing time — unverified

Validate — full cryptographic verification

Validate is the verified counterpart. It performs the complete C2PA validation algorithm in pure Go and returns a structured result with per-step status codes:

f, _ := os.Open("photo.jpg")
defer f.Close()

r := c2pa.Validate(context.Background(), c2pa.JPEG, f)
if r.Valid {
    fmt.Println("verified, signed at", r.SignedAt)
} else {
    fmt.Println("not valid:", r.FirstFailure().Code)
}

// Inspect individual outcomes:
fmt.Println("signature verified:", r.Has(c2pa.StatusClaimSignatureValidated))
fmt.Println("signer trusted:", r.Has(c2pa.StatusSigningCredentialTrusted))
for _, s := range r.Statuses {
    fmt.Printf("[%v] %s — %s\n", s.Severity, s.Code, s.Explanation)
}

What it verifies:

  • COSE signature — the COSE_Sign1 over the claim (ES256/384/512, PS256/384/512, EdDSA).
  • Certificate chain + C2PA profile — chains the signer to the trust list and enforces the C2PA certificate profile (EKU, key usage, no weak algorithms), at the verified signing time.
  • Hash bindings — the hard-binding c2pa.hash.data (the asset content hash) and each assertion's hashed_uri.
  • RFC 3161 timestamp — full CMS signature verification, the TSA chain, and that the timestamp covers this signature.
  • Revocation — OCSP/CRL, opt-in (off by default), soft-fail.
  • Ingredients — recursive validation of nested manifests, with cycle detection.

r.Valid is true exactly when no failure-severity status was recorded. Like Read, Validate never returns an error and never panics — malformed or untrusted input is reported as failure statuses. It reads up to c2pa.ValidateMaxScan (256 MiB) so it can hash the whole asset; an asset larger than the cap reports an informational status rather than a false hash mismatch.

Trust anchors and options

By default Validate uses the official C2PA conformance trust lists, embedded in the binary (see trustlists/). These are a point-in-time snapshot — refresh them periodically. Supply your own anchors and tune behaviour with options:

r := c2pa.Validate(ctx, c2pa.JPEG, f,
    c2pa.WithSigningTrust(myPool),       // override signing-anchor *x509.CertPool
    c2pa.WithTimestampTrust(myTSAPool),  // override TSA-anchor pool
    c2pa.WithOnlineRevocation(true),     // enable OCSP/CRL (network; default off)
    c2pa.WithClock(func() time.Time { return now }), // signing-time fallback
    c2pa.WithMaxIngredientDepth(16),     // bound nested-manifest recursion
    c2pa.WithMaxScan(64 << 20),          // cap bytes read
    c2pa.WithHTTPClient(client),         // HTTP client for OCSP/CRL
)

ValidationResult also exposes Info (the same fields Read returns), ActiveManifestLabel, and the parsed SignerChain. Status codes mirror the C2PA specification §15 (e.g. claimSignature.validated, signingCredential.untrusted, assertion.dataHash.mismatch); each StatusEntry has a Severity (success / informational / failure).

Lower-level

c2pa.WalkBoxes(ctx, jumbf, fn) exposes the JUMBF box-tree walker for callers that want to surface assertions Read doesn't model. Box nesting is depth-capped so adversarial input can't exhaust the stack.

Requirements

License

MIT — see LICENSE. The test fixture under testdata/ is from contentauth/c2pa-rs (see testdata/README.md). The embedded trust lists under trustlists/ are the official C2PA conformance lists (see trustlists/README.md).


Extracted from file-search-on, where it powers the is_c2pa / c2pa_* search attributes.

Documentation

Overview

Package c2pa is a pure-Go, read-only reader for C2PA / Content Credentials (https://c2pa.org) provenance manifests embedded in media files.

It surfaces what a file CLAIMS about its provenance — the creating tool, title, declared media type, whether it declares AI-generated content, and the 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.

This is UNVERIFIED

The reader is deliberately read-only: it does NOT validate the COSE cryptographic signature, and it does NOT check the signer's certificate chain against the C2PA trust list. Full validation requires the Rust c2pa-rs library via CGO, which this pure-Go package intentionally avoids.

Treat every field like EXIF or an email From header: accurate-as-recorded, not authenticated. SignedBy is who the file CLAIMS signed it, not a verified identity. A file with no manifest yields Info{Present:false}; absence of a signal (e.g. AIGenerated) does not prove its negation.

All parsing is best-effort and never panics: malformed or truncated input yields zero values rather than an error. Every input-scaled loop honours the supplied context.Context, so a cancelled call surrenders promptly.

Example

Example reads the Content Credentials a JPEG claims, and surfaces the (unverified) creating tool, AI-generated flag, and signer identity.

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/richardwooding/c2pa"
)

func main() {
	f, err := os.Open("testdata/c2pa_signed.jpg")
	if err != nil {
		panic(err)
	}
	defer func() { _ = f.Close() }()

	info := c2pa.Read(context.Background(), c2pa.JPEG, f)
	if !info.Present {
		fmt.Println("no Content Credentials")
		return
	}
	fmt.Println("title:", info.Title)
	fmt.Println("ai-generated:", info.AIGenerated)
	// SignedBy is the CLAIMED signer — not cryptographically verified.
	fmt.Println("signed by:", info.SignedBy)
}
Output:
title: CA.jpg
ai-generated: false
signed by: C2PA Signer

Index

Examples

Constants

View Source
const MaxScan = 16 << 20

MaxScan caps how many leading bytes Read consumes looking for a manifest. C2PA manifests sit in the file header (before image data) and rarely exceed a few MB even with embedded thumbnails; past the cap Read gives up.

View Source
const ValidateMaxScan = 256 << 20

ValidateMaxScan caps how many leading bytes Validate consumes. Unlike Read's MaxScan (tuned for fast manifest discovery), validation must hash the whole asset for hard-binding checks, so the cap is larger. Assets beyond it cannot have their data hash verified — that is reported as an informational status, never a false mismatch.

Variables

This section is empty.

Functions

func WalkBoxes

func WalkBoxes(ctx context.Context, jumbf []byte, fn func(label, tbox string, content []byte))

WalkBoxes recursively walks a JUMBF box tree, invoking fn(label, tbox, content) for every leaf box. label is the nearest enclosing superbox's jumd label, tbox is the 4-character box type, and content is the box payload. Nesting is capped at an internal depth limit so adversarial input cannot exhaust the stack; ctx is honoured at the top of every iteration.

This is a lower-level primitive — most callers want Read. It is exported for advanced use (e.g. surfacing assertions Read does not model).

Types

type Container

type Container string

Container identifies the carrier file format whose C2PA manifest to read.

const (
	// JPEG reads the manifest from APP11 (0xFFEB) marker segments.
	JPEG Container = "jpeg"
	// PNG reads the manifest from caBX chunks.
	PNG Container = "png"
)

type Info

type Info struct {
	// Present is true when a C2PA manifest was found and parsed.
	Present bool
	// ClaimGenerator is the tool that created/edited the asset (e.g.
	// "Adobe Firefly", "make_test_images/0.33.1 c2pa-rs/0.33.1").
	ClaimGenerator string
	// Title is the claim's dc:title.
	Title string
	// Format is the claim's dc:format (declared media type).
	Format string
	// AIGenerated is true when a c2pa.actions assertion declares a
	// digitalSourceType of trainedAlgorithmicMedia or
	// compositeWithTrainedAlgorithmicMedia.
	AIGenerated bool
	// SignedBy is the COSE_Sign1 signer's leaf x509 certificate common name
	// (Subject CN, falling back to the first Organization). UNVERIFIED — the
	// certificate chain is not validated against the C2PA trust list.
	SignedBy string
	// SignedAt is the signing time from the RFC 3161 timestamp embedded in the
	// signature (sigTst). Zero when absent. UNVERIFIED.
	SignedAt time.Time
}

Info is the surfaced, CLAIMED, UNVERIFIED subset of a C2PA manifest. See the package doc: these are the file's assertions, not authenticated facts.

func Read

func Read(ctx context.Context, container Container, r io.Reader) Info

Read reads up to MaxScan bytes from r and, for the given container, locates and parses the embedded JUMBF manifest. It returns a zero Info (Present=false) when there's no manifest. It never returns an error — provenance is best-effort metadata, surfaced like EXIF.

ctx is honoured at entry and inside the input-scaled scan loops, so a cancelled call surrenders promptly mid-scan rather than parsing a full adversarial header.

type Severity added in v0.2.0

type Severity int

Severity classifies a StatusCode as success, informational, or failure. Only failures flip ValidationResult.Valid to false.

const (
	// SeverityInformational is advisory: the step ran but its outcome neither
	// proves nor disproves validity (e.g. revocation status unknown, an
	// unsupported-but-not-fatal feature, an absent optional timestamp).
	SeverityInformational Severity = iota
	// SeveritySuccess records a validation step that passed.
	SeveritySuccess
	// SeverityFailure records a validation step that failed. Any failure makes
	// the manifest invalid.
	SeverityFailure
)

type StatusCode added in v0.2.0

type StatusCode string

StatusCode is a C2PA validation status code. The string values mirror the codes defined in the C2PA Technical Specification §15 (e.g. "claimSignature.validated", "signingCredential.untrusted") so a caller can match against them directly.

const (
	StatusClaimSignatureValidated     StatusCode = "claimSignature.validated"
	StatusSigningCredentialTrusted    StatusCode = "signingCredential.trusted"
	StatusTimeStampValidated          StatusCode = "timeStamp.validated"
	StatusAssertionHashedURIMatch     StatusCode = "assertion.hashedURI.match"
	StatusAssertionDataHashMatch      StatusCode = "assertion.dataHash.match"
	StatusAssertionBoxesHashMatch     StatusCode = "assertion.boxesHash.match"
	StatusIngredientManifestValidated StatusCode = "ingredient.manifest.validated"
)

Success status codes.

const (
	StatusClaimMissing               StatusCode = "claim.missing"
	StatusClaimRequiredMissing       StatusCode = "claim.required.missing"
	StatusClaimMultiple              StatusCode = "claim.multiple"
	StatusClaimSignatureMissing      StatusCode = "claimSignature.missing"
	StatusClaimSignatureMismatch     StatusCode = "claimSignature.mismatch"
	StatusSigningCredentialUntrusted StatusCode = "signingCredential.untrusted"
	StatusSigningCredentialInvalid   StatusCode = "signingCredential.invalid"
	StatusSigningCredentialRevoked   StatusCode = "signingCredential.revoked"
	StatusSigningCredentialExpired   StatusCode = "signingCredential.expired"
	StatusTimeStampMismatch          StatusCode = "timeStamp.mismatch"
	StatusTimeStampUntrusted         StatusCode = "timeStamp.untrusted"
	StatusTimeStampOutsideValidity   StatusCode = "timeStamp.outsideValidity"
	StatusAssertionHashedURIMismatch StatusCode = "assertion.hashedURI.mismatch"
	StatusAssertionDataHashMismatch  StatusCode = "assertion.dataHash.mismatch"
	StatusAssertionBoxesHashMismatch StatusCode = "assertion.boxesHash.mismatch"
	StatusAssertionMissing           StatusCode = "assertion.missing"
	StatusHardBindingMissing         StatusCode = "hardBinding.missing"
	StatusAlgorithmUnsupported       StatusCode = "algorithm.unsupported"
	StatusIngredientManifestMismatch StatusCode = "ingredient.manifest.mismatch"
	StatusGeneralError               StatusCode = "general.error"
)

Failure status codes.

const (
	StatusRevocationUnknown StatusCode = "signingCredential.revocation.unknown"
	StatusTimeStampMissing  StatusCode = "timeStamp.missing"
	StatusUnsupported       StatusCode = "general.unsupported"
)

Informational status codes.

func (StatusCode) Severity added in v0.2.0

func (c StatusCode) Severity() Severity

Severity returns the StatusCode's severity. Unknown codes are informational.

type StatusEntry added in v0.2.0

type StatusEntry struct {
	Code        StatusCode
	Severity    Severity
	URI         string
	Explanation string
	Err         error
}

StatusEntry is one outcome from the validation pipeline: a C2PA status code, its severity, the JUMBF URI of the subject it concerns (best-effort), a human-readable explanation, and the underlying error for failures.

type ValidateOption added in v0.2.0

type ValidateOption func(*validateConfig)

ValidateOption configures a Validate call. See the With* constructors.

func WithClock added in v0.2.0

func WithClock(now func() time.Time) ValidateOption

WithClock overrides the time source used when no trusted timestamp pins the signing time (defaults to time.Now). Useful for deterministic tests.

func WithHTTPClient added in v0.2.0

func WithHTTPClient(client *http.Client) ValidateOption

WithHTTPClient sets the HTTP client used for OCSP/CRL fetches when online revocation is enabled (defaults to a client with a short timeout).

func WithMaxIngredientDepth added in v0.2.0

func WithMaxIngredientDepth(n int) ValidateOption

WithMaxIngredientDepth caps recursive ingredient/nested-manifest validation depth (default 16).

func WithMaxScan added in v0.2.0

func WithMaxScan(n int) ValidateOption

WithMaxScan overrides how many leading bytes Validate reads (default ValidateMaxScan).

func WithOnlineRevocation added in v0.2.0

func WithOnlineRevocation(enabled bool) ValidateOption

WithOnlineRevocation enables OCSP/CRL revocation checking, which makes network calls. It is off by default; when off, revocation is reported as an informational "unknown" status. Revocation is always soft-fail: a network or parse error is informational, never a validation failure.

func WithSigningTrust added in v0.2.0

func WithSigningTrust(pool *x509.CertPool) ValidateOption

WithSigningTrust overrides the embedded C2PA signing-anchor trust pool used to validate the claim signer's certificate chain.

func WithTimestampTrust added in v0.2.0

func WithTimestampTrust(pool *x509.CertPool) ValidateOption

WithTimestampTrust overrides the embedded C2PA timestamp-authority trust pool used to validate RFC 3161 timestamp tokens.

type ValidationResult added in v0.2.0

type ValidationResult struct {
	// Valid is true iff Statuses contains no SeverityFailure entry.
	Valid bool
	// Info mirrors the surfaced fields Read would return for the same input.
	Info Info
	// Statuses is the ordered list of every validation outcome.
	Statuses []StatusEntry
	// ActiveManifestLabel is the active (last) manifest's JUMBF label.
	ActiveManifestLabel string
	// SignerChain is the COSE signer's certificate chain (leaf first) as
	// presented in the manifest, populated once the chain is parsed.
	SignerChain []*x509.Certificate
	// SignedAt is the signing time from a verified RFC 3161 timestamp, or zero
	// when no trusted timestamp was found.
	SignedAt time.Time
}

ValidationResult is the outcome of Validate. Valid is true exactly when no SeverityFailure status was recorded (see the package docs on the roll-up rule). Statuses is the ordered accumulation of every success, informational, and failure status produced along the way.

func Validate added in v0.2.0

func Validate(ctx context.Context, container Container, r io.Reader, opts ...ValidateOption) ValidationResult

Validate reads up to ValidateMaxScan bytes from r and performs full C2PA validation of the embedded manifest: COSE signature verification, certificate chain + C2PA cert-profile validation against the (embedded or overridden) trust list, assertion and hard-binding hash verification, RFC 3161 timestamp verification, optional revocation checking, and recursive ingredient validation. It reports every outcome as a StatusEntry; ValidationResult.Valid is true exactly when no failure status was recorded.

Like Read, Validate never returns an error and never panics: malformed, truncated, or cancelled input is reported via failure statuses with Valid=false. It is the verified counterpart to Read's fast, unverified scan.

Example

ExampleValidate verifies a JPEG's Content Credentials against the embedded C2PA trust list. The test fixture is signed by the c2pa-rs *test* PKI, so its signature verifies cryptographically but its signer does not chain to a production trust anchor — an honest "valid signature, untrusted signer" verdict. Pass WithSigningTrust / WithTimestampTrust to supply your own anchors.

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/richardwooding/c2pa"
)

func main() {
	f, err := os.Open("testdata/c2pa_signed.jpg")
	if err != nil {
		panic(err)
	}
	defer func() { _ = f.Close() }()

	r := c2pa.Validate(context.Background(), c2pa.JPEG, f)
	fmt.Println("valid:", r.Valid)
	fmt.Println("signature verified:", r.Has(c2pa.StatusClaimSignatureValidated))
	fmt.Println("signer trusted:", r.Has(c2pa.StatusSigningCredentialTrusted))
}
Output:
valid: false
signature verified: true
signer trusted: false

func (ValidationResult) FirstFailure added in v0.2.0

func (r ValidationResult) FirstFailure() *StatusEntry

FirstFailure returns the first SeverityFailure status, or nil if none.

func (ValidationResult) Has added in v0.2.0

func (r ValidationResult) Has(code StatusCode) bool

Has reports whether any recorded status has the given code.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL