From 67161225c74679ad12c74c3b0a44fcbfab1ef451 Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Wed, 29 Apr 2026 13:45:56 -0700 Subject: [PATCH 1/6] chore: sync cawg validation --- .../identity/identity_assertion/assertion.rs | 117 +++++++++++++----- sdk/src/identity/validator.rs | 2 +- sdk/src/manifest.rs | 15 +-- 3 files changed, 93 insertions(+), 41 deletions(-) diff --git a/sdk/src/identity/identity_assertion/assertion.rs b/sdk/src/identity/identity_assertion/assertion.rs index e7cec2f27..ec715a663 100644 --- a/sdk/src/identity/identity_assertion/assertion.rs +++ b/sdk/src/identity/identity_assertion/assertion.rs @@ -17,11 +17,15 @@ use std::{ fmt::{Debug, Formatter}, }; +use async_generic::async_generic; use serde::{Deserialize, Serialize}; use serde_bytes::ByteBuf; use crate::{ - crypto::cose::{CertificateTrustPolicy, Verifier}, + crypto::{ + cose::{parse_cose_sign1, CertificateTrustPolicy, CoseError, Verifier}, + raw_signature::RawSignatureValidationError, + }, dynamic_assertion::PartialClaim, identity::{ claim_aggregation::IcaSignatureVerifier, @@ -33,7 +37,7 @@ use crate::{ signer_payload::SignerPayload, }, internal::debug_byte_slice::DebugByteSlice, - x509::X509SignatureVerifier, + x509::X509SignatureInfo, SignatureVerifier, ToCredentialSummary, ValidationError, }, jumbf::labels::to_assertion_uri, @@ -283,14 +287,20 @@ impl IdentityAssertion { .await } - /// Using the provided [`SignatureVerifier`], check the validity of this - /// identity assertion. + /// Validate this identity assertion against a list of claim assertion URIs. /// - /// If successful, returns the credential-type specific information that can - /// be derived from the signature. This is the [`SignatureVerifier::Output`] - /// type which typically describes the named actor, but may also contain - /// information about the time of signing or the credential's source. - pub(crate) async fn validate_partial_claim( + /// Accepts a plain slice of [`HashedUri`]s so callers do not need to + /// construct the internal [`PartialClaim`] type. + /// + /// The sync variant (`validate_partial_claim`) handles `cawg.x509.cose` + /// fully; other signature types that require network I/O (e.g. + /// `cawg.identity_claims_aggregation`) are skipped with an informational + /// log entry and return `None` from the caller's perspective. + /// + /// The async variant (`validate_partial_claim_async`) handles all known + /// signature types. + #[async_generic] + pub(crate) fn validate_partial_claim( &self, partial_claim: &PartialClaim, status_tracker: &mut StatusTracker, @@ -331,13 +341,47 @@ impl IdentityAssertion { Verifier::IgnoreProfileAndTrustPolicy }; - let verifier = X509SignatureVerifier { cose_verifier }; - - let result = verifier - .check_signature(&self.signer_payload, &self.signature, status_tracker) - .await - .map(|v| v.to_summary()) - .map_err(|e| ValidationError::UnknownSignatureType(e.to_string()))?; + let mut signer_payload_cbor: Vec = vec![]; + c2pa_cbor::to_writer(&mut signer_payload_cbor, &self.signer_payload).map_err(|_| { + ValidationError::InternalError("CBOR serialization error".to_string()) + })?; + + let cose_sign1 = + parse_cose_sign1(&self.signature, &signer_payload_cbor, status_tracker) + .map_err(|e| ValidationError::SignatureError(e.to_string()))?; + + let cert_info = if _sync { + cose_verifier.verify_signature( + &self.signature, + &signer_payload_cbor, + &[], + None, + status_tracker, + ) + } else { + cose_verifier + .verify_signature_async( + &self.signature, + &signer_payload_cbor, + &[], + None, + status_tracker, + ) + .await + } + .map_err(|e| match e { + CoseError::RawSignatureValidationError( + RawSignatureValidationError::SignatureMismatch, + ) => ValidationError::SignatureMismatch, + e => ValidationError::SignatureError(e.to_string()), + })?; + + let info = X509SignatureInfo { + signer_payload: self.signer_payload.clone(), + cose_sign1, + cert_info, + }; + let result = info.to_summary(); log_current_item!( "CAWG X.509 identity signature valid", @@ -351,22 +395,35 @@ impl IdentityAssertion { serde_json::to_value(result) .map_err(|e| ValidationError::UnknownSignatureType(e.to_string())) } else if sig_type == "cawg.identity_claims_aggregation" { - let verifier = IcaSignatureVerifier {}; + if _sync { + // ICA verification requires async network I/O; skip in sync context. + log_current_item!( + "identity_claims_aggregation validation skipped in sync context", + "validate_partial_claim" + ) + .validation_status("cawg.validation_skipped") + .informational(status_tracker); + Err(ValidationError::UnknownSignatureType( + "cawg.identity_claims_aggregation requires async".to_string(), + )) + } else { + let verifier = IcaSignatureVerifier {}; + let result = verifier + .check_signature(&self.signer_payload, &self.signature, status_tracker) + .await + .map(|v| v.to_summary()) + .map_err(|e| ValidationError::UnknownSignatureType(e.to_string()))?; - let result = verifier - .check_signature(&self.signer_payload, &self.signature, status_tracker) - .await - .map(|v| v.to_summary()) - .map_err(|e| ValidationError::UnknownSignatureType(e.to_string()))?; - log_current_item!( - "CAWG identity_claims_aggregation signature valid", - "validate_partial_claim" - ) - .validation_status("cawg.ica.credential_valid") - .success(status_tracker); + log_current_item!( + "CAWG identity_claims_aggregation signature valid", + "validate_partial_claim" + ) + .validation_status("cawg.ica.credential_valid") + .success(status_tracker); - serde_json::to_value(result) - .map_err(|e| ValidationError::UnknownSignatureType(e.to_string())) + serde_json::to_value(result) + .map_err(|e| ValidationError::UnknownSignatureType(e.to_string())) + } } else { Err(ValidationError::UnknownSignatureType(sig_type.to_string())) } diff --git a/sdk/src/identity/validator.rs b/sdk/src/identity/validator.rs index 5b668fd1c..9cdc9b500 100644 --- a/sdk/src/identity/validator.rs +++ b/sdk/src/identity/validator.rs @@ -41,7 +41,7 @@ impl AsyncPostValidator for CawgValidator { let identity_assertion: IdentityAssertion = assertion.to_assertion()?; tracker.push_current_uri(uri.to_string()); let result = identity_assertion - .validate_partial_claim(partial_claim, tracker) + .validate_partial_claim_async(partial_claim, tracker) .await .ok(); tracker.pop_current_uri(); diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs index b0659d8e3..db5269d30 100644 --- a/sdk/src/manifest.rs +++ b/sdk/src/manifest.rs @@ -605,19 +605,14 @@ impl Manifest { let uri = to_assertion_uri(manifest_label, label); validation_log.push_current_uri(&uri); + let identity_assertion: IdentityAssertion = ma.to_assertion()?; let value: Option = if _sync { - crate::log_item!( - uri, - "decoding identity assertions not supported in sync", - "from_store - validating cawg.identity" - ) - .validation_status("cawg.validation_skipped") - .informational(validation_log); - None - } else { - let identity_assertion: IdentityAssertion = ma.to_assertion()?; identity_assertion .validate_partial_claim(&partial_claim, validation_log) + .ok() + } else { + identity_assertion + .validate_partial_claim_async(&partial_claim, validation_log) .await .ok() }; From 0f1a1c09ad8fff2dbd3f2f0c15b2fddd77cb131d Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Mon, 11 May 2026 17:18:12 -0700 Subject: [PATCH 2/6] chore: helper to let old cawg tests use reader without identity translation --- .../built_in_signature_verifier.rs | 6 ++-- sdk/src/identity/tests/mod.rs | 19 ++++++++++++ .../continue_when_possible.rs | 29 +++++-------------- .../identity/tests/validation_method/mod.rs | 2 ++ .../tests/validation_method/stop_on_error.rs | 29 +++++-------------- .../identity/x509/x509_signature_verifier.rs | 9 ++++-- 6 files changed, 45 insertions(+), 49 deletions(-) diff --git a/sdk/src/identity/identity_assertion/built_in_signature_verifier.rs b/sdk/src/identity/identity_assertion/built_in_signature_verifier.rs index 03a2ad1c7..97fa59033 100644 --- a/sdk/src/identity/identity_assertion/built_in_signature_verifier.rs +++ b/sdk/src/identity/identity_assertion/built_in_signature_verifier.rs @@ -235,7 +235,8 @@ mod tests { // Read back the Manifest that was generated. dest.rewind().unwrap(); - let manifest_store = Reader::default().with_stream(format, &mut dest).unwrap(); + let manifest_store = + crate::identity::tests::read_manifest(format, &mut dest).await; assert_eq!(manifest_store.validation_status(), None); let manifest = manifest_store.active_manifest().unwrap(); @@ -392,7 +393,8 @@ mod tests { // Read back the Manifest that was generated. dest.rewind().unwrap(); - let manifest_store = Reader::default().with_stream(format, &mut dest).unwrap(); + let manifest_store = + crate::identity::tests::read_manifest(format, &mut dest).await; assert_eq!(manifest_store.validation_status(), None); let manifest = manifest_store.active_manifest().unwrap(); diff --git a/sdk/src/identity/tests/mod.rs b/sdk/src/identity/tests/mod.rs index deae9506d..e9eaf6ebd 100644 --- a/sdk/src/identity/tests/mod.rs +++ b/sdk/src/identity/tests/mod.rs @@ -19,3 +19,22 @@ mod claim_aggregation; mod examples; pub(crate) mod fixtures; mod validation_method; + +/// Read a manifest store with identity assertion decoding disabled so the raw +/// assertion bytes are preserved for manual validation in tests. +pub(crate) async fn read_manifest( + format: &str, + source: &mut R, +) -> crate::Reader { + let settings = crate::settings::Settings::default() + .with_value("core.decode_identity_assertions", false) + .unwrap(); + let context = crate::Context::new() + .with_settings(settings) + .unwrap() + .into_shared(); + crate::Reader::from_shared_context(&context) + .with_stream_async(format, source) + .await + .unwrap() +} diff --git a/sdk/src/identity/tests/validation_method/continue_when_possible.rs b/sdk/src/identity/tests/validation_method/continue_when_possible.rs index 2421cd37e..09048bb3d 100644 --- a/sdk/src/identity/tests/validation_method/continue_when_possible.rs +++ b/sdk/src/identity/tests/validation_method/continue_when_possible.rs @@ -24,7 +24,6 @@ use crate::{ crypto::raw_signature::SigningAlg, identity::{x509::X509SignatureVerifier, IdentityAssertion}, status_tracker::{LogKind, StatusTracker}, - Reader, }; /// An identity assertion MUST contain a valid CBOR data structure that contains @@ -41,9 +40,7 @@ async fn malformed_cbor() { let mut test_image = Cursor::new(test_image); // Initial read with default `Reader` should pass without issues. - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = super::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); // Re-parse with identity assertion code should find malformed CBOR error. @@ -83,9 +80,7 @@ async fn extra_fields() { let mut test_image = Cursor::new(test_image); // Initial read with default `Reader` should pass without issues. - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = super::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); // Re-parse with identity assertion code should find malformed CBOR error. @@ -141,9 +136,7 @@ async fn assertion_not_in_claim_v1() { let mut test_image = Cursor::new(test_image); // Initial read with default `Reader` should pass without issues. - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = super::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); // Re-parse with identity assertion code should find extra assertion error. @@ -255,9 +248,7 @@ async fn duplicate_assertion_reference() { let mut test_image = Cursor::new(test_image); // Initial read with default `Reader` should pass without issues. - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = super::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); // Re-parse with identity assertion code should find extra assertion error. @@ -358,9 +349,7 @@ async fn no_hard_binding() { let mut test_image = Cursor::new(test_image); // Initial read with default `Reader` should pass without issues. - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = super::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); // Re-parse with identity assertion code should find extra assertion error. @@ -623,9 +612,7 @@ async fn pad1_invalid() { let mut test_image = Cursor::new(test_image); // Initial read with default `Reader` should pass without issues. - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = super::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); // Re-parse with identity assertion code should find invalid pad error. @@ -715,9 +702,7 @@ async fn pad2_invalid() { let mut test_image = Cursor::new(test_image); // Initial read with default `Reader` should pass without issues. - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = super::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); // Re-parse with identity assertion code should find invalid pad error. diff --git a/sdk/src/identity/tests/validation_method/mod.rs b/sdk/src/identity/tests/validation_method/mod.rs index 9918f3937..8f994bb73 100644 --- a/sdk/src/identity/tests/validation_method/mod.rs +++ b/sdk/src/identity/tests/validation_method/mod.rs @@ -23,3 +23,5 @@ mod continue_when_possible; mod stop_on_error; + +pub(super) use super::read_manifest; diff --git a/sdk/src/identity/tests/validation_method/stop_on_error.rs b/sdk/src/identity/tests/validation_method/stop_on_error.rs index bb3e34d0f..525b2ab47 100644 --- a/sdk/src/identity/tests/validation_method/stop_on_error.rs +++ b/sdk/src/identity/tests/validation_method/stop_on_error.rs @@ -23,7 +23,6 @@ use wasm_bindgen_test::wasm_bindgen_test; use crate::{ identity::{x509::X509SignatureVerifier, IdentityAssertion}, status_tracker::{ErrorBehavior, LogKind, StatusTracker}, - Reader, }; /// An identity assertion MUST contain a valid CBOR data structure that contains @@ -40,9 +39,7 @@ async fn malformed_cbor() { let mut test_image = Cursor::new(test_image); // Initial read with default `Reader` should pass without issues. - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = super::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); // Re-parse with identity assertion code should find malformed CBOR error. @@ -82,9 +79,7 @@ async fn extra_fields() { let mut test_image = Cursor::new(test_image); // Initial read with default `Reader` should pass without issues. - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = super::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); // Re-parse with identity assertion code should find malformed CBOR error. @@ -139,9 +134,7 @@ async fn assertion_not_in_claim_v1() { let mut test_image = Cursor::new(test_image); // Initial read with default `Reader` should pass without issues. - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = super::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); // Re-parse with identity assertion code should find extra assertion error. @@ -227,9 +220,7 @@ async fn duplicate_assertion_reference() { let mut test_image = Cursor::new(test_image); // Initial read with default `Reader` should pass without issues. - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = super::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); // Re-parse with identity assertion code should find extra assertion error. @@ -305,9 +296,7 @@ async fn no_hard_binding() { let mut test_image = Cursor::new(test_image); // Initial read with default `Reader` should pass without issues. - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = super::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); // Re-parse with identity assertion code should find extra assertion error. @@ -551,9 +540,7 @@ async fn pad1_invalid() { let mut test_image = Cursor::new(test_image); // Initial read with default `Reader` should pass without issues. - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = super::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); // Re-parse with identity assertion code should find invalid pad error. @@ -617,9 +604,7 @@ async fn pad2_invalid() { let mut test_image = Cursor::new(test_image); // Initial read with default `Reader` should pass without issues. - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = super::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); // Re-parse with identity assertion code should find invalid pad error. diff --git a/sdk/src/identity/x509/x509_signature_verifier.rs b/sdk/src/identity/x509/x509_signature_verifier.rs index 94b053e99..96162f060 100644 --- a/sdk/src/identity/x509/x509_signature_verifier.rs +++ b/sdk/src/identity/x509/x509_signature_verifier.rs @@ -167,12 +167,15 @@ mod tests { }, identity::{ builder::{IdentityAssertionBuilder, IdentityAssertionSigner}, - tests::fixtures::{cert_chain_and_private_key_for_alg, manifest_json, parent_json}, + tests::{ + fixtures::{cert_chain_and_private_key_for_alg, manifest_json, parent_json}, + read_manifest, + }, x509::{X509CredentialHolder, X509SignatureVerifier}, IdentityAssertion, }, status_tracker::{LogKind, StatusTracker}, - Builder, Reader, SigningAlg, + Builder, SigningAlg, }; const TEST_IMAGE: &[u8] = include_bytes!("../../../tests/fixtures/CA.jpg"); @@ -219,7 +222,7 @@ mod tests { // Read back the Manifest that was generated. dest.rewind().unwrap(); - let manifest_store = Reader::default().with_stream(format, &mut dest).unwrap(); + let manifest_store = read_manifest(format, &mut dest).await; assert_eq!(manifest_store.validation_status(), None); let manifest = manifest_store.active_manifest().unwrap(); From 918a0392ad9a46f4b80ea2c9172643de92ebf048 Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Mon, 11 May 2026 17:24:24 -0700 Subject: [PATCH 3/6] fmt --- .../identity_assertion/built_in_signature_verifier.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sdk/src/identity/identity_assertion/built_in_signature_verifier.rs b/sdk/src/identity/identity_assertion/built_in_signature_verifier.rs index 97fa59033..5caf53c8d 100644 --- a/sdk/src/identity/identity_assertion/built_in_signature_verifier.rs +++ b/sdk/src/identity/identity_assertion/built_in_signature_verifier.rs @@ -235,8 +235,7 @@ mod tests { // Read back the Manifest that was generated. dest.rewind().unwrap(); - let manifest_store = - crate::identity::tests::read_manifest(format, &mut dest).await; + let manifest_store = crate::identity::tests::read_manifest(format, &mut dest).await; assert_eq!(manifest_store.validation_status(), None); let manifest = manifest_store.active_manifest().unwrap(); @@ -393,8 +392,7 @@ mod tests { // Read back the Manifest that was generated. dest.rewind().unwrap(); - let manifest_store = - crate::identity::tests::read_manifest(format, &mut dest).await; + let manifest_store = crate::identity::tests::read_manifest(format, &mut dest).await; assert_eq!(manifest_store.validation_status(), None); let manifest = manifest_store.active_manifest().unwrap(); From 2c8957a879e6574efecdac0a9f548c4ff1bcbfa7 Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Tue, 12 May 2026 17:42:41 -0700 Subject: [PATCH 4/6] feat: add full sync identity validation - no post_validate Sync and async paths in validate_partial_claim now run equivalently, so callers no longer need to call post_validate_async / CawgValidator as a separate step after loading a Reader. Removes the post_validate wrapper from c2pa_c_ffi, the validate_cawg helper from c2patool, and the manual post_validate_async call from the SDK integration test. Adds fn check_signature (sync) alongside the renamed async fn check_signature_async on SignatureVerifier and all implementations (IcaSignatureVerifier, X509SignatureVerifier, BuiltInSignatureVerifier, NaiveSignatureVerifier). IcaSignatureVerifier uses async_generic to unify the two check_issuer_signature variants; did:web is sync-guarded. Tests that need raw CBOR access use read_manifest with decode_identity_assertions=false. --- c2pa_c_ffi/src/c_api.rs | 66 ++---- c2pa_c_ffi/src/json_api.rs | 21 +- cli/src/main.rs | 38 +--- .../ica_signature_verifier.rs | 190 ++++++++++++------ .../identity/identity_assertion/assertion.rs | 45 ++--- .../built_in_signature_verifier.rs | 49 ++++- .../identity_assertion/signature_verifier.rs | 22 +- .../tests/claim_aggregation/interop.rs | 35 ++-- .../tests/claim_aggregation/validation.rs | 89 ++------ .../tests/fixtures/naive_credential_holder.rs | 11 +- .../identity/x509/x509_signature_verifier.rs | 48 ++++- sdk/tests/integration.rs | 10 +- 12 files changed, 338 insertions(+), 286 deletions(-) diff --git a/c2pa_c_ffi/src/c_api.rs b/c2pa_c_ffi/src/c_api.rs index 29cfbe913..bd18bd80f 100644 --- a/c2pa_c_ffi/src/c_api.rs +++ b/c2pa_c_ffi/src/c_api.rs @@ -20,12 +20,11 @@ use std::{ #[cfg(feature = "file_io")] use c2pa::Ingredient; use c2pa::{ - assertions::DataHash, identity::validator::CawgValidator, Builder as C2paBuilder, - CallbackSigner, Context, ProgressPhase, Reader as C2paReader, Settings as C2paSettings, - SigningAlg, + assertions::DataHash, Builder as C2paBuilder, CallbackSigner, Context, ProgressPhase, + Reader as C2paReader, Settings as C2paSettings, SigningAlg, }; -use tokio::runtime::Builder; +//use tokio::runtime::Builder; #[cfg(feature = "file_io")] use crate::json_api::{read_file, sign_file}; #[cfg(test)] @@ -1145,30 +1144,6 @@ pub unsafe extern "C" fn c2pa_free_string_array(ptr: *const *const c_char, count Vec::from_raw_parts(mut_ptr, count, count); } -// Run CAWG post-validation - this is async and requires a runtime. -fn post_validate(result: Result) -> Result { - match result { - Ok(mut reader) => { - #[cfg(target_arch = "wasm32")] - let runtime = Builder::new_current_thread().enable_all().build(); - - #[cfg(not(target_arch = "wasm32"))] - let runtime = Builder::new_multi_thread().enable_all().build(); - - let runtime = match runtime { - Ok(runtime) => runtime, - Err(err) => return Err(c2pa::Error::OtherError(Box::new(err))), - }; - - match runtime.block_on(reader.post_validate_async(&CawgValidator {})) { - Ok(_) => Ok(reader), - Err(err) => Err(err), - } - } - Err(err) => Err(err), - } -} - /// Creates a new C2paReader from a default context. /// /// # Safety @@ -1228,9 +1203,8 @@ pub unsafe extern "C" fn c2pa_reader_from_stream( // Legacy C API: inherits thread-local settings set by c2pa_load_settings. // Prefer c2pa_reader_from_context for new C API usage. #[allow(deprecated)] - let result = C2paReader::from_stream(&format, stream); - let result = ok_or_return_null!(post_validate(result)); - box_tracked!(result) + let reader = ok_or_return_null!(C2paReader::from_stream(&format, stream)); + box_tracked!(reader) } /// Configures an existing reader with a stream. @@ -1261,9 +1235,8 @@ pub unsafe extern "C" fn c2pa_reader_with_stream( // Now safe to take ownership - all validations passed untrack_or_return_null!(reader, C2paReader); let reader = Box::from_raw(reader); - let result = (*reader).with_stream(&format, stream); - let result = ok_or_return_null!(post_validate(result)); - box_tracked!(result) + let reader = ok_or_return_null!((*reader).with_stream(&format, stream)); + box_tracked!(reader) } /// Configures an existing passed in Reader with manifest data and a stream. @@ -1300,11 +1273,13 @@ pub unsafe extern "C" fn c2pa_reader_with_manifest_data_and_stream( // Take ownership of the Reader (needs to remove it from tracking to take it) untrack_or_return_null!(reader, C2paReader); let reader = Box::from_raw(reader); - let result = (*reader).with_manifest_data_and_stream(manifest_bytes, &format, stream); - let result = ok_or_return_null!(post_validate(result)); - + let reader = ok_or_return_null!((*reader).with_manifest_data_and_stream( + manifest_bytes, + &format, + stream + )); // New reader, will be tracked now too - box_tracked!(result) + box_tracked!(reader) } /// Configures an existing reader with a fragment stream. @@ -1347,9 +1322,8 @@ pub unsafe extern "C" fn c2pa_reader_with_fragment( // Now safe to take ownership - all validations passed untrack_or_return_null!(reader, C2paReader); let reader = Box::from_raw(reader); - let result = (*reader).with_fragment(&format, stream, fragment); - let result = ok_or_return_null!(post_validate(result)); - box_tracked!(result) + let reader = ok_or_return_null!((*reader).with_fragment(&format, stream, fragment)); + box_tracked!(reader) } /// Creates a new C2paReader from a shared Context. @@ -1396,7 +1370,7 @@ pub unsafe fn c2pa_reader_from_file(path: *const c_char) -> *mut C2paReader { let path = cstr_or_return_null!(path); // Legacy C API: inherits thread-local settings set by c2pa_load_settings. let result = C2paReader::from_file(&path); - box_tracked!(ok_or_return_null!(post_validate(result))) + box_tracked!(ok_or_return_null!(result)) } /// Creates and verifies a C2paReader from an asset stream with the given format and manifest data. @@ -1432,8 +1406,12 @@ pub unsafe extern "C" fn c2pa_reader_from_manifest_data_and_stream( // Legacy C API: inherits thread-local settings set by c2pa_load_settings. #[allow(deprecated)] - let result = C2paReader::from_manifest_data_and_stream(manifest_bytes, &format, stream); - box_tracked!(ok_or_return_null!(post_validate(result))) + let reader = ok_or_return_null!(C2paReader::from_manifest_data_and_stream( + manifest_bytes, + &format, + stream + )); + box_tracked!(reader) } /// Frees a C2paReader allocated by Rust. diff --git a/c2pa_c_ffi/src/json_api.rs b/c2pa_c_ffi/src/json_api.rs index 23b5f71a1..3f1596942 100644 --- a/c2pa_c_ffi/src/json_api.rs +++ b/c2pa_c_ffi/src/json_api.rs @@ -10,8 +10,7 @@ // specific language governing permissions and limitations under // each license. -use c2pa::{identity::validator::CawgValidator, Ingredient, Reader, Relationship}; -use tokio::runtime::Builder; +use c2pa::{Ingredient, Reader, Relationship}; use crate::{Error, Result, SignerInfo}; @@ -22,23 +21,7 @@ use crate::{Error, Result, SignerInfo}; #[allow(deprecated)] pub fn read_file(path: &str, data_dir: Option) -> Result { // Legacy JSON API: inherits thread-local settings set by c2pa_load_settings. - let mut reader = Reader::from_file(path).map_err(Error::from_c2pa_error)?; - - #[cfg(target_arch = "wasm32")] - let runtime = Builder::new_current_thread() - .enable_all() - .build() - .map_err(|e| Error::Other(e.to_string()))?; - - #[cfg(not(target_arch = "wasm32"))] - let runtime = Builder::new_multi_thread() - .enable_all() - .build() - .map_err(|e| Error::Other(e.to_string()))?; - - runtime - .block_on(reader.post_validate_async(&CawgValidator {})) - .map_err(Error::from_c2pa_error)?; + let reader = Reader::from_file(path).map_err(Error::from_c2pa_error)?; Ok(if let Some(dir) = data_dir { let json = reader.json(); diff --git a/cli/src/main.rs b/cli/src/main.rs index e1517563b..c01f1e535 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -29,9 +29,8 @@ use std::{ use anyhow::{anyhow, bail, Context, Result}; use c2pa::{ - format_from_path, identity::validator::CawgValidator, settings::Settings, Builder, - ClaimGeneratorInfo, Context as C2paContext, Error, Ingredient, ManifestDefinition, Reader, - Signer, + format_from_path, settings::Settings, Builder, ClaimGeneratorInfo, Context as C2paContext, + Error, Ingredient, ManifestDefinition, Reader, Signer, }; use clap::{Parser, Subcommand}; use env_logger::Env; @@ -40,11 +39,7 @@ use log::debug; use serde::Deserialize; use signer::SignConfig; use tempfile::NamedTempFile; -#[cfg(not(target_os = "wasi"))] -use tokio::runtime::Runtime; use url::Url; -#[cfg(target_os = "wasi")] -use wstd::runtime::block_on; use crate::{ callback_signer::{CallbackSigner, CallbackSignerConfig, ExternalProcessRunner}, @@ -723,20 +718,6 @@ fn verify_fragmented( Ok(readers) } -// run cawg validation if supported -fn validate_cawg(reader: &mut Reader) -> Result<()> { - #[cfg(not(target_os = "wasi"))] - { - Runtime::new()? - .block_on(reader.post_validate_async(&CawgValidator {})) - .map_err(anyhow::Error::from) - } - #[cfg(target_os = "wasi")] - { - block_on(reader.post_validate_async(&CawgValidator {})).map_err(anyhow::Error::from) - } -} - fn reader_from_args( asset_path: &Path, args: &CliArgs, @@ -1033,10 +1014,9 @@ fn main() -> Result<()> { } // generate a report on the output file - let mut reader = Reader::from_shared_context(&context) + let reader = Reader::from_shared_context(&context) .with_file(&output) .map_err(special_errs)?; - validate_cawg(&mut reader)?; print_reader(&reader, args.detailed, args.crjson)?; } } else { @@ -1064,10 +1044,9 @@ fn main() -> Result<()> { File::create(output.join("ingredient.json"))?.write_all(&report.into_bytes())?; println!("Ingredient report written to the directory {:?}", &output); } else { - let mut reader = Reader::from_shared_context(&context) + let reader = Reader::from_shared_context(&context) .with_file(path) .map_err(special_errs)?; - validate_cawg(&mut reader)?; reader.to_folder(&output)?; let report = reader.to_string(); if args.detailed { @@ -1087,19 +1066,14 @@ fn main() -> Result<()> { fragments_glob: Some(fg), }) = &args.command { - let mut stores = verify_fragmented(path, fg, &context)?; + let stores = verify_fragmented(path, fg, &context)?; if stores.len() == 1 { - validate_cawg(&mut stores[0])?; println!("{}", stores[0]); } else { - for store in &mut stores { - validate_cawg(store)?; - } println!("{} Init manifests validated", stores.len()); } } else { - let mut reader = reader_from_args(path, &args, &context)?; - validate_cawg(&mut reader)?; + let reader = reader_from_args(path, &args, &context)?; print_reader(&reader, args.detailed, args.crjson)?; } diff --git a/sdk/src/identity/claim_aggregation/ica_signature_verifier.rs b/sdk/src/identity/claim_aggregation/ica_signature_verifier.rs index bf515cb78..423dc4eab 100644 --- a/sdk/src/identity/claim_aggregation/ica_signature_verifier.rs +++ b/sdk/src/identity/claim_aggregation/ica_signature_verifier.rs @@ -11,6 +11,7 @@ // specific language governing permissions and limitations under // each license. +use async_generic::async_generic; use async_trait::async_trait; use base64::{prelude::BASE64_URL_SAFE, Engine}; use chrono::{DateTime, Utc}; @@ -19,7 +20,7 @@ use coset::{CoseSign1, RegisteredLabelWithPrivate, TaggedCborSerializable}; use crate::{ crypto::{ asn1::rfc3161::TstInfo, - cose::{validate_cose_tst_info_async, CertificateTrustPolicy}, + cose::{validate_cose_tst_info, validate_cose_tst_info_async, CertificateTrustPolicy}, }, identity::{ claim_aggregation::{ @@ -59,7 +60,77 @@ impl SignatureVerifier for IcaSignatureVerifier { type Error = IcaValidationError; type Output = IcaCredential; - async fn check_signature( + fn check_signature( + &self, + signer_payload: &SignerPayload, + signature: &[u8], + status_tracker: &mut StatusTracker, + ) -> Result> { + self.check_sig_type(signer_payload, status_tracker)?; + + let sign1 = self.decode_cose_sign1(signature, status_tracker)?; + let _ssi_alg = self.decode_signing_alg(&sign1, status_tracker)?; + + let mut ok = true; + + self.check_content_type(&sign1, status_tracker, &mut ok)?; + + let payload_bytes = self.payload_bytes(&sign1, status_tracker)?; + + let mut ica_credential = self.parse_ica_vc_v2(payload_bytes, status_tracker)?; + + self.check_issuer_signature(&sign1, &ica_credential) + .or_else(|err| { + ok = false; + self.handle_signature_error(err, status_tracker) + })?; + + let local_ctp = CertificateTrustPolicy::passthrough(); + let mut timestamp_tracker = StatusTracker::default(); + + let maybe_tst_info = match validate_cose_tst_info( + &sign1, + payload_bytes, + &local_ctp, + &mut timestamp_tracker, + false, + ) + .inspect(|tst_info| self.save_time_stamp(tst_info, &mut ica_credential, status_tracker)) + { + Ok(tst_info) => Some(tst_info), + Err(_err) => { + self.handle_time_stamp_error(&mut timestamp_tracker, status_tracker, &mut ok)?; + None + } + }; + + self.check_valid_from(&ica_credential, maybe_tst_info.as_ref()) + .or_else(|err| { + ok = false; + self.handle_non_fatal_error(err, status_tracker) + })?; + + self.check_valid_until(&ica_credential, maybe_tst_info.as_ref()) + .or_else(|err| { + ok = false; + self.handle_non_fatal_error(err, status_tracker) + })?; + + self.cross_check_signer_payload(&ica_credential, signer_payload, status_tracker, &mut ok)?; + + if ok { + log_current_item!( + "ICA credential is valid", + "IcaSignatureVerifier::check_signature" + ) + .validation_status("cawg.ica.credential_valid") + .success(status_tracker); + } + + Ok(ica_credential) + } + + async fn check_signature_async( &self, signer_payload: &SignerPayload, signature: &[u8], @@ -83,7 +154,7 @@ impl SignatureVerifier for IcaSignatureVerifier { // TO DO (CAI-7970): Add support for VC version 1. let mut ica_credential = self.parse_ica_vc_v2(payload_bytes, status_tracker)?; - self.check_issuer_signature(&sign1, &ica_credential) + self.check_issuer_signature_async(&sign1, &ica_credential) .await .or_else(|err| { ok = false; @@ -116,14 +187,12 @@ impl SignatureVerifier for IcaSignatureVerifier { }; self.check_valid_from(&ica_credential, maybe_tst_info.as_ref()) - .await .or_else(|err| { ok = false; self.handle_non_fatal_error(err, status_tracker) })?; self.check_valid_until(&ica_credential, maybe_tst_info.as_ref()) - .await .or_else(|err| { ok = false; self.handle_non_fatal_error(err, status_tracker) @@ -138,7 +207,7 @@ impl SignatureVerifier for IcaSignatureVerifier { if ok { log_current_item!( "ICA credential is valid", - "IcaSignatureVerifier::check_signature" + "IcaSignatureVerifier::check_signature_async" ) .validation_status("cawg.ica.credential_valid") .success(status_tracker); @@ -355,16 +424,17 @@ impl IcaSignatureVerifier { Ok(ica_credential) } - async fn check_issuer_signature( + // Discover public key for issuer DID and validate signature. + // TEMPORARY version supports did:jwk and did:web only. + // + // TO DO (CAI-7976): Accept issuer DID in either `issuer` or `issuer.id` field. + // Currently only `issuer` field is supported. + #[async_generic] + fn check_issuer_signature( &self, sign1: &CoseSign1, ica_credential: &IcaCredential, ) -> Result<(), ValidationError> { - // Discover public key for issuer DID and validate signature. - // TEMPORARY version supports did:jwk and did:web only. - - // TO DO (CAI-7976): Accept issuer DID in either `issuer` or `issuer.id` field. - // Currently only `issuer` field is supported. let issuer_id = Did::new(&ica_credential.issuer)?; let (primary_did, _fragment) = issuer_id.split_fragment(); @@ -388,47 +458,56 @@ impl IcaSignatureVerifier { } "web" => { - let did_doc = did_web::resolve(&primary_did).await?; - - let Some(vm1) = did_doc.verification_relationships.assertion_method.first() else { - return Err(ValidationError::SignatureError( - IcaValidationError::InvalidDidDocument( - "DID document doesn't contain an assertionMethod entry".to_string(), - ), - )); - }; - - let super::w3c_vc::did_doc::ValueOrReference::Value(vm1) = vm1 else { + if _sync { return Err(ValidationError::SignatureError( - IcaValidationError::InvalidDidDocument( - "DID document's assertionMethod is not a value".to_string(), + IcaValidationError::UnsupportedIssuerDid( + "did:web requires async resolution".to_string(), ), )); - }; - - let Some(jwk_prop) = vm1.properties.get("publicKeyJwk") else { - return Err(ValidationError::SignatureError( - IcaValidationError::InvalidDidDocument( - "DID document's assertionMethod doesn't contain a publicKeyJwk entry" - .to_string(), - ), - )); - }; - - // OMG SO HACKY! - let Ok(jwk_json) = serde_json::to_string_pretty(jwk_prop) else { - return Err(ValidationError::InternalError( - "couldn't re-serialize JWK".to_string(), - )); - }; - - let Ok(jwk) = serde_json::from_str(&jwk_json) else { - return Err(ValidationError::InternalError( - "couldn't re-serialize JWK".to_string(), - )); - }; - - jwk + } else { + let did_doc = did_web::resolve(&primary_did).await?; + + let Some(vm1) = did_doc.verification_relationships.assertion_method.first() + else { + return Err(ValidationError::SignatureError( + IcaValidationError::InvalidDidDocument( + "DID document doesn't contain an assertionMethod entry".to_string(), + ), + )); + }; + + let super::w3c_vc::did_doc::ValueOrReference::Value(vm1) = vm1 else { + return Err(ValidationError::SignatureError( + IcaValidationError::InvalidDidDocument( + "DID document's assertionMethod is not a value".to_string(), + ), + )); + }; + + let Some(jwk_prop) = vm1.properties.get("publicKeyJwk") else { + return Err(ValidationError::SignatureError( + IcaValidationError::InvalidDidDocument( + "DID document's assertionMethod doesn't contain a publicKeyJwk entry" + .to_string(), + ), + )); + }; + + // OMG SO HACKY! + let Ok(jwk_json) = serde_json::to_string_pretty(jwk_prop) else { + return Err(ValidationError::InternalError( + "couldn't re-serialize JWK".to_string(), + )); + }; + + let Ok(jwk) = serde_json::from_str(&jwk_json) else { + return Err(ValidationError::InternalError( + "couldn't re-serialize JWK".to_string(), + )); + }; + + jwk + } } x => { @@ -449,8 +528,6 @@ impl IcaSignatureVerifier { )); } - // Check the signature, which needs to have the same `aad` provided, by - // providing a closure that can do the verify operation. sign1 .verify_signature(b"", |sig, data| { use ed25519_dalek::Verifier; @@ -460,11 +537,6 @@ impl IcaSignatureVerifier { }) .map_err(|_e| ValidationError::SignatureMismatch)?; - // 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). - Ok(()) } @@ -594,7 +666,7 @@ impl IcaSignatureVerifier { // Enforce [§8.1.1.4. Validity]. // // [§8.1.1.4. Validity]: https://cawg.io/identity/1.1-draft/ - async fn check_valid_from( + fn check_valid_from( &self, ica_credential: &IcaCredential, maybe_tst_info: Option<&TstInfo>, @@ -637,7 +709,7 @@ impl IcaSignatureVerifier { Ok(()) } - async fn check_valid_until( + fn check_valid_until( &self, ica_credential: &IcaCredential, maybe_tst_info: Option<&TstInfo>, diff --git a/sdk/src/identity/identity_assertion/assertion.rs b/sdk/src/identity/identity_assertion/assertion.rs index ec715a663..dd69146b0 100644 --- a/sdk/src/identity/identity_assertion/assertion.rs +++ b/sdk/src/identity/identity_assertion/assertion.rs @@ -283,7 +283,7 @@ impl IdentityAssertion { .check_against_manifest(manifest, status_tracker)?; verifier - .check_signature(&self.signer_payload, &self.signature, status_tracker) + .check_signature_async(&self.signer_payload, &self.signature, status_tracker) .await } @@ -395,35 +395,30 @@ impl IdentityAssertion { serde_json::to_value(result) .map_err(|e| ValidationError::UnknownSignatureType(e.to_string())) } else if sig_type == "cawg.identity_claims_aggregation" { - if _sync { - // ICA verification requires async network I/O; skip in sync context. - log_current_item!( - "identity_claims_aggregation validation skipped in sync context", - "validate_partial_claim" - ) - .validation_status("cawg.validation_skipped") - .informational(status_tracker); - Err(ValidationError::UnknownSignatureType( - "cawg.identity_claims_aggregation requires async".to_string(), - )) - } else { - let verifier = IcaSignatureVerifier {}; - let result = verifier + let verifier = IcaSignatureVerifier {}; + + let result = if _sync { + verifier .check_signature(&self.signer_payload, &self.signature, status_tracker) + .map(|v| v.to_summary()) + .map_err(|e| ValidationError::UnknownSignatureType(e.to_string())) + } else { + verifier + .check_signature_async(&self.signer_payload, &self.signature, status_tracker) .await .map(|v| v.to_summary()) - .map_err(|e| ValidationError::UnknownSignatureType(e.to_string()))?; + .map_err(|e| ValidationError::UnknownSignatureType(e.to_string())) + }?; - log_current_item!( - "CAWG identity_claims_aggregation signature valid", - "validate_partial_claim" - ) - .validation_status("cawg.ica.credential_valid") - .success(status_tracker); + log_current_item!( + "CAWG identity_claims_aggregation signature valid", + "validate_partial_claim" + ) + .validation_status("cawg.ica.credential_valid") + .success(status_tracker); - serde_json::to_value(result) - .map_err(|e| ValidationError::UnknownSignatureType(e.to_string())) - } + serde_json::to_value(result) + .map_err(|e| ValidationError::UnknownSignatureType(e.to_string())) } else { Err(ValidationError::UnknownSignatureType(sig_type.to_string())) } diff --git a/sdk/src/identity/identity_assertion/built_in_signature_verifier.rs b/sdk/src/identity/identity_assertion/built_in_signature_verifier.rs index 5caf53c8d..702e39d2e 100644 --- a/sdk/src/identity/identity_assertion/built_in_signature_verifier.rs +++ b/sdk/src/identity/identity_assertion/built_in_signature_verifier.rs @@ -45,7 +45,7 @@ impl SignatureVerifier for BuiltInSignatureVerifier<'_> { type Error = BuiltInSignatureError; type Output = BuiltInCredential; - async fn check_signature( + fn check_signature( &self, signer_payload: &SignerPayload, signature: &[u8], @@ -55,13 +55,36 @@ impl SignatureVerifier for BuiltInSignatureVerifier<'_> { crate::identity::claim_aggregation::CAWG_ICA_SIG_TYPE => self .ica_verifier .check_signature(signer_payload, signature, status_tracker) - .await .map(BuiltInCredential::IdentityClaimsAggregationCredential) .map_err(map_err_to_built_in), crate::identity::x509::CAWG_X509_SIG_TYPE => self .x509_verifier .check_signature(signer_payload, signature, status_tracker) + .map(BuiltInCredential::X509Signature) + .map_err(map_err_to_built_in), + + sig_type => Err(ValidationError::UnknownSignatureType(sig_type.to_string())), + } + } + + async fn check_signature_async( + &self, + signer_payload: &SignerPayload, + signature: &[u8], + status_tracker: &mut StatusTracker, + ) -> Result> { + match signer_payload.sig_type.as_str() { + crate::identity::claim_aggregation::CAWG_ICA_SIG_TYPE => self + .ica_verifier + .check_signature_async(signer_payload, signature, status_tracker) + .await + .map(BuiltInCredential::IdentityClaimsAggregationCredential) + .map_err(map_err_to_built_in), + + crate::identity::x509::CAWG_X509_SIG_TYPE => self + .x509_verifier + .check_signature_async(signer_payload, signature, status_tracker) .await .map(BuiltInCredential::X509Signature) .map_err(map_err_to_built_in), @@ -186,9 +209,8 @@ mod tests { x509::AsyncX509CredentialHolder, IdentityAssertion, SignerPayload, ValidationError, }, - settings::Settings, status_tracker::StatusTracker, - Builder, Context, HashedUri, Reader, SigningAlg, + Builder, HashedUri, SigningAlg, }; const TEST_IMAGE: &[u8] = include_bytes!("../../../tests/fixtures/CA.jpg"); @@ -267,19 +289,24 @@ mod tests { #[c2pa_test_async] async fn adobe_connected_identities() { - let settings = Settings::new() - .with_value("verify.verify_trust", false) - .unwrap(); - let context = Context::new().with_settings(settings).unwrap(); - let format = "image/jpeg"; let test_image = include_bytes!("../tests/fixtures/claim_aggregation/adobe_connected_identities.jpg"); let mut test_image = Cursor::new(test_image); - let reader = Reader::from_context(context) - .with_stream(format, &mut test_image) + let settings = crate::settings::Settings::default() + .with_value("verify.verify_trust", false) + .unwrap() + .with_value("core.decode_identity_assertions", false) + .unwrap(); + let context = crate::Context::new() + .with_settings(settings) + .unwrap() + .into_shared(); + let reader = crate::Reader::from_shared_context(&context) + .with_stream_async(format, &mut test_image) + .await .unwrap(); assert_eq!(reader.validation_status(), None); diff --git a/sdk/src/identity/identity_assertion/signature_verifier.rs b/sdk/src/identity/identity_assertion/signature_verifier.rs index a02efb1f2..a7643885a 100644 --- a/sdk/src/identity/identity_assertion/signature_verifier.rs +++ b/sdk/src/identity/identity_assertion/signature_verifier.rs @@ -43,11 +43,27 @@ pub trait SignatureVerifier: Sync { /// [`ValidationError`]: crate::identity::ValidationError type Error: Debug; - /// Verify the signature, returning an instance of [`Output`] if the - /// signature is valid. + /// Verify the signature synchronously, returning an instance of [`Output`] + /// if the signature is valid. + /// + /// Implementations that require network I/O (e.g. `did:web` resolution) + /// must return an error from this method; use [`check_signature_async`] + /// instead for those cases. + /// + /// [`Output`]: Self::Output + /// [`check_signature_async`]: Self::check_signature_async + fn check_signature( + &self, + signer_payload: &SignerPayload, + signature: &[u8], + status_tracker: &mut StatusTracker, + ) -> Result>; + + /// Verify the signature asynchronously, returning an instance of [`Output`] + /// if the signature is valid. /// /// [`Output`]: Self::Output - async fn check_signature( + async fn check_signature_async( &self, signer_payload: &SignerPayload, signature: &[u8], diff --git a/sdk/src/identity/tests/claim_aggregation/interop.rs b/sdk/src/identity/tests/claim_aggregation/interop.rs index 96c49d209..9a379fefa 100644 --- a/sdk/src/identity/tests/claim_aggregation/interop.rs +++ b/sdk/src/identity/tests/claim_aggregation/interop.rs @@ -27,24 +27,36 @@ use crate::{ }, settings::Settings, status_tracker::StatusTracker, - Context, HashedUri, Reader, + Context, HashedUri, }; -#[c2pa_test_async] -async fn adobe_connected_identities() { +async fn read_manifest_no_trust( + format: &str, + source: &mut R, +) -> crate::Reader { let settings = Settings::new() .with_value("verify.verify_trust", false) + .unwrap() + .with_value("core.decode_identity_assertions", false) .unwrap(); - let context = Context::new().with_settings(settings).unwrap(); + let context = Context::new() + .with_settings(settings) + .unwrap() + .into_shared(); + crate::Reader::from_shared_context(&context) + .with_stream_async(format, source) + .await + .unwrap() +} +#[c2pa_test_async] +async fn adobe_connected_identities() { let format = "image/jpeg"; let test_image = include_bytes!("../fixtures/claim_aggregation/adobe_connected_identities.jpg"); let mut test_image = Cursor::new(test_image); - let reader = Reader::from_context(context) - .with_stream(format, &mut test_image) - .unwrap(); + let reader = read_manifest_no_trust(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); let manifest = reader.active_manifest().unwrap(); @@ -123,19 +135,12 @@ async fn adobe_connected_identities() { #[c2pa_test_async] async fn ims_multiple_manifests() { - let settings = Settings::new() - .with_value("verify.verify_trust", false) - .unwrap(); - let context = Context::new().with_settings(settings).unwrap(); - let format = "image/jpeg"; let test_image = include_bytes!("../fixtures/claim_aggregation/ims_multiple_manifests.jpg"); let mut test_image = Cursor::new(test_image); - let reader = Reader::from_context(context) - .with_stream(format, &mut test_image) - .unwrap(); + let reader = read_manifest_no_trust(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); // Check the summary report for the entire manifest store. diff --git a/sdk/src/identity/tests/claim_aggregation/validation.rs b/sdk/src/identity/tests/claim_aggregation/validation.rs index 35a086c3c..ef35d8b77 100644 --- a/sdk/src/identity/tests/claim_aggregation/validation.rs +++ b/sdk/src/identity/tests/claim_aggregation/validation.rs @@ -30,7 +30,6 @@ use crate::{ IdentityAssertion, ValidationError, }, status_tracker::{LogKind, StatusTracker}, - Reader, }; #[c2pa_test_async] @@ -54,9 +53,7 @@ async fn success_case() { let mut test_image = Cursor::new(test_image); - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = crate::identity::tests::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); let manifest = reader.active_manifest().unwrap(); @@ -120,9 +117,7 @@ async fn invalid_cose_sign1() { let mut test_image = Cursor::new(test_image); - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = crate::identity::tests::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); let manifest = reader.active_manifest().unwrap(); @@ -202,9 +197,7 @@ async fn invalid_cose_sign_alg() { let mut test_image = Cursor::new(test_image); - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = crate::identity::tests::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); let manifest = reader.active_manifest().unwrap(); @@ -266,9 +259,7 @@ async fn missing_cose_sign_alg() { let mut test_image = Cursor::new(test_image); - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = crate::identity::tests::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); let manifest = reader.active_manifest().unwrap(); @@ -331,9 +322,7 @@ async fn invalid_content_type() { let mut test_image = Cursor::new(test_image); - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = crate::identity::tests::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); let manifest = reader.active_manifest().unwrap(); @@ -396,9 +385,7 @@ async fn invalid_content_type_assigned() { let mut test_image = Cursor::new(test_image); - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = crate::identity::tests::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); let manifest = reader.active_manifest().unwrap(); @@ -460,9 +447,7 @@ async fn missing_content_type() { let mut test_image = Cursor::new(test_image); - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = crate::identity::tests::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); let manifest = reader.active_manifest().unwrap(); @@ -536,9 +521,7 @@ async fn missing_vc() { let mut test_image = Cursor::new(test_image); - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = crate::identity::tests::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); let manifest = reader.active_manifest().unwrap(); @@ -596,9 +579,7 @@ async fn invalid_vc() { let mut test_image = Cursor::new(test_image); - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = crate::identity::tests::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); let manifest = reader.active_manifest().unwrap(); @@ -665,9 +646,7 @@ async fn invalid_issuer_did() { let mut test_image = Cursor::new(test_image); - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = crate::identity::tests::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); let manifest = reader.active_manifest().unwrap(); @@ -732,9 +711,7 @@ async fn unsupported_did_method() { let mut test_image = Cursor::new(test_image); - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = crate::identity::tests::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); let manifest = reader.active_manifest().unwrap(); @@ -797,9 +774,7 @@ async fn unresolvable_did() { let mut test_image = Cursor::new(test_image); - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = crate::identity::tests::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); let manifest = reader.active_manifest().unwrap(); @@ -862,9 +837,7 @@ async fn did_doc_without_assertion_method() { let mut test_image = Cursor::new(test_image); - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = crate::identity::tests::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); let manifest = reader.active_manifest().unwrap(); @@ -940,9 +913,7 @@ async fn signature_mismatch() { let mut test_image = Cursor::new(test_image); - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = crate::identity::tests::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); let manifest = reader.active_manifest().unwrap(); @@ -1010,9 +981,7 @@ async fn valid_time_stamp() { let mut test_image = Cursor::new(test_image); - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = crate::identity::tests::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); let manifest = reader.active_manifest().unwrap(); @@ -1099,9 +1068,7 @@ async fn invalid_time_stamp() { let mut test_image = Cursor::new(test_image); - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = crate::identity::tests::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); let manifest = reader.active_manifest().unwrap(); @@ -1170,9 +1137,7 @@ async fn valid_from_missing() { let mut test_image = Cursor::new(test_image); - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = crate::identity::tests::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); let manifest = reader.active_manifest().unwrap(); @@ -1240,9 +1205,7 @@ async fn valid_from_in_future() { let mut test_image = Cursor::new(test_image); - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = crate::identity::tests::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); let manifest = reader.active_manifest().unwrap(); @@ -1315,9 +1278,7 @@ async fn valid_from_after_time_stamp() { let mut test_image = Cursor::new(test_image); - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = crate::identity::tests::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); let manifest = reader.active_manifest().unwrap(); @@ -1413,9 +1374,7 @@ async fn valid_until_in_future() { let mut test_image = Cursor::new(test_image); - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = crate::identity::tests::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); let manifest = reader.active_manifest().unwrap(); @@ -1484,9 +1443,7 @@ async fn valid_until_in_past() { let mut test_image = Cursor::new(test_image); - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = crate::identity::tests::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); let manifest = reader.active_manifest().unwrap(); @@ -1574,9 +1531,7 @@ async fn signer_payload_mismatch() { let mut test_image = Cursor::new(test_image); - let reader = Reader::default() - .with_stream(format, &mut test_image) - .unwrap(); + let reader = crate::identity::tests::read_manifest(format, &mut test_image).await; assert_eq!(reader.validation_status(), None); let manifest = reader.active_manifest().unwrap(); diff --git a/sdk/src/identity/tests/fixtures/naive_credential_holder.rs b/sdk/src/identity/tests/fixtures/naive_credential_holder.rs index e5e89b960..1f47fa728 100644 --- a/sdk/src/identity/tests/fixtures/naive_credential_holder.rs +++ b/sdk/src/identity/tests/fixtures/naive_credential_holder.rs @@ -85,7 +85,7 @@ impl SignatureVerifier for NaiveSignatureVerifier { type Error = (); type Output = NaiveCredential; - async fn check_signature( + fn check_signature( &self, signer_payload: &SignerPayload, signature: &[u8], @@ -101,6 +101,15 @@ impl SignatureVerifier for NaiveSignatureVerifier { Ok(NaiveCredential {}) } } + + async fn check_signature_async( + &self, + signer_payload: &SignerPayload, + signature: &[u8], + status_tracker: &mut StatusTracker, + ) -> Result> { + self.check_signature(signer_payload, signature, status_tracker) + } } pub(crate) struct NaiveCredential {} diff --git a/sdk/src/identity/x509/x509_signature_verifier.rs b/sdk/src/identity/x509/x509_signature_verifier.rs index 96162f060..7c630bd19 100644 --- a/sdk/src/identity/x509/x509_signature_verifier.rs +++ b/sdk/src/identity/x509/x509_signature_verifier.rs @@ -46,7 +46,7 @@ impl SignatureVerifier for X509SignatureVerifier<'_> { type Error = CoseError; type Output = X509SignatureInfo; - async fn check_signature( + fn check_signature( &self, signer_payload: &SignerPayload, signature: &[u8], @@ -74,6 +74,52 @@ impl SignatureVerifier for X509SignatureVerifier<'_> { let cose_sign1 = parse_cose_sign1(signature, &signer_payload_cbor, status_tracker)?; + let cert_info = self + .cose_verifier + .verify_signature(signature, &signer_payload_cbor, &[], None, status_tracker) + .map_err(|e| match e { + CoseError::RawSignatureValidationError( + RawSignatureValidationError::SignatureMismatch, + ) => ValidationError::SignatureMismatch, + + e => ValidationError::SignatureError(e), + })?; + + Ok(X509SignatureInfo { + signer_payload: signer_payload.clone(), + cose_sign1, + cert_info, + }) + } + + async fn check_signature_async( + &self, + signer_payload: &SignerPayload, + signature: &[u8], + status_tracker: &mut StatusTracker, + ) -> Result> { + if signer_payload.sig_type != super::CAWG_X509_SIG_TYPE { + log_current_item!( + "unsupported signature type", + "X509SignatureVerifier::check_signature_async" + ) + .validation_status("cawg.identity.sig_type.unknown") + .failure_no_throw( + status_tracker, + ValidationError::::UnknownSignatureType(signer_payload.sig_type.clone()), + ); + + return Err(ValidationError::UnknownSignatureType( + signer_payload.sig_type.clone(), + )); + } + + let mut signer_payload_cbor: Vec = vec![]; + c2pa_cbor::to_writer(&mut signer_payload_cbor, signer_payload) + .map_err(|_| ValidationError::InternalError("CBOR serialization error".to_string()))?; + + let cose_sign1 = parse_cose_sign1(signature, &signer_payload_cbor, status_tracker)?; + let cert_info = self .cose_verifier .verify_signature_async(signature, &signer_payload_cbor, &[], None, status_tracker) diff --git a/sdk/tests/integration.rs b/sdk/tests/integration.rs index 40a47ff02..928444a3c 100644 --- a/sdk/tests/integration.rs +++ b/sdk/tests/integration.rs @@ -333,15 +333,7 @@ mod integration_1 { // Read back the new file with embedded manifest. let mut file = std::fs::File::open(&output_path)?; - let mut reader = - Reader::from_shared_context(&context).with_stream("image/jpeg", &mut file)?; - - reader - .post_validate_async(&c2pa::identity::validator::CawgValidator {}) - .await - .unwrap(); - - dbg!(&reader); + let reader = Reader::from_shared_context(&context).with_stream("image/jpeg", &mut file)?; // The test credentials are currently flagged as untrusted. // This will be fixed when https://github.com/contentauth/c2pa-rs/pull/1356 From 9822039b57133b637995b941d7c43e69164ff831 Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Tue, 12 May 2026 20:32:51 -0700 Subject: [PATCH 5/6] chore: update post_validate test to ensure it keeps working. --- sdk/src/reader.rs | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/sdk/src/reader.rs b/sdk/src/reader.rs index 5a031da16..f3a32c54e 100644 --- a/sdk/src/reader.rs +++ b/sdk/src/reader.rs @@ -1642,7 +1642,7 @@ pub mod tests { .to_string(); #[allow(clippy::single_match)] match label { - "c2pa.actions" => { + "c2pa.actions.v2" | "c2pa.actions" => { let actions = assertion.to_assertion::()?; // build a comma separated string list of actions let desc = actions @@ -1669,8 +1669,44 @@ pub mod tests { reader.post_validate(&TestValidator {})?; - println!("{reader}"); - //Err(Error::NotImplemented("foo".to_string())) + // Verify the validator replaced c2pa.actions assertion data in the JSON output. + let json: Value = serde_json::from_str(&reader.json()).unwrap(); + let active_label = json["active_manifest"].as_str().unwrap(); + let assertions = json["manifests"][active_label]["assertions"] + .as_array() + .unwrap(); + let actions_assertion = assertions + .iter() + .find(|a| { + matches!( + a["label"].as_str(), + Some("c2pa.actions.v2") | Some("c2pa.actions") + ) + }) + .expect("c2pa.actions or c2pa.actions.v2 assertion not found"); + assert!( + actions_assertion["data"].is_string(), + "c2pa.actions data should be replaced with a string by the validator, got: {}", + actions_assertion["data"] + ); + + // Verify validation results contain the success statuses logged by the validator. + let results = reader + .validation_results() + .expect("validation results should exist after post_validate"); + let active = results + .active_manifest() + .expect("active manifest statuses should exist"); + let success_codes: Vec<&str> = active.success().iter().map(|s| s.code()).collect(); + assert!( + success_codes.contains(&"cai.test.action"), + "expected cai.test.action in success statuses, got: {success_codes:?}" + ); + assert!( + success_codes.contains(&"cai.test.something"), + "expected cai.test.something in success statuses, got: {success_codes:?}" + ); + Ok(()) } From f451ff170b9e37fdff3b962112fe25b882642a1c Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Tue, 12 May 2026 20:33:11 -0700 Subject: [PATCH 6/6] chore: add .claude to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a6ddc6bd5..e0712dadd 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ .cargo/config.toml .vscode +.claude /semver-checks/target/ sdk/target/