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 vocabulary, as the specification uses it:
| Term | What it is |
|---|---|
| Manifest | One signing: assertions, a claim, a signature. A file edited and signed twice carries two, and the later one names the earlier as its parent |
| Assertion | One fact, stored as its own box and hashed into the claim. The ones here: c2pa.actions, c2pa.ingredient, c2pa.hash.data |
| Action | One 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 |
| Ingredient | An 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 binding | The 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 |
| Claim | The list of the assertions' hashes, the claim_generator_info of the product that signed, and the id of the manifest |
| Claim generator | The product that builds and signs the manifest: your pipeline, your CMS, a conformant publishing tool. Not Sealpixel |
| Signer | A 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 list | The validator's list of certificate authorities it accepts. A certificate outside it validates as a correct signature from an unknown signer |
| Validator | Any 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.
What each party contributes:
| Sealpixel | Your product | |
|---|---|---|
| The actions, with regions in output coordinates | @sealpixel/c2pa, from the edit log | |
| The software agent of each action | The renderer's version, by default | Override with softwareAgent |
digitalSourceType | humanEdits by default | digitalSourceTypeByStage for stages you configured |
| The original as the parent ingredient | You attach it; the signer adds c2pa.opened | |
claim_generator_info | Your product's name and version | |
| The hard binding and the claim | @sealpixel/c2pa-sign, if you use it | c2patool or c2pa-rs otherwise |
| The key, the certificate, the timestamp authority | Yours. The module calls your sign(bytes) | |
| Conformance | The 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.
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);c2patool result.jpg --manifest result.c2pa.json --parent original.jpg --output signed.jpg
c2patool signed.jpg # the report: actions, ingredient, validation_statec2patool 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.
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.
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.
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 3Regions: 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.
Two kinds of proof
C2PA validation and Sealpixel's render verification answer different questions. A pipeline that needs both runs both.
| C2PA validation | sealpixel verify | |
|---|---|---|
| Needs | The signed file | The original and the render manifest; the file too, when it is to be checked |
| Proves | The file is the one that was signed, who signed it, when, and what the signer claims was done | These pixels are what this original and this edit log produce with this renderer |
| Does not prove | That the claimed actions actually produced the pixels | Who made the file or whether it changed afterwards, beyond the pixels |
| Breaks on | Any byte written after signing: a re-encode, metadata, a stripped manifest store | A different original, log, plugin set or renderer output |
| Survives | Nothing written into the file afterwards. Sign last | A signer or a tagger writing into the file: pixels identical, bytes differ |
| Reports | Valid, Trusted when the certificate is on the trust list, Invalid, with codes | VERIFIED 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-signcalls yoursign(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_infois 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 throwsREMOTE_MANIFESTwith the URL; nothing is fetched. - Write
c2pa.redactedorc2pa.watermarkedfor what it does. In C2PA those labels mean something else. A redaction isc2pa.editedwith its region; the table of actions has every label and why.
Pitfalls
- The first action must be
c2pa.openedorc2pa.created. The signer addsc2pa.openedfor 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-signrefuses 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
digitalSignatureand 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 verifyreports 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.