Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions packages/c2pa-wasm/src/wasm_signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ use wasm_bindgen::JsValue;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;

use crate::error::WasmError;

#[wasm_bindgen(typescript_custom_section)]
const SIGNER_DEFINITION: &'static str = r#"
interface SignerDefinition {
sign: (bytes: Uint8Array<ArrayBuffer>) => Promise<Uint8Array<ArrayBuffer>>;
reserveSize: number;
alg: string;
certs: Uint8Array[];
directCoseHandling: boolean;
}
"#;

Expand All @@ -35,6 +39,8 @@ pub(crate) struct WasmSigner {
sign_fn: JsFunction,
reserve_size: f64,
signing_alg: SigningAlg,
certs: Vec<Vec<u8>>,
direct_cose_handling: bool,
}

/**
Expand Down Expand Up @@ -65,10 +71,21 @@ impl WasmSigner {

let sign_fn: JsFunction = Reflect::get(&js_value, &"sign".into())?.into();

let direct_cose_handling_js_value = Reflect::get(&js_value, &"directCoseHandling".into())?;
let direct_cose_handling: bool =
serde_wasm_bindgen::from_value(direct_cose_handling_js_value)
.map_err(WasmError::from)?;

let certs_js_value = Reflect::get(&js_value, &"certs".into())?;
let certs: Vec<Vec<u8>> =
serde_wasm_bindgen::from_value(certs_js_value).map_err(WasmError::from)?;

Ok(WasmSigner {
reserve_size: reserve_size_result.into(),
signing_alg,
sign_fn,
direct_cose_handling,
certs,
})
}
}
Expand Down Expand Up @@ -106,15 +123,14 @@ impl AsyncSigner for WasmSigner {

fn certs(&self) -> C2paResult<Vec<Vec<u8>>> {
// @TODO: make configurable
Ok(Vec::new())
Ok(self.certs.clone())
}

fn reserve_size(&self) -> usize {
self.reserve_size as usize
}

fn direct_cose_handling(&self) -> bool {
// @TODO: make configurable
true
self.direct_cose_handling
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

}
}
54 changes: 53 additions & 1 deletion packages/c2pa-web/src/lib/builder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import { getBlobForAsset } from 'test/utils.js';
import { Settings } from './settings.js';
import { createC2pa } from './c2pa.js';
import wasmSrc from '@contentauth/c2pa-web/resources/c2pa.wasm?url';
import { createTestSigner } from 'test/testSigner.js';

import C_with_CAWG_data_thumbnail from 'test/assets/C_with_CAWG_data_thumbnail.jpg';
import C_JPG from 'test/assets/C.jpg';

describe('builder', () => {
Expand Down Expand Up @@ -181,7 +183,6 @@ describe('builder', () => {
instance_id: ''
};

// Create builder with generateC2paArchive enabled
const builder = await c2pa.builder.fromDefinition(manifestDefinition);

const blob = await getBlobForAsset(C_JPG);
Expand Down Expand Up @@ -360,6 +361,57 @@ describe('builder', () => {
expect(definition.ingredients).toHaveLength(1);
expect(definition.ingredients?.[0]).toMatchObject(ingredient);
});

describe('sign', () => {
test.only('should sign and return bytes representing the signed asset', async ({
c2pa
}) => {
const manifestDefinition: ManifestDefinition = {
claim_generator_info: [
{
name: 'c2pa-web-test',
version: '1.0.0'
}
],
title: 'Test_Manifest',
format: 'image/jpeg',
assertions: [],
ingredients: [],
instance_id: ''
};

const builder = await c2pa.builder.fromDefinition(manifestDefinition);

const blob = await getBlobForAsset(C_with_CAWG_data_thumbnail);

const testSigner = await createTestSigner();

const signedBytes = await builder.sign(
testSigner,
'image/jpeg',
blob
);

const reader = await c2pa.reader.fromBlob(
'image/jpeg',
new Blob([signedBytes])
);

const activeManifest = await reader!.activeManifest();

expect(activeManifest.claim_generator_info).toMatchObject(
manifestDefinition.claim_generator_info!
);

expect(activeManifest.signature_info).toMatchObject({
alg: 'Ed25519',
cert_serial_number:
'638838410810235485828984295321338730070538954823',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this coming from?

common_name: 'C2PA Signer',
issuer: 'C2PA Test Signing Cert'
});
});
});
});
});
});
10 changes: 8 additions & 2 deletions packages/c2pa-web/src/lib/signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,27 @@ export interface Signer {
) => Promise<Uint8Array<ArrayBuffer>>;
reserveSize: () => Promise<number>;
alg: SigningAlg;
certs?: Uint8Array<ArrayBuffer>[];
directCoseHandling?: boolean;
}

export interface SerializableSigningPayload {
reserveSize: number;
alg: SigningAlg;
certs: Uint8Array<ArrayBuffer>[];
directCoseHandling: boolean;
}

export async function getSerializablePayload(
signer: Signer
): Promise<SerializableSigningPayload> {
const { alg } = signer;
const { alg, certs, directCoseHandling } = signer;
const reserveSize = await signer.reserveSize();

return {
reserveSize,
alg
alg,
certs: certs ?? [],
directCoseHandling: directCoseHandling ?? true
};
}
13 changes: 10 additions & 3 deletions packages/c2pa-web/src/lib/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,12 @@ rx(
},
async builder_sign(builderId, requestId, payload, format, blob) {
const builder = builderMap.get(builderId);
const signedBytes = (await builder.sign(
const signedBytes = await builder.sign(
{
reserveSize: payload.reserveSize,
alg: payload.alg,
directCoseHandling: payload.directCoseHandling,
certs: payload.certs,
sign: async (bytes) => {
const result = await tx.sign(
requestId,
Expand All @@ -146,8 +148,11 @@ rx(
},
format,
blob
)) as Uint8Array<ArrayBuffer>;
return transfer(signedBytes, signedBytes.buffer);
);
return transfer(
signedBytes as Uint8Array<ArrayBuffer>,
signedBytes.buffer
);
},
async builder_signAndGetManifestBytes(
builderId,
Expand All @@ -161,6 +166,8 @@ rx(
{
reserveSize: payload.reserveSize,
alg: payload.alg,
directCoseHandling: payload.directCoseHandling,
certs: payload.certs,
sign: async (bytes) => {
const result = await tx.sign(
requestId,
Expand Down
76 changes: 76 additions & 0 deletions packages/c2pa-web/test/testSigner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* Copyright 2026 Adobe
* All Rights Reserved.
*
* NOTICE: Adobe permits you to use, modify, and distribute this file in
* accordance with the terms of the Adobe license agreement accompanying
* it.
*/

import type { Signer } from '../src/lib/signer.js';

import ed25519_pub from './trust/ed25519.pub?raw';
import ed25519_pem from './trust/ed25519.pem?raw';

/**
* Creates a signer suitable for testing from an ed25519 private key / certificate pair.
*/
export async function createTestSigner(): Promise<Signer> {
const certBytes = extractDerBytes(ed25519_pub);

const privateKeyBytes = extractDerBytes(ed25519_pem);

const privateKey = await crypto.subtle.importKey(
'pkcs8',
privateKeyBytes,
{
name: 'Ed25519'
},
false,
['sign']
);

return {
async sign(data) {
const signature = await crypto.subtle.sign(
{
name: 'Ed25519'
},
privateKey,
data
);

return new Uint8Array(signature);
},
async reserveSize() {
return 1000;
},
alg: 'ed25519',
certs: [new Uint8Array(certBytes)],
directCoseHandling: false
};
}

function extractDerBytes(str: string): ArrayBuffer {
const pemHeader = '-----BEGIN PRIVATE KEY-----';
const pemFooter = '-----END PRIVATE KEY-----';
const pemContents = str.substring(
pemHeader.length,
str.length - pemFooter.length - 1
);
// base64 decode the string to get the binary data
const binaryDerString = window.atob(pemContents);
// convert from a binary string to an ArrayBuffer
const binaryDer = str2ab(binaryDerString);

return binaryDer;
}

function str2ab(str: string): ArrayBuffer {
const buf = new ArrayBuffer(str.length);
const bufView = new Uint8Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we simplify?

Suggested change
for (let i = 0, strLen = str.length; i < strLen; i++) {
for (let i = 0, i < str.length; i++) {

bufView[i] = str.charCodeAt(i);
}
return buf;
}
3 changes: 3 additions & 0 deletions packages/c2pa-web/test/trust/ed25519.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels weird to commit the private key, but I guess we don't have many other choices. 🤔

MC4CAQAwBQYDK2VwBCIEIL2+9INLPNSLH3STzKQJ3Wen9R6uPbIYOIKA2574YQ4O
-----END PRIVATE KEY-----
15 changes: 15 additions & 0 deletions packages/c2pa-web/test/trust/ed25519.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-----BEGIN CERTIFICATE-----
MIICSDCCAfqgAwIBAgIUb+aBTX1CsjJ1iuMJ9kRudz/7qEcwBQYDK2VwMIGMMQsw
CQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExEjAQBgNVBAcMCVNvbWV3aGVyZTEnMCUG
A1UECgweQzJQQSBUZXN0IEludGVybWVkaWF0ZSBSb290IENBMRkwFwYDVQQLDBBG
T1IgVEVTVElOR19PTkxZMRgwFgYDVQQDDA9JbnRlcm1lZGlhdGUgQ0EwHhcNMjIw
NjEwMTg0NjQxWhcNMzAwODI2MTg0NjQxWjCBgDELMAkGA1UEBhMCVVMxCzAJBgNV
BAgMAkNBMRIwEAYDVQQHDAlTb21ld2hlcmUxHzAdBgNVBAoMFkMyUEEgVGVzdCBT
aWduaW5nIENlcnQxGTAXBgNVBAsMEEZPUiBURVNUSU5HX09OTFkxFDASBgNVBAMM
C0MyUEEgU2lnbmVyMCowBQYDK2VwAyEAMp5+0e83nNgQhdhBW8Rshkjy90sa1A9J
IzkItcDqCuKjeDB2MAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUH
AwQwDgYDVR0PAQH/BAQDAgbAMB0GA1UdDgQWBBTuLrYRqW4wu6yjIK1/iW8ud7dm
kTAfBgNVHSMEGDAWgBRXTAfC/JxQvRlk/bCbdPMDbsSfqTAFBgMrZXADQQB2R6vb
I+X8CTRC54j3NTvsUj454G1/bdzbiHVgl3n+ShOAJ85FJigE7Eoav7SeXeVnNjc8
QZ1UrJGwgBBEP84G
-----END CERTIFICATE-----
8 changes: 6 additions & 2 deletions packages/c2pa-web/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@ export default defineConfig(() => ({
entryRoot: 'src',
tsconfigPath: join(__dirname, 'tsconfig.lib.json'),
afterDiagnostic(diagnostics) {
const errors = diagnostics.filter(d => d.category === ts.DiagnosticCategory.Error);
const errors = diagnostics.filter(
(d) => d.category === ts.DiagnosticCategory.Error
);
if (errors.length > 0) {
throw new Error(`vite-plugin-dts: Found ${errors.length} type error(s).`);
throw new Error(
`vite-plugin-dts: Found ${errors.length} type error(s).`
);
}
}
}),
Expand Down
Loading