Skip to content
Open
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
158 changes: 143 additions & 15 deletions c2pa_c_ffi/src/c_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<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 @@ -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.
Expand Down Expand Up @@ -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<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 @@ -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,
Expand Down
69 changes: 57 additions & 12 deletions sdk/src/callback_signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub struct CallbackSigner {
pub context: *const (),

/// The callback to use to sign data.
pub callback: Box<CallbackFunc>,
pub signing_callback: Box<CallbackFunc>,

/// The signing algorithm to use.
pub alg: SigningAlg,
Expand All @@ -48,6 +48,9 @@ pub struct CallbackSigner {

/// The optional URL of a Time Stamping Authority.
pub tsa_url: Option<String>,

/// The callback to use to timestamp data.
pub tsa_callback: Option<Box<CallbackFunc>>,
}

unsafe impl Send for CallbackSigner {}
Expand All @@ -65,7 +68,7 @@ impl CallbackSigner {

Self {
context: std::ptr::null(),
callback: Box::new(callback),
signing_callback: Box::new(callback),
alg,
certs,
reserve_size,
Expand All @@ -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<F>(mut self, tsa_callback: F) -> Self
where
F: Fn(*const (), &[u8]) -> std::result::Result<Vec<u8>, 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.
Expand Down Expand Up @@ -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<Vec<u8>> {
(self.callback)(self.context, data)
(self.signing_callback)(self.context, data)
}

fn alg(&self) -> SigningAlg {
Expand All @@ -155,16 +170,33 @@ impl Signer for CallbackSigner {
self.reserve_size
}

fn time_authority_url(&self) -> Option<String> {
self.tsa_url.clone()
#[allow(unused)] // message not used on WASM
fn send_timestamp_request(&self, message: &[u8]) -> Option<Result<Vec<u8>>> {
#[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<Vec<(String, String)>> = None;
return Some(
crate::crypto::time_stamp::default_rfc3161_request(
url, headers, &body, message,
)
.map_err(|e| e.into()),
);
}
}

None
}
}

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl AsyncSigner for CallbackSigner {
async fn sign(&self, data: Vec<u8>) -> Result<Vec<u8>> {
(self.callback)(self.context, &data)
(self.signing_callback)(self.context, &data)
}

fn alg(&self) -> SigningAlg {
Expand All @@ -180,12 +212,25 @@ impl AsyncSigner for CallbackSigner {
self.reserve_size
}

fn time_authority_url(&self) -> Option<String> {
self.tsa_url.clone()
}
#[allow(unused)] // message not used on WASM
async fn send_timestamp_request(&self, message: &[u8]) -> Option<Result<Vec<u8>>> {
#[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<Vec<(String, String)>> = 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<Result<Vec<u8>>> {
None
}
}