PageSourceSearch

https://opensource.contentauthenticity.org/assets/js/1651d2b9.d49ff966.js

js contentauthenticity.org collected 2026-10-01 08:31:41 UTC 34,991 bytes, 1 lines download raw bytes

1"use strict";(globalThis.webpackChunkopensource_contentauth_org=globalThis.webpackChunkopensource_contentauth_org||[]).push([[4793],{895:(e,n,t)=>{t.d(n,{Ay:()=>o,RM:()=>r});var i=t(4848),s=t(8453);const r=[];function a(e){const n={a:"a",admonition:"admonition",p:"p",strong:"strong",...(0,s.R)(),...e.components};return(0,i.jsx)(n.admonition,{title:"Warning",type:"warning",children:(0,i.jsxs)(n.p,{children:["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 ",(0,i.jsx)(n.strong,{children:"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 ",(0,i.jsx)(n.a,{href:"/docs/signing/prod-cert",children:"Using a certificate in production"}),"."]})})}function o(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(a,{...e})}):a(e)}},3876:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>S,contentTitle:()=>v,default:()=>k,frontMatter:()=>w,metadata:()=>i,toc:()=>_});const i=JSON.parse('{"id":"tasks/build","title":"Adding and signing a manifest","description":"","source":"@site/docs/tasks/build.mdx","sourceDirName":"tasks","slug":"/tasks/build","permalink":"/docs/tasks/build","draft":false,"unlisted":false,"editUrl":"https://github.com/contentauth/opensource.contentauth.org/edit/main/docs/tasks/build.mdx","tags":[],"version":"current","frontMatter":{"id":"build","title":"Adding and signing a manifest","hide_table_of_contents":true},"sidebar":"docs","previous":{"title":"Using working stores and archives","permalink":"/docs/tasks/archives"},"next":{"title":"Signing and certificates","permalink":"/docs/signing/"}}');var s=t(4848),r=t(8453),a=t(4865),o=t(9365);function c(e){const n={code:"code",p:"p",pre:"pre",...(0,r.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.p,{children:"This is an example of how to assign a manifest to an asset and sign the claim using Python."}),"\n",(0,s.jsxs)(n.p,{children:["Use a ",(0,s.jsx)(n.code,{children:"Builder"})," object to add a manifest to an asset."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:'import json\nfrom c2pa import Builder, Context, Signer, C2paSignerInfo, C2paSigningAlg\n\nmanifest_json = json.dumps({\n    "claim_generator": "python_test/0.1",\n    "assertions": []\n})\n\ntry:\n    with open("tests/fixtures/ps256.pub", "rb") as cert_file, \\\n         open("tests/fixtures/ps256.pem", "rb") as key_file:\n        cert_data = cert_file.read()\n        key_data = key_file.read()\n\n        signer_info = C2paSignerInfo(\n            alg=C2paSigningAlg.PS256,\n            sign_cert=cert_data,\n            private_key=key_data,\n            ta_url=b"http://timestamp.digicert.com"\n        )\n\n        with Context() as ctx:\n            with Signer.from_info(signer_info) as signer:\n                with Builder(manifest_json, ctx) as builder:\n                    # Add an ingredient from a stream.\n                    ingredient_json = json.dumps({\n                        "title": "A.jpg",\n                        "relationship": "parentOf"\n                    })\n                    with open("tests/fixtures/A.jpg", "rb") as ingredient_file:\n                        builder.add_ingredient(ingredient_json, "image/jpeg", ingredient_file)\n\n                    # Sign and write to the output file.\n                    with open("tests/fixtures/A.jpg", "rb") as source, \\\n                         open("target/out.jpg", "w+b") as dest:\n                        builder.sign(signer, "image/jpeg", source, dest)\n\nexcept Exception as err:\n    print(err)\n'})})]})}function l(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(c,{...e})}):c(e)}function d(e){const n={code:"code",p:"p",pre:"pre",...(0,r.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.p,{children:"This is an example of how to assign a manifest to an asset and sign the claim using C++:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-cpp",children:'#include <iostream>\n#include <memory>\n#include <string>\n#include "c2pa.hpp"\n\nusing namespace c2pa;\n\nconst std::string manifest_json = R"({\n    "claim_generator_info": [\n      {\n        "name": "c2pa-cpp test",\n        "version": "0.1"\n      }\n    ],\n    "assertions": [\n    {\n      "label": "c2pa.training-mining",\n      "data": {\n        "entries": {\n          "c2pa.ai_generative_training": { "use": "notAllowed" },\n          "c2pa.ai_inference": { "use": "notAllowed" },\n          "c2pa.ai_training": { "use": "notAllowed" },\n          "c2pa.data_mining": { "use": "notAllowed" }\n        }\n      }\n    }\n  ]\n})";\n\nauto context = std::make_shared<c2pa::Context>();\nauto builder = c2pa::Builder(context, manifest_json);\n\nSigner signer = c2pa::Signer("es256", certs, private_key, "http://timestamp.digicert.com");\nauto manifest_data = builder.sign("source_asset.jpg", "output_asset.jpg", signer);\n'})})]})}function u(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(d,{...e})}):d(e)}function h(e){const n={code:"code",p:"p",pre:"pre",...(0,r.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.p,{children:"This is an example of how to assign a manifest to an asset and sign the claim using Rust."}),"\n",(0,s.jsxs)(n.p,{children:["Configure a ",(0,s.jsx)(n.code,{children:"Context"})," with signer settings and use ",(0,s.jsx)(n.code,{children:"Builder::from_context"})," to create and sign a manifest:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-rust",children:'use c2pa::{Context, Builder, Result};\nuse serde_json::json;\nuse std::io::Cursor;\n\nfn main() -> Result<()> {\n    let settings = json!({\n        "signer": {\n            "local": {\n                "alg": "ps256",\n                "sign_cert": "path/to/cert.pem",\n                "private_key": "path/to/key.pem",\n                "tsa_url": "http://timestamp.digicert.com"\n            }\n        },\n        "builder": {\n            "claim_generator_info": { "name": "My App", "version": "1.0" },\n            "intent": { "Create": "digitalCapture" }\n        }\n    });
1\n    \n    let context = Context::new()\n        .with_settings(settings)?;\n\n    let mut builder = Builder::from_context(context)\n        .with_definition(json!({"title": "My Image"}))?;\n\n    let mut source = std::fs::File::open("source.jpg")?;\n    let mut dest = Cursor::new(Vec::new());\n    builder.save_to_stream("image/jpeg", &mut source, &mut dest)?;\n\n    Ok(())\n}\n'})})]})}function p(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(h,{...e})}):h(e)}function g(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",li:"li",ol:"ol",p:"p",pre:"pre",ul:"ul",...(0,r.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h2,{id:"using-a-remote-signer",children:"Using a remote signer"}),"\n",(0,s.jsx)(n.admonition,{type:"info",children:(0,s.jsxs)(n.p,{children:["Although you can use ",(0,s.jsx)(n.code,{children:"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."]})}),"\n",(0,s.jsxs)(n.p,{children:["Use ",(0,s.jsx)(n.a,{href:"https://github.com/contentauth/c2pa-js/tree/main/packages/c2pa-web",children:(0,s.jsx)(n.code,{children:"@contentauth/c2pa-web"})})," to build manifests and sign assets in the browser."]}),"\n",(0,s.jsx)(n.p,{children:"The high-level flow is:"}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:["Call ",(0,s.jsx)(n.code,{children:"await createC2pa({ wasmSrc, settings? })"})]}),"\n",(0,s.jsxs)(n.li,{children:["Call ",(0,s.jsx)(n.code,{children:"c2pa.builder.new()"})," / ",(0,s.jsx)(n.a,{href:"https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.BuilderFactory.html#fromdefinition",children:(0,s.jsx)(n.code,{children:"fromDefinition"})})," / ",(0,s.jsx)(n.a,{href:"https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.BuilderFactory.html#fromarchive",children:(0,s.jsx)(n.code,{children:"fromArchive"})})]}),"\n",(0,s.jsx)(n.li,{children:"Add actions, ingredients, thumbnails"}),"\n",(0,s.jsxs)(n.li,{children:["Call ",(0,s.jsx)(n.a,{href:"https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Builder.html#sign",children:(0,s.jsx)(n.code,{children:"sign"})})," or ",(0,s.jsx)(n.a,{href:"https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Builder.html#signandgetmanifestbytes",children:(0,s.jsx)(n.code,{children:"signAndGetManifestBytes"})}),".","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["To publish a remote manifest instead of embedding, call ",(0,s.jsx)(n.a,{href:"https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Builder.html#setremoteurl",children:(0,s.jsx)(n.code,{children:"setRemoteUrl"})})," and ",(0,s.jsx)(n.a,{href:"https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Builder.html#setnoembed",children:(0,s.jsx)(n.code,{children:"setNoEmbed(true)"})})," before this call."]}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["Call ",(0,s.jsx)(n.code,{children:"await builder.free()"})," and ",(0,s.jsx)(n.code,{children:"c2pa.dispose()"}),"."]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["You implement the ",(0,s.jsx)(n.a,{href:"https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Signer.html",children:(0,s.jsx)(n.code,{children:"Signer"})})," interface (",(0,s.jsx)(n.code,{children:"alg"}),", ",(0,s.jsx)(n.code,{children:"reserveSize"}),", ",(0,s.jsx)(n.code,{children:"sign"}),") so signing can call your backend, WebCrypto, or a test key."]}),"\n",(0,s.jsx)(n.admonition,{type:"warning",children:(0,s.jsx)(n.p,{children:"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."})}),"\n",(0,s.jsx)(n.p,{children:"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++."}),"\n",(0,s.jsx)(n.h3,{id:"remote-signer-example",children:"Remote signer example"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { createC2pa, type Signer } from '@contentauth/c2pa-web';\nimport wasmSrc from '@contentauth/c2pa-web/resources/c2pa.wasm?url';\n\nfunction createRemoteSigner(): Signer {\n  return {\n    alg: 'es256',\n    reserveSize: async () => 4096,\n    sign: async (toBeSigned: Uint8Array) =>
1 {\n      const res = await fetch('/api/sign', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/octet-stream' },\n        body: toBeSigned,\n      });\n      if (!res.ok) throw new Error('Signing failed');\n      return new Uint8Array(await res.arrayBuffer());\n    },\n  };\n}\n\nasync function run() {\n  const c2pa = await createC2pa({ wasmSrc });\n\n  const resp = await fetch('/image-to-sign.jpg');\n  const assetBlob = await resp.blob();\n\n  // To instead create a minimal manifest defiintion, use\n  // const builder = await c2pa.builder.new();\n  const builder = await c2pa.builder.fromDefinition({\n    claim_generator_info: [{ name: 'my-app', version: '1.0.0' }],\n    title: 'My image',\n    format: 'image/jpeg',\n    assertions: [],\n    ingredients: [],\n  });\n\n  await builder.addAction({\n    action: 'c2pa.created',\n    digitalSourceType:\n      'http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture',\n  });\n  await builder.setThumbnailFromBlob('image/jpeg', assetBlob);\n\n  const signer = createRemoteSigner();\n  const signedBytes = await builder.sign(signer, assetBlob.type, assetBlob);\n\n  const signedBlob = new Blob([signedBytes], { type: assetBlob.type });\n  const url = URL.createObjectURL(signedBlob);\n  const a = document.createElement('a');\n  a.href = url;\n  a.download = 'signed.jpg';\n  a.click();\n  URL.revokeObjectURL(url);\n\n  await builder.free();\n  c2pa.dispose();\n}\n\nvoid run();\n"})})]})}function m(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(g,{...e})}):g(e)}function f(e){const n={a:"a",admonition:"admonition",code:"code",h2:"h2",h3:"h3",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,r.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.a,{href:"#using-a-local-signer",children:"Using a local signer"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.a,{href:"#using-a-remote-signer",children:"Using a remote signer"})}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"using-a-local-signer",children:"Using a local signer"}),"\n",(0,s.jsx)(n.admonition,{type:"info",children:(0,s.jsxs)(n.p,{children:["You can use ",(0,s.jsx)(n.code,{children:"c2pa-web"})," in the browser by implementing the ",(0,s.jsx)(n.a,{href:"https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Signer.html",children:(0,s.jsx)(n.code,{children:"Signer"})})," interface. That callback can use ",(0,s.jsx)(n.strong,{children:"local"})," cryptographic material (for example a non-extractable ",(0,s.jsx)(n.a,{href:"https://developer.mozilla.org/en-US/docs/Web/API/CryptoKey",children:(0,s.jsx)(n.code,{children:"CryptoKey"})})," from Web Crypto) so the private key never leaves the client. The current WASM integration uses ",(0,s.jsx)(n.strong,{children:"direct COSE handling"}),": ",(0,s.jsx)(n.code,{children:"sign"})," receives the claim bytes and must return a ",(0,s.jsx)(n.strong,{children:"complete, encoded COSE signature"})," for C2PA (certificates, timestamps as required by your policy, and the signature itself). A raw ECDSA or RSA signature from ",(0,s.jsx)(n.code,{children:"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."]})}),"\n",(0,s.jsxs)(n.p,{children:["Use ",(0,s.jsx)(n.a,{href:"https://github.com/contentauth/c2pa-js/tree/main/packages/c2pa-web",children:(0,s.jsx)(n.code,{children:"@contentauth/c2pa-web"})})," to build manifests and sign assets in the browser."]}),"\n",(0,s.jsx)(n.p,{children:"The high-level flow is:"}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:["Call ",(0,s.jsx)(n.code,{children:"await createC2pa({ wasmSrc, settings? })"})]}),"\n",(0,s.jsxs)(n.li,{children:["Call ",(0,s.jsx)(n.code,{children:"c2pa.builder.new()"})," / ",(0,s.jsx)(n.a,{href:"https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.BuilderFactory.html#fromdefinition",children:(0,s.jsx)(n.code,{children:"fromDefinition"})})," / ",(0,s.jsx)(n.a,{href:"https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.BuilderFactory.html#fromarchive",children:(0,s.jsx)(n.code,{children:"fromArchive"})})]}),"\n",(0,s.jsx)(n.li,{children:"Add actions, ingredients, thumbnails"}),"\n",(0,s.jsxs)(n.li,{children:["Call ",(0,s.jsx)(n.a,{href:"https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Builder.html#sign",children:(0,s.jsx)(n.code,{children:"sign"})})," or ",(0,s.jsx)(n.a,{href:"https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Builder.html#signandgetmanifestbytes",children:(0,s.jsx)(n.code,{children:"signAndGetManifestBytes"})}),".","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["To publish a remote manifest instead of embedding, call ",(0,s.jsx)(n.a,{href:"https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Builder.html#setremoteurl",children:(0,s.jsx)(n.code,{children:"setRemoteUrl"})})," and ",(0,s.jsx)(n.a,{href:"https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Builder.html#setnoembed",children:(0,s.jsx)(n.code,{children:"setNoEmbed(true)"})})," before this call."]}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["Call ",(0,s.jsx)(n.code,{children:"await builder.free()"})," and ",(0,s.jsx)(n.code,{children:"c2pa.dispose()"}),"."]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["You implement the ",(0,s.jsx)(n.a,{href:"https://contentauth.github.io/c2pa-js/interfaces/_contentauth_c2pa-web.Signer.html",children:(0,s.jsx)(n.code,{children:"Signer"})})," interface (",(0,s.jsx)(n.code,{children:"alg"}),", ",(0,s.jsx)(n.code,{children:"reserveSize"}),", ",(0,s.jsx)(n.code,{children:"sign"}),") so the WASM layer can obtain a COSE signature for each claim."]}),"\n",(0,s.jsx)(n.admonition,{type:"warning",children:(0,s.jsxs)(n.p,{children:["Browser pages are exposed to XSS: any script on the page can try to use keys that JavaScript can reach. Prefer ",(0,s.jsx)(n.strong,{children:"non-extractable"})," Web Crypto keys, user-gated imports (file input, ",(0,s.jsx)(n.code,{children:"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 ",(0,s.jsx)(n.strong,{children:"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 \u201csign this blob\u201d endpoint for an authenticated session."]})}),"\n",(0,s.jsxs)(n.p,{children:["For obtaining and packaging certificates and ke
1ys outside the browser, see ",(0,s.jsx)(n.a,{href:"../../docs/signing/local-signing",children:"Signing with local credentials"}),"."]}),"\n",(0,s.jsx)(n.p,{children:"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++."}),"\n",(0,s.jsx)(n.h3,{id:"local-web-crypto-signer-example",children:"Local Web Crypto signer example"}),"\n",(0,s.jsxs)(n.p,{children:["The following shows how to use a ",(0,s.jsx)(n.strong,{children:"P-256 PKCS#8"})," key imported into Web Crypto inside ",(0,s.jsx)(n.code,{children:"Signer.sign"}),". You still need to turn the raw signature and your certificate material into the ",(0,s.jsx)(n.strong,{children:"COSE Sign1"})," bytes C2PA expects (and honor ",(0,s.jsx)(n.code,{children:"reserveSize"}),"). The library does not ship a browser COSE encoder; use your own implementation or a signing path that already returns COSE."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { createC2pa, type Signer } from '@contentauth/c2pa-web';\nimport wasmSrc from '@contentauth/c2pa-web/resources/c2pa.wasm?url';\n\n/** Strip PEM headers and decode base64 DER (PKCS#8 private key). */\nfunction pkcs8PemToArrayBuffer(pem: string): ArrayBuffer {\n  const b64 = pem\n    .replace(/-----BEGIN PRIVATE KEY-----/, '')\n    .replace(/-----END PRIVATE KEY-----/, '')\n    .replace(/\\s/g, '');\n  const binary = atob(b64);\n  const bytes = new Uint8Array(binary.length);\n  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n  return bytes.buffer;\n}\n\nasync function importEs256PrivateKey(pem: string): Promise<CryptoKey> {\n  return crypto.subtle.importKey(\n    'pkcs8',\n    pkcs8PemToArrayBuffer(pem),\n    { name: 'ECDSA', namedCurve: 'P-256' },\n    false,\n    ['sign']\n  );\n}\n\nfunction createLocalWebCryptoSigner(privateKey: CryptoKey): Signer {\n  return {\n    alg: 'es256',\n    reserveSize: async () => 4096,\n    sign: async (toBeSigned: Uint8Array, reserveSize: number) => {\n      const raw = new Uint8Array(\n        await crypto.subtle.sign(\n          { name: 'ECDSA', hash: 'SHA-256' },\n          privateKey,\n          toBeSigned\n        )\n      );\n      // Required for real manifests: build C2PA COSE_Sign1 using `raw`, your\n      // end-entity certificate chain, timestamp policy, etc., and pad or size\n      // the result to match `reserveSize`.\n      void raw;\n      void reserveSize;\n      throw new Error(\n        'Implement COSE Sign1 packaging for C2PA; raw Web Crypto output alone is not enough.'\n      );\n    },\n  };\n}\n\nasync function run() {\n  const c2pa = await createC2pa({ wasmSrc });\n\n  const resp = await fetch('/image-to-sign.jpg');\n  const assetBlob = await resp.blob();\n\n  // To instead create a minimal manifest definition, use\n  // const builder = await c2pa.builder.new();\n  const builder = await c2pa.builder.fromDefinition({\n    claim_generator_info: [{ name: 'my-app', version: '1.0.0' }],\n    title: 'My image',\n    format: 'image/jpeg',\n    assertions: [],\n    ingredients: [],\n  });\n\n  await builder.addAction({\n    action: 'c2pa.created',\n    digitalSourceType:\n      'http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture',\n  });\n  await builder.setThumbnailFromBlob('image/jpeg', assetBlob);\n\n  // Example: load PKCS#8 PEM from a file the user selects (keeps material out of the bundle).\n  const pem = await new Promise<string>((resolve, reject) => {\n    const input = document.createElement('input');\n    input.type = 'file';\n    input.accept = '.pem,.key';\n    input.onchange = async () => {\n      const file = input.files?.[0];\n      if (!file) reject(new Error('No file'));\n      else resolve(await file.text());\n    };\n    input.click();\n  });\n\n  const privateKey = await importEs256PrivateKey(pem);\n  const signer = createLocalWebCryptoSigner(privateKey);\n  const signedBytes = await builder.sign(signer, assetBlob.type, assetBlob);\n\n  const signedBlob = new Blob([signedBytes], { type: assetBlob.type });\n  const url = URL.createObjectURL(signedBlob);\n  const a = document.createElement('a');\n  a.href = url;\n  a.download = 'signed.jpg';\n  a.click();\n  URL.revokeObjectURL(url);\n\n  await builder.free();\n  c2pa.dispose();\n}\n\nvoid run();\n"})}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsxs)(n.p,{children:["Until ",(0,s.jsx)(n.code,{children:"sign"})," returns valid COSE bytes, ",(0,s.jsx)(n.code,{children:"builder.sign"})," will not succeed. If you already have a service or module that returns ",(0,s.jsx)(n.strong,{children:"finished"})," COSE for the claim, you can keep using Web Crypto only for the asymmetric step inside that module, or call that module from ",(0,s.jsx)(n.code,{children:"sign"})," without using ",(0,s.jsx)(n.code,{children:"fetch"})," to your own backend (for example an in-browser worker with the same origin). The important distinction is ",(0,s.jsx)(n.strong,{children:"where the private key lives"})," and ",(0,s.jsx)(n.strong,{children:"who builds COSE"}),", not whether ",(0,s.jsx)(n.code,{children:"sign"})," is async."]})})]})}function b(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(f,{...e})}):f(e)}function j(e){const n={a:"a",code:"code",h3:"h3",p:"p",pre:"pre",...(0,r.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsxs)(n.p,{children:["Use the ",(0,s.jsx)(n.code,{children:"Builder"})," and ",(0,s.jsx)(n.code,{children:"LocalSigner"})," classes from ",(0,s.jsx)(n.code,{children:"@contentauth/c2pa-node"})," to assemble manifest data and sign an asset."]}),"\n",(0,s.jsx)(n.h3,{id:"create-a-builder",children:"Create a builder"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"import { Builder } from '@contentauth/c2pa-node';\n\n// Default settings\nconst builder = Builder.new();\n\n// With settings (JSON object or string;
1 see [Settings](../settings.mdx))\nconst withSettings = Builder.new({\n  builder: { generate_c2pa_archive: true },\n});\n\n// From an existing manifest definition (see manifest JSON reference)\nconst fromDefinition = Builder.withJson({\n  claim_generator_info: [{ name: 'my-app', version: '1.0.0' }],\n  title: 'My image',\n  format: 'image/jpeg',\n  assertions: [],\n  resources: { resources: {} },\n});\n"})}),"\n",(0,s.jsx)(n.h3,{id:"add-assertions-and-resources",children:"Add assertions and resources"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"builder.addAssertion('c2pa.actions', {\n  actions: [{ action: 'c2pa.created' }],\n});\n\nawait builder.addResource('resource://example/thumb', {\n  buffer: thumbnailBytes,\n  mimeType: 'image/jpeg',\n});\n"})}),"\n",(0,s.jsx)(n.h3,{id:"sign-with-a-local-certificate-and-key",children:"Sign with a local certificate and key"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"LocalSigner.newSigner"})," takes the signing certificate (PEM), private key (PEM), algorithm (",(0,s.jsx)(n.code,{children:"es256"}),", ",(0,s.jsx)(n.code,{children:"ps256"}),", ",(0,s.jsx)(n.code,{children:"ed25519"}),", etc.), and an optional RFC 3161 timestamp URL."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"import { Builder, LocalSigner } from '@contentauth/c2pa-node';\nimport { readFile } from 'node:fs/promises';\n\nconst cert = await readFile('signer.pem');\nconst key = await readFile('signer.key');\nconst signer = LocalSigner.newSigner(cert, key, 'es256');\n\nconst builder = Builder.withJson({\n  claim_generator_info: [{ name: 'my-app', version: '1.0.0' }],\n  title: 'output.jpg',\n  format: 'image/jpeg',\n  assertions: [],\n  resources: { resources: {} },\n});\n\nbuilder.setIntent('edit');\n\n// Output to a file\nbuilder.sign(signer, { path: 'input.jpg' }, { path: 'signed.jpg' });\n"})}),"\n",(0,s.jsx)(n.h3,{id:"sign-to-an-in-memory-buffer",children:"Sign to an in-memory buffer"}),"\n",(0,s.jsxs)(n.p,{children:["Use a destination object with ",(0,s.jsx)(n.code,{children:"buffer: null"}),"; after ",(0,s.jsx)(n.code,{children:"sign"}),", the signed asset bytes are written into ",(0,s.jsx)(n.code,{children:"dest.buffer"}),"."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"const dest = { buffer: null };\nbuilder.sign(signer, { path: 'input.jpg' }, dest);\nconst signedBytes = dest.buffer;\n"})}),"\n",(0,s.jsxs)(n.h3,{id:"callback-signing-signasync",children:["Callback signing (",(0,s.jsx)(n.code,{children:"signAsync"}),")"]}),"\n",(0,s.jsxs)(n.p,{children:["For signing in hardware, a remote service, or other custom flows, use ",(0,s.jsx)(n.code,{children:"CallbackSigner"})," and ",(0,s.jsx)(n.code,{children:"signAsync"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-javascript",children:"import { Builder, CallbackSigner } from '@contentauth/c2pa-node';\nimport { readFile } from 'node:fs/promises';\n\nconst cert = await readFile('signer.pem');\n\nconst callbackSigner = CallbackSigner.newSigner(\n  {\n    alg: 'es256',\n    certs: [cert],\n    reserveSize: 1024,\n  },\n  async (data) => {\n    return customSign(data);\n  },\n);\n\nconst builder = Builder.new();\nawait builder.signAsync(\n  callbackSigner,\n  { path: 'input.jpg' },\n  { path: 'signed.jpg' },\n);\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Replace ",(0,s.jsx)(n.code,{children:"customSign"})," with your implementation that returns the detached signature bytes for the C2PA claim."]}),"\n",(0,s.jsxs)(n.p,{children:["For identity assertions (CAWG), see ",(0,s.jsx)(n.code,{children:"IdentityAssertionBuilder"})," and ",(0,s.jsx)(n.code,{children:"IdentityAssertionSigner"})," in the ",(0,s.jsx)(n.a,{href:"https://github.com/contentauth/c2pa-js/tree/main/packages/c2pa-node#identity-assertion-components",children:"c2pa-node README"}),"."]})]})}function x(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(j,{...e})}):j(e)}var y=t(895);const w={id:"build",title:"Adding and signing a manifest",hide_table_of_contents:!0},v=void 0,S={},_=[...y.RM,{value:"Using a local signer",id:"using-a-local-signer",level:2},{value:"Local Web Crypto signer example",id:"local-web-crypto-signer-example",level:3},{value:"Using a remote signer",id:"using-a-remote-signer",level:2},{value:"Remote signer example",id:"remote-signer-example",level:3},{value:"Create a builder",id:"create-a-builder",level:3},{value:"Add assertions and resources",id:"add-assertions-and-resources",level:3},{value:"Sign with a local certificate and key",id:"sign-with-a-local-certificate-and-key",level:3},{value:"Sign to an in-memory buffer",id:"sign-to-an-in-memory-buffer",level:3},{value:"Callback signing (<code>signAsync</code>)",id:"callback-signing-signasync",level:3}];function C(e){return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(y.Ay,{}),"\n",(0,s.jsxs)(a.A,{groupId:"programming-lang",queryString:"lang",children:[(0,s.jsx)(o.A,{value:"rust",label:"Rust",children:(0,s.jsx)(p,{name:"rust-build"})}),(0,s.jsx)(o.A,{value:"cpp",label:"C++",children:(0,s.jsx)(u,{name:"cpp-build"})}),(0,s.jsx)(o.A,{value:"python",label:"Python",default:!0,children:(0,s.jsx)(l,{name:"python-build"})}),(0,s.jsxs)(o.A,{value:"js",label:"JavaScript",children:[(0,s.jsx)(b,{name:"js-build"}),(0,s.jsx)(m,{name:"js-build"})]}),(0,s.jsx)(o.A,{value:"node",label:"Node.js",children:(0,s.jsx)(x,{name:"node-build"})})]})]})}function k(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(C,{...e})}):C()}},4865:(e,n,t)=>{t.d(n,{A:()=>p});t(6540);var i=t(8215),s=t(7559),r=t(7751),a=t(3104),o=t(2303);const c={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};
1var l=t(4848);function d({className:e}){const{selectedValue:n,selectValue:t,tabValues:s,block:o}=(0,r.uc)(),d=[],{blockElementScrollPositionUntilNextRender:u}=(0,a.a_)(),h=e=>{const i=e.currentTarget,r=d.indexOf(i),a=s[r].value;a!==n&&(u(i),t(a))},p=e=>{let n=null;switch(e.key){case"Enter":h(e);break;case"ArrowRight":{const t=d.indexOf(e.currentTarget)+1;n=d[t]??d[0];break}case"ArrowLeft":{const t=d.indexOf(e.currentTarget)-1;n=d[t]??d[d.length-1];break}}n?.focus()};return(0,l.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,i.A)("tabs",{"tabs--block":o},e),children:s.map(({value:e,label:t,attributes:s})=>(0,l.jsx)("li",{role:"tab",tabIndex:n===e?0:-1,"aria-selected":n===e,ref:e=>{d.push(e)},onKeyDown:p,onClick:h,...s,className:(0,i.A)("tabs__item",c.tabItem,s?.className,{"tabs__item--active":n===e}),children:t??e},e))})}function u({children:e}){return(0,l.jsx)("div",{className:"margin-top--md",children:e})}function h({className:e,children:n}){return(0,l.jsxs)("div",{className:(0,i.A)(s.G.tabs.container,"tabs-container",c.tabList),children:[(0,l.jsx)(d,{className:e}),(0,l.jsx)(u,{children:n})]})}function p(e){const n=(0,o.A)(),t=(0,r.OC)(e);return(0,l.jsx)(r.O_,{value:t,children:(0,l.jsx)(h,{className:e.className,children:(0,r.vT)(e.children)})},String(n))}},7751:(e,n,t)=>{t.d(n,{OC:()=>g,O_:()=>b,uc:()=>f,vT:()=>d});var i=t(6540),s=t(6347),r=t(205),a=t(7485),o=t(679),c=t(1682),l=t(4848);function d(e){return i.Children.toArray(e).filter(e=>"\n"!==e)}function u(e){const{values:n,children:t}=e;return(0,i.useMemo)(()=>{const e=n??function(e){return i.Children.toArray(e).flatMap(e=>{if(!e)return[];if((0,i.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return[e];const n="string"==typeof e.type?e.type:e.type.name;throw new Error(`Docusaurus error: Bad <Tabs> child <${n}>: all children of the <Tabs> component should be <TabItem>, and every <TabItem> should have a unique "value" prop.\nIf you do not want to pass on a "value" prop to the direct children of <Tabs>, you can also pass an explicit <Tabs values={...}> prop.`)}).map(({props:{value:e,label:n,attributes:t,default:i}})=>({value:e,label:n,attributes:t,default:i}))}(t);return function(e){const n=(0,c.XI)(e,(e,n)=>e.value===n.value);if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map(e=>`'${e.value}'`).join(", ")}" found in <Tabs>. Every value needs to be unique.`)}(e),e},[n,t])}function h({value:e,tabValues:n}){return n.some(n=>n.value===e)}function p({queryString:e=!1,groupId:n}){const t=(0,s.W6)(),r=function({queryString:e=!1,groupId:n}){if("string"==typeof e)return e;if(!1===e)return null;if(!0===e&&!n)throw new Error('Docusaurus error: The <Tabs> component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return n??null}({queryString:e,groupId:n});return[(0,a.aZ)(r),(0,i.useCallback)(e=>{if(!r)return;const n=new URLSearchParams(t.location.search);n.set(r,e),t.replace({...t.location,search:n.toString()})},[r,t])]}function g(e){const{defaultValue:n,queryString:t=!1,groupId:s}=e,a=u(e),[c,l]=(0,i.useState)(()=>function({defaultValue:e,tabValues:n}){if(0===n.length)throw new Error("Docusaurus error: the <Tabs> component requires at least one <TabItem> children component");if(e){if(!h({value:e,tabValues:n}))throw new Error(`Docusaurus error: The <Tabs> has a defaultValue "${e}" but none of its children has the corresponding value. Available values are: ${n.map(e=>e.value).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return e}const t=n.find(e=>e.default)??n[0];if(!t)throw new Error("Unexpected error: 0 tabValues");return t.value}({defaultValue:n,tabValues:a})),[d,g]=p({queryString:t,groupId:s}),[m,f]=function({groupId:e}){const n=function(e){return e?`docusaurus.tab.${e}`:null}(e),[t,s]=(0,o.Dv)(n);return[t,(0,i.useCallback)(e=>{n&&s.set(e)},[n,s])]}({groupId:s}),b=(()=>{const e=d??m;return h({value:e,tabValues:a})?e:null})();(0,r.A)(()=>{b&&l(b)},[b]);return{selectedValue:c,selectValue:(0,i.useCallback)(e=>{if(!h({value:e,tabValues:a}))throw new Error(`Can't select invalid tab value=${e}`);l(e),g(e),f(e)},[g,f,a]),tabValues:a,lazy:e.lazy??!1,block:e.block??!1}}const m=(0,i.createContext)(null);function f(){const e=i.useContext(m);if(!e)throw new Error("useTabsContext() must be used within a Tabs component");return e}function b(e){return(0,l.jsx)(m.Provider,{value:e.value,children:e.children})}},8453:(e,n,t)=>{t.d(n,{R:()=>a,x:()=>o});var i=t(6540);const s={},r=i.createContext(s);function a(e){const n=i.useContext(r);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:a(e.components),i.createElement(r.Provider,{value:n},e.children)}},9365:(e,n,t)=>{t.d(n,{A:()=>c});t(6540);var i=t(8215),s=t(7751);const r={tabItem:"tabItem_Ymn6"};var a=t(4848);function o({children:e,className:n,hidden:t}){return(0,a.jsx)("div",{role:"tabpanel",className:(0,i.A)(r.tabItem,n),hidden:t,children:e})}function c({children:e,className:n,value:t}){const{selectedValue:i,lazy:r}=(0,s.uc)(),c=t===i;return!c&&r?null:(0,a.jsx)(o,{className:n,hidden:!c,children:e})}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.