Skip to content

C2PA, explained for developers ​

C2PA is the open standard behind Content Credentials: a signed record inside an image that says where the image came from and what was done to it. A viewer reads the record, checks the signature and shows the history. This page explains the parts a developer meets, where Sealpixel fits, and the four ways to get from an edit log to a signed file. The reference pages are C2PA manifest definition and Sign with your key.

The one sentence to keep: Sealpixel writes what happened to the pixels; your product signs it with its own key. Sealpixel never holds a key and is not the claim generator.

What is in a signed file ​

A signed file carries a manifest store beside the pixels: a JUMBF box in the file, in APP11 segments of a JPEG, in a caBX chunk of a PNG. The box holds one manifest per signing. A manifest has three parts: assertions, which are the facts; a claim, which lists the assertions by hash; and a signature over the claim.

The parts of a signed fileA signed file holds the pixels and a manifest store. The manifest store holds one manifest. The manifest holds assertions: the actions, the ingredient for the original, and the hard binding over the file. The claim lists the assertions by hash, and the signature, with the certificate chain and an optional timestamp, covers the claim. Sealpixel writes the actions assertion; your product writes the rest and signs.THE SIGNED FILE · signed.jpgPixelsthe renderWhat the hardbinding hashes:everything in thefile except themanifest storeitself.Manifest storeone manifest per signingManifesturn:c2pa:…Assertionsthe facts, each hashedc2pa.actionsfrom the edit log · Sealpixelc2pa.ingredientthe originalc2pa.hash.datahard bindingClaimlists the assertions by hash,names the claim generatorSignatureCOSE over the claim, with yourcertificate chain and a timestampYour product is the claim generator: it adds the original as theingredient, computes the binding, builds the claim and signs.Sealpixel contributes the one assertion only it can write.
The actions assertion is Sealpixel's part. The ingredient, the hard binding, the claim and the signature are the signer's.

The vocabulary, as the specification uses it:

TermWhat it is
ManifestOne signing: assertions, a claim, a signature. A file edited and signed twice carries two, and the later one names the earlier as its parent
AssertionOne fact, stored as its own box and hashed into the claim. The ones here: c2pa.actions, c2pa.ingredient, c2pa.hash.data
ActionOne entry in the actions assertion: a label such as c2pa.cropped, a digitalSourceType, the software agent that did it, and for an edit of a region the region in percent of the asset
IngredientAn asset that went into this one. The original is the parentOf ingredient; a placed image would be componentOf. An ingredient carries the hash of that asset, and its own manifest when it had one
Hard bindingThe hash of the file with the manifest store left out, so a validator can tell whether the file is the one that was signed. Any byte written afterwards breaks it, metadata included: write metadata first, sign last
ClaimThe list of the assertions' hashes, the claim_generator_info of the product that signed, and the id of the manifest
Claim generatorThe product that builds and signs the manifest: your pipeline, your CMS, a conformant publishing tool. Not Sealpixel
SignerA certificate chain and the key behind it. The signature is COSE, ES256 or another algorithm the specification allows, with an optional RFC 3161 timestamp for the time of signing
Trust listThe validator's list of certificate authorities it accepts. A certificate outside it validates as a correct signature from an unknown signer
ValidatorAny reader of the store: c2patool, c2pa-rs, Content Credentials Verify, a viewer in a product. It checks the hashes, the signature and the chain and reports a state

Where Sealpixel fits ​

Sealpixel knows one thing nobody else in the pipeline knows: which operations were applied to the pixels, in what order, and where. The edit log holds them as decisions a person made. @sealpixel/c2pa maps that log to the actions assertion. @sealpixel/c2pa-sign does the signer's part too, when you want it in your own process rather than in a separate tool, and it still signs with your key, through a function you supply.

From the edit log to a signed fileIn the browser, the editor keeps an edit log. On your server, @sealpixel/render renders the original with that log; @sealpixel/c2pa writes the manifest definition from the same log; a signer, c2patool, c2pa-rs or @sealpixel/c2pa-sign, attaches the original as the parent, builds the claim and signs with your key. The signed file goes to your storage. Anyone with a validator reads the manifest; anyone with the original can also verify the render with sealpixel verify.BROWSERYOUR SERVERANYONE, LATEREditorSealpixeledit logdecisions, no pixels@sealpixel/renderoriginal + log → file@sealpixel/c2palog → definitionOriginalthe parentSignerthe claim generatorc2patool, c2pa-rs, or@sealpixel/c2pa-signadds the parent, builds the claim,signs with your keySigned fileyour storage, your CDNValidatorsignature, chain, trust listsealpixel verifyre-renders, compares pixelswith the original and the log
Two Sealpixel packages on your server, one signer of your choice. The key stays in the signer's box: a KMS, an HSM, a file you hold.

What each party contributes:

SealpixelYour product
The actions, with regions in output coordinates@sealpixel/c2pa, from the edit log
The software agent of each actionThe renderer's version, by defaultOverride with softwareAgent
digitalSourceTypehumanEdits by defaultdigitalSourceTypeByStage for stages you configured
The original as the parent ingredientYou attach it; the signer adds c2pa.opened
claim_generator_infoYour product's name and version
The hard binding and the claim@sealpixel/c2pa-sign, if you use itc2patool or c2pa-rs otherwise
The key, the certificate, the timestamp authorityYours. The module calls your sign(bytes)
ConformanceThe Conformance Program evaluates the product that signs

Four workflows ​

Each one ends in the same file. The difference is who runs the signer and where.

1. Render, then sign with c2patool ​

The free path, and the one to start with. Render on your server, write the definition, and let c2patool sign in a batch step, a CI job or a publishing script.

ts
const result = await renderer.render({ original, editLog, output: { format: 'image/jpeg' } });
const { json } = await manifestDefinition({ log: editLog, plugins: defaultPlugins, title: 'result.jpg' });
await writeFile('result.jpg', result.bytes);
await writeFile('result.c2pa.json', json);
bash
c2patool result.jpg --manifest result.c2pa.json --parent original.jpg --output signed.jpg
c2patool signed.jpg   # the report: actions, ingredient, validation_state

c2patool takes the key and the chain from its settings file. The native CLI writes the same definition for a plan with sealpixel render … --c2pa, so a pipeline without Node works the same way.

2. Sign in your own process ​

The render and the signature in one request, with the key behind a function. @sealpixel/c2pa-sign builds the claim and the binding from the definition and calls your signer once per file. This is the path for a render endpoint, a Lambda or a worker that must hand back a signed file.

ts
const c2pa = await loadC2paSign();
const { bytes } = await c2pa.sign({
  image: result.bytes,
  format: 'image/jpeg',
  definition: json,
  parent: { bytes: original, title: 'original.jpg' },
  signer: {
    alg: 'es256',
    certificates: chainPem,
    sign: (data) => kms.sign(data),
    timestampAuthority: 'http://timestamp.digicert.com',
  },
});

The demo's render endpoint runs this path with a test certificate. The details, the signer's fields and the runtime are on Sign with your key. The package needs the Provenance licence in production; the definition stays free.

3. A conformant product signs at publish time ​

A newsroom CMS, a DAM or a publishing tool that already signs with its own credentials stays the claim generator. It takes the definition as the input of its c2pa-rs Builder, adds its claim_generator_info and its ingredients, and signs when the asset is published. Sealpixel's render is the asset; the original is the parent; the actions are Sealpixel's.

rust
let definition: ManifestDefinition = serde_json::from_str(&json)?;
let mut builder = Builder::from_context(context).with_definition(definition)?;
builder.set_intent(BuilderIntent::Edit);
// parentOf, or the Builder treats the original as a component and adds no c2pa.opened.
let parent = r#"{ "relationship": "parentOf", "title": "original.jpg" }"#;
builder.add_ingredient_from_stream(parent, "image/jpeg", &mut original)?;
builder.sign(&signer, "image/jpeg", &mut rendered, &mut signed)?;

Nothing from Sealpixel runs in that product's signing path; the definition is JSON. This is the path when conformance matters, because the Conformance Program evaluates the product that signs, and that product is yours.

4. Later, from a render manifest ​

A queue renders now and signs later, or signs only what an editor approves. Render with manifest: true and keep the render manifest beside the file. The signing job verifies the render first and writes the definition from the manifest, so the log, the index and the renderer's version are the ones that produced the pixels.

ts
const report = await renderer.verify({ manifest, original, output: rendered });
if (!report.verified) throw new Error('not the render the manifest describes');
const { json } = await manifestDefinitionFor(manifest, { plugins: defaultPlugins });
// then path 1, 2 or 3
Workflow 2 as a sequence: a render endpoint that signs in processThe browser sends the edit log and the id of the original to your API. Your API renders with @sealpixel/render, writes the definition with @sealpixel/c2pa, and calls @sealpixel/c2pa-sign, which hands the bytes to sign to your KMS and takes the signature back. The API stores the signed file and answers the browser. Later, a validator reads the file.BrowserYour APISealpixel packagesYour KMS{ log, index, image }render(original, log)the file, its hashmanifestDefinition(log), then sign(file, definition, parent, signer)sign(bytes)the raw signaturethe signed filestored, and answered12345
Step 4 is the only one that touches the key, and it happens in your KMS. The module sees the signature, never the key.

Regions: from the log to the manifest ​

A redact region in the edit log is in image space: 0 to 1 over the source after EXIF orientation. A C2PA region refers to the signed asset, the output, in percent. Between the two sit the crop and the resize, and the definition moves every region through them before it writes it. The rules, rotation, a cover resize and a region the crop cut away, are in Regions on the reference page; Coordinate spaces has the spaces.

A redact region through the crop into output percentOn the left, the source in image space with a redact region at x 0.2, y 0.25, width 0.4, height 0.5, and a crop frame. In the middle, the crop keeps part of the source. On the right, the output at the crop's ratio: the region is written in percent of the output, clipped to the frame.1 · The source, image space 0..1crop frameredact { x: 0.2, y: 0.25, width: 0.4, height: 0.5 }2 · The crop keepsthe part inside the frame3 · The output, percentc2pa.editedclipped, in percent of the output{ x: 0, y: 13.6, w: 36.4, h: 63.6 }A region the crop cut away entirely writes no region. A rotating crop writes a polygon.
The numbers on the right are for this frame and would differ for another; a resize that keeps the ratio leaves them as they are. The point: the manifest speaks about the output, so the region is moved before it is written.

Two kinds of proof ​

C2PA validation and Sealpixel's render verification answer different questions. A pipeline that needs both runs both.

C2PA validationsealpixel verify
NeedsThe signed fileThe original and the render manifest; the file too, when it is to be checked
ProvesThe file is the one that was signed, who signed it, when, and what the signer claims was doneThese pixels are what this original and this edit log produce with this renderer
Does not proveThat the claimed actions actually produced the pixelsWho made the file or whether it changed afterwards, beyond the pixels
Breaks onAny byte written after signing: a re-encode, metadata, a stripped manifest storeA different original, log, plugin set or renderer output
SurvivesNothing written into the file afterwards. Sign lastA signer or a tagger writing into the file: pixels identical, bytes differ
ReportsValid, Trusted when the certificate is on the trust list, Invalid, with codesVERIFIED on the CLI; verified with every check by name from renderer.verify()

The manifest says what happened; the re-render proves it. A validator shows an unknown signer as a correct signature from a certificate it does not trust, signingCredential.untrusted in c2pa-rs, until the certificate authority is on the trust list the validator consults. That is the validator's policy, not a defect of the file; the demo's test certificate shows it on purpose.

What Sealpixel does not do ​

  • Hold a key. @sealpixel/c2pa-sign calls your sign(bytes) and takes the signature back. A KMS, an HSM, a subprocess or a file: your choice, your custody.
  • Act as the claim generator. claim_generator_info is your product's name and version. The Conformance Program evaluates that product.
  • Sign in the browser. The browser entry of the signing package exists so a bundle resolves. A key does not belong in a browser.
  • Read remote manifests. read() of a file that only points at a manifest URL throws REMOTE_MANIFEST with the URL; nothing is fetched.
  • Write c2pa.redacted or c2pa.watermarked for what it does. In C2PA those labels mean something else. A redaction is c2pa.edited with its region; the table of actions has every label and why.

Pitfalls ​

  • The first action must be c2pa.opened or c2pa.created. The signer adds c2pa.opened for the parent, which is why the parent is required. Signed without one, c2pa-rs takes the rendered file for its own original: the file validates, and its provenance is wrong. @sealpixel/c2pa-sign refuses to sign without a parent for that reason.
  • A self-signed certificate is refused. c2pa-rs wants a chain: a CA and a signing certificate under it, with digitalSignature and an extended key usage. For tests, make both on the spot; never commit them.
  • The hash in the response is the render's. A signed file differs from the render by the manifest store it carries, so its sha256 is another number. sealpixel verify reports that as pixels identical, bytes differ.
  • A re-encode after signing breaks the binding. CDNs and image proxies that recompress or strip metadata remove or invalidate the manifest. Serve the signed bytes as they are.
  • A reader built on an older vocabulary shows two of the labels as actions it has no name for; the file validates all the same. The reference page names them.
  • Sign last. The hard binding covers every byte outside the manifest store, so metadata written after signing, by a tagger or a CMS, invalidates it. Write metadata after the render and before the signature; see Metadata.
  • The timestamp authority is a network call per signing. Without one the signature carries no trusted time. With one, a slow authority slows every signing; give it a budget.