Skip to content

Commit 94bf7ce

Browse files
committed
Broken CAWG signing.
1 parent 9f92c7f commit 94bf7ce

File tree

8 files changed

+145
-50
lines changed

8 files changed

+145
-50
lines changed

js-src/Builder.spec.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -381,9 +381,6 @@ describe("Builder", () => {
381381
const reader = await Reader.fromAsset(dest);
382382
const manifest = reader.json();
383383

384-
// Ensure JSON parsing the manifest doesn't include any backslashes
385-
const manifestJson = JSON.stringify(manifest);
386-
387384
// Check that our specific JSON assertion doesn't have escaped characters
388385
const activeManifest = manifest.manifests[manifest.active_manifest!];
389386
const fingerprintAssertionData = activeManifest?.assertions?.find(
@@ -405,7 +402,6 @@ describe("Builder", () => {
405402
expect(JSON.stringify(fingerprintAssertionData?.data)).not.toContain(
406403
"\\",
407404
);
408-
console.log(manifestJson);
409405
});
410406

411407
it("should archive and restore builder with ingredient thumbnail", async () => {

js-src/IdentityAssertion.spec.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ describe("IdentityAssertionBuilder", () => {
150150

151151
const source = {
152152
buffer: await fs.readFile("./tests/fixtures/CA.jpg"),
153-
mimeType: "jpeg",
153+
mimeType: "image/jpeg",
154154
};
155155
const dest: DestinationBufferAsset = {
156156
buffer: null,
@@ -161,30 +161,30 @@ describe("IdentityAssertionBuilder", () => {
161161

162162
// Add the required resources
163163
await builder.addResource("thumbnail.jpg", {
164-
mimeType: "jpeg",
164+
mimeType: "image/jpeg",
165165
buffer: await fs.readFile("./tests/fixtures/thumbnail.jpg"),
166166
});
167167
await builder.addResource("ingredient-thumb.jpg", {
168-
mimeType: "jpeg",
168+
mimeType: "image/jpeg",
169169
buffer: await fs.readFile("./tests/fixtures/thumbnail.jpg"),
170170
});
171171

172172
// Create and configure the identity assertion
173-
const ia_signer = IdentityAssertionSigner.new(c2paSigner);
173+
const iaSigner = IdentityAssertionSigner.new(c2paSigner.signer());
174174
const iab =
175175
await IdentityAssertionBuilder.identityBuilderForCredentialHolder(
176-
cawgSigner,
176+
cawgSigner.signer(),
177177
);
178178
iab.addReferencedAssertions(["cawg.training-mining"]);
179-
ia_signer.addIdentityAssertion(iab);
179+
iaSigner.addIdentityAssertion(iab);
180180

181-
// Sign the manifest
182-
await builder.signAsync(c2paSigner, source, dest);
181+
// Sign the manifest (standard async flow)
182+
await builder.signAsync(iaSigner, source, dest);
183183

184184
// Verify the manifest
185185
const reader = await Reader.fromAsset({
186186
buffer: dest.buffer! as Buffer,
187-
mimeType: "jpeg",
187+
mimeType: "image/jpeg",
188188
});
189189
await reader.postValidateCawg();
190190
});

js-src/Settings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export async function loadSettingsFromFile(filePath: string): Promise<void> {
4242
const ext = path.extname(filePath).toLowerCase();
4343
const content = await fs.readFile(filePath, "utf8");
4444
if (ext === ".toml") {
45-
loadC2paSettings(content);
45+
loadC2paSettingsToml(content);
4646
return;
4747
}
4848
// Assume JSON (or JSON5 that is valid JSON) otherwise

js-src/Signer.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313

1414
const neon = require("./index.node");
1515
import type {
16-
LocalSignerInterface,
1716
CallbackSignerInterface,
18-
SigningAlg,
17+
CallbackCredentialHolderInterface,
1918
JsCallbackSignerConfig,
19+
LocalSignerInterface,
20+
SignerPayload,
21+
SigningAlg,
2022
} from "./types";
2123

2224
export class LocalSigner implements LocalSignerInterface {
@@ -99,3 +101,40 @@ export class CallbackSigner implements CallbackSignerInterface {
99101
return neon.callbackSignerTimeAuthorityUrl.call(this.callbackSigner);
100102
}
101103
}
104+
105+
export class CallbackCredentialHolder
106+
implements CallbackCredentialHolderInterface
107+
{
108+
constructor(
109+
private callbackCredentialHolder: CallbackSignerInterface,
110+
) {}
111+
112+
signer(): CallbackSignerInterface {
113+
return this.callbackCredentialHolder;
114+
}
115+
116+
static newSigner(
117+
config: JsCallbackSignerConfig,
118+
callback: (data: Buffer) => Promise<Buffer>,
119+
) {
120+
// Convert the config object to a JsBox<CallbackSignerConfig>
121+
const configBox = neon.callbackSignerConfigFromJs(config);
122+
const signer = neon.callbackSignerFromConfig(configBox, callback);
123+
return new CallbackCredentialHolder(signer);
124+
}
125+
126+
async sign(payload: SignerPayload): Promise<Buffer> {
127+
return neon.callbackSignerSignPayload.call(
128+
this.callbackCredentialHolder,
129+
payload,
130+
);
131+
}
132+
133+
reserveSize(): number {
134+
return neon.callbackSignerReserveSize.call(this.callbackCredentialHolder);
135+
}
136+
137+
sigType(): string {
138+
return "cawg.x509.cose";
139+
}
140+
}

js-src/index.node.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ import type {
1818
CallbackSignerInterface,
1919
ClaimVersion,
2020
DestinationAsset,
21+
CallbackCredentialHolderInterface,
2122
IdentityAssertionBuilderInterface,
2223
IdentityAssertionSignerInterface,
2324
JsCallbackSignerConfig,
2425
LocalSignerInterface,
2526
ManifestAssertionKind,
2627
ReaderInterface,
28+
SignerPayload,
2729
SigningAlg,
2830
SourceAsset,
2931
TrustmarkConfig,

js-src/types.d.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,13 @@ export interface CallbackSignerInterface {
121121
signer(): CallbackSignerInterface;
122122
}
123123

124+
export interface CallbackCredentialHolderInterface {
125+
sigType(): string;
126+
reserveSize(): number;
127+
sign(payload: SignerPayload): Promise<Buffer>;
128+
signer(): CallbackSignerInterface;
129+
}
130+
124131
/**
125132
* @internal
126133
* Internal type used for Rust/Node.js interop
@@ -141,6 +148,18 @@ export interface JsCallbackSignerConfig {
141148
directCoseHandling: boolean;
142149
}
143150

151+
export interface SignerPayload {
152+
referencedAssertions: HashedUri[];
153+
sigType: string;
154+
roles: string[];
155+
}
156+
157+
export interface HashedUri {
158+
url: string;
159+
hash: Uint8Array;
160+
alg?: string;
161+
}
162+
144163
export interface BuilderInterface {
145164
/**
146165
* Set the no embed flag of the manifest
@@ -221,7 +240,7 @@ export interface BuilderInterface {
221240
* @returns the bytes of the c2pa_manifest that was embedded
222241
*/
223242
signAsync(
224-
callbackSigner: CallbackSignerInterface,
243+
callbackSigner: CallbackSignerInterface | IdentityAssertionSignerInterface,
225244
input: SourceAsset,
226245
output: DestinationAsset,
227246
): Promise<Buffer>;
@@ -239,20 +258,6 @@ export interface BuilderInterface {
239258
output: DestinationAsset,
240259
): Buffer;
241260

242-
/**
243-
* Sign an asset from a buffer or file asynchronously, using an
244-
* IdentityAssertionSigner
245-
* @param signer The IdentityAssertionSigner
246-
* @param source The file or buffer containing the asset
247-
* @param dest The file or buffer to write the asset to
248-
* @returns the bytes of the c2pa_manifest that was embedded
249-
*/
250-
identitySignAsync(
251-
callbackSigner: IdentityAssertionSignerInterface,
252-
input: SourceAsset,
253-
output: DestinationAsset,
254-
): Promise<Buffer>;
255-
256261
/**
257262
* Getter for the builder's manifest definition
258263
* @returns The manifest definition

src/neon_identity_assertion_builder.rs

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ use c2pa::{
1717
identity::{builder::AsyncCredentialHolder, SignerPayload},
1818
};
1919
use neon::prelude::*;
20+
use serde::{Deserialize, Serialize};
21+
use serde_bytes::ByteBuf;
2022
use std::ops::Deref;
2123
use std::sync::RwLock;
2224

@@ -28,6 +30,22 @@ pub struct NeonIdentityAssertionBuilder {
2830
roles: RwLock<Vec<String>>,
2931
}
3032

33+
#[derive(Deserialize, Serialize)]
34+
struct IdentityAssertion {
35+
signer_payload: SignerPayload,
36+
37+
signature: Vec<u8>,
38+
39+
pad1: Vec<u8>,
40+
41+
// Must use explicit ByteBuf here because #[serde(with = "serde_bytes")]
42+
// does not work with Option<Vec<u8>>.
43+
pad2: Option<ByteBuf>,
44+
45+
// Label for the assertion. Only assigned when reading from a manifest.
46+
label: Option<String>,
47+
}
48+
3149
// Note: unwrap is used on read() and write() results, as poisoning only occurs as a result of a
3250
// panic on another thread.
3351
impl Clone for NeonIdentityAssertionBuilder {
@@ -108,9 +126,11 @@ impl AsyncDynamicAssertion for NeonIdentityAssertionBuilder {
108126
let referenced_assertions = claim
109127
.assertions()
110128
.filter(|a| {
129+
// Always include hash data assertions
111130
if a.url().contains("c2pa.assertions/c2pa.hash.") {
112131
return true;
113132
}
133+
// Otherwise include if user-added label matches
114134
let label = if let Some((_, label)) = a.url().rsplit_once('/') {
115135
label.to_string()
116136
} else {
@@ -138,20 +158,52 @@ impl AsyncDynamicAssertion for NeonIdentityAssertionBuilder {
138158
.await
139159
.map_err(|e| c2pa::Error::OtherError(Box::new(e)))?;
140160

141-
let mut assertion_cbor: Vec<u8> = vec![];
142-
ciborium::into_writer(&(signer_payload, signature), &mut assertion_cbor)
161+
finalize_identity_assertion(signer_payload, size, signature)
162+
}
163+
}
164+
fn finalize_identity_assertion(
165+
signer_payload: SignerPayload,
166+
size: Option<usize>,
167+
signature: Vec<u8>,
168+
) -> c2pa::Result<DynamicAssertionContent> {
169+
let mut ia = IdentityAssertion {
170+
signer_payload,
171+
signature,
172+
pad1: vec![],
173+
pad2: None,
174+
label: None,
175+
};
176+
177+
let mut assertion_cbor: Vec<u8> = vec![];
178+
ciborium::into_writer(&ia, &mut assertion_cbor)
179+
.map_err(|e| c2pa::Error::BadParam(e.to_string()))?;
180+
// TO DO: Think through how errors map into crate::Error.
181+
182+
if let Some(assertion_size) = size {
183+
if assertion_cbor.len() > assertion_size {
184+
return Err(c2pa::Error::BadParam(format!("Serialized assertion is {len} bytes, which exceeds the planned size of {assertion_size} bytes", len = assertion_cbor.len())));
185+
}
186+
187+
ia.pad1 = vec![0u8; assertion_size - assertion_cbor.len() - 15];
188+
189+
assertion_cbor.clear();
190+
ciborium::into_writer(&ia, &mut assertion_cbor)
143191
.map_err(|e| c2pa::Error::BadParam(e.to_string()))?;
192+
// TO DO: Think through how errors map into crate::Error.
144193

145-
if let Some(assertion_size) = size {
146-
if assertion_cbor.len() > assertion_size {
147-
return Err(c2pa::Error::BadParam(format!(
148-
"Serialized assertion is {} bytes, which exceeds the planned size of {} bytes",
149-
assertion_cbor.len(),
150-
assertion_size
151-
)));
152-
}
153-
}
194+
ia.pad2 = Some(ByteBuf::from(vec![
195+
0u8;
196+
assertion_size - assertion_cbor.len() - 6
197+
]));
154198

155-
Ok(DynamicAssertionContent::Cbor(assertion_cbor))
199+
assertion_cbor.clear();
200+
ciborium::into_writer(&ia, &mut assertion_cbor)
201+
.map_err(|e| c2pa::Error::BadParam(e.to_string()))?;
202+
// TO DO: Think through how errors map into crate::Error.
203+
204+
// TO DO: See if this approach ever fails. IMHO it "should" work for all cases.
205+
assert_eq!(assertion_size, assertion_cbor.len());
156206
}
207+
208+
Ok(DynamicAssertionContent::Cbor(assertion_cbor))
157209
}

src/neon_signer.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ use async_trait::async_trait;
1616
use c2pa::{
1717
create_signer,
1818
crypto::{
19-
cose::{sign, TimeStampStorage},
2019
raw_signature::{AsyncRawSigner, RawSigner, RawSignerError},
2120
time_stamp::{AsyncTimeStampProvider, TimeStampProvider},
2221
},
@@ -26,7 +25,7 @@ use c2pa::{
2625
},
2726
AsyncSigner,
2827
Error::OtherError,
29-
Signer, SigningAlg,
28+
HashedUri, Signer, SigningAlg,
3029
};
3130
use neon::prelude::*;
3231
use neon::types::buffer::TypedArray;
@@ -319,10 +318,10 @@ impl TimeStampProvider for NeonCallbackSigner {
319318

320319
impl AsyncTimeStampProvider for NeonCallbackSigner {
321320
fn time_stamp_service_url(&self) -> Option<String> {
322-
TimeStampProvider::time_stamp_service_url(self)
321+
self.config.tsa_url.clone()
323322
}
324323
fn time_stamp_request_headers(&self) -> Option<Vec<(String, String)>> {
325-
TimeStampProvider::time_stamp_request_headers(self)
324+
self.config.tsa_headers.clone()
326325
}
327326
}
328327

@@ -394,7 +393,7 @@ impl AsyncRawSigner for NeonCallbackSigner {
394393

395394
impl RawSigner for NeonCallbackSigner {
396395
fn sign(&self, _data: &[u8]) -> Result<Vec<u8>, RawSignerError> {
397-
// Instead of blocking, we'll return an error since this is a sync context
396+
// Synchronous signing is not supported; use AsyncRawSigner instead
398397
Err(RawSignerError::InternalError(
399398
"Synchronous signing not supported - use AsyncRawSigner instead".to_string(),
400399
))
@@ -428,8 +427,10 @@ impl AsyncCredentialHolder for NeonCallbackSigner {
428427
ciborium::into_writer(signer_payload, &mut sp_cbor)
429428
.map_err(|e| IdentityBuilderError::CborGenerationError(e.to_string()))?;
430429

431-
// Create a COSE_Sign1 structure using the raw signature
432-
sign(self, &sp_cbor, None, TimeStampStorage::V2_sigTst2_CTT)
430+
// Package into COSE_Sign1 using library-side COSE handling via async wrapper
431+
// Use AsyncSigner::sign to avoid any sync context
432+
c2pa::AsyncSigner::sign(self, sp_cbor)
433+
.await
433434
.map_err(|e| IdentityBuilderError::SignerError(e.to_string()))
434435
}
435436
}

0 commit comments

Comments
 (0)