diff --git a/Cargo.lock b/Cargo.lock index 341d559ec..278aa7849 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -540,6 +540,9 @@ version = "0.69.0" dependencies = [ "c2pa", "cbindgen", + "chrono", + "fern", + "log", "scopeguard", "serde", "serde_json", @@ -1368,6 +1371,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fern" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" +dependencies = [ + "log", +] + [[package]] name = "ff" version = "0.13.1" diff --git a/c2pa_c_ffi/Cargo.toml b/c2pa_c_ffi/Cargo.toml index bbdf694a7..a635a28f9 100644 --- a/c2pa_c_ffi/Cargo.toml +++ b/c2pa_c_ffi/Cargo.toml @@ -34,6 +34,9 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0.64" tokio = { version = "1.36", features = ["rt-multi-thread", "rt"] } +log = "0.4" +fern = "0.6" +chrono = "0.4" # For timestamps [dev-dependencies] tempfile = "3.7.0" diff --git a/c2pa_c_ffi/src/c_api.rs b/c2pa_c_ffi/src/c_api.rs index 4a9b6da60..1987b2c81 100644 --- a/c2pa_c_ffi/src/c_api.rs +++ b/c2pa_c_ffi/src/c_api.rs @@ -17,6 +17,9 @@ use std::{ ptr, }; +use chrono::Local; +use fern::Dispatch; + /// Validates that a buffer size is within safe bounds and doesn't cause integer overflow /// when used with pointer arithmetic. /// @@ -270,6 +273,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. @@ -297,6 +328,36 @@ pub unsafe extern "C" fn c2pa_version() -> *mut c_char { to_c_string(version) } +/// Sets up file logging. +/// The logger will append any logged text to the end of the log_file provided. +/// +/// # Safety +/// Reads from NULL-terminated C strings. +#[no_mangle] +pub unsafe extern "C" fn c2pa_init_file_logging(log_file: *const c_char) -> c_int { + let log_file = from_cstr_or_return_int!(log_file); + let result = Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "[{}][{}] {}", + Local::now().format("%Y-%m-%d %H:%M:%S"), + record.level(), + message + )) + }) + .level(log::LevelFilter::Info) + .chain(std::io::stdout()) + // Log to a file + .chain(fern::log_file(log_file).unwrap()) + .apply(); + + if result.is_ok() { + 0 + } else { + -1 + } +} + /// Returns the last error message. /// /// # Safety @@ -677,6 +738,23 @@ pub unsafe extern "C" fn c2pa_reader_detailed_json(reader_ptr: *mut C2paReader) to_c_string(c2pa_reader.detailed_json()) } +/// Returns the certificate chain found in the active manifest from a C2paReader. +/// +/// # Safety +/// The returned value MUST be released by calling c2pa_string_free +/// and it is no longer valid after that call. +#[no_mangle] +pub unsafe extern "C" fn c2pa_certs_from_reader(reader_ptr: *mut C2paReader) -> *mut c_char { + check_or_return_null!(reader_ptr); + let c2pa_reader = guard_boxed!(reader_ptr); + if let Some(manifest) = c2pa_reader.active_manifest() { + if let Some(si) = manifest.signature_info() { + return to_c_string(si.cert_chain().to_string()); + } + } + std::ptr::null_mut() +} + /// Returns the remote url of the manifest if it was obtained remotely. /// /// # Parameters @@ -1298,7 +1376,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 +1420,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), })) @@ -1719,6 +1856,9 @@ mod tests { assert!(json_content.contains("manifest")); assert!(json_content.contains("com.example.test-action")); + let certs = unsafe { c2pa_certs_from_reader(reader) }; + assert!(!certs.is_null()); + TestC2paStream::drop_c_stream(source_stream); TestC2paStream::drop_c_stream(read_stream); unsafe { @@ -1804,6 +1944,10 @@ mod tests { #[test] #[cfg(feature = "file_io")] fn test_c2pa_sign_file_null_source_path() { + let log_path_str = "c2pa_test.log"; + let log_path = CString::new(log_path_str).unwrap(); + unsafe { c2pa_init_file_logging(log_path.as_ptr()) }; + let dest_path = CString::new("/tmp/output.jpg").unwrap(); let manifest = CString::new("{}").unwrap(); let signer_info = C2paSignerInfo { @@ -1825,6 +1969,12 @@ mod tests { let error = unsafe { c2pa_error() }; let error_str = unsafe { CString::from_raw(error) }; assert_eq!(error_str.to_str().unwrap(), "NullParameter: source_path"); + + use std::fs; + let content = fs::read(log_path_str); + assert!(content.is_ok()); + assert!(content.unwrap().is_empty()); + let _ = fs::remove_file(log_path_str); } #[test] @@ -2004,6 +2154,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..fd6fa3d26 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 { @@ -184,8 +216,25 @@ impl AsyncSigner for CallbackSigner { self.tsa_url.clone() } - #[cfg(target_arch = "wasm32")] - async fn send_timestamp_request(&self, _message: &[u8]) -> Option>> { + #[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()), + ); + } + } + None } }