#![deny(missing_docs)]
use async_trait::async_trait;
use crate::{crypto::raw_signature::SigningAlg, AsyncSigner, Error, Result, Signer};
pub type CallbackFunc =
dyn Fn(*const (), &[u8]) -> std::result::Result<Vec<u8>, Error> + Send + Sync;
pub struct CallbackSigner {
pub context: *const (),
pub callback: Box<CallbackFunc>,
pub alg: SigningAlg,
pub certs: Vec<u8>,
pub reserve_size: usize,
pub tsa_url: Option<String>,
}
unsafe impl Send for CallbackSigner {}
unsafe impl Sync for CallbackSigner {}
impl CallbackSigner {
pub fn new<F, T>(callback: F, alg: SigningAlg, certs: T) -> Self
where
F: Fn(*const (), &[u8]) -> std::result::Result<Vec<u8>, Error> + Send + Sync + 'static,
T: Into<Vec<u8>>,
{
let certs = certs.into();
let reserve_size = 10000 + certs.len();
Self {
context: std::ptr::null(),
callback: Box::new(callback),
alg,
certs,
reserve_size,
..Default::default()
}
}
pub fn set_tsa_url<S: Into<String>>(mut self, url: S) -> Self {
self.tsa_url = Some(url.into());
self
}
pub fn set_context(mut self, context: *const ()) -> Self {
self.context = context;
self
}
pub fn ed25519_sign(data: &[u8], private_key: &[u8]) -> Result<Vec<u8>> {
use ed25519_dalek::{Signature, Signer, SigningKey};
use pem::parse;
let pem = parse(private_key).map_err(|e| Error::OtherError(Box::new(e)))?;
let key_bytes = pem.contents().get(16..).ok_or(Error::InvalidSigningKey)?;
let signing_key =
SigningKey::try_from(key_bytes).map_err(|e| Error::OtherError(Box::new(e)))?;
let signature: Signature = signing_key.sign(data);
Ok(signature.to_bytes().to_vec())
}
}
impl Default for CallbackSigner {
fn default() -> Self {
Self {
context: std::ptr::null(),
callback: Box::new(|_, _| Err(Error::UnsupportedType)),
alg: SigningAlg::Es256,
certs: Vec::new(),
reserve_size: 10000,
tsa_url: None,
}
}
}
impl Signer for CallbackSigner {
fn sign(&self, data: &[u8]) -> Result<Vec<u8>> {
(self.callback)(self.context, data)
}
fn alg(&self) -> SigningAlg {
self.alg
}
fn certs(&self) -> Result<Vec<Vec<u8>>> {
let pems = pem::parse_many(&self.certs).map_err(|e| Error::OtherError(Box::new(e)))?;
Ok(pems.into_iter().map(|p| p.into_contents()).collect())
}
fn reserve_size(&self) -> usize {
self.reserve_size
}
fn time_authority_url(&self) -> Option<String> {
self.tsa_url.clone()
}
}
#[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)
}
fn alg(&self) -> SigningAlg {
self.alg
}
fn certs(&self) -> Result<Vec<Vec<u8>>> {
let pems = pem::parse_many(&self.certs).map_err(|e| Error::OtherError(Box::new(e)))?;
Ok(pems.into_iter().map(|p| p.into_contents()).collect())
}
fn reserve_size(&self) -> usize {
self.reserve_size
}
fn time_authority_url(&self) -> Option<String> {
self.tsa_url.clone()
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use c2pa_macros::c2pa_test_async;
#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
use wasm_bindgen_test::wasm_bindgen_test;
use super::*;
use crate::{AsyncSigner, Signer, SigningAlg};
const ED25519_CERTS: &[u8] = include_bytes!("../tests/fixtures/certs/ed25519.pub");
const ED25519_PRIVATE_KEY: &[u8] = include_bytes!("../tests/fixtures/certs/ed25519.pem");
fn make_ed25519_signer() -> CallbackSigner {
let callback =
|_ctx: *const (), data: &[u8]| CallbackSigner::ed25519_sign(data, ED25519_PRIVATE_KEY);
CallbackSigner::new(callback, SigningAlg::Ed25519, ED25519_CERTS)
}
#[test]
fn new_sets_expected_defaults() {
let signer = make_ed25519_signer();
assert_eq!(signer.alg, SigningAlg::Ed25519);
assert_eq!(signer.tsa_url, None);
assert!(signer.context.is_null());
assert!(!signer.certs.is_empty());
assert!(signer.reserve_size >= 10000);
}
#[test]
fn set_tsa_url_stores_url() {
let signer = make_ed25519_signer().set_tsa_url("http://timestamp.example.com");
assert_eq!(
signer.tsa_url,
Some("http://timestamp.example.com".to_string())
);
assert_eq!(
Signer::time_authority_url(&signer),
Some("http://timestamp.example.com".to_string())
);
}
#[test]
fn set_context_stores_pointer() {
let value: u32 = 42;
let ptr = &value as *const u32 as *const ();
let signer = make_ed25519_signer().set_context(ptr);
assert_eq!(signer.context, ptr);
}
#[test]
fn ed25519_sign_produces_valid_signature() {
let data = b"test data to sign";
let sig = CallbackSigner::ed25519_sign(data, ED25519_PRIVATE_KEY).unwrap();
assert_eq!(sig.len(), 64); }
#[test]
fn signer_trait_sign_and_alg() {
let signer = make_ed25519_signer();
assert_eq!(Signer::alg(&signer), SigningAlg::Ed25519);
let sig = Signer::sign(&signer, b"hello").unwrap();
assert_eq!(sig.len(), 64);
}
#[test]
fn signer_trait_certs_parses_pem() {
let signer = make_ed25519_signer();
let certs = Signer::certs(&signer).unwrap();
assert!(!certs.is_empty());
}
#[test]
fn signer_trait_reserve_size_is_reasonable() {
let signer = make_ed25519_signer();
assert!(Signer::reserve_size(&signer) >= 10000);
}
#[test]
fn default_callback_returns_unsupported_type() {
let signer = CallbackSigner::default();
assert!(matches!(
Signer::sign(&signer, b"data"),
Err(crate::Error::UnsupportedType)
));
}
#[c2pa_test_async]
async fn async_signer_sign_produces_same_result() {
let signer = make_ed25519_signer();
let data = b"async test data".to_vec();
let sig = AsyncSigner::sign(&signer, data.clone()).await.unwrap();
assert_eq!(sig.len(), 64);
let sync_sig = Signer::sign(&signer, &data).unwrap();
assert_eq!(sig, sync_sig);
}
#[c2pa_test_async]
async fn async_signer_alg_and_certs_match_sync() {
let signer = make_ed25519_signer();
assert_eq!(AsyncSigner::alg(&signer), Signer::alg(&signer));
assert_eq!(
AsyncSigner::certs(&signer).unwrap(),
Signer::certs(&signer).unwrap()
);
assert_eq!(
AsyncSigner::reserve_size(&signer),
Signer::reserve_size(&signer)
);
assert_eq!(
AsyncSigner::time_authority_url(&signer),
Signer::time_authority_url(&signer)
);
}
}