Skip to main content

C2PA Node.js

The c2pa-node repository implements a Node.js API that can:

WARNING: This is an early prerelease version of this library. There may be bugs and unimplemented features, and the API is subject to change.

Installation

Prerequisites

You must install:

If you need to manage multiple versions of Node on your machine, use a tool such as nvm.

Installing for use in a client app

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.

Building custom binaries

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.

Installing for project contributions

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

Testing

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

Usage

Creating a c2pa object

Instantiate a c2pa object by using createC2pa():

import { createC2pa } from 'c2pa-node';
const c2pa = createC2pa();

Reading a manifest

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');

Creating a manifest

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',
},
},
],
});

Adding an ingredient

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);

Signing a manifest

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.

Signing buffers

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);

Signing files

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);

Local signing

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);

Remote signing

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);

API documentation

For the API documentation, see the /docs/ directory.

WARNING: The API is subject to change in this early prerelease library.