diff --git a/cawg_identity/src/builder/identity_assertion_builder.rs b/cawg_identity/src/builder/identity_assertion_builder.rs index fb9043d1c..6eeacbc38 100644 --- a/cawg_identity/src/builder/identity_assertion_builder.rs +++ b/cawg_identity/src/builder/identity_assertion_builder.rs @@ -12,7 +12,7 @@ // each license. use async_trait::async_trait; -use c2pa::{AsyncDynamicAssertion, DynamicAssertion, PreliminaryClaim}; +use c2pa::{AsyncDynamicAssertion, DynamicAssertion, DynamicAssertionContent, PreliminaryClaim}; use serde_bytes::ByteBuf; use super::{CredentialHolder, IdentityBuilderError}; @@ -50,8 +50,8 @@ impl DynamicAssertion for IdentityAssertionBuilder { "cawg.identity".to_string() } - fn reserve_size(&self) -> usize { - self.credential_holder.reserve_size() + fn reserve_size(&self) -> c2pa::Result { + Ok(self.credential_holder.reserve_size()) // TO DO: Credential holder will state reserve size for signature. // Add additional size for CBOR wrapper outside signature. } @@ -61,7 +61,7 @@ impl DynamicAssertion for IdentityAssertionBuilder { _label: &str, size: Option, claim: &PreliminaryClaim, - ) -> c2pa::Result> { + ) -> c2pa::Result { // TO DO: Better filter for referenced assertions. // For now, just require hard binding. @@ -120,8 +120,8 @@ impl AsyncDynamicAssertion for AsyncIdentityAssertionBuilder { "cawg.identity".to_string() } - fn reserve_size(&self) -> usize { - self.credential_holder.reserve_size() + fn reserve_size(&self) -> c2pa::Result { + Ok(self.credential_holder.reserve_size()) // TO DO: Credential holder will state reserve size for signature. // Add additional size for CBOR wrapper outside signature. } @@ -131,7 +131,7 @@ impl AsyncDynamicAssertion for AsyncIdentityAssertionBuilder { _label: &str, size: Option, claim: &PreliminaryClaim, - ) -> c2pa::Result> { + ) -> c2pa::Result { // TO DO: Better filter for referenced assertions. // For now, just require hard binding. @@ -158,7 +158,7 @@ fn finalize_identity_assertion( signer_payload: SignerPayload, size: Option, signature_result: Result, IdentityBuilderError>, -) -> c2pa::Result> { +) -> c2pa::Result { // TO DO: Think through how errors map into c2pa::Error. let signature = signature_result.map_err(|e| c2pa::Error::BadParam(e.to_string()))?; @@ -202,5 +202,5 @@ fn finalize_identity_assertion( assert_eq!(assertion_size, assertion_cbor.len()); } - Ok(assertion_cbor) + Ok(DynamicAssertionContent::Cbor(assertion_cbor)) } diff --git a/sdk/src/dynamic_assertion.rs b/sdk/src/dynamic_assertion.rs index 25f9663bf..cf509340d 100644 --- a/sdk/src/dynamic_assertion.rs +++ b/sdk/src/dynamic_assertion.rs @@ -19,6 +19,18 @@ use async_trait::async_trait; use crate::{hashed_uri::HashedUri, Result}; +/// The type of content that can be returned by a [`DynamicAssertion`] content call. +pub enum DynamicAssertionContent { + /// The assertion is a CBOR-encoded binary blob. + Cbor(Vec), + + /// The assertion is a JSON-encoded string. + Json(String), + + /// The assertion is a binary blob with a content type. + Binary(String, Vec), +} + /// A `DynamicAssertion` is an assertion that has the ability to adjust /// its content based on other assertions within the overall [`Manifest`]. /// @@ -41,7 +53,7 @@ pub trait DynamicAssertion { /// assertion). /// /// [`Builder`]: crate::Builder - fn reserve_size(&self) -> usize; + fn reserve_size(&self) -> Result; /// Return the final assertion content. /// @@ -61,7 +73,7 @@ pub trait DynamicAssertion { label: &str, size: Option, claim: &PreliminaryClaim, - ) -> Result>; + ) -> Result; } /// An `AsyncDynamicAssertion` is an assertion that has the ability @@ -89,7 +101,7 @@ pub trait AsyncDynamicAssertion: Sync { /// assertion). /// /// [`Builder`]: crate::Builder - fn reserve_size(&self) -> usize; + fn reserve_size(&self) -> Result; /// Return the final assertion content. /// @@ -109,7 +121,7 @@ pub trait AsyncDynamicAssertion: Sync { label: &str, size: Option, claim: &PreliminaryClaim, - ) -> Result>; + ) -> Result; } /// An `AsyncDynamicAssertion` is an assertion that has the ability @@ -137,7 +149,7 @@ pub trait AsyncDynamicAssertion { /// assertion). /// /// [`Builder`]: crate::Builder - fn reserve_size(&self) -> usize; + fn reserve_size(&self) -> Result; /// Return the final assertion content. /// @@ -157,7 +169,7 @@ pub trait AsyncDynamicAssertion { label: &str, size: Option, claim: &PreliminaryClaim, - ) -> Result>; + ) -> Result; } /// Describes information from the preliminary C2PA Claim that may diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 7bc2a88ed..b7fd0966c 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -107,7 +107,9 @@ pub use builder::{Builder, ManifestDefinition}; pub use c2pa_crypto::raw_signature::SigningAlg; pub use callback_signer::{CallbackFunc, CallbackSigner}; pub use claim_generator_info::ClaimGeneratorInfo; -pub use dynamic_assertion::{AsyncDynamicAssertion, DynamicAssertion, PreliminaryClaim}; +pub use dynamic_assertion::{ + AsyncDynamicAssertion, DynamicAssertion, DynamicAssertionContent, PreliminaryClaim, +}; pub use error::{Error, Result}; pub use external_manifest::ManifestPatchCallback; pub use hash_utils::{hash_stream_by_alg, HashRange}; diff --git a/sdk/src/store.rs b/sdk/src/store.rs index 7aa5ccf26..a6f8ae239 100644 --- a/sdk/src/store.rs +++ b/sdk/src/store.rs @@ -44,7 +44,7 @@ use crate::{ assertions::{ labels::{self, CLAIM}, BmffHash, DataBox, DataHash, DataMap, ExclusionsMap, Ingredient, Relationship, SubsetMap, - UserCbor, + User, UserCbor, }, asset_io::{ CAIRead, CAIReadWrite, HashBlockObjectType, HashObjectPositions, RemoteRefEmbedType, @@ -55,7 +55,9 @@ use crate::{ }, cose_sign::{cose_sign, cose_sign_async}, cose_validator::{verify_cose, verify_cose_async}, - dynamic_assertion::{AsyncDynamicAssertion, DynamicAssertion, PreliminaryClaim}, + dynamic_assertion::{ + AsyncDynamicAssertion, DynamicAssertion, DynamicAssertionContent, PreliminaryClaim, + }, error::{Error, Result}, external_manifest::ManifestPatchCallback, hash_utils::{hash_by_alg, vec_compare, verify_by_alg}, @@ -2042,7 +2044,7 @@ impl Store { // Two passes since we are accessing two fields in self. let mut assertions = Vec::new(); for da in dyn_assertions.iter() { - let reserve_size = da.reserve_size(); + let reserve_size = da.reserve_size()?; let data1 = serde_cbor::ser::to_vec_packed(&vec![0; reserve_size])?; let cbor_delta = data1.len() - reserve_size; let da_data = serde_cbor::ser::to_vec_packed(&vec![0; reserve_size - cbor_delta])?; @@ -2080,16 +2082,24 @@ impl Store { let label = crate::jumbf::labels::assertion_label_from_uri(&uri.url()) .ok_or(Error::BadParam("write_dynamic_assertions".to_string()))?; - let da_size = da.reserve_size(); + let da_size = da.reserve_size()?; let da_data = if _sync { da.content(&label, Some(da_size), preliminary_claim)? } else { da.content(&label, Some(da_size), preliminary_claim).await? }; - // TO DO: Add new assertion to preliminary_claim - // todo: support for non-CBOR asssertions? - final_assertions.push(UserCbor::new(&label, da_data).to_assertion()?); + match da_data { + DynamicAssertionContent::Cbor(data) => { + final_assertions.push(UserCbor::new(&label, data).to_assertion()?); + } + DynamicAssertionContent::Json(data) => { + final_assertions.push(User::new(&label, &data).to_assertion()?); + } + DynamicAssertionContent::Binary(format, data) => { + todo!("Binary dynamic assertions not yet supported"); + } + } } let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; @@ -6137,11 +6147,11 @@ pub mod tests { "com.mycompany.myassertion".to_string() } - fn reserve_size(&self) -> usize { + fn reserve_size(&self) -> Result { let assertion = TestAssertion { my_tag: "some value I will replace".to_string(), }; - serde_cbor::to_vec(&assertion).unwrap().len() + Ok(serde_cbor::to_vec(&assertion)?.len()) } fn content( @@ -6149,7 +6159,7 @@ pub mod tests { _label: &str, _size: Option, claim: &PreliminaryClaim, - ) -> Result> { + ) -> Result { assert!(claim .assertions() .inspect(|a| { @@ -6161,7 +6171,9 @@ pub mod tests { my_tag: "some value I will replace".to_string(), }; - Ok(serde_cbor::to_vec(&assertion).unwrap()) + Ok(DynamicAssertionContent::Cbor( + serde_cbor::to_vec(&assertion).unwrap(), + )) } } @@ -6268,11 +6280,11 @@ pub mod tests { "com.mycompany.myassertion".to_string() } - fn reserve_size(&self) -> usize { + fn reserve_size(&self) -> Result { let assertion = TestAssertion { my_tag: "some value I will replace".to_string(), }; - serde_cbor::to_vec(&assertion).unwrap().len() + Ok(serde_cbor::to_vec(&assertion)?.len()) } async fn content( @@ -6280,7 +6292,7 @@ pub mod tests { _label: &str, _size: Option, claim: &PreliminaryClaim, - ) -> Result> { + ) -> Result { assert!(claim .assertions() .inspect(|a| { @@ -6292,7 +6304,9 @@ pub mod tests { my_tag: "some value I will replace".to_string(), }; - Ok(serde_cbor::to_vec(&assertion).unwrap()) + Ok(DynamicAssertionContent::Cbor( + serde_cbor::to_vec(&assertion).unwrap(), + )) } } diff --git a/sdk/tests/test_builder.rs b/sdk/tests/test_builder.rs index e1ced9ace..5616c6863 100644 --- a/sdk/tests/test_builder.rs +++ b/sdk/tests/test_builder.rs @@ -182,3 +182,129 @@ fn test_builder_embedded_v1_otgp() -> Result<()> { Ok(()) } + +#[test] +#[cfg_attr(not(any(target_arch = "wasm32", feature = "openssl")), ignore)] +fn test_dynamic_assertions_builder() -> Result<()> { + use c2pa::{ + // assertions::{CreativeWork, SchemaDotOrgPerson}, + DynamicAssertion, + DynamicAssertionContent, + PreliminaryClaim, + Signer, + SigningAlg, + }; + use serde::Serialize; + #[derive(Serialize)] + struct TestAssertion { + my_tag: String, + } + + #[derive(Debug)] + struct TestDynamicAssertion {} + + impl DynamicAssertion for TestDynamicAssertion { + fn label(&self) -> String { + //CreativeWork::LABEL.to_string() + "com.mycompany.myassertion".to_string() + } + + fn reserve_size(&self) -> Result { + let assertion = TestAssertion { + my_tag: "some value I will replace".to_string(), + }; + // let assertion = CreativeWork::new() + // .add_author(SchemaDotOrgPerson::new().set_name("me").unwrap()) + // .unwrap(); + Ok(serde_json::to_string(&assertion)?.len()) + } + + fn content( + &self, + _label: &str, + _size: Option, + claim: &PreliminaryClaim, + ) -> Result { + assert!(claim + .assertions() + .inspect(|a| { + dbg!(a); + }) + .any(|a| a.url().contains("c2pa.hash"))); + + // let assertion = + // CreativeWork::new().add_author(SchemaDotOrgPerson::new().set_name("me")?)?; + + let assertion = TestAssertion { + my_tag: "some value I will replace".to_string(), + }; + + Ok(DynamicAssertionContent::Json(serde_json::to_string( + &assertion, + )?)) + } + } + + /// This is a Signer wrapped around a local temp signer, + /// that implements the DynamicAssertion trait. + struct DynamicSigner(Box); + + impl DynamicSigner { + fn new() -> Self { + Self(Box::new(test_signer())) + } + } + + impl Signer for DynamicSigner { + fn sign(&self, data: &[u8]) -> Result> { + self.0.sign(data) + } + + fn alg(&self) -> SigningAlg { + self.0.alg() + } + + fn certs(&self) -> crate::Result>> { + self.0.certs() + } + + fn reserve_size(&self) -> usize { + self.0.reserve_size() + } + + fn time_authority_url(&self) -> Option { + self.0.time_authority_url() + } + + fn ocsp_val(&self) -> Option> { + self.0.ocsp_val() + } + + // Returns our dynamic assertion here. + fn dynamic_assertions(&self) -> Vec> { + vec![Box::new(TestDynamicAssertion {})] + } + } + + let manifest_def = std::fs::read_to_string(fixtures_path("simple_manifest.json"))?; + let mut builder = Builder::from_json(&manifest_def)?; + + const TEST_IMAGE: &[u8] = include_bytes!("fixtures/CA.jpg"); + let format = "image/jpeg"; + let mut source = Cursor::new(TEST_IMAGE); + + let mut dest = Cursor::new(Vec::new()); + + let signer = DynamicSigner::new(); + builder.sign(&signer, format, &mut source, &mut dest)?; + + dest.set_position(0); + + let reader = Reader::from_stream(format, &mut dest).unwrap(); + + println!("reader: {}", reader); + + assert_ne!(reader.validation_state(), ValidationState::Invalid); + + Ok(()) +}