Skip to content

Commit

Permalink
feat: Add ES256 and ES384 Rust native signing (#941)
Browse files Browse the repository at this point in the history
Add native rust feature to c2pa crate
  • Loading branch information
cdmurph32 authored Feb 24, 2025
1 parent 5a6a8bb commit d9d2526
Show file tree
Hide file tree
Showing 6 changed files with 216 additions and 24 deletions.
4 changes: 2 additions & 2 deletions internal/crypto/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
194 changes: 194 additions & 0 deletions internal/crypto/src/raw_signature/rust_native/signers/ecdsa_signer.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<u8>>,
cert_chain_len: usize,

signing_key: EcdsaSigningKey,

time_stamp_service_url: Option<String>,
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<String>,
) -> Result<Self, RawSignerError> {
let cert_chain = Pem::iter_from_buffer(cert_chain)
.map(|r| match r {
Ok(pem) => Ok(pem.contents),
Err(e) => Err(e),
})
.collect::<Result<Vec<Vec<u8>>, 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<Vec<u8>, 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<Vec<Vec<u8>>, RawSignerError> {
Ok(self.cert_chain.clone())
}
}

impl TimeStampProvider for EcdsaSigner {
fn time_stamp_service_url(&self) -> Option<String> {
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");
}
}
}
10 changes: 10 additions & 0 deletions internal/crypto/src/raw_signature/rust_native/signers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use crate::raw_signature::{RawSigner, RawSignerError, SigningAlg};

mod ecdsa_signer;
mod ed25519_signer;
mod rsa_signer;

Expand Down Expand Up @@ -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}"
))),
Expand Down
27 changes: 9 additions & 18 deletions internal/crypto/src/tests/cose/certificate_trust_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
Expand All @@ -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"))]
Expand Down Expand Up @@ -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);
Expand All @@ -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();
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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();
Expand All @@ -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"))]
Expand Down
1 change: 1 addition & 0 deletions sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []

Expand Down
4 changes: 0 additions & 4 deletions sdk/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
}

Expand Down

0 comments on commit d9d2526

Please sign in to comment.