c2pa

package
v0.1.4 Latest Latest
Warning

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

Go to latest
Published: Aug 19, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package c2pa builds, signs and verifies the content credential that travels inside a receipts bundle.

The credential is a C2PA-aligned JSON manifest: it binds the SHA-256 of the published body, the tool that produced it, and a set of labelled assertions, to one Ed25519 author key. Signing follows SPEC.md section 7.1: Ed25519 over

SHA-256( UTF8("folio.c2pa.sig.v1") || UTF8( JCS(credential without signature.value) ) )

where JCS is RFC 8785 canonical JSON, implemented here and in verifier/src/jcs.ts so a producer in either language verifies in the other.

GAP FROM FULL C2PA. The full standard wraps the assertion store in CBOR and signs with COSE_Sign1 over an X.509 trust list. This manifest uses JSON and raw Ed25519 because personal publishers have no CAs and a self-anchored public key fingerprint is verifiable today. The folio.* labels are the namespace for extensions; c2pa.* labels mirror the standard where the data is meaningful.

The package is pure: it holds types and crypto, reads no files and never loads or stores a key.

Index

Constants

View Source
const ContextURI = "https://c2pa.org/ns/manifest/1.4"

ContextURI is the C2PA manifest context the credential declares.

View Source
const CredSigTag = "folio.c2pa.sig.v1"

CredSigTag is the domain separation tag mixed into the credential digest, distinct from the bundle tag so a credential signature can never be replayed as a bundle signature or the reverse.

View Source
const ManifestType = "ContentCredential"

ManifestType is the credential type.

Variables

This section is empty.

Functions

func DefaultMining

func DefaultMining() map[string]string

DefaultMining is the protective default for the c2pa.training-mining assertion: no training, no generative training, no inference, no data mining.

func Digest

func Digest(s SignedManifest) ([]byte, error)

Digest computes the credential signing digest from SPEC.md section 7.1: SHA-256 over the domain tag followed by the RFC 8785 canonical JSON of the credential with only signature.value removed. signature.alg and signature.public_key stay inside the signed payload, so neither the declared algorithm nor the author key can be swapped after signing.

func Optional added in v0.1.3

func Optional(s string) *string

Optional makes an optional credential string. Presence is part of the object the signature covers, so these fields are pointers; this is the ergonomic way for a producer to set one.

func ValidateShape added in v0.1.2

func ValidateShape(s SignedManifest) error

Verify recomputes the credential digest and checks the signature against the embedded public key, and rejects a `created_at` that is not whole-second UTC (SPEC.md section 3). Malformed input is an error, never a panic, so a verifier can reject a credential rather than abort. ValidateShape checks that a credential is the object SPEC section 7 describes, before any cryptography is attempted.

A signature proves that whatever is in front of you was signed by the named key. It says nothing about whether that object is a content credential. Without this, a signed `{"asset":{"sha256":...}, "signature":...}` verifies and gets presented to a reader as a valid content credential, which is a stronger thing than what was checked.

func Verify

func Verify(s SignedManifest) error

Types

type AIRange

type AIRange struct {
	From    int    `json:"from"`
	To      int    `json:"to"`
	Model   string `json:"model"`
	EventID string `json:"event_id"`
	Hash    string `json:"hash"`
	When    string `json:"when"`
}

AIRange is one disclosed AI-authored span, as the folio.ai_ranges assertion carries it: the span, the model, and the provenance event that recorded it.

type Assertion

type Assertion struct {
	Label string `json:"label"`
	// Required, and its value may be null, so it is stored verbatim and
	// always emitted. A pointer could not tell a null payload from a
	// missing one, which let a credential signed with `"data": null`
	// verify after the member was deleted.
	Data json.RawMessage `json:"data"`
	// contains filtered or unexported fields
}

Assertion is one labelled claim about the asset. Data is carried as raw JSON so a producer can extend the assertion set without this package modelling every label.

func (Assertion) MarshalJSON added in v0.1.2

func (a Assertion) MarshalJSON() ([]byte, error)

func (*Assertion) UnmarshalJSON added in v0.1.2

func (a *Assertion) UnmarshalJSON(data []byte) error

type Asset

type Asset struct {
	SHA256 string `json:"sha256"`
	Size   int64  `json:"size"`
	MIME   string `json:"mime"`
	// Pointers: the digest is taken over a re-encoding of this object,
	// so whether a member was there at all has to survive the trip. With
	// a plain string plus omitempty, a credential signed without a title
	// verified with `"title": ""` added, and one signed with an empty
	// title verified with it removed. A literal null is refused at parse
	// rather than represented, since a pointer cannot tell it from
	// absent (SPEC section 3.1).
	Title *string `json:"title,omitempty"`
	URL   *string `json:"url,omitempty"`
	// contains filtered or unexported fields
}

Asset identifies the published body the manifest is about.

func (Asset) MarshalJSON added in v0.1.2

func (a Asset) MarshalJSON() ([]byte, error)

func (*Asset) UnmarshalJSON added in v0.1.2

func (a *Asset) UnmarshalJSON(data []byte) error

type BuildInput

type BuildInput struct {
	// Asset identifies the published body.
	Asset Asset
	// Generator describes the producing tool. Build stamps
	// claim_generator as "<Name>/<Version>".
	Generator GeneratorInfo
	// CreatedAt is the credential timestamp. Zero means now. It is
	// truncated to whole UTC seconds so its RFC 3339 form is exact.
	CreatedAt time.Time
	// AIRanges are the disclosed spans. An empty slice omits the
	// folio.ai_ranges assertion entirely.
	AIRanges []AIRange
	// ChainLen is the length of the provenance chain the ranges came
	// from, recorded alongside them.
	ChainLen int
	// Mining sets the c2pa.training-mining entries. Nil selects
	// DefaultMining, which withholds every use.
	Mining map[string]string
}

BuildInput carries everything Build needs. The caller has already read the body, hashed it and resolved its metadata: this package opens no files and consults no configuration.

type GeneratorInfo

type GeneratorInfo struct {
	Name string `json:"name"`
	// Pointers for the same reason as Asset.Title: presence is part of
	// the object the signature covers.
	Version *string `json:"version,omitempty"`
	URL     *string `json:"url,omitempty"`
	// contains filtered or unexported fields
}

GeneratorInfo describes the tool chain that produced the manifest.

func (GeneratorInfo) MarshalJSON added in v0.1.2

func (g GeneratorInfo) MarshalJSON() ([]byte, error)

func (*GeneratorInfo) UnmarshalJSON added in v0.1.2

func (g *GeneratorInfo) UnmarshalJSON(data []byte) error

type Manifest

type Manifest struct {
	Context        string        `json:"@context"`
	Type           string        `json:"type"`
	Asset          Asset         `json:"asset"`
	ClaimGenerator string        `json:"claim_generator"`
	GeneratorInfo  GeneratorInfo `json:"claim_generator_info"`
	CreatedAt      time.Time     `json:"created_at"`
	Assertions     []Assertion   `json:"assertions"`
}

Manifest is the unsigned content credential body.

func Build

func Build(in BuildInput) (Manifest, error)

Build assembles the unsigned manifest from in. It is deterministic: the same input always produces the same manifest, so signing it twice produces the same bytes.

type Signature

type Signature struct {
	Alg       string `json:"alg"`
	PublicKey string `json:"public_key"` // base64url, unpadded, raw 32-byte key
	Value     string `json:"value"`      // base64url, unpadded, raw 64-byte signature
	// contains filtered or unexported fields
}

Signature is the Ed25519 envelope.

func (Signature) MarshalJSON added in v0.1.2

func (s Signature) MarshalJSON() ([]byte, error)

func (*Signature) UnmarshalJSON added in v0.1.2

func (s *Signature) UnmarshalJSON(data []byte) error

type SignedManifest

type SignedManifest struct {
	Manifest
	Signature Signature `json:"signature"`
	// contains filtered or unexported fields
}

SignedManifest is a manifest plus its signature.

The signature covers every member of the received object, including members this package does not model, so unknown members are preserved in `extras` and re-emitted. A credential from a newer implementation therefore still verifies here instead of failing because a field was dropped in transit.

What is deliberately NOT done: keeping the original bytes and re-emitting those. That made a parsed credential two objects at once, the bytes it arrived as and the fields a caller could edit. Verifying checked the bytes while the binding checks in receipts/credential.go read the fields, so a parsed credential could be mutated and still verify, then marshal to something the verification never saw. One authoritative object, rebuilt from current fields plus extras, cannot drift from itself that way.

func Sign

Sign signs m with key and returns the signed credential. The same (manifest, key) pair always yields the same signature bytes.

Sign normalizes m.CreatedAt to whole UTC seconds, as SPEC.md section 3 requires of every timestamp in a bundle. Build already does this, but a producer may assemble a Manifest by hand, and a credential is not conforming if its wire timestamp carries sub-second precision or a zone offset other than Z. Sign refuses to produce a credential its own Verify would reject. Emitting one is worse than failing: the producer learns nothing until a reader tries to check the receipt, and by then it is published.

func (SignedManifest) MarshalJSON

func (s SignedManifest) MarshalJSON() ([]byte, error)

MarshalJSON encodes the current fields, then restores any unknown members. Member order does not matter: the digest is taken over the RFC 8785 canonical form, which sorts.

func (*SignedManifest) UnmarshalJSON

func (s *SignedManifest) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes the modelled fields and keeps any others aside.

Jump to

Keyboard shortcuts

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