Skip to content

Commit 595304e

Browse files
authored
feat: Allow C2PA to perform COSE signing if desired (#21)
* feat: Allow C2PA to perform COSE signing if desired Implement AsyncRawSigner so that the rust SDK will handle COSE signing if `directCoseHandling` is true. https://github.com/contentauth/c2pa-rs/blob/2ffde3d1321aedef6ee0632b06453b74da370940/sdk/src/cose_sign.rs#L145
1 parent ac62652 commit 595304e

File tree

6 files changed

+105
-8
lines changed

6 files changed

+105
-8
lines changed

.changeset/chilly-carrots-marry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@contentauth/c2pa-node": minor
3+
---
4+
5+
Implement AsyncRawSigner so that the rust SDK will handle COSE signing if directCoseHandling is true.

js-src/Signer.spec.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ import * as fs from "fs-extra";
1515
import * as crypto from "crypto";
1616

1717
import { CallbackSigner } from "./Signer.js";
18+
import { Builder } from "./Builder.js";
19+
import { Reader } from "./Reader.js";
1820
import type { JsCallbackSignerConfig, SigningAlg } from "./types.d.ts";
21+
import type { Manifest } from "@contentauth/c2pa-types";
1922

2023
class TestSigner {
2124
private privateKey: crypto.KeyObject;
@@ -106,4 +109,75 @@ describe("CallbackSigner", () => {
106109
const signer = CallbackSigner.newSigner(config, async (data) => data);
107110
expect(signer.timeAuthorityUrl()).toBeUndefined();
108111
});
112+
113+
it("should create valid COSE signature with buffer output", async () => {
114+
const testSigner = new TestSigner(
115+
await fs.readFile("./tests/fixtures/certs/es256.pem"),
116+
);
117+
118+
const createRawSignatureCallback = () => {
119+
return async (data: Buffer): Promise<Buffer> => {
120+
// For directCoseHandling: false, return raw signature data
121+
// The c2pa SDK will handle COSE wrapping internally
122+
return await testSigner.sign(data);
123+
};
124+
};
125+
126+
// Create a simple manifest
127+
const manifestDefinition: Manifest = {
128+
vendor: "test",
129+
claim_generator: "test-generator",
130+
claim_generator_info: [
131+
{
132+
name: "c2pa_test",
133+
version: "1.0.0",
134+
},
135+
],
136+
title: "Test_Manifest_Buffer",
137+
format: "image/jpeg",
138+
instance_id: "5678",
139+
assertions: [
140+
{
141+
label: "org.test.assertion",
142+
data: { message: "Buffer test with directCoseHandling: false" },
143+
},
144+
],
145+
resources: { resources: {} },
146+
};
147+
148+
const builder = Builder.withJson(manifestDefinition);
149+
const source = {
150+
buffer: await fs.readFile("./tests/fixtures/CA.jpg"),
151+
mimeType: "image/jpeg",
152+
};
153+
154+
const config: JsCallbackSignerConfig = {
155+
alg: "es256" as SigningAlg,
156+
certs: [fs.readFileSync("./tests/fixtures/certs/es256.pub")],
157+
reserveSize: 10000,
158+
tsaUrl: undefined,
159+
directCoseHandling: false,
160+
};
161+
162+
const callback = createRawSignatureCallback();
163+
const signer = CallbackSigner.newSigner(config, callback);
164+
165+
// Sign the manifest to buffer
166+
const dest = { buffer: null };
167+
await builder.signAsync(signer, source, dest);
168+
169+
// Read the signed manifest back and verify signature is valid
170+
const reader = await Reader.fromAsset({
171+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
172+
buffer: dest.buffer! as Buffer,
173+
mimeType: "image/jpeg",
174+
});
175+
const manifestStore = reader.json();
176+
const activeManifest = reader.getActive();
177+
178+
// If validation_status is undefined, the signature is valid
179+
expect(manifestStore.validation_status).toBeUndefined();
180+
expect(manifestStore.active_manifest).not.toBeUndefined();
181+
expect(activeManifest?.title).toBe("Test_Manifest_Buffer");
182+
});
109183
});

js-src/Signer.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,10 @@ export class CallbackSigner implements CallbackSignerInterface {
105105
this.callbackSigner,
106106
);
107107
}
108+
109+
directCoseHandling(): boolean {
110+
return getNeonBinary().callbackSignerDirectCoseHandling.call(
111+
this.callbackSigner,
112+
);
113+
}
108114
}

js-src/types.d.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export interface CallbackSignerInterface {
118118
certs(): Array<Buffer>;
119119
reserveSize(): number;
120120
timeAuthorityUrl(): string | undefined;
121+
directCoseHandling(): boolean;
121122
signer(): NeonCallbackSignerHandle;
122123
}
123124

@@ -153,10 +154,8 @@ export interface JsCallbackSignerConfig {
153154
tsaUrl?: string;
154155
tsaHeaders?: Array<[string, string]>;
155156
tsaBody?: Buffer;
156-
// TODO: directCoseHandling is currently not implemented in the signing logic.
157-
// The field is read from config but the actual signing implementation does not
158-
// differentiate between directCoseHandling: true/false - both cases pass the
159-
// same data to the JavaScript callback regardless of this setting.
157+
// When true, the callback function should return fully-formed COSE data.
158+
// When false, the callback function should return raw signature data and the c2pa SDK will handle COSE wrapping.
160159
directCoseHandling: boolean;
161160
}
162161

src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> {
131131
"callbackSignerTimeAuthorityUrl",
132132
neon_signer::NeonCallbackSigner::time_authority_url,
133133
)?;
134+
cx.export_function(
135+
"callbackSignerDirectCoseHandling",
136+
neon_signer::NeonCallbackSigner::direct_cose_handling,
137+
)?;
134138

135139
// Identity Assertions
136140
cx.export_function(

src/neon_signer.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,11 @@ impl NeonCallbackSigner {
201201
}
202202
}
203203

204+
pub fn direct_cose_handling(mut cx: FunctionContext) -> JsResult<JsBoolean> {
205+
let this = cx.this::<JsBox<Self>>()?;
206+
Ok(cx.boolean(this.config.direct_cose_handling))
207+
}
208+
204209
pub fn sign(mut cx: FunctionContext) -> JsResult<JsPromise> {
205210
let rt = runtime();
206211
let this = cx.this::<JsBox<Self>>()?;
@@ -321,12 +326,16 @@ impl AsyncSigner for NeonCallbackSigner {
321326
}
322327

323328
fn direct_cose_handling(&self) -> bool {
324-
// TODO: The direct_cose_handling field is currently not used in the signing logic.
325-
// The field is read from config and exposed via this getter, but the actual signing
326-
// implementation does not differentiate between directCoseHandling: true/false.
327-
// Both cases pass the same data to the JavaScript callback regardless of this setting.
328329
self.config.direct_cose_handling
329330
}
331+
332+
fn async_raw_signer(&self) -> Option<Box<&dyn AsyncRawSigner>> {
333+
if self.config.direct_cose_handling {
334+
None
335+
} else {
336+
Some(Box::new(self))
337+
}
338+
}
330339
}
331340

332341
impl TimeStampProvider for NeonCallbackSigner {

0 commit comments

Comments
 (0)