diff --git a/docs/partials/_local-sign-warning.md b/docs/partials/_local-sign-warning.md new file mode 100644 index 0000000..e443c4b --- /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/signing/index.md b/docs/signing/index.md index 96ca592..bc1382c 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 92% rename from docs/signing/local-signing.md rename to docs/signing/local-signing.mdx index 0ff9535..671cda3 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. @@ -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). -::: + ## Example diff --git a/docs/signing/prod-cert.mdx b/docs/signing/prod-cert.mdx index 2065b08..52f989b 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. diff --git a/docs/tasks/archives.mdx b/docs/tasks/archives.mdx index f557edf..0a16924 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'; import Nodearchives from './node/_node-archives.mdx'; _Working stores_ and _archives_ provide a standard way to save and restore the state of a `Builder`: @@ -37,6 +38,12 @@ _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 8549a03..c1644d3 100644 --- a/docs/tasks/build.mdx +++ b/docs/tasks/build.mdx @@ -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'; + + + @@ -32,6 +39,14 @@ import NodeBuild from './node/_node-build.md'; + + + + + + + + diff --git a/docs/tasks/get-resources.mdx b/docs/tasks/get-resources.mdx index 1922453..94db5e7 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'; 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. @@ -33,6 +34,12 @@ 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 7b22221..a94c6d7 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'; 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. @@ -43,6 +44,12 @@ 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 0000000..49c6b5c --- /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-local.md b/docs/tasks/js/_js-build-local.md new file mode 100644 index 0000000..56ba007 --- /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 34ae516..eb4b595 100644 --- a/docs/tasks/js/_js-build.md +++ b/docs/tasks/js/_js-build.md @@ -1,95 +1,92 @@ -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). +## 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. +::: + +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()`. + -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. -::: 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. Instead use 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(); + // 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', + 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: -```js -const { asset, manifest } = await builder.signAndGetManifestBytes( - signer, - assetBlob.type, - 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 + diff --git a/docs/tasks/js/_js-get-resources.md b/docs/tasks/js/_js-get-resources.md index 31851c3..090f040 100644 --- a/docs/tasks/js/_js-get-resources.md +++ b/docs/tasks/js/_js-get-resources.md @@ -1,19 +1,58 @@ -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' -); +async function getActiveManifestThumbnail(url) { + const c2pa = await createC2pa({ wasmSrc }); + + 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 active = await reader.activeManifest(); + const uri = active.thumbnail?.identifier; + if (!uri) { + await reader.free(); + c2pa.dispose(); + return; + } -const blob = await response.blob(); -const reader = await c2pa.reader.fromBlob(blob.type, blob); + 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'); ``` -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 0000000..a8730ad --- /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 3694230..5c6cf02 100644 --- a/docs/tasks/js/_js-read.md +++ b/docs/tasks/js/_js-read.md @@ -1,25 +1,58 @@ -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' -); +async function readCredentials(url) { + const c2pa = await createC2pa({ wasmSrc }); + + 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 manifestStore = await reader.manifestStore(); + console.log(JSON.stringify(manifestStore, null, 2)); -const blob = await response.blob(); -const reader = await c2pa.reader.fromBlob(blob.type, blob); -const manifestStore = await reader.manifestStore(); + const active = await reader.activeManifest(); + console.log('Active title:', active.title); + + await reader.free(); + c2pa.dispose(); +} + +await readCredentials('/signed-image.jpg'); +``` -console.log(manifestStore); +Optional [per-read settings](../settings.mdx) override the defaults passed to `createC2pa`: -// Free SDK objects when they are no longer needed to avoid memory leaks. -await reader.free(); +```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 5410872..0a355ce 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 43b55d3..a20b4c2 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'; import NodeRead from './node/_node-read.md'; @@ -32,6 +33,12 @@ import NodeRead from './node/_node-read.md'; + + + + + + diff --git a/docs/tasks/settings.mdx b/docs/tasks/settings.mdx index b0d3d5c..4dbef61 100644 --- a/docs/tasks/settings.mdx +++ b/docs/tasks/settings.mdx @@ -10,31 +10,17 @@ 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'; import NodeSettings from './node/_node-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 +59,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. + @@ -93,6 +87,12 @@ For Boolean values, use JSON `true` and `false`, not the strings `"true"` and `" + + + + + +