diff --git a/Cargo.lock b/Cargo.lock index 4e8d4a175..b70b5c833 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -877,7 +877,7 @@ dependencies = [ [[package]] name = "cawg-identity" -version = "0.7.0" +version = "0.8.0" dependencies = [ "async-trait", "base64 0.22.1", @@ -982,9 +982,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.28" +version = "4.5.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff" +checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184" dependencies = [ "clap_builder", "clap_derive", @@ -992,9 +992,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.27" +version = "4.5.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" +checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9" dependencies = [ "anstream", "anstyle", diff --git a/cawg_identity/CHANGELOG.md b/cawg_identity/CHANGELOG.md index 0d4b8efee..3c870cacb 100644 --- a/cawg_identity/CHANGELOG.md +++ b/cawg_identity/CHANGELOG.md @@ -6,6 +6,13 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm The format of this changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [0.8.0](https://github.com/contentauth/c2pa-rs/compare/cawg-identity-v0.7.0...cawg-identity-v0.8.0) +_12 February 2025_ + +### Added + +* *(cawg_identity)* Add new functions for generating a `Serialize`-able report for entire manifest store (#920) + ## [0.7.0](https://github.com/contentauth/c2pa-rs/compare/cawg-identity-v0.6.1...cawg-identity-v0.7.0) _11 February 2025_ diff --git a/cawg_identity/Cargo.toml b/cawg_identity/Cargo.toml index 359ed1e7d..631bba065 100644 --- a/cawg_identity/Cargo.toml +++ b/cawg_identity/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cawg-identity" -version = "0.7.0" +version = "0.8.0" description = "Rust SDK for CAWG (Creator Assertions Working Group) identity assertion" authors = [ "Eric Scouten ", @@ -24,6 +24,9 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(test)'] } all-features = true rustdoc-args = ["--cfg", "docsrs"] +[features] +v1_api = ["c2pa/v1_api"] + [dependencies] async-trait = "0.1.78" base64 = "0.22.1" diff --git a/cawg_identity/src/identity_assertion/assertion.rs b/cawg_identity/src/identity_assertion/assertion.rs index 0928d9e3a..51fce7ee7 100644 --- a/cawg_identity/src/identity_assertion/assertion.rs +++ b/cawg_identity/src/identity_assertion/assertion.rs @@ -11,15 +11,21 @@ // specific language governing permissions and limitations under // each license. -use std::fmt::{Debug, Formatter}; +use std::{ + collections::BTreeMap, + fmt::{Debug, Formatter}, +}; -use c2pa::Manifest; +use c2pa::{Manifest, Reader}; use serde::{Deserialize, Serialize}; use serde_bytes::ByteBuf; use crate::{ identity_assertion::{ - report::{IdentityAssertionReport, IdentityAssertionsForManifest, SignerPayloadReport}, + report::{ + IdentityAssertionReport, IdentityAssertionsForManifest, + IdentityAssertionsForManifestStore, SignerPayloadReport, + }, signer_payload::SignerPayload, }, internal::debug_byte_slice::DebugByteSlice, @@ -116,6 +122,15 @@ impl IdentityAssertion { manifest: &Manifest, verifier: &SV, ) -> impl Serialize { + Self::summarize_all_impl(manifest, verifier).await + } + + pub(crate) async fn summarize_all_impl( + manifest: &Manifest, + verifier: &SV, + ) -> IdentityAssertionsForManifest< + <::Output as ToCredentialSummary>::CredentialSummary, + > { // NOTE: We can't write this using .map(...).collect() because there are async // calls. let mut reports: Vec< @@ -142,6 +157,65 @@ impl IdentityAssertion { } } + /// Summarize all of the identity assertions found for a [`ManifestStore`]. + /// + /// [`ManifestStore`]: c2pa::ManifestStore + #[cfg(feature = "v1_api")] + pub async fn summarize_manifest_store( + store: &c2pa::ManifestStore, + verifier: &SV, + ) -> impl Serialize { + // NOTE: We can't write this using .map(...).collect() because there are async + // calls. + let mut reports: BTreeMap< + String, + IdentityAssertionsForManifest< + <::Output as ToCredentialSummary>::CredentialSummary, + >, + > = BTreeMap::new(); + + for (id, manifest) in store.manifests() { + let report = Self::summarize_all_impl(manifest, verifier).await; + reports.insert(id.clone(), report); + } + + IdentityAssertionsForManifestStore::< + <::Output as ToCredentialSummary>::CredentialSummary, + > { + assertions_for_manifest: reports, + } + } + + /// Summarize all of the identity assertions found for a [`Reader`]. + pub async fn summarize_from_reader( + reader: &Reader, + verifier: &SV, + ) -> impl Serialize { + // NOTE: We can't write this using .map(...).collect() because there are async + // calls. + let mut reports: BTreeMap< + String, + IdentityAssertionsForManifest< + <::Output as ToCredentialSummary>::CredentialSummary, + >, + > = BTreeMap::new(); + + for manifest in reader.iter_manifests() { + let report = Self::summarize_all_impl(manifest, verifier).await; + + // TO DO: What to do if manifest doesn't have a label? + if let Some(label) = manifest.label() { + reports.insert(label.to_owned(), report); + } + } + + IdentityAssertionsForManifestStore::< + <::Output as ToCredentialSummary>::CredentialSummary, + > { + assertions_for_manifest: reports, + } + } + /// Using the provided [`SignatureVerifier`], check the validity of this /// identity assertion. /// diff --git a/cawg_identity/src/identity_assertion/report.rs b/cawg_identity/src/identity_assertion/report.rs index 98f165572..af4451bd4 100644 --- a/cawg_identity/src/identity_assertion/report.rs +++ b/cawg_identity/src/identity_assertion/report.rs @@ -11,10 +11,33 @@ // specific language governing permissions and limitations under // each license. -use serde::{ser::SerializeSeq, Serialize}; +use std::collections::BTreeMap; + +use serde::{ + ser::{SerializeMap, SerializeSeq}, + Serialize, +}; use crate::identity_assertion::signer_payload::SignerPayload; +#[doc(hidden)] +pub struct IdentityAssertionsForManifestStore { + pub(crate) assertions_for_manifest: BTreeMap>, +} + +impl Serialize for IdentityAssertionsForManifestStore { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut map = serializer.serialize_map(Some(self.assertions_for_manifest.len()))?; + for (manifest_id, report) in self.assertions_for_manifest.iter() { + map.serialize_entry(manifest_id, report)?; + } + map.end() + } +} + #[doc(hidden)] pub struct IdentityAssertionsForManifest { pub(crate) assertion_reports: Vec>, diff --git a/cawg_identity/src/tests/claim_aggregation/interop.rs b/cawg_identity/src/tests/claim_aggregation/interop.rs index c6dd74687..50b6df71c 100644 --- a/cawg_identity/src/tests/claim_aggregation/interop.rs +++ b/cawg_identity/src/tests/claim_aggregation/interop.rs @@ -33,10 +33,10 @@ async fn adobe_connected_identities() { let mut test_image = Cursor::new(test_image); - let manifest_store = Reader::from_stream(format, &mut test_image).unwrap(); - assert_eq!(manifest_store.validation_status(), None); + let reader = Reader::from_stream(format, &mut test_image).unwrap(); + assert_eq!(reader.validation_status(), None); - let manifest = manifest_store.active_manifest().unwrap(); + let manifest = reader.active_manifest().unwrap(); let mut ia_iter = IdentityAssertion::from_manifest(manifest); // Should find exactly one identity assertion. @@ -82,6 +82,15 @@ async fn adobe_connected_identities() { } ); + // Check the summary report for the entire manifest store. + let ia_summary = IdentityAssertion::summarize_from_reader(&reader, &isv).await; + let ia_json = serde_json::to_string(&ia_summary).unwrap(); + + assert_eq!( + ia_json, + r#"{"urn:uuid:b55062ef-96b6-4f6e-bb7d-9c415f130471":[{"sig_type":"cawg.identity_claims_aggregation","referenced_assertions":["c2pa.hash.data"],"named_actor":{"@context":["https://www.w3.org/ns/credentials/v2","https://creator-assertions.github.io/tbd/tbd"],"type":["VerifiableCredential","IdentityClaimsAggregationCredential"],"issuer":"did:web:connected-identities.identity-stage.adobe.com","validFrom":"2024-10-03T21:47:02Z","verifiedIdentities":[{"type":"cawg.social_media","username":"Robert Tiles","uri":"https://net.s2stagehance.com/roberttiles","verifiedAt":"2024-09-24T18:15:11Z","provider":{"id":"https://behance.net","name":"behance"}}],"credentialSchema":[{"id":"https://creator-assertions.github.io/schemas/v1/creator-identity-assertion.json","type":"JSONSchema"}]}}]}"# + ); + // Check the summary report for this manifest. let ia_summary = IdentityAssertion::summarize_all(manifest, &isv).await; let ia_json = serde_json::to_string(&ia_summary).unwrap(); @@ -91,3 +100,25 @@ async fn adobe_connected_identities() { r#"[{"sig_type":"cawg.identity_claims_aggregation","referenced_assertions":["c2pa.hash.data"],"named_actor":{"@context":["https://www.w3.org/ns/credentials/v2","https://creator-assertions.github.io/tbd/tbd"],"type":["VerifiableCredential","IdentityClaimsAggregationCredential"],"issuer":"did:web:connected-identities.identity-stage.adobe.com","validFrom":"2024-10-03T21:47:02Z","verifiedIdentities":[{"type":"cawg.social_media","username":"Robert Tiles","uri":"https://net.s2stagehance.com/roberttiles","verifiedAt":"2024-09-24T18:15:11Z","provider":{"id":"https://behance.net","name":"behance"}}],"credentialSchema":[{"id":"https://creator-assertions.github.io/schemas/v1/creator-identity-assertion.json","type":"JSONSchema"}]}}]"# ); } + +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +async fn ims_multiple_manifests() { + 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_stream(format, &mut test_image).unwrap(); + assert_eq!(reader.validation_status(), None); + + // Check the summary report for the entire manifest store. + let isv = IcaSignatureVerifier {}; + let ia_summary = IdentityAssertion::summarize_from_reader(&reader, &isv).await; + let ia_json = serde_json::to_string(&ia_summary).unwrap(); + + assert_eq!( + ia_json, + r#"{"urn:uuid:7256ca36-2a90-44ec-914d-f17c8d70c31f":[{"sig_type":"cawg.identity_claims_aggregation","referenced_assertions":["c2pa.hash.data"],"named_actor":{"@context":["https://www.w3.org/ns/credentials/v2","https://creator-assertions.github.io/tbd/tbd"],"type":["VerifiableCredential","IdentityClaimsAggregationCredential"],"issuer":"did:web:connected-identities.identity-stage.adobe.com","validFrom":"2025-02-13T00:40:47Z","verifiedIdentities":[{"type":"cawg.social_media","username":"firstlast555","uri":"https://net.s2stagehance.com/firstlast555","verifiedAt":"2025-01-10T19:53:59Z","provider":{"id":"https://behance.net","name":"behance"}}],"credentialSchema":[{"id":"https://cawg.io/schemas/v1/creator-identity-assertion.json","type":"JSONSchema"}]}}],"urn:uuid:b55062ef-96b6-4f6e-bb7d-9c415f130471":[{"sig_type":"cawg.identity_claims_aggregation","referenced_assertions":["c2pa.hash.data"],"named_actor":{"@context":["https://www.w3.org/ns/credentials/v2","https://creator-assertions.github.io/tbd/tbd"],"type":["VerifiableCredential","IdentityClaimsAggregationCredential"],"issuer":"did:web:connected-identities.identity-stage.adobe.com","validFrom":"2024-10-03T21:47:02Z","verifiedIdentities":[{"type":"cawg.social_media","username":"Robert Tiles","uri":"https://net.s2stagehance.com/roberttiles","verifiedAt":"2024-09-24T18:15:11Z","provider":{"id":"https://behance.net","name":"behance"}}],"credentialSchema":[{"id":"https://creator-assertions.github.io/schemas/v1/creator-identity-assertion.json","type":"JSONSchema"}]}}]}"# + ); +} diff --git a/cawg_identity/src/tests/fixtures/claim_aggregation/ims_multiple_manifests.jpg b/cawg_identity/src/tests/fixtures/claim_aggregation/ims_multiple_manifests.jpg new file mode 100644 index 000000000..725613782 Binary files /dev/null and b/cawg_identity/src/tests/fixtures/claim_aggregation/ims_multiple_manifests.jpg differ