c2pa-rs

A fork of https://github.com/contentauth/c2pa-rs/
git clone git://archive.git.mtrnord.blog/mtrnords-photography-manager/c2pa-rs.git
Log | Files | Refs | README

commit 66a872d37239f7833512da725dc1a1ba4fe34ba7
parent 089f27f2e8b2791cc9bf5c1ee85eb41f38a51b37
Author: mauricefisher64 <92736594+mauricefisher64@users.noreply.github.com>
Date:   Mon,  6 Feb 2023 08:54:36 -0500

Update Ingredient and VC hashes to 1.2 spec (#184)

* 1.2 manifest hash support
1.2 VC hash support
Salt VC for 1.2 redaction support

* cleanup and unit test

* cleanup comments

* Fix function name typo

* Make sure we preserve salt for all paths by adding to HashedUri
Diffstat:
Msdk/src/claim.rs | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msdk/src/hashed_uri.rs | 13+++++++++++++
Msdk/src/ingredient.rs | 7+++++--
Msdk/src/jumbf/boxes.rs | 13++++++++++++-
Msdk/src/store.rs | 281++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Asdk/tests/fixtures/legacy_ingredient_hash.jpg | 0
6 files changed, 301 insertions(+), 104 deletions(-)

diff --git a/sdk/src/claim.rs b/sdk/src/claim.rs @@ -23,7 +23,11 @@ use crate::{ get_thumbnail_image_type, get_thumbnail_instance, get_thumbnail_type, Assertion, AssertionBase, AssertionData, }, - assertions::{self, labels, BmffHash, DataHash}, + assertions::{ + self, + labels::{self, CLAIM}, + BmffHash, DataHash, + }, cose_validator::{get_signing_info, verify_cose, verify_cose_async}, error::{Error, Result}, hashed_uri::HashedUri, @@ -32,8 +36,9 @@ use crate::{ boxes::{ CAICBORAssertionBox, CAIJSONAssertionBox, CAIUUIDAssertionBox, JumbfEmbeddedFileBox, }, + labels::{ASSERTIONS, CREDENTIALS, SIGNATURE}, }, - salt::{SaltGenerator, NO_SALT}, + salt::{DefaultSalt, SaltGenerator, NO_SALT}, status_tracker::{log_item, OneShotStatusTracker, StatusTracker}, utils::hash_utils::{hash_by_alg, vec_compare, verify_by_alg}, validation_status, @@ -200,7 +205,7 @@ pub struct Claim { // Internal list of verifiable credentials for claim. // These are serialized manually based on need. #[serde(skip_deserializing, skip_serializing)] - vc_store: Vec<AssertionData>, + vc_store: Vec<(HashedUri, AssertionData)>, claim_generator: String, // generator of this claim @@ -211,6 +216,10 @@ pub struct Claim { #[serde(skip_deserializing, skip_serializing)] original_bytes: Option<Vec<u8>>, + // original JUMBF box order need to recalculate JUMBF box hash + #[serde(skip_deserializing, skip_serializing)] + original_box_order: Option<Vec<&'static str>>, + #[serde(skip_serializing_if = "Option::is_none")] redacted_assertions: Option<Vec<String>>, // list of redacted assertions @@ -294,6 +303,7 @@ impl Claim { vc_store: Vec::new(), assertions: Vec::new(), original_bytes: None, + original_box_order: None, redacted_assertions: None, alg: Some(BUILD_HASH_ALG.to_string()), alg_soft: None, @@ -325,6 +335,7 @@ impl Claim { vc_store: Vec::new(), assertions: Vec::new(), original_bytes: None, + original_box_order: None, redacted_assertions: None, alg: Some(BUILD_HASH_ALG.into()), alg_soft: None, @@ -413,6 +424,22 @@ impl Claim { self.title.as_ref() } + /// order for which to generate the JUMBF boxes with writing manifest + pub fn set_box_order(&mut self, box_order: Vec<&'static str>) { + self.original_box_order = Some(box_order); + } + + /// order to process + pub fn get_box_order(&self) -> &[&str] { + const DEFAULT_MANIFEST_ORDER: [&str; 4] = [ASSERTIONS, CLAIM, SIGNATURE, CREDENTIALS]; + + if let Some(bo) = &self.original_box_order { + bo + } else { + &DEFAULT_MANIFEST_ORDER + } + } + /// get algorithm pub fn alg(&self) -> &str { match self.alg.as_ref() { @@ -503,7 +530,7 @@ impl Claim { self.claim_generator_hints.as_ref() } - pub fn calc_box_hash( + pub fn calc_assertion_box_hash( label: &str, assertion: &Assertion, salt: Option<Vec<u8>>, @@ -578,13 +605,14 @@ impl Claim { // Get salted hash of the assertion's contents. let salt = salt_generator.generate_salt(); - let hash = Claim::calc_box_hash(&as_label, &assertion, salt.clone(), self.alg())?; + let hash = Claim::calc_assertion_box_hash(&as_label, &assertion, salt.clone(), self.alg())?; // Build hash link. let link = jumbf::labels::to_assertion_uri(self.label(), &as_label); let link_relative = jumbf::labels::to_relative_uri(&link); - let c2pa_assertion = C2PAAssertion::new(link_relative, None, &hash); + let mut c2pa_assertion = C2PAAssertion::new(link_relative, None, &hash); + c2pa_assertion.add_salt(salt.clone()); // Add to assertion store. let (_l, instance) = Claim::assertion_label_from_link(&as_label); @@ -623,21 +651,56 @@ impl Claim { // the "id" value will be used as the label in the vcstore pub fn add_verifiable_credential(&mut self, vc_json: &str) -> Result<HashedUri> { let id = Claim::vc_id(vc_json)?; - - let hash = hash_by_alg(self.alg(), vc_json.as_bytes(), None); + let credential = AssertionData::Json(vc_json.to_string()); let link = jumbf::labels::to_verifiable_credential_uri(self.label(), &id); - let c2pa_assertion = C2PAAssertion::new(link, Some(self.alg().to_string()), &hash); + // salt box for 1.2 VC redaction support + let ds = DefaultSalt::default(); + let salt = ds.generate_salt(); + + // assertion JUMBF box hash for 1.2 validation + let assertion = Assertion::from_data_json(&id, vc_json.as_bytes())?; + let hash = Claim::calc_assertion_box_hash(&id, &assertion, salt.clone(), self.alg())?; + + let mut c2pa_assertion = C2PAAssertion::new(link, Some(self.alg().to_string()), &hash); + c2pa_assertion.add_salt(salt); // add credential to vcstore - let credential = AssertionData::Json(vc_json.to_string()); - self.vc_store.push(credential); + self.vc_store.push((c2pa_assertion.clone(), credential)); Ok(c2pa_assertion) } - pub fn get_verifiable_credentials(&self) -> &Vec<AssertionData> { + /// Load known VC with optional salt + pub fn put_verifiable_credential( + &mut self, + vc_json: &str, + salt: Option<Vec<u8>>, + ) -> Result<()> { + let id = Claim::vc_id(vc_json)?; + let credential = AssertionData::Json(vc_json.to_string()); + + let link = jumbf::labels::to_verifiable_credential_uri(self.label(), &id); + + // assertion JUMBF box hash for 1.2 validation + let assertion = Assertion::from_data_json(&id, vc_json.as_bytes())?; + let hash = Claim::calc_assertion_box_hash(&id, &assertion, salt.clone(), self.alg())?; + + let mut c2pa_assertion = C2PAAssertion::new(link, Some(self.alg().to_string()), &hash); + c2pa_assertion.add_salt(salt); + + // add credential to vcstore + self.vc_store.push((c2pa_assertion, credential)); + + Ok(()) + } + + pub fn get_verifiable_credentials(&self) -> Vec<&AssertionData> { + self.vc_store.iter().map(|t| &t.1).collect::<Vec<_>>() + } + + pub fn get_verifiable_credentials_store(&self) -> &Vec<(HashedUri, AssertionData)> { &self.vc_store } @@ -669,7 +732,7 @@ impl Claim { data_hash.pad_to_size(original_len)?; replacement_assertion = data_hash.to_assertion()?; - let replacement_hash = Claim::calc_box_hash( + let replacement_hash = Claim::calc_assertion_box_hash( &dh_assertion.label(), &replacement_assertion, dh_assertion.salt().clone(), @@ -709,7 +772,7 @@ impl Claim { Some(ref mut bmff_assertion) => { let original_hash = bmff_assertion.hash().to_vec(); - let replacement_hash = Claim::calc_box_hash( + let replacement_hash = Claim::calc_assertion_box_hash( &bmff_assertion.label(), &replacement_assertion, bmff_assertion.salt().clone(), diff --git a/sdk/src/hashed_uri.rs b/sdk/src/hashed_uri.rs @@ -24,6 +24,10 @@ pub struct HashedUri { alg: Option<String>, #[serde(with = "serde_bytes")] hash: Vec<u8>, // hash stored as cbor byte string + + // salt used to generate hash + #[serde(skip_deserializing, skip_serializing)] + salt: Option<Vec<u8>>, } impl HashedUri { @@ -32,6 +36,7 @@ impl HashedUri { url, alg, hash: hash_bytes.to_vec(), + salt: None, } } @@ -54,6 +59,14 @@ impl HashedUri { pub(crate) fn update_hash(&mut self, hash: Vec<u8>) { self.hash = hash; } + + pub fn add_salt(&mut self, salt: Option<Vec<u8>>) { + self.salt = salt; + } + + pub fn salt(&self) -> &Option<Vec<u8>> { + &self.salt + } } impl fmt::Display for HashedUri { diff --git a/sdk/src/ingredient.rs b/sdk/src/ingredient.rs @@ -687,7 +687,8 @@ impl Ingredient { }; // have Store check and load ingredients and add them to a claim - Store::load_ingredient_to_claim(claim, &manifest_label, &buffer, redactions)?; + let ingredient_store = + Store::load_ingredient_to_claim(claim, &manifest_label, &buffer, redactions)?; // get the ingredient map loaded in previous match claim.claim_ingredient(&manifest_label) { @@ -697,7 +698,9 @@ impl Ingredient { .iter() .find(|c| c.label() == manifest_label) { - let hash = ingredient_active_claim.hash(); + let hash = + ingredient_store.get_manifest_box_hash(ingredient_active_claim); // get C2PA 1.2 JUMBF box hash + let uri = jumbf::labels::to_manifest_uri(&manifest_label); // if there are validations and they have all passed, then use the parent claim thumbnail if available diff --git a/sdk/src/jumbf/boxes.rs b/sdk/src/jumbf/boxes.rs @@ -1384,6 +1384,10 @@ impl CAIStore { // we REALLY want to return a CAIAssertionStore but can't do to referencing... self.store.data_box_as_superbox(0) } + + pub fn set_salt(&mut self, salt: Vec<u8>) -> JumbfParseResult<()> { + self.store.desc_box.set_salt(salt) + } } // ANCHOR CAI Block @@ -2070,7 +2074,14 @@ impl BoxReader { None => (buf, None), } } - _ => (buf, None), + _ => { + // we do not store the trailing 0 on load + if buf[buf.len() - 1] == 0 { + buf.pop(); + } + + (buf, None) + } }; Ok(JUMBFEmbeddedFileDescriptionBox::from( diff --git a/sdk/src/store.rs b/sdk/src/store.rs @@ -23,31 +23,37 @@ use log::error; use crate::embedded_xmp; #[cfg(feature = "async_signer")] use crate::AsyncSigner; -#[cfg(feature = "sign")] use crate::{ - assertion::AssertionData, - assertions::DataHash, - asset_io::{CAIReadWrite, HashBlockObjectType, HashObjectPositions}, - cose_sign::cose_sign, - cose_validator::verify_cose, - jumbf_io::{object_locations_from_stream, save_jumbf_to_stream}, - utils::{ - hash_utils::{hash256, Exclusion}, - patch::patch_bytes, + assertion::{ + Assertion, AssertionBase, AssertionData, AssertionDecodeError, AssertionDecodeErrorCause, + }, + assertions::{ + labels::{self, CLAIM}, + Ingredient, Relationship, }, - Signer, -}; -use crate::{ - assertion::{Assertion, AssertionBase, AssertionDecodeError, AssertionDecodeErrorCause}, - assertions::{labels, Ingredient, Relationship}, claim::{Claim, ClaimAssertion, ClaimAssetData}, error::{Error, Result}, hash_utils::{hash_by_alg, vec_compare, verify_by_alg}, - jumbf::{self, boxes::*}, + jumbf::{ + self, + boxes::*, + labels::{ASSERTIONS, CREDENTIALS, SIGNATURE}, + }, jumbf_io::load_jumbf_from_memory, status_tracker::{log_item, OneShotStatusTracker, StatusTracker}, + utils::hash_utils::hash256, validation_status, ManifestStoreReport, }; +#[cfg(feature = "sign")] +use crate::{ + assertions::DataHash, + asset_io::{CAIReadWrite, HashBlockObjectType, HashObjectPositions}, + cose_sign::cose_sign, + cose_validator::verify_cose, + jumbf_io::{object_locations_from_stream, save_jumbf_to_stream}, + utils::{hash_utils::Exclusion, patch::patch_bytes}, + Signer, +}; #[cfg(feature = "file_io")] use crate::{ assertions::{BmffHash, DataMap, ExclusionsMap, SubsetMap}, @@ -66,6 +72,7 @@ const MANIFEST_STORE_EXT: &str = "c2pa"; // file extension for external manifest #[derive(Debug, PartialEq)] pub struct Store { claims_map: HashMap<String, usize>, + manifest_box_hash_cache: HashMap<String, Vec<u8>>, claims: Vec<Claim>, label: String, provenance_path: Option<String>, @@ -108,6 +115,7 @@ impl Store { pub fn new_with_label(label: &str) -> Self { Store { claims_map: HashMap::new(), + manifest_box_hash_cache: HashMap::new(), claims: Vec::new(), label: label.to_string(), provenance_path: None, @@ -143,6 +151,15 @@ impl Store { &self.claims } + /// the JUMBF manifest box hash (spec 1.2) + pub fn get_manifest_box_hash(&self, claim: &Claim) -> Vec<u8> { + if let Some(bh) = self.manifest_box_hash_cache.get(claim.label()) { + bh.clone() + } else { + Store::calc_manifest_box_hash(claim, None, claim.alg()).unwrap_or(Vec::new()) + } + } + /// Add a new Claim to this Store. The function /// will return the label of the claim. pub fn commit_claim(&mut self, mut claim: Claim) -> Result<String> { @@ -324,8 +341,7 @@ impl Store { // Returns placeholder that will be searched for and replaced // with actual signature data. - #[cfg(feature = "sign")] - fn sign_claim_placeholder(&self, claim: &Claim, min_reserve_size: usize) -> Vec<u8> { + fn sign_claim_placeholder(claim: &Claim, min_reserve_size: usize) -> Vec<u8> { let placeholder_str = format!("signature placeholder:{}", claim.label()); let mut placeholder = hash256(placeholder_str.as_bytes()).as_bytes().to_vec(); @@ -445,7 +461,6 @@ impl Store { self.claims_map.insert(label, index); } - #[cfg(feature = "sign")] fn add_assertion_to_jumbf_store( store: &mut CAIAssertionStore, claim_assertion: &ClaimAssertion, @@ -536,7 +551,7 @@ impl Store { .data_box_as_json_box(0) .ok_or(Error::JumbfBoxNotFound)?; let assertion = Assertion::from_data_json(&raw_label, json_box.json())?; - let hash = Claim::calc_box_hash(label, &assertion, salt.clone(), &alg)?; + let hash = Claim::calc_assertion_box_hash(label, &assertion, salt.clone(), &alg)?; Ok(ClaimAssertion::new(assertion, instance, &hash, &alg, salt)) } CAI_EMBEDDED_FILE_UUID => { @@ -549,7 +564,7 @@ impl Store { let media_type = ef_box.media_type(); let assertion = Assertion::from_data_binary(&raw_label, &media_type, data_box.data()); - let hash = Claim::calc_box_hash(label, &assertion, salt.clone(), &alg)?; + let hash = Claim::calc_assertion_box_hash(label, &assertion, salt.clone(), &alg)?; Ok(ClaimAssertion::new(assertion, instance, &hash, &alg, salt)) } CAI_CBOR_ASSERTION_UUID => { @@ -557,7 +572,7 @@ impl Store { .data_box_as_cbor_box(0) .ok_or(Error::JumbfBoxNotFound)?; let assertion = Assertion::from_data_cbor(&raw_label, cbor_box.cbor()); - let hash = Claim::calc_box_hash(label, &assertion, salt.clone(), &alg)?; + let hash = Claim::calc_assertion_box_hash(label, &assertion, salt.clone(), &alg)?; Ok(ClaimAssertion::new(assertion, instance, &hash, &alg, salt)) } CAI_UUID_ASSERTION_UUID => { @@ -567,7 +582,7 @@ impl Store { let uuid_str = hex::encode(uuid_box.uuid()); let assertion = Assertion::from_data_uuid(&raw_label, &uuid_str, uuid_box.data()); - let hash = Claim::calc_box_hash(label, &assertion, salt.clone(), &alg)?; + let hash = Claim::calc_assertion_box_hash(label, &assertion, salt.clone(), &alg)?; Ok(ClaimAssertion::new(assertion, instance, &hash, &alg, salt)) } _ => Err(Error::JumbfCreationError), @@ -614,77 +629,117 @@ impl Store { // Add claims and assertions in this store to the JUMBF store. for claim in &self.claims { - let label = claim.label(); - - let mut cai_store = CAIStore::new(label, claim.update_manifest()); + let cai_store = Store::build_manifest_box(claim, min_reserve_size)?; - // Add claim box. Note the order of the boxes are set by the spec - let mut cb = CAIClaimBox::new(); + // add the completed CAI store into the CAI block. + cai_block.add_box(Box::new(cai_store)); + } - // Create the CAI assertion store. - let mut a_store = CAIAssertionStore::new(); + // Write it to memory. + let mut mem_box: Vec<u8> = Vec::new(); + cai_block.write_box(&mut mem_box)?; - // Add assertions to CAI assertion store. - let cas = claim.claim_assertion_store(); - for assertion in cas { - Store::add_assertion_to_jumbf_store(&mut a_store, assertion)?; - } + if mem_box.is_empty() { + Err(Error::JumbfCreationError) + } else { + Ok(mem_box) + } + } - // Add the CAI assertion store to the CAI store. - cai_store.add_box(Box::new(a_store)); + fn build_manifest_box(claim: &Claim, min_reserve_size: usize) -> Result<CAIStore> { + // box label + let label = claim.label(); - // Add the Claim json - let claim_cbor_bytes = claim.data()?; - let c_cbor = JUMBFCBORContentBox::new(claim_cbor_bytes); - cb.add_claim(Box::new(c_cbor)); - cai_store.add_box(Box::new(cb)); + let mut cai_store = CAIStore::new(label, claim.update_manifest()); - // Create a signature and add placeholder data to the CAI store. - let mut sigb = CAISignatureBox::new(); - let signed_data = match claim.signature_val().is_empty() { - false => claim.signature_val().clone(), // existing claims have sig values - true => self.sign_claim_placeholder(claim, min_reserve_size), // empty is the new sig to be replaced - }; + for manifest_box in claim.get_box_order() { + match *manifest_box { + ASSERTIONS => { + let mut a_store = CAIAssertionStore::new(); - let sigc = JUMBFCBORContentBox::new(signed_data); - sigb.add_signature(Box::new(sigc)); - cai_store.add_box(Box::new(sigb)); - - // add vc_store if needed - if !claim.get_verifiable_credentials().is_empty() { - // Create VC store. - let mut vc_store = CAIVerifiableCredentialStore::new(); - - // Add assertions to CAI assertion store. - let vcs = claim.get_verifiable_credentials(); - for assertion_data in vcs { - if let AssertionData::Json(j) = assertion_data { - let id = Claim::vc_id(j)?; - let mut json_data = CAIJSONAssertionBox::new(&id); - json_data.add_json(j.as_bytes().to_vec()); - vc_store.add_credential(Box::new(json_data)); - } else { - return Err(Error::BadParam("VC data must be JSON".to_string())); + // add assertions to CAI assertion store. + let cas = claim.claim_assertion_store(); + for assertion in cas { + Store::add_assertion_to_jumbf_store(&mut a_store, assertion)?; } + + cai_store.add_box(Box::new(a_store)); // add the assertion store to the manifest } + CLAIM => { + let mut cb = CAIClaimBox::new(); - // Add the CAI assertion store to the CAI store. - cai_store.add_box(Box::new(vc_store)); - } + // Add the Claim json + let claim_cbor_bytes = claim.data()?; + let c_cbor = JUMBFCBORContentBox::new(claim_cbor_bytes); + cb.add_claim(Box::new(c_cbor)); - // Finally add the completed CAI store into the CAI block. - cai_block.add_box(Box::new(cai_store)); + cai_store.add_box(Box::new(cb)); // add claim to manifest + } + SIGNATURE => { + // create a signature and add placeholder data to the CAI store. + let mut sigb = CAISignatureBox::new(); + let signed_data = match claim.signature_val().is_empty() { + false => claim.signature_val().clone(), // existing claims have sig values + true => Store::sign_claim_placeholder(claim, min_reserve_size), // empty is the new sig to be replaced + }; + + let sigc = JUMBFCBORContentBox::new(signed_data); + sigb.add_signature(Box::new(sigc)); + + cai_store.add_box(Box::new(sigb)); // add signature to manifest + } + CREDENTIALS => { + // add vc_store if needed + if !claim.get_verifiable_credentials().is_empty() { + let mut vc_store = CAIVerifiableCredentialStore::new(); + + // Add assertions to CAI assertion store. + let vcs = claim.get_verifiable_credentials_store(); + for (uri, assertion_data) in vcs { + if let AssertionData::Json(j) = assertion_data { + let id = Claim::vc_id(j)?; + let mut json_data = CAIJSONAssertionBox::new(&id); + json_data.add_json(j.as_bytes().to_vec()); + + if let Some(salt) = uri.salt() { + json_data.set_salt(salt.clone())?; + } + + vc_store.add_credential(Box::new(json_data)); + } else { + return Err(Error::BadParam("VC data must be JSON".to_string())); + } + } + cai_store.add_box(Box::new(vc_store)); // add the CAI assertion store to manifest + } + } + _ => return Err(Error::ClaimInvalidContent), + } } - // Write it to memory. - let mut mem_box: Vec<u8> = Vec::new(); - cai_block.write_box(&mut mem_box)?; + Ok(cai_store) + } - if mem_box.is_empty() { - Err(Error::JumbfCreationError) - } else { - Ok(mem_box) + // calculate the hash of the manifest JUMBF box + pub fn calc_manifest_box_hash( + claim: &Claim, + salt: Option<Vec<u8>>, + alg: &str, + ) -> Result<Vec<u8>> { + let mut hash_bytes = Vec::with_capacity(4096); + + // build box + let mut cai_store = Store::build_manifest_box(claim, 0)?; + + // add salt if requested + if let Some(salt) = salt { + cai_store.set_salt(salt)?; } + + // box content as Vec + cai_store.super_box().write_box_payload(&mut hash_bytes)?; + + Ok(hash_by_alg(alg, &hash_bytes, None)) } fn manifest_map<'a>(sb: &'a JUMBFSuperBox) -> Result<HashMap<String, ManifestInfo<'a>>> { @@ -761,6 +816,9 @@ impl Store { continue; } + // remember the order of the boxes to insure the box hashes can be regenerated + let mut box_order: Vec<&str> = Vec::new(); + // make sure there are not multiple claim boxes let mut claim_box_cnt = 0; for i in 0..cai_store_box.data_box_count() { @@ -791,6 +849,23 @@ impl Store { InvalidClaimError::C2paMultipleClaimBoxes, )); } + + match desc_box.label().as_ref() { + ASSERTIONS => box_order.push(ASSERTIONS), + CLAIM => box_order.push(CLAIM), + SIGNATURE => box_order.push(SIGNATURE), + CREDENTIALS => box_order.push(CREDENTIALS), + _ => { + let log_item = + log_item!("JUMBF", "unrecognized manifest box", "from_jumbf") + .error(Error::InvalidClaim(InvalidClaimError::ClaimBoxData)) + .validation_status(validation_status::CLAIM_MULTIPLE); + validation_log.log( + log_item, + Some(Error::InvalidClaim(InvalidClaimError::ClaimBoxData)), + )?; + } + } } let is_update_manifest = cai_store_desc_box.uuid() == CAI_UPDATE_MANIFEST_UUID; @@ -901,6 +976,9 @@ impl Store { // set the type of manifest claim.set_update_manifest(is_update_manifest); + // set order to process JUMBF boxes + claim.set_box_order(box_order); + // retrieve & set signature for each claim claim.set_signature_val(sig_data.cbor().clone()); // load the stored signature @@ -972,10 +1050,18 @@ impl Store { let json_str = String::from_utf8(vc_json.json().to_vec()) .map_err(|_| InvalidClaimError::VerifiableCredentialStoreInvalid)?; - claim.add_verifiable_credential(&json_str)?; + let salt = vc_desc_box.get_salt(); + + claim.put_verifiable_credential(&json_str, salt)?; } } + // save the hash of the loaded manifest for ingredient validation + store.manifest_box_hash_cache.insert( + claim.label().to_owned(), + Store::calc_manifest_box_hash(&claim, None, claim.alg())?, + ); + // add claim to store store.insert_restored_claim(cai_store_desc_box.label(), claim); } @@ -1019,7 +1105,18 @@ impl Store { Some(a) => a, None => ingredient.alg().to_owned(), }; - if !verify_by_alg(&alg, &c2pa_manifest.hash(), &ingredient.data()?, None) { + + // get the 1.1-1.2 box hash + let no_hash: Vec<u8> = Vec::new(); + let box_hash = store + .manifest_box_hash_cache + .get(&label) + .unwrap_or(&no_hash); + + // test for 1.1 hash then 1.0 version + if !vec_compare(&c2pa_manifest.hash(), box_hash) + && !verify_by_alg(&alg, &c2pa_manifest.hash(), &ingredient.data()?, None) + { let log_item = log_item!( &c2pa_manifest.url(), "ingredient hash incorrect", @@ -1448,7 +1545,7 @@ impl Store { let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; let sig = self.sign_claim(pc, signer, signer.reserve_size())?; - let sig_placeholder = self.sign_claim_placeholder(pc, signer.reserve_size()); + let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); match self.finish_save_stream(jumbf_bytes, format, stream, sig, &sig_placeholder) { Ok((s, m)) => { @@ -1483,7 +1580,7 @@ impl Store { let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; let sig = self.sign_claim(pc, signer, signer.reserve_size())?; - let sig_placeholder = self.sign_claim_placeholder(pc, signer.reserve_size()); + let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); // get correct output path for remote manifest let output_path = match pc.remote_manifest() { @@ -1540,7 +1637,7 @@ impl Store { let sig = self .sign_claim_async(pc, signer, signer.reserve_size()) .await?; - let sig_placeholder = self.sign_claim_placeholder(pc, signer.reserve_size()); + let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); // get correct output path for remote manifest let output_path = match pc.remote_manifest() { @@ -1596,7 +1693,7 @@ impl Store { let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; let sig = remote_signer.sign_remote(&pc.data()?).await?; - let sig_placeholder = self.sign_claim_placeholder(pc, remote_signer.reserve_size()); + let sig_placeholder = Store::sign_claim_placeholder(pc, remote_signer.reserve_size()); // get correct output path for remote manifest let output_path = match pc.remote_manifest() { @@ -2198,10 +2295,11 @@ impl Store { provenance_label: &str, data: &[u8], redactions: Option<Vec<String>>, - ) -> Result<()> { + ) -> Result<Store> { let mut report = OneShotStatusTracker::new(); let store = Store::from_jumbf(data, &mut report)?; - claim.add_ingredient_data(provenance_label, store.claims, redactions) + claim.add_ingredient_data(provenance_label, store.claims.clone(), redactions)?; + Ok(store) } } @@ -3086,6 +3184,15 @@ pub mod tests { } #[test] + fn test_legacy_ingredient_hash() { + // test 1.0 ingredient hash + let ap = fixture_path("legacy_ingredient_hash.jpg"); + let mut report = DetailedStatusTracker::new(); + let store = Store::load_from_asset(&ap, true, &mut report).expect("load_from_asset"); + println!("store = {store}"); + } + + #[test] #[cfg(all(feature = "file_io", feature = "bmff"))] fn test_bmff_jumbf_generation() { // test adding to actual image diff --git a/sdk/tests/fixtures/legacy_ingredient_hash.jpg b/sdk/tests/fixtures/legacy_ingredient_hash.jpg Binary files differ.