From d9d25260588445f225d97e315be7f8149ee47639 Mon Sep 17 00:00:00 2001 From: Colin D Murphy Date: Mon, 24 Feb 2025 17:01:30 -0500 Subject: [PATCH] feat: Add ES256 and ES384 Rust native signing (#941) Add native rust feature to c2pa crate --- internal/crypto/README.md | 4 +- .../rust_native/signers/ecdsa_signer.rs | 194 ++++++++++++++++++ .../raw_signature/rust_native/signers/mod.rs | 10 + .../tests/cose/certificate_trust_policy.rs | 27 +-- sdk/Cargo.toml | 1 + sdk/src/builder.rs | 4 - 6 files changed, 216 insertions(+), 24 deletions(-) create mode 100644 internal/crypto/src/raw_signature/rust_native/signers/ecdsa_signer.rs diff --git a/internal/crypto/README.md b/internal/crypto/README.md index 86520c568..cd1a61b69 100644 --- a/internal/crypto/README.md +++ b/internal/crypto/README.md @@ -19,8 +19,8 @@ This crate has two features, neither of which are enabled by default: | C2PA `SigningAlg` | Default (*) | `feature = "rust_native_crypto"` (*) | WASM | | --- | --- | --- | --- | -| `es256` | OpenSSL | OpenSSL | ❌ | -| `es384` | OpenSSL | OpenSSL | ❌ | +| `es256` | OpenSSL | `p256` | `p256` | +| `es384` | OpenSSL | `p384` | `p384` | | `es512` | OpenSSL | OpenSSL | ❌ | | `ed25519` | OpenSSL | `ed25519-dalek` | `ed25519-dalek` | | `ps256` | OpenSSL | `rsa` | `rsa` | diff --git a/internal/crypto/src/raw_signature/rust_native/signers/ecdsa_signer.rs b/internal/crypto/src/raw_signature/rust_native/signers/ecdsa_signer.rs new file mode 100644 index 000000000..d9c9a5194 --- /dev/null +++ b/internal/crypto/src/raw_signature/rust_native/signers/ecdsa_signer.rs @@ -0,0 +1,194 @@ +// Copyright 2025 Adobe. All rights reserved. +// This file is licensed to you under the Apache License, +// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +// or the MIT license (http://opensource.org/licenses/MIT), +// at your option. + +// Unless required by applicable law or agreed to in writing, +// this software is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or +// implied. See the LICENSE-MIT and LICENSE-APACHE files for the +// specific language governing permissions and limitations under +// each license. + +use ecdsa::signature::Signer; +use p256::ecdsa::{Signature as P256Signature, SigningKey as P256SigningKey}; +use p384::ecdsa::{Signature as P384Signature, SigningKey as P384SigningKey}; +use pkcs8::DecodePrivateKey; +use x509_parser::{error::PEMError, pem::Pem}; + +use crate::{ + raw_signature::{RawSigner, RawSignerError, SigningAlg}, + time_stamp::TimeStampProvider, +}; + +// Rust native does not support Es512 +enum EcdsaSigningAlg { + Es256, + Es384, +} + +// Signing keys for ES256 and ES384 are different types +pub enum EcdsaSigningKey { + Es256(P256SigningKey), + Es384(P384SigningKey), +} + +pub struct EcdsaSigner { + alg: EcdsaSigningAlg, + + cert_chain: Vec>, + cert_chain_len: usize, + + signing_key: EcdsaSigningKey, + + time_stamp_service_url: Option, + time_stamp_size: usize, +} + +impl EcdsaSigner { + pub(crate) fn from_cert_chain_and_private_key( + cert_chain: &[u8], + private_key: &[u8], + algorithm: SigningAlg, + time_stamp_service_url: Option, + ) -> Result { + let cert_chain = Pem::iter_from_buffer(cert_chain) + .map(|r| match r { + Ok(pem) => Ok(pem.contents), + Err(e) => Err(e), + }) + .collect::>, PEMError>>() + .map_err(|e| RawSignerError::InvalidSigningCredentials(e.to_string()))?; + + let cert_chain_len = cert_chain.len(); + + let private_key_pem = std::str::from_utf8(private_key).map_err(|e| { + RawSignerError::InvalidSigningCredentials(format!("invalid private key: {e}")) + })?; + + let (signing_key, alg) = match algorithm { + SigningAlg::Es256 => { + let key = P256SigningKey::from_pkcs8_pem(private_key_pem).map_err(|e| { + RawSignerError::InvalidSigningCredentials(format!( + "invalid ES256 private key: {e}" + )) + })?; + (EcdsaSigningKey::Es256(key), EcdsaSigningAlg::Es256) + } + SigningAlg::Es384 => { + let key = P384SigningKey::from_pkcs8_pem(private_key_pem).map_err(|e| { + RawSignerError::InvalidSigningCredentials(format!( + "invalid ES384 private key: {e}" + )) + })?; + (EcdsaSigningKey::Es384(key), EcdsaSigningAlg::Es384) + } + SigningAlg::Es512 => { + return Err(RawSignerError::InvalidSigningCredentials( + "Rust Crypto does not support Es512, only OpenSSL".to_string(), + )) + } + _ => { + return Err(RawSignerError::InvalidSigningCredentials( + "Unsupported algorithm".to_string(), + )) + } + }; + + Ok(EcdsaSigner { + alg, + cert_chain, + cert_chain_len, + signing_key, + time_stamp_service_url, + time_stamp_size: 10000, + }) + } +} + +impl RawSigner for EcdsaSigner { + fn sign(&self, data: &[u8]) -> Result, RawSignerError> { + match self.signing_key { + EcdsaSigningKey::Es256(ref key) => { + let signature: P256Signature = key.sign(data); + Ok(signature.to_vec()) + } + EcdsaSigningKey::Es384(ref key) => { + let signature: P384Signature = key.sign(data); + Ok(signature.to_vec()) + } + } + } + + fn alg(&self) -> SigningAlg { + match self.alg { + EcdsaSigningAlg::Es256 => SigningAlg::Es256, + EcdsaSigningAlg::Es384 => SigningAlg::Es384, + } + } + + fn reserve_size(&self) -> usize { + 1024 + self.cert_chain_len + self.time_stamp_size + } + + fn cert_chain(&self) -> Result>, RawSignerError> { + Ok(self.cert_chain.clone()) + } +} + +impl TimeStampProvider for EcdsaSigner { + fn time_stamp_service_url(&self) -> Option { + self.time_stamp_service_url.clone() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::raw_signature::SigningAlg; + + #[test] + fn test_es512_not_supported() { + let cert_chain = include_bytes!("../../../tests/fixtures/raw_signature/es512.pub"); + let private_key = include_bytes!("../../../tests/fixtures/raw_signature/es512.priv"); + let algorithm = SigningAlg::Es512; + let time_stamp_service_url = None; + + let result = EcdsaSigner::from_cert_chain_and_private_key( + cert_chain, + private_key, + algorithm, + time_stamp_service_url, + ); + + assert!(result.is_err()); + if let Err(RawSignerError::InvalidSigningCredentials(err_msg)) = result { + assert_eq!(err_msg, "Rust Crypto does not support Es512, only OpenSSL"); + } else { + unreachable!("Expected InvalidSigningCredentials error"); + } + } + + #[test] + fn test_other_not_supported() { + let cert_chain = include_bytes!("../../../tests/fixtures/raw_signature/ps256.pub"); + let private_key = include_bytes!("../../../tests/fixtures/raw_signature/ps256.priv"); + let algorithm = SigningAlg::Ps256; + let time_stamp_service_url = None; + + let result = EcdsaSigner::from_cert_chain_and_private_key( + cert_chain, + private_key, + algorithm, + time_stamp_service_url, + ); + + assert!(result.is_err()); + if let Err(RawSignerError::InvalidSigningCredentials(err_msg)) = result { + assert_eq!(err_msg, "Unsupported algorithm"); + } else { + unreachable!("Expected InvalidSigningCredentials error"); + } + } +} diff --git a/internal/crypto/src/raw_signature/rust_native/signers/mod.rs b/internal/crypto/src/raw_signature/rust_native/signers/mod.rs index 7246959ee..b0e8804f1 100644 --- a/internal/crypto/src/raw_signature/rust_native/signers/mod.rs +++ b/internal/crypto/src/raw_signature/rust_native/signers/mod.rs @@ -16,6 +16,7 @@ use crate::raw_signature::{RawSigner, RawSignerError, SigningAlg}; +mod ecdsa_signer; mod ed25519_signer; mod rsa_signer; @@ -51,6 +52,15 @@ pub(crate) fn signer_from_cert_chain_and_private_key( )?, )), + SigningAlg::Es256 | SigningAlg::Es384 => Ok(Box::new( + ecdsa_signer::EcdsaSigner::from_cert_chain_and_private_key( + cert_chain, + private_key, + alg, + time_stamp_service_url, + )?, + )), + _ => Err(RawSignerError::InternalError(format!( "unsupported signing algorithm {alg}" ))), diff --git a/internal/crypto/src/tests/cose/certificate_trust_policy.rs b/internal/crypto/src/tests/cose/certificate_trust_policy.rs index 9daad1f60..5d42aec70 100644 --- a/internal/crypto/src/tests/cose/certificate_trust_policy.rs +++ b/internal/crypto/src/tests/cose/certificate_trust_policy.rs @@ -264,9 +264,7 @@ fn test_trust_store() { let ps256 = test_signer(SigningAlg::Ps256); let ps384 = test_signer(SigningAlg::Ps384); let ps512 = test_signer(SigningAlg::Ps512); - #[cfg(not(target_arch = "wasm32"))] let es256 = test_signer(SigningAlg::Es256); - #[cfg(not(target_arch = "wasm32"))] let es384 = test_signer(SigningAlg::Es384); #[cfg(not(target_arch = "wasm32"))] let es512 = test_signer(SigningAlg::Es512); @@ -275,9 +273,7 @@ fn test_trust_store() { let ps256_certs = ps256.cert_chain().unwrap(); let ps384_certs = ps384.cert_chain().unwrap(); let ps512_certs = ps512.cert_chain().unwrap(); - #[cfg(not(target_arch = "wasm32"))] let es256_certs = es256.cert_chain().unwrap(); - #[cfg(not(target_arch = "wasm32"))] let es384_certs = es384.cert_chain().unwrap(); #[cfg(not(target_arch = "wasm32"))] let es512_certs = es512.cert_chain().unwrap(); @@ -289,10 +285,8 @@ fn test_trust_store() { .unwrap(); ctp.check_certificate_trust(&ps512_certs[1..], &ps512_certs[0], None) .unwrap(); - #[cfg(not(target_arch = "wasm32"))] ctp.check_certificate_trust(&es256_certs[1..], &es256_certs[0], None) .unwrap(); - #[cfg(not(target_arch = "wasm32"))] ctp.check_certificate_trust(&es384_certs[1..], &es384_certs[0], None) .unwrap(); #[cfg(not(target_arch = "wasm32"))] @@ -349,9 +343,7 @@ fn test_broken_trust_chain() { let ps256 = test_signer(SigningAlg::Ps256); let ps384 = test_signer(SigningAlg::Ps384); let ps512 = test_signer(SigningAlg::Ps512); - #[cfg(not(target_arch = "wasm32"))] let es256 = test_signer(SigningAlg::Es256); - #[cfg(not(target_arch = "wasm32"))] let es384 = test_signer(SigningAlg::Es384); #[cfg(not(target_arch = "wasm32"))] let es512 = test_signer(SigningAlg::Es512); @@ -360,9 +352,7 @@ fn test_broken_trust_chain() { let ps256_certs = ps256.cert_chain().unwrap(); let ps384_certs = ps384.cert_chain().unwrap(); let ps512_certs = ps512.cert_chain().unwrap(); - #[cfg(not(target_arch = "wasm32"))] let es256_certs = es256.cert_chain().unwrap(); - #[cfg(not(target_arch = "wasm32"))] let es384_certs = es384.cert_chain().unwrap(); #[cfg(not(target_arch = "wasm32"))] let es512_certs = es512.cert_chain().unwrap(); @@ -393,14 +383,12 @@ fn test_broken_trust_chain() { CertificateTrustError::CertificateNotTrusted ); - #[cfg(not(target_arch = "wasm32"))] assert_eq!( ctp.check_certificate_trust(&es256_certs[2..], &es256_certs[0], None) .unwrap_err(), CertificateTrustError::CertificateNotTrusted ); - #[cfg(not(target_arch = "wasm32"))] assert_eq!( ctp.check_certificate_trust(&es384_certs[2..], &es384_certs[0], None) .unwrap_err(), @@ -518,20 +506,25 @@ fn test_allowed_list() { let ps256 = test_signer(SigningAlg::Ps256); let ps384 = test_signer(SigningAlg::Ps384); let ps512 = test_signer(SigningAlg::Ps512); - #[cfg(not(target_arch = "wasm32"))] let es256 = test_signer(SigningAlg::Es256); - #[cfg(not(target_arch = "wasm32"))] let es384 = test_signer(SigningAlg::Es384); #[cfg(not(target_arch = "wasm32"))] let es512 = test_signer(SigningAlg::Es512); let ed25519 = test_signer(SigningAlg::Ed25519); + assert_eq!(ps256.alg(), SigningAlg::Ps256); + assert_eq!(ps384.alg(), SigningAlg::Ps384); + assert_eq!(ps512.alg(), SigningAlg::Ps512); + assert_eq!(es256.alg(), SigningAlg::Es256); + assert_eq!(es384.alg(), SigningAlg::Es384); + #[cfg(not(target_arch = "wasm32"))] + assert_eq!(es512.alg(), SigningAlg::Es512); + assert_eq!(ed25519.alg(), SigningAlg::Ed25519); + let ps256_certs = ps256.cert_chain().unwrap(); let ps384_certs = ps384.cert_chain().unwrap(); let ps512_certs = ps512.cert_chain().unwrap(); - #[cfg(not(target_arch = "wasm32"))] let es256_certs = es256.cert_chain().unwrap(); - #[cfg(not(target_arch = "wasm32"))] let es384_certs = es384.cert_chain().unwrap(); #[cfg(not(target_arch = "wasm32"))] let es512_certs = es512.cert_chain().unwrap(); @@ -543,10 +536,8 @@ fn test_allowed_list() { .unwrap(); ctp.check_certificate_trust(&ps512_certs[1..], &ps512_certs[0], None) .unwrap(); - #[cfg(not(target_arch = "wasm32"))] ctp.check_certificate_trust(&es256_certs[1..], &es256_certs[0], None) .unwrap(); - #[cfg(not(target_arch = "wasm32"))] ctp.check_certificate_trust(&es384_certs[1..], &es384_certs[0], None) .unwrap(); #[cfg(not(target_arch = "wasm32"))] diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 20b9bbcd8..6f49f088b 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -32,6 +32,7 @@ serialize_thumbnails = [] no_interleaved_io = ["file_io"] fetch_remote_manifests = ["dep:wasi"] json_schema = ["dep:schemars", "c2pa-crypto/json_schema"] +rust_native_crypto = ["c2pa-crypto/rust_native_crypto"] pdf = ["dep:lopdf"] v1_api = [] diff --git a/sdk/src/builder.rs b/sdk/src/builder.rs index 386bfc84a..30fba321b 100644 --- a/sdk/src/builder.rs +++ b/sdk/src/builder.rs @@ -1485,8 +1485,6 @@ mod tests { dest.rewind().unwrap(); let manifest_store = Reader::from_stream(format, &mut dest).expect("from_bytes"); - //println!("{}", manifest_store); - #[cfg(not(target_arch = "wasm32"))] // skip this until we get wasm async signing working assert_eq!(manifest_store.validation_status(), None); assert_eq!( @@ -1583,7 +1581,6 @@ mod tests { let reader = crate::Reader::from_stream("image/jpeg", output_stream).unwrap(); println!("{reader}"); - #[cfg(not(target_arch = "wasm32"))] // skip this until we get wasm async signing working assert_eq!(reader.validation_status(), None); } @@ -1653,7 +1650,6 @@ mod tests { .await .unwrap(); //println!("{reader}"); - #[cfg(not(target_arch = "wasm32"))] // skip this until we get wasm async signing working assert_eq!(_reader.validation_status(), None); }