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
- func DefaultMining() map[string]string
- func Digest(s SignedManifest) ([]byte, error)
- func Optional(s string) *string
- func ValidateShape(s SignedManifest) error
- func Verify(s SignedManifest) error
- type AIRange
- type Assertion
- type Asset
- type BuildInput
- type GeneratorInfo
- type Manifest
- type Signature
- type SignedManifest
Constants ¶
const ContextURI = "https://c2pa.org/ns/manifest/1.4"
ContextURI is the C2PA manifest context the credential declares.
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.
const ManifestType = "ContentCredential"
ManifestType is the credential type.
Variables ¶
This section is empty.
Functions ¶
func DefaultMining ¶
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
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 (*Assertion) UnmarshalJSON ¶ added in v0.1.2
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 (*Asset) UnmarshalJSON ¶ added in v0.1.2
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 (*Signature) UnmarshalJSON ¶ added in v0.1.2
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 ¶
func Sign(m Manifest, key ed25519.PrivateKey) (SignedManifest, error)
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.