store.rs (199220B)
1 // Copyright 2022 Adobe. All rights reserved. 2 // This file is licensed to you under the Apache License, 3 // Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 4 // or the MIT license (http://opensource.org/licenses/MIT), 5 // at your option. 6 7 // Unless required by applicable law or agreed to in writing, 8 // this software is distributed on an "AS IS" BASIS, WITHOUT 9 // WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or 10 // implied. See the LICENSE-MIT and LICENSE-APACHE files for the 11 // specific language governing permissions and limitations under 12 // each license. 13 14 use std::{ 15 collections::HashMap, 16 io::{Cursor, Read, Seek, SeekFrom}, 17 }; 18 #[cfg(feature = "file_io")] 19 use std::{fs, path::Path}; 20 21 use async_generic::async_generic; 22 use tracing::error; 23 24 #[cfg(feature = "file_io")] 25 use crate::jumbf_io::{ 26 get_file_extension, get_supported_file_extension, load_jumbf_from_file, object_locations, 27 remove_jumbf_from_file, save_jumbf_to_file, 28 }; 29 use crate::{ 30 assertion::{ 31 Assertion, AssertionBase, AssertionData, AssertionDecodeError, AssertionDecodeErrorCause, 32 }, 33 assertions::{ 34 labels::{self, CLAIM}, 35 BmffHash, DataBox, DataHash, DataMap, ExclusionsMap, Ingredient, Relationship, SubsetMap, 36 }, 37 asset_io::{ 38 CAIRead, CAIReadWrite, HashBlockObjectType, HashObjectPositions, RemoteRefEmbedType, 39 }, 40 claim::{Claim, ClaimAssertion, ClaimAssetData, RemoteManifest}, 41 cose_sign::{cose_sign, cose_sign_async}, 42 cose_validator::{check_ocsp_status, verify_cose, verify_cose_async}, 43 error::{Error, Result}, 44 hash_utils::{hash_by_alg, vec_compare, verify_by_alg}, 45 jumbf::{ 46 self, 47 boxes::*, 48 labels::{to_absolute_uri, ASSERTIONS, CREDENTIALS, DATABOXES, SIGNATURE}, 49 }, 50 jumbf_io::{ 51 get_assetio_handler, is_bmff_format, load_jumbf_from_stream, object_locations_from_stream, 52 save_jumbf_to_memory, save_jumbf_to_stream, 53 }, 54 manifest_store_report::ManifestStoreReport, 55 salt::DefaultSalt, 56 settings::get_settings_value, 57 status_tracker::{log_item, OneShotStatusTracker, StatusTracker}, 58 trust_handler::TrustHandlerConfig, 59 utils::{ 60 hash_utils::{hash_sha256, HashRange}, 61 patch::patch_bytes, 62 }, 63 validation_status, AsyncSigner, RemoteSigner, Signer, 64 }; 65 66 const MANIFEST_STORE_EXT: &str = "c2pa"; // file extension for external manifests 67 68 /// A `Store` maintains a list of `Claim` structs. 69 /// 70 /// Typically, this list of `Claim`s represents all of the claims in an asset. 71 #[derive(Debug)] 72 pub struct Store { 73 claims_map: HashMap<String, usize>, 74 manifest_box_hash_cache: HashMap<String, Vec<u8>>, 75 claims: Vec<Claim>, 76 label: String, 77 provenance_path: Option<String>, 78 trust_handler: Box<dyn TrustHandlerConfig>, 79 } 80 81 struct ManifestInfo<'a> { 82 pub desc_box: &'a JUMBFDescriptionBox, 83 pub sbox: &'a JUMBFSuperBox, 84 } 85 86 trait PushGetIndex { 87 type Item; 88 fn push_get_index(&mut self, item: Self::Item) -> usize; 89 } 90 91 impl<T> PushGetIndex for Vec<T> { 92 type Item = T; 93 94 fn push_get_index(&mut self, item: T) -> usize { 95 let index = self.len(); 96 self.push(item); 97 index 98 } 99 } 100 101 impl Default for Store { 102 fn default() -> Self { 103 Self::new() 104 } 105 } 106 107 impl Store { 108 /// Create a new, empty claims store. 109 pub fn new() -> Self { 110 Self::new_with_label(MANIFEST_STORE_EXT) 111 } 112 113 /// Create a new, empty claims store with a custom label. 114 /// 115 /// In most cases, calling [`Store::new()`] is preferred. 116 pub fn new_with_label(label: &str) -> Self { 117 let mut store = Store { 118 claims_map: HashMap::new(), 119 manifest_box_hash_cache: HashMap::new(), 120 claims: Vec::new(), 121 label: label.to_string(), 122 #[cfg(feature = "openssl")] 123 trust_handler: Box::new(crate::openssl::OpenSSLTrustHandlerConfig::new()), 124 #[cfg(all(not(feature = "openssl"), target_arch = "wasm32"))] 125 trust_handler: Box::new(crate::wasm::WebTrustHandlerConfig::new()), 126 #[cfg(all(not(feature = "openssl"), not(target_arch = "wasm32")))] 127 trust_handler: Box::new(crate::trust_handler::TrustPassThrough::new()), 128 provenance_path: None, 129 }; 130 131 // load the trust handler settings, don't worry about status as these are checked during setting generation 132 let _ = get_settings_value::<Option<String>>("trust.trust_anchors").map(|ta_opt| { 133 if let Some(ta) = ta_opt { 134 let _v = store.add_trust(ta.as_bytes()); 135 } 136 }); 137 138 let _ = get_settings_value::<Option<String>>("trust.private_anchors").map(|pa_opt| { 139 if let Some(pa) = pa_opt { 140 let _v = store.add_private_trust_anchors(pa.as_bytes()); 141 } 142 }); 143 144 let _ = get_settings_value::<Option<String>>("trust.trust_config").map(|tc_opt| { 145 if let Some(tc) = tc_opt { 146 let _v = store.add_trust_config(tc.as_bytes()); 147 } 148 }); 149 150 let _ = get_settings_value::<Option<String>>("trust.allowed_list").map(|al_opt| { 151 if let Some(al) = al_opt { 152 let _v = store.add_trust_allowed_list(al.as_bytes()); 153 } 154 }); 155 156 store 157 } 158 159 /// Return label for the store 160 pub fn label(&self) -> &str { 161 &self.label 162 } 163 164 /// Load set of trust anchors used for certificate validation. [u8] containing the 165 /// trust anchors is passed in the trust_vec variable. 166 pub fn add_trust(&mut self, trust_vec: &[u8]) -> Result<()> { 167 let mut trust_reader = Cursor::new(trust_vec); 168 self.trust_handler 169 .load_trust_anchors_from_data(&mut trust_reader) 170 } 171 172 // Load set of private trust anchors used for certificate validation. [u8] to the 173 /// private trust anchors is passed in the trust_vec variable. This can be called multiple times 174 /// if there are additional trust stores. 175 pub fn add_private_trust_anchors(&mut self, trust_vec: &[u8]) -> Result<()> { 176 let mut trust_reader = Cursor::new(trust_vec); 177 self.trust_handler 178 .append_private_trust_data(&mut trust_reader) 179 } 180 181 pub fn add_trust_config(&mut self, trust_vec: &[u8]) -> Result<()> { 182 let mut trust_reader = Cursor::new(trust_vec); 183 self.trust_handler.load_configuration(&mut trust_reader) 184 } 185 186 pub fn add_trust_allowed_list(&mut self, allowed_vec: &[u8]) -> Result<()> { 187 let mut trust_reader = Cursor::new(allowed_vec); 188 self.trust_handler.load_allowed_list(&mut trust_reader) 189 } 190 191 /// Clear all existing trust anchors 192 pub fn clear_trust_anchors(&mut self) { 193 self.trust_handler.clear(); 194 } 195 196 fn trust_handler(&self) -> &dyn TrustHandlerConfig { 197 self.trust_handler.as_ref() 198 } 199 200 /// Get the provenance if available. 201 /// If loaded from an existing asset it will be provenance from the last claim. 202 /// If a new claim is committed that will be the provenance claim 203 pub fn provenance_path(&self) -> Option<String> { 204 if self.provenance_path.is_none() { 205 // if we have claims and no provenance, return last claim 206 if let Some(claim) = self.claims.last() { 207 return Some(Claim::to_claim_uri(claim.label())); 208 } 209 } 210 self.provenance_path.as_ref().cloned() 211 } 212 213 // set the path of the current provenance claim 214 fn set_provenance_path(&mut self, claim_label: &str) { 215 let path = Claim::to_claim_uri(claim_label); 216 self.provenance_path = Some(path); 217 } 218 219 /// get the list of claims for this store 220 pub const fn claims(&self) -> &Vec<Claim> { 221 &self.claims 222 } 223 224 /// the JUMBF manifest box hash (spec 1.2) 225 pub fn get_manifest_box_hash(&self, claim: &Claim) -> Vec<u8> { 226 if let Some(bh) = self.manifest_box_hash_cache.get(claim.label()) { 227 bh.clone() 228 } else { 229 Store::calc_manifest_box_hash(claim, None, claim.alg()).unwrap_or_default() 230 } 231 } 232 233 /// Add a new Claim to this Store. The function 234 /// will return the label of the claim. 235 pub fn commit_claim(&mut self, mut claim: Claim) -> Result<String> { 236 // make sure there is no pending unsigned claim 237 if let Some(pc) = self.provenance_claim() { 238 if pc.signature_val().is_empty() { 239 return Err(Error::ClaimUnsigned); 240 } 241 } 242 // verify the claim is valid 243 claim.build()?; 244 245 // load the claim ingredients 246 // parse first to make sure we can load them 247 let mut ingredient_claims: Vec<Claim> = Vec::new(); 248 for (pc, claims) in claim.claim_ingredient_store() { 249 let mut valid_pc = false; 250 251 // expand for flat list insertion 252 for ingredient_claim in claims { 253 // recreate claim from original bytes 254 let claim_clone = ingredient_claim.clone(); 255 if pc == claim_clone.label() { 256 valid_pc = true; 257 } 258 ingredient_claims.push(claim_clone); 259 } 260 if !valid_pc { 261 return Err(Error::IngredientNotFound); 262 } 263 } 264 265 // update the provenance path 266 self.set_provenance_path(claim.label()); 267 268 let claim_label = claim.label().to_string(); 269 270 // insert ingredients if needed 271 for ingredient_claim in ingredient_claims { 272 let label = ingredient_claim.label().to_owned(); 273 274 if let std::collections::hash_map::Entry::Vacant(e) = self.claims_map.entry(label) { 275 let index = self.claims.push_get_index(ingredient_claim); 276 e.insert(index); 277 } 278 } 279 280 // add claim to store after ingredients 281 let index = self.claims.push_get_index(claim); 282 self.claims_map.insert(claim_label.clone(), index); 283 284 Ok(claim_label) 285 } 286 287 /// Add a new update manifest to this Store. The manifest label 288 /// may be updated to reflect is position in the manifest Store 289 /// if there are conflicting label names. The function 290 /// will return the label of the claim used 291 pub fn commit_update_manifest(&mut self, mut claim: Claim) -> Result<String> { 292 claim.set_update_manifest(true); 293 294 // check for disallowed assertions 295 if claim.has_assertion_type(labels::DATA_HASH) 296 || claim.has_assertion_type(labels::ACTIONS) 297 || claim.has_assertion_type(labels::BMFF_HASH) 298 { 299 return Err(Error::ClaimInvalidContent); 300 } 301 302 // must have exactly one ingredient 303 let ingredient = match claim.get_assertion(Ingredient::LABEL, 0) { 304 Some(i) => { 305 if claim.count_instances(Ingredient::LABEL) > 1 { 306 return Err(Error::ClaimInvalidContent); 307 } else { 308 i 309 } 310 } 311 None => return Err(Error::ClaimInvalidContent), 312 }; 313 314 let ingredient_helper = Ingredient::from_assertion(ingredient)?; 315 316 // must have a parent relationship 317 if ingredient_helper.relationship != Relationship::ParentOf { 318 return Err(Error::IngredientNotFound); 319 } 320 321 // make sure ingredient c2pa.manifest points to provenance claim 322 if let Some(c2pa_manifest) = ingredient_helper.c2pa_manifest { 323 // the manifest should refer to provenance claim 324 if let Some(pc) = self.provenance_claim() { 325 if !c2pa_manifest.url().contains(pc.label()) { 326 return Err(Error::IngredientNotFound); 327 } 328 } else { 329 return Err(Error::IngredientNotFound); 330 } 331 } else { 332 return Err(Error::IngredientNotFound); 333 } 334 335 self.commit_claim(claim) 336 } 337 338 /// Get Claim by label 339 // Returns Option<&Claim> 340 pub fn get_claim(&self, label: &str) -> Option<&Claim> { 341 #![allow(clippy::unwrap_used)] // since it's only in a debug_assert 342 let index = self.claims_map.get(label)?; 343 debug_assert!(self.claims.get(*index).unwrap().label() == label); 344 self.claims.get(*index) 345 } 346 347 /// Get Claim by label 348 // Returns Option<&Claim> 349 pub fn get_claim_mut(&mut self, label: &str) -> Option<&mut Claim> { 350 #![allow(clippy::unwrap_used)] // since it's only in a debug_assert 351 let index = self.claims_map.get(label)?; 352 debug_assert!(self.claims.get(*index).unwrap().label() == label); 353 self.claims.get_mut(*index) 354 } 355 356 /// returns a Claim given a jumbf uri 357 pub fn get_claim_from_uri(&self, uri: &str) -> Result<&Claim> { 358 let claim_label = Store::manifest_label_from_path(uri); 359 self.get_claim(&claim_label) 360 .ok_or_else(|| Error::ClaimMissing { 361 label: claim_label.to_owned(), 362 }) 363 } 364 365 /// returns a ClaimAssertion given a jumbf uri, resolving to the right claim in the store 366 pub fn get_claim_assertion_from_uri(&self, uri: &str) -> Result<&ClaimAssertion> { 367 // first find the right claim and then look for the assertion there 368 let claim = self.get_claim_from_uri(uri)?; 369 let (label, instance) = Claim::assertion_label_from_link(uri); 370 claim 371 .get_claim_assertion(&label, instance) 372 .ok_or_else(|| Error::ClaimMissing { 373 label: label.to_owned(), 374 }) 375 } 376 377 /// Returns an Assertion referenced by JUMBF URI. The URI should be absolute and include 378 /// the desired Claim in the path. If you need to specify the Claim for this URI use 379 /// get_assertion_from_uri_and_claim. 380 /// uri - The JUMBF URI for desired Assertion. 381 pub fn get_assertion_from_uri(&self, uri: &str) -> Option<&Assertion> { 382 let claim_label = Store::manifest_label_from_path(uri); 383 let (assertion_label, instance) = Claim::assertion_label_from_link(uri); 384 385 if let Some(claim) = self.get_claim(&claim_label) { 386 claim.get_assertion(&assertion_label, instance) 387 } else { 388 None 389 } 390 } 391 392 /// Returns an Assertion referenced by JUMBF URI. Only the Claim specified by target_claim_label 393 /// will be searched. The target_claim_label can be a Claim label or JUMBF URI. 394 /// uri - The JUMBF URI for desired Assertion. 395 /// target_claim_label - Label or URI of the Claim to search for the case when the URI is a relative path. 396 pub fn get_assertion_from_uri_and_claim( 397 &self, 398 uri: &str, 399 target_claim_label: &str, 400 ) -> Option<&Assertion> { 401 let (assertion_label, instance) = Claim::assertion_label_from_link(uri); 402 403 let label = Store::manifest_label_from_path(target_claim_label); 404 405 if let Some(claim) = self.get_claim(&label) { 406 claim.get_assertion(&assertion_label, instance) 407 } else { 408 None 409 } 410 } 411 412 /// Returns a DataBox referenced by JUMBF URI if it exists. 413 /// 414 /// Relative paths will use the provenance claim to resolve the DataBox.d 415 pub fn get_data_box_from_uri_and_claim( 416 &self, 417 uri: &str, 418 target_claim_label: &str, 419 ) -> Option<&DataBox> { 420 match jumbf::labels::manifest_label_from_uri(uri) { 421 Some(label) => self.get_claim(&label), // use the manifest label from the thumbnail uri 422 None => self.get_claim(target_claim_label), // relative so use the target claim label 423 } 424 .and_then(|claim| { 425 let uri = if target_claim_label != self.label() { 426 to_absolute_uri(target_claim_label, uri) 427 } else { 428 uri.to_owned() 429 }; 430 claim 431 .databoxes() 432 .iter() 433 .find(|(h, _d)| h.url() == uri) 434 .map(|(_sh, data_box)| data_box) 435 }) 436 } 437 438 // Returns placeholder that will be searched for and replaced 439 // with actual signature data. 440 fn sign_claim_placeholder(claim: &Claim, min_reserve_size: usize) -> Vec<u8> { 441 let placeholder_str = format!("signature placeholder:{}", claim.label()); 442 let mut placeholder = hash_sha256(placeholder_str.as_bytes()); 443 444 use std::cmp::max; 445 placeholder.resize(max(placeholder.len(), min_reserve_size), 0); 446 447 placeholder 448 } 449 450 /// Return certificate chain for the provenance claim 451 #[cfg(feature = "v1_api")] 452 pub(crate) fn get_provenance_cert_chain(&self) -> Result<String> { 453 let claim = self.provenance_claim().ok_or(Error::ProvenanceMissing)?; 454 455 match claim.get_cert_chain() { 456 Ok(chain) => String::from_utf8(chain).map_err(|_e| Error::CoseInvalidCert), 457 Err(e) => Err(e), 458 } 459 } 460 461 /// Return OCSP info if available 462 // Currently only called from manifest_store behind a feature flag but this is allowable 463 // anywhere so allow dead code here for future uses to compile 464 #[allow(dead_code)] 465 pub(crate) fn get_ocsp_status(&self) -> Option<String> { 466 let claim = self 467 .provenance_claim() 468 .ok_or(Error::ProvenanceMissing) 469 .ok()?; 470 471 let sig = claim.signature_val(); 472 let data = claim.data().ok()?; 473 let mut validation_log = OneShotStatusTracker::new(); 474 475 if let Ok(info) = check_ocsp_status(sig, &data, self.trust_handler(), &mut validation_log) { 476 if let Some(revoked_at) = &info.revoked_at { 477 Some(format!( 478 "Certificate Status: Revoked, revoked at: {}", 479 revoked_at 480 )) 481 } else { 482 Some(format!( 483 "Certificate Status: Good, next update: {}", 484 info.next_update 485 )) 486 } 487 } else { 488 None 489 } 490 } 491 492 /// Sign the claim and return signature. 493 #[async_generic(async_signature( 494 &self, 495 claim: &Claim, 496 signer: &dyn AsyncSigner, 497 box_size: usize, 498 ))] 499 pub fn sign_claim( 500 &self, 501 claim: &Claim, 502 signer: &dyn Signer, 503 box_size: usize, 504 ) -> Result<Vec<u8>> { 505 let claim_bytes = claim.data()?; 506 507 let result = if _sync { 508 if signer.direct_cose_handling() { 509 // Let the signer do all the COSE processing and return the structured COSE data. 510 return signer.sign(&claim_bytes); // do not verify remote signers (we never did) 511 } else { 512 cose_sign(signer, &claim_bytes, box_size) 513 } 514 } else { 515 if signer.direct_cose_handling() { 516 // Let the signer do all the COSE processing and return the structured COSE data. 517 return signer.sign(claim_bytes.clone()).await; // do not verify remote signers (we never did) 518 } else { 519 cose_sign_async(signer, &claim_bytes, box_size).await 520 } 521 }; 522 match result { 523 Ok(sig) => { 524 // Sanity check: Ensure that this signature is valid. 525 if let Ok(verify_after_sign) = 526 get_settings_value::<bool>("verify.verify_after_sign") 527 { 528 if verify_after_sign { 529 let mut cose_log = OneShotStatusTracker::new(); 530 531 let result = if _sync { 532 verify_cose( 533 &sig, 534 &claim_bytes, 535 b"", 536 false, 537 self.trust_handler(), 538 &mut cose_log, 539 ) 540 } else { 541 verify_cose_async( 542 sig.clone(), 543 claim_bytes, 544 b"".to_vec(), 545 false, 546 self.trust_handler(), 547 &mut cose_log, 548 ) 549 .await 550 }; 551 if let Err(err) = result { 552 error!( 553 "Signature that was just generated does not validate: {:#?}", 554 err 555 ); 556 return Err(err); 557 } 558 } 559 } 560 Ok(sig) 561 } 562 Err(e) => Err(e), 563 } 564 } 565 566 /// return the current provenance claim label if available 567 pub fn provenance_label(&self) -> Option<String> { 568 self.provenance_path() 569 .map(|provenance| Store::manifest_label_from_path(&provenance)) 570 } 571 572 /// return the current provenance claim if available 573 pub fn provenance_claim(&self) -> Option<&Claim> { 574 match self.provenance_path() { 575 Some(provenance) => { 576 let claim_label = Store::manifest_label_from_path(&provenance); 577 self.get_claim(&claim_label) 578 } 579 None => None, 580 } 581 } 582 583 /// return the current provenance claim as mutable if available 584 pub fn provenance_claim_mut(&mut self) -> Option<&mut Claim> { 585 match self.provenance_path() { 586 Some(provenance) => { 587 let claim_label = Store::manifest_label_from_path(&provenance); 588 self.get_claim_mut(&claim_label) 589 } 590 None => None, 591 } 592 } 593 594 // add a restored claim 595 fn insert_restored_claim(&mut self, label: String, claim: Claim) { 596 let index = self.claims.push_get_index(claim); 597 self.claims_map.insert(label, index); 598 } 599 600 fn add_assertion_to_jumbf_store( 601 store: &mut CAIAssertionStore, 602 claim_assertion: &ClaimAssertion, 603 ) -> Result<()> { 604 // Grab assertion data object. 605 let d = claim_assertion.assertion().decode_data(); 606 607 match d { 608 AssertionData::Json(_) => { 609 let mut json_data = CAIJSONAssertionBox::new(&claim_assertion.label()); 610 json_data.add_json(claim_assertion.assertion().data().to_vec()); 611 if let Some(salt) = claim_assertion.salt() { 612 json_data.set_salt(salt.clone())?; 613 } 614 store.add_assertion(Box::new(json_data)); 615 } 616 AssertionData::Binary(_) => { 617 // TODO: Handle other binary box types if needed. 618 let mut data = JumbfEmbeddedFileBox::new(&claim_assertion.label()); 619 data.add_data( 620 claim_assertion.assertion().data().to_vec(), 621 claim_assertion.assertion().mime_type(), 622 None, 623 ); 624 if let Some(salt) = claim_assertion.salt() { 625 data.set_salt(salt.clone())?; 626 } 627 store.add_assertion(Box::new(data)); 628 } 629 AssertionData::Cbor(_) => { 630 let mut cbor_data = CAICBORAssertionBox::new(&claim_assertion.label()); 631 cbor_data.add_cbor(claim_assertion.assertion().data().to_vec()); 632 if let Some(salt) = claim_assertion.salt() { 633 cbor_data.set_salt(salt.clone())?; 634 } 635 store.add_assertion(Box::new(cbor_data)); 636 } 637 AssertionData::Uuid(s, _) => { 638 let mut uuid_data = CAIUUIDAssertionBox::new(&claim_assertion.label()); 639 uuid_data.add_uuid(s, claim_assertion.assertion().data().to_vec())?; 640 if let Some(salt) = claim_assertion.salt() { 641 uuid_data.set_salt(salt.clone())?; 642 } 643 store.add_assertion(Box::new(uuid_data)); 644 } 645 } 646 Ok(()) 647 } 648 649 // look for old style hashing to determine if this is a pre 1.0 claim 650 fn is_old_assertion(alg: &str, data: &[u8], original_hash: &[u8]) -> bool { 651 let old_hash = hash_by_alg(alg, data, None); 652 vec_compare(&old_hash, original_hash) 653 } 654 655 fn get_assertion_from_jumbf_store( 656 claim: &Claim, 657 assertion_box: &JUMBFSuperBox, 658 label: &str, 659 check_for_legacy_assertion: bool, 660 ) -> Result<ClaimAssertion> { 661 let assertion_desc_box = assertion_box.desc_box(); 662 663 let (raw_label, instance) = Claim::assertion_label_from_link(label); 664 let instance_label = Claim::label_with_instance(&raw_label, instance); 665 let assertion_hashed_uri = claim 666 .assertion_hashed_uri_from_label(&instance_label) 667 .ok_or_else(|| { 668 Error::AssertionDecoding(AssertionDecodeError { 669 label: instance_label.to_string(), 670 version: None, // TODO: Plumb this through 671 content_type: "TO DO: Get content type".to_string(), 672 source: AssertionDecodeErrorCause::AssertionDataIncorrect, 673 }) 674 })?; 675 676 let alg = match assertion_hashed_uri.alg() { 677 Some(ref a) => a.clone(), 678 None => claim.alg().to_string(), 679 }; 680 681 // get salt value if set 682 let salt = assertion_desc_box.get_salt(); 683 684 let result = match assertion_desc_box.uuid().as_ref() { 685 CAI_JSON_ASSERTION_UUID => { 686 let json_box = assertion_box 687 .data_box_as_json_box(0) 688 .ok_or(Error::JumbfBoxNotFound)?; 689 let assertion = Assertion::from_data_json(&raw_label, json_box.json())?; 690 let hash = Claim::calc_assertion_box_hash(label, &assertion, salt.clone(), &alg)?; 691 Ok(ClaimAssertion::new(assertion, instance, &hash, &alg, salt)) 692 } 693 CAI_EMBEDDED_FILE_UUID => { 694 let ef_box = assertion_box 695 .data_box_as_embedded_media_type_box(0) 696 .ok_or(Error::JumbfBoxNotFound)?; 697 let data_box = assertion_box 698 .data_box_as_embedded_file_content_box(1) 699 .ok_or(Error::JumbfBoxNotFound)?; 700 let media_type = ef_box.media_type(); 701 let assertion = 702 Assertion::from_data_binary(&raw_label, &media_type, data_box.data()); 703 let hash = Claim::calc_assertion_box_hash(label, &assertion, salt.clone(), &alg)?; 704 Ok(ClaimAssertion::new(assertion, instance, &hash, &alg, salt)) 705 } 706 CAI_CBOR_ASSERTION_UUID => { 707 let cbor_box = assertion_box 708 .data_box_as_cbor_box(0) 709 .ok_or(Error::JumbfBoxNotFound)?; 710 let assertion = Assertion::from_data_cbor(&raw_label, cbor_box.cbor()); 711 let hash = Claim::calc_assertion_box_hash(label, &assertion, salt.clone(), &alg)?; 712 Ok(ClaimAssertion::new(assertion, instance, &hash, &alg, salt)) 713 } 714 CAI_UUID_ASSERTION_UUID => { 715 let uuid_box = assertion_box 716 .data_box_as_uuid_box(0) 717 .ok_or(Error::JumbfBoxNotFound)?; 718 let uuid_str = hex::encode(uuid_box.uuid()); 719 let assertion = Assertion::from_data_uuid(&raw_label, &uuid_str, uuid_box.data()); 720 721 let hash = Claim::calc_assertion_box_hash(label, &assertion, salt.clone(), &alg)?; 722 Ok(ClaimAssertion::new(assertion, instance, &hash, &alg, salt)) 723 } 724 _ => Err(Error::JumbfCreationError), 725 }; 726 727 if check_for_legacy_assertion { 728 // make sure this is not pre 1.0 data 729 match result { 730 Ok(r) => { 731 // look for old style hashing 732 if Store::is_old_assertion( 733 &alg, 734 r.assertion().data(), 735 &assertion_hashed_uri.hash(), 736 ) { 737 Err(Error::PrereleaseError) 738 } else { 739 Ok(r) 740 } 741 } 742 Err(e) => Err(e), 743 } 744 } else { 745 result 746 } 747 } 748 749 /// Convert this claims store to a JUMBF box. 750 pub fn to_jumbf(&self, signer: &dyn Signer) -> Result<Vec<u8>> { 751 self.to_jumbf_internal(signer.reserve_size()) 752 } 753 754 /// Convert this claims store to a JUMBF box. 755 pub fn to_jumbf_async(&self, signer: &dyn AsyncSigner) -> Result<Vec<u8>> { 756 self.to_jumbf_internal(signer.reserve_size()) 757 } 758 759 fn to_jumbf_internal(&self, min_reserve_size: usize) -> Result<Vec<u8>> { 760 // Create the CAI block. 761 let mut cai_block = Cai::new(); 762 763 // Add claims and assertions in this store to the JUMBF store. 764 for claim in &self.claims { 765 let cai_store = Store::build_manifest_box(claim, min_reserve_size)?; 766 767 // add the completed CAI store into the CAI block. 768 cai_block.add_box(Box::new(cai_store)); 769 } 770 771 // Write it to memory. 772 let mut mem_box: Vec<u8> = Vec::new(); 773 cai_block.write_box(&mut mem_box)?; 774 775 if mem_box.is_empty() { 776 Err(Error::JumbfCreationError) 777 } else { 778 Ok(mem_box) 779 } 780 } 781 782 fn build_manifest_box(claim: &Claim, min_reserve_size: usize) -> Result<CAIStore> { 783 // box label 784 let label = claim.label(); 785 786 let mut cai_store = CAIStore::new(label, claim.update_manifest()); 787 788 for manifest_box in claim.get_box_order() { 789 match *manifest_box { 790 ASSERTIONS => { 791 let mut a_store = CAIAssertionStore::new(); 792 793 // add assertions to CAI assertion store. 794 let cas = claim.claim_assertion_store(); 795 for assertion in cas { 796 Store::add_assertion_to_jumbf_store(&mut a_store, assertion)?; 797 } 798 799 cai_store.add_box(Box::new(a_store)); // add the assertion store to the manifest 800 } 801 CLAIM => { 802 let mut cb = CAIClaimBox::new(); 803 804 // Add the Claim json 805 let claim_cbor_bytes = claim.data()?; 806 let c_cbor = JUMBFCBORContentBox::new(claim_cbor_bytes); 807 cb.add_claim(Box::new(c_cbor)); 808 809 cai_store.add_box(Box::new(cb)); // add claim to manifest 810 } 811 SIGNATURE => { 812 // create a signature and add placeholder data to the CAI store. 813 let mut sigb = CAISignatureBox::new(); 814 let signed_data = match claim.signature_val().is_empty() { 815 false => claim.signature_val().clone(), // existing claims have sig values 816 true => Store::sign_claim_placeholder(claim, min_reserve_size), /* empty is the new sig to be replaced */ 817 }; 818 819 let sigc = JUMBFCBORContentBox::new(signed_data); 820 sigb.add_signature(Box::new(sigc)); 821 822 cai_store.add_box(Box::new(sigb)); // add signature to manifest 823 } 824 CREDENTIALS => { 825 // add vc_store if needed 826 if !claim.get_verifiable_credentials().is_empty() { 827 let mut vc_store = CAIVerifiableCredentialStore::new(); 828 829 // Add assertions to CAI assertion store. 830 let vcs = claim.get_verifiable_credentials_store(); 831 for (uri, assertion_data) in vcs { 832 if let AssertionData::Json(j) = assertion_data { 833 let id = Claim::vc_id(j)?; 834 let mut json_data = CAIJSONAssertionBox::new(&id); 835 json_data.add_json(j.as_bytes().to_vec()); 836 837 if let Some(salt) = uri.salt() { 838 json_data.set_salt(salt.clone())?; 839 } 840 841 vc_store.add_credential(Box::new(json_data)); 842 } else { 843 return Err(Error::BadParam("VC data must be JSON".to_string())); 844 } 845 } 846 cai_store.add_box(Box::new(vc_store)); // add the CAI assertion store to manifest 847 } 848 } 849 DATABOXES => { 850 // Add the data boxes 851 if !claim.databoxes().is_empty() { 852 let mut databoxes = CAIDataboxStore::new(); 853 854 for (uri, db) in claim.databoxes() { 855 let db_cbor_bytes = 856 serde_cbor::to_vec(db).map_err(|_err| Error::AssertionEncoding)?; 857 858 let (link, instance) = Claim::assertion_label_from_link(&uri.url()); 859 let label = Claim::label_with_instance(&link, instance); 860 861 let mut db_cbor = CAICBORAssertionBox::new(&label); 862 db_cbor.add_cbor(db_cbor_bytes); 863 864 if let Some(salt) = uri.salt() { 865 db_cbor.set_salt(salt.clone())?; 866 } 867 868 databoxes.add_databox(Box::new(db_cbor)); 869 } 870 871 cai_store.add_box(Box::new(databoxes)); // add claim to manifest 872 } 873 } 874 _ => return Err(Error::ClaimInvalidContent), 875 } 876 } 877 878 Ok(cai_store) 879 } 880 881 // calculate the hash of the manifest JUMBF box 882 pub fn calc_manifest_box_hash( 883 claim: &Claim, 884 salt: Option<Vec<u8>>, 885 alg: &str, 886 ) -> Result<Vec<u8>> { 887 let mut hash_bytes = Vec::with_capacity(4096); 888 889 // build box 890 let mut cai_store = Store::build_manifest_box(claim, 0)?; 891 892 // add salt if requested 893 if let Some(salt) = salt { 894 cai_store.set_salt(salt)?; 895 } 896 897 // box content as Vec 898 cai_store.super_box().write_box_payload(&mut hash_bytes)?; 899 900 Ok(hash_by_alg(alg, &hash_bytes, None)) 901 } 902 903 fn manifest_map<'a>(sb: &'a JUMBFSuperBox) -> Result<HashMap<String, ManifestInfo<'a>>> { 904 let mut box_info: HashMap<String, ManifestInfo<'a>> = HashMap::new(); 905 for i in 0..sb.data_box_count() { 906 let sbox = sb.data_box_as_superbox(i).ok_or(Error::JumbfBoxNotFound)?; 907 let desc_box = sbox.desc_box(); 908 909 let label = desc_box.uuid(); 910 911 let mi = ManifestInfo { desc_box, sbox }; 912 913 box_info.insert(label, mi); 914 } 915 916 Ok(box_info) 917 } 918 919 // Compare two version labels 920 // base_version_label - is the source label 921 // desired_version_label - is the label to compare to the base 922 // returns true if desired version is <= base version 923 fn check_label_version(base_version_label: &str, desired_version_label: &str) -> bool { 924 if let Some(desired_version) = labels::version(desired_version_label) { 925 if let Some(base_version) = labels::version(base_version_label) { 926 if desired_version > base_version { 927 return false; 928 } 929 } 930 } 931 true 932 } 933 934 pub fn from_jumbf(buffer: &[u8], validation_log: &mut impl StatusTracker) -> Result<Store> { 935 if buffer.is_empty() { 936 return Err(Error::JumbfNotFound); 937 } 938 939 let mut store = Store::new(); 940 941 // setup a cursor for reading the buffer... 942 let mut buf_reader = Cursor::new(buffer); 943 944 // this loads up all the boxes... 945 let super_box = BoxReader::read_super_box(&mut buf_reader)?; 946 947 // this loads up all the boxes... 948 let cai_block = Cai::from(super_box); 949 950 // check the CAI Block 951 let desc_box = cai_block.desc_box(); 952 if desc_box.uuid() != CAI_BLOCK_UUID { 953 let log_item = log_item!("JUMBF", "c2pa box not found", "from_jumbf") 954 .error(Error::InvalidClaim(InvalidClaimError::C2paBlockNotFound)); 955 validation_log.log( 956 log_item, 957 Some(Error::InvalidClaim(InvalidClaimError::C2paBlockNotFound)), 958 )?; 959 960 return Err(Error::InvalidClaim(InvalidClaimError::C2paBlockNotFound)); 961 } 962 963 let num_stores = cai_block.data_box_count(); 964 for idx in 0..num_stores { 965 let cai_store_box = cai_block 966 .data_box_as_superbox(idx) 967 .ok_or(Error::JumbfBoxNotFound)?; 968 let cai_store_desc_box = cai_store_box.desc_box(); 969 970 // ignore unknown boxes per the spec 971 if cai_store_desc_box.uuid() != CAI_UPDATE_MANIFEST_UUID 972 && cai_store_desc_box.uuid() != CAI_STORE_UUID 973 { 974 continue; 975 } 976 977 // remember the order of the boxes to insure the box hashes can be regenerated 978 let mut box_order: Vec<&str> = Vec::new(); 979 980 // make sure there are not multiple claim boxes 981 let mut claim_box_cnt = 0; 982 for i in 0..cai_store_box.data_box_count() { 983 let sbox = cai_store_box 984 .data_box_as_superbox(i) 985 .ok_or(Error::JumbfBoxNotFound)?; 986 let desc_box = sbox.desc_box(); 987 988 if desc_box.uuid() == CAI_CLAIM_UUID { 989 claim_box_cnt += 1; 990 } 991 992 if claim_box_cnt > 1 { 993 let log_item = 994 log_item!("JUMBF", "c2pa multiple claim boxes found", "from_jumbf") 995 .error(Error::InvalidClaim( 996 InvalidClaimError::C2paMultipleClaimBoxes, 997 )) 998 .validation_status(validation_status::CLAIM_MULTIPLE); 999 validation_log.log( 1000 log_item, 1001 Some(Error::InvalidClaim( 1002 InvalidClaimError::C2paMultipleClaimBoxes, 1003 )), 1004 )?; 1005 1006 return Err(Error::InvalidClaim( 1007 InvalidClaimError::C2paMultipleClaimBoxes, 1008 )); 1009 } 1010 1011 match desc_box.label().as_ref() { 1012 ASSERTIONS => box_order.push(ASSERTIONS), 1013 CLAIM => box_order.push(CLAIM), 1014 SIGNATURE => box_order.push(SIGNATURE), 1015 CREDENTIALS => box_order.push(CREDENTIALS), 1016 DATABOXES => box_order.push(DATABOXES), 1017 _ => { 1018 let log_item = 1019 log_item!("JUMBF", "unrecognized manifest box", "from_jumbf") 1020 .error(Error::InvalidClaim(InvalidClaimError::ClaimBoxData)) 1021 .validation_status(validation_status::CLAIM_MULTIPLE); 1022 validation_log.log( 1023 log_item, 1024 Some(Error::InvalidClaim(InvalidClaimError::ClaimBoxData)), 1025 )?; 1026 } 1027 } 1028 } 1029 1030 let is_update_manifest = cai_store_desc_box.uuid() == CAI_UPDATE_MANIFEST_UUID; 1031 1032 // get map of boxes in this manifest 1033 let manifest_boxes = Store::manifest_map(cai_store_box)?; 1034 1035 // retrieve the claim & validate 1036 let claim_superbox = manifest_boxes 1037 .get(CAI_CLAIM_UUID) 1038 .ok_or(Error::InvalidClaim( 1039 InvalidClaimError::ClaimSuperboxNotFound, 1040 ))? 1041 .sbox; 1042 let claim_desc_box = manifest_boxes 1043 .get(CAI_CLAIM_UUID) 1044 .ok_or(Error::InvalidClaim( 1045 InvalidClaimError::ClaimDescriptionBoxNotFound, 1046 ))? 1047 .desc_box; 1048 1049 // check if version is supported 1050 let claim_box_ver = claim_desc_box.label(); 1051 if !Self::check_label_version(Claim::build_version(), &claim_box_ver) { 1052 return Err(Error::InvalidClaim(InvalidClaimError::ClaimVersionTooNew)); 1053 } 1054 1055 // check box contents 1056 if claim_desc_box.uuid() == CAI_CLAIM_UUID { 1057 // must be have only one claim 1058 if claim_superbox.data_box_count() > 1 { 1059 return Err(Error::InvalidClaim(InvalidClaimError::DuplicateClaimBox { 1060 label: claim_desc_box.label(), 1061 })); 1062 } 1063 // better be, but just in case... 1064 1065 let cbor_box = match claim_superbox.data_box_as_cbor_box(0) { 1066 Some(c) => c, 1067 None => { 1068 // check for old claims for reporting 1069 match claim_superbox.data_box_as_json_box(0) { 1070 Some(_c) => { 1071 let log_item = 1072 log_item!("JUMBF", "error loading claim data", "from_jumbf") 1073 .error(Error::PrereleaseError); 1074 validation_log.log_silent(log_item); 1075 1076 return Err(Error::PrereleaseError); 1077 } 1078 None => { 1079 let log_item = 1080 log_item!("JUMBF", "error loading claim data", "from_jumbf") 1081 .error(Error::InvalidClaim( 1082 InvalidClaimError::ClaimBoxData, 1083 )); 1084 validation_log.log_silent(log_item); 1085 return Err(Error::InvalidClaim(InvalidClaimError::ClaimBoxData)); 1086 } 1087 } 1088 } 1089 }; 1090 1091 if cbor_box.box_uuid() != JUMBF_CBOR_UUID { 1092 return Err(Error::InvalidClaim( 1093 InvalidClaimError::ClaimDescriptionBoxInvalid, 1094 )); 1095 } 1096 } 1097 1098 // retrieve the signature 1099 let sig_superbox = manifest_boxes 1100 .get(CAI_SIGNATURE_UUID) 1101 .ok_or(Error::InvalidClaim( 1102 InvalidClaimError::ClaimSignatureBoxNotFound, 1103 ))? 1104 .sbox; 1105 let sig_desc_box = manifest_boxes 1106 .get(CAI_SIGNATURE_UUID) 1107 .ok_or(Error::InvalidClaim( 1108 InvalidClaimError::ClaimSignatureDescriptionBoxNotFound, 1109 ))? 1110 .desc_box; 1111 1112 // check box contents 1113 if sig_desc_box.uuid() == CAI_SIGNATURE_UUID { 1114 // better be, but just in case... 1115 let sig_box = sig_superbox 1116 .data_box_as_cbor_box(0) 1117 .ok_or(Error::JumbfBoxNotFound)?; 1118 if sig_box.box_uuid() != JUMBF_CBOR_UUID { 1119 return Err(Error::InvalidClaim( 1120 InvalidClaimError::ClaimSignatureDescriptionBoxInvalid, 1121 )); 1122 } 1123 } 1124 // save signature to be validated on load 1125 let sig_data = sig_superbox 1126 .data_box_as_cbor_box(0) 1127 .ok_or(Error::JumbfBoxNotFound)?; 1128 1129 // Create a new Claim object from jumbf data after validations 1130 let cbor_box = claim_superbox 1131 .data_box_as_cbor_box(0) 1132 .ok_or(Error::JumbfBoxNotFound)?; 1133 let mut claim = Claim::from_data(&cai_store_desc_box.label(), cbor_box.cbor())?; 1134 1135 // set the type of manifest 1136 claim.set_update_manifest(is_update_manifest); 1137 1138 // set order to process JUMBF boxes 1139 claim.set_box_order(box_order); 1140 1141 // retrieve & set signature for each claim 1142 claim.set_signature_val(sig_data.cbor().clone()); // load the stored signature 1143 1144 // retrieve the assertion store 1145 let assertion_store_box = manifest_boxes 1146 .get(CAI_ASSERTION_STORE_UUID) 1147 .ok_or(Error::InvalidClaim( 1148 InvalidClaimError::AssertionStoreSuperboxNotFound, 1149 ))? 1150 .sbox; 1151 1152 let num_assertions = assertion_store_box.data_box_count(); 1153 1154 // loop over all assertions... 1155 let mut check_for_legacy_assertion = true; 1156 for idx in 0..num_assertions { 1157 let assertion_box = assertion_store_box 1158 .data_box_as_superbox(idx) 1159 .ok_or(Error::JumbfBoxNotFound)?; 1160 let assertion_desc_box = assertion_box.desc_box(); 1161 1162 // Add assertions to claim after validation 1163 let label = assertion_desc_box.label(); 1164 match Store::get_assertion_from_jumbf_store( 1165 &claim, 1166 assertion_box, 1167 &label, 1168 check_for_legacy_assertion, 1169 ) { 1170 Ok(assertion) => { 1171 claim.put_assertion_store(assertion); // restore assertion data to claim 1172 check_for_legacy_assertion = false; // only need to check once 1173 } 1174 Err(e) => { 1175 // if this is an old manifest always return 1176 if std::mem::discriminant(&e) 1177 == std::mem::discriminant(&Error::PrereleaseError) 1178 { 1179 let log_item = 1180 log_item!("JUMBF", "error loading assertion", "from_jumbf") 1181 .error(e); 1182 validation_log.log_silent(log_item); 1183 return Err(Error::PrereleaseError); 1184 } else { 1185 let log_item = 1186 log_item!("JUMBF", "error loading assertion", "from_jumbf") 1187 .error(e); 1188 validation_log.log(log_item, None)?; 1189 } 1190 } 1191 } 1192 } 1193 1194 // load vc_store if available 1195 if let Some(mi) = manifest_boxes.get(CAI_VERIFIABLE_CREDENTIALS_STORE_UUID) { 1196 let vc_store = mi.sbox; 1197 let num_vcs = vc_store.data_box_count(); 1198 1199 for idx in 0..num_vcs { 1200 let vc_box = vc_store 1201 .data_box_as_superbox(idx) 1202 .ok_or(Error::JumbfBoxNotFound)?; 1203 let vc_json = vc_box 1204 .data_box_as_json_box(0) 1205 .ok_or(Error::JumbfBoxNotFound)?; 1206 let vc_desc_box = vc_box.desc_box(); 1207 let _id = vc_desc_box.label(); 1208 1209 let json_str = String::from_utf8(vc_json.json().to_vec()) 1210 .map_err(|_| InvalidClaimError::VerifiableCredentialStoreInvalid)?; 1211 1212 let salt = vc_desc_box.get_salt(); 1213 1214 claim.put_verifiable_credential(&json_str, salt)?; 1215 } 1216 } 1217 1218 // load databox store if available 1219 if let Some(mi) = manifest_boxes.get(CAI_DATABOXES_STORE_UUID) { 1220 let databox_store = mi.sbox; 1221 let num_databoxes = databox_store.data_box_count(); 1222 1223 for idx in 0..num_databoxes { 1224 let db_box = databox_store 1225 .data_box_as_superbox(idx) 1226 .ok_or(Error::JumbfBoxNotFound)?; 1227 let db_cbor = db_box 1228 .data_box_as_cbor_box(0) 1229 .ok_or(Error::JumbfBoxNotFound)?; 1230 let db_desc_box = db_box.desc_box(); 1231 let label = db_desc_box.label(); 1232 1233 let salt = db_desc_box.get_salt(); 1234 1235 claim.put_data_box(&label, db_cbor.cbor(), salt)?; 1236 } 1237 } 1238 1239 // save the hash of the loaded manifest for ingredient validation 1240 store.manifest_box_hash_cache.insert( 1241 claim.label().to_owned(), 1242 Store::calc_manifest_box_hash(&claim, None, claim.alg())?, 1243 ); 1244 1245 // add claim to store 1246 store.insert_restored_claim(cai_store_desc_box.label(), claim); 1247 } 1248 1249 Ok(store) 1250 } 1251 1252 // Get the store label from jumbf path 1253 pub fn manifest_label_from_path(claim_path: &str) -> String { 1254 if let Some(s) = jumbf::labels::manifest_label_from_uri(claim_path) { 1255 s 1256 } else { 1257 claim_path.to_owned() 1258 } 1259 } 1260 1261 // wake the ingredients and validate 1262 fn ingredient_checks( 1263 store: &Store, 1264 claim: &Claim, 1265 asset_data: &mut ClaimAssetData<'_>, 1266 validation_log: &mut impl StatusTracker, 1267 ) -> Result<()> { 1268 let mut num_parent_ofs = 0; 1269 1270 // walk the ingredients 1271 for i in claim.ingredient_assertions() { 1272 let ingredient_assertion = Ingredient::from_assertion(i)?; 1273 1274 // is this an ingredient 1275 if let Some(ref c2pa_manifest) = &ingredient_assertion.c2pa_manifest { 1276 let label = Store::manifest_label_from_path(&c2pa_manifest.url()); 1277 1278 // check for parentOf relationships 1279 if ingredient_assertion.relationship == Relationship::ParentOf { 1280 num_parent_ofs += 1; 1281 } 1282 1283 if let Some(ingredient) = store.get_claim(&label) { 1284 let alg = match c2pa_manifest.alg() { 1285 Some(a) => a, 1286 None => ingredient.alg().to_owned(), 1287 }; 1288 1289 // get the 1.1-1.2 box hash 1290 let no_hash: Vec<u8> = Vec::new(); 1291 let box_hash = store 1292 .manifest_box_hash_cache 1293 .get(&label) 1294 .unwrap_or(&no_hash); 1295 1296 // test for 1.1 hash then 1.0 version 1297 if !vec_compare(&c2pa_manifest.hash(), box_hash) 1298 && !verify_by_alg(&alg, &c2pa_manifest.hash(), &ingredient.data()?, None) 1299 { 1300 let log_item = log_item!( 1301 &c2pa_manifest.url(), 1302 "ingredient hash incorrect", 1303 "ingredient_checks" 1304 ) 1305 .error(Error::HashMismatch( 1306 "ingredient hash does not match found ingredient".to_string(), 1307 )) 1308 .validation_status(validation_status::INGREDIENT_HASHEDURI_MISMATCH); 1309 validation_log.log( 1310 log_item, 1311 Some(Error::HashMismatch( 1312 "ingredient hash does not match found ingredient".to_string(), 1313 )), 1314 )?; 1315 } 1316 1317 // make sure 1318 // verify the ingredient claim 1319 Claim::verify_claim( 1320 ingredient, 1321 asset_data, 1322 false, 1323 store.trust_handler(), 1324 validation_log, 1325 )?; 1326 } else { 1327 let log_item = log_item!( 1328 &c2pa_manifest.url(), 1329 "ingredient not found", 1330 "ingredient_checks" 1331 ) 1332 .error(Error::ClaimVerification(format!( 1333 "ingredient: {label} is missing" 1334 ))) 1335 .validation_status(validation_status::CLAIM_MISSING); 1336 validation_log.log( 1337 log_item, 1338 Some(Error::ClaimVerification(format!( 1339 "ingredient: {label} is missing" 1340 ))), 1341 )?; 1342 } 1343 } 1344 } 1345 1346 // check ingredient rules 1347 if claim.update_manifest() { 1348 if !(num_parent_ofs == 1 && claim.ingredient_assertions().len() == 1) { 1349 let log_item = log_item!( 1350 &claim.uri(), 1351 "update manifest must have one parent", 1352 "ingredient_checks" 1353 ) 1354 .error(Error::ClaimVerification( 1355 "update manifest must have one parent".to_string(), 1356 )) 1357 .validation_status(validation_status::MANIFEST_UPDATE_WRONG_PARENTS); 1358 validation_log.log( 1359 log_item, 1360 Some(Error::ClaimVerification( 1361 "update manifest must have one parent".to_string(), 1362 )), 1363 )?; 1364 } 1365 } else if num_parent_ofs > 1 { 1366 let log_item = log_item!( 1367 &claim.uri(), 1368 "too many ingredient parents", 1369 "ingredient_checks" 1370 ) 1371 .error(Error::ClaimVerification( 1372 "ingredient has more than one parent".to_string(), 1373 )) 1374 .validation_status(validation_status::MANIFEST_MULTIPLE_PARENTS); 1375 validation_log.log( 1376 log_item, 1377 Some(Error::ClaimVerification( 1378 "ingredient has more than one parent".to_string(), 1379 )), 1380 )?; 1381 } 1382 1383 Ok(()) 1384 } 1385 1386 // wake the ingredients and validate 1387 async fn ingredient_checks_async( 1388 store: &Store, 1389 claim: &Claim, 1390 asset_data: &mut ClaimAssetData<'_>, 1391 validation_log: &mut impl StatusTracker, 1392 ) -> Result<()> { 1393 // walk the ingredients 1394 for i in claim.ingredient_assertions() { 1395 let ingredient_assertion = Ingredient::from_assertion(i)?; 1396 1397 // is this an ingredient 1398 if let Some(ref c2pa_manifest) = &ingredient_assertion.c2pa_manifest { 1399 let label = Store::manifest_label_from_path(&c2pa_manifest.url()); 1400 1401 if let Some(ingredient) = store.get_claim(&label) { 1402 let alg = match c2pa_manifest.alg() { 1403 Some(a) => a, 1404 None => ingredient.alg().to_owned(), 1405 }; 1406 1407 // get the 1.1-1.2 box hash 1408 let no_hash: Vec<u8> = Vec::new(); 1409 let box_hash = store 1410 .manifest_box_hash_cache 1411 .get(&label) 1412 .unwrap_or(&no_hash); 1413 1414 // test for 1.1 hash then 1.0 version 1415 if !vec_compare(&c2pa_manifest.hash(), box_hash) 1416 && !verify_by_alg(&alg, &c2pa_manifest.hash(), &ingredient.data()?, None) 1417 { 1418 let log_item = log_item!( 1419 &c2pa_manifest.url(), 1420 "ingredient hash incorrect", 1421 "ingredient_checks_async" 1422 ) 1423 .error(Error::HashMismatch( 1424 "ingredient hash does not match found ingredient".to_string(), 1425 )) 1426 .validation_status(validation_status::INGREDIENT_HASHEDURI_MISMATCH); 1427 validation_log.log( 1428 log_item, 1429 Some(Error::HashMismatch( 1430 "ingredient hash does not match found ingredient".to_string(), 1431 )), 1432 )?; 1433 } 1434 // verify the ingredient claim 1435 Claim::verify_claim_async( 1436 ingredient, 1437 asset_data, 1438 false, 1439 store.trust_handler(), 1440 validation_log, 1441 ) 1442 .await?; 1443 } else { 1444 let log_item = log_item!( 1445 &c2pa_manifest.url(), 1446 "ingredient not found", 1447 "ingredient_checks_async" 1448 ) 1449 .error(Error::ClaimVerification(format!( 1450 "ingredient: {label} is missing" 1451 ))) 1452 .validation_status(validation_status::CLAIM_MISSING); 1453 validation_log.log( 1454 log_item, 1455 Some(Error::ClaimVerification(format!( 1456 "ingredient: {label} is missing" 1457 ))), 1458 )?; 1459 } 1460 } 1461 } 1462 1463 Ok(()) 1464 } 1465 1466 /// Verify Store 1467 /// store: Store to validate 1468 /// xmp_str: String containing entire XMP block of the asset 1469 /// asset_bytes: bytes of the asset to be verified 1470 /// validation_log: If present all found errors are logged and returned, other wise first error causes exit and is returned 1471 pub async fn verify_store_async( 1472 store: &Store, 1473 asset_data: &mut ClaimAssetData<'_>, 1474 validation_log: &mut impl StatusTracker, 1475 ) -> Result<()> { 1476 let claim = match store.provenance_claim() { 1477 Some(c) => c, 1478 None => { 1479 let log_item = 1480 log_item!("Unknown", "could not find active manifest", "verify_store") 1481 .error(Error::ProvenanceMissing) 1482 .validation_status(validation_status::CLAIM_MISSING); 1483 validation_log.log(log_item, Some(Error::ProvenanceMissing))?; 1484 1485 return Err(Error::ProvenanceMissing); 1486 } 1487 }; 1488 1489 // verify the provenance claim 1490 Claim::verify_claim_async( 1491 claim, 1492 asset_data, 1493 true, 1494 store.trust_handler(), 1495 validation_log, 1496 ) 1497 .await?; 1498 1499 Store::ingredient_checks_async(store, claim, asset_data, validation_log).await?; 1500 1501 Ok(()) 1502 } 1503 1504 /// Verify Store 1505 /// store: Store to validate 1506 /// xmp_str: String containing entire XMP block of the asset 1507 /// asset_bytes: bytes of the asset to be verified 1508 /// validation_log: If present all found errors are logged and returned, other wise first error causes exit and is returned 1509 pub fn verify_store( 1510 store: &Store, 1511 asset_data: &mut ClaimAssetData<'_>, 1512 validation_log: &mut impl StatusTracker, 1513 ) -> Result<()> { 1514 let claim = match store.provenance_claim() { 1515 Some(c) => c, 1516 None => { 1517 let log_item = 1518 log_item!("Unknown", "could not find active manifest", "verify_store") 1519 .error(Error::ProvenanceMissing) 1520 .validation_status(validation_status::CLAIM_MISSING); 1521 validation_log.log(log_item, Some(Error::ProvenanceMissing))?; 1522 1523 return Err(Error::ProvenanceMissing); 1524 } 1525 }; 1526 1527 // verify the provenance claim 1528 Claim::verify_claim( 1529 claim, 1530 asset_data, 1531 true, 1532 store.trust_handler(), 1533 validation_log, 1534 )?; 1535 1536 Store::ingredient_checks(store, claim, asset_data, validation_log)?; 1537 1538 Ok(()) 1539 } 1540 1541 // generate a list of AssetHashes based on the location of objects in the file 1542 #[cfg(feature = "file_io")] 1543 fn generate_data_hashes( 1544 asset_path: &Path, 1545 alg: &str, 1546 block_locations: &mut Vec<HashObjectPositions>, 1547 calc_hashes: bool, 1548 ) -> Result<Vec<DataHash>> { 1549 let mut file = std::fs::File::open(asset_path)?; 1550 Self::generate_data_hashes_for_stream(&mut file, alg, block_locations, calc_hashes) 1551 } 1552 1553 // generate a list of AssetHashes based on the location of objects in the stream 1554 fn generate_data_hashes_for_stream<R>( 1555 stream: &mut R, 1556 alg: &str, 1557 block_locations: &mut Vec<HashObjectPositions>, 1558 calc_hashes: bool, 1559 ) -> Result<Vec<DataHash>> 1560 where 1561 R: Read + Seek + ?Sized, 1562 { 1563 if block_locations.is_empty() { 1564 let out: Vec<DataHash> = vec![]; 1565 return Ok(out); 1566 } 1567 1568 let stream_len = stream.seek(SeekFrom::End(0))?; 1569 stream.rewind()?; 1570 1571 let mut hashes: Vec<DataHash> = Vec::new(); 1572 1573 // sort blocks by offset 1574 block_locations.sort_by(|a, b| a.offset.cmp(&b.offset)); 1575 1576 // generate default data hash that excludes jumbf block 1577 // find the first jumbf block (ours are always in order) 1578 // find the first block after the jumbf blocks 1579 let mut block_start: usize = 0; 1580 let mut block_end: usize = 0; 1581 let mut found_jumbf = false; 1582 for item in block_locations { 1583 // find start of jumbf 1584 if !found_jumbf && item.htype == HashBlockObjectType::Cai { 1585 block_start = item.offset; 1586 found_jumbf = true; 1587 } 1588 1589 // find start of block after jumbf blocks 1590 if found_jumbf && item.htype == HashBlockObjectType::Cai { 1591 block_end = item.offset + item.length; 1592 } 1593 } 1594 1595 if found_jumbf { 1596 // add exclusion hash for bytes before and after jumbf 1597 let mut dh = DataHash::new("jumbf manifest", alg); 1598 if block_end > block_start { 1599 dh.add_exclusion(HashRange::new(block_start, block_end - block_start)); 1600 } 1601 1602 if calc_hashes { 1603 // this check is only valid on the final sized asset 1604 if block_end as u64 > stream_len { 1605 return Err(Error::BadParam( 1606 "data hash exclusions out of range".to_string(), 1607 )); 1608 } 1609 1610 dh.gen_hash_from_stream(stream)?; 1611 } else { 1612 match alg { 1613 "sha256" => dh.set_hash([0u8; 32].to_vec()), 1614 "sha384" => dh.set_hash([0u8; 48].to_vec()), 1615 "sha512" => dh.set_hash([0u8; 64].to_vec()), 1616 _ => return Err(Error::UnsupportedType), 1617 } 1618 } 1619 hashes.push(dh); 1620 } 1621 1622 Ok(hashes) 1623 } 1624 1625 fn generate_bmff_data_hashes( 1626 asset_stream: &mut dyn CAIRead, 1627 alg: &str, 1628 calc_hashes: bool, 1629 ) -> Result<Vec<BmffHash>> { 1630 use serde_bytes::ByteBuf; 1631 1632 // The spec has mandatory BMFF exclusion ranges for certain atoms. 1633 // The function makes sure those are included. 1634 1635 let mut hashes: Vec<BmffHash> = Vec::new(); 1636 1637 let mut dh = BmffHash::new("jumbf manifest", alg, None); 1638 let exclusions = dh.exclusions_mut(); 1639 1640 // jumbf exclusion 1641 let mut uuid = ExclusionsMap::new("/uuid".to_owned()); 1642 let data = DataMap { 1643 offset: 8, 1644 value: vec![ 1645 216, 254, 195, 214, 27, 14, 72, 60, 146, 151, 88, 40, 135, 126, 196, 129, 1646 ], // C2PA identifier 1647 }; 1648 let data_vec = vec![data]; 1649 uuid.data = Some(data_vec); 1650 exclusions.push(uuid); 1651 1652 // ftyp exclusion 1653 let ftyp = ExclusionsMap::new("/ftyp".to_owned()); 1654 exclusions.push(ftyp); 1655 1656 // meta/iloc exclusion 1657 let iloc = ExclusionsMap::new("/meta/iloc".to_owned()); 1658 exclusions.push(iloc); 1659 1660 // /mfra/tfra exclusion 1661 let tfra = ExclusionsMap::new("/mfra/tfra".to_owned()); 1662 exclusions.push(tfra); 1663 1664 // /moov/trak/mdia/minf/stbl/stco exclusion 1665 let mut stco = ExclusionsMap::new("/moov/trak/mdia/minf/stbl/stco".to_owned()); 1666 let subset_stco = SubsetMap { 1667 offset: 16, 1668 length: 0, 1669 }; 1670 let subset_stco_vec = vec![subset_stco]; 1671 stco.subset = Some(subset_stco_vec); 1672 exclusions.push(stco); 1673 1674 // /moov/trak/mdia/minf/stbl/co64 exclusion 1675 let mut co64 = ExclusionsMap::new("/moov/trak/mdia/minf/stbl/co64".to_owned()); 1676 let subset_co64 = SubsetMap { 1677 offset: 16, 1678 length: 0, 1679 }; 1680 let subset_co64_vec = vec![subset_co64]; 1681 co64.subset = Some(subset_co64_vec); 1682 exclusions.push(co64); 1683 1684 // /moof/traf/tfhd exclusion 1685 let mut tfhd = ExclusionsMap::new("/moof/traf/tfhd".to_owned()); 1686 let subset_tfhd = SubsetMap { 1687 offset: 16, 1688 length: 8, 1689 }; 1690 let subset_tfhd_vec = vec![subset_tfhd]; 1691 tfhd.subset = Some(subset_tfhd_vec); 1692 tfhd.flags = Some(ByteBuf::from([1, 0, 0])); 1693 exclusions.push(tfhd); 1694 1695 // /moof/traf/trun exclusion 1696 let mut trun = ExclusionsMap::new("/moof/traf/trun".to_owned()); 1697 let subset_trun = SubsetMap { 1698 offset: 16, 1699 length: 4, 1700 }; 1701 let subset_trun_vec = vec![subset_trun]; 1702 trun.subset = Some(subset_trun_vec); 1703 trun.flags = Some(ByteBuf::from([1, 0, 0])); 1704 exclusions.push(trun); 1705 1706 // V2 exclusions 1707 /* Enable this when we support Merkle trees and fragmented MP4 1708 // /mdat exclusion 1709 let mut mdat = ExclusionsMap::new("/mdat".to_owned()); 1710 let subset_mdat = SubsetMap { 1711 offset: 16, 1712 length: 0, 1713 }; 1714 let subset_mdat_vec = vec![subset_mdat]; 1715 mdat.subset = Some(subset_mdat_vec); 1716 exclusions.push(mdat); 1717 */ 1718 1719 if calc_hashes { 1720 dh.gen_hash_from_stream(asset_stream)?; 1721 } else { 1722 match alg { 1723 "sha256" => dh.set_hash([0u8; 32].to_vec()), 1724 "sha384" => dh.set_hash([0u8; 48].to_vec()), 1725 "sha512" => dh.set_hash([0u8; 64].to_vec()), 1726 _ => return Err(Error::UnsupportedType), 1727 } 1728 } 1729 hashes.push(dh); 1730 1731 Ok(hashes) 1732 } 1733 1734 // move or copy data from source to dest 1735 #[cfg(feature = "file_io")] 1736 fn move_or_copy(source: &Path, dest: &Path) -> Result<()> { 1737 // copy temp file to asset 1738 std::fs::rename(source, dest) 1739 // if rename fails, try to copy in case we are on different volumes or output does not exist 1740 .or_else(|_| std::fs::copy(source, dest).and(Ok(()))) 1741 .map_err(Error::IoError) 1742 } 1743 1744 // copy output and possibly the external manifest to final destination 1745 #[cfg(feature = "file_io")] 1746 fn copy_c2pa_to_output(source: &Path, dest: &Path, remote_type: RemoteManifest) -> Result<()> { 1747 match remote_type { 1748 RemoteManifest::NoRemote => Store::move_or_copy(source, dest)?, 1749 RemoteManifest::SideCar 1750 | RemoteManifest::Remote(_) 1751 | RemoteManifest::EmbedWithRemote(_) => { 1752 // make correct path names 1753 let source_asset = source; 1754 let source_cai = source_asset.with_extension(MANIFEST_STORE_EXT); 1755 let dest_cai = dest.with_extension(MANIFEST_STORE_EXT); 1756 1757 Store::move_or_copy(&source_cai, &dest_cai)?; // copy manifest 1758 Store::move_or_copy(source_asset, dest)?; // copy asset 1759 } 1760 } 1761 Ok(()) 1762 } 1763 1764 /// This function is used to pre-generate a manifest with place holders for the final 1765 /// DataHash and Manifest Signature. The DataHash will reserve space for at least 10 1766 /// Exclusion ranges. The Signature box reserved size is based on the size required by 1767 /// the Signer you plan to use. This function is not needed when using Box Hash. This function is used 1768 /// in conjunction with `get_data_hashed_embeddable_manifest`. The manifest returned 1769 /// from `get_data_hashed_embeddable_manifest` will have a size that matches this function. 1770 pub fn get_data_hashed_manifest_placeholder( 1771 &mut self, 1772 reserve_size: usize, 1773 format: &str, 1774 ) -> Result<Vec<u8>> { 1775 let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; 1776 1777 // if user did not supply a hash 1778 if pc.hash_assertions().is_empty() { 1779 // create placeholder DataHash large enough for 10 Exclusions 1780 let mut ph = DataHash::new("jumbf manifest", pc.alg()); 1781 for _ in 0..10 { 1782 ph.add_exclusion(HashRange::new(0, 2)); 1783 } 1784 let data = vec![1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 1785 let mut stream = Cursor::new(data); 1786 ph.gen_hash_from_stream(&mut stream)?; 1787 1788 pc.add_assertion_with_salt(&ph, &DefaultSalt::default())?; 1789 } 1790 1791 let jumbf_bytes = self.to_jumbf_internal(reserve_size)?; 1792 1793 let composed = Self::get_composed_manifest(&jumbf_bytes, format)?; 1794 1795 Ok(composed) 1796 } 1797 1798 fn prep_embeddable_store( 1799 &mut self, 1800 reserve_size: usize, 1801 dh: &DataHash, 1802 asset_reader: Option<&mut dyn CAIRead>, 1803 ) -> Result<Vec<u8>> { 1804 let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; 1805 1806 // make sure there are data hashes present before generating 1807 if pc.hash_assertions().is_empty() { 1808 return Err(Error::BadParam( 1809 "Claim must have hash binding assertion".to_string(), 1810 )); 1811 } 1812 1813 // don't allow BMFF assertions to be present 1814 if !pc.bmff_hash_assertions().is_empty() { 1815 return Err(Error::BadParam( 1816 "BMFF assertions not supported in embeddable manifests".to_string(), 1817 )); 1818 } 1819 1820 let mut adjusted_dh = DataHash::new("jumbf manifest", pc.alg()); 1821 adjusted_dh.exclusions.clone_from(&dh.exclusions); 1822 adjusted_dh.hash.clone_from(&dh.hash); 1823 1824 if let Some(reader) = asset_reader { 1825 // calc hashes 1826 adjusted_dh.gen_hash_from_stream(reader)?; 1827 } 1828 1829 // update the placeholder hash 1830 pc.update_data_hash(adjusted_dh)?; 1831 1832 self.to_jumbf_internal(reserve_size) 1833 } 1834 1835 fn finish_embeddable_store( 1836 &mut self, 1837 sig: &[u8], 1838 sig_placeholder: &[u8], 1839 jumbf_bytes: &mut Vec<u8>, 1840 format: &str, 1841 ) -> Result<Vec<u8>> { 1842 if sig_placeholder.len() != sig.len() { 1843 return Err(Error::CoseSigboxTooSmall); 1844 } 1845 1846 patch_bytes(jumbf_bytes, sig_placeholder, sig).map_err(|_| Error::JumbfCreationError)?; 1847 1848 Self::get_composed_manifest(jumbf_bytes, format) 1849 } 1850 1851 /// Returns a finalized, signed manifest. The manifest are only supported 1852 /// for cases when the client has provided a data hash content hash binding. Note, 1853 /// this function will not work for cases like BMFF where the position 1854 /// of the content is also encoded. This function is not compatible with 1855 /// BMFF hash binding. If a BMFF data hash or box hash is detected that is 1856 /// an error. The DataHash placeholder assertion will be adjusted to the contain 1857 /// the correct values. If the asset_reader value is supplied it will also perform 1858 /// the hash calculations, otherwise the function uses the caller supplied values. 1859 /// It is an error if `get_data_hashed_manifest_placeholder` was not called first 1860 /// as this call inserts the DataHash placeholder assertion to reserve space for the 1861 /// actual hash values not required when using BoxHashes. 1862 pub fn get_data_hashed_embeddable_manifest( 1863 &mut self, 1864 dh: &DataHash, 1865 signer: &dyn Signer, 1866 format: &str, 1867 asset_reader: Option<&mut dyn CAIRead>, 1868 ) -> Result<Vec<u8>> { 1869 let mut jumbf_bytes = 1870 self.prep_embeddable_store(signer.reserve_size(), dh, asset_reader)?; 1871 1872 // sign contents 1873 let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; 1874 let sig = self.sign_claim(pc, signer, signer.reserve_size())?; 1875 1876 let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); 1877 1878 self.finish_embeddable_store(&sig, &sig_placeholder, &mut jumbf_bytes, format) 1879 } 1880 1881 /// Returns a finalized, signed manifest. The manifest are only supported 1882 /// for cases when the client has provided a data hash content hash binding. Note, 1883 /// this function will not work for cases like BMFF where the position 1884 /// of the content is also encoded. This function is not compatible with 1885 /// BMFF hash binding. If a BMFF data hash or box hash is detected that is 1886 /// an error. The DataHash placeholder assertion will be adjusted to the contain 1887 /// the correct values. If the asset_reader value is supplied it will also perform 1888 /// the hash calculations, otherwise the function uses the caller supplied values. 1889 /// It is an error if `get_data_hashed_manifest_placeholder` was not called first 1890 /// as this call inserts the DataHash placeholder assertion to reserve space for the 1891 /// actual hash values not required when using BoxHashes. 1892 pub async fn get_data_hashed_embeddable_manifest_async( 1893 &mut self, 1894 dh: &DataHash, 1895 signer: &dyn AsyncSigner, 1896 format: &str, 1897 asset_reader: Option<&mut dyn CAIRead>, 1898 ) -> Result<Vec<u8>> { 1899 let mut jumbf_bytes = 1900 self.prep_embeddable_store(signer.reserve_size(), dh, asset_reader)?; 1901 1902 // sign contents 1903 let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; 1904 let sig = self 1905 .sign_claim_async(pc, signer, signer.reserve_size()) 1906 .await?; 1907 1908 let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); 1909 1910 self.finish_embeddable_store(&sig, &sig_placeholder, &mut jumbf_bytes, format) 1911 } 1912 1913 /// Returns a finalized, signed manifest. The manifest are only supported 1914 /// for cases when the client has provided a data hash content hash binding. Note, 1915 /// this function will not work for cases like BMFF where the position 1916 /// of the content is also encoded. This function is not compatible with 1917 /// BMFF hash binding. If a BMFF data hash or box hash is detected that is 1918 /// an error. The DataHash placeholder assertion will be adjusted to the contain 1919 /// the correct values. If the asset_reader value is supplied it will also perform 1920 /// the hash calculations, otherwise the function uses the caller supplied values. 1921 /// It is an error if `get_data_hashed_manifest_placeholder` was not called first 1922 /// as this call inserts the DataHash placeholder assertion to reserve space for the 1923 /// actual hash values not required when using BoxHashes. 1924 pub async fn get_data_hashed_embeddable_manifest_remote( 1925 &mut self, 1926 dh: &DataHash, 1927 signer: &dyn RemoteSigner, 1928 format: &str, 1929 asset_reader: Option<&mut dyn CAIRead>, 1930 ) -> Result<Vec<u8>> { 1931 let mut jumbf_bytes = 1932 self.prep_embeddable_store(signer.reserve_size(), dh, asset_reader)?; 1933 1934 // sign contents 1935 let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; 1936 let claim_bytes = pc.data()?; 1937 let sig = signer.sign_remote(&claim_bytes).await?; 1938 1939 let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); 1940 1941 self.finish_embeddable_store(&sig, &sig_placeholder, &mut jumbf_bytes, format) 1942 } 1943 1944 /// Returns a finalized, signed manifest. The client is required to have 1945 /// included the necessary box hash assertion with the pregenerated hashes. 1946 pub fn get_box_hashed_embeddable_manifest(&mut self, signer: &dyn Signer) -> Result<Vec<u8>> { 1947 let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; 1948 1949 // make sure there is only one 1950 if pc.hash_assertions().len() != 1 { 1951 return Err(Error::BadParam( 1952 "Claim must have exactly one hash binding assertion".to_string(), 1953 )); 1954 } 1955 1956 // only allow box hash assertions to be present 1957 if pc.box_hash_assertions().is_empty() { 1958 return Err(Error::BadParam("Missing box hash assertion".to_string())); 1959 } 1960 1961 let mut jumbf_bytes = self.to_jumbf_internal(signer.reserve_size())?; 1962 1963 // sign contents 1964 let sig = self.sign_claim(pc, signer, signer.reserve_size())?; 1965 let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); 1966 1967 if sig_placeholder.len() != sig.len() { 1968 return Err(Error::CoseSigboxTooSmall); 1969 } 1970 1971 patch_bytes(&mut jumbf_bytes, &sig_placeholder, &sig) 1972 .map_err(|_| Error::JumbfCreationError)?; 1973 1974 Ok(jumbf_bytes) 1975 } 1976 1977 /// Returns a finalized, signed manifest. The client is required to have 1978 /// included the necessary box hash assertion with the pregenerated hashes. 1979 pub async fn get_box_hashed_embeddable_manifest_async( 1980 &mut self, 1981 signer: &dyn AsyncSigner, 1982 ) -> Result<Vec<u8>> { 1983 let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; 1984 1985 // make sure there is only one 1986 if pc.hash_assertions().len() != 1 { 1987 return Err(Error::BadParam( 1988 "Claim must have exactly one hash binding assertion".to_string(), 1989 )); 1990 } 1991 1992 // only allow box hash assertions to be present 1993 if pc.box_hash_assertions().is_empty() { 1994 return Err(Error::BadParam("Missing box hash assertion".to_string())); 1995 } 1996 1997 let mut jumbf_bytes = self.to_jumbf_internal(signer.reserve_size())?; 1998 1999 // sign contents 2000 let sig = self 2001 .sign_claim_async(pc, signer, signer.reserve_size()) 2002 .await?; 2003 let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); 2004 2005 if sig_placeholder.len() != sig.len() { 2006 return Err(Error::CoseSigboxTooSmall); 2007 } 2008 2009 patch_bytes(&mut jumbf_bytes, &sig_placeholder, &sig) 2010 .map_err(|_| Error::JumbfCreationError)?; 2011 2012 Ok(jumbf_bytes) 2013 } 2014 2015 /// Returns the supplied manifest composed to be directly compatible with the desired format. 2016 /// For example, if format is JPEG function will return the set of APP11 segments that contains 2017 /// the manifest. Similarly for PNG it would be the PNG chunk complete with header and CRC. 2018 pub fn get_composed_manifest(manifest_bytes: &[u8], format: &str) -> Result<Vec<u8>> { 2019 if let Some(h) = get_assetio_handler(format) { 2020 if let Some(composed_data_handler) = h.composed_data_ref() { 2021 return composed_data_handler.compose_manifest(manifest_bytes, format); 2022 } 2023 } 2024 Err(Error::UnsupportedType) 2025 } 2026 2027 /// Embed the claims store as jumbf into a stream. Updates XMP with provenance record. 2028 /// When called, the stream should contain an asset matching format. 2029 /// on return, the stream will contain the new manifest signed with signer 2030 /// This directly modifies the asset in stream, backup stream first if you need to preserve it. 2031 /// This can also handle remote signing if direct_cose_handling() is true. 2032 #[async_generic(async_signature( 2033 &mut self, 2034 format: &str, 2035 input_stream: &mut dyn CAIRead, 2036 output_stream: &mut dyn CAIReadWrite, 2037 signer: &dyn AsyncSigner, 2038 ))] 2039 pub fn save_to_stream( 2040 &mut self, 2041 format: &str, 2042 input_stream: &mut dyn CAIRead, 2043 output_stream: &mut dyn CAIReadWrite, 2044 signer: &dyn Signer, 2045 ) -> Result<Vec<u8>> { 2046 let intermediate_output: Vec<u8> = Vec::new(); 2047 let mut intermediate_stream = Cursor::new(intermediate_output); 2048 2049 let jumbf_bytes = self.start_save_stream( 2050 format, 2051 input_stream, 2052 &mut intermediate_stream, 2053 signer.reserve_size(), 2054 )?; 2055 2056 intermediate_stream.set_position(0); 2057 let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; 2058 let sig = if _sync { 2059 self.sign_claim(pc, signer, signer.reserve_size()) 2060 } else { 2061 self.sign_claim_async(pc, signer, signer.reserve_size()) 2062 .await 2063 }?; 2064 let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); 2065 2066 intermediate_stream.rewind()?; 2067 match self.finish_save_stream( 2068 jumbf_bytes, 2069 format, 2070 &mut intermediate_stream, 2071 output_stream, 2072 sig, 2073 &sig_placeholder, 2074 ) { 2075 Ok((s, m)) => { 2076 // save sig so store is up to date 2077 let pc_mut = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; 2078 pc_mut.set_signature_val(s); 2079 2080 Ok(m) 2081 } 2082 Err(e) => Err(e), 2083 } 2084 } 2085 2086 /// Async RemoteSigner used to embed the claims store and returns memory representation of the 2087 /// asset and manifest. Updates XMP with provenance record. 2088 /// When called, the stream should contain an asset matching format. 2089 /// Returns a tuple (output asset, manifest store) with a `Vec<u8>` containing the output asset and a `Vec<u8>` containing the insert manifest store. (output asset, ) 2090 pub(crate) async fn save_to_memory_remote_signed( 2091 &mut self, 2092 format: &str, 2093 asset: &[u8], 2094 remote_signer: &dyn crate::signer::RemoteSigner, 2095 ) -> Result<(Vec<u8>, Vec<u8>)> { 2096 let mut input_stream = Cursor::new(asset); 2097 let output_vec: Vec<u8> = Vec::new(); 2098 let mut output_stream = Cursor::new(output_vec); 2099 2100 let jumbf_bytes = self.start_save_stream( 2101 format, 2102 &mut input_stream, 2103 &mut output_stream, 2104 remote_signer.reserve_size(), 2105 )?; 2106 2107 let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; 2108 let sig = remote_signer.sign_remote(&pc.data()?).await?; 2109 let sig_placeholder = Store::sign_claim_placeholder(pc, remote_signer.reserve_size()); 2110 2111 match self.finish_save_to_memory( 2112 jumbf_bytes, 2113 format, 2114 &output_stream.into_inner(), 2115 sig, 2116 &sig_placeholder, 2117 ) { 2118 Ok((s, output_asset, output_jumbf)) => { 2119 // save sig so store is up to date 2120 let pc_mut = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; 2121 pc_mut.set_signature_val(s); 2122 2123 Ok((output_asset, output_jumbf)) 2124 } 2125 Err(e) => Err(e), 2126 } 2127 } 2128 2129 /// Embed the claims store as jumbf into an asset. Updates XMP with provenance record. 2130 #[cfg(feature = "file_io")] 2131 pub fn save_to_asset( 2132 &mut self, 2133 asset_path: &Path, 2134 signer: &dyn Signer, 2135 dest_path: &Path, 2136 ) -> Result<()> { 2137 // set up temp dir, contents auto deleted 2138 let td = tempfile::TempDir::new()?; 2139 let temp_path = td.path(); 2140 let temp_file = temp_path.join( 2141 dest_path 2142 .file_name() 2143 .ok_or_else(|| Error::BadParam("invalid destination path".to_string()))?, 2144 ); 2145 2146 let jumbf_bytes = self.start_save(asset_path, &temp_file, signer.reserve_size())?; 2147 2148 let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; 2149 let sig = self.sign_claim(pc, signer, signer.reserve_size())?; 2150 let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); 2151 2152 // get correct output path for remote manifest 2153 let output_path = match pc.remote_manifest() { 2154 RemoteManifest::NoRemote | RemoteManifest::EmbedWithRemote(_) => { 2155 temp_file.to_path_buf() 2156 } 2157 RemoteManifest::SideCar | RemoteManifest::Remote(_) => { 2158 temp_file.with_extension(MANIFEST_STORE_EXT) 2159 } 2160 }; 2161 2162 match self.finish_save(jumbf_bytes, &output_path, sig, &sig_placeholder) { 2163 Ok((s, m)) => { 2164 // save sig so store is up to date 2165 let pc_mut = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; 2166 pc_mut.set_signature_val(s); 2167 2168 // do we need to make a C2PA file in addition to standard embedded output 2169 if let RemoteManifest::EmbedWithRemote(_url) = pc_mut.remote_manifest() { 2170 let c2pa = output_path.with_extension(MANIFEST_STORE_EXT); 2171 std::fs::write(c2pa, &m)?; 2172 } 2173 2174 // copy the correct files upon completion 2175 Store::copy_c2pa_to_output(&temp_file, dest_path, pc_mut.remote_manifest())?; 2176 2177 Ok(()) 2178 } 2179 Err(e) => Err(e), 2180 } 2181 } 2182 2183 /// Embed the claims store as jumbf into an asset using an async signer. Updates XMP with provenance record. 2184 #[cfg(feature = "file_io")] 2185 pub async fn save_to_asset_async( 2186 &mut self, 2187 asset_path: &Path, 2188 signer: &dyn AsyncSigner, 2189 dest_path: &Path, 2190 ) -> Result<Vec<u8>> { 2191 // set up temp dir, contents auto deleted 2192 let td = tempfile::TempDir::new()?; 2193 let temp_path = td.path(); 2194 let temp_file = temp_path.join( 2195 dest_path 2196 .file_name() 2197 .ok_or_else(|| Error::BadParam("invalid destination path".to_string()))?, 2198 ); 2199 2200 let jumbf_bytes = self.start_save(asset_path, &temp_file, signer.reserve_size())?; 2201 2202 let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; 2203 let sig = self 2204 .sign_claim_async(pc, signer, signer.reserve_size()) 2205 .await?; 2206 let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size()); 2207 2208 // get correct output path for remote manifest 2209 let output_path = match pc.remote_manifest() { 2210 RemoteManifest::NoRemote | RemoteManifest::EmbedWithRemote(_) => { 2211 temp_file.to_path_buf() 2212 } 2213 RemoteManifest::SideCar | RemoteManifest::Remote(_) => { 2214 temp_file.with_extension(MANIFEST_STORE_EXT) 2215 } 2216 }; 2217 2218 match self.finish_save(jumbf_bytes, &output_path, sig, &sig_placeholder) { 2219 Ok((s, m)) => { 2220 // save sig so store is up to date 2221 let pc_mut = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; 2222 pc_mut.set_signature_val(s); 2223 2224 // do we need to make a C2PA file in addition to standard embedded output 2225 if let RemoteManifest::EmbedWithRemote(_url) = pc_mut.remote_manifest() { 2226 let c2pa = output_path.with_extension(MANIFEST_STORE_EXT); 2227 std::fs::write(c2pa, &m)?; 2228 } 2229 2230 // copy the correct files upon completion 2231 Store::copy_c2pa_to_output(&temp_file, dest_path, pc_mut.remote_manifest())?; 2232 2233 Ok(m) 2234 } 2235 Err(e) => Err(e), 2236 } 2237 } 2238 2239 /// Embed the claims store as jumbf into an asset using an CoseSign box generated remotely. Updates XMP with provenance record. 2240 #[cfg(feature = "file_io")] 2241 pub async fn save_to_asset_remote_signed( 2242 &mut self, 2243 asset_path: &Path, 2244 remote_signer: &dyn crate::signer::RemoteSigner, 2245 dest_path: &Path, 2246 ) -> Result<Vec<u8>> { 2247 // set up temp dir, contents auto deleted 2248 let td = tempfile::TempDir::new()?; 2249 let temp_path = td.path(); 2250 let temp_file = temp_path.join( 2251 dest_path 2252 .file_name() 2253 .ok_or_else(|| Error::BadParam("invalid destination path".to_string()))?, 2254 ); 2255 2256 let jumbf_bytes = self.start_save(asset_path, &temp_file, remote_signer.reserve_size())?; 2257 2258 let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; 2259 let sig = remote_signer.sign_remote(&pc.data()?).await?; 2260 2261 let sig_placeholder = Store::sign_claim_placeholder(pc, remote_signer.reserve_size()); 2262 2263 // get correct output path for remote manifest 2264 let output_path = match pc.remote_manifest() { 2265 RemoteManifest::NoRemote | RemoteManifest::EmbedWithRemote(_) => { 2266 temp_file.to_path_buf() 2267 } 2268 RemoteManifest::SideCar | RemoteManifest::Remote(_) => { 2269 temp_file.with_extension(MANIFEST_STORE_EXT) 2270 } 2271 }; 2272 2273 match self.finish_save(jumbf_bytes, &output_path, sig, &sig_placeholder) { 2274 Ok((s, m)) => { 2275 // save sig so store is up to date 2276 let pc_mut = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; 2277 pc_mut.set_signature_val(s); 2278 2279 // do we need to make a C2PA file in addition to standard embedded output 2280 if let RemoteManifest::EmbedWithRemote(_url) = pc_mut.remote_manifest() { 2281 let c2pa = output_path.with_extension(MANIFEST_STORE_EXT); 2282 std::fs::write(c2pa, &m)?; 2283 } 2284 2285 // copy the correct files upon completion 2286 Store::copy_c2pa_to_output(&temp_file, dest_path, pc_mut.remote_manifest())?; 2287 2288 Ok(m) 2289 } 2290 Err(e) => Err(e), 2291 } 2292 } 2293 2294 fn start_save_stream( 2295 &mut self, 2296 format: &str, 2297 input_stream: &mut dyn CAIRead, 2298 output_stream: &mut dyn CAIReadWrite, 2299 reserve_size: usize, 2300 ) -> Result<Vec<u8>> { 2301 let intermediate_output: Vec<u8> = Vec::new(); 2302 let mut intermediate_stream = Cursor::new(intermediate_output); 2303 2304 let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; 2305 2306 // Add remote reference XMP if needed and strip out existing manifest 2307 // We don't need to strip manifests if we are replacing an exsiting one 2308 let (url, remove_manifests) = match pc.remote_manifest() { 2309 RemoteManifest::NoRemote => (None, false), 2310 RemoteManifest::SideCar => (None, true), 2311 RemoteManifest::Remote(url) => (Some(url), true), 2312 RemoteManifest::EmbedWithRemote(url) => (Some(url), false), 2313 }; 2314 2315 let io_handler = get_assetio_handler(format).ok_or(Error::UnsupportedType)?; 2316 2317 // Do not assume the handler supports XMP or removing manifests unless we need it to 2318 if let Some(url) = url { 2319 let external_ref_writer = io_handler 2320 .remote_ref_writer_ref() 2321 .ok_or(Error::XmpNotSupported)?; 2322 2323 if remove_manifests { 2324 let manifest_writer = io_handler 2325 .get_writer(format) 2326 .ok_or(Error::UnsupportedType)?; 2327 2328 let tmp_output: Vec<u8> = Vec::new(); 2329 let mut tmp_stream = Cursor::new(tmp_output); 2330 manifest_writer.remove_cai_store_from_stream(input_stream, &mut tmp_stream)?; 2331 2332 // add external ref if possible 2333 tmp_stream.set_position(0); 2334 external_ref_writer.embed_reference_to_stream( 2335 &mut tmp_stream, 2336 &mut intermediate_stream, 2337 RemoteRefEmbedType::Xmp(url), 2338 )?; 2339 } else { 2340 // add external ref if possible 2341 external_ref_writer.embed_reference_to_stream( 2342 input_stream, 2343 &mut intermediate_stream, 2344 RemoteRefEmbedType::Xmp(url), 2345 )?; 2346 } 2347 } else if remove_manifests { 2348 let manifest_writer = io_handler 2349 .get_writer(format) 2350 .ok_or(Error::UnsupportedType)?; 2351 2352 manifest_writer.remove_cai_store_from_stream(input_stream, &mut intermediate_stream)?; 2353 } else { 2354 // just clone stream 2355 input_stream.rewind()?; 2356 std::io::copy(input_stream, &mut intermediate_stream)?; 2357 } 2358 2359 let is_bmff = is_bmff_format(format); 2360 2361 let mut data; 2362 let jumbf_size; 2363 2364 if is_bmff { 2365 // 2) Get hash ranges if needed, do not generate for update manifests 2366 if !pc.update_manifest() { 2367 intermediate_stream.rewind()?; 2368 let bmff_hashes = 2369 Store::generate_bmff_data_hashes(&mut intermediate_stream, pc.alg(), false)?; 2370 for hash in bmff_hashes { 2371 pc.add_assertion(&hash)?; 2372 } 2373 } 2374 2375 // 3) Generate in memory CAI jumbf block 2376 // and write preliminary jumbf store to file 2377 // source and dest the same so save_jumbf_to_file will use the same file since we have already cloned 2378 data = self.to_jumbf_internal(reserve_size)?; 2379 jumbf_size = data.len(); 2380 // write the jumbf to the output stream if we are embedding the manifest 2381 if !remove_manifests { 2382 intermediate_stream.rewind()?; 2383 save_jumbf_to_stream(format, &mut intermediate_stream, output_stream, &data)?; 2384 } else { 2385 // just copy the asset to the output stream without an embedded manifest (may be stripping one out here) 2386 intermediate_stream.rewind()?; 2387 std::io::copy(&mut intermediate_stream, output_stream)?; 2388 } 2389 2390 // generate actual hash values 2391 let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; // reborrow to change mutability 2392 2393 if !pc.update_manifest() { 2394 let bmff_hashes = pc.bmff_hash_assertions(); 2395 2396 if !bmff_hashes.is_empty() { 2397 let mut bmff_hash = BmffHash::from_assertion(bmff_hashes[0])?; 2398 intermediate_stream.rewind()?; 2399 output_stream.rewind()?; 2400 std::io::copy(output_stream, &mut intermediate_stream)?; // remove this once we can get a CAIReader from CAIReadWrite safely 2401 bmff_hash.gen_hash_from_stream(&mut intermediate_stream)?; 2402 pc.update_bmff_hash(bmff_hash)?; 2403 } 2404 } 2405 } else { 2406 // we will not do automatic hashing if we detect a box hash present 2407 let mut needs_hashing = false; 2408 if pc.hash_assertions().is_empty() { 2409 // 2) Get hash ranges if needed, do not generate for update manifests 2410 let mut hash_ranges = 2411 object_locations_from_stream(format, &mut intermediate_stream)?; 2412 let hashes: Vec<DataHash> = if pc.update_manifest() { 2413 Vec::new() 2414 } else { 2415 Store::generate_data_hashes_for_stream( 2416 &mut intermediate_stream, 2417 pc.alg(), 2418 &mut hash_ranges, 2419 false, 2420 )? 2421 }; 2422 2423 // add the placeholder data hashes to provenance claim so that the required space is reserved 2424 for mut hash in hashes { 2425 // add padding to account for possible cbor expansion of final DataHash 2426 let padding: Vec<u8> = vec![0x0; 10]; 2427 hash.add_padding(padding); 2428 2429 pc.add_assertion(&hash)?; 2430 } 2431 needs_hashing = true; 2432 } 2433 2434 // 3) Generate in memory CAI jumbf block 2435 data = self.to_jumbf_internal(reserve_size)?; 2436 jumbf_size = data.len(); 2437 2438 // write the jumbf to the output stream if we are embedding the manifest 2439 if !remove_manifests { 2440 intermediate_stream.rewind()?; 2441 save_jumbf_to_stream(format, &mut intermediate_stream, output_stream, &data)?; 2442 } else { 2443 // just copy the asset to the output stream without an embedded manifest (may be stripping one out here) 2444 intermediate_stream.rewind()?; 2445 std::io::copy(&mut intermediate_stream, output_stream)?; 2446 } 2447 2448 // 4) determine final object locations and patch the asset hashes with correct offset 2449 // replace the source with correct asset hashes so that the claim hash will be correct 2450 if needs_hashing { 2451 let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; 2452 2453 // get the final hash ranges, but not for update manifests 2454 intermediate_stream.rewind()?; 2455 output_stream.rewind()?; 2456 std::io::copy(output_stream, &mut intermediate_stream)?; // can remove this once we can get a CAIReader from CAIReadWrite safely 2457 let mut new_hash_ranges = 2458 object_locations_from_stream(format, &mut intermediate_stream)?; 2459 let updated_hashes = if pc.update_manifest() { 2460 Vec::new() 2461 } else { 2462 Store::generate_data_hashes_for_stream( 2463 &mut intermediate_stream, 2464 pc.alg(), 2465 &mut new_hash_ranges, 2466 true, 2467 )? 2468 }; 2469 2470 // patch existing claim hash with updated data 2471 for hash in updated_hashes { 2472 pc.update_data_hash(hash)?; 2473 } 2474 } 2475 } 2476 2477 // regenerate the jumbf because the cbor changed 2478 data = self.to_jumbf_internal(reserve_size)?; 2479 if jumbf_size != data.len() { 2480 return Err(Error::JumbfCreationError); 2481 } 2482 2483 Ok(data) // return JUMBF data 2484 } 2485 2486 fn finish_save_stream( 2487 &self, 2488 mut jumbf_bytes: Vec<u8>, 2489 format: &str, 2490 input_stream: &mut dyn CAIRead, 2491 output_stream: &mut dyn CAIReadWrite, 2492 sig: Vec<u8>, 2493 sig_placeholder: &[u8], 2494 ) -> Result<(Vec<u8>, Vec<u8>)> { 2495 if sig_placeholder.len() != sig.len() { 2496 return Err(Error::CoseSigboxTooSmall); 2497 } 2498 2499 patch_bytes(&mut jumbf_bytes, sig_placeholder, &sig) 2500 .map_err(|_| Error::JumbfCreationError)?; 2501 2502 // re-save to file 2503 let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; 2504 match pc.remote_manifest() { 2505 RemoteManifest::NoRemote | RemoteManifest::EmbedWithRemote(_) => { 2506 save_jumbf_to_stream(format, input_stream, output_stream, &jumbf_bytes)?; 2507 } 2508 RemoteManifest::SideCar | RemoteManifest::Remote(_) => { 2509 // just copy the asset to the output stream without an embedded manifest (may be stripping one out here) 2510 std::io::copy(input_stream, output_stream)?; 2511 } 2512 } 2513 2514 Ok((sig, jumbf_bytes)) 2515 } 2516 2517 fn finish_save_to_memory( 2518 &self, 2519 mut jumbf_bytes: Vec<u8>, 2520 format: &str, 2521 source_asset: &[u8], 2522 sig: Vec<u8>, 2523 sig_placeholder: &[u8], 2524 ) -> Result<(Vec<u8>, Vec<u8>, Vec<u8>)> { 2525 if sig_placeholder.len() != sig.len() { 2526 return Err(Error::CoseSigboxTooSmall); 2527 } 2528 2529 patch_bytes(&mut jumbf_bytes, sig_placeholder, &sig) 2530 .map_err(|_| Error::JumbfCreationError)?; 2531 2532 // return sig and output 2533 Ok(( 2534 sig, 2535 save_jumbf_to_memory(format, source_asset, &jumbf_bytes)?, 2536 jumbf_bytes, 2537 )) 2538 } 2539 2540 #[cfg(feature = "file_io")] 2541 fn start_save( 2542 &mut self, 2543 asset_path: &Path, 2544 dest_path: &Path, 2545 reserve_size: usize, 2546 ) -> Result<Vec<u8>> { 2547 // force generate external manifests for unknown types 2548 2549 let ext = match get_supported_file_extension(dest_path) { 2550 Some(ext) => ext, 2551 None => { 2552 let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; 2553 pc.set_external_manifest(); // generate external manifests for unknown types 2554 MANIFEST_STORE_EXT.to_owned() 2555 } 2556 }; 2557 2558 // clone the source to working copy if requested 2559 if asset_path != dest_path { 2560 fs::copy(asset_path, dest_path).map_err(Error::IoError)?; 2561 } 2562 2563 // update file following the steps outlined in CAI spec 2564 2565 // 1) Add DC provenance XMP 2566 let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; 2567 let output_path = match pc.remote_manifest() { 2568 crate::claim::RemoteManifest::NoRemote => dest_path.to_path_buf(), 2569 crate::claim::RemoteManifest::SideCar => { 2570 // remove any previous c2pa manifest from the asset 2571 match remove_jumbf_from_file(dest_path) { 2572 Ok(_) | Err(Error::UnsupportedType) => { 2573 dest_path.with_extension(MANIFEST_STORE_EXT) 2574 } 2575 Err(e) => return Err(e), 2576 } 2577 } 2578 crate::claim::RemoteManifest::Remote(url) => { 2579 let d = dest_path.with_extension(MANIFEST_STORE_EXT); 2580 // remove any previous c2pa manifest from the asset 2581 remove_jumbf_from_file(dest_path)?; 2582 2583 if let Some(h) = get_assetio_handler(&ext) { 2584 if let Some(external_ref_writer) = h.remote_ref_writer_ref() { 2585 external_ref_writer 2586 .embed_reference(dest_path, RemoteRefEmbedType::Xmp(url))?; 2587 } else { 2588 return Err(Error::XmpNotSupported); 2589 } 2590 } else { 2591 return Err(Error::UnsupportedType); 2592 } 2593 2594 d 2595 } 2596 crate::claim::RemoteManifest::EmbedWithRemote(url) => { 2597 if let Some(h) = get_assetio_handler(&ext) { 2598 if let Some(external_ref_writer) = h.remote_ref_writer_ref() { 2599 external_ref_writer 2600 .embed_reference(dest_path, RemoteRefEmbedType::Xmp(url))?; 2601 } else { 2602 return Err(Error::XmpNotSupported); 2603 } 2604 } else { 2605 return Err(Error::UnsupportedType); 2606 } 2607 dest_path.to_path_buf() 2608 } 2609 }; 2610 2611 // get the provenance claim changing mutability 2612 let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; 2613 2614 let is_bmff = is_bmff_format(&ext); 2615 2616 let mut data; 2617 let jumbf_size; 2618 2619 if is_bmff { 2620 // 2) Get hash ranges if needed, do not generate for update manifests 2621 if !pc.update_manifest() { 2622 let mut file = std::fs::File::open(asset_path)?; 2623 let bmff_hashes = Store::generate_bmff_data_hashes(&mut file, pc.alg(), false)?; 2624 for hash in bmff_hashes { 2625 pc.add_assertion(&hash)?; 2626 } 2627 } 2628 2629 // 3) Generate in memory CAI jumbf block 2630 // and write preliminary jumbf store to file 2631 // source and dest the same so save_jumbf_to_file will use the same file since we have already cloned 2632 data = self.to_jumbf_internal(reserve_size)?; 2633 jumbf_size = data.len(); 2634 save_jumbf_to_file(&data, &output_path, Some(&output_path))?; 2635 2636 // generate actual hash values 2637 let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; // reborrow to change mutability 2638 2639 if !pc.update_manifest() { 2640 let bmff_hashes = pc.bmff_hash_assertions(); 2641 2642 if !bmff_hashes.is_empty() { 2643 let mut bmff_hash = BmffHash::from_assertion(bmff_hashes[0])?; 2644 bmff_hash.gen_hash(dest_path)?; 2645 pc.update_bmff_hash(bmff_hash)?; 2646 } 2647 } 2648 } else { 2649 // we will not do automatic hashing if we detect a box hash present 2650 let mut needs_hashing = false; 2651 if pc.box_hash_assertions().is_empty() { 2652 // 2) Get hash ranges if needed, do not generate for update manifests 2653 let mut hash_ranges = object_locations(&output_path)?; 2654 let hashes: Vec<DataHash> = if pc.update_manifest() { 2655 Vec::new() 2656 } else { 2657 Store::generate_data_hashes(dest_path, pc.alg(), &mut hash_ranges, false)? 2658 }; 2659 2660 // add the placeholder data hashes to provenance claim so that the required space is reserved 2661 for mut hash in hashes { 2662 // add padding to account for possible cbor expansion of final DataHash 2663 let padding: Vec<u8> = vec![0x0; 10]; 2664 hash.add_padding(padding); 2665 2666 pc.add_assertion(&hash)?; 2667 } 2668 needs_hashing = true; 2669 } 2670 2671 // 3) Generate in memory CAI jumbf block 2672 // and write preliminary jumbf store to file 2673 // source and dest the same so save_jumbf_to_file will use the same file since we have already cloned 2674 data = self.to_jumbf_internal(reserve_size)?; 2675 jumbf_size = data.len(); 2676 save_jumbf_to_file(&data, &output_path, Some(&output_path))?; 2677 2678 // 4) determine final object locations and patch the asset hashes with correct offset 2679 // replace the source with correct asset hashes so that the claim hash will be correct 2680 // If box hash is present we don't do any other 2681 if needs_hashing { 2682 let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; 2683 2684 // get the final hash ranges, but not for update manifests 2685 let mut new_hash_ranges = object_locations(&output_path)?; 2686 let updated_hashes = if pc.update_manifest() { 2687 Vec::new() 2688 } else { 2689 Store::generate_data_hashes(dest_path, pc.alg(), &mut new_hash_ranges, true)? 2690 }; 2691 2692 // patch existing claim hash with updated data 2693 for hash in updated_hashes { 2694 pc.update_data_hash(hash)?; 2695 } 2696 } 2697 } 2698 2699 // regenerate the jumbf because the cbor changed 2700 data = self.to_jumbf_internal(reserve_size)?; 2701 if jumbf_size != data.len() { 2702 return Err(Error::JumbfCreationError); 2703 } 2704 2705 Ok(data) // return JUMBF data 2706 } 2707 2708 #[cfg(feature = "file_io")] 2709 fn finish_save( 2710 &self, 2711 mut jumbf_bytes: Vec<u8>, 2712 output_path: &Path, 2713 sig: Vec<u8>, 2714 sig_placeholder: &[u8], 2715 ) -> Result<(Vec<u8>, Vec<u8>)> { 2716 if sig_placeholder.len() != sig.len() { 2717 return Err(Error::CoseSigboxTooSmall); 2718 } 2719 2720 patch_bytes(&mut jumbf_bytes, sig_placeholder, &sig) 2721 .map_err(|_| Error::JumbfCreationError)?; 2722 2723 // re-save to file 2724 save_jumbf_to_file(&jumbf_bytes, output_path, Some(output_path))?; 2725 2726 Ok((sig, jumbf_bytes)) 2727 } 2728 2729 /// Verify Store from an existing asset 2730 /// asset_path: path to input asset 2731 /// validation_log: If present all found errors are logged and returned, otherwise first error causes exit and is returned 2732 #[cfg(feature = "file_io")] 2733 pub fn verify_from_path( 2734 &mut self, 2735 asset_path: &'_ Path, 2736 validation_log: &mut impl StatusTracker, 2737 ) -> Result<()> { 2738 Store::verify_store(self, &mut ClaimAssetData::Path(asset_path), validation_log) 2739 } 2740 2741 // verify from a buffer without file i/o 2742 pub fn verify_from_buffer( 2743 &mut self, 2744 buf: &[u8], 2745 asset_type: &str, 2746 validation_log: &mut impl StatusTracker, 2747 ) -> Result<()> { 2748 Store::verify_store( 2749 self, 2750 &mut ClaimAssetData::Bytes(buf, asset_type), 2751 validation_log, 2752 ) 2753 } 2754 2755 // verify from a buffer without file i/o 2756 pub fn verify_from_stream( 2757 &mut self, 2758 reader: &mut dyn CAIRead, 2759 asset_type: &str, 2760 validation_log: &mut impl StatusTracker, 2761 ) -> Result<()> { 2762 Store::verify_store( 2763 self, 2764 &mut ClaimAssetData::Stream(reader, asset_type), 2765 validation_log, 2766 ) 2767 } 2768 2769 // fetch remote manifest if possible 2770 #[cfg(feature = "fetch_remote_manifests")] 2771 fn fetch_remote_manifest(url: &str) -> Result<Vec<u8>> { 2772 use conv::ValueFrom; 2773 use ureq::Error as uError; 2774 2775 //const MANIFEST_CONTENT_TYPE: &str = "application/x-c2pa-manifest-store"; // todo verify once these are served 2776 const DEFAULT_MANIFEST_RESPONSE_SIZE: usize = 10 * 1024 * 1024; // 10 MB 2777 2778 match ureq::get(url).call() { 2779 Ok(response) => { 2780 if response.status() == 200 { 2781 let len = response 2782 .header("Content-Length") 2783 .and_then(|s| s.parse::<usize>().ok()) 2784 .unwrap_or(DEFAULT_MANIFEST_RESPONSE_SIZE); // todo figure out good max to accept 2785 2786 let mut response_bytes: Vec<u8> = Vec::with_capacity(len); 2787 2788 let len64 = u64::value_from(len) 2789 .map_err(|_err| Error::BadParam("value out of range".to_string()))?; 2790 2791 response 2792 .into_reader() 2793 .take(len64) 2794 .read_to_end(&mut response_bytes) 2795 .map_err(|_err| { 2796 Error::RemoteManifestFetch("error reading content stream".to_string()) 2797 })?; 2798 2799 Ok(response_bytes) 2800 } else { 2801 Err(Error::RemoteManifestFetch(format!( 2802 "fetch failed: code: {}, status: {}", 2803 response.status(), 2804 response.status_text() 2805 ))) 2806 } 2807 } 2808 Err(uError::Status(code, resp)) => Err(Error::RemoteManifestFetch(format!( 2809 "code: {}, response: {}", 2810 code, 2811 resp.status_text() 2812 ))), 2813 Err(uError::Transport(_)) => Err(Error::RemoteManifestFetch(url.to_string())), 2814 } 2815 } 2816 2817 /// Handles remote manifests when file_io/fetch_remote_manifests feature is enabled 2818 fn handle_remote_manifest(ext_ref: &str) -> Result<Vec<u8>> { 2819 // verify provenance path is remote url 2820 if Store::is_valid_remote_url(ext_ref) { 2821 #[cfg(feature = "fetch_remote_manifests")] 2822 { 2823 Store::fetch_remote_manifest(ext_ref) 2824 } 2825 #[cfg(not(feature = "fetch_remote_manifests"))] 2826 Err(Error::RemoteManifestUrl(ext_ref.to_owned())) 2827 } else { 2828 Err(Error::JumbfNotFound) 2829 } 2830 } 2831 2832 /// Return Store from in memory asset 2833 fn load_cai_from_memory( 2834 asset_type: &str, 2835 data: &[u8], 2836 validation_log: &mut impl StatusTracker, 2837 ) -> Result<Store> { 2838 let mut input_stream = Cursor::new(data); 2839 Store::load_jumbf_from_stream(asset_type, &mut input_stream) 2840 .map(|manifest_bytes| Store::from_jumbf(&manifest_bytes, validation_log))? 2841 } 2842 2843 /// load jumbf given a stream 2844 /// 2845 /// This handles, embedded and remote manifests 2846 /// 2847 /// asset_type - mime type of the stream 2848 /// stream - a readable stream of an asset 2849 pub fn load_jumbf_from_stream(asset_type: &str, stream: &mut dyn CAIRead) -> Result<Vec<u8>> { 2850 match load_jumbf_from_stream(asset_type, stream) { 2851 Ok(manifest_bytes) => Ok(manifest_bytes), 2852 Err(Error::JumbfNotFound) => { 2853 stream.rewind()?; 2854 if let Some(ext_ref) = 2855 crate::utils::xmp_inmemory_utils::XmpInfo::from_source(stream, asset_type) 2856 .provenance 2857 { 2858 Store::handle_remote_manifest(&ext_ref) 2859 } else { 2860 Err(Error::JumbfNotFound) 2861 } 2862 } 2863 Err(e) => Err(e), 2864 } 2865 } 2866 2867 /// load jumbf given a file path 2868 /// 2869 /// This handles, embedded, sidecar and remote manifests 2870 /// 2871 /// in_path - path to source file 2872 /// validation_log - optional vec to contain addition info about the asset 2873 #[cfg(feature = "file_io")] 2874 pub fn load_jumbf_from_path(in_path: &Path) -> Result<Vec<u8>> { 2875 let external_manifest = in_path.with_extension(MANIFEST_STORE_EXT); 2876 let external_exists = external_manifest.exists(); 2877 2878 match load_jumbf_from_file(in_path) { 2879 Ok(manifest_bytes) => Ok(manifest_bytes), 2880 Err(Error::UnsupportedType) => { 2881 if external_exists { 2882 std::fs::read(external_manifest).map_err(Error::IoError) 2883 } else { 2884 Err(Error::UnsupportedType) 2885 } 2886 } 2887 Err(Error::JumbfNotFound) => { 2888 if external_exists { 2889 std::fs::read(external_manifest).map_err(Error::IoError) 2890 } else { 2891 // check for remote manifest 2892 let mut asset_reader = std::fs::File::open(in_path)?; 2893 let ext = get_file_extension(in_path).ok_or(Error::UnsupportedType)?; 2894 if let Some(ext_ref) = crate::utils::xmp_inmemory_utils::XmpInfo::from_source( 2895 &mut asset_reader, 2896 &ext, 2897 ) 2898 .provenance 2899 { 2900 Store::handle_remote_manifest(&ext_ref) 2901 } else { 2902 Err(Error::JumbfNotFound) 2903 } 2904 } 2905 } 2906 Err(e) => Err(e), 2907 } 2908 } 2909 2910 /// load a CAI store from a file 2911 /// 2912 /// in_path - path to source file 2913 /// validation_log - optional vec to contain addition info about the asset 2914 #[cfg(feature = "file_io")] 2915 fn load_cai_from_file( 2916 in_path: &Path, 2917 validation_log: &mut impl StatusTracker, 2918 ) -> Result<Store> { 2919 match Self::load_jumbf_from_path(in_path) { 2920 Ok(manifest_bytes) => { 2921 // load and validate with CAI toolkit 2922 Store::from_jumbf(&manifest_bytes, validation_log) 2923 } 2924 Err(e) => Err(e), 2925 } 2926 } 2927 2928 /// Load Store from claims in an existing asset 2929 /// asset_path: path to input asset 2930 /// verify: determines whether to verify the contents of the provenance claim. Must be set true to use validation_log 2931 /// validation_log: If present all found errors are logged and returned, otherwise first error causes exit and is returned 2932 #[cfg(feature = "file_io")] 2933 pub fn load_from_asset( 2934 asset_path: &Path, 2935 verify: bool, 2936 validation_log: &mut impl StatusTracker, 2937 ) -> Result<Store> { 2938 // load jumbf if available 2939 Self::load_cai_from_file(asset_path, validation_log) 2940 .and_then(|mut store| { 2941 // verify the store 2942 if verify { 2943 store.verify_from_path(asset_path, validation_log)?; 2944 } 2945 2946 Ok(store) 2947 }) 2948 .map_err(|e| { 2949 validation_log.log_silent( 2950 log_item!("asset", "error loading file", "load_from_asset").set_error(&e), 2951 ); 2952 e 2953 }) 2954 } 2955 2956 pub fn get_store_from_memory( 2957 asset_type: &str, 2958 data: &[u8], 2959 validation_log: &mut impl StatusTracker, 2960 ) -> Result<Store> { 2961 // load jumbf if available 2962 Self::load_cai_from_memory(asset_type, data, validation_log).map_err(|e| { 2963 validation_log.log_silent( 2964 log_item!("asset", "error loading asset", "get_store_from_memory").set_error(&e), 2965 ); 2966 e 2967 }) 2968 } 2969 2970 /// Returns embedded remote manifest URL if available 2971 /// asset_type: extensions or mime type of the data 2972 /// data: byte array containing the asset 2973 pub fn get_remote_manifest_url(asset_type: &str, data: &[u8]) -> Option<String> { 2974 let mut buf_reader = Cursor::new(data); 2975 2976 if let Some(ext_ref) = 2977 crate::utils::xmp_inmemory_utils::XmpInfo::from_source(&mut buf_reader, asset_type) 2978 .provenance 2979 { 2980 // make sure it parses 2981 let _u = url::Url::parse(&ext_ref).ok()?; 2982 Some(ext_ref) 2983 } else { 2984 None 2985 } 2986 } 2987 2988 /// check the input url to see if it is a supported remotes URI 2989 pub fn is_valid_remote_url(url: &str) -> bool { 2990 match url::Url::parse(url) { 2991 Ok(u) => u.scheme() == "http" || u.scheme() == "https", 2992 Err(_) => false, 2993 } 2994 } 2995 2996 /// Load Store from a in-memory asset 2997 /// asset_type: asset extension or mime type 2998 /// data: reference to bytes of the the file 2999 /// verify: if true will run verification checks when loading 3000 /// validation_log: If present all found errors are logged and returned, otherwise first error causes exit and is returned 3001 pub fn load_from_memory( 3002 asset_type: &str, 3003 data: &[u8], 3004 verify: bool, 3005 validation_log: &mut impl StatusTracker, 3006 ) -> Result<Store> { 3007 Store::get_store_from_memory(asset_type, data, validation_log).and_then(|store| { 3008 // verify the store 3009 if verify { 3010 // verify store and claims 3011 Store::verify_store( 3012 &store, 3013 &mut ClaimAssetData::Bytes(data, asset_type), 3014 validation_log, 3015 )?; 3016 } 3017 3018 Ok(store) 3019 }) 3020 } 3021 3022 /// Load Store from a in-memory asset asynchronously validating 3023 /// asset_type: asset extension or mime type 3024 /// data: reference to bytes of the file 3025 /// verify: if true will run verification checks when loading 3026 /// validation_log: If present all found errors are logged and returned, otherwise first error causes exit and is returned 3027 pub async fn load_from_memory_async( 3028 asset_type: &str, 3029 data: &[u8], 3030 verify: bool, 3031 validation_log: &mut impl StatusTracker, 3032 ) -> Result<Store> { 3033 let store = Store::get_store_from_memory(asset_type, data, validation_log)?; 3034 3035 // verify the store 3036 if verify { 3037 // verify store and claims 3038 Store::verify_store_async( 3039 &store, 3040 &mut ClaimAssetData::Bytes(data, asset_type), 3041 validation_log, 3042 ) 3043 .await?; 3044 } 3045 3046 Ok(store) 3047 } 3048 3049 /// Load Store from a in-memory asset 3050 /// asset_type: asset extension or mime type 3051 /// data: reference to bytes of the the file 3052 /// verify: if true will run verification checks when loading 3053 /// validation_log: If present all found errors are logged and returned, otherwise first error causes exit and is returned 3054 pub fn load_fragment_from_memory( 3055 asset_type: &str, 3056 init_segment: &[u8], 3057 fragment: &[u8], 3058 verify: bool, 3059 validation_log: &mut impl StatusTracker, 3060 ) -> Result<Store> { 3061 Store::get_store_from_memory(asset_type, init_segment, validation_log).and_then(|store| { 3062 // verify the store 3063 if verify { 3064 let mut init_segment_stream = Cursor::new(init_segment); 3065 let mut fragment_stream = Cursor::new(fragment); 3066 3067 // verify store and claims 3068 Store::verify_store( 3069 &store, 3070 &mut ClaimAssetData::StreamFragment( 3071 &mut init_segment_stream, 3072 &mut fragment_stream, 3073 asset_type, 3074 ), 3075 validation_log, 3076 )?; 3077 } 3078 3079 Ok(store) 3080 }) 3081 } 3082 3083 /// Load Store from a in-memory asset asynchronously validating 3084 /// asset_type: asset extension or mime type 3085 /// init_segment: reference to bytes of the init segment 3086 /// fragment: reference to bytes of the fragment to validate 3087 /// verify: if true will run verification checks when loading 3088 /// validation_log: If present all found errors are logged and returned, otherwise first error causes exit and is returned 3089 pub async fn load_fragment_from_memory_async( 3090 asset_type: &str, 3091 init_segment: &[u8], 3092 fragment: &[u8], 3093 verify: bool, 3094 validation_log: &mut impl StatusTracker, 3095 ) -> Result<Store> { 3096 let store = Store::get_store_from_memory(asset_type, init_segment, validation_log)?; 3097 3098 // verify the store 3099 if verify { 3100 let mut init_segment_stream = Cursor::new(init_segment); 3101 let mut fragment_stream = Cursor::new(fragment); 3102 3103 // verify store and claims 3104 Store::verify_store_async( 3105 &store, 3106 &mut ClaimAssetData::StreamFragment( 3107 &mut init_segment_stream, 3108 &mut fragment_stream, 3109 asset_type, 3110 ), 3111 validation_log, 3112 ) 3113 .await?; 3114 } 3115 3116 Ok(store) 3117 } 3118 3119 /// Load Store from memory and add its content as a claim ingredient 3120 /// claim: claim to add an ingredient 3121 /// provenance_label: label of the provenance claim used as key into ingredient map 3122 /// data: jumbf data block 3123 pub fn load_ingredient_to_claim( 3124 claim: &mut Claim, 3125 provenance_label: &str, 3126 data: &[u8], 3127 redactions: Option<Vec<String>>, 3128 ) -> Result<Store> { 3129 let mut report = OneShotStatusTracker::new(); 3130 let store = Store::from_jumbf(data, &mut report)?; 3131 claim.add_ingredient_data(provenance_label, store.claims.clone(), redactions)?; 3132 Ok(store) 3133 } 3134 } 3135 3136 impl std::fmt::Display for Store { 3137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 3138 let report = &ManifestStoreReport::from_store(self).unwrap_or_default(); 3139 f.write_str(&format!("{}", &report)) 3140 } 3141 } 3142 3143 /// `InvalidClaimError` provides additional detail on error cases for [`Store::from_jumbf`]. 3144 #[derive(Debug, thiserror::Error)] 3145 pub enum InvalidClaimError { 3146 /// The "c2pa" block was not found in the asset. 3147 #[error("\"c2pa\" block not found")] 3148 C2paBlockNotFound, 3149 3150 #[error("\"c2pa\" multiple claim boxes found in manifest")] 3151 C2paMultipleClaimBoxes, 3152 3153 /// The claim superbox was not found. 3154 #[error("claim superbox not found")] 3155 ClaimSuperboxNotFound, 3156 3157 /// The claim description box was not found. 3158 #[error("claim description box not found")] 3159 ClaimDescriptionBoxNotFound, 3160 3161 /// More than one claim description box was found. 3162 #[error("more than one claim description box was found for {label}")] 3163 DuplicateClaimBox { label: String }, 3164 3165 /// The expected data not found in claim box. 3166 #[error("claim cbor box not valid")] 3167 ClaimBoxData, 3168 3169 /// The claim has a version that is newer than supported by this crate. 3170 #[error("claim version is too new, not supported")] 3171 ClaimVersionTooNew, 3172 3173 /// The claim description box could not be parsed. 3174 #[error("claim description box was invalid")] 3175 ClaimDescriptionBoxInvalid, 3176 3177 /// The claim signature box was not found. 3178 #[error("claim signature box was not found")] 3179 ClaimSignatureBoxNotFound, 3180 3181 /// The claim signature description box was not found. 3182 #[error("claim signature description box was not found")] 3183 ClaimSignatureDescriptionBoxNotFound, 3184 3185 /// The claim signature description box was invalid. 3186 #[error("claim signature description box was invalid")] 3187 ClaimSignatureDescriptionBoxInvalid, 3188 3189 /// The assertion store superbox was not found. 3190 #[error("assertion store superbox not found")] 3191 AssertionStoreSuperboxNotFound, 3192 3193 /// The verifiable credentials store could not be read. 3194 #[error("the verifiable credentials store could not be read")] 3195 VerifiableCredentialStoreInvalid, 3196 3197 /// The assertion store does not contain the expected number of assertions. 3198 #[error( 3199 "unexpected number of assertions in assertion store (expected {expected}, found {found})" 3200 )] 3201 AssertionCountMismatch { expected: usize, found: usize }, 3202 } 3203 3204 #[cfg(test)] 3205 #[cfg(feature = "file_io")] 3206 pub mod tests { 3207 #![allow(clippy::expect_used)] 3208 #![allow(clippy::panic)] 3209 #![allow(clippy::unwrap_used)] 3210 3211 use std::io::Write; 3212 3213 use memchr::memmem; 3214 use sha2::{Digest, Sha256}; 3215 use tempfile::tempdir; 3216 3217 use super::*; 3218 use crate::{ 3219 assertion::AssertionJson, 3220 assertions::{labels::BOX_HASH, Action, Actions, BoxHash, Uuid}, 3221 claim::AssertionStoreJsonFormat, 3222 jumbf_io::{get_assetio_handler_from_path, update_file_jumbf}, 3223 status_tracker::*, 3224 utils::{ 3225 hash_utils::Hasher, 3226 patch::patch_file, 3227 test::{ 3228 create_test_claim, fixture_path, temp_dir_path, temp_fixture_path, temp_signer, 3229 write_jpeg_placeholder_file, 3230 }, 3231 }, 3232 SigningAlg, 3233 }; 3234 3235 fn create_editing_claim(claim: &mut Claim) -> Result<&mut Claim> { 3236 let uuid_str = "deadbeefdeadbeefdeadbeefdeadbeef"; 3237 3238 // add a binary thumbnail assertion ('deadbeefadbeadbe') 3239 let some_binary_data: Vec<u8> = vec![ 3240 0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d, 3241 0x0b, 0x0e, 3242 ]; 3243 3244 let uuid_assertion = Uuid::new("test uuid", uuid_str.to_string(), some_binary_data); 3245 3246 claim.add_assertion(&uuid_assertion)?; 3247 3248 Ok(claim) 3249 } 3250 3251 fn create_capture_claim(claim: &mut Claim) -> Result<&mut Claim> { 3252 let actions = Actions::new().add_action(Action::new("c2pa.created")); 3253 3254 claim.add_assertion(&actions)?; 3255 3256 Ok(claim) 3257 } 3258 3259 #[test] 3260 #[cfg(feature = "file_io")] 3261 fn test_jumbf_generation() { 3262 // test adding to actual image 3263 let ap = fixture_path("earth_apollo17.jpg"); 3264 let temp_dir = tempdir().expect("temp dir"); 3265 let op = temp_dir_path(&temp_dir, "test-image.jpg"); 3266 3267 // Create claims store. 3268 let mut store = Store::new(); 3269 3270 // Create a new claim. 3271 let claim1 = create_test_claim().unwrap(); 3272 3273 // Create a new claim. 3274 let mut claim2 = Claim::new("Photoshop", Some("Adobe")); 3275 create_editing_claim(&mut claim2).unwrap(); 3276 3277 // Create a 3rd party claim 3278 let mut claim_capture = Claim::new("capture", Some("claim_capture")); 3279 create_capture_claim(&mut claim_capture).unwrap(); 3280 3281 // Do we generate JUMBF? 3282 let signer = temp_signer(); 3283 3284 // Test generate JUMBF 3285 // Get labels for label test 3286 let claim1_label = claim1.label().to_string(); 3287 let capture = claim_capture.label().to_string(); 3288 let claim2_label = claim2.label().to_string(); 3289 3290 // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commits 3291 store.commit_claim(claim1).unwrap(); 3292 store.save_to_asset(&ap, signer.as_ref(), &op).unwrap(); 3293 store.commit_claim(claim_capture).unwrap(); 3294 store.save_to_asset(&op, signer.as_ref(), &op).unwrap(); 3295 store.commit_claim(claim2).unwrap(); 3296 store.save_to_asset(&op, signer.as_ref(), &op).unwrap(); 3297 3298 // test finding claims by label 3299 let c1 = store.get_claim(&claim1_label); 3300 let c2 = store.get_claim(&capture); 3301 let c3 = store.get_claim(&claim2_label); 3302 assert_eq!(&claim1_label, c1.unwrap().label()); 3303 assert_eq!(&capture, c2.unwrap().label()); 3304 assert_eq!(claim2_label, c3.unwrap().label()); 3305 3306 // write to new file 3307 println!("Provenance: {}\n", store.provenance_path().unwrap()); 3308 3309 // read from new file 3310 let new_store = 3311 Store::load_from_asset(&op, true, &mut OneShotStatusTracker::new()).unwrap(); 3312 3313 // can we get by the ingredient data back 3314 let _some_binary_data: Vec<u8> = vec![ 3315 0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d, 3316 0x0b, 0x0e, 3317 ]; 3318 3319 // dump store and compare to original 3320 for claim in new_store.claims() { 3321 let _restored_json = claim 3322 .to_json(AssertionStoreJsonFormat::OrderedList, false) 3323 .unwrap(); 3324 let _orig_json = store 3325 .get_claim(claim.label()) 3326 .unwrap() 3327 .to_json(AssertionStoreJsonFormat::OrderedList, false) 3328 .unwrap(); 3329 3330 // these better match 3331 //assert_eq!(orig_json, restored_json); 3332 //assert_eq!(claim.hash(), store.claims()[idx].hash()); 3333 3334 println!( 3335 "Claim: {} \n{}", 3336 claim.label(), 3337 claim 3338 .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true) 3339 .expect("could not restore from json") 3340 ); 3341 3342 for hashed_uri in claim.assertions() { 3343 let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url()); 3344 claim.get_claim_assertion(&label, instance).unwrap(); 3345 } 3346 } 3347 3348 // test patch file - bytes should be same so error should not be detected 3349 let mut splice_point = 3350 patch_file(&op, "thumbnail".as_bytes(), "testme".as_bytes()).unwrap(); 3351 3352 let mut restore_point = 3353 patch_file(&op, "testme".as_bytes(), "thumbnail".as_bytes()).unwrap(); 3354 3355 assert_eq!(splice_point, restore_point); 3356 3357 Store::load_from_asset(&op, true, &mut OneShotStatusTracker::new()) 3358 .expect("Should still verify"); 3359 3360 // test patching jumbf - error should be detected 3361 3362 splice_point = update_file_jumbf(&op, "thumbnail".as_bytes(), "testme".as_bytes()).unwrap(); 3363 restore_point = 3364 update_file_jumbf(&op, "testme".as_bytes(), "thumbnail.v1".as_bytes()).unwrap(); 3365 3366 assert_eq!(splice_point, restore_point); 3367 3368 Store::load_from_asset(&op, true, &mut OneShotStatusTracker::new()) 3369 .expect_err("Should not verify"); 3370 } 3371 3372 #[test] 3373 #[cfg(feature = "file_io")] 3374 fn test_unknown_asset_type_generation() { 3375 // test adding to actual image 3376 let ap = fixture_path("unsupported_type.txt"); 3377 let temp_dir = tempdir().expect("temp dir"); 3378 let op = temp_dir_path(&temp_dir, "unsupported_type.txt"); 3379 3380 // Create claims store. 3381 let mut store = Store::new(); 3382 3383 // Create a new claim. 3384 let claim1 = create_test_claim().unwrap(); 3385 3386 // Create a new claim. 3387 let mut claim2 = Claim::new("Photoshop", Some("Adobe")); 3388 create_editing_claim(&mut claim2).unwrap(); 3389 3390 // Create a 3rd party claim 3391 let mut claim_capture = Claim::new("capture", Some("claim_capture")); 3392 create_capture_claim(&mut claim_capture).unwrap(); 3393 3394 // Do we generate JUMBF? 3395 let signer = temp_signer(); 3396 3397 // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commits 3398 store.commit_claim(claim1).unwrap(); 3399 store.save_to_asset(&ap, signer.as_ref(), &op).unwrap(); 3400 3401 // read from new file 3402 let new_store = 3403 Store::load_from_asset(&op, true, &mut OneShotStatusTracker::new()).unwrap(); 3404 3405 // can we get by the ingredient data back 3406 3407 // dump store and compare to original 3408 for claim in new_store.claims() { 3409 let _restored_json = claim 3410 .to_json(AssertionStoreJsonFormat::OrderedList, false) 3411 .unwrap(); 3412 let _orig_json = store 3413 .get_claim(claim.label()) 3414 .unwrap() 3415 .to_json(AssertionStoreJsonFormat::OrderedList, false) 3416 .unwrap(); 3417 3418 println!( 3419 "Claim: {} \n{}", 3420 claim.label(), 3421 claim 3422 .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true) 3423 .expect("could not restore from json") 3424 ); 3425 3426 for hashed_uri in claim.assertions() { 3427 let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url()); 3428 claim.get_claim_assertion(&label, instance).unwrap(); 3429 } 3430 } 3431 } 3432 3433 struct BadSigner {} 3434 3435 impl crate::Signer for BadSigner { 3436 fn sign(&self, _data: &[u8]) -> Result<Vec<u8>> { 3437 Ok(b"not a valid signature".to_vec()) 3438 } 3439 3440 fn alg(&self) -> SigningAlg { 3441 SigningAlg::Ps256 3442 } 3443 3444 fn certs(&self) -> Result<Vec<Vec<u8>>> { 3445 Ok(Vec::new()) 3446 } 3447 3448 fn reserve_size(&self) -> usize { 3449 42 3450 } 3451 } 3452 3453 #[test] 3454 #[cfg(feature = "file_io")] 3455 fn test_detects_unverifiable_signature() { 3456 // test adding to actual image 3457 let ap = fixture_path("earth_apollo17.jpg"); 3458 let temp_dir = tempdir().expect("temp dir"); 3459 let op = temp_dir_path(&temp_dir, "test-image-unverified.jpg"); 3460 3461 let mut store = Store::new(); 3462 3463 let claim = create_test_claim().unwrap(); 3464 3465 let signer = BadSigner {}; 3466 3467 // JUMBF generation should fail because this signature won't validate. 3468 store.commit_claim(claim).unwrap(); 3469 3470 // TO DO: This generates a log spew when running this test. 3471 // I don't have time to fix this right now. 3472 // [(date) ERROR c2pa::store] Signature that was just generated does not validate: CoseCbor 3473 3474 store.save_to_asset(&ap, &signer, &op).unwrap_err(); 3475 } 3476 3477 #[test] 3478 #[cfg(feature = "file_io")] 3479 fn test_sign_with_expired_cert() { 3480 use crate::{openssl::RsaSigner, signer::ConfigurableSigner, SigningAlg}; 3481 3482 // test adding to actual image 3483 let ap = fixture_path("earth_apollo17.jpg"); 3484 let temp_dir = tempdir().expect("temp dir"); 3485 let op = temp_dir_path(&temp_dir, "test-image-expired-cert.jpg"); 3486 3487 let mut store = Store::new(); 3488 3489 let claim = create_test_claim().unwrap(); 3490 3491 let signcert_path = fixture_path("rsa-pss256_key-expired.pub"); 3492 let pkey_path = fixture_path("rsa-pss256-expired.pem"); 3493 let signer = 3494 RsaSigner::from_files(signcert_path, pkey_path, SigningAlg::Ps256, None).unwrap(); 3495 3496 store.commit_claim(claim).unwrap(); 3497 3498 let r = store.save_to_asset(&ap, &signer, &op); 3499 assert!(r.is_err()); 3500 assert_eq!(r.err().unwrap().to_string(), "COSE certificate has expired"); 3501 } 3502 3503 #[test] 3504 #[cfg(feature = "file_io")] 3505 fn test_jumbf_replacement_generation() { 3506 // Create claims store. 3507 let mut store = Store::new(); 3508 3509 // Create a new claim. 3510 let claim1 = create_test_claim().unwrap(); 3511 store.commit_claim(claim1).unwrap(); 3512 3513 // do we generate JUMBF 3514 let jumbf_bytes = store.to_jumbf_internal(512).unwrap(); 3515 assert!(!jumbf_bytes.is_empty()); 3516 3517 // test adding to actual image 3518 let ap = fixture_path("prerelease.jpg"); 3519 let temp_dir = tempdir().expect("temp dir"); 3520 let op = temp_dir_path(&temp_dir, "replacement_test.jpg"); 3521 3522 // grab jumbf from original 3523 let original_jumbf = load_jumbf_from_file(&ap).unwrap(); 3524 3525 // replace with new jumbf 3526 save_jumbf_to_file(&jumbf_bytes, &ap, Some(&op)).unwrap(); 3527 3528 let saved_jumbf = load_jumbf_from_file(&op).unwrap(); 3529 3530 // saved data should be the new data 3531 assert_eq!(&jumbf_bytes, &saved_jumbf); 3532 3533 // original data should not be in file anymore check for first 1k 3534 let buf = fs::read(&op).unwrap(); 3535 assert!(memmem::find(&buf, &original_jumbf[0..1024]).is_none()); 3536 } 3537 3538 #[actix::test] 3539 async fn test_jumbf_generation_async() { 3540 let signer = crate::openssl::temp_signer_async::AsyncSignerAdapter::new(SigningAlg::Ps256); 3541 3542 // test adding to actual image 3543 let ap = fixture_path("earth_apollo17.jpg"); 3544 let temp_dir = tempdir().expect("temp dir"); 3545 let op = temp_dir_path(&temp_dir, "test-async.jpg"); 3546 3547 // Create claims store. 3548 let mut store = Store::new(); 3549 3550 // Create a new claim. 3551 let claim1 = create_test_claim().unwrap(); 3552 3553 // Create a new claim. 3554 let mut claim2 = Claim::new("Photoshop", Some("Adobe")); 3555 create_editing_claim(&mut claim2).unwrap(); 3556 3557 // Create a 3rd party claim 3558 let mut claim_capture = Claim::new("capture", Some("claim_capture")); 3559 create_capture_claim(&mut claim_capture).unwrap(); 3560 3561 // Test generate JUMBF 3562 // Get labels for label test 3563 let claim1_label = claim1.label().to_string(); 3564 let capture = claim_capture.label().to_string(); 3565 let claim2_label = claim2.label().to_string(); 3566 3567 store.commit_claim(claim1).unwrap(); 3568 store.save_to_asset_async(&ap, &signer, &op).await.unwrap(); 3569 store.commit_claim(claim_capture).unwrap(); 3570 store.save_to_asset_async(&ap, &signer, &op).await.unwrap(); 3571 store.commit_claim(claim2).unwrap(); 3572 store.save_to_asset_async(&ap, &signer, &op).await.unwrap(); 3573 3574 // test finding claims by label 3575 let c1 = store.get_claim(&claim1_label); 3576 let c2 = store.get_claim(&capture); 3577 let c3 = store.get_claim(&claim2_label); 3578 assert_eq!(&claim1_label, c1.unwrap().label()); 3579 assert_eq!(&capture, c2.unwrap().label()); 3580 assert_eq!(claim2_label, c3.unwrap().label()); 3581 3582 // Do we generate JUMBF 3583 let jumbf_bytes = store.to_jumbf_async(&signer).unwrap(); 3584 assert!(!jumbf_bytes.is_empty()); 3585 3586 // write to new file 3587 println!("Provenance: {}\n", store.provenance_path().unwrap()); 3588 3589 // make sure we can read from new file 3590 let mut report = DetailedStatusTracker::new(); 3591 let _new_store = Store::load_from_asset(&op, true, &mut report).unwrap(); 3592 } 3593 3594 #[actix::test] 3595 async fn test_jumbf_generation_remote() { 3596 // test adding to actual image 3597 let ap = fixture_path("earth_apollo17.jpg"); 3598 let temp_dir = tempdir().expect("temp dir"); 3599 let op = temp_dir_path(&temp_dir, "test-async.jpg"); 3600 3601 // Create claims store. 3602 let mut store = Store::new(); 3603 3604 // Create a new claim. 3605 let claim1 = create_test_claim().unwrap(); 3606 3607 // create my remote signer to map the CoseSign1 data back into the asset 3608 let remote_signer = crate::utils::test::temp_remote_signer(); 3609 3610 store.commit_claim(claim1).unwrap(); 3611 store 3612 .save_to_asset_remote_signed(&ap, remote_signer.as_ref(), &op) 3613 .await 3614 .unwrap(); 3615 3616 // make sure we can read from new file 3617 let mut report = DetailedStatusTracker::new(); 3618 let _new_store = Store::load_from_asset(&op, true, &mut report).unwrap(); 3619 } 3620 3621 #[test] 3622 #[cfg(feature = "file_io")] 3623 fn test_png_jumbf_generation() { 3624 // test adding to actual image 3625 let ap = fixture_path("libpng-test.png"); 3626 let temp_dir = tempdir().expect("temp dir"); 3627 let op = temp_dir_path(&temp_dir, "libpng-test-c2pa.png"); 3628 3629 // Create claims store. 3630 let mut store = Store::new(); 3631 3632 // Create a new claim. 3633 let claim1 = create_test_claim().unwrap(); 3634 3635 // Create a new claim. 3636 let mut claim2 = Claim::new("Photoshop", Some("Adobe")); 3637 create_editing_claim(&mut claim2).unwrap(); 3638 3639 // Create a 3rd party claim 3640 let mut claim_capture = Claim::new("capture", Some("claim_capture")); 3641 create_capture_claim(&mut claim_capture).unwrap(); 3642 3643 // Do we generate JUMBF? 3644 let signer = temp_signer(); 3645 3646 // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commits 3647 store.commit_claim(claim1).unwrap(); 3648 store.save_to_asset(&ap, signer.as_ref(), &op).unwrap(); 3649 store.commit_claim(claim_capture).unwrap(); 3650 store.save_to_asset(&op, signer.as_ref(), &op).unwrap(); 3651 store.commit_claim(claim2).unwrap(); 3652 store.save_to_asset(&op, signer.as_ref(), &op).unwrap(); 3653 3654 // write to new file 3655 println!("Provenance: {}\n", store.provenance_path().unwrap()); 3656 3657 let mut report = DetailedStatusTracker::new(); 3658 3659 // read from new file 3660 let new_store = Store::load_from_asset(&op, true, &mut report).unwrap(); 3661 3662 // can we get by the ingredient data back 3663 let _some_binary_data: Vec<u8> = vec![ 3664 0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d, 3665 0x0b, 0x0e, 3666 ]; 3667 3668 // dump store and compare to original 3669 for claim in new_store.claims() { 3670 let _restored_json = claim 3671 .to_json(AssertionStoreJsonFormat::OrderedList, false) 3672 .unwrap(); 3673 let _orig_json = store 3674 .get_claim(claim.label()) 3675 .unwrap() 3676 .to_json(AssertionStoreJsonFormat::OrderedList, false) 3677 .unwrap(); 3678 3679 println!( 3680 "Claim: {} \n{}", 3681 claim.label(), 3682 claim 3683 .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true) 3684 .expect("could not restore from json") 3685 ); 3686 3687 for hashed_uri in claim.assertions() { 3688 let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url()); 3689 claim 3690 .get_claim_assertion(&label, instance) 3691 .expect("Should find assertion"); 3692 } 3693 } 3694 } 3695 3696 #[test] 3697 #[cfg(feature = "file_io")] 3698 fn test_get_data_boxes() { 3699 // Create a new claim. 3700 use crate::jumbf::labels::to_relative_uri; 3701 let claim1 = create_test_claim().unwrap(); 3702 3703 for (uri, db) in claim1.databoxes() { 3704 // test full path 3705 assert!(claim1.get_data_box(&uri.url()).is_some()); 3706 3707 // test with relative path 3708 let rel_path = to_relative_uri(&uri.url()); 3709 assert!(claim1.get_data_box(&rel_path).is_some()); 3710 3711 // test values 3712 assert_eq!(db, claim1.get_data_box(&uri.url()).unwrap()); 3713 } 3714 } 3715 3716 /* reenable this test once we place for large test files 3717 #[test] 3718 #[cfg(feature = "file_io")] 3719 fn test_arw_jumbf_generation() { 3720 let ap = fixture_path("sample1.arw"); 3721 let temp_dir = tempdir().expect("temp dir"); 3722 let op = temp_dir_path(&temp_dir, "ssample1.arw"); 3723 3724 // Create claims store. 3725 let mut store = Store::new(); 3726 3727 // Create a new claim. 3728 let claim1 = create_test_claim().unwrap(); 3729 3730 // Create a new claim. 3731 let mut claim2 = Claim::new("Photoshop", Some("Adobe")); 3732 create_editing_claim(&mut claim2).unwrap(); 3733 3734 // Create a 3rd party claim 3735 let mut claim_capture = Claim::new("capture", Some("claim_capture")); 3736 create_capture_claim(&mut claim_capture).unwrap(); 3737 3738 // Do we generate JUMBF? 3739 let signer = temp_signer(); 3740 3741 // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commmits 3742 store.commit_claim(claim1).unwrap(); 3743 store.save_to_asset(&ap, signer.as_ref(), &op).unwrap(); 3744 store.commit_claim(claim_capture).unwrap(); 3745 store.save_to_asset(&op, signer.as_ref(), &op).unwrap(); 3746 store.commit_claim(claim2).unwrap(); 3747 store.save_to_asset(&op, signer.as_ref(), &op).unwrap(); 3748 3749 // write to new file 3750 println!("Provenance: {}\n", store.provenance_path().unwrap()); 3751 3752 let mut report = DetailedStatusTracker::new(); 3753 3754 // read from new file 3755 let new_store = Store::load_from_asset(&op, true, &mut report).unwrap(); 3756 3757 // can we get by the ingredient data back 3758 let _some_binary_data: Vec<u8> = vec![ 3759 0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d, 3760 0x0b, 0x0e, 3761 ]; 3762 3763 // dump store and compare to original 3764 for claim in new_store.claims() { 3765 let _restored_json = claim 3766 .to_json(AssertionStoreJsonFormat::OrderedList, false) 3767 .unwrap(); 3768 let _orig_json = store 3769 .get_claim(claim.label()) 3770 .unwrap() 3771 .to_json(AssertionStoreJsonFormat::OrderedList, false) 3772 .unwrap(); 3773 3774 println!( 3775 "Claim: {} \n{}", 3776 claim.label(), 3777 claim 3778 .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true) 3779 .expect("could not restore from json") 3780 ); 3781 3782 for hashed_uri in claim.assertions() { 3783 let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url()); 3784 claim 3785 .get_claim_assertion(&label, instance) 3786 .expect("Should find assertion"); 3787 } 3788 } 3789 } 3790 #[test] 3791 #[cfg(feature = "file_io")] 3792 fn test_nef_jumbf_generation() { 3793 let ap = fixture_path("sample1.nef"); 3794 let temp_dir = tempdir().expect("temp dir"); 3795 let op = temp_dir_path(&temp_dir, "ssample1.nef"); 3796 3797 // Create claims store. 3798 let mut store = Store::new(); 3799 3800 // Create a new claim. 3801 let claim1 = create_test_claim().unwrap(); 3802 3803 // Create a new claim. 3804 let mut claim2 = Claim::new("Photoshop", Some("Adobe")); 3805 create_editing_claim(&mut claim2).unwrap(); 3806 3807 // Create a 3rd party claim 3808 let mut claim_capture = Claim::new("capture", Some("claim_capture")); 3809 create_capture_claim(&mut claim_capture).unwrap(); 3810 3811 // Do we generate JUMBF? 3812 let signer = temp_signer(); 3813 3814 // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commmits 3815 store.commit_claim(claim1).unwrap(); 3816 store.save_to_asset(&ap, signer.as_ref(), &op).unwrap(); 3817 store.commit_claim(claim_capture).unwrap(); 3818 store.save_to_asset(&op, signer.as_ref(), &op).unwrap(); 3819 store.commit_claim(claim2).unwrap(); 3820 store.save_to_asset(&op, signer.as_ref(), &op).unwrap(); 3821 3822 // write to new file 3823 println!("Provenance: {}\n", store.provenance_path().unwrap()); 3824 3825 let mut report = DetailedStatusTracker::new(); 3826 3827 // read from new file 3828 let new_store = Store::load_from_asset(&op, true, &mut report).unwrap(); 3829 3830 // can we get by the ingredient data back 3831 let _some_binary_data: Vec<u8> = vec![ 3832 0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d, 3833 0x0b, 0x0e, 3834 ]; 3835 3836 // dump store and compare to original 3837 for claim in new_store.claims() { 3838 let _restored_json = claim 3839 .to_json(AssertionStoreJsonFormat::OrderedList, false) 3840 .unwrap(); 3841 let _orig_json = store 3842 .get_claim(claim.label()) 3843 .unwrap() 3844 .to_json(AssertionStoreJsonFormat::OrderedList, false) 3845 .unwrap(); 3846 3847 println!( 3848 "Claim: {} \n{}", 3849 claim.label(), 3850 claim 3851 .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true) 3852 .expect("could not restore from json") 3853 ); 3854 3855 for hashed_uri in claim.assertions() { 3856 let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url()); 3857 claim 3858 .get_claim_assertion(&label, instance) 3859 .expect("Should find assertion"); 3860 } 3861 } 3862 } 3863 */ 3864 #[test] 3865 #[cfg(feature = "file_io")] 3866 fn test_wav_jumbf_generation() { 3867 let ap = fixture_path("sample1.wav"); 3868 let temp_dir = tempdir().expect("temp dir"); 3869 let op = temp_dir_path(&temp_dir, "ssample1.wav"); 3870 3871 // Create claims store. 3872 let mut store = Store::new(); 3873 3874 // Create a new claim. 3875 let claim1 = create_test_claim().unwrap(); 3876 3877 // Create a new claim. 3878 let mut claim2 = Claim::new("Photoshop", Some("Adobe")); 3879 create_editing_claim(&mut claim2).unwrap(); 3880 3881 // Create a 3rd party claim 3882 let mut claim_capture = Claim::new("capture", Some("claim_capture")); 3883 create_capture_claim(&mut claim_capture).unwrap(); 3884 3885 // Do we generate JUMBF? 3886 let signer = temp_signer(); 3887 3888 // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commits 3889 store.commit_claim(claim1).unwrap(); 3890 store.save_to_asset(&ap, signer.as_ref(), &op).unwrap(); 3891 store.commit_claim(claim_capture).unwrap(); 3892 store.save_to_asset(&op, signer.as_ref(), &op).unwrap(); 3893 store.commit_claim(claim2).unwrap(); 3894 store.save_to_asset(&op, signer.as_ref(), &op).unwrap(); 3895 3896 // write to new file 3897 println!("Provenance: {}\n", store.provenance_path().unwrap()); 3898 3899 let mut report = DetailedStatusTracker::new(); 3900 3901 // read from new file 3902 let new_store = Store::load_from_asset(&op, true, &mut report).unwrap(); 3903 3904 // can we get by the ingredient data back 3905 let _some_binary_data: Vec<u8> = vec![ 3906 0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d, 3907 0x0b, 0x0e, 3908 ]; 3909 3910 // dump store and compare to original 3911 for claim in new_store.claims() { 3912 let _restored_json = claim 3913 .to_json(AssertionStoreJsonFormat::OrderedList, false) 3914 .unwrap(); 3915 let _orig_json = store 3916 .get_claim(claim.label()) 3917 .unwrap() 3918 .to_json(AssertionStoreJsonFormat::OrderedList, false) 3919 .unwrap(); 3920 3921 println!( 3922 "Claim: {} \n{}", 3923 claim.label(), 3924 claim 3925 .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true) 3926 .expect("could not restore from json") 3927 ); 3928 3929 for hashed_uri in claim.assertions() { 3930 let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url()); 3931 claim 3932 .get_claim_assertion(&label, instance) 3933 .expect("Should find assertion"); 3934 } 3935 } 3936 } 3937 3938 #[test] 3939 #[cfg(feature = "file_io")] 3940 fn test_avi_jumbf_generation() { 3941 let ap = fixture_path("test.avi"); 3942 let temp_dir = tempdir().expect("temp dir"); 3943 let op = temp_dir_path(&temp_dir, "test.avi"); 3944 3945 // Create claims store. 3946 let mut store = Store::new(); 3947 3948 // Create a new claim. 3949 let claim1 = create_test_claim().unwrap(); 3950 3951 // Create a new claim. 3952 let mut claim2 = Claim::new("Photoshop", Some("Adobe")); 3953 create_editing_claim(&mut claim2).unwrap(); 3954 3955 // Create a 3rd party claim 3956 let mut claim_capture = Claim::new("capture", Some("claim_capture")); 3957 create_capture_claim(&mut claim_capture).unwrap(); 3958 3959 // Do we generate JUMBF? 3960 let signer = temp_signer(); 3961 3962 // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commits 3963 store.commit_claim(claim1).unwrap(); 3964 store.save_to_asset(&ap, signer.as_ref(), &op).unwrap(); 3965 store.commit_claim(claim_capture).unwrap(); 3966 store.save_to_asset(&op, signer.as_ref(), &op).unwrap(); 3967 store.commit_claim(claim2).unwrap(); 3968 store.save_to_asset(&op, signer.as_ref(), &op).unwrap(); 3969 3970 // write to new file 3971 println!("Provenance: {}\n", store.provenance_path().unwrap()); 3972 3973 let mut report = DetailedStatusTracker::new(); 3974 3975 // read from new file 3976 let new_store = Store::load_from_asset(&op, true, &mut report).unwrap(); 3977 3978 // can we get by the ingredient data back 3979 let _some_binary_data: Vec<u8> = vec![ 3980 0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d, 3981 0x0b, 0x0e, 3982 ]; 3983 3984 // dump store and compare to original 3985 for claim in new_store.claims() { 3986 let _restored_json = claim 3987 .to_json(AssertionStoreJsonFormat::OrderedList, false) 3988 .unwrap(); 3989 let _orig_json = store 3990 .get_claim(claim.label()) 3991 .unwrap() 3992 .to_json(AssertionStoreJsonFormat::OrderedList, false) 3993 .unwrap(); 3994 3995 println!( 3996 "Claim: {} \n{}", 3997 claim.label(), 3998 claim 3999 .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true) 4000 .expect("could not restore from json") 4001 ); 4002 4003 for hashed_uri in claim.assertions() { 4004 let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url()); 4005 claim 4006 .get_claim_assertion(&label, instance) 4007 .expect("Should find assertion"); 4008 } 4009 } 4010 } 4011 4012 #[test] 4013 #[cfg(feature = "file_io")] 4014 fn test_webp_jumbf_generation() { 4015 let ap = fixture_path("sample1.webp"); 4016 let temp_dir = tempdir().expect("temp dir"); 4017 let op = temp_dir_path(&temp_dir, "sample1.webp"); 4018 4019 // Create claims store. 4020 let mut store = Store::new(); 4021 4022 // Create a new claim. 4023 let claim1 = create_test_claim().unwrap(); 4024 4025 // Create a new claim. 4026 let mut claim2 = Claim::new("Photoshop", Some("Adobe")); 4027 create_editing_claim(&mut claim2).unwrap(); 4028 4029 // Create a 3rd party claim 4030 let mut claim_capture = Claim::new("capture", Some("claim_capture")); 4031 create_capture_claim(&mut claim_capture).unwrap(); 4032 4033 // Do we generate JUMBF? 4034 let signer = temp_signer(); 4035 4036 // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commits 4037 store.commit_claim(claim1).unwrap(); 4038 store.save_to_asset(&ap, signer.as_ref(), &op).unwrap(); 4039 store.commit_claim(claim_capture).unwrap(); 4040 store.save_to_asset(&op, signer.as_ref(), &op).unwrap(); 4041 store.commit_claim(claim2).unwrap(); 4042 store.save_to_asset(&op, signer.as_ref(), &op).unwrap(); 4043 4044 // write to new file 4045 println!("Provenance: {}\n", store.provenance_path().unwrap()); 4046 4047 let mut report = DetailedStatusTracker::new(); 4048 4049 // read from new file 4050 let new_store = Store::load_from_asset(&op, true, &mut report).unwrap(); 4051 4052 // can we get by the ingredient data back 4053 let _some_binary_data: Vec<u8> = vec![ 4054 0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d, 4055 0x0b, 0x0e, 4056 ]; 4057 4058 // dump store and compare to original 4059 for claim in new_store.claims() { 4060 let _restored_json = claim 4061 .to_json(AssertionStoreJsonFormat::OrderedList, false) 4062 .unwrap(); 4063 let _orig_json = store 4064 .get_claim(claim.label()) 4065 .unwrap() 4066 .to_json(AssertionStoreJsonFormat::OrderedList, false) 4067 .unwrap(); 4068 4069 println!( 4070 "Claim: {} \n{}", 4071 claim.label(), 4072 claim 4073 .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true) 4074 .expect("could not restore from json") 4075 ); 4076 4077 for hashed_uri in claim.assertions() { 4078 let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url()); 4079 claim 4080 .get_claim_assertion(&label, instance) 4081 .expect("Should find assertion"); 4082 } 4083 } 4084 } 4085 4086 #[test] 4087 #[cfg(feature = "file_io")] 4088 fn test_heic() { 4089 let ap = fixture_path("sample1.heic"); 4090 let temp_dir = tempdir().expect("temp dir"); 4091 let op = temp_dir_path(&temp_dir, "sample1.heic"); 4092 4093 // Create claims store. 4094 let mut store = Store::new(); 4095 4096 // Create a new claim. 4097 let claim1 = create_test_claim().unwrap(); 4098 4099 // Do we generate JUMBF? 4100 let signer = temp_signer(); 4101 4102 // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commits 4103 store.commit_claim(claim1).unwrap(); 4104 store.save_to_asset(&ap, signer.as_ref(), &op).unwrap(); 4105 4106 let mut report = DetailedStatusTracker::new(); 4107 4108 // read from new file 4109 let new_store = Store::load_from_asset(&op, true, &mut report).unwrap(); 4110 4111 // dump store and compare to original 4112 for claim in new_store.claims() { 4113 println!( 4114 "Claim: {} \n{}", 4115 claim.label(), 4116 claim 4117 .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true) 4118 .expect("could not restore from json") 4119 ); 4120 4121 for hashed_uri in claim.assertions() { 4122 let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url()); 4123 claim 4124 .get_claim_assertion(&label, instance) 4125 .expect("Should find assertion"); 4126 } 4127 } 4128 } 4129 4130 #[test] 4131 #[cfg(feature = "file_io")] 4132 fn test_avif() { 4133 let ap = fixture_path("sample1.avif"); 4134 let temp_dir = tempdir().expect("temp dir"); 4135 let op = temp_dir_path(&temp_dir, "sample1.avif"); 4136 4137 // Create claims store. 4138 let mut store = Store::new(); 4139 4140 // Create a new claim. 4141 let claim1 = create_test_claim().unwrap(); 4142 4143 // Do we generate JUMBF? 4144 let signer = temp_signer(); 4145 4146 // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commits 4147 store.commit_claim(claim1).unwrap(); 4148 store.save_to_asset(&ap, signer.as_ref(), &op).unwrap(); 4149 4150 let mut report = DetailedStatusTracker::new(); 4151 4152 // read from new file 4153 let new_store = Store::load_from_asset(&op, true, &mut report).unwrap(); 4154 4155 // dump store and compare to original 4156 for claim in new_store.claims() { 4157 println!( 4158 "Claim: {} \n{}", 4159 claim.label(), 4160 claim 4161 .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true) 4162 .expect("could not restore from json") 4163 ); 4164 4165 for hashed_uri in claim.assertions() { 4166 let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url()); 4167 claim 4168 .get_claim_assertion(&label, instance) 4169 .expect("Should find assertion"); 4170 } 4171 } 4172 } 4173 4174 #[test] 4175 #[cfg(feature = "file_io")] 4176 fn test_heif() { 4177 let ap = fixture_path("sample1.heif"); 4178 let temp_dir = tempdir().expect("temp dir"); 4179 let op = temp_dir_path(&temp_dir, "sample1.heif"); 4180 4181 // Create claims store. 4182 let mut store = Store::new(); 4183 4184 // Create a new claim. 4185 let claim1 = create_test_claim().unwrap(); 4186 4187 // Do we generate JUMBF? 4188 let signer = temp_signer(); 4189 4190 // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commits 4191 store.commit_claim(claim1).unwrap(); 4192 store.save_to_asset(&ap, signer.as_ref(), &op).unwrap(); 4193 4194 let mut report = DetailedStatusTracker::new(); 4195 4196 // read from new file 4197 let new_store = Store::load_from_asset(&op, true, &mut report).unwrap(); 4198 4199 // dump store and compare to original 4200 for claim in new_store.claims() { 4201 println!( 4202 "Claim: {} \n{}", 4203 claim.label(), 4204 claim 4205 .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true) 4206 .expect("could not restore from json") 4207 ); 4208 4209 for hashed_uri in claim.assertions() { 4210 let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url()); 4211 claim 4212 .get_claim_assertion(&label, instance) 4213 .expect("Should find assertion"); 4214 } 4215 } 4216 } 4217 4218 /* todo: disable until we can generate a valid file with no xmp 4219 #[test] 4220 fn test_manifest_no_xmp() { 4221 let ap = fixture_path("CAICAI_NO_XMP.jpg"); 4222 assert!(Store::load_from_asset(&ap, true, None).is_ok()); 4223 } 4224 */ 4225 4226 #[test] 4227 fn test_manifest_bad_sig() { 4228 let ap = fixture_path("CE-sig-CA.jpg"); 4229 assert!(Store::load_from_asset(&ap, true, &mut OneShotStatusTracker::new()).is_err()); 4230 } 4231 4232 #[test] 4233 fn test_unsupported_type_without_external_manifest() { 4234 let ap = fixture_path("Purple Square.psd"); 4235 let mut report = DetailedStatusTracker::new(); 4236 let result = Store::load_from_asset(&ap, true, &mut report); 4237 assert!(matches!(result, Err(Error::UnsupportedType))); 4238 println!("Error report for {}: {:?}", ap.display(), report); 4239 assert!(!report.get_log().is_empty()); 4240 4241 assert!(report_has_err(report.get_log(), Error::UnsupportedType)); 4242 } 4243 4244 #[test] 4245 fn test_bad_jumbf() { 4246 // test bad jumbf 4247 let ap = fixture_path("prerelease.jpg"); 4248 let mut report = DetailedStatusTracker::new(); 4249 let _r = Store::load_from_asset(&ap, true, &mut report); 4250 4251 // error report 4252 println!("Error report for {}: {:?}", ap.display(), report); 4253 assert!(!report.get_log().is_empty()); 4254 4255 assert!(report_has_err(report.get_log(), Error::PrereleaseError)); 4256 } 4257 4258 #[test] 4259 fn test_detect_byte_change() { 4260 // test bad jumbf 4261 let ap = fixture_path("XCA.jpg"); 4262 let mut report = DetailedStatusTracker::new(); 4263 Store::load_from_asset(&ap, true, &mut report).unwrap(); 4264 4265 // error report 4266 println!("Error report for {}: {:?}", ap.display(), report); 4267 assert!(!report.get_log().is_empty()); 4268 4269 let errs = report_split_errors(report.get_log_mut()); 4270 assert!(report_has_status( 4271 &errs, 4272 validation_status::ASSERTION_DATAHASH_MISMATCH 4273 )); 4274 } 4275 4276 #[test] 4277 #[cfg(feature = "file_io")] 4278 fn test_file_not_found() { 4279 let ap = fixture_path("this_does_not_exist.jpg"); 4280 let mut report = DetailedStatusTracker::new(); 4281 let _result = Store::load_from_asset(&ap, true, &mut report); 4282 4283 println!("Error report for {}: {:?}", ap.display(), report.get_log()); 4284 assert!(!report.get_log().is_empty()); 4285 let errors = report_split_errors(report.get_log_mut()); 4286 assert!(errors[0].error_str().unwrap().starts_with("IoError")); 4287 } 4288 4289 #[test] 4290 fn test_old_manifest() { 4291 let ap = fixture_path("prerelease.jpg"); 4292 let mut report = DetailedStatusTracker::new(); 4293 let _r = Store::load_from_asset(&ap, true, &mut report); 4294 4295 println!("Error report for {}: {:?}", ap.display(), report.get_log()); 4296 assert!(!report.get_log().is_empty()); 4297 let errors = report_split_errors(report.get_log_mut()); 4298 assert!(errors[0].error_str().unwrap().starts_with("Prerelease")); 4299 } 4300 4301 #[test] 4302 #[cfg(feature = "file_io")] 4303 fn test_verifiable_credentials() { 4304 use crate::utils::test::create_test_store; 4305 4306 let signer = temp_signer(); 4307 4308 // test adding to actual image 4309 let ap = fixture_path("earth_apollo17.jpg"); 4310 let temp_dir = tempdir().expect("temp dir"); 4311 let op = temp_dir_path(&temp_dir, "earth_apollo17.jpg"); 4312 4313 // get default store with default claim 4314 let mut store = create_test_store().unwrap(); 4315 4316 // save to output 4317 store 4318 .save_to_asset(ap.as_path(), signer.as_ref(), op.as_path()) 4319 .unwrap(); 4320 4321 // read back in 4322 let restored_store = 4323 Store::load_from_asset(op.as_path(), true, &mut OneShotStatusTracker::new()).unwrap(); 4324 4325 let pc = restored_store.provenance_claim().unwrap(); 4326 4327 let vc = pc.get_verifiable_credentials(); 4328 4329 assert!(!vc.is_empty()); 4330 match &vc[0] { 4331 AssertionData::Json(s) => { 4332 assert!(s.contains("did:nppa:eb1bb9934d9896a374c384521410c7f14")) 4333 } 4334 _ => panic!("expected JSON assertion data"), 4335 } 4336 } 4337 4338 #[test] 4339 #[cfg(feature = "file_io")] 4340 fn test_data_box_creation() { 4341 use crate::utils::test::create_test_store; 4342 4343 let signer = temp_signer(); 4344 4345 // test adding to actual image 4346 let ap = fixture_path("earth_apollo17.jpg"); 4347 let temp_dir = tempdir().expect("temp dir"); 4348 let op = temp_dir_path(&temp_dir, "earth_apollo17.jpg"); 4349 4350 // get default store with default claim 4351 let mut store = create_test_store().unwrap(); 4352 4353 // save to output 4354 store 4355 .save_to_asset(ap.as_path(), signer.as_ref(), op.as_path()) 4356 .unwrap(); 4357 4358 // read back in 4359 let restored_store = 4360 Store::load_from_asset(op.as_path(), true, &mut OneShotStatusTracker::new()).unwrap(); 4361 4362 let pc = restored_store.provenance_claim().unwrap(); 4363 4364 let databoxes = pc.databoxes(); 4365 4366 assert!(!databoxes.is_empty()); 4367 4368 for (uri, db) in databoxes { 4369 println!( 4370 "URI: {}, data: {}", 4371 uri.url(), 4372 String::from_utf8_lossy(&db.data) 4373 ); 4374 } 4375 } 4376 4377 /// copies a fixture, replaces some bytes and returns a validation report 4378 fn patch_and_report( 4379 fixture_name: &str, 4380 search_bytes: &[u8], 4381 replace_bytes: &[u8], 4382 ) -> impl StatusTracker { 4383 let temp_dir = tempdir().expect("temp dir"); 4384 let path = temp_fixture_path(&temp_dir, fixture_name); 4385 patch_file(&path, search_bytes, replace_bytes).expect("patch_file"); 4386 let mut report = DetailedStatusTracker::default(); 4387 let _r = Store::load_from_asset(&path, true, &mut report); // errs are in report 4388 println!("report: {report:?}"); 4389 report 4390 } 4391 4392 #[test] 4393 #[cfg(feature = "file_io")] 4394 fn test_update_manifest() { 4395 use crate::{hashed_uri::HashedUri, utils::test::create_test_store}; 4396 4397 let signer = temp_signer(); 4398 4399 // test adding to actual image 4400 let ap = fixture_path("earth_apollo17.jpg"); 4401 let temp_dir = tempdir().expect("temp dir"); 4402 let op = temp_dir_path(&temp_dir, "update_manifest.jpg"); 4403 4404 // get default store with default claim 4405 let mut store = create_test_store().unwrap(); 4406 4407 // save to output 4408 store 4409 .save_to_asset(ap.as_path(), signer.as_ref(), op.as_path()) 4410 .unwrap(); 4411 4412 let mut report = OneShotStatusTracker::default(); 4413 // read back in 4414 let mut restored_store = Store::load_from_asset(op.as_path(), true, &mut report).unwrap(); 4415 4416 let pc = restored_store.provenance_claim().unwrap(); 4417 4418 // should be a regular manifest 4419 assert!(!pc.update_manifest()); 4420 4421 // create a new update manifest 4422 let mut claim = Claim::new("adobe unit test", Some("update_manfifest")); 4423 4424 // must contain an ingredient 4425 let parent_hashed_uri = HashedUri::new( 4426 restored_store.provenance_path().unwrap(), 4427 Some(pc.alg().to_string()), 4428 &pc.hash(), 4429 ); 4430 4431 let ingredient = Ingredient::new( 4432 "update_manifest.jpg", 4433 "image/jpeg", 4434 "xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d", 4435 Some("xmp.did:87d51599-286e-43b2-9478-88c79f49c347"), 4436 ) 4437 .set_parent() 4438 .set_c2pa_manifest_from_hashed_uri(Some(parent_hashed_uri)); 4439 4440 claim.add_assertion(&ingredient).unwrap(); 4441 4442 restored_store.commit_update_manifest(claim).unwrap(); 4443 restored_store 4444 .save_to_asset(op.as_path(), signer.as_ref(), op.as_path()) 4445 .unwrap(); 4446 4447 // read back in store with update manifest 4448 let um_store = Store::load_from_asset(op.as_path(), true, &mut report).unwrap(); 4449 4450 let um = um_store.provenance_claim().unwrap(); 4451 4452 // should be an update manifest 4453 assert!(um.update_manifest()); 4454 } 4455 4456 #[test] 4457 fn test_claim_decoding() { 4458 // modify a required field label in the claim - causes failure to read claim from cbor 4459 let report = patch_and_report("C.jpg", b"claim_generator", b"claim_generatur"); 4460 assert!(!report.get_log().is_empty()); 4461 assert!(report.get_log()[0] 4462 .error_str() 4463 .unwrap() 4464 .starts_with("ClaimDecoding")) 4465 } 4466 4467 #[test] 4468 fn test_claim_modified() { 4469 // replace the title that is inside the claim data - should cause signature to not match 4470 let mut report = patch_and_report("C.jpg", b"C.jpg", b"X.jpg"); 4471 assert!(!report.get_log().is_empty()); 4472 let errors = report_split_errors(report.get_log_mut()); 4473 4474 assert!(report_has_err(&errors, Error::CoseSignature)); 4475 assert!(report_has_err(&errors, Error::CoseTimeStampMismatch)); 4476 4477 assert!(report_has_status( 4478 &errors, 4479 validation_status::CLAIM_SIGNATURE_MISMATCH 4480 )); 4481 assert!(report_has_status( 4482 &errors, 4483 validation_status::TIMESTAMP_MISMATCH 4484 )); 4485 } 4486 4487 #[test] 4488 fn test_assertion_hash_mismatch() { 4489 // modifies content of an action assertion - causes an assertion hashuri mismatch 4490 let mut report = patch_and_report("CA.jpg", b"brightnesscontrast", b"brightnesscontraxx"); 4491 let errors = report_split_errors(report.get_log_mut()); 4492 4493 assert_eq!( 4494 errors[0].validation_status.as_deref(), 4495 Some(validation_status::ASSERTION_HASHEDURI_MISMATCH) 4496 ); 4497 } 4498 4499 #[test] 4500 fn test_claim_missing() { 4501 // patch jumbf url from c2pa_manifest field in an ingredient to cause claim_missing 4502 // note this includes hex for Jumbf blocks, so may need some manual tweaking 4503 const SEARCH_BYTES: &[u8] = 4504 b"c2pa_manifest\xA3\x63url\x78\x4aself#jumbf=/c2pa/contentauth:urn:uuid:"; 4505 const REPLACE_BYTES: &[u8] = 4506 b"c2pa_manifest\xA3\x63url\x78\x4aself#jumbf=/c2pa/contentauth:urn:uuix:"; 4507 let mut report = patch_and_report("CIE-sig-CA.jpg", SEARCH_BYTES, REPLACE_BYTES); 4508 let errors = report_split_errors(report.get_log_mut()); 4509 assert_eq!( 4510 errors[0].validation_status.as_deref(), 4511 Some(validation_status::ASSERTION_HASHEDURI_MISMATCH) 4512 ); 4513 assert_eq!( 4514 errors[1].validation_status.as_deref(), 4515 Some(validation_status::CLAIM_MISSING) 4516 ); 4517 } 4518 4519 #[test] 4520 fn test_display() { 4521 let ap = fixture_path("CA.jpg"); 4522 let mut report = DetailedStatusTracker::new(); 4523 let store = Store::load_from_asset(&ap, true, &mut report).expect("load_from_asset"); 4524 let _errors = report_split_errors(report.get_log_mut()); 4525 4526 println!("store = {store}"); 4527 } 4528 4529 #[test] 4530 fn test_legacy_ingredient_hash() { 4531 // test 1.0 ingredient hash 4532 let ap = fixture_path("legacy_ingredient_hash.jpg"); 4533 let mut report = DetailedStatusTracker::new(); 4534 let store = Store::load_from_asset(&ap, true, &mut report).expect("load_from_asset"); 4535 println!("store = {store}"); 4536 } 4537 4538 #[test] 4539 fn test_bmff_legacy() { 4540 // test 1.0 bmff hash 4541 let ap = fixture_path("legacy.mp4"); 4542 let mut report = DetailedStatusTracker::new(); 4543 let store = Store::load_from_asset(&ap, true, &mut report).expect("load_from_asset"); 4544 println!("store = {store}"); 4545 } 4546 4547 /* 4548 #[test] 4549 fn test_bmff_fragments() { 4550 let init_stream_path = fixture_path("dashinit.mp4"); 4551 let segment_stream_path = fixture_path("dash1.m4s"); 4552 4553 let init_stream = std::fs::read(init_stream_path).unwrap(); 4554 let segment_stream = std::fs::read(segment_stream_path).unwrap(); 4555 4556 let mut report = DetailedStatusTracker::new(); 4557 let store = Store::load_fragment_from_memory( 4558 "mp4", 4559 &init_stream, 4560 &segment_stream, 4561 true, 4562 &mut report, 4563 ) 4564 .expect("load_from_asset"); 4565 println!("store = {store}"); 4566 } 4567 */ 4568 4569 #[test] 4570 fn test_bmff_jumbf_generation() { 4571 // test adding to actual image 4572 let ap = fixture_path("video1.mp4"); 4573 let temp_dir = tempdir().expect("temp dir"); 4574 let op = temp_dir_path(&temp_dir, "video1.mp4"); 4575 4576 // Create claims store. 4577 let mut store = Store::new(); 4578 4579 // Create a new claim. 4580 let claim1 = create_test_claim().unwrap(); 4581 4582 let signer = temp_signer(); 4583 4584 // Move the claim to claims list. 4585 store.commit_claim(claim1).unwrap(); 4586 store.save_to_asset(&ap, signer.as_ref(), &op).unwrap(); 4587 4588 let mut report = DetailedStatusTracker::new(); 4589 4590 // can we read back in 4591 let _new_store = Store::load_from_asset(&op, true, &mut report).unwrap(); 4592 } 4593 4594 #[test] 4595 #[cfg(feature = "file_io")] 4596 fn test_removed_jumbf() { 4597 // test adding to actual image 4598 let ap = fixture_path("no_manifest.jpg"); 4599 4600 let mut report = DetailedStatusTracker::new(); 4601 4602 // can we read back in 4603 let _store = Store::load_from_asset(&ap, true, &mut report); 4604 4605 assert!(report_has_err(report.get_log(), Error::JumbfNotFound)); 4606 } 4607 4608 #[test] 4609 fn test_external_manifest_sidecar() { 4610 // test adding to actual image 4611 let ap = fixture_path("libpng-test.png"); 4612 let temp_dir = tempdir().expect("temp dir"); 4613 let op = temp_dir_path(&temp_dir, "libpng-test-c2pa.png"); 4614 4615 let sidecar = op.with_extension(MANIFEST_STORE_EXT); 4616 4617 // Create claims store. 4618 let mut store = Store::new(); 4619 4620 // Create a new claim. 4621 let mut claim = create_test_claim().unwrap(); 4622 4623 // set claim for side car generation 4624 claim.set_external_manifest(); 4625 4626 // Do we generate JUMBF? 4627 let signer = temp_signer(); 4628 4629 store.commit_claim(claim).unwrap(); 4630 4631 let saved_manifest = store.save_to_asset(&ap, signer.as_ref(), &op).unwrap(); 4632 4633 assert!(sidecar.exists()); 4634 4635 // load external manifest 4636 let loaded_manifest = std::fs::read(sidecar).unwrap(); 4637 4638 // compare returned to external 4639 //assert_eq!(saved_manifest, loaded_manifest); 4640 4641 // test auto loading of sidecar with validation 4642 let mut validation_log = OneShotStatusTracker::default(); 4643 Store::load_from_asset(&op, true, &mut validation_log).unwrap(); 4644 } 4645 4646 // generalize test for multipe file types 4647 fn external_manifest_test(file_name: &str) { 4648 // test adding to actual image 4649 let ap = fixture_path(file_name); 4650 let extension = ap.extension().unwrap().to_str().unwrap(); 4651 let temp_dir = tempdir().expect("temp dir"); 4652 let mut op = temp_dir_path(&temp_dir, file_name); 4653 op.set_extension(extension); 4654 4655 let sidecar = op.with_extension(MANIFEST_STORE_EXT); 4656 4657 // Create claims store. 4658 let mut store = Store::new(); 4659 4660 // Create a new claim. 4661 let mut claim = create_test_claim().unwrap(); 4662 4663 // Do we generate JUMBF? 4664 let signer = temp_signer(); 4665 4666 // start with base url 4667 let fp = format!("file:/{}", sidecar.to_str().unwrap()); 4668 let url = url::Url::parse(&fp).unwrap(); 4669 4670 let url_string: String = url.into(); 4671 4672 // set claim for side car with remote manifest embedding generation 4673 claim.set_remote_manifest(url_string.clone()).unwrap(); 4674 4675 store.commit_claim(claim).unwrap(); 4676 4677 let saved_manifest = store.save_to_asset(&ap, signer.as_ref(), &op).unwrap(); 4678 4679 assert!(sidecar.exists()); 4680 4681 // load external manifest 4682 let loaded_manifest = std::fs::read(sidecar).unwrap(); 4683 4684 // compare returned to external 4685 //assert_eq!(saved_manifest, loaded_manifest); 4686 4687 // load the jumbf back into a store 4688 let mut asset_reader = std::fs::File::open(op.clone()).unwrap(); 4689 let ext_ref = 4690 crate::utils::xmp_inmemory_utils::XmpInfo::from_source(&mut asset_reader, extension) 4691 .provenance 4692 .unwrap(); 4693 4694 assert_eq!(ext_ref, url_string); 4695 4696 // make sure it validates 4697 let mut validation_log = OneShotStatusTracker::default(); 4698 Store::load_from_asset(&op, true, &mut validation_log).unwrap(); 4699 } 4700 4701 #[test] 4702 fn test_external_manifest_embedded_png() { 4703 external_manifest_test("libpng-test.png"); 4704 } 4705 4706 #[test] 4707 fn test_external_manifest_embedded_tiff() { 4708 external_manifest_test("TUSCANY.TIF"); 4709 } 4710 4711 #[test] 4712 fn test_external_manifest_embedded_webp() { 4713 external_manifest_test("sample1.webp"); 4714 } 4715 4716 #[test] 4717 fn test_user_guid_external_manifest_embedded() { 4718 // test adding to actual image 4719 let ap = fixture_path("libpng-test.png"); 4720 let temp_dir = tempdir().expect("temp dir"); 4721 let op = temp_dir_path(&temp_dir, "libpng-test-c2pa.png"); 4722 4723 let sidecar = op.with_extension(MANIFEST_STORE_EXT); 4724 4725 // Create claims store. 4726 let mut store = Store::new(); 4727 4728 // Create a new claim. 4729 let mut claim = create_test_claim().unwrap(); 4730 4731 // Do we generate JUMBF? 4732 let signer = temp_signer(); 4733 4734 // start with base url 4735 let fp = format!("file:/{}", sidecar.to_str().unwrap()); 4736 let url = url::Url::parse(&fp).unwrap(); 4737 4738 let url_string: String = url.into(); 4739 4740 // set claim for side car with remote manifest embedding generation 4741 claim.set_embed_remote_manifest(url_string.clone()).unwrap(); 4742 4743 store.commit_claim(claim).unwrap(); 4744 4745 let saved_manifest = store.save_to_asset(&ap, signer.as_ref(), &op).unwrap(); 4746 4747 assert!(sidecar.exists()); 4748 4749 // load external manifest 4750 let loaded_manifest = std::fs::read(sidecar).unwrap(); 4751 4752 // compare returned to external 4753 //assert_eq!(saved_manifest, loaded_manifest); 4754 4755 let mut asset_reader = std::fs::File::open(op.clone()).unwrap(); 4756 let ext_ref = 4757 crate::utils::xmp_inmemory_utils::XmpInfo::from_source(&mut asset_reader, "png") 4758 .provenance 4759 .unwrap(); 4760 4761 assert_eq!(ext_ref, url_string); 4762 4763 // make sure it validates 4764 let mut validation_log = OneShotStatusTracker::default(); 4765 Store::load_from_asset(&op, true, &mut validation_log).unwrap(); 4766 } 4767 4768 #[test] 4769 fn test_external_manifest_from_memory() { 4770 // test adding to actual image 4771 let ap = fixture_path("libpng-test.png"); 4772 let temp_dir = tempdir().expect("temp dir"); 4773 let op = temp_dir_path(&temp_dir, "libpng-test-c2pa.png"); 4774 4775 let sidecar = op.with_extension(MANIFEST_STORE_EXT); 4776 4777 // Create claims store. 4778 let mut store = Store::new(); 4779 4780 // Create a new claim. 4781 let mut claim = create_test_claim().unwrap(); 4782 4783 // Do we generate JUMBF? 4784 let signer = temp_signer(); 4785 4786 // start with base url 4787 let fp = format!("file:/{}", sidecar.to_str().unwrap()); 4788 let url = url::Url::parse(&fp).unwrap(); 4789 4790 let url_string: String = url.into(); 4791 4792 // set claim for side car with remote manifest embedding generation 4793 claim.set_remote_manifest(url_string).unwrap(); 4794 4795 store.commit_claim(claim).unwrap(); 4796 4797 let saved_manifest = store.save_to_asset(&ap, signer.as_ref(), &op).unwrap(); 4798 4799 // delete the sidecar so we can test for url only rea 4800 // std::fs::remove_file(sidecar); 4801 4802 assert!(sidecar.exists()); 4803 4804 // load external manifest 4805 let loaded_manifest = std::fs::read(sidecar).unwrap(); 4806 4807 // compare returned to external 4808 // assert_eq!(saved_manifest, loaded_manifest); 4809 4810 // Load the exported file into a buffer 4811 let file_buffer = std::fs::read(&op).unwrap(); 4812 4813 let mut validation_log = OneShotStatusTracker::default(); 4814 let result = Store::load_from_memory("png", &file_buffer, true, &mut validation_log); 4815 4816 assert!(result.is_err()); 4817 4818 // We should get a `JumbfNotFound` error since the external reference points to a file URL, not a remote URL 4819 match result { 4820 Ok(_store) => panic!("did not expect to have a store"), 4821 Err(e) => match e { 4822 Error::JumbfNotFound => {} 4823 e => panic!("unexpected error: {e}"), 4824 }, 4825 } 4826 } 4827 4828 #[actix::test] 4829 async fn test_jumbf_generation_stream() { 4830 let file_buffer = include_bytes!("../tests/fixtures/earth_apollo17.jpg").to_vec(); 4831 // convert buffer to cursor with Read/Write/Seek capability 4832 let mut buf_io = Cursor::new(file_buffer); 4833 4834 // Create claims store. 4835 let mut store = Store::new(); 4836 4837 // Create a new claim. 4838 let claim1 = create_test_claim().unwrap(); 4839 4840 let signer = temp_signer(); 4841 4842 store.commit_claim(claim1).unwrap(); 4843 4844 let mut result: Vec<u8> = Vec::new(); 4845 let mut result_stream = Cursor::new(result); 4846 4847 store 4848 .save_to_stream("jpeg", &mut buf_io, &mut result_stream, signer.as_ref()) 4849 .unwrap(); 4850 4851 // convert our cursor back into a buffer 4852 result = result_stream.into_inner(); 4853 4854 // make sure we can read from new file 4855 let mut report = DetailedStatusTracker::new(); 4856 let _new_store = Store::load_from_memory("jpeg", &result, true, &mut report).unwrap(); 4857 4858 let errors = report_split_errors(report.get_log_mut()); 4859 assert!(errors.is_empty()); 4860 4861 // std::fs::write("target/test.jpg", result).unwrap(); 4862 } 4863 4864 #[test] 4865 #[cfg(feature = "file_io")] 4866 fn test_tiff_jumbf_generation() { 4867 // test adding to actual image 4868 let ap = fixture_path("TUSCANY.TIF"); 4869 let temp_dir = tempdir().expect("temp dir"); 4870 let op = temp_dir_path(&temp_dir, "TUSCANY-OUTPUT.TIF"); 4871 4872 // Create claims store. 4873 let mut store = Store::new(); 4874 4875 // Create a new claim. 4876 let claim1 = create_test_claim().unwrap(); 4877 4878 // Create a new claim. 4879 let mut claim2 = Claim::new("Photoshop", Some("Adobe")); 4880 create_editing_claim(&mut claim2).unwrap(); 4881 4882 // Create a 3rd party claim 4883 let mut claim_capture = Claim::new("capture", Some("claim_capture")); 4884 create_capture_claim(&mut claim_capture).unwrap(); 4885 4886 // Do we generate JUMBF? 4887 let signer = temp_signer(); 4888 4889 // Move the claim to claims list. Note this is not real, the claims would have to be signed in between commits 4890 store.commit_claim(claim1).unwrap(); 4891 store.save_to_asset(&ap, signer.as_ref(), &op).unwrap(); 4892 store.commit_claim(claim_capture).unwrap(); 4893 store.save_to_asset(&op, signer.as_ref(), &op).unwrap(); 4894 store.commit_claim(claim2).unwrap(); 4895 store.save_to_asset(&op, signer.as_ref(), &op).unwrap(); 4896 4897 println!("Provenance: {}\n", store.provenance_path().unwrap()); 4898 4899 let mut report = DetailedStatusTracker::new(); 4900 4901 // read from new file 4902 let new_store = Store::load_from_asset(&op, true, &mut report).unwrap(); 4903 4904 let errors = report_split_errors(report.get_log_mut()); 4905 assert!(errors.is_empty()); 4906 4907 // dump store and compare to original 4908 for claim in new_store.claims() { 4909 let _restored_json = claim 4910 .to_json(AssertionStoreJsonFormat::OrderedList, false) 4911 .unwrap(); 4912 let _orig_json = store 4913 .get_claim(claim.label()) 4914 .unwrap() 4915 .to_json(AssertionStoreJsonFormat::OrderedList, false) 4916 .unwrap(); 4917 4918 println!( 4919 "Claim: {} \n{}", 4920 claim.label(), 4921 claim 4922 .to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true) 4923 .expect("could not restore from json") 4924 ); 4925 4926 for hashed_uri in claim.assertions() { 4927 let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url()); 4928 claim 4929 .get_claim_assertion(&label, instance) 4930 .expect("Should find assertion"); 4931 } 4932 } 4933 } 4934 4935 #[actix::test] 4936 #[cfg(feature = "file_io")] 4937 async fn test_boxhash_embeddable_manifest_async() { 4938 // test adding to actual image 4939 let ap = fixture_path("boxhash.jpg"); 4940 let box_hash_path = fixture_path("boxhash.json"); 4941 4942 // Create claims store. 4943 let mut store = Store::new(); 4944 4945 // Create a new claim. 4946 let mut claim = create_test_claim().unwrap(); 4947 4948 // add box hash for CA.jpg 4949 let box_hash_data = std::fs::read(box_hash_path).unwrap(); 4950 let assertion = Assertion::from_data_json(BOX_HASH, &box_hash_data).unwrap(); 4951 let box_hash = BoxHash::from_json_assertion(&assertion).unwrap(); 4952 claim.add_assertion(&box_hash).unwrap(); 4953 4954 store.commit_claim(claim).unwrap(); 4955 4956 // Do we generate JUMBF? 4957 let signer = crate::openssl::temp_signer_async::AsyncSignerAdapter::new(SigningAlg::Ps256); 4958 4959 // get the embeddable manifest 4960 let em = store 4961 .get_box_hashed_embeddable_manifest_async(&signer) 4962 .await 4963 .unwrap(); 4964 4965 // get composed version for embedding to JPEG 4966 let cm = Store::get_composed_manifest(&em, "jpg").unwrap(); 4967 4968 // insert manifest into output asset 4969 let jpeg_io = get_assetio_handler_from_path(&ap).unwrap(); 4970 let ol = jpeg_io.get_object_locations(&ap).unwrap(); 4971 4972 let cai_loc = ol 4973 .iter() 4974 .find(|o| o.htype == HashBlockObjectType::Cai) 4975 .unwrap(); 4976 4977 // remove any existing manifest 4978 jpeg_io.read_cai_store(&ap).unwrap(); 4979 4980 // build new asset in memory inserting new manifest 4981 let outbuf = Vec::new(); 4982 let mut out_stream = Cursor::new(outbuf); 4983 let mut input_file = std::fs::File::open(&ap).unwrap(); 4984 4985 // write before 4986 let mut before = vec![0u8; cai_loc.offset]; 4987 input_file.read_exact(before.as_mut_slice()).unwrap(); 4988 out_stream.write_all(&before).unwrap(); 4989 4990 // write composed bytes 4991 out_stream.write_all(&cm).unwrap(); 4992 4993 // write bytes after 4994 let mut after_buf = Vec::new(); 4995 input_file.read_to_end(&mut after_buf).unwrap(); 4996 out_stream.write_all(&after_buf).unwrap(); 4997 4998 // save to output file 4999 let temp_dir = tempfile::tempdir().unwrap(); 5000 let output = temp_dir_path(&temp_dir, "boxhash-out.jpg"); 5001 let mut output_file = std::fs::OpenOptions::new() 5002 .read(true) 5003 .write(true) 5004 .create(true) 5005 .truncate(true) 5006 .open(&output) 5007 .unwrap(); 5008 output_file.write_all(&out_stream.into_inner()).unwrap(); 5009 5010 let mut report = DetailedStatusTracker::new(); 5011 let _new_store = Store::load_from_asset(&output, true, &mut report).unwrap(); 5012 5013 let errors = report_split_errors(report.get_log_mut()); 5014 assert!(errors.is_empty()); 5015 } 5016 5017 #[test] 5018 #[cfg(feature = "file_io")] 5019 fn test_boxhash_embeddable_manifest() { 5020 // test adding to actual image 5021 let ap = fixture_path("boxhash.jpg"); 5022 let box_hash_path = fixture_path("boxhash.json"); 5023 5024 // Create claims store. 5025 let mut store = Store::new(); 5026 5027 // Create a new claim. 5028 let mut claim = create_test_claim().unwrap(); 5029 5030 // add box hash for CA.jpg 5031 let box_hash_data = std::fs::read(box_hash_path).unwrap(); 5032 let assertion = Assertion::from_data_json(BOX_HASH, &box_hash_data).unwrap(); 5033 let box_hash = BoxHash::from_json_assertion(&assertion).unwrap(); 5034 claim.add_assertion(&box_hash).unwrap(); 5035 5036 store.commit_claim(claim).unwrap(); 5037 5038 // Do we generate JUMBF? 5039 let signer = temp_signer(); 5040 5041 // get the embeddable manifest 5042 let em = store 5043 .get_box_hashed_embeddable_manifest(signer.as_ref()) 5044 .unwrap(); 5045 5046 // get composed version for embedding to JPEG 5047 let cm = Store::get_composed_manifest(&em, "jpg").unwrap(); 5048 5049 // insert manifest into output asset 5050 let jpeg_io = get_assetio_handler_from_path(&ap).unwrap(); 5051 let ol = jpeg_io.get_object_locations(&ap).unwrap(); 5052 5053 let cai_loc = ol 5054 .iter() 5055 .find(|o| o.htype == HashBlockObjectType::Cai) 5056 .unwrap(); 5057 5058 // remove any existing manifest 5059 jpeg_io.read_cai_store(&ap).unwrap(); 5060 5061 // build new asset in memory inserting new manifest 5062 let outbuf = Vec::new(); 5063 let mut out_stream = Cursor::new(outbuf); 5064 let mut input_file = std::fs::File::open(&ap).unwrap(); 5065 5066 // write before 5067 let mut before = vec![0u8; cai_loc.offset]; 5068 input_file.read_exact(before.as_mut_slice()).unwrap(); 5069 out_stream.write_all(&before).unwrap(); 5070 5071 // write composed bytes 5072 out_stream.write_all(&cm).unwrap(); 5073 5074 // write bytes after 5075 let mut after_buf = Vec::new(); 5076 input_file.read_to_end(&mut after_buf).unwrap(); 5077 out_stream.write_all(&after_buf).unwrap(); 5078 5079 // save to output file 5080 let temp_dir = tempfile::tempdir().unwrap(); 5081 let output = temp_dir_path(&temp_dir, "boxhash-out.jpg"); 5082 let mut output_file = std::fs::OpenOptions::new() 5083 .read(true) 5084 .write(true) 5085 .create(true) 5086 .truncate(true) 5087 .open(&output) 5088 .unwrap(); 5089 output_file.write_all(&out_stream.into_inner()).unwrap(); 5090 5091 let mut report = DetailedStatusTracker::new(); 5092 let _new_store = Store::load_from_asset(&output, true, &mut report).unwrap(); 5093 5094 let errors = report_split_errors(report.get_log_mut()); 5095 assert!(errors.is_empty()); 5096 } 5097 5098 #[actix::test] 5099 #[cfg(feature = "file_io")] 5100 async fn test_datahash_embeddable_manifest_async() { 5101 // test adding to actual image 5102 let ap = fixture_path("cloud.jpg"); 5103 5104 // Do we generate JUMBF? 5105 let signer = crate::openssl::temp_signer_async::AsyncSignerAdapter::new(SigningAlg::Ps256); 5106 5107 // Create claims store. 5108 let mut store = Store::new(); 5109 5110 // Create a new claim. 5111 let claim = create_test_claim().unwrap(); 5112 5113 store.commit_claim(claim).unwrap(); 5114 5115 // get a placeholder the manifest 5116 let placeholder = store 5117 .get_data_hashed_manifest_placeholder(signer.reserve_size(), "jpeg") 5118 .unwrap(); 5119 5120 let temp_dir = tempfile::tempdir().unwrap(); 5121 let output = temp_dir_path(&temp_dir, "boxhash-out.jpg"); 5122 let mut output_file = std::fs::OpenOptions::new() 5123 .read(true) 5124 .write(true) 5125 .create(true) 5126 .truncate(true) 5127 .open(&output) 5128 .unwrap(); 5129 5130 // write a jpeg file with a placeholder for the manifest (returns offset of the placeholder) 5131 let offset = 5132 write_jpeg_placeholder_file(&placeholder, &ap, &mut output_file, None).unwrap(); 5133 5134 // build manifest to insert in the hole 5135 5136 // create an hash exclusion for the manifest 5137 let exclusion = HashRange::new(offset, placeholder.len()); 5138 let exclusions = vec![exclusion]; 5139 5140 let mut dh = DataHash::new("source_hash", "sha256"); 5141 dh.exclusions = Some(exclusions); 5142 5143 // get the embeddable manifest, letting API do the hashing 5144 output_file.rewind().unwrap(); 5145 let cm = store 5146 .get_data_hashed_embeddable_manifest_async(&dh, &signer, "jpeg", Some(&mut output_file)) 5147 .await 5148 .unwrap(); 5149 5150 // path in new composed manifest 5151 output_file.seek(SeekFrom::Start(offset as u64)).unwrap(); 5152 output_file.write_all(&cm).unwrap(); 5153 5154 let mut report = DetailedStatusTracker::new(); 5155 let _new_store = Store::load_from_asset(&output, true, &mut report).unwrap(); 5156 5157 let errors = report_split_errors(report.get_log_mut()); 5158 assert!(errors.is_empty()); 5159 } 5160 5161 #[test] 5162 #[cfg(feature = "file_io")] 5163 fn test_datahash_embeddable_manifest() { 5164 // test adding to actual image 5165 let ap = fixture_path("cloud.jpg"); 5166 5167 // Do we generate JUMBF? 5168 let signer = temp_signer(); 5169 5170 // Create claims store. 5171 let mut store = Store::new(); 5172 5173 // Create a new claim. 5174 let claim = create_test_claim().unwrap(); 5175 5176 store.commit_claim(claim).unwrap(); 5177 5178 // get a placeholder the manifest 5179 let placeholder = store 5180 .get_data_hashed_manifest_placeholder(signer.reserve_size(), "jpeg") 5181 .unwrap(); 5182 5183 let temp_dir = tempfile::tempdir().unwrap(); 5184 let output = temp_dir_path(&temp_dir, "boxhash-out.jpg"); 5185 let mut output_file = std::fs::OpenOptions::new() 5186 .read(true) 5187 .write(true) 5188 .create(true) 5189 .truncate(true) 5190 .open(&output) 5191 .unwrap(); 5192 5193 // write a jpeg file with a placeholder for the manifest (returns offset of the placeholder) 5194 let offset = 5195 write_jpeg_placeholder_file(&placeholder, &ap, &mut output_file, None).unwrap(); 5196 5197 // build manifest to insert in the hole 5198 5199 // create an hash exclusion for the manifest 5200 let exclusion = HashRange::new(offset, placeholder.len()); 5201 let exclusions = vec![exclusion]; 5202 5203 let mut dh = DataHash::new("source_hash", "sha256"); 5204 dh.exclusions = Some(exclusions); 5205 5206 // get the embeddable manifest, letting API do the hashing 5207 output_file.rewind().unwrap(); 5208 let cm = store 5209 .get_data_hashed_embeddable_manifest( 5210 &dh, 5211 signer.as_ref(), 5212 "jpeg", 5213 Some(&mut output_file), 5214 ) 5215 .unwrap(); 5216 5217 // path in new composed manifest 5218 output_file.seek(SeekFrom::Start(offset as u64)).unwrap(); 5219 output_file.write_all(&cm).unwrap(); 5220 5221 let mut report = DetailedStatusTracker::new(); 5222 let _new_store = Store::load_from_asset(&output, true, &mut report).unwrap(); 5223 5224 let errors = report_split_errors(report.get_log_mut()); 5225 assert!(errors.is_empty()); 5226 } 5227 5228 #[test] 5229 #[cfg(feature = "file_io")] 5230 fn test_datahash_embeddable_manifest_user_hashed() { 5231 // test adding to actual image 5232 let ap = fixture_path("cloud.jpg"); 5233 5234 let mut hasher = Hasher::SHA256(Sha256::new()); 5235 5236 // Do we generate JUMBF? 5237 let signer = temp_signer(); 5238 5239 // Create claims store. 5240 let mut store = Store::new(); 5241 5242 // Create a new claim. 5243 let claim = create_test_claim().unwrap(); 5244 5245 store.commit_claim(claim).unwrap(); 5246 5247 // get a placeholder for the manifest 5248 let placeholder = store 5249 .get_data_hashed_manifest_placeholder(signer.reserve_size(), "jpeg") 5250 .unwrap(); 5251 5252 let temp_dir = tempfile::tempdir().unwrap(); 5253 let output = temp_dir_path(&temp_dir, "boxhash-out.jpg"); 5254 let mut output_file = std::fs::OpenOptions::new() 5255 .read(true) 5256 .write(true) 5257 .create(true) 5258 .truncate(true) 5259 .open(&output) 5260 .unwrap(); 5261 5262 // write a jpeg file with a placeholder for the manifest (returns offset of the placeholder) 5263 let offset = 5264 write_jpeg_placeholder_file(&placeholder, &ap, &mut output_file, Some(&mut hasher)) 5265 .unwrap(); 5266 5267 // create target data hash 5268 // create an hash exclusion for the manifest 5269 let exclusion = HashRange::new(offset, placeholder.len()); 5270 let exclusions = vec![exclusion]; 5271 5272 //input_file.rewind().unwrap(); 5273 let mut dh = DataHash::new("source_hash", "sha256"); 5274 dh.hash = Hasher::finalize(hasher); 5275 dh.exclusions = Some(exclusions); 5276 5277 // get the embeddable manifest, using user hashing 5278 let cm = store 5279 .get_data_hashed_embeddable_manifest(&dh, signer.as_ref(), "jpeg", None) 5280 .unwrap(); 5281 5282 // path in new composed manifest 5283 output_file.seek(SeekFrom::Start(offset as u64)).unwrap(); 5284 output_file.write_all(&cm).unwrap(); 5285 5286 let mut report = DetailedStatusTracker::new(); 5287 let _new_store = Store::load_from_asset(&output, true, &mut report).unwrap(); 5288 5289 let errors = report_split_errors(report.get_log_mut()); 5290 assert!(errors.is_empty()); 5291 } 5292 }