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 that a developer meets, where Sealpixel fits, and the four ways 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. The store is 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. The assertions are the facts. The claim lists the assertions by hash. The signature covers the claim.
The vocabulary, as the specification uses it:
| Term | What it is |
|---|---|
| Manifest | One signing: assertions, a claim, a signature. A file that was edited and signed twice carries two manifests. The later one names the earlier one 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 without the manifest store. A validator uses it to tell whether the file is the one that was signed. Any byte that is written afterwards breaks it, metadata included. Write metadata first, sign last |
| Claim | The list of the hashes of the assertions, 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, with ES256 or another algorithm that the specification allows, and with an optional RFC 3161 timestamp for the time of signing |
| Trust list | The list of certificate authorities that the validator 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 it reports a state |
Where Sealpixel fits
Sealpixel knows one thing that nobody else in the pipeline knows: which operations were applied to the pixels, in which order, and where. The edit log holds them as decisions that a person made. @sealpixel/c2pa maps that log to the actions assertion. @sealpixel/c2pa-sign does the part of the signer too, when you want it in your own process and not in a separate tool. It still signs with your key, through a function that 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 version of the renderer, 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 | The name and the version of your product | |
| 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. 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 return 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 render endpoint of the demo runs this path with a test certificate. Sign with your key has the details: the fields of the signer and the runtime. 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. The render of Sealpixel is the asset. The original is the parent. The actions come from Sealpixel.
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 the signing path of that product. 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. Then it writes the definition from the manifest, so the log, the index and the version of the renderer 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, which is the output, in percent. The definition writes it as a polygon. A plain rectangle has four vertices. The crop and the resize sit between the two spaces, and the definition moves every region through them before it writes it. Regions on the reference page has the rules: rotation, a cover resize, a region that the crop removed, and why the shape is never a rectangle. Coordinate spaces has the spaces.
Two kinds of proof
C2PA validation and the render verification of Sealpixel answer different questions. A pipeline that needs both runs both.
| C2PA validation | sealpixel verify | |
|---|---|---|
| Needs | The signed file | The original and the render manifest. Also the file, when the file 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 really produced the pixels | Who made the file, or whether it changed afterwards beyond the pixels |
| Breaks on | Any byte that is written after signing: a re-encode, metadata, a removed manifest store | A different original, log, plugin set or renderer output |
| Survives | Nothing that is written into the file afterwards. Sign last | A signer or a tagger that writes 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 that it does not trust, signingCredential.untrusted in c2pa-rs, until the certificate authority is on the trust list that the validator consults. That is the policy of the validator, not a defect of the file. The test certificate of the demo 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 the name and the version of your product. The Conformance Program evaluates that product. - Sign in the browser. The browser entry of the signing package exists so that 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 the reason.
Pitfalls
- The first action must be
c2pa.openedorc2pa.created. The signer addsc2pa.openedfor the parent. That is why the parent is required. Without a parent, 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 hash of the render. A signed file differs from the render by the manifest store that 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 with an older vocabulary shows two of the labels as actions without a name. The file validates all the same. The reference page names them.
- Sign last. The hard binding covers every byte outside the manifest store. Metadata that is written after signing, by a tagger or a CMS, invalidates it. Write metadata after the render and before the signature. See Metadata.
- Verify names the certificate, not your product, for a version 2 claim. c2pa-rs writes claim version 2 by default. The public Verify site reads
claim_generator_infoonly from a version 1 claim. Setclaim_version: 1in the definition when you want your name under "App or device used" there. Sign with your key shows how. - 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.