Skip to content
Closed
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
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions c2pa_c_ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
221 changes: 206 additions & 15 deletions c2pa_c_ffi/src/c_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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<Vec<u8>, 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<u8> = 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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<u8> = vec![0; signed_len_max];
let signed_size = unsafe {
(callback)(
let timestamped_len_max = 100_000;
let mut timestamped_bytes: Vec<u8> = 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),
}))
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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]
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading