Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/partials/_local-sign-warning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:::warning Warning
Accessing a private key and certificate directly from the file system as shown in these examples is fine during development, but doing so in production is **not secure**. Instead use a Key Management Service (KMS) or a hardware security module (HSM) to access the certificate and key; For more information, see [Using a certificate in production](../signing/prod-cert.mdx).
:::
2 changes: 1 addition & 1 deletion docs/signing/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ As you're developing an application that uses the CAI SDK, there are three ways
1. [**Initial prototyping and development**](test-certs.md): Use the test certificates and keys included with the SDK libraries to sign claims. These certs and keys are provided for convenience, but aren't valid for actual signing.
1. **Local/internal testing**: Once your code is working with the test certs and keys, you can move on to:
- [Purchase your own certificate](get-cert.md) from a certificate authority (CA).
- [Use the certificate and key *locally*](local-signing.md) (directly from the file system) to sign manifest claims. IMPORTANT: this is *not* safe in production.
- [Use the certificate and key *locally*](local-signing.mdx) (directly from the file system) to sign manifest claims. IMPORTANT: this is *not* safe in production.
1. [**Production testing/deployment**](prod-cert.mdx): To secure your private key for use in a publicly-accessible production application, store it in a hardware security module (HSM) or key management service (KMS) where your application can access it securely .

:::note
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ id: local-signing
title: Signing with local credentials
---

import LocalSignWarning from '../partials/_local-sign-warning.md';

## Overview

To sign a claim in a C2PA manifest you need an end-entity certificate that complies with the C2PA trust model. Then you can use your private key with the certificate to sign it.
Expand All @@ -13,11 +15,9 @@ Trust lists connect the end-entity certificate that signed a manifest back to th

The simplest way to add a C2PA manifest to an asset file and sign it is by using C2PA Tool (`c2patool`). You can run C2PA Tool manually from the command line (for example, during development) and more generally from any executable program that can call out to the shell.

Similarly, using the Rust SDK, you can [add a manifest to an asset file](https://docs.rs/c2pa/latest/c2pa/#example-adding-a-manifest-to-a-file), referencing the certificate and private key file. The [Node.js](../c2pa-node), [Python](../c2pa-python), and [C++](../c2pa-cpp) libraries can also add and sign a manifest.
Similarly, using the Rust SDK, you can [add a manifest to an asset file](https://docs.rs/c2pa/latest/c2pa/#adding-a-signed-manifest-to-a-file), referencing the certificate and private key file. The [Node.js](../c2pa-node), [Python](../c2pa-python), and [C++](../c2pa-cpp) libraries can also add and sign a manifest.

:::warning Warning
Accessing a private key and certificate directly from the file system is fine during development, but doing so in production is not secure. Instead use a Key Management Service (KMS) or a hardware security module (HSM) to access the certificate and key; For more information, see [Using a certificate in production](prod-cert.mdx).
:::
<LocalSignWarning />

## Example

Expand Down
2 changes: 1 addition & 1 deletion docs/signing/prod-cert.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ id: prod-cert
title: Using a signing certificate in production
---

Accessing a private key and certificate [directly from the local file system](local-signing.md) is fine during development, but doing so in production is not secure. Instead use one or both of:
Accessing a private key and certificate [directly from the local file system](local-signing.mdx) is fine during development, but doing so in production is not secure. Instead use one or both of:

- A **key management service** (KMS), to securely store and manage cryptographic keys, including generation, storage, rotation, and revocation. Popular KMS providers include [Amazon KMS](https://aws.amazon.com/kms/), [Google Cloud Key Management](https://cloud.google.com/security/products/security-key-management), and [Azure Key Vault](https://azure.microsoft.com/en-us/products/key-vault).
- A **hardware security module** (HSM), a physical device that attaches directly to a server to securely manage and perform operations on cryptographic keys.
Expand Down
7 changes: 7 additions & 0 deletions docs/tasks/archives.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import TabItem from '@theme/TabItem';
import Rustarchives from './rust/_rust-archives.mdx';
import Cpparchives from './cpp/_cpp-archives.mdx';
import Pythonarchives from './python/_python-archives.mdx';
import Jsarchives from './js/_js-archives.mdx';
import Nodearchives from './node/_node-archives.mdx';

_Working stores_ and _archives_ provide a standard way to save and restore the state of a `Builder`:
Expand Down Expand Up @@ -37,6 +38,12 @@ _Working stores_ and _archives_ provide a standard way to save and restore the s

</TabItem>

<TabItem value="js" label="JavaScript">

<Jsarchives name="js-archives" />

</TabItem>

<TabItem value="node" label="Node.js">

<Nodearchives name="node-archives" />
Expand Down
15 changes: 15 additions & 0 deletions docs/tasks/build.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,15 @@ import TabItem from '@theme/TabItem';
import PythonBuild from './python/_python-build.md';
import CppBuild from './cpp/_cpp-build.md';
import RustBuild from './rust/_rust-build.md';
import JsBuild from './js/_js-build.md';
import JsBuildLocal from './js/_js-build-local.md';

import NodeBuild from './node/_node-build.md';

import LocalSignWarning from '../partials/_local-sign-warning.md';

<LocalSignWarning />

<Tabs groupId="programming-lang" queryString="lang">

<TabItem value="rust" label="Rust">
Expand All @@ -32,6 +39,14 @@ import NodeBuild from './node/_node-build.md';

</TabItem>

<TabItem value="js" label="JavaScript">

<JsBuildLocal name="js-build" />

<JsBuild name="js-build" />

</TabItem>

<TabItem value="node" label="Node.js">

<NodeBuild name="node-build" />
Expand Down
7 changes: 7 additions & 0 deletions docs/tasks/get-resources.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import TabItem from '@theme/TabItem';
import PythonGetResources from './python/_python-get-resources.md';
import CppGetResources from './cpp/_cpp-get-resources.md';
import RustGetResources from './rust/_rust-get-resources.md';
import JsGetResources from './js/_js-get-resources.md';
import NodeGetResources from './node/_node-get-resources.md';

Manifest data can include binary resources such as thumbnail and icon images which are referenced by JUMBF URIs in manifest data.
Expand All @@ -33,6 +34,12 @@ Manifest data can include binary resources such as thumbnail and icon images whi

</TabItem>

<TabItem value="js" label="JavaScript">

<JsGetResources name="js-get-resources" />

</TabItem>

<TabItem value="node" label="Node.js">

<NodeGetResources name="node-get-resources" />
Expand Down
7 changes: 7 additions & 0 deletions docs/tasks/intents.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import TabItem from '@theme/TabItem';
import Rustintents from './rust/_rust-intents.md';
import Cppintents from './cpp/_cpp-intents.md';
import Pythonintents from './python/_python-intents.md';
import Jsintents from './js/_js-intents.md';
import Nodeintents from './node/_node-intents.md';

_Intents_ tell the `Builder` what kind of manifest you are creating. They enable validation, add required default actions, and help prevent invalid operations.
Expand Down Expand Up @@ -43,6 +44,12 @@ There are three types of intents, shown here:

</TabItem>

<TabItem value="js" label="JavaScript">

<Jsintents name="js-intents" />

</TabItem>

<TabItem value="node" label="Node.js">

<Nodeintents name="node-intents" />
Expand Down
90 changes: 90 additions & 0 deletions docs/tasks/js/_js-archives.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import TOCInline from '@theme/TOCInline';

<TOCInline toc={toc} />

The snippets below assume `const c2pa = await createC2pa({ wasmSrc });` is in scope (see [Configuring SDK settings](../settings.mdx) and [Reading manifests](../read.mdx) for initialization). Call `c2pa.dispose()` when you no longer need the worker.

### Save a working store to bytes

[`Builder.toArchive`](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Builder.html#toarchive) returns a `Uint8Array` in standard `application/c2pa` form. You can persist it, attach it as an ingredient, or download it as a sidecar.

```typescript
const builder = await c2pa.builder.fromDefinition({
claim_generator_info: [{ name: 'my-app', version: '1.0.0' }],
title: 'Draft',
format: 'image/jpeg',
assertions: [],
ingredients: [],
});

const ingredientBlob = await fetch('/source.jpg').then((r) => r.blob());
await builder.addIngredientFromBlob(
{ title: 'source.jpg', format: 'image/jpeg', instance_id: 'src-1' },
ingredientBlob.type,
ingredientBlob,
);

const archive = await builder.toArchive();
await builder.free();

const file = new File([archive], 'draft.c2pa', { type: 'application/c2pa' });
```

### Restore a builder from an archive

[`BuilderFactory.fromArchive`](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.BuilderFactory.html#fromarchive) accepts a `Blob` of archive bytes. Optional [settings](../settings.mdx) apply only to that builder instance.

```typescript
const restored = await c2pa.builder.fromArchive(new Blob([archive]));

const definition = await restored.getDefinition();
console.log(definition.ingredients?.length);

await restored.free();
```

### Read an archive with `Reader`

Use MIME type `application/c2pa` so the reader treats the blob as an archive (not a raster image).

```typescript
const reader = await c2pa.reader.fromBlob(
'application/c2pa',
new Blob([archive]),
{ verify: { verifyAfterReading: false } },
);

if (!reader) {
throw new Error('Could not read archive');
}

const store = await reader.manifestStore();
const active = await reader.activeManifest();
await reader.free();
```

### Two-phase workflow

**Phase 1 — prepare** on the client, then upload or store the archive bytes.

**Phase 2 — sign** by loading the archive into a builder and calling `sign` with the final asset blob (see [Adding and signing a manifest](../build.mdx)).

### Use an archive as an ingredient

Pass `application/c2pa` as the ingredient format so provenance from a prior builder is embedded as an ingredient (often with `edit` intent).

```typescript
const archive = await originalBuilder.toArchive();
await originalBuilder.free();

const newBuilder = await c2pa.builder.new();
await newBuilder.setIntent('edit');

await newBuilder.addIngredientFromBlob(
{ title: 'previous-work.c2pa', relationship: 'parentOf' },
'application/c2pa',
new Blob([archive]),
);
```

See [c2pa-web — Creating and reusing builder archives](https://github.com/contentauth/c2pa-js/tree/main/packages/c2pa-web#creating-and-reusing-builder-archives) for additional examples.
141 changes: 141 additions & 0 deletions docs/tasks/js/_js-build-local.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
- [Using a local signer](#using-a-local-signer)
- [Using a remote signer](#using-a-remote-signer)

## Using a local signer

:::info
You can use `c2pa-web` in the browser by implementing the [`Signer`](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Signer.html) interface. That callback can use **local** cryptographic material (for example a non-extractable [`CryptoKey`](https://developer.mozilla.org/en-US/docs/Web/API/CryptoKey) from Web Crypto) so the private key never leaves the client. The current WASM integration uses **direct COSE handling**: `sign` receives the claim bytes and must return a **complete, encoded COSE signature** for C2PA (certificates, timestamps as required by your policy, and the signature itself). A raw ECDSA or RSA signature from `crypto.subtle.sign` is not sufficient on its own. Teams often implement that COSE step in a hardened signing service; doing it entirely in client JavaScript is possible but requires a COSE/C2PA-aware encoder on top of Web Crypto.
:::

Use [`@contentauth/c2pa-web`](https://github.com/contentauth/c2pa-js/tree/main/packages/c2pa-web) to build manifests and sign assets in the browser.

The high-level flow is:
1. Call `await createC2pa({ wasmSrc, settings? })`
1. Call `c2pa.builder.new()` / [`fromDefinition`](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.BuilderFactory.html#fromdefinition) / [`fromArchive`](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.BuilderFactory.html#fromarchive)
1. Add actions, ingredients, thumbnails
1. Call [`sign`](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Builder.html#sign) or [`signAndGetManifestBytes`](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Builder.html#signandgetmanifestbytes).
- To publish a remote manifest instead of embedding, call [`setRemoteUrl`](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Builder.html#setremoteurl) and [`setNoEmbed(true)`](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Builder.html#setnoembed) before this call.
1. Call `await builder.free()` and `c2pa.dispose()`.

You implement the [`Signer`](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Signer.html) interface (`alg`, `reserveSize`, `sign`) so the WASM layer can obtain a COSE signature for each claim.

:::warning
Browser pages are exposed to XSS: any script on the page can try to use keys that JavaScript can reach. Prefer **non-extractable** Web Crypto keys, user-gated imports (file input, `navigator.credentials`, or hardware where supported), and hardening such as CSP. Do not ship production private keys as PEM strings or other recoverable secrets in frontend bundles. If you use a **remote** signer instead, treat it like an API that must authorize exactly what is being signed (for example bind requests to a content hash and tight scopes), not as a generic “sign this blob” endpoint for an authenticated session.
:::

For obtaining and packaging certificates and keys outside the browser, see [Signing with local credentials](../../signing/local-signing).

Embedding signed manifests into binary formats is handled here for supported web formats; for server-side embedding across all formats, use Node, Python, Rust, or C++.

### Local Web Crypto signer example

The following shows how to use a **P-256 PKCS#8** key imported into Web Crypto inside `Signer.sign`. You still need to turn the raw signature and your certificate material into the **COSE Sign1** bytes C2PA expects (and honor `reserveSize`). The library does not ship a browser COSE encoder; use your own implementation or a signing path that already returns COSE.

```typescript
import { createC2pa, type Signer } from '@contentauth/c2pa-web';
import wasmSrc from '@contentauth/c2pa-web/resources/c2pa.wasm?url';

/** Strip PEM headers and decode base64 DER (PKCS#8 private key). */
function pkcs8PemToArrayBuffer(pem: string): ArrayBuffer {
const b64 = pem
.replace(/-----BEGIN PRIVATE KEY-----/, '')
.replace(/-----END PRIVATE KEY-----/, '')
.replace(/\s/g, '');
const binary = atob(b64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return bytes.buffer;
}

async function importEs256PrivateKey(pem: string): Promise<CryptoKey> {
return crypto.subtle.importKey(
'pkcs8',
pkcs8PemToArrayBuffer(pem),
{ name: 'ECDSA', namedCurve: 'P-256' },
false,
['sign']
);
}

function createLocalWebCryptoSigner(privateKey: CryptoKey): Signer {
return {
alg: 'es256',
reserveSize: async () => 4096,
sign: async (toBeSigned: Uint8Array, reserveSize: number) => {
const raw = new Uint8Array(
await crypto.subtle.sign(
{ name: 'ECDSA', hash: 'SHA-256' },
privateKey,
toBeSigned
)
);
// Required for real manifests: build C2PA COSE_Sign1 using `raw`, your
// end-entity certificate chain, timestamp policy, etc., and pad or size
// the result to match `reserveSize`.
void raw;
void reserveSize;
throw new Error(
'Implement COSE Sign1 packaging for C2PA; raw Web Crypto output alone is not enough.'
);
},
};
}

async function run() {
const c2pa = await createC2pa({ wasmSrc });

const resp = await fetch('/image-to-sign.jpg');
const assetBlob = await resp.blob();

// To instead create a minimal manifest definition, use
// const builder = await c2pa.builder.new();
const builder = await c2pa.builder.fromDefinition({
claim_generator_info: [{ name: 'my-app', version: '1.0.0' }],
title: 'My image',
format: 'image/jpeg',
assertions: [],
ingredients: [],
});

await builder.addAction({
action: 'c2pa.created',
digitalSourceType:
'http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture',
});
await builder.setThumbnailFromBlob('image/jpeg', assetBlob);

// Example: load PKCS#8 PEM from a file the user selects (keeps material out of the bundle).
const pem = await new Promise<string>((resolve, reject) => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.pem,.key';
input.onchange = async () => {
const file = input.files?.[0];
if (!file) reject(new Error('No file'));
else resolve(await file.text());
};
input.click();
});

const privateKey = await importEs256PrivateKey(pem);
const signer = createLocalWebCryptoSigner(privateKey);
const signedBytes = await builder.sign(signer, assetBlob.type, assetBlob);

const signedBlob = new Blob([signedBytes], { type: assetBlob.type });
const url = URL.createObjectURL(signedBlob);
const a = document.createElement('a');
a.href = url;
a.download = 'signed.jpg';
a.click();
URL.revokeObjectURL(url);

await builder.free();
c2pa.dispose();
}

void run();
```

:::note
Until `sign` returns valid COSE bytes, `builder.sign` will not succeed. If you already have a service or module that returns **finished** COSE for the claim, you can keep using Web Crypto only for the asymmetric step inside that module, or call that module from `sign` without using `fetch` to your own backend (for example an in-browser worker with the same origin). The important distinction is **where the private key lives** and **who builds COSE**, not whether `sign` is async.
:::
Loading
Loading