diff --git a/Cargo.lock b/Cargo.lock index e0eb532fb..13bf8e16c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -882,6 +882,7 @@ dependencies = [ "base64 0.22.1", "c2pa", "c2pa-crypto", + "c2pa-status-tracker", "chrono", "ciborium", "coset", diff --git a/cawg_identity/Cargo.toml b/cawg_identity/Cargo.toml index d4128e04d..8461c0b29 100644 --- a/cawg_identity/Cargo.toml +++ b/cawg_identity/Cargo.toml @@ -29,6 +29,7 @@ async-trait = "0.1.78" base64 = "0.22.1" c2pa = { path = "../sdk", version = "0.43.0", features = ["openssl", "unstable_api"] } c2pa-crypto = { path = "../internal/crypto", version = "0.5.0" } +c2pa-status-tracker = { path = "../internal/status-tracker", version = "0.3.0" } chrono = { version = "0.4.38", features = ["serde"] } ciborium = "0.2.2" coset = "0.3.8" diff --git a/cawg_identity/src/builder/error.rs b/cawg_identity/src/builder/error.rs index c40288b2b..0433d0468 100644 --- a/cawg_identity/src/builder/error.rs +++ b/cawg_identity/src/builder/error.rs @@ -13,7 +13,6 @@ use std::fmt::Debug; -use c2pa_crypto::raw_signature::RawSignerError; use thiserror::Error; /// Describes errors that can occur when building a CAWG identity assertion. @@ -27,9 +26,13 @@ pub enum IdentityBuilderError { #[error("error while generating CBOR ({0})")] CborGenerationError(String), - /// An error occurred when generating the underlying raw signature. - #[error(transparent)] - RawSignerError(#[from] RawSignerError), + /// The credentials provided could not be used. + #[error("credential-related error ({0})")] + CredentialError(String), + + /// An error occurred when generating the underlying signature. + #[error("error while generating signature ({0})")] + SignerError(String), /// An unexpected internal error occured while requesting the time stamp /// response. diff --git a/cawg_identity/src/claim_aggregation/ica_signature_verifier.rs b/cawg_identity/src/claim_aggregation/ica_signature_verifier.rs index 1f8c02a73..7b4fc2ff0 100644 --- a/cawg_identity/src/claim_aggregation/ica_signature_verifier.rs +++ b/cawg_identity/src/claim_aggregation/ica_signature_verifier.rs @@ -213,6 +213,8 @@ impl SignatureVerifier for IcaSignatureVerifier { )); }; + // TO DO: Enforce signer_payload matches what was stated outside the signature. + // TO DO: Enforce validity window as compared to sig time (or now if no TSA // time). diff --git a/cawg_identity/src/lib.rs b/cawg_identity/src/lib.rs index 220d1d0e9..33fa27eba 100644 --- a/cawg_identity/src/lib.rs +++ b/cawg_identity/src/lib.rs @@ -31,3 +31,5 @@ pub(crate) mod internal; #[cfg(test)] pub(crate) mod tests; + +pub mod x509; diff --git a/cawg_identity/src/tests/mod.rs b/cawg_identity/src/tests/mod.rs index 9a9ba7b6f..0c9033b09 100644 --- a/cawg_identity/src/tests/mod.rs +++ b/cawg_identity/src/tests/mod.rs @@ -23,6 +23,7 @@ mod claim_aggregation; pub(crate) mod fixtures; mod identity_assertion; mod internal; +mod x509; #[cfg(target_arch = "wasm32")] wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); diff --git a/cawg_identity/src/tests/x509.rs b/cawg_identity/src/tests/x509.rs new file mode 100644 index 000000000..5756560be --- /dev/null +++ b/cawg_identity/src/tests/x509.rs @@ -0,0 +1,145 @@ +// 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 std::io::{Cursor, Seek}; + +use c2pa::{Builder, Reader, SigningAlg}; +use c2pa_crypto::raw_signature; +use serde_json::json; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_test::wasm_bindgen_test; + +use crate::{ + builder::{IdentityAssertionBuilder, IdentityAssertionSigner}, + tests::fixtures::cert_chain_and_private_key_for_alg, + x509::{X509CredentialHolder, X509SignatureVerifier}, + IdentityAssertion, +}; + +const TEST_IMAGE: &[u8] = include_bytes!("../../../sdk/tests/fixtures/CA.jpg"); +const TEST_THUMBNAIL: &[u8] = include_bytes!("../../../sdk/tests/fixtures/thumbnail.jpg"); + +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +async fn simple_case() { + let format = "image/jpeg"; + let mut source = Cursor::new(TEST_IMAGE); + let mut dest = Cursor::new(Vec::new()); + + let mut builder = Builder::from_json(&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_cert_chain, cawg_private_key) = + cert_chain_and_private_key_for_alg(SigningAlg::Ed25519); + + let cawg_raw_signer = raw_signature::async_signer_from_cert_chain_and_private_key( + &cawg_cert_chain, + &cawg_private_key, + SigningAlg::Ed25519, + None, + ) + .unwrap(); + + let x509_holder = X509CredentialHolder::from_async_raw_signer(cawg_raw_signer); + let iab = IdentityAssertionBuilder::for_credential_holder(x509_holder); + c2pa_signer.add_identity_assertion(iab); + + builder + .sign_async(&c2pa_signer, format, &mut source, &mut dest) + .await + .unwrap(); + + // Read back the Manifest that was generated. + dest.rewind().unwrap(); + + let manifest_store = Reader::from_stream(format, &mut dest).unwrap(); + assert_eq!(manifest_store.validation_status(), None); + + let manifest = manifest_store.active_manifest().unwrap(); + let mut ia_iter = IdentityAssertion::from_manifest(manifest); + + // Should find exactly one identity assertion. + let ia = ia_iter.next().unwrap().unwrap(); + assert!(ia_iter.next().is_none()); + + // And that identity assertion should be valid for this manifest. + let x509_verifier = X509SignatureVerifier {}; + let sig_info = ia.validate(manifest, &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" + ); + + // TO DO: Not sure what to check from COSE_Sign1. +} + +fn manifest_json() -> String { + json!({ + "vendor": "test", + "claim_generator_info": [ + { + "name": "c2pa_test", + "version": "1.0.0" + } + ], + "metadata": [ + { + "dateTime": "1985-04-12T23:20:50.52Z", + "my_custom_metadata": "my custom metatdata value" + } + ], + "title": "Test_Manifest", + "format": "image/tiff", + "instance_id": "1234", + "thumbnail": { + "format": "image/jpeg", + "identifier": "thumbnail.jpg" + }, + "ingredients": [ + { + "title": "Test", + "format": "image/jpeg", + "instance_id": "12345", + "relationship": "componentOf" + } + ], + "assertions": [ + { + "label": "org.test.assertion", + "data": "assertion" + } + ] + }) + .to_string() +} + +fn parent_json() -> String { + json!({ + "title": "Parent Test", + "format": "image/jpeg", + "instance_id": "12345", + "relationship": "parentOf" + }) + .to_string() +} diff --git a/cawg_identity/src/x509/mod.rs b/cawg_identity/src/x509/mod.rs new file mode 100644 index 000000000..b57ff675c --- /dev/null +++ b/cawg_identity/src/x509/mod.rs @@ -0,0 +1,28 @@ +// 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. + +//! Contains implementations of [`CredentialHolder`] and [`SignatureVerifier`] +//! for the X.509 certificates credential type described as specified in +//! [§8.2, X.509 certificates and COSE signatures]. +//! +//! [`CredentialHolder`]: crate::builder::CredentialHolder +//! [`SignatureVerifier`]: crate::SignatureVerifier +//! [§8.2, X.509 certificates and COSE signatures]: https://cawg.io/identity/1.1-draft/#_x_509_certificates_and_cose_signatures + +mod x509_credential_holder; +pub use x509_credential_holder::X509CredentialHolder; + +mod x509_signature_verifier; +pub use x509_signature_verifier::X509SignatureVerifier; + +const CAWG_X509_SIG_TYPE: &str = "cawg.x509.cose"; diff --git a/cawg_identity/src/x509/x509_credential_holder.rs b/cawg_identity/src/x509/x509_credential_holder.rs new file mode 100644 index 000000000..3a406dbc3 --- /dev/null +++ b/cawg_identity/src/x509/x509_credential_holder.rs @@ -0,0 +1,96 @@ +// 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 async_trait::async_trait; +use c2pa_crypto::{ + cose::{sign_async, TimeStampStorage}, + raw_signature::AsyncRawSigner, +}; + +use crate::{ + builder::{CredentialHolder, IdentityBuilderError}, + SignerPayload, +}; + +/// An implementation of [`CredentialHolder`] that generates COSE signatures +/// using X.509 credentials as specified in [§8.2, X.509 certificates and COSE +/// signatures]. +/// +/// [`SignatureVerifier`]: crate::SignatureVerifier +/// [§8.2, X.509 certificates and COSE signatures]: https://cawg.io/identity/1.1-draft/#_x_509_certificates_and_cose_signatures +#[cfg(not(target_arch = "wasm32"))] +pub struct X509CredentialHolder(Box); + +/// An implementation of [`CredentialHolder`] that generates COSE signatures +/// using X.509 credentials as specified in [§8.2, X.509 certificates and COSE +/// signatures]. +/// +/// [`CredentialHolder`]: crate::builder::CredentialHolder +/// [§8.2, X.509 certificates and COSE signatures]: https://cawg.io/identity/1.1-draft/#_x_509_certificates_and_cose_signatures +#[cfg(target_arch = "wasm32")] +pub struct X509CredentialHolder(Box); + +impl X509CredentialHolder { + /// Create an `X509CredentialHolder` instance by wrapping an instance of + /// [`AsyncRawSigner`]. + /// + /// The [`AsyncRawSigner`] implementation actually holds (or has access to) + /// the relevant certificates and private key material. + /// + /// [`AsyncRawSigner`]: c2pa_crypto::raw_signature::AsyncRawSigner + #[cfg(not(target_arch = "wasm32"))] + pub fn from_async_raw_signer(signer: Box) -> Self { + Self(signer) + } + + /// Create an `X509CredentialHolder` instance by wrapping an instance of + /// [`AsyncRawSigner`]. + /// + /// The [`AsyncRawSigner`] implementation actually holds (or has access to) + /// the relevant certificates and private key material. + /// + /// [`AsyncRawSigner`]: c2pa_crypto::raw_signature::AsyncRawSigner + #[cfg(target_arch = "wasm32")] + pub fn from_async_raw_signer(signer: Box) -> Self { + Self(signer) + } +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +impl CredentialHolder for X509CredentialHolder { + fn sig_type(&self) -> &'static str { + super::CAWG_X509_SIG_TYPE + } + + fn reserve_size(&self) -> usize { + self.0.reserve_size() + } + + async fn sign(&self, signer_payload: &SignerPayload) -> Result, IdentityBuilderError> { + // TO DO: Check signing cert (see signing_cert_valid in c2pa-rs's cose_sign). + + let mut sp_cbor: Vec = vec![]; + ciborium::into_writer(signer_payload, &mut sp_cbor) + .map_err(|e| IdentityBuilderError::CborGenerationError(e.to_string()))?; + + Ok(sign_async( + self.0.as_ref(), + &sp_cbor, + None, + TimeStampStorage::V2_sigTst2_CTT, + ) + .await + .map_err(|e| IdentityBuilderError::SignerError(e.to_string()))?) + } +} diff --git a/cawg_identity/src/x509/x509_signature_verifier.rs b/cawg_identity/src/x509/x509_signature_verifier.rs new file mode 100644 index 000000000..fdfacbe11 --- /dev/null +++ b/cawg_identity/src/x509/x509_signature_verifier.rs @@ -0,0 +1,87 @@ +// 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 async_trait::async_trait; +use c2pa_crypto::{ + cose::{parse_cose_sign1, CertificateInfo, CoseError, Verifier}, + raw_signature::RawSignatureValidationError, +}; +use c2pa_status_tracker::DetailedStatusTracker; +use coset::CoseSign1; + +use crate::{SignatureVerifier, SignerPayload, ValidationError}; + +/// An implementation of [`SignatureVerifier`] that supports COSE signatures +/// generated from X.509 credentials as specified in [§8.2, X.509 certificates +/// and COSE signatures]. +/// +/// [`SignatureVerifier`]: crate::SignatureVerifier +/// [§8.2, X.509 certificates and COSE signatures]: https://cawg.io/identity/1.1-draft/#_x_509_certificates_and_cose_signatures +pub struct X509SignatureVerifier {} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +impl SignatureVerifier for X509SignatureVerifier { + type Error = CoseError; + type Output = X509SignatureInfo; + + async fn check_signature( + &self, + signer_payload: &SignerPayload, + signature: &[u8], + ) -> Result> { + if signer_payload.sig_type != super::CAWG_X509_SIG_TYPE { + return Err(ValidationError::UnknownSignatureType( + signer_payload.sig_type.clone(), + )); + } + + let mut signer_payload_cbor: Vec = vec![]; + ciborium::into_writer(signer_payload, &mut signer_payload_cbor) + .map_err(|_| ValidationError::InternalError("CBOR serialization error".to_string()))?; + + // TO DO: Add options for trust list and certificate policy config. + let verifier = Verifier::IgnoreProfileAndTrustPolicy; + + // TO DO: Figure out how to provide a validation log. + let mut validation_log = DetailedStatusTracker::default(); + + let cose_sign1 = parse_cose_sign1(signature, &signer_payload_cbor, &mut validation_log)?; + + let cert_info = verifier + .verify_signature_async(signature, &signer_payload_cbor, &[], &mut validation_log) + .await + .map_err(|e| match e { + CoseError::RawSignatureValidationError( + RawSignatureValidationError::SignatureMismatch, + ) => ValidationError::InvalidSignature, + + e => ValidationError::SignatureError(e), + })?; + + Ok(X509SignatureInfo { + cose_sign1, + cert_info, + }) + } +} + +/// Contains information the X.509 certificate chain and the COSE signature that +/// was used to generate this identity assertion signature. +pub struct X509SignatureInfo { + /// Parsed COSE signature. + pub cose_sign1: CoseSign1, + + /// Information about the X.509 certificate chain. + pub cert_info: CertificateInfo, +}