c2pa

package module
v0.1.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: 11 Imported by: 0

README

c2pa

Go Reference CI Go Report Card

A small, pure-Go, read-only reader for C2PA / Content Credentials provenance manifests embedded in JPEG and PNG files.

It surfaces what a file claims about its provenance — the creating tool, title, declared format, 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.

go get github.com/richardwooding/c2pa

⚠️ This is UNVERIFIED — read, not validate

This library is the equivalent of reading EXIF, or an email From: header: it reports the file's claims, it does not authenticate them.

  • It does not verify the COSE cryptographic signature.
  • It does not check the signer's certificate chain against the C2PA trust list.
  • SignedBy is who the file claims signed it, not a verified identity.

Full validation requires the Rust c2pa-rs library via CGO. This package intentionally stays pure-Go and read-only — useful for search, indexing, triage, and inventory ("find images with Content Credentials", "find AI-generated assets", "who does this file claim signed it"), not for trust decisions.

Usage

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

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

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
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).


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.

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.

Jump to

Keyboard shortcuts

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