Skip to content
Draft
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 5 additions & 1 deletion sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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" }

Expand Down
5 changes: 3 additions & 2 deletions sdk/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
}
Expand Down
28 changes: 28 additions & 0 deletions sdk/src/create_signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -69,3 +72,28 @@ pub fn from_files<P: AsRef<Path>>(

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<String>,
) -> Result<BoxedSigner> {
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,
)?)))
}
5 changes: 5 additions & 0 deletions sdk/src/crypto/raw_signature/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
76 changes: 76 additions & 0 deletions sdk/src/crypto/raw_signature/openssl/cert_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<Vec<u8>>, RawSignerError> {
cert_chain
.iter()
.map(|cert| {
cert.to_der().map_err(|_| {
RawSignerError::CryptoLibraryError(
"could not encode certificate to DER".to_string(),
)
})
})
.collect::<Result<Vec<_>, RawSignerError>>()
}

// Verify the certificate chain order.
//
// Return `true` if each cert in the chain can be verified as issued by the next
Expand Down Expand Up @@ -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(())
}
24 changes: 9 additions & 15 deletions sdk/src/crypto/raw_signature/openssl/signers/ecdsa_signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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::<Result<Vec<_>, 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());
Expand Down Expand Up @@ -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<Vec<Vec<u8>>, 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 {
Expand Down
24 changes: 9 additions & 15 deletions sdk/src/crypto/raw_signature/openssl/signers/ed25519_signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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::<Result<Vec<_>, 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());
Expand Down Expand Up @@ -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<Vec<Vec<u8>>, 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 {
Expand Down
20 changes: 20 additions & 0 deletions sdk/src/crypto/raw_signature/openssl/signers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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<String>,
) -> Result<Box<dyn RawSigner + Send + Sync>, RawSignerError> {
Ok(Box::new(
remote_signer::RemoteRawSigner::from_cert_chain_and_url(
cert_chain,
url,
alg,
time_stamp_service_url,
)?,
))
}
Loading