The c2pa-node repository implements a Node.js API that can:
- Read and validate C2PA data from media files in supported formats.
- Add signed manifests to media files in supported formats.
WARNING: This is an early prerelease version of this library. There may be bugs and unimplemented features, and the API is subject to change.
Contents:
You must install:
If you need to manage multiple versions of Node on your machine, use a tool such as nvm.
Using npm:
$ npm install c2pa-node
Using Yarn:
$ yarn add c2pa-node
Using pnpm:
$ pnpm add c2pa-node
This command will download precompiled binaries for the following systems:
- Linux x86_64
- Linux aarch64 (ARM)
- macOS aarch64 (Apple Silicon)
- macOS x86_64 (Intel Mac)
- Windows x86
- Windows ARM
For all other platforms, you will need Rust installed on your system, as the postinstall
step will attempt to build our Rust SDK into a native Node.js module on your machine.
For platforms or architectures that do not have a precompiled binaries or Rust tooling installed, you may need to build custom binaries. To pre-build a binary, install the Rust toolchain and then run the following commands on the target system or VM:
$ cd c2pa-node
$ pnpm install
$ pnpm build:rust
Then, you can copy the binary to a place that is accessible by your application (in this example, it is /path/to/my/application/resources
) and set the path to the c2pa.node
module via the C2PA_LIBRARY_PATH
environment variable. Enter these commands:
$ cd /path/to/my/application
$ mkdir resources
$ cp /path/to/c2pa-node/generated/c2pa.node resources/c2pa.node
$ export C2PA_LIBRARY_PATH=resources/c2pa.node
$ npm install c2pa-node
$ npm start
Important: C2PA_LIBRARY_PATH
must be set while both installing or adding c2pa-node to your app to avoid building the Rust code. It must also be set while running your app so that it loads the bindings from the correct location.
If you want to contribute to this project, install the project with npm. In the project directory, enter these commands:
# Switch to the supported version of Node.js for building
$ nvm use
# Install pnpm
$ npm install -g pnpm
# Install dependencies
$ pnpm install
# Build the SDK
$ pnpm run build
After installation, run the test suite by entering this command:
$ pnpm test
In case the tests don't run, you may need to run a build first:
$ pnpm build
Instantiate a c2pa
object by using createC2pa()
:
import { createC2pa } from 'c2pa-node';
const c2pa = createC2pa();
Use the c2pa.read()
function to read a manifest; for example:
import { createC2pa } from 'c2pa-node';
import { readFile } from 'node:fs/promises';
const c2pa = createC2pa();
async function read(path, mimeType) {
const buffer = await readFile(path);
const result = await c2pa.read({ buffer, mimeType });
if (result) {
const { active_manifest, manifests, validation_status } = result;
console.log(active_manifest);
} else {
console.log('No claim found');
}
}
await read('my-c2pa-file.jpg', 'image/jpeg');
To create a manifest, pass the claim information to the ManifestBuilder
object constructor; for example:
import { ManifestBuilder } from 'c2pa-node';
const manifest = new ManifestBuilder({
claim_generator: 'my-app/1.0.0',
format: 'image/jpeg',
title: 'node_test_local_signer.jpg',
assertions: [
{
label: 'c2pa.actions',
data: {
actions: [
{
action: 'c2pa.created',
},
],
},
},
{
label: 'com.custom.my-assertion',
data: {
description: 'My custom test assertion',
version: '1.0.0',
},
},
],
});
Use c2pa.createIngredient()
to load ingredient data for inclusion into a manifest. You can store the ingredient data on the backend and load it at signing time if necessary (for example if the original ingredient is no longer available); for example:
// Create the ingredient asset from a buffer
const ingredientAssetFromBuffer = {
buffer: await readFile('my-ingredient.jpg'),
mimeType: 'image/jpeg',
};
// Or load from a file
const ingredientAssetFromFile = {
path: resolve('my-ingredient.jpg'),
};
// Create the ingredient
const ingredient = await c2pa.createIngredient({
asset: ingredientAssetFromBuffer,
title: 'ingredient.jpg',
});
// Add it to the manifest
manifest.addIngredient(ingredient);
Use the c2pa.sign()
method to sign an ingredient, either locally if you have a signing certificate and key available, or by using a remote signing API.
If you have an asset file's data loaded into memory, you can sign the the asset using a buffer.
NOTE: Signing using a buffer is currently supported only for image/jpeg
and image/png
data. For all other file types, use the file-based approach .
import { readFile } from 'node:fs/promises';
import { createC2pa, createTestSigner } from 'c2pa-node';
// read an asset into a buffer
const buffer = await readFile('to-be-signed.jpg');
const asset: Asset = { buffer, mimeType: 'image/jpeg' };
// build a manifest to use for signing
const manifest = new ManifestBuilder(
{
claim_generator: 'my-app/1.0.0',
format: 'image/jpeg',
title: 'buffer_signer.jpg',
assertions: [
{
label: 'c2pa.actions',
data: {
actions: [
{
action: 'c2pa.created',
},
],
},
},
{
label: 'com.custom.my-assertion',
data: {
description: 'My custom test assertion',
version: '1.0.0',
},
},
],
},
{ vendor: 'cai' },
);
// create a signing function
async function sign(asset, manifest) {
const signer = await createTestSigner();
const c2pa = createC2pa({
signer,
});
const { signedAsset, signedManifest } = await c2pa.sign({
asset,
manifest,
});
}
// sign
await sign(asset, manifest);
To avoid loading the entire asset into memory (or for file types other than JPEG and PNG that don't support in-memory signing), pass in the file path to the asset file to sign it; for example:
import { resolve } from 'node:path';
import { createC2pa, createTestSigner } from 'c2pa-node';
// get the asset full path
const asset = {
path: resolve('to-be-signed.jpg'),
};
// define a location where to place the signed asset
const outputPath = resolve('signed.jpg');
// create a signing function
async function sign(asset, manifest) {
const signer = await createTestSigner();
const c2pa = createC2pa({
signer,
});
const { signedAsset, signedManifest } = await c2pa.sign({
manifest,
asset,
options: {
outputPath,
},
});
}
// build a manifest to use for signing
const manifest = new ManifestBuilder(
{
claim_generator: 'my-app/1.0.0',
format: 'image/jpeg',
title: 'buffer_signer.jpg',
assertions: [
{
label: 'c2pa.actions',
data: {
actions: [
{
action: 'c2pa.created',
},
],
},
},
{
label: 'com.custom.my-assertion',
data: {
description: 'My custom test assertion',
version: '1.0.0',
},
},
],
},
{ vendor: 'cai' },
);
// sign
await sign(asset, manifest);
If you have a signing certificate and key, you can sign locally using a local signer. This is fine during development, but doing it in production may be insecure. Instead use a Key Management Service (KMS) or a hardware security module (HSM) to access them; for example as show in the C2PA Python Example.
For example:
import { readFile } from 'node:fs/promises';
import { SigningAlgorithm } from 'c2pa-node';
// create a local signer
async function createLocalSigner() {
// make sure to update file paths to read from to match locations where you keep them
const [certificate, privateKey] = await Promise.all([
readFile('<ES256 certificate_file_location>.pem'),
readFile('<ES256 certificate_file_location>.pub'),
]);
return {
type: 'local',
certificate,
privateKey,
algorithm: SigningAlgorithm.ES256,
tsaUrl: 'http://timestamp.digicert.com',
};
}
// read the asset
const buffer = await readFile('to-be-signed.jpg');
// asset mimetype must match the asset type being read
const asset: Asset = { buffer, mimeType: 'image/jpeg' };
// create a signing function
async function sign(asset, manifest) {
const signer = await createLocalSigner();
const c2pa = createC2pa({
signer,
});
const { signedAsset, signedManifest } = await c2pa.sign({
asset,
manifest,
});
}
// build a manifest to use for signing
const manifest = new ManifestBuilder(
{
claim_generator: 'my-app/1.0.0',
format: 'image/jpeg',
title: 'buffer_signer.jpg',
assertions: [
{
label: 'c2pa.actions',
data: {
actions: [
{
action: 'c2pa.created',
},
],
},
},
{
label: 'com.custom.my-assertion',
data: {
description: 'My custom test assertion',
version: '1.0.0',
},
},
],
},
{ vendor: 'cai' },
);
// sign
await sign(asset, manifest);
If you have access to a web service that performs signing, you can use it to sign remotely; for example:
import { readFile } from 'node:fs/promises';
import { fetch, Headers } from 'node-fetch';
import { createC2pa, SigningAlgorithm } from 'c2pa-node';
function createRemoteSigner() {
return {
type: 'remote',
async reserveSize() {
const url = `https://my.signing.service/box-size`;
const res = await fetch(url);
const data = (await res.json()) as { boxSize: number };
return data.boxSize;
},
async sign({ reserveSize, toBeSigned }) {
const url = `https://my.signing.service/sign?boxSize=${reserveSize}`;
const res = await fetch(url, {
method: 'POST',
headers: new Headers({
'Content-Type': 'application/octet-stream',
}),
body: toBeSigned,
});
return res.buffer();
},
};
}
async function sign(asset, manifest) {
const signer = createRemoteSigner();
const c2pa = createC2pa({
signer,
});
const { signedAsset, signedManifest } = await c2pa.sign({
asset,
manifest,
});
}
const buffer = await readFile('to-be-signed.jpg');
const asset: Asset = { buffer, mimeType: 'image/jpeg' };
const manifest = new ManifestBuilder(
{
claim_generator: 'my-app/1.0.0',
format: 'image/jpeg',
title: 'buffer_signer.jpg',
assertions: [
{
label: 'c2pa.actions',
data: {
actions: [
{
action: 'c2pa.created',
},
],
},
},
{
label: 'com.custom.my-assertion',
data: {
description: 'My custom test assertion',
version: '1.0.0',
},
},
],
},
{ vendor: 'cai' },
);
await sign(asset, manifest);
For the API documentation, see the /docs/
directory.
WARNING: The API is subject to change in this early prerelease library.