diff --git a/c2pa_c_ffi/src/c_api.rs b/c2pa_c_ffi/src/c_api.rs index 4a9b6da60..741cbbcdd 100644 --- a/c2pa_c_ffi/src/c_api.rs +++ b/c2pa_c_ffi/src/c_api.rs @@ -270,6 +270,34 @@ pub type SignerCallback = unsafe extern "C" fn( signed_len: usize, ) -> isize; +/// Creates a signing callback closure from a C callback function. +/// This helper function is shared between c2pa_signer_create and c2pa_signer_create_with_tsa_callback. +fn create_signing_callback( + callback: SignerCallback, +) -> impl Fn(*const (), &[u8]) -> Result, c2pa::Error> + Send + Sync + 'static { + move |context: *const (), data: &[u8]| { + // we need to guess at a max signed size, the callback must verify this is big enough or fail. + let signed_len_max = data.len() * 2; + let mut signed_bytes: Vec = vec![0; signed_len_max]; + let signed_size = unsafe { + (callback)( + context, + data.as_ptr(), + data.len(), + signed_bytes.as_mut_ptr(), + signed_len_max, + ) + }; + if signed_size < 0 { + return Err(c2pa::Error::CoseSignature); // todo:: return errors from callback + } + unsafe { + signed_bytes.set_len(signed_size as usize); + } + Ok(signed_bytes) + } +} + // Internal routine to return a rust String reference to C as *mut c_char. // The returned value MUST be released by calling release_string // and it is no longer valid after that call. @@ -1298,7 +1326,7 @@ pub unsafe extern "C" fn c2pa_format_embeddable( }) } -/// Creates a C2paSigner from a callback and configuration. +/// Creates a C2paSigner from a signing callback and configuration. /// /// # Parameters /// * callback: a callback function to sign data. @@ -1342,30 +1370,89 @@ pub unsafe extern "C" fn c2pa_signer_create( // Create a callback that uses the provided C callback function // The callback ignores its context parameter and will use // the context set on the CallbackSigner closure - let c_callback = move |context: *const (), data: &[u8]| { + let c_callback = create_signing_callback(callback); + + let mut signer = CallbackSigner::new(c_callback, alg.into(), certs).set_context(context); + if let Some(tsa_url) = tsa_url.as_ref() { + signer = signer.set_tsa_url(tsa_url); + } + Box::into_raw(Box::new(C2paSigner { + signer: Box::new(signer), + })) +} + +/// Creates a C2paSigner from a signing callback, a timestamping callback, and configuration. +/// +/// # Parameters +/// * callback: a callback function to sign data. +/// * alg: the signing algorithm. +/// * certs: a pointer to a NULL-terminated string containing the certificate chain in PEM format. +/// * tsa_callback: a callback function to uses for RFC 3161 compliant timestamping of data. +/// +/// # Errors +/// Returns NULL if there were errors, otherwise returns a pointer to a C2paSigner. +/// The error string can be retrieved by calling c2pa_error. +/// +/// # Safety +/// Reads from NULL-terminated C strings. +/// The returned value MUST be released by calling c2pa_signer_free +/// and it is no longer valid after that call. +/// When binding through the C API to other languages, the callback must live long +/// enough, possibly being re-used and called multiple times. The callback is logically +/// owned by the host/caller. +/// +/// # Example +/// ```c +/// auto result = c2pa_signer_create(callback, alg, certs, tsa_callback); +/// if (result == NULL) { +/// auto error = c2pa_error(); +/// printf("Error: %s\n", error); +/// c2pa_string_free(error); +/// } +/// ``` +#[no_mangle] +pub unsafe extern "C" fn c2pa_signer_create_with_tsa_callback( + context: *const c_void, + callback: SignerCallback, + alg: C2paSigningAlg, + certs: *const c_char, + tsa_callback: SignerCallback, +) -> *mut C2paSigner { + let certs = from_cstr_or_return_null!(certs); + let context = context as *const (); + + // Create a callback for signing that uses the provided C callback function + // The callback ignores its context parameter and will use + // the context set on the CallbackSigner closure + let c_signing_callback = create_signing_callback(callback); + + // Create a callback for timestamping that uses the provided C callback function + // The callback ignores its context parameter and will use + // the context set on the CallbackSigner closure + let c_timestamp_callback = move |context: *const (), data: &[u8]| { // we need to guess at a max signed size, the callback must verify this is big enough or fail. - let signed_len_max = data.len() * 2; - let mut signed_bytes: Vec = vec![0; signed_len_max]; - let signed_size = unsafe { - (callback)( + let timestamped_len_max = 100_000; + let mut timestamped_bytes: Vec = vec![0; timestamped_len_max]; + let timestamped_size = unsafe { + (tsa_callback)( context, data.as_ptr(), data.len(), - signed_bytes.as_mut_ptr(), - signed_len_max, + timestamped_bytes.as_mut_ptr(), + timestamped_len_max, ) }; - if signed_size < 0 { + if timestamped_size == -1 { return Err(c2pa::Error::CoseSignature); // todo:: return errors from callback } - signed_bytes.set_len(signed_size as usize); - Ok(signed_bytes) + timestamped_bytes.set_len(timestamped_size as usize); + Ok(timestamped_bytes) }; - let mut signer = CallbackSigner::new(c_callback, alg.into(), certs).set_context(context); - if let Some(tsa_url) = tsa_url.as_ref() { - signer = signer.set_tsa_url(tsa_url); - } + let signer = CallbackSigner::new(c_signing_callback, alg.into(), certs) + .set_context(context) + .set_tsa_callback(c_timestamp_callback); + Box::into_raw(Box::new(C2paSigner { signer: Box::new(signer), })) @@ -2004,6 +2091,47 @@ mod tests { unsafe { c2pa_signer_free(signer) }; } + #[test] + fn test_tsa_callback_signer() { + let certs = include_str!(fixture_path!("certs/ed25519.pub")); + let certs_cstr = CString::new(certs).unwrap(); + + extern "C" fn test_sign_callback( + _context: *const (), + _data: *const c_uchar, + _len: usize, + _signed_bytes: *mut c_uchar, + _signed_len: usize, + ) -> isize { + // Placeholder signer + 1 + } + + extern "C" fn test_tsa_callback( + _context: *const (), + _data: *const c_uchar, + _len: usize, + _signed_bytes: *mut c_uchar, + _signed_len: usize, + ) -> isize { + // Placeholder timestamper + 2 + } + let signer = unsafe { + c2pa_signer_create_with_tsa_callback( + std::ptr::null(), + test_sign_callback, + C2paSigningAlg::Ps384, + certs_cstr.as_ptr(), + test_tsa_callback, + ) + }; + + assert!(!signer.is_null()); + + unsafe { c2pa_signer_free(signer) }; + } + #[test] fn test_sign_with_callback_signer() { // Create an example callback that uses the Ed25519 signing function, diff --git a/sdk/src/callback_signer.rs b/sdk/src/callback_signer.rs index 54e4ff373..882cbc9ce 100644 --- a/sdk/src/callback_signer.rs +++ b/sdk/src/callback_signer.rs @@ -35,7 +35,7 @@ pub struct CallbackSigner { pub context: *const (), /// The callback to use to sign data. - pub callback: Box, + pub signing_callback: Box, /// The signing algorithm to use. pub alg: SigningAlg, @@ -48,6 +48,9 @@ pub struct CallbackSigner { /// The optional URL of a Time Stamping Authority. pub tsa_url: Option, + + /// The callback to use to timestamp data. + pub tsa_callback: Option>, } unsafe impl Send for CallbackSigner {} @@ -65,7 +68,7 @@ impl CallbackSigner { Self { context: std::ptr::null(), - callback: Box::new(callback), + signing_callback: Box::new(callback), alg, certs, reserve_size, @@ -89,6 +92,17 @@ impl CallbackSigner { self } + /// Sets the optional callback for performing C2PA Timestamping. + /// + /// The TSA URL will be used if this is not set. + pub fn set_tsa_callback(mut self, tsa_callback: F) -> Self + where + F: Fn(*const (), &[u8]) -> std::result::Result, Error> + Send + Sync + 'static, + { + self.tsa_callback = Some(Box::new(tsa_callback)); + self + } + /// Sign data using an Ed25519 private key. /// This static function is provided for testing with [`CallbackSigner`]. /// For a released product the private key should be stored securely. @@ -128,18 +142,19 @@ impl Default for CallbackSigner { fn default() -> Self { Self { context: std::ptr::null(), - callback: Box::new(|_, _| Err(Error::UnsupportedType)), + signing_callback: Box::new(|_, _| Err(Error::UnsupportedType)), alg: SigningAlg::Es256, certs: Vec::new(), reserve_size: 10000, tsa_url: None, + tsa_callback: None, } } } impl Signer for CallbackSigner { fn sign(&self, data: &[u8]) -> Result> { - (self.callback)(self.context, data) + (self.signing_callback)(self.context, data) } fn alg(&self) -> SigningAlg { @@ -155,8 +170,25 @@ impl Signer for CallbackSigner { self.reserve_size } - fn time_authority_url(&self) -> Option { - self.tsa_url.clone() + #[allow(unused)] // message not used on WASM + fn send_timestamp_request(&self, message: &[u8]) -> Option>> { + #[cfg(not(target_arch = "wasm32"))] + // Try to use the timestamp callback but then fallback on the URL. + if let Some(tsa_callback) = &self.tsa_callback { + return Some(tsa_callback(self.context, message)); + } else if let Some(url) = &self.tsa_url { + if let Ok(body) = crate::crypto::time_stamp::default_rfc3161_message(message) { + let headers: Option> = None; + return Some( + crate::crypto::time_stamp::default_rfc3161_request( + url, headers, &body, message, + ) + .map_err(|e| e.into()), + ); + } + } + + None } } @@ -164,7 +196,7 @@ impl Signer for CallbackSigner { #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl AsyncSigner for CallbackSigner { async fn sign(&self, data: Vec) -> Result> { - (self.callback)(self.context, &data) + (self.signing_callback)(self.context, &data) } fn alg(&self) -> SigningAlg { @@ -180,12 +212,25 @@ impl AsyncSigner for CallbackSigner { self.reserve_size } - fn time_authority_url(&self) -> Option { - self.tsa_url.clone() - } + #[allow(unused)] // message not used on WASM + async fn send_timestamp_request(&self, message: &[u8]) -> Option>> { + #[cfg(not(target_arch = "wasm32"))] + // Try to use the timestamp callback but then fallback on the URL. + if let Some(tsa_callback) = &self.tsa_callback { + return Some(tsa_callback(self.context, message)); + } else if let Some(url) = &self.tsa_url { + if let Ok(body) = crate::crypto::time_stamp::default_rfc3161_message(message) { + let headers: Option> = None; + return Some( + crate::crypto::time_stamp::default_rfc3161_request_async( + url, headers, &body, message, + ) + .await + .map_err(|e| e.into()), + ); + } + } - #[cfg(target_arch = "wasm32")] - async fn send_timestamp_request(&self, _message: &[u8]) -> Option>> { None } }