From 5e61da74566d160d1a5780cfd445931f9f56b2b0 Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Tue, 28 Apr 2026 15:09:17 -0700 Subject: [PATCH 1/9] Add tasks for js-web --- docs/tasks/archives.mdx | 7 +++ docs/tasks/build.mdx | 7 +++ docs/tasks/get-resources.mdx | 7 +++ docs/tasks/intents.mdx | 7 +++ docs/tasks/js/_js-archives.mdx | 90 ++++++++++++++++++++++++++++++ docs/tasks/js/_js-build.md | 75 ++++++++++++------------- docs/tasks/js/_js-get-resources.md | 51 ++++++++++++++--- docs/tasks/js/_js-intents.md | 70 +++++++++++++++++++++++ docs/tasks/js/_js-read.md | 49 ++++++++++++---- docs/tasks/js/_js-settings.md | 52 ++++++----------- docs/tasks/read.mdx | 7 +++ docs/tasks/settings.mdx | 7 +++ 12 files changed, 337 insertions(+), 92 deletions(-) create mode 100644 docs/tasks/js/_js-archives.mdx create mode 100644 docs/tasks/js/_js-intents.md diff --git a/docs/tasks/archives.mdx b/docs/tasks/archives.mdx index ba345e16..464fd313 100644 --- a/docs/tasks/archives.mdx +++ b/docs/tasks/archives.mdx @@ -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'; _Working stores_ and _archives_ provide a standard way to save and restore the state of a `Builder`: @@ -35,5 +36,11 @@ _Working stores_ and _archives_ provide a standard way to save and restore the s + + + + + + diff --git a/docs/tasks/build.mdx b/docs/tasks/build.mdx index c5db6648..1601485c 100644 --- a/docs/tasks/build.mdx +++ b/docs/tasks/build.mdx @@ -10,6 +10,7 @@ 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'; @@ -30,5 +31,11 @@ import RustBuild from './rust/_rust-build.md'; + + + + + + diff --git a/docs/tasks/get-resources.mdx b/docs/tasks/get-resources.mdx index 9d113561..60df2dba 100644 --- a/docs/tasks/get-resources.mdx +++ b/docs/tasks/get-resources.mdx @@ -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'; Manifest data can include binary resources such as thumbnail and icon images which are referenced by JUMBF URIs in manifest data. @@ -32,4 +33,10 @@ Manifest data can include binary resources such as thumbnail and icon images whi + + + + + + diff --git a/docs/tasks/intents.mdx b/docs/tasks/intents.mdx index 752a1714..e28d1cfa 100644 --- a/docs/tasks/intents.mdx +++ b/docs/tasks/intents.mdx @@ -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'; _Intents_ tell the `Builder` what kind of manifest you are creating. They enable validation, add required default actions, and help prevent invalid operations. @@ -41,5 +42,11 @@ There are three types of intents, shown here: + + + + + + diff --git a/docs/tasks/js/_js-archives.mdx b/docs/tasks/js/_js-archives.mdx new file mode 100644 index 00000000..49c6b5c7 --- /dev/null +++ b/docs/tasks/js/_js-archives.mdx @@ -0,0 +1,90 @@ +import TOCInline from '@theme/TOCInline'; + + + +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. diff --git a/docs/tasks/js/_js-build.md b/docs/tasks/js/_js-build.md index 34ae5165..246a3df5 100644 --- a/docs/tasks/js/_js-build.md +++ b/docs/tasks/js/_js-build.md @@ -1,95 +1,90 @@ -Using client-side JavaScript, you can: -- Create and compose manifests using the JavaScript library manifest `builder` API. -- Perform signing in the browser if you have a private key available to the client (using WebCrypto, an imported key, or an ephemeral/test key). The web library provides a `Signer` interface you can implement to call into whatever signing capability you have in the browser. -- Produce a manifest store or sidecar (`.c2pa`) file entirely in the browser (so you can download a signed sidecar next to the asset). -Signing Content Credentials requires an end-entity X.509 certificate that fits the C2PA trust model to produce publicly verifiable signatures. Getting and protecting that certificate/private key in a browser is risky and should not be done in production. +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: `await createC2pa({ wasmSrc, settings? })` → `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) → add actions, ingredients, thumbnails → [`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) → `await builder.free()` and `c2pa.dispose()`. -::: warning -Never put production private keys into client code! +You implement the [`Signer`](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Signer.html) interface (`alg`, `reserveSize`, `sign`) so signing can call your backend, WebCrypto, or a test key. + +:::warning +Never embed production private keys in client-side code. Prefer a remote signer (your API, KMS, or HSM) and harden the page against XSS. ::: -Embedding into binaries: While you can build and sign manifests client-side, embedding a signed manifest properly into binary file formats (JPEG/PNG/MP4, etc.) is functionality that server-side libraries (Node.js, Python, C/C++, and Rust) explicitly provide. +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++. -Security & auditability: Browser-stored keys (or keys entered by users) are exposed to theft, malware, or cross-site scripting (XSS) attacks. For production signing, best practice is to use a remote signer (KMS/HSM) or server-side signing where keys are protected and signing operations are auditable. +### Remote signer example -```js +```typescript import { createC2pa, type Signer } from '@contentauth/c2pa-web'; import wasmSrc from '@contentauth/c2pa-web/resources/c2pa.wasm?url'; -// 1) Create a Signer that calls your backend (which returns the signature bytes) function createRemoteSigner(): Signer { return { alg: 'es256', - reserveSize: async () => 4096, // bytes to reserve for TSA/countersignature (tune as needed) - sign: async (toBeSigned: Uint8Array, _reserveSize: number) => { + reserveSize: async () => 4096, + sign: async (toBeSigned: Uint8Array) => { const res = await fetch('/api/sign', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - alg: 'es256', - payload: Array.from(toBeSigned), - }), + headers: { 'Content-Type': 'application/octet-stream' }, + body: toBeSigned, }); if (!res.ok) throw new Error('Signing failed'); - const sigBytes = new Uint8Array(await res.arrayBuffer()); - return sigBytes; + return new Uint8Array(await res.arrayBuffer()); }, }; } async function run() { - // 2) Initialize the SDK const c2pa = await createC2pa({ wasmSrc }); - // 3) Fetch the asset to sign - const imgUrl = 'https://contentauth.github.io/example-assets/images/cloudscape-ACA-Cr.jpeg'; - const resp = await fetch(imgUrl); + const resp = await fetch('/image-to-sign.jpg'); const assetBlob = await resp.blob(); - // 4) Build a simple manifest (add a created action and optional thumbnail) const builder = await c2pa.builder.new(); await builder.addAction({ action: 'c2pa.created', - digitalSourceType: 'http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture', + digitalSourceType: + 'http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture', }); - // Optional: include a thumbnail that represents the asset await builder.setThumbnailFromBlob('image/jpeg', assetBlob); - // 5) Sign and get a new asset with the manifest embedded const signer = createRemoteSigner(); const signedBytes = await builder.sign(signer, assetBlob.type, assetBlob); - // 6) Use/save the signed asset const signedBlob = new Blob([signedBytes], { type: assetBlob.type }); const url = URL.createObjectURL(signedBlob); - // e.g., download const a = document.createElement('a'); a.href = url; a.download = 'signed.jpg'; a.click(); URL.revokeObjectURL(url); - // Cleanup await builder.free(); c2pa.dispose(); } -run().catch(console.error); +void run(); ``` -To retrieve manifest bytes alongside the signed asset: +### Manifest bytes and remote manifest -```js +```typescript const { asset, manifest } = await builder.signAndGetManifestBytes( signer, assetBlob.type, - assetBlob + assetBlob, ); -// asset -> signed asset bytes -// manifest -> embedded manifest bytes ``` -Notes: -- You provide the `Signer` used in the example above. In production, this object wraps a service/HSM that returns a proper signature for your algorithm (`es256`, `ps256`, `ed25519`, etc.). Set `reserveSize` to a value large enough for timestamps/countersignatures your signer adds. -- To attach a remote manifest instead of embedding, use `builder.setRemoteUrl(url)` and `builder.setNoEmbed(true)` before signing. \ No newline at end of file +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 `sign`. + +### Start from a manifest definition + +```typescript +const builder = await c2pa.builder.fromDefinition({ + claim_generator_info: [{ name: 'my-app', version: '1.0.0' }], + title: 'My image', + format: 'image/jpeg', + assertions: [], + ingredients: [], +}); +``` + +For [intents](../intents.mdx) and [archives](../archives.mdx), see the dedicated task pages. diff --git a/docs/tasks/js/_js-get-resources.md b/docs/tasks/js/_js-get-resources.md index 31851c36..742d2279 100644 --- a/docs/tasks/js/_js-get-resources.md +++ b/docs/tasks/js/_js-get-resources.md @@ -1,19 +1,54 @@ -The example below shows how to get resources from manifest data using the JavaScript library. -```js +After you obtain a [`Reader`](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Reader.html), use [`resourceToBytes(uri)`](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Reader.html#resourcetobytes) to resolve a JUMBF URI from the manifest (for example `activeManifest.thumbnail.identifier`) into raw bytes. + +### Thumbnail from the active manifest + +```typescript import { createC2pa } from '@contentauth/c2pa-web'; import wasmSrc from '@contentauth/c2pa-web/resources/c2pa.wasm?url'; -const c2pa = createC2pa({ wasmSrc }); -const response = await fetch( - 'https://contentauth.github.io/example-assets/images/Firefly_tabby_cat.jpg' -); +const c2pa = await createC2pa({ wasmSrc }); +const response = await fetch('/signed-image.jpg'); const blob = await response.blob(); + const reader = await c2pa.reader.fromBlob(blob.type, blob); +if (!reader) { + c2pa.dispose(); + return; +} + +const active = await reader.activeManifest(); +const uri = active.thumbnail?.identifier; +if (!uri) { + await reader.free(); + c2pa.dispose(); + return; +} -... +const bytes = await reader.resourceToBytes(uri); +const thumbBlob = new Blob([bytes], { + type: active.thumbnail?.format ?? 'image/jpeg', +}); +const thumbUrl = URL.createObjectURL(thumbBlob); + +await reader.free(); +c2pa.dispose(); ``` -More TBD. +### Ingredient resources + +Ingredients can expose their own `thumbnail` or other resource references. Inspect `active.ingredients` (or the full [`manifestStore`](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Reader.html#manifeststore)) and pass each `identifier` string to `resourceToBytes`. +```typescript +const store = await reader.manifestStore(); +const label = store.active_manifest; +const manifest = label ? store.manifests[label] : undefined; + +for (const ing of manifest?.ingredients ?? []) { + if (ing.thumbnail?.identifier) { + const data = await reader.resourceToBytes(ing.thumbnail.identifier); + // use data (Uint8Array) + } +} +``` diff --git a/docs/tasks/js/_js-intents.md b/docs/tasks/js/_js-intents.md new file mode 100644 index 00000000..a8730ad5 --- /dev/null +++ b/docs/tasks/js/_js-intents.md @@ -0,0 +1,70 @@ +### Setting the intent + +The snippets below use `c2pa` from `const c2pa = await createC2pa({ wasmSrc });` (see [Reading and verifying manifest data](../read.mdx)). + +Call [`setIntent`](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Builder.html#setintent) on a builder from `c2pa.builder`. Intents match the same **Create** / **Edit** / **Update** semantics as other CAI SDKs (see the table on this page). + +```typescript +const builder = await c2pa.builder.new(); + +await builder.setIntent({ + create: + 'http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia', +}); + +await builder.setIntent('edit'); + +await builder.setIntent('update'); +``` + +### Create intent + +Use `create` with a [digital source type](https://c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_digital_source_type) URI. There must be no parent ingredient; the SDK may add `c2pa.created` when appropriate. + +```typescript +const builder = await c2pa.builder.new(); + +await builder.setIntent({ + create: + 'http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture', +}); +// Add assertions, thumbnail, then sign (see [Build](../build.mdx)). +``` + +### Edit intent + +Use `edit` when changing pixel or editorial content. If you do not add a parent ingredient, one can be derived from the source blob you pass to [`sign`](../build.mdx). + +```typescript +const builder = await c2pa.builder.new(); +await builder.setIntent('edit'); +``` + +Add a parent explicitly with [`addIngredientFromBlob`](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Builder.html#addingredientfromblob): + +```typescript +const parentBlob = await fetch('/original.jpg').then((r) => r.blob()); +const builder = await c2pa.builder.new(); +await builder.setIntent('edit'); + +await builder.addIngredientFromBlob( + { + title: 'Original Photo', + relationship: 'parentOf', + format: 'image/jpeg', + }, + parentBlob.type, + parentBlob, +); +``` + +### Update intent + +Use `update` for restricted, metadata-oriented edits (single parent ingredient, no change to the parent’s hashed payload per C2PA rules). + +```typescript +const builder = await c2pa.builder.new(); +await builder.setIntent('update'); +``` + +More background: [c2pa-rs `Builder`](https://docs.rs/c2pa/latest/c2pa/struct.Builder.html) and the [c2pa-web README](https://github.com/contentauth/c2pa-js/tree/main/packages/c2pa-web#setting-builder-intent). diff --git a/docs/tasks/js/_js-read.md b/docs/tasks/js/_js-read.md index 36942302..ebbf6fcf 100644 --- a/docs/tasks/js/_js-read.md +++ b/docs/tasks/js/_js-read.md @@ -1,25 +1,54 @@ -Once you've used [`createC2pa`](https://contentauth.github.io/c2pa-js/functions/_contentauth_c2pa-web.index.createC2pa.html) to create an instance of c2pa-web (for example in `c2pa` in this example), use `c2pa.reader.fromBlob()` to create a [Reader](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.index.Reader.html) for an asset. +Initialize the SDK with [`createC2pa`](https://contentauth.github.io/c2pa-js/modules/_contentauth_c2pa-web.html#createc2pa), then use [`reader.fromBlob`](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.ReaderFactory.html#fromblob) to open an asset. Call [`manifestStore`](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Reader.html#manifeststore) for the full store, or [`activeManifest`](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Reader.html#activemanifest) for the active claim. -Then use Reader's [`manifestStore()` method](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.index.Reader.html#manifeststore) to read manifest data (if any) from the asset. +Always call [`reader.free()`](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Reader.html#free) when finished, and [`c2pa.dispose()`](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.C2paSdk.html#dispose) when tearing down the worker. -For example: +### Read from a `Blob` -```js +With Vite (or similar), load the Wasm URL: + +```typescript import { createC2pa } from '@contentauth/c2pa-web'; import wasmSrc from '@contentauth/c2pa-web/resources/c2pa.wasm?url'; -const c2pa = createC2pa({ wasmSrc }); -const response = await fetch( - 'https://contentauth.github.io/example-assets/images/Firefly_tabby_cat.jpg' -); +const c2pa = await createC2pa({ wasmSrc }); +const response = await fetch('/signed-image.jpg'); const blob = await response.blob(); + const reader = await c2pa.reader.fromBlob(blob.type, blob); +if (!reader) { + console.log('No C2PA manifest found.'); + c2pa.dispose(); + return; +} + const manifestStore = await reader.manifestStore(); +console.log(JSON.stringify(manifestStore, null, 2)); -console.log(manifestStore); +const active = await reader.activeManifest(); +console.log('Active title:', active.title); -// Free SDK objects when they are no longer needed to avoid memory leaks. await reader.free(); +c2pa.dispose(); +``` + +Optional [per-read settings](../settings.mdx) override the defaults passed to `createC2pa`: + +```typescript +const reader = await c2pa.reader.fromBlob(blob.type, blob, { + verify: { verifyAfterReading: false, verifyTrust: true }, +}); ``` + +### Inline Wasm (no separate `.wasm` request) + +For constrained environments, use [`@contentauth/c2pa-web/inline`](https://github.com/contentauth/c2pa-js/tree/main/packages/c2pa-web#using-an-inline-wasm-binary) (larger bundle): + +```typescript +import { createC2pa } from '@contentauth/c2pa-web/inline'; + +const c2pa = await createC2pa(); +``` + +See the [c2pa-web README](https://github.com/contentauth/c2pa-js/tree/main/packages/c2pa-web) for CDN-hosted Wasm and API details. diff --git a/docs/tasks/js/_js-settings.md b/docs/tasks/js/_js-settings.md index 54108721..0a355cec 100644 --- a/docs/tasks/js/_js-settings.md +++ b/docs/tasks/js/_js-settings.md @@ -1,59 +1,43 @@ -Load settings as defined inline: -```js +In the browser, there is no `Context` class. You pass a **camelCase** [`Settings`](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Settings.html) object to [`createC2pa`](https://contentauth.github.io/c2pa-js/modules/_contentauth_c2pa-web.html#createc2pa); that becomes the default for new readers and builders. You can still pass a `Settings` object as the last argument to [`reader.fromBlob`](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.ReaderFactory.html#fromblob) or [`builder.new`](https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.BuilderFactory.html#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) to override those defaults for a single instance. + +For the full JSON schema (including `version` and snake_case fields used by Rust tooling), see [SDK object reference — Settings](../../manifest/json-ref/settings-schema). The web SDK maps the camelCase `Settings` shape to what the Wasm layer expects. + +### Default `createC2pa` settings + +```typescript import { createC2pa } from '@contentauth/c2pa-web'; -// With Vite (or similar), this resolves to the hosted WASM binary URL: import wasmSrc from '@contentauth/c2pa-web/resources/c2pa.wasm?url'; const settings = { - // Turn trust verification on/off (defaults to true) verify: { verifyTrust: true }, - - // Configure trust (PEM text, a URL, or an array of URLs) trust: { - // Example: load system trust anchors from a URL (PEM file) trustAnchors: 'https://example.com/trust_anchors.pem', - // Optional user anchors (also PEM text or URLs) - // userAnchors: '-----BEGIN CERTIFICATE-----\n...' }, - - // Optional builder settings - // builder: { generateC2paArchive: true }, + builder: { generateC2paArchive: true }, }; const c2pa = await createC2pa({ wasmSrc, settings }); - -// Use the SDK (example: read an asset) -const res = await fetch('https://contentauth.github.io/example-assets/images/cloudscape-ACA-Cr.jpeg'); -const blob = await res.blob(); - -const reader = await c2pa.reader.fromBlob(blob.type, blob); -const manifestStore = await reader.manifestStore(); -console.log(manifestStore); - -await reader.free(); -c2pa.dispose(); ``` -To load settings from a JSON file: - -```js -import { createC2pa } from '@contentauth/c2pa-web'; -import wasmSrc from '@contentauth/c2pa-web/resources/c2pa.wasm?url'; +### Load JSON from your origin -const settings = await fetch('/c2pa-settings.json').then(r => r.json()); -const c2pa = await createC2pa({ wasmSrc, settings }); +```typescript +const loaded = await fetch('/c2pa-settings.json').then((r) => r.json()); +const c2pa = await createC2pa({ wasmSrc, settings: loaded }); ``` -Where `c2pa-settings.json` is: +Only fetch settings from origins you trust, over HTTPS. + +Example `c2pa-settings.json` (camelCase keys as consumed by c2pa-web): -```js +```json { - "verify": { "verifyTrust": true }, + "verify": { "verifyTrust": true, "verifyAfterReading": true }, "trust": { "trustAnchors": "https://example.com/trust_anchors.pem", "userAnchors": "https://example.com/user_anchors.pem" }, "builder": { "generateC2paArchive": true } } -``` \ No newline at end of file +``` diff --git a/docs/tasks/read.mdx b/docs/tasks/read.mdx index fe80cc31..30493317 100644 --- a/docs/tasks/read.mdx +++ b/docs/tasks/read.mdx @@ -10,6 +10,7 @@ import TabItem from '@theme/TabItem'; import PythonRead from './python/_python-read.md'; import CppRead from './cpp/_cpp-read.md'; import RustRead from './rust/_rust-read.md'; +import JsRead from './js/_js-read.md'; @@ -31,4 +32,10 @@ import RustRead from './rust/_rust-read.md'; + + + + + + diff --git a/docs/tasks/settings.mdx b/docs/tasks/settings.mdx index acd23431..f4d6ae18 100644 --- a/docs/tasks/settings.mdx +++ b/docs/tasks/settings.mdx @@ -10,6 +10,7 @@ import TabItem from '@theme/TabItem'; import PythonRead from './python/_python-settings.md'; import CppRead from './cpp/_cpp-settings.md'; import RustRead from './rust/_rust-settings.md'; +import JsSettings from './js/_js-settings.md'; Regardless of which language you're working in, you use the `Context` and `Settings` classes to control SDK behavior including verification, trust anchors, thumbnails, signing, and more. @@ -92,4 +93,10 @@ For Boolean values, use JSON `true` and `false`, not the strings `"true"` and `" + + + + + + From 99ddb1dfb035bcff018453aa39efc3f2ad675817 Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Wed, 29 Apr 2026 08:33:15 -0700 Subject: [PATCH 2/9] Update settings doc --- docs/tasks/js/_js-build.md | 11 +++++++++-- docs/tasks/settings.mdx | 33 +++++++++++++-------------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/tasks/js/_js-build.md b/docs/tasks/js/_js-build.md index 246a3df5..94aaf3d4 100644 --- a/docs/tasks/js/_js-build.md +++ b/docs/tasks/js/_js-build.md @@ -1,10 +1,17 @@ -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: `await createC2pa({ wasmSrc, settings? })` → `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) → add actions, ingredients, thumbnails → [`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) → `await builder.free()` and `c2pa.dispose()`. +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) +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 signing can call your backend, WebCrypto, or a test key. :::warning -Never embed production private keys in client-side code. Prefer a remote signer (your API, KMS, or HSM) and harden the page against XSS. +Never embed production private keys in client-side code. Instead use a remote signer (your API, KMS, or HSM) and harden the page against XSS. ::: 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++. diff --git a/docs/tasks/settings.mdx b/docs/tasks/settings.mdx index f4d6ae18..0c0646cc 100644 --- a/docs/tasks/settings.mdx +++ b/docs/tasks/settings.mdx @@ -12,29 +12,14 @@ import CppRead from './cpp/_cpp-settings.md'; import RustRead from './rust/_rust-settings.md'; import JsSettings from './js/_js-settings.md'; -Regardless of which language you're working in, you use the `Context` and `Settings` classes to control SDK behavior including verification, trust anchors, thumbnails, signing, and more. - -## Overview of Context - -`Context` encapsulates SDK configuration that controls how `Reader`, `Builder`, and other components operate, including: - -- **Settings**: Trust configuration, builder behavior, thumbnails, and more. -- **Signer configuration**: Optional signing credentials that can be stored for reuse. - -Using `Context` provides explicit, isolated configuration without thread-local state. It enables you to run different configurations simultaneously (for example for development with test certificates or production with strict validation), simplifies testing, and improves code clarity. - -`Context`: - -- Can be moved but not copied. After moving, `is_valid()` returns `false` on the source. -- Is used at construction: `Reader` and `Builder` copy configuration from the `Context` at construction time. The `Context` doesn't need to outlive them. -- Is reusable: Use the same `Context` to create multiple `Reader` and `Builder` instances. +Regardless of which language you're working in, you use the `Context` and `Settings` classes to control SDK behavior including verification, trust anchors, thumbnails, signing, and more: +- `Settings` is a declarative configuration for the SDK that you can load from a JSON file or set programmatically. +- `Context` controls how `Reader`, `Builder`, and other components operate, including: + - **Settings**: Trust configuration, builder behavior, thumbnails, and more. + - **Signer configuration**: Optional signing credentials that can be stored for reuse. ## Overview of Settings -You can specify a declarative configuration for the SDK using `Settings`, that you can load from a JSON file or set programmatically. - -### Settings object structure - > [!TIP] > For the complete reference to the Settings object, see [SDK object reference - Settings](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema). @@ -73,6 +58,14 @@ For Boolean values, use JSON `true` and `false`, not the strings `"true"` and `" ## Creating and using Context +Using `Context` provides explicit, isolated configuration without thread-local state. It enables you to run different configurations simultaneously (for example for development with test certificates or production with strict validation), simplifies testing, and improves code clarity. + +`Context`: + +- Can be moved but not copied. After moving, `is_valid()` returns `false` on the source. +- Is used at construction: `Reader` and `Builder` copy configuration from the `Context` at construction time. The `Context` doesn't need to outlive them. +- Is reusable: Use the same `Context` to create multiple `Reader` and `Builder` instances. + From ff6834acdc4ca260aaa55606361b99100012fa91 Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Wed, 6 May 2026 13:21:42 -0700 Subject: [PATCH 3/9] Add caveat instead of example of using remote signging --- docs/tasks/js/_js-build.md | 97 +------------------------------------- 1 file changed, 1 insertion(+), 96 deletions(-) diff --git a/docs/tasks/js/_js-build.md b/docs/tasks/js/_js-build.md index 94aaf3d4..a5a1b865 100644 --- a/docs/tasks/js/_js-build.md +++ b/docs/tasks/js/_js-build.md @@ -1,97 +1,2 @@ +Although you can use [`@contentauth/c2pa-web`](https://github.com/contentauth/c2pa-js/tree/main/packages/c2pa-web) to build manifests and sign assets in the browser using a remote signing service, doing so presents a security vulnerability because someone can use an authenticated session to call the signing endpoint to sign an arbitrary claim. -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) -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 signing can call your backend, WebCrypto, or a test key. - -:::warning -Never embed production private keys in client-side code. Instead use a remote signer (your API, KMS, or HSM) and harden the page against XSS. -::: - -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++. - -### Remote signer example - -```typescript -import { createC2pa, type Signer } from '@contentauth/c2pa-web'; -import wasmSrc from '@contentauth/c2pa-web/resources/c2pa.wasm?url'; - -function createRemoteSigner(): Signer { - return { - alg: 'es256', - reserveSize: async () => 4096, - sign: async (toBeSigned: Uint8Array) => { - const res = await fetch('/api/sign', { - method: 'POST', - headers: { 'Content-Type': 'application/octet-stream' }, - body: toBeSigned, - }); - if (!res.ok) throw new Error('Signing failed'); - return new Uint8Array(await res.arrayBuffer()); - }, - }; -} - -async function run() { - const c2pa = await createC2pa({ wasmSrc }); - - const resp = await fetch('/image-to-sign.jpg'); - const assetBlob = await resp.blob(); - - const builder = await c2pa.builder.new(); - await builder.addAction({ - action: 'c2pa.created', - digitalSourceType: - 'http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture', - }); - await builder.setThumbnailFromBlob('image/jpeg', assetBlob); - - const signer = createRemoteSigner(); - 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(); -``` - -### Manifest bytes and remote manifest - -```typescript -const { asset, manifest } = await builder.signAndGetManifestBytes( - signer, - assetBlob.type, - assetBlob, -); -``` - -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 `sign`. - -### Start from a manifest definition - -```typescript -const builder = await c2pa.builder.fromDefinition({ - claim_generator_info: [{ name: 'my-app', version: '1.0.0' }], - title: 'My image', - format: 'image/jpeg', - assertions: [], - ingredients: [], -}); -``` - -For [intents](../intents.mdx) and [archives](../archives.mdx), see the dedicated task pages. From d1ed4fd28dcd8877b47c5c6c333e8c65f604dd76 Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Wed, 6 May 2026 13:26:26 -0700 Subject: [PATCH 4/9] wording --- docs/tasks/js/_js-build.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/tasks/js/_js-build.md b/docs/tasks/js/_js-build.md index a5a1b865..14bb4d16 100644 --- a/docs/tasks/js/_js-build.md +++ b/docs/tasks/js/_js-build.md @@ -1,2 +1,3 @@ -Although you can use [`@contentauth/c2pa-web`](https://github.com/contentauth/c2pa-js/tree/main/packages/c2pa-web) to build manifests and sign assets in the browser using a remote signing service, doing so presents a security vulnerability because someone can use an authenticated session to call the signing endpoint to sign an arbitrary claim. +Although you can use [`@contentauth/c2pa-web`](https://github.com/contentauth/c2pa-js/tree/main/packages/c2pa-web) to build manifests and sign assets in the browser using a remote signing service, doing so presents a security vulnerability because someone can use an authenticated session to call the signing endpoint to sign any asset. +Instead, perform signing using one of the other libraries, for example, Node.js, C++, or Python. From 96138164652c301e6409261f0b1ab88c2a552d6b Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Thu, 7 May 2026 13:17:04 -0700 Subject: [PATCH 5/9] Change example code per Tim --- docs/tasks/js/_js-get-resources.md | 44 ++++++++++++++++-------------- docs/tasks/js/_js-read.md | 34 +++++++++++++---------- 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/docs/tasks/js/_js-get-resources.md b/docs/tasks/js/_js-get-resources.md index 742d2279..090f0400 100644 --- a/docs/tasks/js/_js-get-resources.md +++ b/docs/tasks/js/_js-get-resources.md @@ -7,33 +7,37 @@ After you obtain a [`Reader`](https://contentauth.github.io/c2pa-js/interfaces/_ import { createC2pa } from '@contentauth/c2pa-web'; import wasmSrc from '@contentauth/c2pa-web/resources/c2pa.wasm?url'; -const c2pa = await createC2pa({ wasmSrc }); +async function getActiveManifestThumbnail(url) { + const c2pa = await createC2pa({ wasmSrc }); -const response = await fetch('/signed-image.jpg'); -const blob = await response.blob(); + const response = await fetch(url); + const blob = await response.blob(); -const reader = await c2pa.reader.fromBlob(blob.type, blob); -if (!reader) { - c2pa.dispose(); - return; -} + const reader = await c2pa.reader.fromBlob(blob.type, blob); + if (!reader) { + c2pa.dispose(); + return; + } + + const active = await reader.activeManifest(); + const uri = active.thumbnail?.identifier; + if (!uri) { + await reader.free(); + c2pa.dispose(); + return; + } + + const bytes = await reader.resourceToBytes(uri); + const thumbBlob = new Blob([bytes], { + type: active.thumbnail?.format ?? 'image/jpeg', + }); + const thumbUrl = URL.createObjectURL(thumbBlob); -const active = await reader.activeManifest(); -const uri = active.thumbnail?.identifier; -if (!uri) { await reader.free(); c2pa.dispose(); - return; } -const bytes = await reader.resourceToBytes(uri); -const thumbBlob = new Blob([bytes], { - type: active.thumbnail?.format ?? 'image/jpeg', -}); -const thumbUrl = URL.createObjectURL(thumbBlob); - -await reader.free(); -c2pa.dispose(); +await getActiveManifestThumbnail('/signed-image.jpg'); ``` ### Ingredient resources diff --git a/docs/tasks/js/_js-read.md b/docs/tasks/js/_js-read.md index ebbf6fcf..5c6cf02a 100644 --- a/docs/tasks/js/_js-read.md +++ b/docs/tasks/js/_js-read.md @@ -11,26 +11,30 @@ With Vite (or similar), load the Wasm URL: import { createC2pa } from '@contentauth/c2pa-web'; import wasmSrc from '@contentauth/c2pa-web/resources/c2pa.wasm?url'; -const c2pa = await createC2pa({ wasmSrc }); +async function readCredentials(url) { + const c2pa = await createC2pa({ wasmSrc }); -const response = await fetch('/signed-image.jpg'); -const blob = await response.blob(); + const response = await fetch(url); + const blob = await response.blob(); -const reader = await c2pa.reader.fromBlob(blob.type, blob); -if (!reader) { - console.log('No C2PA manifest found.'); - c2pa.dispose(); - return; -} + const reader = await c2pa.reader.fromBlob(blob.type, blob); + if (!reader) { + console.log('No C2PA manifest found.'); + c2pa.dispose(); + return; + } + + const manifestStore = await reader.manifestStore(); + console.log(JSON.stringify(manifestStore, null, 2)); -const manifestStore = await reader.manifestStore(); -console.log(JSON.stringify(manifestStore, null, 2)); + const active = await reader.activeManifest(); + console.log('Active title:', active.title); -const active = await reader.activeManifest(); -console.log('Active title:', active.title); + await reader.free(); + c2pa.dispose(); +} -await reader.free(); -c2pa.dispose(); +await readCredentials('/signed-image.jpg'); ``` Optional [per-read settings](../settings.mdx) override the defaults passed to `createC2pa`: From 7137b80d76a6b948665894f03866037a774be23d Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Thu, 7 May 2026 14:35:22 -0700 Subject: [PATCH 6/9] Add warning partial, restore js build code ex --- docs/partials/_local-sign-warning.md | 3 + docs/tasks/build.mdx | 4 ++ docs/tasks/js/_js-build.md | 91 +++++++++++++++++++++++++++- 3 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 docs/partials/_local-sign-warning.md diff --git a/docs/partials/_local-sign-warning.md b/docs/partials/_local-sign-warning.md new file mode 100644 index 00000000..e443c4bc --- /dev/null +++ b/docs/partials/_local-sign-warning.md @@ -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). +::: \ No newline at end of file diff --git a/docs/tasks/build.mdx b/docs/tasks/build.mdx index d20cfd38..3e5ba03d 100644 --- a/docs/tasks/build.mdx +++ b/docs/tasks/build.mdx @@ -13,6 +13,10 @@ import RustBuild from './rust/_rust-build.md'; import JsBuild from './js/_js-build.md'; import NodeBuild from './node/_node-build.md'; +import LocalSignWarning from '../partials/_local-sign-warning.md'; + + + diff --git a/docs/tasks/js/_js-build.md b/docs/tasks/js/_js-build.md index 14bb4d16..b8c2655f 100644 --- a/docs/tasks/js/_js-build.md +++ b/docs/tasks/js/_js-build.md @@ -1,3 +1,90 @@ -Although you can use [`@contentauth/c2pa-web`](https://github.com/contentauth/c2pa-js/tree/main/packages/c2pa-web) to build manifests and sign assets in the browser using a remote signing service, doing so presents a security vulnerability because someone can use an authenticated session to call the signing endpoint to sign any asset. +:::info +Although you can use `c2pa-web` to build manifests and sign assets in the browser using a remote signing service, doing so presents a security vulnerability because someone can use an authenticated session to call the signing endpoint to sign any asset. +::: + +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 signing can call your backend, WebCrypto, or a test key. + +:::warning +Never embed production private keys in client-side code. Instead use a remote signer (your API, KMS, or HSM) and harden the page against XSS. +::: + +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++. + +### Remote signer example + +```typescript +import { createC2pa, type Signer } from '@contentauth/c2pa-web'; +import wasmSrc from '@contentauth/c2pa-web/resources/c2pa.wasm?url'; + +function createRemoteSigner(): Signer { + return { + alg: 'es256', + reserveSize: async () => 4096, + sign: async (toBeSigned: Uint8Array) => { + const res = await fetch('/api/sign', { + method: 'POST', + headers: { 'Content-Type': 'application/octet-stream' }, + body: toBeSigned, + }); + if (!res.ok) throw new Error('Signing failed'); + return new Uint8Array(await res.arrayBuffer()); + }, + }; +} + +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 defiintion, 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); + + const signer = createRemoteSigner(); + 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(); +``` + + + -Instead, perform signing using one of the other libraries, for example, Node.js, C++, or Python. From 5fcd117fb96c9bcace5ac42e3c504a11db191958 Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Thu, 7 May 2026 14:38:42 -0700 Subject: [PATCH 7/9] Use partial --- docs/signing/index.md | 2 +- docs/signing/{local-signing.md => local-signing.mdx} | 8 +++++--- docs/signing/prod-cert.mdx | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) rename docs/signing/{local-signing.md => local-signing.mdx} (95%) diff --git a/docs/signing/index.md b/docs/signing/index.md index 96ca5924..bc1382ca 100644 --- a/docs/signing/index.md +++ b/docs/signing/index.md @@ -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 diff --git a/docs/signing/local-signing.md b/docs/signing/local-signing.mdx similarity index 95% rename from docs/signing/local-signing.md rename to docs/signing/local-signing.mdx index 0ff9535c..93834867 100644 --- a/docs/signing/local-signing.md +++ b/docs/signing/local-signing.mdx @@ -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. @@ -15,9 +17,9 @@ The simplest way to add a C2PA manifest to an asset file and sign it is by using 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. -:::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). -::: + + + ## Example diff --git a/docs/signing/prod-cert.mdx b/docs/signing/prod-cert.mdx index 2065b087..52f989b4 100644 --- a/docs/signing/prod-cert.mdx +++ b/docs/signing/prod-cert.mdx @@ -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. From 17d64e59648386fbc92e974135268bed8263ea97 Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Thu, 7 May 2026 14:43:14 -0700 Subject: [PATCH 8/9] c2pa-cpp --- docs/signing/local-signing.mdx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/signing/local-signing.mdx b/docs/signing/local-signing.mdx index 93834867..671cda3f 100644 --- a/docs/signing/local-signing.mdx +++ b/docs/signing/local-signing.mdx @@ -15,9 +15,7 @@ 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. From 8f01df9af233335352a1ccc3dc9ea28036b1d413 Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Fri, 8 May 2026 10:30:38 -0700 Subject: [PATCH 9/9] Add strawman local signer for JS --- docs/tasks/build.mdx | 4 + docs/tasks/js/_js-build-local.md | 141 +++++++++++++++++++++++++++++++ docs/tasks/js/_js-build.md | 2 + 3 files changed, 147 insertions(+) create mode 100644 docs/tasks/js/_js-build-local.md diff --git a/docs/tasks/build.mdx b/docs/tasks/build.mdx index 3e5ba03d..c1644d39 100644 --- a/docs/tasks/build.mdx +++ b/docs/tasks/build.mdx @@ -11,6 +11,8 @@ 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'; @@ -39,6 +41,8 @@ import LocalSignWarning from '../partials/_local-sign-warning.md'; + + diff --git a/docs/tasks/js/_js-build-local.md b/docs/tasks/js/_js-build-local.md new file mode 100644 index 00000000..56ba007a --- /dev/null +++ b/docs/tasks/js/_js-build-local.md @@ -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 { + 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((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. +::: diff --git a/docs/tasks/js/_js-build.md b/docs/tasks/js/_js-build.md index b8c2655f..eb4b5959 100644 --- a/docs/tasks/js/_js-build.md +++ b/docs/tasks/js/_js-build.md @@ -1,3 +1,5 @@ +## Using a remote signer + :::info Although you can use `c2pa-web` to build manifests and sign assets in the browser using a remote signing service, doing so presents a security vulnerability because someone can use an authenticated session to call the signing endpoint to sign any asset. :::