use http::Request;
use serde::{Deserialize, Serialize};
use crate::{
create_signer,
crypto::raw_signature::RawSigner,
dynamic_assertion::DynamicAssertion,
http::{SyncGenericResolver, SyncHttpResolver},
identity::{builder::IdentityAssertionBuilder, x509::X509CredentialHolder},
settings::{Settings, SettingsValidate},
BoxedSigner, Error, Result, Signer, SigningAlg,
};
#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum SignerSettings {
Local {
alg: SigningAlg,
sign_cert: String,
private_key: String,
tsa_url: Option<String>,
referenced_assertions: Option<Vec<String>>,
roles: Option<Vec<String>>,
},
Remote {
url: String,
alg: SigningAlg,
sign_cert: String,
tsa_url: Option<String>,
referenced_assertions: Option<Vec<String>>,
roles: Option<Vec<String>>,
},
}
impl SignerSettings {
pub fn signer() -> Result<BoxedSigner> {
let signer_info = match Settings::get_thread_local_value::<Option<SignerSettings>>("signer")
{
Ok(Some(signer_info)) => signer_info,
#[cfg(test)]
_ => {
return Ok(crate::utils::test_signer::test_signer(SigningAlg::Ps256));
}
#[cfg(not(test))]
_ => {
return Err(Error::MissingSignerSettings);
}
};
let c2pa_signer = Self::c2pa_signer(signer_info)?;
if let Ok(Some(cawg_x509_settings)) =
Settings::get_thread_local_value::<Option<SignerSettings>>("cawg_x509_signer")
{
cawg_x509_settings.cawg_signer(c2pa_signer)
} else {
Ok(c2pa_signer)
}
}
pub fn c2pa_signer(self) -> Result<BoxedSigner> {
match self {
SignerSettings::Local {
alg,
sign_cert,
private_key,
tsa_url,
referenced_assertions: _,
roles: _,
} => {
create_signer::from_keys(sign_cert.as_bytes(), private_key.as_bytes(), alg, tsa_url)
}
SignerSettings::Remote {
url,
alg,
sign_cert,
tsa_url,
referenced_assertions: _,
roles: _,
} => Ok(Box::new(RemoteSigner {
url,
alg,
reserve_size: 10000 + sign_cert.len(),
certs: vec![sign_cert.into_bytes()],
tsa_url,
})),
}
}
pub fn cawg_signer(self, c2pa_signer: BoxedSigner) -> Result<BoxedSigner> {
match self {
SignerSettings::Local {
alg: cawg_alg,
sign_cert: cawg_sign_cert,
private_key: cawg_private_key,
tsa_url: cawg_tsa_url,
referenced_assertions: cawg_referenced_assertions,
roles: cawg_roles,
} => {
let cawg_dual_signer = CawgX509IdentitySigner {
c2pa_signer,
cawg_alg,
cawg_sign_cert,
cawg_private_key,
cawg_tsa_url,
cawg_referenced_assertions: cawg_referenced_assertions.unwrap_or_default(),
cawg_roles: cawg_roles.unwrap_or_default(),
};
Ok(Box::new(cawg_dual_signer))
}
SignerSettings::Remote {
url: _url,
alg: _alg,
sign_cert: _sign_cert,
tsa_url: _tsa_url,
referenced_assertions: _,
roles: _,
} => todo!("Remote CAWG X.509 signing not yet supported"),
}
}
}
impl SettingsValidate for SignerSettings {
fn validate(&self) -> Result<()> {
Ok(())
}
}
struct CawgX509IdentitySigner {
c2pa_signer: BoxedSigner,
cawg_alg: SigningAlg,
cawg_sign_cert: String,
cawg_private_key: String,
cawg_tsa_url: Option<String>,
cawg_referenced_assertions: Vec<String>,
cawg_roles: Vec<String>,
}
impl Signer for CawgX509IdentitySigner {
fn sign(&self, data: &[u8]) -> Result<Vec<u8>> {
Signer::sign(&self.c2pa_signer, data)
}
fn alg(&self) -> SigningAlg {
Signer::alg(&self.c2pa_signer)
}
fn certs(&self) -> Result<Vec<Vec<u8>>> {
self.c2pa_signer.certs()
}
fn reserve_size(&self) -> usize {
Signer::reserve_size(&self.c2pa_signer)
}
fn time_authority_url(&self) -> Option<String> {
self.c2pa_signer.time_authority_url()
}
fn timestamp_request_headers(&self) -> Option<Vec<(String, String)>> {
self.c2pa_signer.timestamp_request_headers()
}
fn timestamp_request_body(&self, message: &[u8]) -> Result<Vec<u8>> {
self.c2pa_signer.timestamp_request_body(message)
}
fn send_timestamp_request(&self, message: &[u8]) -> Option<Result<Vec<u8>>> {
self.c2pa_signer.send_timestamp_request(message)
}
fn ocsp_val(&self) -> Option<Vec<u8>> {
self.c2pa_signer.ocsp_val()
}
fn direct_cose_handling(&self) -> bool {
self.c2pa_signer.direct_cose_handling()
}
fn dynamic_assertions(&self) -> Vec<Box<dyn DynamicAssertion>> {
let Ok(raw_signer) = crate::crypto::raw_signature::signer_from_cert_chain_and_private_key(
self.cawg_sign_cert.as_bytes(),
self.cawg_private_key.as_bytes(),
self.cawg_alg,
self.cawg_tsa_url.clone(),
) else {
return vec![];
};
let x509_credential_holder = X509CredentialHolder::from_raw_signer(raw_signer);
let mut iab = IdentityAssertionBuilder::for_credential_holder(x509_credential_holder);
if !self.cawg_referenced_assertions.is_empty() {
let referenced_assertions: Vec<&str> = self
.cawg_referenced_assertions
.iter()
.map(|s| s.as_str())
.collect();
iab.add_referenced_assertions(&referenced_assertions);
}
if !self.cawg_roles.is_empty() {
let roles: Vec<&str> = self.cawg_roles.iter().map(|s| s.as_str()).collect();
iab.add_roles(&roles);
}
vec![Box::new(iab)]
}
fn raw_signer(&self) -> Option<Box<&dyn RawSigner>> {
self.c2pa_signer.raw_signer()
}
}
#[derive(Debug)]
pub(crate) struct RemoteSigner {
url: String,
alg: SigningAlg,
certs: Vec<Vec<u8>>,
reserve_size: usize,
tsa_url: Option<String>,
}
impl Signer for RemoteSigner {
fn sign(&self, data: &[u8]) -> Result<Vec<u8>> {
use std::io::Read;
let request = Request::post(&self.url).body(data.to_vec())?;
let response = SyncGenericResolver::with_redirects()
.unwrap_or_default()
.http_resolve(request)
.map_err(|_| Error::FailedToRemoteSign)?;
let mut bytes: Vec<u8> = Vec::with_capacity(self.reserve_size);
response
.into_body()
.take(self.reserve_size as u64)
.read_to_end(&mut bytes)?;
Ok(bytes)
}
fn alg(&self) -> SigningAlg {
self.alg
}
fn certs(&self) -> Result<Vec<Vec<u8>>> {
Ok(self.certs.clone())
}
fn reserve_size(&self) -> usize {
self.reserve_size
}
fn time_authority_url(&self) -> Option<String> {
self.tsa_url.clone()
}
}
#[cfg(test)]
pub mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::expect_used)]
use crate::{settings::Settings, utils::test_signer, SigningAlg};
#[cfg(not(target_arch = "wasm32"))]
fn remote_signer_mock_server<'a>(
server: &'a httpmock::MockServer,
signed_bytes: &[u8],
) -> httpmock::Mock<'a> {
server.mock(|when, then| {
when.method(httpmock::Method::POST);
then.status(200).body(signed_bytes);
})
}
#[test]
#[allow(deprecated)]
fn test_thread_local_signer() {
assert!(Settings::signer().is_ok());
}
#[test]
fn test_make_local_signer() {
let alg = SigningAlg::Ps384;
let (sign_cert, private_key) = test_signer::cert_chain_and_private_key_for_alg(alg);
let settings = Settings::new()
.with_toml(
&toml::toml! {
[signer.local]
alg = (alg.to_string())
sign_cert = (String::from_utf8(sign_cert.to_vec()).unwrap())
private_key = (String::from_utf8(private_key.to_vec()).unwrap())
}
.to_string(),
)
.unwrap();
let signer_settings = settings.signer.expect("signer settings should be present");
let signer = signer_settings.c2pa_signer().unwrap();
assert_eq!(signer.alg(), alg);
assert_eq!(signer.time_authority_url(), None);
assert!(signer.sign(&[1, 2, 3]).is_ok());
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn test_make_remote_signer() {
use httpmock::MockServer;
use crate::create_signer;
let alg = SigningAlg::Ps384;
let (sign_cert, private_key) = test_signer::cert_chain_and_private_key_for_alg(alg);
let signer = create_signer::from_keys(sign_cert, private_key, alg, None).unwrap();
let signed_bytes = signer.sign(&[1, 2, 3]).unwrap();
let server = MockServer::start();
let mock = remote_signer_mock_server(&server, &signed_bytes);
let settings = Settings::new()
.with_toml(
&toml::toml! {
[signer.remote]
url = (server.base_url())
alg = (alg.to_string())
sign_cert = (String::from_utf8(sign_cert.to_vec()).unwrap())
}
.to_string(),
)
.unwrap();
let signer_settings = settings.signer.expect("signer settings should be present");
let signer = signer_settings.c2pa_signer().unwrap();
assert_eq!(signer.alg(), alg);
assert_eq!(signer.time_authority_url(), None);
assert_eq!(signer.sign(&[1, 2, 3]).unwrap(), signed_bytes);
mock.assert();
}
}