diff --git a/Makefile b/Makefile index 7e2aaa96d..3479c3bc8 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ clippy: cargo clippy --features="file_io" --all-targets -- -D warnings test-local: - cargo test --features="file_io, fetch_remote_manifests, add_thumbnails" --all-targets + cargo test --features="file_io, fetch_remote_manifests, add_thumbnails, remote_signing" --all-targets test-wasm: cd sdk && wasm-pack test --node -- --no-default-features --features="rust_native_crypto, fetch_remote_manifests, http_reqwest" diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index a4eea2f41..a471b7a92 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -48,6 +48,10 @@ rust_native_crypto = [ "dep:rsa", "dep:spki", ] +remote_signing = [ + "openssl", + "dep:reqwest", +] # TODO: make our async tests always use the async API so we don't need ureq + reqwest # see https://github.com/contentauth/c2pa-rs/issues/1364 default_http = ["http_reqwest", "http_ureq", "http_wasi", "http_wstd"] @@ -290,7 +294,7 @@ mockall = "0.14.0" wasm-bindgen-test = "0.3.55" [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] -httpmock = "0.8.2" +httpmock = "0.8.3" tokio = { version = "1.44.2", features = ["full"] } criterion = { package = "codspeed-criterion-compat", version = "4.2.1" } diff --git a/sdk/src/context.rs b/sdk/src/context.rs index fc607df71..218707d7a 100644 --- a/sdk/src/context.rs +++ b/sdk/src/context.rs @@ -200,11 +200,12 @@ impl Default for Context { settings: Settings::default(), sync_resolver: SyncResolverState::Default(OnceLock::new()), async_resolver: AsyncResolverState::Default(OnceLock::new()), - #[cfg(test)] + // Testing cawg remote signer created from Context requires SignerState::FromSettings + #[cfg(all(test, not(feature = "remote_signing")))] signer: SignerState::Custom(crate::utils::test_signer::test_signer( crate::SigningAlg::Ps256, )), - #[cfg(not(test))] + #[cfg(any(not(test), feature = "remote_signing"))] signer: SignerState::FromSettings(OnceLock::new()), async_signer: AsyncSignerState::FromSettings(OnceLock::new()), } diff --git a/sdk/src/create_signer.rs b/sdk/src/create_signer.rs index a46b5561f..955404a87 100644 --- a/sdk/src/create_signer.rs +++ b/sdk/src/create_signer.rs @@ -18,6 +18,9 @@ #[cfg(feature = "file_io")] use std::path::Path; +#[cfg(feature = "remote_signing")] +use url::Url; + use crate::{ crypto::raw_signature::{signer_from_cert_chain_and_private_key, SigningAlg}, error::Result, @@ -69,3 +72,28 @@ pub fn from_files>( from_keys(&cert_chain, &private_key, alg, tsa_url) } + +/// Creates a [`Signer`](crate::Signer) instance using a signing certificate and +/// a remote signing service url +/// +/// The signature operation by the created signer is delegated to the remote service at `url`. +/// +/// # Arguments +/// +/// * `signcert` - Signing certificate chain in PEM format +/// * `url` - Remote signing service URL +/// * `alg` - Format for signing +/// * `tsa_url` - Optional URL for a timestamp authority +#[cfg(feature = "remote_signing")] +pub fn from_remote_url( + signcert: &[u8], + url: Url, + alg: SigningAlg, + tsa_url: Option, +) -> Result { + use crate::crypto::raw_signature::signer_from_cert_chain_and_url; + + Ok(Box::new(RawSignerWrapper(signer_from_cert_chain_and_url( + signcert, url, alg, tsa_url, + )?))) +} diff --git a/sdk/src/crypto/raw_signature/mod.rs b/sdk/src/crypto/raw_signature/mod.rs index c3e911cc2..fa2b0606a 100644 --- a/sdk/src/crypto/raw_signature/mod.rs +++ b/sdk/src/crypto/raw_signature/mod.rs @@ -29,6 +29,11 @@ pub use signer::{ async_signer_from_cert_chain_and_private_key, signer_from_cert_chain_and_private_key, AsyncRawSigner, RawSigner, RawSignerError, }; +#[cfg(all( + feature = "remote_signing", + not(all(feature = "rust_native_crypto", target_arch = "wasm32")) +))] +pub use signer::{async_signer_from_cert_chain_and_url, signer_from_cert_chain_and_url}; mod signing_alg; pub use signing_alg::{SigningAlg, UnknownAlgorithmError}; diff --git a/sdk/src/crypto/raw_signature/openssl/cert_chain.rs b/sdk/src/crypto/raw_signature/openssl/cert_chain.rs index 5d531fcfb..5b8052335 100644 --- a/sdk/src/crypto/raw_signature/openssl/cert_chain.rs +++ b/sdk/src/crypto/raw_signature/openssl/cert_chain.rs @@ -13,6 +13,28 @@ use openssl::x509::X509; +use crate::crypto::raw_signature::RawSignerError; + +/// Converts an X509 certificate stack to DER format. +/// +/// # Arguments +/// * `cert_chain` - A slice of X509 certificates to convert +/// +/// # Returns +/// A Result containing a Vec of DER-encoded certificates or an error +pub(crate) fn cert_chain_to_der(cert_chain: &[X509]) -> Result>, RawSignerError> { + cert_chain + .iter() + .map(|cert| { + cert.to_der().map_err(|_| { + RawSignerError::CryptoLibraryError( + "could not encode certificate to DER".to_string(), + ) + }) + }) + .collect::, RawSignerError>>() +} + // Verify the certificate chain order. // // Return `true` if each cert in the chain can be verified as issued by the next @@ -44,3 +66,57 @@ pub(crate) fn check_chain_order(certs: &[X509]) -> bool { true } + +#[test] +#[cfg_attr( + all(target_arch = "wasm32", not(target_os = "wasi")), + wasm_bindgen_test +)] +fn test_cert_chain_to_der() -> Result<(), RawSignerError> { + use openssl::x509::X509; + + use crate::crypto::raw_signature::{openssl::OpenSslMutex, RawSignerError}; + + let _openssl = + OpenSslMutex::acquire().map_err(|e| RawSignerError::CryptoLibraryError(e.to_string()))?; + + let cert_chain_pem = + include_bytes!("../../../../tests/fixtures/crypto/raw_signature/es256.pub"); + + let cert_stack = + X509::stack_from_pem(cert_chain_pem).map_err(|e| RawSignerError::IoError(e.to_string()))?; + + assert!( + !cert_stack.is_empty(), + "Certificate stack should not be empty" + ); + assert_eq!( + cert_stack.len(), + 2, + "Certificate stack should have two certificates" + ); + + // Test the cert_chain_to_der function + let der_certs = cert_chain_to_der(&cert_stack)?; + + assert!( + !der_certs.is_empty(), + "DER certificate list should not be empty" + ); + assert_eq!( + der_certs.len(), + 2, + "DER certificate list should have two certificates" + ); + + // Verify each DER certificate is not empty + for (i, der_cert) in der_certs.iter().enumerate() { + assert!( + !der_cert.is_empty(), + "DER certificate [{}] should not be empty", + i + ); + } + + Ok(()) +} diff --git a/sdk/src/crypto/raw_signature/openssl/signers/ecdsa_signer.rs b/sdk/src/crypto/raw_signature/openssl/signers/ecdsa_signer.rs index d797e805a..daab00cf5 100644 --- a/sdk/src/crypto/raw_signature/openssl/signers/ecdsa_signer.rs +++ b/sdk/src/crypto/raw_signature/openssl/signers/ecdsa_signer.rs @@ -22,7 +22,10 @@ use openssl::{ use crate::crypto::{ ec_utils::{der_to_p1363, ec_curve_from_private_key_der}, raw_signature::{ - openssl::{cert_chain::check_chain_order, OpenSslMutex}, + openssl::{ + cert_chain::{cert_chain_to_der, check_chain_order}, + OpenSslMutex, + }, RawSigner, RawSignerError, SigningAlg, }, time_stamp::TimeStampProvider, @@ -77,16 +80,7 @@ impl EcdsaSigner { } // certs in DER format - let cert_chain = cert_chain - .iter() - .map(|cert| { - cert.to_der().map_err(|_| { - RawSignerError::CryptoLibraryError( - "could not encode certificate to DER".to_string(), - ) - }) - }) - .collect::, RawSignerError>>()?; + let cert_chain = cert_chain_to_der(&cert_chain)?; // get the actual length of the certificate chain let cert_chain_len = cert_chain.iter().fold(0usize, |sum, c| sum + c.len()); @@ -141,15 +135,15 @@ impl RawSigner for EcdsaSigner { } } - fn reserve_size(&self) -> usize { - 1024 + self.cert_chain_len + self.time_stamp_size - } - fn cert_chain(&self) -> Result>, RawSignerError> { let _openssl = OpenSslMutex::acquire()?; Ok(self.cert_chain.clone()) } + + fn reserve_size(&self) -> usize { + 1024 + self.cert_chain_len + self.time_stamp_size + } } impl TimeStampProvider for EcdsaSigner { diff --git a/sdk/src/crypto/raw_signature/openssl/signers/ed25519_signer.rs b/sdk/src/crypto/raw_signature/openssl/signers/ed25519_signer.rs index 43e26bf5f..6bdc16cb2 100644 --- a/sdk/src/crypto/raw_signature/openssl/signers/ed25519_signer.rs +++ b/sdk/src/crypto/raw_signature/openssl/signers/ed25519_signer.rs @@ -19,7 +19,10 @@ use openssl::{ use crate::crypto::{ raw_signature::{ - openssl::{cert_chain::check_chain_order, OpenSslMutex}, + openssl::{ + cert_chain::{cert_chain_to_der, check_chain_order}, + OpenSslMutex, + }, RawSigner, RawSignerError, SigningAlg, }, time_stamp::TimeStampProvider, @@ -54,16 +57,7 @@ impl Ed25519Signer { } // certs in DER format - let cert_chain = cert_chain - .iter() - .map(|cert| { - cert.to_der().map_err(|_| { - RawSignerError::CryptoLibraryError( - "could not encode certificate to DER".to_string(), - ) - }) - }) - .collect::, RawSignerError>>()?; + let cert_chain = cert_chain_to_der(&cert_chain)?; // get the actual length of the certificate chain let cert_chain_len = cert_chain.iter().fold(0usize, |sum, c| sum + c.len()); @@ -94,15 +88,15 @@ impl RawSigner for Ed25519Signer { SigningAlg::Ed25519 } - fn reserve_size(&self) -> usize { - 1024 + self.cert_chain_len + self.time_stamp_size - } - fn cert_chain(&self) -> Result>, RawSignerError> { let _openssl = OpenSslMutex::acquire()?; Ok(self.cert_chain.clone()) } + + fn reserve_size(&self) -> usize { + 1024 + self.cert_chain_len + self.time_stamp_size + } } impl TimeStampProvider for Ed25519Signer { diff --git a/sdk/src/crypto/raw_signature/openssl/signers/mod.rs b/sdk/src/crypto/raw_signature/openssl/signers/mod.rs index 4d4bfb4ba..e64e7ea04 100644 --- a/sdk/src/crypto/raw_signature/openssl/signers/mod.rs +++ b/sdk/src/crypto/raw_signature/openssl/signers/mod.rs @@ -20,6 +20,9 @@ mod ecdsa_signer; mod ed25519_signer; mod rsa_signer; +#[cfg(feature = "remote_signing")] +mod remote_signer; + /// Return a built-in [`RawSigner`] instance using the provided signing /// certificate and private key. /// @@ -62,3 +65,20 @@ pub(crate) fn signer_from_cert_chain_and_private_key( )), } } + +#[cfg(feature = "remote_signing")] +pub(crate) fn signer_from_cert_chain_and_url( + cert_chain: &[u8], + url: url::Url, + alg: SigningAlg, + time_stamp_service_url: Option, +) -> Result, RawSignerError> { + Ok(Box::new( + remote_signer::RemoteRawSigner::from_cert_chain_and_url( + cert_chain, + url, + alg, + time_stamp_service_url, + )?, + )) +} diff --git a/sdk/src/crypto/raw_signature/openssl/signers/remote_signer.rs b/sdk/src/crypto/raw_signature/openssl/signers/remote_signer.rs new file mode 100644 index 000000000..89fe46d03 --- /dev/null +++ b/sdk/src/crypto/raw_signature/openssl/signers/remote_signer.rs @@ -0,0 +1,118 @@ +use http::Request; +use openssl::x509::X509; +use url::Url; + +use crate::{ + crypto::{ + raw_signature::{ + openssl::{ + cert_chain::{cert_chain_to_der, check_chain_order}, + OpenSslMutex, + }, + RawSigner, RawSignerError, + }, + time_stamp::TimeStampProvider, + }, + http::{SyncGenericResolver, SyncHttpResolver}, + Error, SigningAlg, +}; + +// ============================================================================ +// Remote Raw Signer for CAWG Identity +// ============================================================================ + +/// A raw signer that delegates signing to a remote HTTP service. +/// Used for CAWG identity signing when configured in remote mode. +pub struct RemoteRawSigner { + /// The URL endpoint for the signing service + url: Url, + + /// Parsed certificate chain in DER format + cert_chain: Vec>, + + /// Certificate chain byte size + cert_chain_len: usize, + /// The signing algorithm + alg: SigningAlg, + + /// Optional TSA URL + time_stamp_service_url: Option, + /// Timestamp size + time_stamp_size: usize, +} + +impl RemoteRawSigner { + pub fn from_cert_chain_and_url( + cert_chain: &[u8], + url: Url, + alg: SigningAlg, + time_stamp_service_url: Option, + ) -> Result { + let _openssl = OpenSslMutex::acquire()?; + + let cert_chain = X509::stack_from_pem(cert_chain)?; + + if !check_chain_order(&cert_chain) { + return Err(RawSignerError::InvalidSigningCredentials( + "certificate chain in incorrect order".to_string(), + )); + } + + let cert_chain = cert_chain_to_der(&cert_chain)?; + + let cert_chain_len = cert_chain.iter().fold(0usize, |sum, c| sum + c.len()); + + Ok(Self { + url, + cert_chain, + cert_chain_len, + alg, + time_stamp_service_url, + time_stamp_size: 10_000, + // TODO: Call out to time stamp service to get actual time stamp and use that size? + }) + } +} + +impl RawSigner for RemoteRawSigner { + fn sign(&self, data: &[u8]) -> Result, RawSignerError> { + use std::io::Read; + + let request = Request::post(self.url.as_str()) + .body(data.to_vec()) + .map_err(|_| Error::FailedToRemoteSign)?; + + let response = SyncGenericResolver::new() + .http_resolve(request) + .map_err(|_| Error::FailedToRemoteSign)?; + + let reserve_size = self.reserve_size(); + let mut bytes: Vec = Vec::with_capacity(reserve_size); + response + .into_body() + .take(reserve_size as u64) + .read_to_end(&mut bytes) + .map_err(|_| Error::FailedToRemoteSign)?; + + Ok(bytes) + } + + fn alg(&self) -> SigningAlg { + self.alg + } + + fn cert_chain(&self) -> Result>, RawSignerError> { + let _openssl = OpenSslMutex::acquire()?; + Ok(self.cert_chain.clone()) + } + + fn reserve_size(&self) -> usize { + 10_000 + self.cert_chain_len + self.time_stamp_size + } +} + +impl TimeStampProvider for RemoteRawSigner { + fn time_stamp_service_url(&self) -> Option { + self.time_stamp_service_url.clone() + } +} diff --git a/sdk/src/crypto/raw_signature/openssl/signers/rsa_signer.rs b/sdk/src/crypto/raw_signature/openssl/signers/rsa_signer.rs index b9227f613..feace12c8 100644 --- a/sdk/src/crypto/raw_signature/openssl/signers/rsa_signer.rs +++ b/sdk/src/crypto/raw_signature/openssl/signers/rsa_signer.rs @@ -21,7 +21,10 @@ use openssl::{ use crate::crypto::{ raw_signature::{ - openssl::{cert_chain::check_chain_order, OpenSslMutex}, + openssl::{ + cert_chain::{cert_chain_to_der, check_chain_order}, + OpenSslMutex, + }, RawSigner, RawSignerError, SigningAlg, }, time_stamp::TimeStampProvider, @@ -65,16 +68,7 @@ impl RsaSigner { } // certs in DER format - let cert_chain = cert_chain - .iter() - .map(|cert| { - cert.to_der().map_err(|_| { - RawSignerError::CryptoLibraryError( - "could not encode certificate to DER".to_string(), - ) - }) - }) - .collect::, RawSignerError>>()?; + let cert_chain = cert_chain_to_der(&cert_chain)?; // get the actual length of the certificate chain let cert_chain_len = cert_chain.iter().fold(0usize, |sum, c| sum + c.len()); @@ -169,8 +163,12 @@ impl RawSigner for RsaSigner { Ok(signer.sign_oneshot_to_vec(data)?) } - fn reserve_size(&self) -> usize { - 1024 + self.cert_chain_len + self.time_stamp_size + fn alg(&self) -> SigningAlg { + match self.alg { + RsaSigningAlg::Ps256 => SigningAlg::Ps256, + RsaSigningAlg::Ps384 => SigningAlg::Ps384, + RsaSigningAlg::Ps512 => SigningAlg::Ps512, + } } fn cert_chain(&self) -> Result>, RawSignerError> { @@ -179,12 +177,8 @@ impl RawSigner for RsaSigner { Ok(self.cert_chain.clone()) } - fn alg(&self) -> SigningAlg { - match self.alg { - RsaSigningAlg::Ps256 => SigningAlg::Ps256, - RsaSigningAlg::Ps384 => SigningAlg::Ps384, - RsaSigningAlg::Ps512 => SigningAlg::Ps512, - } + fn reserve_size(&self) -> usize { + 1024 + self.cert_chain_len + self.time_stamp_size } } diff --git a/sdk/src/crypto/raw_signature/signer.rs b/sdk/src/crypto/raw_signature/signer.rs index c4c665a12..10299be82 100644 --- a/sdk/src/crypto/raw_signature/signer.rs +++ b/sdk/src/crypto/raw_signature/signer.rs @@ -13,6 +13,7 @@ use async_trait::async_trait; use thiserror::Error; +use url::Url; use crate::{ crypto::{ @@ -202,6 +203,42 @@ pub fn signer_from_cert_chain_and_private_key( ))) } +/// Return a built-in [`RawSigner`] instance using the provided signing +/// certificate and url to a remote signing service. +/// +/// Which signers are available may vary depending on the platform and which +/// crate features were enabled. If the desired signing algorithm is +/// unavailable, will respond with `Err(RawSignerError::InternalError)`. +/// +/// May return an `Err` response if the certificate chain or url are invalid. +#[allow(unused)] +pub fn signer_from_cert_chain_and_url( + cert_chain: &[u8], + url: Url, + alg: SigningAlg, + time_stamp_service_url: Option, +) -> Result, RawSignerError> { + let cert_chain = fix_json_pem(cert_chain); + + #[cfg(all( + feature = "openssl", + feature = "remote_signing", + not(all(feature = "rust_native_crypto", target_arch = "wasm32")) + ))] + { + return crate::crypto::raw_signature::openssl::signers::signer_from_cert_chain_and_url( + &cert_chain, + url, + alg, + time_stamp_service_url, + ); + } + + Err(RawSignerError::InternalError(format!( + "unsupported remote signing for algorithm: {alg}" + ))) +} + /// Return a built-in [`AsyncRawSigner`] instance using the provided signing /// certificate and private key. /// @@ -227,6 +264,26 @@ pub fn async_signer_from_cert_chain_and_private_key( Ok(Box::new(AsyncRawSignerWrapper(sync_signer))) } +/// Return a built-in [`AsyncRawSigner`] instance using the provided signing +/// certificate and url to a remote signing service. +/// +/// Which signers are available may vary depending on the platform and which +/// crate features were enabled. If the desired signing algorithm is +/// unavailable, it will respond with `Err(RawSignerError::InternalError)`. +/// +/// May return an `Err` response if the certificate chain or url are invalid. +#[allow(unused)] +pub fn async_signer_from_cert_chain_and_url( + cert_chain: &[u8], + url: Url, + alg: SigningAlg, + time_stamp_service_url: Option, +) -> Result, RawSignerError> { + let sync_signer = signer_from_cert_chain_and_url(cert_chain, url, alg, time_stamp_service_url)?; + + Ok(Box::new(AsyncRawSignerWrapper(sync_signer))) +} + struct AsyncRawSignerWrapper(Box); #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] diff --git a/sdk/src/crypto/raw_signature/tests/async_signers.rs b/sdk/src/crypto/raw_signature/tests/async_signers.rs index 30b8b1504..58401f8df 100644 --- a/sdk/src/crypto/raw_signature/tests/async_signers.rs +++ b/sdk/src/crypto/raw_signature/tests/async_signers.rs @@ -17,8 +17,13 @@ use c2pa_macros::c2pa_test_async; #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] use wasm_bindgen_test::wasm_bindgen_test; -use crate::crypto::raw_signature::{ - async_signer_from_cert_chain_and_private_key, validator_for_signing_alg, SigningAlg, +use crate::{ + create_signer, + crypto::raw_signature::{ + async_signer_from_cert_chain_and_private_key, signer::async_signer_from_cert_chain_and_url, + validator_for_signing_alg, SigningAlg, + }, + utils::test_signer, }; #[c2pa_test_async] @@ -196,3 +201,38 @@ async fn ps512() { let validator = validator_for_signing_alg(SigningAlg::Ps512).unwrap(); validator.validate(&signature, data, pub_key).unwrap(); } + +#[cfg(feature = "remote_signing")] +#[c2pa_test_async] +async fn remote_signing() { + use httpmock::MockServer; + + use crate::utils::test_remote_signer; + + let alg = SigningAlg::Es256; + let cert_chain = include_bytes!("../../../../tests/fixtures/crypto/raw_signature/es256.pub"); + let private_key = include_bytes!("../../../../tests/fixtures/crypto/raw_signature/es256.priv"); + + let data = b"some sample content to sign"; + let mock_signer = create_signer::from_keys(cert_chain, private_key, alg, None).unwrap(); + let signed_bytes = mock_signer.sign(data).unwrap(); + + let server = MockServer::start(); + let mock = test_remote_signer::remote_signer_mock_server(&server, &signed_bytes); + + let url = url::Url::parse(&server.base_url()).unwrap(); + + let signer = async_signer_from_cert_chain_and_url(cert_chain, url, alg, None).unwrap(); + + let signature = signer.sign(data.to_vec()).await.unwrap(); + + println!("signature len = {}", signature.len()); + assert!(signature.len() <= signer.reserve_size()); + + let pub_key = include_bytes!("../../../../tests/fixtures/crypto/raw_signature/es256.pub_key"); + + let validator = validator_for_signing_alg(alg).unwrap(); + validator.validate(&signature, data, pub_key).unwrap(); + + mock.assert(); +} diff --git a/sdk/src/crypto/raw_signature/tests/signers.rs b/sdk/src/crypto/raw_signature/tests/signers.rs index 906e25904..95670e43d 100644 --- a/sdk/src/crypto/raw_signature/tests/signers.rs +++ b/sdk/src/crypto/raw_signature/tests/signers.rs @@ -193,3 +193,44 @@ fn ps512() { let validator = validator_for_signing_alg(SigningAlg::Ps512).unwrap(); validator.validate(&signature, data, pub_key).unwrap(); } + +#[test] +#[cfg(all( + feature = "remote_signing", + not(all(feature = "rust_native_crypto", target_arch = "wasm32")) +))] +fn remote_signing() { + use httpmock::MockServer; + + use crate::{ + create_signer, crypto::raw_signature::openssl::signers::signer_from_cert_chain_and_url, + utils::test_remote_signer, Signer, + }; + + let alg = SigningAlg::Es256; + let cert_chain = include_bytes!("../../../../tests/fixtures/crypto/raw_signature/es256.pub"); + let private_key = include_bytes!("../../../../tests/fixtures/crypto/raw_signature/es256.priv"); + + let data = b"some sample content to sign"; + let mock_signer = create_signer::from_keys(cert_chain, private_key, alg, None).unwrap(); + let signed_bytes = mock_signer.sign(data).unwrap(); + + let server = MockServer::start(); + let mock = test_remote_signer::remote_signer_mock_server(&server, &signed_bytes); + + let url = url::Url::parse(&server.base_url()).unwrap(); + + let signer = signer_from_cert_chain_and_url(cert_chain, url, alg, None).unwrap(); + + let signature = signer.sign(data).unwrap(); + + println!("signature len = {}", signature.len()); + assert!(signature.len() <= signer.reserve_size()); + + let pub_key = include_bytes!("../../../../tests/fixtures/crypto/raw_signature/es256.pub_key"); + + let validator = validator_for_signing_alg(alg).unwrap(); + validator.validate(&signature, data, pub_key).unwrap(); + + mock.assert(); +} diff --git a/sdk/src/error.rs b/sdk/src/error.rs index 425c0b683..e74e04df8 100644 --- a/sdk/src/error.rs +++ b/sdk/src/error.rs @@ -225,6 +225,12 @@ pub enum Error { #[error("failed to fetch the remote settings")] FailedToFetchSettings, + #[error(transparent)] + InvalidRemoteUrl(#[from] url::ParseError), + + #[error("remote signing feature is not enabled")] + RemoteSigningNotEnabled, + #[error("failed to remotely sign data")] FailedToRemoteSign, diff --git a/sdk/src/identity/x509/x509_credential_holder.rs b/sdk/src/identity/x509/x509_credential_holder.rs index ccc609de0..129dbbbbd 100644 --- a/sdk/src/identity/x509/x509_credential_holder.rs +++ b/sdk/src/identity/x509/x509_credential_holder.rs @@ -37,7 +37,7 @@ impl X509CredentialHolder { /// The [`RawSigner`] implementation actually holds (or has access to) /// the relevant certificates and private key material. /// - /// [`RawSigner`]: crate::crypto::raw_signature::RawSigner + /// [`RawSigner`]: RawSigner pub fn from_raw_signer(signer: Box) -> Self { Self(signer) } @@ -179,4 +179,104 @@ mod tests { // No need to restore settings - we never modified global state! } + + #[cfg(feature = "remote_signing")] + #[c2pa_test_async] + async fn remote_signing_case() { + use httpmock::MockServer; + + use crate::{create_signer, settings, utils::test_remote_signer}; + + // Create a context with decode_identity_assertions disabled + let settings = settings::Settings::default() + .with_value("core.decode_identity_assertions", false) + .unwrap(); + let context = crate::Context::new() + .with_settings(settings) + .unwrap() + .into_shared(); + + let format = "image/jpeg"; + let mut source = Cursor::new(TEST_IMAGE); + let mut dest = Cursor::new(Vec::new()); + + // Use the context when creating the Builder + let mut builder = Builder::from_shared_context(&context) + .with_definition(manifest_json()) + .unwrap(); + builder + .add_ingredient_from_stream(parent_json(), format, &mut source) + .unwrap(); + + builder + .add_resource("thumbnail.jpg", Cursor::new(TEST_THUMBNAIL)) + .unwrap(); + + let mut c2pa_signer = IdentityAssertionSigner::from_test_credentials(SigningAlg::Ps256); + + let cawg_alg = SigningAlg::Ed25519; + let (cawg_cert_chain, cawg_private_key) = cert_chain_and_private_key_for_alg(cawg_alg); + + let local_cawg_raw_signer = + create_signer::from_keys(&cawg_cert_chain, &cawg_private_key, cawg_alg, None).unwrap(); + + let cawg_server = MockServer::start(); + let _cawg_mock = test_remote_signer::remote_signer_respond_with_signature( + &cawg_server, + local_cawg_raw_signer, + ); + + let cawg_remote_signer = raw_signature::signer_from_cert_chain_and_url( + &cawg_cert_chain, + url::Url::parse(cawg_server.base_url().as_str()).expect("Invalid URL"), + cawg_alg, + None, + ) + .expect("Error creating cawg remote signer"); + + let x509_holder = X509CredentialHolder::from_raw_signer(cawg_remote_signer); + let iab = IdentityAssertionBuilder::for_credential_holder(x509_holder); + c2pa_signer.add_identity_assertion(iab); + + builder + .sign(&c2pa_signer, format, &mut source, &mut dest) + .unwrap(); + + // Assert mocked remote signer was called + _cawg_mock.assert(); + + // Read back the Manifest that was generated using the same context + dest.rewind().unwrap(); + + let manifest_store = Reader::from_shared_context(&context) + .with_stream(format, &mut dest) + .unwrap(); + assert_eq!(manifest_store.validation_status(), None); + + let manifest = manifest_store.active_manifest().unwrap(); + let mut st = StatusTracker::default(); + let mut ia_iter = IdentityAssertion::from_manifest(manifest, &mut st); + + // Should find exactly one identity assertion. + let ia = ia_iter.next().unwrap().unwrap(); + assert!(ia_iter.next().is_none()); + drop(ia_iter); + + // And that identity assertion should be valid for this manifest. + let x509_verifier = X509SignatureVerifier { + cose_verifier: Verifier::IgnoreProfileAndTrustPolicy, + }; + + let sig_info = ia + .validate(manifest, &mut st, &x509_verifier) + .await + .unwrap(); + + let cert_info = &sig_info.cert_info; + assert_eq!(cert_info.alg.unwrap(), cawg_alg); + assert_eq!( + cert_info.issuer_org.as_ref().unwrap(), + "C2PA Test Signing Cert" + ); + } } diff --git a/sdk/src/settings/signer.rs b/sdk/src/settings/signer.rs index 6e8331317..c729e7878 100644 --- a/sdk/src/settings/signer.rs +++ b/sdk/src/settings/signer.rs @@ -11,19 +11,46 @@ // specific language governing permissions and limitations under // each license. -use http::Request; use serde::{Deserialize, Serialize}; +use url::Url; use crate::{ create_signer, - crypto::raw_signature::RawSigner, + crypto::raw_signature::{signer_from_cert_chain_and_private_key, RawSigner}, dynamic_assertion::DynamicAssertion, - http::{SyncGenericResolver, SyncHttpResolver}, identity::{builder::IdentityAssertionBuilder, x509::X509CredentialHolder}, settings::{Settings, SettingsValidate}, BoxedSigner, Error, Result, Signer, SigningAlg, }; +/// Enum representing the CAWG X.509 signing mode: local (with private key) or remote (via URL). +/// +/// This enum encapsulates the credentials needed for either local or remote CAWG X.509 identity signing. +/// It is used internally by [`CawgX509IdentitySigner`] to determine which signing path to use when +/// creating dynamic assertions. +#[derive(Clone, Debug)] +enum CawgSigningMode { + /// Local signing mode: credentials are stored locally on the signer instance. + Local { + /// Signing certificate chain in PEM format. + sign_cert: String, + /// Private key in PEM format. + private_key: String, + /// Optional time stamp authority URL. + tsa_url: Option, + }, + /// Remote signing mode: credentials are accessed via a remote signing service. + #[allow(unused)] + Remote { + /// Remote signing service URL. + url: Url, + /// Signing certificate chain in PEM format. + sign_cert: String, + /// Optional time stamp authority URL. + tsa_url: Option, + }, +} + /// Settings for configuring a local or remote [`Signer`]. /// /// A [`Signer`] can be obtained by calling the [`signer()`] function. @@ -111,6 +138,7 @@ impl SignerSettings { } => { create_signer::from_keys(sign_cert.as_bytes(), private_key.as_bytes(), alg, tsa_url) } + #[cfg_attr(not(feature = "remote_signing"), allow(unused))] SignerSettings::Remote { url, alg, @@ -118,17 +146,29 @@ impl SignerSettings { 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, - })), + } => { + #[cfg(feature = "remote_signing")] + { + match Url::parse(&url) { + Ok(url) => { + create_signer::from_remote_url(sign_cert.as_bytes(), url, alg, tsa_url) + } + Err(e) => Err(Error::InvalidRemoteUrl(e)), + } + } + + #[cfg(not(feature = "remote_signing"))] + { + Err(Error::RemoteSigningNotEnabled) + } + } } } /// Returns a CAWG X.509 identity signer that wraps the provided c2pa signer. + /// + /// Supports both local signing (with private key stored locally) and remote signing + /// (delegated to a remote signing service via URL). pub fn cawg_signer(self, c2pa_signer: BoxedSigner) -> Result { match self { SignerSettings::Local { @@ -139,12 +179,16 @@ impl SignerSettings { referenced_assertions: cawg_referenced_assertions, roles: cawg_roles, } => { + let signing_mode = CawgSigningMode::Local { + sign_cert: cawg_sign_cert, + private_key: cawg_private_key, + tsa_url: cawg_tsa_url, + }; + let cawg_dual_signer = CawgX509IdentitySigner { c2pa_signer, cawg_alg, - cawg_sign_cert, - cawg_private_key, - cawg_tsa_url, + signing_mode, cawg_referenced_assertions: cawg_referenced_assertions.unwrap_or_default(), cawg_roles: cawg_roles.unwrap_or_default(), }; @@ -152,14 +196,39 @@ impl SignerSettings { Ok(Box::new(cawg_dual_signer)) } + #[cfg_attr(not(feature = "remote_signing"), allow(unused))] 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"), + url, + alg: cawg_alg, + sign_cert: cawg_sign_cert, + tsa_url: cawg_tsa_url, + referenced_assertions: cawg_referenced_assertions, + roles: cawg_roles, + } => { + #[cfg(feature = "remote_signing")] + { + let url = Url::parse(&url).map_err(|e| Error::InvalidRemoteUrl(e))?; + let signing_mode = CawgSigningMode::Remote { + url, + sign_cert: cawg_sign_cert, + tsa_url: cawg_tsa_url, + }; + + let cawg_dual_signer = CawgX509IdentitySigner { + c2pa_signer, + cawg_alg, + signing_mode, + cawg_referenced_assertions: cawg_referenced_assertions.unwrap_or_default(), + cawg_roles: cawg_roles.unwrap_or_default(), + }; + + Ok(Box::new(cawg_dual_signer)) + } + #[cfg(not(feature = "remote_signing"))] + { + Err(Error::RemoteSigningNotEnabled) + } + } } } } @@ -170,17 +239,18 @@ impl SettingsValidate for SignerSettings { } } +/// Signing mode (local or remote) with all required credentials. +/// +/// We store the signing mode enum here because we can't clone or transfer ownership +/// of an `X509CredentialHolder` inside the `dynamic_assertions()` callback. +/// Instead, we store the raw credentials and create the `X509CredentialHolder` +/// on-demand in `dynamic_assertions()` when needed. struct CawgX509IdentitySigner { c2pa_signer: BoxedSigner, cawg_alg: SigningAlg, - cawg_sign_cert: String, - cawg_private_key: String, - cawg_tsa_url: Option, + signing_mode: CawgSigningMode, cawg_referenced_assertions: Vec, cawg_roles: Vec, - // NOTE: The CAWG signing settings are stored here because - // we can't clone or transfer ownership of an `X509CredentialHolder` - // inside the dynamic_assertions callback. } impl Signer for CawgX509IdentitySigner { @@ -225,12 +295,40 @@ impl Signer for CawgX509IdentitySigner { } fn dynamic_assertions(&self) -> Vec> { - 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 { + let raw_signer = match &self.signing_mode { + CawgSigningMode::Local { + sign_cert, + private_key, + tsa_url, + } => signer_from_cert_chain_and_private_key( + sign_cert.as_bytes(), + private_key.as_bytes(), + self.cawg_alg, + tsa_url.clone(), + ), + #[cfg_attr(not(feature = "remote_signing"), allow(unused))] + CawgSigningMode::Remote { + url, + sign_cert, + tsa_url, + } => { + #[cfg(feature = "remote_signing")] + { + crate::crypto::raw_signature::signer_from_cert_chain_and_url( + sign_cert.as_bytes(), + url.clone(), + self.cawg_alg, + tsa_url.clone(), + ) + } + #[cfg(not(feature = "remote_signing"))] + { + return vec![]; + } + } + }; + + let Ok(raw_signer) = raw_signer else { // dynamic_assertions() API doesn't let us fail. // signer_from_cert_chain_and_private_key rarely fails, // so when it does, we do so silently. @@ -265,65 +363,11 @@ impl Signer for CawgX509IdentitySigner { } } -#[derive(Debug)] -pub(crate) struct RemoteSigner { - url: String, - alg: SigningAlg, - certs: Vec>, - reserve_size: usize, - tsa_url: Option, -} - -impl Signer for RemoteSigner { - fn sign(&self, data: &[u8]) -> Result> { - use std::io::Read; - - let request = Request::post(&self.url).body(data.to_vec())?; - let response = SyncGenericResolver::new() - .http_resolve(request) - .map_err(|_| Error::FailedToRemoteSign)?; - let mut bytes: Vec = 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>> { - Ok(self.certs.clone()) - } - - fn reserve_size(&self) -> usize { - self.reserve_size - } - - fn time_authority_url(&self) -> Option { - self.tsa_url.clone() - } -} - #[cfg(test)] pub mod tests { #![allow(clippy::unwrap_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] fn test_make_test_signer() { // Makes a default test signer. @@ -355,16 +399,16 @@ pub mod tests { assert!(signer.sign(&[1, 2, 3]).is_ok()); } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(feature = "remote_signing", not(target_arch = "wasm32")))] #[test] fn test_make_remote_signer() { - use httpmock::MockServer; - - use crate::create_signer; - #[cfg(target_os = "wasi")] Settings::reset().unwrap(); + use httpmock::MockServer; + + use crate::{create_signer, utils::test_remote_signer}; + let alg = SigningAlg::Ps384; let (sign_cert, private_key) = test_signer::cert_chain_and_private_key_for_alg(alg); @@ -372,7 +416,7 @@ pub mod tests { let signed_bytes = signer.sign(&[1, 2, 3]).unwrap(); let server = MockServer::start(); - let mock = remote_signer_mock_server(&server, &signed_bytes); + let mock = test_remote_signer::remote_signer_mock_server(&server, &signed_bytes); Settings::from_toml( &toml::toml! { @@ -392,4 +436,206 @@ pub mod tests { mock.assert(); } + + #[cfg(all(not(feature = "remote_signing"), not(target_arch = "wasm32")))] + #[test] + fn test_make_remote_signer_disabled() { + #[cfg(target_os = "wasi")] + Settings::reset().unwrap(); + + let alg = SigningAlg::Ps384; + let (sign_cert, _) = test_signer::cert_chain_and_private_key_for_alg(alg); + + Settings::from_toml( + &toml::toml! { + [signer.remote] + url = "http:://dummy_url:2026/" + alg = (alg.to_string()) + sign_cert = (String::from_utf8(sign_cert.to_vec()).unwrap()) + } + .to_string(), + ) + .unwrap(); + + let signer = Settings::signer(); + assert!(signer.is_err()); + } + + #[cfg(not(target_arch = "wasm32"))] + #[test] + fn test_make_local_cawg_signer() { + #[cfg(target_os = "wasi")] + Settings::reset().unwrap(); + + let c2pa_alg = SigningAlg::Ps384; + let cawg_alg = SigningAlg::Es384; + let (c2pa_sign_cert, c2pa_private_key) = + test_signer::cert_chain_and_private_key_for_alg(c2pa_alg); + let (cawg_cert, cawg_private_key) = + test_signer::cert_chain_and_private_key_for_alg(cawg_alg); + + Settings::from_toml( + &toml::toml! { + [signer.local] + alg = (c2pa_alg.to_string()) + sign_cert = (String::from_utf8(c2pa_sign_cert.to_vec()).unwrap()) + private_key = (String::from_utf8(c2pa_private_key.to_vec()).unwrap()) + + [cawg_x509_signer.local] + alg = (cawg_alg.to_string()) + sign_cert = (String::from_utf8(cawg_cert.to_vec()).unwrap()) + private_key = (String::from_utf8(cawg_private_key.to_vec()).unwrap()) + } + .to_string(), + ) + .unwrap(); + + let signer = Settings::signer().unwrap(); + assert_eq!( + signer.alg(), + c2pa_alg, + "Should have the same alg as the CAWG signer" + ); + + assert!( + !signer.dynamic_assertions().is_empty(), + "Should have dynamic assertions" + ); + assert!(signer.sign(&[1, 2, 3]).is_ok()); + } + + #[cfg(not(target_arch = "wasm32"))] + #[cfg(feature = "remote_signing")] + #[c2pa_macros::c2pa_test_async] + async fn test_make_cawg_c2pa_remote_signers() { + use std::io::{Cursor, Seek}; + + use httpmock::MockServer; + + use crate::{ + create_signer, + crypto::cose::Verifier, + identity::{ + tests::fixtures::{manifest_json, parent_json}, + x509::X509SignatureVerifier, + IdentityAssertion, + }, + status_tracker::StatusTracker, + utils::test_remote_signer, + Builder, Context, Reader, + }; + + // Create mock for C2PA signer + let c2pa_alg = SigningAlg::Ps384; + let (c2pa_sign_cert, c2pa_private_key) = + test_signer::cert_chain_and_private_key_for_alg(c2pa_alg); + + let local_c2pa_signer = + create_signer::from_keys(c2pa_sign_cert, c2pa_private_key, c2pa_alg, None).unwrap(); + + let c2pa_server = MockServer::start(); + let _c2pa_mock = test_remote_signer::remote_signer_respond_with_signature( + &c2pa_server, + local_c2pa_signer, + ); + + // Create mock for CAWG signer + let cawg_alg = SigningAlg::Ed25519; + let (cawg_sign_cert, cawg_private_key) = + test_signer::cert_chain_and_private_key_for_alg(cawg_alg); + let local_cawg_signer = + create_signer::from_keys(&cawg_sign_cert, &cawg_private_key, cawg_alg, None).unwrap(); + + let cawg_server = MockServer::start(); + let _cawg_mock = test_remote_signer::remote_signer_respond_with_signature( + &cawg_server, + local_cawg_signer, + ); + + let config_settings = toml::toml! { + [signer.remote] + url = (c2pa_server.base_url()) + alg = (c2pa_alg.to_string()) + sign_cert = (String::from_utf8(c2pa_sign_cert.to_vec()).unwrap()) + + [cawg_x509_signer.remote] + url = (cawg_server.base_url()) + alg = (cawg_alg.to_string()) + sign_cert = (String::from_utf8(cawg_sign_cert.to_vec()).unwrap()) + } + .to_string(); + + let config_settings = Settings::new() + .with_toml(config_settings.as_str()) + .expect("Error parsing config settings") + .with_value("core.decode_identity_assertions", false) + .expect("Error setting core.decode_identity_assertions to false"); + + let context = Context::new() + .with_settings(&config_settings) + .expect("Error creating context") + .into_shared(); + + let format = "image/jpeg"; + let mut source = Cursor::new(include_bytes!("../../tests/fixtures/CA.jpg")); + let mut dest = Cursor::new(Vec::new()); + + // Use the context when creating the Builder + let mut builder = Builder::from_shared_context(&context) + .with_definition(manifest_json()) + .unwrap(); + builder + .add_ingredient_from_stream(parent_json(), format, &mut source) + .unwrap(); + + builder + .add_resource( + "thumbnail.jpg", + Cursor::new(include_bytes!("../../tests/fixtures/thumbnail.jpg")), + ) + .unwrap(); + + let cawg_signer = context.signer().expect("Error getting signer from context"); + + builder + .sign(cawg_signer, format, &mut source, &mut dest) + .unwrap(); + + // Read back the Manifest that was generated using the same context + dest.rewind().unwrap(); + + let manifest_store = Reader::from_shared_context(&context) + .with_stream(format, &mut dest) + .unwrap(); + assert_eq!(manifest_store.validation_status(), None); + + let manifest = manifest_store.active_manifest().unwrap(); + let mut st = StatusTracker::default(); + let mut ia_iter = IdentityAssertion::from_manifest(manifest, &mut st); + + // Should find exactly one identity assertion. + let ia = ia_iter.next().unwrap().unwrap(); + assert!(ia_iter.next().is_none()); + drop(ia_iter); + + // And that identity assertion should be valid for this manifest. + let x509_verifier = X509SignatureVerifier { + cose_verifier: Verifier::IgnoreProfileAndTrustPolicy, + }; + + let sig_info = ia + .validate(manifest, &mut st, &x509_verifier) + .await + .unwrap(); + + let cert_info = &sig_info.cert_info; + assert_eq!(cert_info.alg.unwrap(), SigningAlg::Ed25519); + assert_eq!( + cert_info.issuer_org.as_ref().unwrap(), + "C2PA Test Signing Cert" + ); + + _c2pa_mock.assert(); + _cawg_mock.assert(); + } } diff --git a/sdk/src/utils/mod.rs b/sdk/src/utils/mod.rs index 87a163db8..bbc53af9b 100644 --- a/sdk/src/utils/mod.rs +++ b/sdk/src/utils/mod.rs @@ -38,6 +38,9 @@ pub mod test; #[cfg(test)] pub(crate) mod test_signer; +#[cfg(all(test, feature = "remote_signing", not(target_arch = "wasm32")))] +pub(crate) mod test_remote_signer; + // fast 0 vector test using byte alignment to perform faster native byte align comparison pub(crate) fn is_zero(bytes: &[u8]) -> bool { if bytes.is_empty() { diff --git a/sdk/src/utils/test_remote_signer.rs b/sdk/src/utils/test_remote_signer.rs new file mode 100644 index 000000000..dc8f3ce77 --- /dev/null +++ b/sdk/src/utils/test_remote_signer.rs @@ -0,0 +1,29 @@ +use httpmock::{HttpMockRequest, HttpMockResponse}; + +use crate::signer::BoxedSigner; +pub(crate) 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); + }) +} + +pub(crate) fn remote_signer_respond_with_signature( + server: &'_ httpmock::MockServer, + signer: BoxedSigner, +) -> httpmock::Mock<'_> { + server.mock(|when, then| { + when.path("/").method(httpmock::Method::POST); + then.respond_with(move |req: &HttpMockRequest| { + let signature = signer.sign(req.body_ref()).unwrap(); + HttpMockResponse::builder() + .status(200) + .header("content-type", "application/octet-stream") + .body(signature) + .build() + }); + }) +} diff --git a/sdk/src/utils/test_signer.rs b/sdk/src/utils/test_signer.rs index 92a08e521..284a987f8 100644 --- a/sdk/src/utils/test_signer.rs +++ b/sdk/src/utils/test_signer.rs @@ -42,9 +42,9 @@ pub(crate) fn test_cawg_signer( let (cert_chain, private_key) = cert_chain_and_private_key_for_alg(alg); let c2pa_raw_signer = - signer_from_cert_chain_and_private_key(cert_chain, private_key, alg, None).unwrap(); + signer_from_cert_chain_and_private_key(cert_chain, private_key, alg, None)?; let cawg_raw_signer = - signer_from_cert_chain_and_private_key(cert_chain, private_key, alg, None).unwrap(); + signer_from_cert_chain_and_private_key(cert_chain, private_key, alg, None)?; let mut ia_signer = crate::identity::builder::IdentityAssertionSigner::new(c2pa_raw_signer); @@ -137,10 +137,6 @@ impl AsyncSigner for AsyncRawSignerWrapper { self.0.reserve_size() } - async fn ocsp_val(&self) -> Option> { - self.0.ocsp_response().await - } - fn time_authority_url(&self) -> Option { self.0.time_stamp_service_url() } @@ -162,6 +158,10 @@ impl AsyncSigner for AsyncRawSignerWrapper { .map(|r| r.map_err(|e| e.into())) } + async fn ocsp_val(&self) -> Option> { + self.0.ocsp_response().await + } + fn async_raw_signer(&self) -> Option> { Some(Box::new(&*self.0)) }