claim.rs (80289B)
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 #[cfg(feature = "file_io")] 15 use std::path::Path; 16 use std::{collections::HashMap, fmt}; 17 18 use chrono::{DateTime, Utc}; 19 use serde::{Deserialize, Serialize}; 20 use serde_json::{json, Map, Value}; 21 use uuid::Uuid; 22 23 use crate::{ 24 assertion::{ 25 get_thumbnail_image_type, get_thumbnail_instance, get_thumbnail_type, Assertion, 26 AssertionBase, AssertionData, 27 }, 28 assertions::{ 29 self, 30 labels::{self, CLAIM}, 31 AssetType, BmffHash, BoxHash, DataBox, DataHash, 32 }, 33 asset_io::CAIRead, 34 cose_validator::{get_signing_info, verify_cose, verify_cose_async}, 35 error::{Error, Result}, 36 hashed_uri::HashedUri, 37 jumbf::{ 38 self, 39 boxes::{ 40 CAICBORAssertionBox, CAIJSONAssertionBox, CAIUUIDAssertionBox, JumbfEmbeddedFileBox, 41 }, 42 labels::{ 43 box_name_from_uri, manifest_label_from_uri, to_databox_uri, ASSERTIONS, CREDENTIALS, 44 DATABOX, DATABOXES, SIGNATURE, 45 }, 46 }, 47 jumbf_io::get_assetio_handler, 48 salt::{DefaultSalt, SaltGenerator, NO_SALT}, 49 status_tracker::{log_item, OneShotStatusTracker, StatusTracker}, 50 trust_handler::TrustHandlerConfig, 51 utils::{ 52 base64, 53 hash_utils::{hash_by_alg, vec_compare, verify_by_alg}, 54 }, 55 validation_status, 56 validator::ValidationInfo, 57 ClaimGeneratorInfo, 58 }; 59 60 const BUILD_HASH_ALG: &str = "sha256"; 61 62 /// JSON structure representing an Assertion reference in a Claim's "assertions" list 63 use HashedUri as C2PAAssertion; 64 65 const GH_FULL_VERSION_LIST: &str = "Sec-CH-UA-Full-Version-List"; 66 const GH_UA: &str = "Sec-CH-UA"; 67 68 // Enum to encapsulate the data type of the source asset. This simplifies 69 // having different implementations for functions as a single entry point can be 70 // used to handle different data types. 71 pub enum ClaimAssetData<'a> { 72 #[cfg(feature = "file_io")] 73 Path(&'a Path), 74 Bytes(&'a [u8], &'a str), 75 Stream(&'a mut dyn CAIRead, &'a str), 76 StreamFragment(&'a mut dyn CAIRead, &'a mut dyn CAIRead, &'a str), 77 } 78 79 // helper struct to allow arbitrary order for assertions stored in jumbf. The instance is 80 // stored separate from the Assertion to allow for late binding to the label. Also, 81 // we can load assertions in any order and know the position without re-parsing label. We also 82 // save on parsing the cbor assertion each time we need its contents 83 #[derive(PartialEq, Eq, Clone)] 84 pub struct ClaimAssertion { 85 assertion: Assertion, 86 instance: usize, 87 hash_val: Vec<u8>, 88 hash_alg: String, 89 salt: Option<Vec<u8>>, 90 } 91 92 impl ClaimAssertion { 93 pub fn new( 94 assertion: Assertion, 95 instance: usize, 96 hashval: &[u8], 97 alg: &str, 98 salt: Option<Vec<u8>>, 99 ) -> ClaimAssertion { 100 ClaimAssertion { 101 assertion, 102 instance, 103 hash_val: hashval.to_vec(), 104 hash_alg: alg.to_string(), 105 salt, 106 } 107 } 108 109 pub fn update_assertion(&mut self, assertion: Assertion, hash: Vec<u8>) -> Result<()> { 110 self.hash_val = hash; 111 self.assertion = assertion; 112 Ok(()) 113 } 114 115 pub fn label(&self) -> String { 116 let al_ref = self.assertion.label(); 117 if self.instance > 0 { 118 if get_thumbnail_type(&al_ref) == labels::INGREDIENT_THUMBNAIL { 119 format!( 120 "{}__{}.{}", 121 get_thumbnail_type(&al_ref), 122 self.instance, 123 get_thumbnail_image_type(&al_ref) 124 ) 125 } else { 126 format!("{}__{}", al_ref, self.instance) 127 } 128 } else { 129 self.assertion.label() 130 } 131 } 132 133 pub const fn instance(&self) -> usize { 134 self.instance 135 } 136 137 pub fn instance_string(&self) -> String { 138 format!("{}", self.instance) 139 } 140 141 pub fn label_raw(&self) -> String { 142 self.assertion.label() 143 } 144 145 pub const fn assertion(&self) -> &Assertion { 146 &self.assertion 147 } 148 149 pub fn hash(&self) -> &[u8] { 150 &self.hash_val 151 } 152 153 pub const fn salt(&self) -> &Option<Vec<u8>> { 154 &self.salt 155 } 156 157 pub fn hash_alg(&self) -> &str { 158 &self.hash_alg 159 } 160 161 /// returns true if assertions are of the same enum variant 162 pub fn is_same_type(&self, input_assertion: &Assertion) -> bool { 163 Assertion::assertions_eq(&self.assertion, input_assertion) 164 } 165 } 166 167 impl fmt::Debug for ClaimAssertion { 168 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 169 write!(f, "{:?}, instance: {}", self.assertion, self.instance) 170 } 171 } 172 173 /// A `Claim` gathers together all the `Assertion`s about an asset 174 /// from an actor at a given time, and may also include one or more 175 /// hashes of the asset itself, and a reference to the previous `Claim`. 176 /// 177 /// It has all the same properties as an `Assertion` including being 178 /// assigned a label (`c2pa.claim.v1`) and being either embedded into the 179 /// asset or in the cloud. The claim is cryptographically hashed and 180 /// that hash is signed to produce the claim signature. 181 #[derive(Deserialize, Serialize, Debug, Default, Clone)] 182 pub struct Claim { 183 // external manifest 184 #[serde(skip_deserializing, skip_serializing)] 185 remote_manifest: RemoteManifest, 186 187 // root of CAI store 188 #[serde(skip_deserializing, skip_serializing)] 189 update_manifest: bool, 190 191 #[serde(skip_serializing_if = "Option::is_none", rename = "dc:title")] 192 pub title: Option<String>, // title for this claim, generally the name of the containing asset 193 194 #[serde(rename = "dc:format")] 195 pub format: String, // mime format of document containing this claim 196 197 #[serde(rename = "instanceID")] 198 pub instance_id: String, // instance Id of document containing this claim 199 200 // Internal list of ingredients 201 #[serde(skip_deserializing, skip_serializing)] 202 ingredients_store: HashMap<String, Vec<Claim>>, 203 204 // internal scratch objects 205 #[serde(skip_deserializing, skip_serializing)] 206 box_prefix: String, // where in JUMBF hierarchy should this claim exist 207 208 #[serde(skip_deserializing, skip_serializing)] 209 signature_val: Vec<u8>, // the signature of the loaded/saved claim 210 211 // root of CAI store 212 #[serde(skip_deserializing, skip_serializing)] 213 #[allow(dead_code)] 214 root: String, 215 216 // internal scratch objects 217 #[serde(skip_deserializing, skip_serializing)] 218 label: String, // label of claim 219 220 // Internal list of assertions for claim. 221 // These are serialized manually based on need. 222 #[serde(skip_deserializing, skip_serializing)] 223 assertion_store: Vec<ClaimAssertion>, 224 225 // Internal list of verifiable credentials for claim. 226 // These are serialized manually based on need. 227 #[serde(skip_deserializing, skip_serializing)] 228 vc_store: Vec<(HashedUri, AssertionData)>, 229 230 claim_generator: String, // generator of this claim 231 232 pub(crate) claim_generator_info: Option<Vec<ClaimGeneratorInfo>>, /* detailed generator info of this claim */ 233 234 signature: String, // link to signature box 235 assertions: Vec<C2PAAssertion>, // list of assertion hashed URIs 236 237 // original JSON bytes of claim; only present when reading from asset 238 #[serde(skip_deserializing, skip_serializing)] 239 original_bytes: Option<Vec<u8>>, 240 241 // original JUMBF box order need to recalculate JUMBF box hash 242 #[serde(skip_deserializing, skip_serializing)] 243 original_box_order: Option<Vec<&'static str>>, 244 245 #[serde(skip_serializing_if = "Option::is_none")] 246 redacted_assertions: Option<Vec<String>>, // list of redacted assertions 247 248 #[serde(skip_serializing_if = "Option::is_none")] 249 alg: Option<String>, // hashing algorithm (default to Sha256) 250 251 #[serde(skip_serializing_if = "Option::is_none")] 252 alg_soft: Option<String>, // hashing algorithm for soft bindings 253 254 #[serde(skip_serializing_if = "Option::is_none")] 255 claim_generator_hints: Option<HashMap<String, Value>>, 256 257 #[serde(skip_deserializing, skip_serializing)] 258 data_boxes: Vec<(HashedUri, DataBox)>, /* list of the data boxes and their hashed URIs found for this manifest */ 259 } 260 261 /// Enum to define how assertions are are stored when output to json 262 pub enum AssertionStoreJsonFormat { 263 None, // no assertion store 264 KeyValue, // key (uri), value (Assertion json object) 265 KeyValueNoBinary, // KeyValue omitting binary results 266 OrderedList, // list of Assertions as json objects 267 OrderedListNoBinary, // list of Assertions as json objects omitting binaries results 268 } 269 270 /// Remote manifest options. Use 'set_remote_manifest' to generate external manifests. 271 #[derive(Clone, Debug, PartialEq, Eq)] 272 pub enum RemoteManifest { 273 NoRemote, // No external manifest (default) 274 SideCar, // Manifest will be saved as a side car file, output asset is untouched. 275 Remote(String), /* Manifest will be saved as a side car file, output asset will contain remote reference */ 276 EmbedWithRemote(String), /* Manifest will be embedded with a remote reference, sidecar will be generated */ 277 } 278 279 impl Default for RemoteManifest { 280 fn default() -> Self { 281 Self::NoRemote 282 } 283 } 284 285 #[derive(Serialize, Deserialize, Debug)] 286 pub struct JsonOrderedAssertionData { 287 label: String, 288 data: Value, 289 hash: String, 290 is_binary: bool, 291 mime_type: String, 292 } 293 294 impl Claim { 295 /// Label prefix for a claim assertion. 296 /// 297 /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_overview_4>. 298 pub const LABEL: &'static str = assertions::labels::CLAIM; 299 300 /// Create a new claim. 301 /// vendor: name used to label the claim (unique instance number is automatically calculated) 302 /// claim_generator: User agent see c2pa spec for format 303 pub fn new<S: Into<String>>(claim_generator: S, vendor: Option<&str>) -> Self { 304 let urn = Uuid::new_v4(); 305 let l = match vendor { 306 Some(v) => format!( 307 "{}:{}", 308 v.to_lowercase(), 309 urn.urn().encode_lower(&mut Uuid::encode_buffer()) 310 ), 311 None => urn 312 .urn() 313 .encode_lower(&mut Uuid::encode_buffer()) 314 .to_string(), 315 }; 316 317 Claim { 318 remote_manifest: RemoteManifest::NoRemote, 319 box_prefix: "self#jumbf".to_string(), 320 root: jumbf::labels::MANIFEST_STORE.to_string(), 321 signature_val: Vec::new(), 322 ingredients_store: HashMap::new(), 323 label: l, 324 signature: "".to_string(), 325 326 claim_generator: claim_generator.into(), 327 claim_generator_info: None, 328 assertion_store: Vec::new(), 329 vc_store: Vec::new(), 330 assertions: Vec::new(), 331 original_bytes: None, 332 original_box_order: None, 333 redacted_assertions: None, 334 alg: Some(BUILD_HASH_ALG.to_string()), 335 alg_soft: None, 336 claim_generator_hints: None, 337 338 title: None, 339 format: "".to_string(), 340 instance_id: "".to_string(), 341 342 update_manifest: false, 343 data_boxes: Vec::new(), 344 } 345 } 346 347 /// Create a new claim with a user supplied GUID. 348 /// user_guid: is user supplied guid conforming the C2PA spec for manifest names 349 /// claim_generator: User agent see c2pa spec for format 350 pub fn new_with_user_guid<S: Into<String>>(claim_generator: S, user_guid: S) -> Self { 351 Claim { 352 remote_manifest: RemoteManifest::NoRemote, 353 box_prefix: "self#jumbf".to_string(), 354 root: jumbf::labels::MANIFEST_STORE.to_string(), 355 signature_val: Vec::new(), 356 ingredients_store: HashMap::new(), 357 label: user_guid.into(), // todo figure out how to validate this 358 signature: "".to_string(), 359 360 claim_generator: claim_generator.into(), 361 claim_generator_info: None, 362 assertion_store: Vec::new(), 363 vc_store: Vec::new(), 364 assertions: Vec::new(), 365 original_bytes: None, 366 original_box_order: None, 367 redacted_assertions: None, 368 alg: Some(BUILD_HASH_ALG.into()), 369 alg_soft: None, 370 claim_generator_hints: None, 371 372 title: None, 373 format: "".to_string(), 374 instance_id: "".to_string(), 375 376 update_manifest: false, 377 data_boxes: Vec::new(), 378 } 379 } 380 381 /// Build a claim and verify its integrity. 382 pub fn build(&mut self) -> Result<()> { 383 // A claim must have a signature box. 384 if self.signature.is_empty() { 385 self.add_signature_box_link(); 386 } 387 388 Ok(()) 389 } 390 391 /// return version this claim supports 392 pub const fn build_version() -> &'static str { 393 Self::LABEL 394 } 395 396 /// Return the JUMBF label for this claim. 397 pub fn label(&self) -> &str { 398 &self.label 399 } 400 401 /// Return the JUMBF URI for this claim. 402 pub fn uri(&self) -> String { 403 jumbf::labels::to_manifest_uri(&self.label) 404 } 405 406 /// Return the JUMBF URI for an assertion on this claim. 407 pub fn assertion_uri(&self, assertion_label: &str) -> String { 408 jumbf::labels::to_assertion_uri(&self.label, assertion_label) 409 } 410 411 /// Return the JUMBF Signature URI for this claim. 412 pub fn signature_uri(&self) -> String { 413 jumbf::labels::to_signature_uri(&self.label) 414 } 415 416 // Add link to the signature box for this claim. 417 fn add_signature_box_link(&mut self) { 418 self.signature = format!("{}={}", self.box_prefix, jumbf::labels::SIGNATURE); 419 } 420 421 /// set signature of the claim 422 pub(crate) fn set_signature_val(&mut self, signature: Vec<u8>) { 423 self.signature_val = signature; 424 } 425 426 /// get signature of the claim 427 pub const fn signature_val(&self) -> &Vec<u8> { 428 &self.signature_val 429 } 430 431 /// get claim generator 432 pub fn claim_generator(&self) -> &str { 433 &self.claim_generator 434 } 435 436 /// get format 437 pub fn format(&self) -> &str { 438 &self.format 439 } 440 441 /// get instance_id 442 pub fn instance_id(&self) -> &str { 443 &self.instance_id 444 } 445 446 /// set title 447 pub fn set_title(&mut self, title: Option<String>) { 448 self.title = title; 449 } 450 451 /// get title 452 pub const fn title(&self) -> Option<&String> { 453 self.title.as_ref() 454 } 455 456 /// order for which to generate the JUMBF boxes with writing manifest 457 pub fn set_box_order(&mut self, box_order: Vec<&'static str>) { 458 self.original_box_order = Some(box_order); 459 } 460 461 /// order to process 462 pub fn get_box_order(&self) -> &[&str] { 463 const DEFAULT_MANIFEST_ORDER: [&str; 5] = 464 [ASSERTIONS, CLAIM, SIGNATURE, CREDENTIALS, DATABOXES]; 465 466 if let Some(bo) = &self.original_box_order { 467 bo 468 } else { 469 &DEFAULT_MANIFEST_ORDER 470 } 471 } 472 473 /// get algorithm 474 pub fn alg(&self) -> &str { 475 match self.alg.as_ref() { 476 Some(alg) => alg, 477 None => BUILD_HASH_ALG, 478 } 479 } 480 481 /// get soft algorithm 482 pub const fn alg_soft(&self) -> Option<&String> { 483 self.alg_soft.as_ref() 484 } 485 486 /// Is this an update manifest 487 pub const fn update_manifest(&self) -> bool { 488 self.update_manifest 489 } 490 491 pub fn set_remote_manifest<S: Into<String> + AsRef<str>>( 492 &mut self, 493 remote_url: S, 494 ) -> Result<()> { 495 let url = url::Url::parse(remote_url.as_ref()) 496 .map_err(|_e| Error::BadParam("remote url is badly formed".to_string()))?; 497 self.remote_manifest = RemoteManifest::Remote(url.to_string()); 498 499 Ok(()) 500 } 501 502 pub fn set_embed_remote_manifest<S: Into<String> + AsRef<str>>( 503 &mut self, 504 remote_url: S, 505 ) -> Result<()> { 506 let url = url::Url::parse(remote_url.as_ref()) 507 .map_err(|_e| Error::BadParam("remote url is badly formed".to_string()))?; 508 self.remote_manifest = RemoteManifest::EmbedWithRemote(url.to_string()); 509 510 Ok(()) 511 } 512 513 pub fn set_external_manifest(&mut self) { 514 self.remote_manifest = RemoteManifest::SideCar; 515 } 516 517 pub(crate) fn remote_manifest(&self) -> RemoteManifest { 518 self.remote_manifest.clone() 519 } 520 521 pub(crate) fn set_update_manifest(&mut self, is_update_manifest: bool) { 522 self.update_manifest = is_update_manifest; 523 } 524 525 pub fn add_claim_generator_info(&mut self, info: ClaimGeneratorInfo) -> &mut Self { 526 match self.claim_generator_info.as_mut() { 527 Some(cgi) => cgi.push(info), 528 None => self.claim_generator_info = Some([info].to_vec()), 529 } 530 self 531 } 532 533 pub fn claim_generator_info(&self) -> Option<&[ClaimGeneratorInfo]> { 534 self.claim_generator_info.as_deref() 535 } 536 537 pub fn add_claim_generator_hint(&mut self, hint_key: &str, hint_value: Value) { 538 if self.claim_generator_hints.is_none() { 539 self.claim_generator_hints = Some(HashMap::new()); 540 } 541 542 if let Some(map) = &mut self.claim_generator_hints { 543 // if the key is already there do we need to merge the new value, so get its value 544 let curr_val = match hint_key { 545 // keys where new values should be merges 546 GH_UA | GH_FULL_VERSION_LIST => { 547 if let Some(curr_ch_ua) = map.get(hint_key) { 548 curr_ch_ua.as_str().map(|curr_val| curr_val.to_owned()) 549 } else { 550 None 551 } 552 } 553 _ => None, 554 }; 555 556 // had an existing value so merge 557 if let Some(curr_val) = curr_val { 558 if let Some(append_val) = hint_value.as_str() { 559 map.insert( 560 hint_key.to_string(), 561 Value::String(format!("{curr_val}, {append_val}")), 562 ); 563 } 564 return; 565 } 566 567 // all other keys treat as replacement 568 map.insert(hint_key.to_string(), hint_value); 569 } 570 } 571 572 pub const fn get_claim_generator_hint_map(&self) -> Option<&HashMap<String, Value>> { 573 self.claim_generator_hints.as_ref() 574 } 575 576 pub fn calc_assertion_box_hash( 577 label: &str, 578 assertion: &Assertion, 579 salt: Option<Vec<u8>>, 580 alg: &str, 581 ) -> Result<Vec<u8>> { 582 // Grab assertion data object. 583 let d = assertion.decode_data(); 584 585 let mut hash_bytes = Vec::with_capacity(2048); 586 587 match d { 588 AssertionData::Json(_) => { 589 let mut json_data = CAIJSONAssertionBox::new(label); 590 json_data.add_json(assertion.data().to_vec()); 591 if let Some(salt) = salt { 592 json_data.set_salt(salt)?; 593 } 594 json_data.super_box().write_box_payload(&mut hash_bytes)?; 595 } 596 AssertionData::Binary(_) => { 597 // TODO: Handle other binary box types if needed. 598 let mut data = JumbfEmbeddedFileBox::new(label); 599 data.add_data(assertion.data().to_vec(), assertion.mime_type(), None); 600 if let Some(salt) = salt { 601 data.set_salt(salt)?; 602 } 603 data.super_box().write_box_payload(&mut hash_bytes)?; 604 } 605 AssertionData::Cbor(_) => { 606 let mut cbor_data = CAICBORAssertionBox::new(label); 607 cbor_data.add_cbor(assertion.data().to_vec()); 608 if let Some(salt) = salt { 609 cbor_data.set_salt(salt)?; 610 } 611 cbor_data.super_box().write_box_payload(&mut hash_bytes)?; 612 } 613 AssertionData::Uuid(uuid_str, _) => { 614 let mut data = CAIUUIDAssertionBox::new(label); 615 data.add_uuid(uuid_str, assertion.data().to_vec())?; 616 if let Some(salt) = salt { 617 data.set_salt(salt)?; 618 } 619 data.super_box().write_box_payload(&mut hash_bytes)?; 620 } 621 } 622 623 Ok(hash_by_alg(alg, &hash_bytes, None)) 624 } 625 626 /// Add an assertion to this claim and verify 627 pub fn add_assertion( 628 &mut self, 629 assertion_builder: &impl AssertionBase, 630 ) -> Result<C2PAAssertion> { 631 self.add_assertion_with_salt(assertion_builder, NO_SALT) 632 } 633 634 /// Add an assertion to this claim and verify with a salted assertion store 635 /// This version should be used if the assertion may be redacted for addition protection. 636 pub fn add_assertion_with_salt( 637 &mut self, 638 assertion_builder: &impl AssertionBase, 639 salt_generator: &impl SaltGenerator, 640 ) -> Result<C2PAAssertion> { 641 // make sure the assertion is valid 642 let assertion = assertion_builder.to_assertion()?; 643 644 // Update label if there are multiple instances of 645 // the same claim type. 646 let as_label = self.make_assertion_instance_label(assertion.label().as_ref()); 647 648 // Get salted hash of the assertion's contents. 649 let salt = salt_generator.generate_salt(); 650 651 let hash = Claim::calc_assertion_box_hash(&as_label, &assertion, salt.clone(), self.alg())?; 652 653 // Build hash link. 654 let link = jumbf::labels::to_assertion_uri(self.label(), &as_label); 655 let link_relative = jumbf::labels::to_relative_uri(&link); 656 657 let mut c2pa_assertion = C2PAAssertion::new(link_relative, None, &hash); 658 c2pa_assertion.add_salt(salt.clone()); 659 660 // Add to assertion store. 661 let (_l, instance) = Claim::assertion_label_from_link(&as_label); 662 let ca = ClaimAssertion::new(assertion, instance, &hash, self.alg(), salt); 663 self.assertion_store.push(ca); 664 self.assertions.push(c2pa_assertion.clone()); 665 666 Ok(c2pa_assertion) 667 } 668 669 // Add a new DataBox and return the HashedURI reference 670 pub fn add_databox( 671 &mut self, 672 format: &str, 673 data: Vec<u8>, 674 data_types: Option<Vec<AssetType>>, 675 ) -> Result<HashedUri> { 676 // create data box 677 let new_db = DataBox { 678 format: format.to_string(), 679 data, 680 data_types, 681 }; 682 683 // serialize to cbor 684 let db_cbor = serde_cbor::to_vec(&new_db).map_err(|_err| Error::AssertionEncoding)?; 685 686 // get the index for the new assertion 687 let mut index = 0; 688 for (uri, _db) in &self.data_boxes { 689 let (_l, i) = Claim::assertion_label_from_link(&uri.url()); 690 if i >= index { 691 index = i + 1; 692 } 693 } 694 695 let label = Claim::label_with_instance(DATABOX, index); 696 let link = jumbf::labels::to_databox_uri(self.label(), &label); 697 698 // salt box for 1.2 VC redaction support 699 let ds = DefaultSalt::default(); 700 let salt = ds.generate_salt(); 701 702 // assertion JUMBF box hash for 1.2 validation 703 let assertion = Assertion::from_data_cbor(&label, &db_cbor); 704 let hash = Claim::calc_assertion_box_hash(&label, &assertion, salt.clone(), self.alg())?; 705 706 let mut databox_uri = C2PAAssertion::new(link, Some(self.alg().to_string()), &hash); 707 databox_uri.add_salt(salt); 708 709 // add databox to databox store 710 self.data_boxes.push((databox_uri.clone(), new_db)); 711 712 Ok(databox_uri) 713 } 714 715 pub(crate) const fn databoxes(&self) -> &Vec<(HashedUri, DataBox)> { 716 &self.data_boxes 717 } 718 719 pub fn find_databox(&self, uri: &str) -> Option<&DataBox> { 720 self.data_boxes 721 .iter() 722 .find(|(h, _d)| h.url() == uri) 723 .map(|(_sh, data_box)| data_box) 724 } 725 726 /// Load known VC with optional salt 727 pub(crate) fn put_data_box( 728 &mut self, 729 label: &str, 730 databox_cbor: &[u8], 731 salt: Option<Vec<u8>>, 732 ) -> Result<()> { 733 let link = jumbf::labels::to_databox_uri(self.label(), label); 734 735 // assertion JUMBF box hash for 1.2 validation 736 let assertion = Assertion::from_data_cbor(label, databox_cbor); 737 let hash = Claim::calc_assertion_box_hash(label, &assertion, salt.clone(), self.alg())?; 738 739 let mut uri = C2PAAssertion::new(link, Some(self.alg().to_string()), &hash); 740 uri.add_salt(salt); 741 742 let db: DataBox = 743 serde_cbor::from_slice(databox_cbor).map_err(|_err| Error::AssertionEncoding)?; 744 745 // add data box to data box store 746 self.data_boxes.push((uri, db)); 747 748 Ok(()) 749 } 750 751 pub fn get_data_box(&self, uri: &str) -> Option<&DataBox> { 752 // normalize uri 753 let normalized_uri = if let Some(manifest) = manifest_label_from_uri(uri) { 754 if manifest != self.label() { 755 return None; 756 } 757 uri.to_owned() 758 } else { 759 // make a full path 760 if let Some(box_name) = box_name_from_uri(uri) { 761 to_databox_uri(self.label(), &box_name) 762 } else { 763 return None; 764 } 765 }; 766 767 self.data_boxes.iter().find_map(|x| { 768 if x.0.url() == normalized_uri { 769 Some(&x.1) 770 } else { 771 None 772 } 773 }) 774 } 775 776 pub(crate) fn vc_id(vc_json: &str) -> Result<String> { 777 let vc: Value = 778 serde_json::from_str(vc_json).map_err(|_err| Error::VerifiableCredentialInvalid)?; // check for json validity 779 780 let credential_subject = vc 781 .get("credentialSubject") 782 .ok_or(Error::VerifiableCredentialInvalid)?; 783 let id = credential_subject 784 .get("id") 785 .ok_or(Error::VerifiableCredentialInvalid)? 786 .as_str() 787 .ok_or(Error::VerifiableCredentialInvalid)?; 788 789 Ok(id.to_string()) 790 } 791 792 /// Add a verifiable credential to vc store and return a JUMBF URI 793 /// the credential json must contain "credentialsSubject" object like: 794 /// ```json 795 /// "credentialSubject": { 796 /// "id": "did:nppa:eb1bb9934d9896a374c384521410c7f14", 797 /// "name": "Bob Ross", 798 /// "memberOf": "https://nppa.org/" 799 /// }, 800 /// ``` 801 // the "id" value will be used as the label in the vcstore 802 pub fn add_verifiable_credential(&mut self, vc_json: &str) -> Result<HashedUri> { 803 let id = Claim::vc_id(vc_json)?; 804 let credential = AssertionData::Json(vc_json.to_string()); 805 806 let link = jumbf::labels::to_verifiable_credential_uri(self.label(), &id); 807 808 // salt box for 1.2 VC redaction support 809 let ds = DefaultSalt::default(); 810 let salt = ds.generate_salt(); 811 812 // assertion JUMBF box hash for 1.2 validation 813 let assertion = Assertion::from_data_json(&id, vc_json.as_bytes())?; 814 let hash = Claim::calc_assertion_box_hash(&id, &assertion, salt.clone(), self.alg())?; 815 816 let mut c2pa_assertion = C2PAAssertion::new(link, Some(self.alg().to_string()), &hash); 817 c2pa_assertion.add_salt(salt); 818 819 // add credential to vcstore 820 self.vc_store.push((c2pa_assertion.clone(), credential)); 821 822 Ok(c2pa_assertion) 823 } 824 825 /// Load known VC with optional salt 826 pub(crate) fn put_verifiable_credential( 827 &mut self, 828 vc_json: &str, 829 salt: Option<Vec<u8>>, 830 ) -> Result<()> { 831 let id = Claim::vc_id(vc_json)?; 832 let credential = AssertionData::Json(vc_json.to_string()); 833 834 let link = jumbf::labels::to_verifiable_credential_uri(self.label(), &id); 835 836 // assertion JUMBF box hash for 1.2 validation 837 let assertion = Assertion::from_data_json(&id, vc_json.as_bytes())?; 838 let hash = Claim::calc_assertion_box_hash(&id, &assertion, salt.clone(), self.alg())?; 839 840 let mut c2pa_assertion = C2PAAssertion::new(link, Some(self.alg().to_string()), &hash); 841 c2pa_assertion.add_salt(salt); 842 843 // add credential to vcstore 844 self.vc_store.push((c2pa_assertion, credential)); 845 846 Ok(()) 847 } 848 849 pub fn get_verifiable_credentials(&self) -> Vec<&AssertionData> { 850 self.vc_store.iter().map(|t| &t.1).collect::<Vec<_>>() 851 } 852 853 pub const fn get_verifiable_credentials_store(&self) -> &Vec<(HashedUri, AssertionData)> { 854 &self.vc_store 855 } 856 857 /// Add directly to store during a reload of a claim 858 pub(crate) fn put_assertion_store(&mut self, assertion: ClaimAssertion) { 859 self.assertion_store.push(assertion); 860 } 861 862 // Patch an existing assertion with new contents. 863 // 864 // `replace_with` should match in name and size of an existing assertion. 865 fn update_assertion<MatchFn, PatchFn>( 866 &mut self, 867 replace_with: Assertion, 868 match_fn: MatchFn, 869 patch_fn: PatchFn, 870 ) -> Result<()> 871 where 872 MatchFn: Fn(&ClaimAssertion) -> bool, 873 PatchFn: FnOnce(&ClaimAssertion, Assertion) -> Result<Assertion>, 874 { 875 // Find the assertion that should be replaced. 876 let Some(ref mut target_assertion) = self 877 .assertion_store 878 .iter_mut() 879 .find(|ca| Assertion::assertions_eq(&replace_with, ca.assertion()) && match_fn(ca)) 880 else { 881 return Err(Error::NotFound); 882 }; 883 884 // Save off copy of original hash to cross-check before 885 // replacing it. 886 let original_hash = target_assertion.hash().to_vec(); 887 888 // Give caller a chance to patch/replace the assertion. 889 let replace_with = patch_fn(target_assertion, replace_with)?; 890 891 // Calculate new hash, given new content. 892 let replacement_hash = Claim::calc_assertion_box_hash( 893 &target_assertion.label(), 894 &replace_with, 895 target_assertion.salt().clone(), 896 target_assertion.hash_alg(), 897 )?; 898 899 target_assertion.update_assertion(replace_with, replacement_hash)?; 900 901 let target_label = target_assertion.label(); 902 let target_hash = target_assertion.hash(); 903 904 // Replace the existing hash in the hashed URI reference 905 // with the newly-calculated hash. 906 let Some(f) = self 907 .assertions 908 .iter_mut() 909 .find(|f| f.url().contains(&target_label) && vec_compare(&f.hash(), &original_hash)) 910 else { 911 return Err(Error::NotFound); 912 }; 913 914 // Replace existing hash with newly-calculated hash. 915 f.update_hash(target_hash.to_vec()); 916 Ok(()) 917 } 918 919 // Crate private function to allow for patching a data hash with final contents. 920 pub(crate) fn update_data_hash(&mut self, mut data_hash: DataHash) -> Result<()> { 921 let dh_name = data_hash.name.clone(); 922 923 self.update_assertion( 924 data_hash.to_assertion()?, 925 |ca: &ClaimAssertion| { 926 if let Ok(dh) = DataHash::from_assertion(ca.assertion()) { 927 dh.name == dh_name 928 } else { 929 false 930 } 931 }, 932 |target_assertion: &ClaimAssertion, _: Assertion| { 933 let original_len = target_assertion.assertion().data().len(); 934 data_hash.pad_to_size(original_len)?; 935 data_hash.to_assertion() 936 }, 937 ) 938 } 939 940 // Crate private function to allow for patching a BMFF hash with final contents. 941 pub(crate) fn update_bmff_hash(&mut self, bmff_hash: BmffHash) -> Result<()> { 942 self.replace_assertion(bmff_hash.to_assertion()?) 943 } 944 945 // Patch an existing assertion with new contents. 946 // 947 // `replace_with` should match in name and size of an existing assertion. 948 pub(crate) fn replace_assertion(&mut self, replace_with: Assertion) -> Result<()> { 949 self.update_assertion( 950 replace_with, 951 |_: &ClaimAssertion| true, 952 |_: &ClaimAssertion, a: Assertion| Ok(a), 953 ) 954 } 955 956 /// Redact an assertion from a prior claim. 957 /// This will remove the assertion from the JUMBF 958 fn redact_assertion(&mut self, assertion_uri: &str) -> Result<()> { 959 // cannot redact action assertions per the spec 960 let (label, _instance) = Claim::assertion_label_from_link(assertion_uri); 961 if label == labels::ACTIONS { 962 return Err(Error::AssertionInvalidRedaction); 963 } 964 965 // delete assertion 966 if let Some(index) = self 967 .assertion_store 968 .iter() 969 .position(|x| assertion_uri.contains(&x.label())) 970 { 971 self.assertion_store.remove(index); 972 Ok(()) 973 } else { 974 Err(Error::AssertionInvalidRedaction) 975 } 976 } 977 978 /// Return a hash of this claim. 979 pub fn hash(&self) -> Vec<u8> { 980 match self.data() { 981 Ok(claim_data) => hash_by_alg(self.alg(), &claim_data, None), 982 Err(_) => Vec::new(), // should never happen bug if it does just give no hash 983 } 984 } 985 986 /// Return the signing date and time for this claim, if there is one. 987 pub fn signing_time(&self) -> Option<DateTime<Utc>> { 988 if let Some(validation_data) = self.signature_info() { 989 validation_data.date 990 } else { 991 None 992 } 993 } 994 995 /// Return the signing issuer for this claim, if there is one. 996 pub fn signing_issuer(&self) -> Option<String> { 997 if let Some(validation_data) = self.signature_info() { 998 validation_data.issuer_org 999 } else { 1000 None 1001 } 1002 } 1003 1004 /// Return the cert's serial number, if there is one. 1005 pub fn signing_cert_serial(&self) -> Option<String> { 1006 self.signature_info() 1007 .and_then(|validation_info| validation_info.cert_serial_number) 1008 .map(|serial| serial.to_string()) 1009 } 1010 1011 /// Return information about the signature 1012 pub fn signature_info(&self) -> Option<ValidationInfo> { 1013 let sig = self.signature_val(); 1014 let data = self.data().ok()?; 1015 let mut validation_log = OneShotStatusTracker::new(); 1016 1017 Some(get_signing_info(sig, &data, &mut validation_log)) 1018 } 1019 1020 /// Verify claim signature, assertion store and asset hashes 1021 /// claim - claim to be verified 1022 /// asset_bytes - reference to bytes of the asset 1023 pub(crate) async fn verify_claim_async<'a>( 1024 claim: &Claim, 1025 asset_data: &mut ClaimAssetData<'_>, 1026 is_provenance: bool, 1027 th: &dyn TrustHandlerConfig, 1028 validation_log: &mut impl StatusTracker, 1029 ) -> Result<()> { 1030 // Parse COSE signed data (signature) and validate it. 1031 let sig = claim.signature_val().clone(); 1032 let additional_bytes: Vec<u8> = Vec::new(); 1033 let claim_data = claim.data()?; 1034 1035 // make sure signature manifest if present points to this manifest 1036 let sig_box_err = match jumbf::labels::manifest_label_from_uri(&claim.signature) { 1037 Some(signature_url) if signature_url != claim.label() => true, 1038 _ => { 1039 jumbf::labels::box_name_from_uri(&claim.signature).unwrap_or_default() 1040 != jumbf::labels::SIGNATURE 1041 } // relative signature box 1042 }; 1043 1044 if sig_box_err { 1045 let log_item = log_item!( 1046 claim.signature_uri(), 1047 "signature missing", 1048 "verify_claim_async" 1049 ) 1050 .error(Error::ClaimMissingSignatureBox) 1051 .validation_status(validation_status::CLAIM_SIGNATURE_MISSING); 1052 1053 validation_log.log(log_item, Some(Error::ClaimMissingSignatureBox))?; 1054 } 1055 1056 let verified = verify_cose_async( 1057 sig, 1058 claim_data, 1059 additional_bytes, 1060 !is_provenance, 1061 th, 1062 validation_log, 1063 ) 1064 .await; 1065 Claim::verify_internal(claim, asset_data, is_provenance, verified, validation_log) 1066 } 1067 1068 /// Verify claim signature, assertion store and asset hashes 1069 /// claim - claim to be verified 1070 /// asset_bytes - reference to bytes of the asset 1071 pub(crate) fn verify_claim( 1072 claim: &Claim, 1073 asset_data: &mut ClaimAssetData<'_>, 1074 is_provenance: bool, 1075 th: &dyn TrustHandlerConfig, 1076 validation_log: &mut impl StatusTracker, 1077 ) -> Result<()> { 1078 // Parse COSE signed data (signature) and validate it. 1079 let sig = claim.signature_val(); 1080 let additional_bytes: Vec<u8> = Vec::new(); 1081 1082 // make sure signature manifest if present points to this manifest 1083 let sig_box_err = match jumbf::labels::manifest_label_from_uri(&claim.signature) { 1084 Some(signature_url) if signature_url != claim.label() => true, 1085 _ => { 1086 jumbf::labels::box_name_from_uri(&claim.signature).unwrap_or_default() 1087 != jumbf::labels::SIGNATURE 1088 } // relative signature box 1089 }; 1090 1091 if sig_box_err { 1092 let log_item = log_item!(claim.signature_uri(), "signature missing", "verify_claim") 1093 .error(Error::ClaimMissingSignatureBox) 1094 .validation_status(validation_status::CLAIM_SIGNATURE_MISSING); 1095 validation_log.log(log_item, Some(Error::ClaimMissingSignatureBox))?; 1096 } 1097 1098 let data = if let Some(ref original_bytes) = claim.original_bytes { 1099 original_bytes 1100 } else { 1101 return Err(Error::ClaimDecoding); 1102 }; 1103 1104 let verified = verify_cose( 1105 sig, 1106 data, 1107 &additional_bytes, 1108 !is_provenance, 1109 th, 1110 validation_log, 1111 ); 1112 1113 Claim::verify_internal(claim, asset_data, is_provenance, verified, validation_log) 1114 } 1115 1116 /// Get the signing certificate chain as PEM bytes 1117 pub fn get_cert_chain(&self) -> Result<Vec<u8>> { 1118 let sig = self.signature_val(); 1119 let data = self.data()?; 1120 let mut validation_log = OneShotStatusTracker::new(); 1121 1122 let vi = get_signing_info(sig, &data, &mut validation_log); 1123 1124 Ok(vi.cert_chain) 1125 } 1126 1127 fn verify_internal( 1128 claim: &Claim, 1129 asset_data: &mut ClaimAssetData<'_>, 1130 is_provenance: bool, 1131 verified: Result<ValidationInfo>, 1132 validation_log: &mut impl StatusTracker, 1133 ) -> Result<()> { 1134 const UNNAMED: &str = "unnamed"; 1135 let default_str = |s: &String| s.clone(); 1136 1137 match verified { 1138 Ok(vi) => { 1139 if !vi.validated { 1140 let log_item = log_item!( 1141 claim.signature_uri(), 1142 "claim signature is not valid", 1143 "verify_internal" 1144 ) 1145 .error(Error::CoseSignature) 1146 .validation_status(validation_status::CLAIM_SIGNATURE_MISMATCH); 1147 validation_log.log(log_item, Some(Error::CoseSignature))?; 1148 } else { 1149 let log_item = log_item!( 1150 claim.signature_uri(), 1151 "claim signature valid", 1152 "verify_internal" 1153 ) 1154 .validation_status(validation_status::CLAIM_SIGNATURE_VALIDATED); 1155 validation_log.log_silent(log_item); 1156 } 1157 } 1158 Err(parse_err) => { 1159 let log_item = log_item!( 1160 claim.signature_uri(), 1161 "claim signature is not valid", 1162 "verify_internal" 1163 ) 1164 .error(parse_err) 1165 .validation_status(validation_status::CLAIM_SIGNATURE_MISMATCH); 1166 validation_log.log(log_item, Some(Error::CoseSignature))?; 1167 } 1168 }; 1169 1170 // check for self redacted assertions and illegal redactions 1171 if let Some(redactions) = claim.redactions() { 1172 for r in redactions { 1173 let r_manifest = jumbf::labels::manifest_label_from_uri(r) 1174 .ok_or(Error::AssertionInvalidRedaction)?; 1175 if claim.label().contains(&r_manifest) { 1176 let log_item = log_item!( 1177 claim.uri(), 1178 "claim contains self redaction", 1179 "verify_internal" 1180 ) 1181 .error(Error::ClaimSelfRedact) 1182 .validation_status(validation_status::ASSERTION_SELF_REDACTED); 1183 validation_log.log(log_item, Some(Error::ClaimSelfRedact))?; 1184 } 1185 1186 if r.contains(assertions::labels::ACTIONS) { 1187 let log_item = log_item!( 1188 claim.uri(), 1189 "redaction of action assertions disallowed", 1190 "verify_internal" 1191 ) 1192 .error(Error::ClaimDisallowedRedaction) 1193 .validation_status(validation_status::ACTION_ASSERTION_REDACTED); 1194 validation_log.log(log_item, Some(Error::ClaimDisallowedRedaction))?; 1195 } 1196 } 1197 } 1198 1199 // make sure UpdateManifests do not contain actions 1200 if claim.update_manifest() && claim.label().contains(assertions::labels::ACTIONS) { 1201 let log_item = log_item!( 1202 claim.uri(), 1203 "update manifests cannot contain actions", 1204 "verify_internal" 1205 ) 1206 .error(Error::UpdateManifestInvalid) 1207 .validation_status(validation_status::MANIFEST_UPDATE_INVALID); 1208 validation_log.log(log_item, Some(Error::UpdateManifestInvalid))?; 1209 } 1210 1211 // verify assertion structure comparing hashes from assertion list to contents of assertion store 1212 for assertion in claim.assertions() { 1213 let (label, instance) = Claim::assertion_label_from_link(&assertion.url()); 1214 match claim.get_claim_assertion(&label, instance) { 1215 // get the assertion if label and hash match 1216 Some(ca) => { 1217 if !vec_compare(ca.hash(), &assertion.hash()) { 1218 let log_item = log_item!( 1219 assertion.url(), 1220 format!("hash does not match assertion data: {}", assertion.url()), 1221 "verify_internal" 1222 ) 1223 .error(Error::HashMismatch(format!( 1224 "Assertion hash failure: {}", 1225 assertion.url() 1226 ))) 1227 .validation_status(validation_status::ASSERTION_HASHEDURI_MISMATCH); 1228 validation_log.log( 1229 log_item, 1230 Some(Error::HashMismatch(format!( 1231 "Assertion hash failure: {}", 1232 assertion.url() 1233 ))), 1234 )?; 1235 } else { 1236 let log_item = log_item!( 1237 assertion.url(), 1238 format!("hashed uri matched: {}", assertion.url()), 1239 "verify_internal" 1240 ) 1241 .validation_status(validation_status::ASSERTION_HASHEDURI_MATCH); 1242 validation_log.log_silent(log_item); 1243 } 1244 } 1245 None => { 1246 let log_item = log_item!( 1247 assertion.url(), 1248 format!("cannot find matching assertion: {}", assertion.url()), 1249 "verify_internal" 1250 ) 1251 .error(Error::AssertionMissing { 1252 url: assertion.url(), 1253 }) 1254 .validation_status(validation_status::ASSERTION_MISSING); 1255 validation_log.log( 1256 log_item, 1257 Some(Error::AssertionMissing { 1258 url: assertion.url(), 1259 }), 1260 )?; 1261 } 1262 } 1263 } 1264 1265 // verify data hashes for provenance claims 1266 if is_provenance { 1267 // must have at least one hard binding for normal manifests 1268 if claim.hash_assertions().is_empty() && !claim.update_manifest() { 1269 let log_item = log_item!( 1270 &claim.uri(), 1271 "claim missing data binding", 1272 "verify_internal" 1273 ) 1274 .error(Error::ClaimMissingHardBinding) 1275 .validation_status(validation_status::HARD_BINDINGS_MISSING); 1276 validation_log.log(log_item, Some(Error::ClaimMissingHardBinding))?; 1277 } 1278 1279 // update manifests cannot have data hashes 1280 if !claim.hash_assertions().is_empty() && claim.update_manifest() { 1281 let log_item = log_item!( 1282 &claim.uri(), 1283 "update manifests cannot contain data hash assertions", 1284 "verify_internal" 1285 ) 1286 .error(Error::UpdateManifestInvalid) 1287 .validation_status(validation_status::MANIFEST_UPDATE_INVALID); 1288 validation_log.log(log_item, Some(Error::UpdateManifestInvalid))?; 1289 } 1290 1291 for hash_binding_assertion in claim.hash_assertions() { 1292 if hash_binding_assertion.label_root() == DataHash::LABEL { 1293 let dh = DataHash::from_assertion(hash_binding_assertion)?; 1294 let name = dh.name.as_ref().map_or(UNNAMED.to_string(), default_str); 1295 if !dh.is_remote_hash() { 1296 // only verify local hashes here 1297 let hash_result = match asset_data { 1298 #[cfg(feature = "file_io")] 1299 ClaimAssetData::Path(asset_path) => { 1300 dh.verify_hash(asset_path, Some(claim.alg())) 1301 } 1302 ClaimAssetData::Bytes(asset_bytes, _) => { 1303 dh.verify_in_memory_hash(asset_bytes, Some(claim.alg())) 1304 } 1305 ClaimAssetData::Stream(stream_data, _) => { 1306 dh.verify_stream_hash(*stream_data, Some(claim.alg())) 1307 } 1308 _ => return Err(Error::UnsupportedType), /* this should never happen (coding error) */ 1309 }; 1310 1311 match hash_result { 1312 Ok(_a) => { 1313 let log_item = log_item!( 1314 claim.assertion_uri(&hash_binding_assertion.label()), 1315 "data hash valid", 1316 "verify_internal" 1317 ) 1318 .validation_status(validation_status::ASSERTION_DATAHASH_MATCH); 1319 validation_log.log_silent(log_item); 1320 1321 continue; 1322 } 1323 Err(e) => { 1324 let log_item = log_item!( 1325 claim.assertion_uri(&hash_binding_assertion.label()), 1326 format!("asset hash error, name: {name}, error: {e}"), 1327 "verify_internal" 1328 ) 1329 .error(Error::HashMismatch(format!("Asset hash failure: {e}"))) 1330 .validation_status(validation_status::ASSERTION_DATAHASH_MISMATCH); 1331 1332 validation_log.log( 1333 log_item, 1334 Some(Error::HashMismatch(format!("Asset hash failure: {e}"))), 1335 )?; 1336 } 1337 } 1338 } 1339 } else if hash_binding_assertion.label_root() == BmffHash::LABEL { 1340 // handle BMFF data hashes 1341 let dh = BmffHash::from_assertion(hash_binding_assertion)?; 1342 1343 let name = dh.name().map_or("unnamed".to_string(), default_str); 1344 1345 let hash_result = match asset_data { 1346 #[cfg(feature = "file_io")] 1347 ClaimAssetData::Path(asset_path) => { 1348 dh.verify_hash(asset_path, Some(claim.alg())) 1349 } 1350 ClaimAssetData::Bytes(asset_bytes, _) => { 1351 dh.verify_in_memory_hash(asset_bytes, Some(claim.alg())) 1352 } 1353 ClaimAssetData::Stream(stream_data, _) => { 1354 dh.verify_stream_hash(*stream_data, Some(claim.alg())) 1355 } 1356 ClaimAssetData::StreamFragment(initseg_data, fragment_data, _) => dh 1357 .verify_stream_segment( 1358 *initseg_data, 1359 *fragment_data, 1360 Some(claim.alg()), 1361 ), 1362 }; 1363 1364 match hash_result { 1365 Ok(_a) => { 1366 let log_item = log_item!( 1367 claim.assertion_uri(&hash_binding_assertion.label()), 1368 "data hash valid", 1369 "verify_internal" 1370 ) 1371 .validation_status(validation_status::ASSERTION_BMFFHASH_MATCH); 1372 validation_log.log_silent(log_item); 1373 1374 continue; 1375 } 1376 Err(e) => { 1377 let log_item = log_item!( 1378 claim.assertion_uri(&hash_binding_assertion.label()), 1379 format!("asset hash error, name: {name}, error: {e}"), 1380 "verify_internal" 1381 ) 1382 .error(Error::HashMismatch(format!("Asset hash failure: {e}"))) 1383 .validation_status(validation_status::ASSERTION_BMFFHASH_MISMATCH); 1384 1385 validation_log.log( 1386 log_item, 1387 Some(Error::HashMismatch(format!("Asset hash failure: {e}"))), 1388 )?; 1389 } 1390 } 1391 } else if hash_binding_assertion.label_root() == BoxHash::LABEL { 1392 // box hash case 1393 // handle BMFF data hashes 1394 let bh = BoxHash::from_assertion(hash_binding_assertion)?; 1395 1396 let hash_result = match asset_data { 1397 #[cfg(feature = "file_io")] 1398 ClaimAssetData::Path(asset_path) => { 1399 let box_hash_processor = 1400 crate::jumbf_io::get_assetio_handler_from_path(asset_path) 1401 .ok_or(Error::UnsupportedType)? 1402 .asset_box_hash_ref() 1403 .ok_or(Error::HashMismatch( 1404 "Box hash not supported".to_string(), 1405 ))?; 1406 1407 bh.verify_hash(asset_path, Some(claim.alg()), box_hash_processor) 1408 } 1409 ClaimAssetData::Bytes(asset_bytes, asset_type) => { 1410 let box_hash_processor = get_assetio_handler(asset_type) 1411 .ok_or(Error::UnsupportedType)? 1412 .asset_box_hash_ref() 1413 .ok_or(Error::HashMismatch(format!( 1414 "Box hash not supported for: {asset_type}" 1415 )))?; 1416 1417 bh.verify_in_memory_hash( 1418 asset_bytes, 1419 Some(claim.alg()), 1420 box_hash_processor, 1421 ) 1422 } 1423 ClaimAssetData::Stream(stream_data, asset_type) => { 1424 let box_hash_processor = get_assetio_handler(asset_type) 1425 .ok_or(Error::UnsupportedType)? 1426 .asset_box_hash_ref() 1427 .ok_or(Error::HashMismatch(format!( 1428 "Box hash not supported for: {asset_type}" 1429 )))?; 1430 1431 bh.verify_stream_hash( 1432 *stream_data, 1433 Some(claim.alg()), 1434 box_hash_processor, 1435 ) 1436 } 1437 _ => return Err(Error::UnsupportedType), 1438 }; 1439 1440 match hash_result { 1441 Ok(_a) => { 1442 let log_item = log_item!( 1443 claim.assertion_uri(&hash_binding_assertion.label()), 1444 "data hash valid", 1445 "verify_internal" 1446 ) 1447 .validation_status(validation_status::ASSERTION_BOXHASH_MATCH); 1448 validation_log.log_silent(log_item); 1449 1450 continue; 1451 } 1452 Err(e) => { 1453 let log_item = log_item!( 1454 claim.assertion_uri(&hash_binding_assertion.label()), 1455 format!("asset hash error: {e}"), 1456 "verify_internal" 1457 ) 1458 .error(Error::HashMismatch(format!("Asset hash failure: {e}"))) 1459 .validation_status(validation_status::ASSERTION_BOXHASH_MISMATCH); 1460 1461 validation_log.log( 1462 log_item, 1463 Some(Error::HashMismatch(format!("Asset hash failure: {e}"))), 1464 )?; 1465 } 1466 } 1467 } 1468 } 1469 } 1470 Ok(()) 1471 } 1472 1473 /// Verify hash against self. True if match, 1474 /// false if no match or unsupported 1475 pub fn verify_hash(&self, hash: &[u8]) -> bool { 1476 // get hash of self for comparison 1477 if let Some(ref original_bytes) = self.original_bytes { 1478 verify_by_alg(self.alg(), hash, original_bytes, None) 1479 } else if let Ok(claim_data) = self.data() { 1480 verify_by_alg(self.alg(), hash, &claim_data, None) 1481 } else { 1482 false 1483 } 1484 } 1485 1486 /// Return list of data hash assertions 1487 pub fn hash_assertions(&self) -> Vec<&Assertion> { 1488 let dummy_data = AssertionData::Cbor(Vec::new()); 1489 let dummy_hash = Assertion::new(DataHash::LABEL, None, dummy_data); 1490 let mut data_hashes = self.assertions_by_type(&dummy_hash); 1491 1492 // add in an BMFF hashes 1493 let dummy_bmff_data = AssertionData::Cbor(Vec::new()); 1494 let dummy_bmff_hash = Assertion::new(assertions::labels::BMFF_HASH, None, dummy_bmff_data); 1495 data_hashes.append(&mut self.assertions_by_type(&dummy_bmff_hash)); 1496 1497 // add in an box hashes 1498 let dummy_box_data = AssertionData::Cbor(Vec::new()); 1499 let dummy_box_hash = Assertion::new(assertions::labels::BOX_HASH, None, dummy_box_data); 1500 data_hashes.append(&mut self.assertions_by_type(&dummy_box_hash)); 1501 1502 data_hashes 1503 } 1504 1505 pub fn bmff_hash_assertions(&self) -> Vec<&Assertion> { 1506 // add in an BMFF hashes 1507 let dummy_bmff_data = AssertionData::Cbor(Vec::new()); 1508 let dummy_bmff_hash = Assertion::new(assertions::labels::BMFF_HASH, None, dummy_bmff_data); 1509 self.assertions_by_type(&dummy_bmff_hash) 1510 } 1511 1512 pub fn box_hash_assertions(&self) -> Vec<&Assertion> { 1513 // add in an BMFF hashes 1514 let dummy_box_data = AssertionData::Cbor(Vec::new()); 1515 let dummy_box_hash = Assertion::new(assertions::labels::BOX_HASH, None, dummy_box_data); 1516 self.assertions_by_type(&dummy_box_hash) 1517 } 1518 1519 /// Return list of ingredient assertions. This function 1520 /// is only useful on committed or loaded claims since ingredients 1521 /// are resolved at commit time. 1522 pub fn ingredient_assertions(&self) -> Vec<&Assertion> { 1523 let dummy_data = AssertionData::Cbor(Vec::new()); 1524 let dummy_ingredient = Assertion::new(labels::INGREDIENT, None, dummy_data); 1525 self.assertions_by_type(&dummy_ingredient) 1526 } 1527 1528 /// Return reference to the internal claim assertion store. 1529 pub const fn claim_assertion_store(&self) -> &Vec<ClaimAssertion> { 1530 &self.assertion_store 1531 } 1532 1533 /// Return reference to the internal claim ingredient store. 1534 /// Used during generation 1535 pub const fn claim_ingredient_store(&self) -> &HashMap<String, Vec<Claim>> { 1536 &self.ingredients_store 1537 } 1538 1539 /// Return reference to the internal claim ingredient store matching this guid. 1540 /// Used during generation 1541 pub fn claim_ingredient(&self, claim_guid: &str) -> Option<&Vec<Claim>> { 1542 self.ingredients_store.get(claim_guid) 1543 } 1544 1545 /// Adds ingredients, this data will be written out during commit of the Claim 1546 pub(crate) fn add_ingredient_data( 1547 &mut self, 1548 provenance_label: &str, 1549 mut ingredient: Vec<Claim>, 1550 redactions_opt: Option<Vec<String>>, 1551 ) -> Result<()> { 1552 // redact assertion from incoming ingredients 1553 if let Some(redactions) = &redactions_opt { 1554 for redaction in redactions { 1555 if let Some(claim) = ingredient 1556 .iter_mut() 1557 .find(|x| redaction.contains(x.label())) 1558 { 1559 claim.redact_assertion(redaction)?; 1560 } else { 1561 return Err(Error::AssertionRedactionNotFound); 1562 } 1563 } 1564 } 1565 1566 // all have been removed (if necessary) so replace redaction list 1567 self.redacted_assertions = redactions_opt; 1568 1569 // add ingredients 1570 self.ingredients_store 1571 .insert(provenance_label.to_string(), ingredient); 1572 1573 Ok(()) 1574 } 1575 1576 /// List of redactions 1577 pub const fn redactions(&self) -> Option<&Vec<String>> { 1578 self.redacted_assertions.as_ref() 1579 } 1580 1581 /// Return snapshot clone of the claim's assertions. 1582 pub fn assertion_store(&self) -> Vec<Assertion> { 1583 self.assertion_store 1584 .iter() 1585 .map(|x| x.assertion.clone()) 1586 .collect() 1587 } 1588 1589 pub fn assertions_by_type(&self, assertion_proto: &Assertion) -> Vec<&Assertion> { 1590 self.assertion_store 1591 .iter() 1592 .filter_map(|x| { 1593 if Assertion::assertions_eq(assertion_proto, x.assertion()) { 1594 Some(&x.assertion) 1595 } else { 1596 None 1597 } 1598 }) 1599 .collect() 1600 } 1601 1602 /// Return reference to the assertions list. 1603 /// 1604 /// This list matches item-for-item with the `Assertion`s 1605 /// stored in the assertion store. 1606 pub const fn assertions(&self) -> &Vec<C2PAAssertion> { 1607 &self.assertions 1608 } 1609 1610 /// Returns the cbor binary value of the claim data. 1611 /// If this claim was read from a file, returns the exact byte 1612 /// sequence that was read from the file. If this claim was 1613 /// constructed locally, contains the claim data that was/will be 1614 /// generated locally. 1615 pub fn data(&self) -> Result<Vec<u8>> { 1616 match self.original_bytes { 1617 Some(ref ob) => Ok(ob.clone()), 1618 None => Ok(serde_cbor::ser::to_vec(&self).map_err(|_err| Error::ClaimEncoding)?), 1619 } 1620 } 1621 1622 /// Create claim from binary data (not including assertions). 1623 pub fn from_data(label: &str, data: &[u8]) -> Result<Claim> { 1624 let mut claim: Claim = serde_cbor::from_slice(data).map_err(|_err| Error::ClaimDecoding)?; 1625 1626 claim.label = label.to_string(); 1627 claim.original_bytes = Some(data.to_owned()); 1628 1629 Ok(claim) 1630 } 1631 1632 /// Generate a JSON representation of the Claim 1633 /// returns Result as a String 1634 pub fn to_json( 1635 &self, 1636 assertion_store_format: AssertionStoreJsonFormat, 1637 pretty: bool, 1638 ) -> Result<String> { 1639 let mut v = serde_json::to_value(self)?; 1640 1641 match assertion_store_format { 1642 AssertionStoreJsonFormat::None => {} 1643 AssertionStoreJsonFormat::KeyValue | AssertionStoreJsonFormat::KeyValueNoBinary => { 1644 // add additional data if needed to the assertion store 1645 if let Value::Object(ref mut map) = v { 1646 // merge the label with the data 1647 let mut json_map: Map<String, Value> = Map::new(); 1648 let iter = self.assertions.iter().zip(&self.assertion_store); 1649 1650 for (_key, claim_assertion) in iter { 1651 let link = claim_assertion.label(); 1652 let (label, instance) = Self::assertion_label_from_link(&link); 1653 let label = Self::label_with_instance(&label, instance); 1654 1655 match claim_assertion.assertion.decode_data() { 1656 AssertionData::Json(x) => { 1657 // json strings 1658 let decoded = serde_json::from_str(x)?; 1659 json_map.insert(label, decoded); 1660 } 1661 AssertionData::Cbor(x) => { 1662 // some types are not translatable to json so explicitly convert 1663 let buf: Vec<u8> = Vec::new(); 1664 let mut from = serde_cbor::Deserializer::from_slice(x); 1665 let mut to = serde_json::Serializer::new(buf); 1666 1667 serde_transcode::transcode(&mut from, &mut to) 1668 .map_err(|_err| Error::AssertionEncoding)?; 1669 let buf2 = to.into_inner(); 1670 1671 let decoded: Value = serde_json::from_slice(&buf2) 1672 .map_err(|_err| Error::AssertionEncoding)?; 1673 1674 json_map.insert(label, decoded); 1675 } 1676 AssertionData::Binary(x) => { 1677 // binary vecs 1678 let d = match assertion_store_format { 1679 AssertionStoreJsonFormat::KeyValue => { 1680 Value::String(base64::encode(x)) 1681 } 1682 AssertionStoreJsonFormat::KeyValueNoBinary => { 1683 Value::String("omitted".to_owned()) 1684 } 1685 _ => Value::String("".to_owned()), 1686 }; 1687 json_map.insert(label, d); 1688 continue; 1689 } 1690 AssertionData::Uuid(s, x) => { 1691 // binary vecs 1692 let d = match assertion_store_format { 1693 AssertionStoreJsonFormat::KeyValue => { 1694 Value::String(base64::encode(x)) 1695 } 1696 AssertionStoreJsonFormat::KeyValueNoBinary => { 1697 Value::String("omitted".to_owned()) 1698 } 1699 _ => Value::String("".to_owned()), 1700 }; 1701 1702 let m = json!({ 1703 "uuid": s, 1704 "data": d, 1705 }); 1706 1707 json_map.insert(label, m); 1708 continue; 1709 } 1710 } 1711 } 1712 //let s = serde_json::to_string(&json_map)?; 1713 //let as_val = serde_json::from_str(&s)?; 1714 let as_val = serde_json::to_value(json_map)?; 1715 map.insert("assertion_store".to_string(), as_val); 1716 1717 // add vcstore 1718 map.insert( 1719 "vc_store".to_string(), 1720 serde_json::to_value(&self.vc_store)?, 1721 ); 1722 1723 // add claim label 1724 map.insert("label".to_string(), Value::String(self.label.to_string())); 1725 } 1726 } 1727 AssertionStoreJsonFormat::OrderedList 1728 | AssertionStoreJsonFormat::OrderedListNoBinary => { 1729 // add additional data if needed to the assertion store 1730 if let Value::Object(ref mut map) = v { 1731 let mut json_vec: Vec<Value> = Vec::new(); 1732 1733 // assertion values 1734 for claim_assertion in self.claim_assertion_store() { 1735 match claim_assertion.assertion.decode_data() { 1736 AssertionData::Json(x) => { 1737 let d: Value = serde_json::from_str(x) 1738 .map_err(|_err| Error::AssertionEncoding)?; 1739 1740 let j = JsonOrderedAssertionData { 1741 label: claim_assertion.label().to_owned(), 1742 hash: base64::encode(claim_assertion.hash()), 1743 data: d, 1744 is_binary: false, 1745 mime_type: claim_assertion.assertion.mime_type(), 1746 }; 1747 1748 let new_val = serde_json::to_value(j)?; 1749 json_vec.push(new_val); 1750 } 1751 AssertionData::Cbor(x) => { 1752 // some types are not translatable to json so explicitly convert 1753 let buf: Vec<u8> = Vec::new(); 1754 let mut from = serde_cbor::Deserializer::from_slice(x); 1755 let mut to = serde_json::Serializer::new(buf); 1756 1757 serde_transcode::transcode(&mut from, &mut to) 1758 .map_err(|_err| Error::AssertionEncoding)?; 1759 let buf2 = to.into_inner(); 1760 1761 let d: Value = serde_json::from_slice(&buf2) 1762 .map_err(|_err| Error::AssertionEncoding)?; 1763 1764 let j = JsonOrderedAssertionData { 1765 label: claim_assertion.label().to_owned(), 1766 hash: base64::encode(claim_assertion.hash()), 1767 data: d, 1768 is_binary: false, 1769 mime_type: claim_assertion.assertion.mime_type(), 1770 }; 1771 1772 let new_val = serde_json::to_value(j)?; 1773 json_vec.push(new_val); 1774 } 1775 AssertionData::Binary(x) => { 1776 // binary data 1777 let d = match assertion_store_format { 1778 AssertionStoreJsonFormat::OrderedList => { 1779 Value::String(base64::encode(x)) 1780 } 1781 AssertionStoreJsonFormat::OrderedListNoBinary => { 1782 Value::String("omitted".to_owned()) 1783 } 1784 _ => Value::String("".to_owned()), 1785 }; 1786 1787 let j = JsonOrderedAssertionData { 1788 label: claim_assertion.label().to_owned(), 1789 hash: base64::encode(claim_assertion.hash()), 1790 data: d, 1791 is_binary: true, 1792 mime_type: claim_assertion.assertion.mime_type(), 1793 }; 1794 1795 let new_val = serde_json::to_value(j)?; 1796 json_vec.push(new_val); 1797 } 1798 AssertionData::Uuid(s, x) => { 1799 // binary data 1800 let d = match assertion_store_format { 1801 AssertionStoreJsonFormat::OrderedList => { 1802 Value::String(base64::encode(x)) 1803 } 1804 AssertionStoreJsonFormat::OrderedListNoBinary => { 1805 Value::String("omitted".to_owned()) 1806 } 1807 _ => Value::String("".to_owned()), 1808 }; 1809 1810 let m = json!({ 1811 "uuid": s, 1812 "data": d, 1813 }); 1814 1815 let j = JsonOrderedAssertionData { 1816 label: claim_assertion.label().to_owned(), 1817 hash: base64::encode(claim_assertion.hash()), 1818 data: m, 1819 is_binary: true, 1820 mime_type: claim_assertion.assertion.mime_type(), 1821 }; 1822 1823 let new_val = serde_json::to_value(j)?; 1824 json_vec.push(new_val); 1825 } 1826 } 1827 } 1828 1829 let as_val = serde_json::to_value(json_vec)?; 1830 map.insert("assertion_store".to_string(), as_val); 1831 1832 // add claim label 1833 map.insert("label".to_string(), Value::String(self.label.to_string())); 1834 } 1835 } 1836 } 1837 1838 if pretty { 1839 serde_json::to_string_pretty(&v).map_err(|e| e.into()) 1840 } else { 1841 serde_json::to_string(&v).map_err(|e| e.into()) 1842 } 1843 } 1844 1845 /// Return the label for this assertion given its link 1846 pub fn assertion_label_from_link(assertion_link: &str) -> (String, usize) { 1847 let v = jumbf::labels::to_normalized_uri(assertion_link); 1848 1849 let v2: Vec<&str> = v.split('/').collect(); 1850 if let Some(s) = v2.last() { 1851 // treat ingredient thumbnails differently ingredient.thumbnail 1852 if get_thumbnail_type(s) == labels::INGREDIENT_THUMBNAIL { 1853 let instance = get_thumbnail_instance(s).unwrap_or(0); 1854 let label = match get_thumbnail_image_type(s).as_str() { 1855 "none" => get_thumbnail_type(s), 1856 image_type => format!("{}.{}", get_thumbnail_type(s), image_type), 1857 }; 1858 (label, instance) 1859 } else { 1860 let label_parts: Vec<&str> = s.split("__").collect(); 1861 let mut instance: usize = 0; 1862 1863 if label_parts.len() == 2 { 1864 match label_parts[1].parse::<usize>() { 1865 Ok(i) => instance = i, 1866 _ => instance = 0, 1867 } 1868 } 1869 1870 (label_parts[0].to_owned(), instance) 1871 } 1872 } else { 1873 (v2[0].to_owned(), 0) 1874 } 1875 } 1876 1877 /// generates label with instance if needed 1878 pub fn label_with_instance(label: &str, instance: usize) -> String { 1879 if instance == 0 { 1880 label.to_string() 1881 } else if get_thumbnail_type(label) == labels::INGREDIENT_THUMBNAIL { 1882 let tn_type = get_thumbnail_image_type(label); 1883 format!("{}__{}.{}", get_thumbnail_type(label), instance, tn_type) 1884 } else { 1885 format!("{label}__{instance}") 1886 } 1887 } 1888 1889 pub fn assertion_hashed_uri_from_label(&self, assertion_label: &str) -> Option<&C2PAAssertion> { 1890 self.assertions() 1891 .iter() 1892 .find(|hashed_uri| hashed_uri.url().contains(assertion_label)) 1893 } 1894 1895 // Given a proposed label, make a new label that is unique within this 1896 // assertion store. Typically this is done by adding `__{n}` where `n` is 1897 // an integer starting from 1. Ingredient thumbnails have special handling. 1898 fn make_assertion_instance_label(&self, assertion_label: &str) -> String { 1899 let cnt = self.next_instance(assertion_label); 1900 1901 Claim::label_with_instance(assertion_label, cnt) 1902 } 1903 1904 /// returns first instance of an assertion whose label and instance match 1905 pub fn get_assertion(&self, assertion_label: &str, instance: usize) -> Option<&Assertion> { 1906 let mut iter = self.claim_assertion_store().iter().filter_map(|ca| { 1907 if ca.label_raw() == assertion_label && ca.instance() == instance { 1908 Some(ca.assertion()) 1909 } else { 1910 None 1911 } 1912 }); 1913 1914 iter.next() 1915 } 1916 1917 /// returns instance of an assertion whose label and instance match 1918 pub fn get_claim_assertion( 1919 &self, 1920 assertion_label: &str, 1921 instance: usize, 1922 ) -> Option<&ClaimAssertion> { 1923 self.claim_assertion_store() 1924 .iter() 1925 .find(|ca| ca.label_raw() == assertion_label && ca.instance() == instance) 1926 } 1927 1928 /// returns hash of an assertion whose label and instance match 1929 pub fn get_claim_assertion_hash(&self, assertion_label: &str) -> Option<Vec<u8>> { 1930 let (l, i) = Claim::assertion_label_from_link(assertion_label); 1931 self.get_claim_assertion(&l, i).map(|a| a.hash().to_vec()) 1932 } 1933 1934 /// Returns how many assertions of this assertion type exist? 1935 pub fn count_instances(&self, in_label: &str) -> usize { 1936 let (l, i) = Claim::assertion_label_from_link(in_label); 1937 let label = Claim::label_with_instance(&l, i); 1938 self.assertions 1939 .iter() 1940 .filter(|assertion| assertion.url().contains(&label)) 1941 .count() 1942 } 1943 1944 // Get the next highest instance label 1945 fn next_instance(&self, in_label: &str) -> usize { 1946 let (label, _) = Claim::assertion_label_from_link(in_label); 1947 match self 1948 .assertion_store 1949 .iter() 1950 .filter(|&x| x.assertion.label().contains(&label)) 1951 .map(|x| { 1952 let (_l, i) = Claim::assertion_label_from_link(&x.label()); 1953 i 1954 }) 1955 .max() 1956 { 1957 Some(last_instance) => last_instance + 1, 1958 None => 0, 1959 } 1960 } 1961 1962 // Do any assertions of this type exist? 1963 pub fn has_assertion_type(&self, in_label: &str) -> bool { 1964 let (label, _) = Claim::assertion_label_from_link(in_label); 1965 1966 self.assertion_store 1967 .iter() 1968 .any(|x| x.assertion.label().starts_with(&label)) 1969 } 1970 1971 // Create a JUMBF URI from a claim label. 1972 pub(crate) fn to_claim_uri(manifest_label: &str) -> String { 1973 format!( 1974 "{}/{}", 1975 jumbf::labels::to_manifest_uri(manifest_label), 1976 Self::LABEL 1977 ) 1978 } 1979 } 1980 1981 #[cfg(not(target_arch = "wasm32"))] 1982 #[cfg(test)] 1983 pub mod tests { 1984 #![allow(clippy::expect_used)] 1985 #![allow(clippy::unwrap_used)] 1986 1987 use super::*; 1988 use crate::{resource_store::UriOrResource, utils::test::create_test_claim}; 1989 1990 #[test] 1991 fn test_build_claim() { 1992 // Create a new claim. 1993 let mut claim = create_test_claim().expect("create test claim"); 1994 1995 // Add a redaction. 1996 // claim.redact_assertion("as_tp_1/c2pa.location.precise"); 1997 1998 // Build claim checking rules. 1999 claim.build().expect("bad claim"); 2000 2001 // Test round-tripping of binary. 2002 let orig_binary = claim.data().expect("failure returning data"); 2003 let restored_claim = 2004 Claim::from_data("as_adbe_1", &orig_binary).expect("could not restore from binary"); 2005 let restored_binary = restored_claim.data().expect("failure returning data"); 2006 2007 assert_eq!(orig_binary, restored_binary); 2008 println!("Restored Claim: {restored_claim:?}"); 2009 2010 // NOTE: I added a separate mirror of original data because a third-party's 2011 // JSON serialization could differ from our re-serialization of that same data. 2012 // When reading claims from assets and verifying signatures of those claims, 2013 // we need the exact original bytes of the signed JSON or the signature verification 2014 // will fail. 2015 assert_eq!(orig_binary, restored_claim.original_bytes.unwrap()); 2016 2017 // JSON examples 2018 let json_str = claim 2019 .to_json(AssertionStoreJsonFormat::OrderedList, true) 2020 .expect("could not generate json"); 2021 2022 println!("Claim: {json_str}"); 2023 } 2024 2025 #[test] 2026 fn test_build_claim_generator_hints() { 2027 // Create a new claim. 2028 let mut claim = create_test_claim().expect("create test claim"); 2029 2030 claim.add_claim_generator_hint( 2031 GH_FULL_VERSION_LIST, 2032 Value::String(r#""user app";v="2.3.4""#.to_string()), 2033 ); 2034 claim.add_claim_generator_hint( 2035 GH_FULL_VERSION_LIST, 2036 Value::String(r#""some toolkit";v="1.0.0""#.to_string()), 2037 ); 2038 2039 let expected_value = r#""user app";v="2.3.4", "some toolkit";v="1.0.0""#; 2040 2041 let cg_map = claim.get_claim_generator_hint_map().unwrap(); 2042 let value = &cg_map[GH_FULL_VERSION_LIST]; 2043 2044 assert_eq!(expected_value, value.as_str().unwrap()); 2045 } 2046 2047 #[test] 2048 fn test_build_claim_generator_info() { 2049 // Create a new claim. 2050 let mut claim = create_test_claim().expect("create test claim"); 2051 2052 let mut info = ClaimGeneratorInfo::new("test app"); 2053 info.version = Some("2.3.4".to_string()); 2054 info.icon = Some(UriOrResource::HashedUri(HashedUri::new( 2055 "self#jumbf=c2pa.databoxes.data_box".to_string(), 2056 None, 2057 b"hashed", 2058 ))); 2059 info.insert("something", "else"); 2060 2061 claim.add_claim_generator_info(info); 2062 2063 let cgi = claim.claim_generator_info().unwrap(); 2064 2065 assert_eq!(&cgi[0].name, "test app"); 2066 assert_eq!(cgi[0].version.as_deref(), Some("2.3.4")); 2067 if let UriOrResource::HashedUri(r) = cgi[0].icon.as_ref().unwrap() { 2068 assert_eq!(r.hash(), b"hashed"); 2069 } 2070 } 2071 }