manifest.rs (100665B)
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::{borrow::Cow, collections::HashMap, io::Cursor}; 15 #[cfg(feature = "file_io")] 16 use std::{fs::create_dir_all, path::Path}; 17 18 use async_generic::async_generic; 19 #[cfg(feature = "json_schema")] 20 use schemars::JsonSchema; 21 use serde::{de::DeserializeOwned, Deserialize, Serialize}; 22 use serde_json::Value; 23 use tracing::{debug, error}; 24 use uuid::Uuid; 25 26 use crate::{ 27 assertion::{AssertionBase, AssertionData}, 28 assertions::{ 29 labels, Actions, CreativeWork, DataHash, Exif, SoftwareAgent, Thumbnail, User, UserCbor, 30 }, 31 asset_io::{CAIRead, CAIReadWrite}, 32 claim::{Claim, RemoteManifest}, 33 error::{Error, Result}, 34 ingredient::Ingredient, 35 jumbf, 36 manifest_assertion::ManifestAssertion, 37 resource_store::{mime_from_uri, skip_serializing_resources, ResourceRef, ResourceStore}, 38 salt::DefaultSalt, 39 store::Store, 40 AsyncSigner, ClaimGeneratorInfo, HashRange, ManifestAssertionKind, RemoteSigner, Signer, 41 SigningAlg, 42 }; 43 44 /// A Manifest represents all the information in a c2pa manifest 45 #[derive(Debug, Default, Deserialize, Serialize)] 46 #[cfg_attr(feature = "json_schema", derive(JsonSchema))] 47 pub struct Manifest { 48 /// Optional prefix added to the generated Manifest Label 49 /// This is typically Internet domain name for the vendor (i.e. `adobe`) 50 #[serde(skip_serializing_if = "Option::is_none")] 51 vendor: Option<String>, 52 53 /// A User Agent formatted string identifying the software/hardware/system produced this claim 54 /// Spaces are not allowed in names, versions can be specified with product/1.0 syntax 55 #[serde(default = "default_claim_generator")] 56 pub claim_generator: String, 57 58 /// A list of claim generator info data identifying the software/hardware/system produced this claim 59 #[serde(skip_serializing_if = "Option::is_none")] 60 pub claim_generator_info: Option<Vec<ClaimGeneratorInfo>>, 61 62 /// A human-readable title, generally source filename. 63 #[serde(skip_serializing_if = "Option::is_none")] 64 title: Option<String>, 65 66 /// The format of the source file as a MIME type. 67 #[serde(default = "default_format")] 68 format: String, 69 70 /// Instance ID from `xmpMM:InstanceID` in XMP metadata. 71 #[serde(default = "default_instance_id")] 72 instance_id: String, 73 74 #[serde(skip_serializing_if = "Option::is_none")] 75 claim_generator_hints: Option<HashMap<String, Value>>, 76 77 #[serde(skip_serializing_if = "Option::is_none")] 78 thumbnail: Option<ResourceRef>, 79 80 /// A List of ingredients 81 #[serde(default = "default_vec::<Ingredient>")] 82 ingredients: Vec<Ingredient>, 83 84 /// A List of verified credentials 85 #[serde(skip_serializing_if = "Option::is_none")] 86 credentials: Option<Vec<Value>>, 87 88 /// A list of assertions 89 #[serde(default = "default_vec::<ManifestAssertion>")] 90 assertions: Vec<ManifestAssertion>, 91 92 /// A list of redactions - URIs to a redacted assertions 93 #[serde(skip_serializing_if = "Option::is_none")] 94 redactions: Option<Vec<String>>, 95 96 /// Signature data (only used for reporting) 97 #[serde(skip_serializing_if = "Option::is_none")] 98 signature_info: Option<SignatureInfo>, 99 100 #[serde(skip_serializing_if = "Option::is_none")] 101 label: Option<String>, 102 103 /// Indicates where a generated manifest goes 104 #[serde(skip)] 105 remote_manifest: Option<RemoteManifest>, 106 107 /// container for binary assets (like thumbnails) 108 #[serde(skip_deserializing)] 109 #[serde(skip_serializing_if = "skip_serializing_resources")] 110 resources: ResourceStore, 111 } 112 113 fn default_claim_generator() -> String { 114 format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")) 115 } 116 117 fn default_instance_id() -> String { 118 format!("xmp:iid:{}", Uuid::new_v4()) 119 } 120 121 fn default_format() -> String { 122 "application/octet-stream".to_owned() 123 } 124 125 const fn default_vec<T>() -> Vec<T> { 126 Vec::new() 127 } 128 129 impl Manifest { 130 /// Create a new Manifest 131 /// requires a claim_generator string (User Agent)) 132 pub fn new<S: Into<String>>(claim_generator: S) -> Self { 133 Self { 134 claim_generator: claim_generator.into(), 135 format: default_format(), 136 instance_id: default_instance_id(), 137 ..Default::default() 138 } 139 } 140 141 /// Returns a User Agent formatted string identifying the software/hardware/system produced this claim 142 pub fn claim_generator(&self) -> &str { 143 self.claim_generator.as_str() 144 } 145 146 /// returns the manifest label for this Manifest, as referenced in a ManifestStore 147 pub fn label(&self) -> Option<&str> { 148 self.label.as_deref() 149 } 150 151 /// Returns a MIME content_type for the asset associated with this manifest. 152 pub fn format(&self) -> &str { 153 &self.format 154 } 155 156 /// Returns the instance identifier. 157 pub fn instance_id(&self) -> &str { 158 &self.instance_id 159 } 160 161 /// Returns a user-displayable title for this manifest 162 pub fn title(&self) -> Option<&str> { 163 self.title.as_deref() 164 } 165 166 /// Returns thumbnail tuple with Some((format, bytes)) or None 167 pub fn thumbnail(&self) -> Option<(&str, Cow<Vec<u8>>)> { 168 self.thumbnail 169 .as_ref() 170 .and_then(|t| Some(t.format.as_str()).zip(self.resources.get(&t.identifier).ok())) 171 } 172 173 /// Returns a thumbnail ResourceRef or `None`. 174 pub const fn thumbnail_ref(&self) -> Option<&ResourceRef> { 175 self.thumbnail.as_ref() 176 } 177 178 /// Returns immutable [Ingredient]s used by this Manifest 179 /// This can include a parent as well as any placed assets 180 pub fn ingredients(&self) -> &[Ingredient] { 181 &self.ingredients 182 } 183 184 /// Returns mutable [Ingredient]s used by this Manifest 185 /// This can include a parent as well as any placed assets 186 pub fn ingredients_mut(&mut self) -> &mut [Ingredient] { 187 &mut self.ingredients 188 } 189 190 /// Returns Assertions for this Manifest 191 pub fn assertions(&self) -> &[ManifestAssertion] { 192 &self.assertions 193 } 194 195 /// Returns Verifiable Credentials 196 pub fn credentials(&self) -> Option<&[Value]> { 197 self.credentials.as_deref() 198 } 199 200 /// Returns the remote_manifest Url if there is one 201 /// This is only used when creating a manifest, it will always be None when reading 202 pub fn remote_manifest_url(&self) -> Option<&str> { 203 match self.remote_manifest.as_ref() { 204 Some(RemoteManifest::Remote(url)) => Some(url.as_str()), 205 Some(RemoteManifest::EmbedWithRemote(url)) => Some(url.as_str()), 206 _ => None, 207 } 208 } 209 210 /// Sets the vendor prefix to be used when generating manifest labels 211 /// Optional prefix added to the generated Manifest Label 212 /// This is typically a lower case Internet domain name for the vendor (i.e. `adobe`) 213 pub fn set_vendor<S: Into<String>>(&mut self, vendor: S) -> &mut Self { 214 self.vendor = Some(vendor.into()); 215 self 216 } 217 218 /// Sets the label for this manifest 219 /// A label will be generated if this is not called 220 /// This is needed if embedding a URL that references the manifest label 221 pub fn set_label<S: Into<String>>(&mut self, label: S) -> &mut Self { 222 self.label = Some(label.into()); 223 self 224 } 225 226 /// Sets a human readable name for the product that created this manifest 227 pub fn set_claim_generator<S: Into<String>>(&mut self, generator: S) -> &mut Self { 228 self.claim_generator = generator.into(); 229 self 230 } 231 232 /// Sets a human-readable title for this ingredient. 233 pub fn set_format<S: Into<String>>(&mut self, format: S) -> &mut Self { 234 self.format = format.into(); 235 self 236 } 237 238 /// Sets a human-readable title for this ingredient. 239 pub fn set_instance_id<S: Into<String>>(&mut self, instance_id: S) -> &mut Self { 240 self.instance_id = instance_id.into(); 241 self 242 } 243 244 /// Sets a human-readable title for this ingredient. 245 pub fn set_title<S: Into<String>>(&mut self, title: S) -> &mut Self { 246 self.title = Some(title.into()); 247 self 248 } 249 250 /// Sets the thumbnail from a ResourceRef. 251 pub fn set_thumbnail_ref(&mut self, thumbnail: ResourceRef) -> Result<&mut Self> { 252 // verify the resource referenced exists 253 if thumbnail.format != "none" && !self.resources.exists(&thumbnail.identifier) { 254 return Err(Error::NotFound); 255 }; 256 self.thumbnail = Some(thumbnail); 257 Ok(self) 258 } 259 260 /// Sets the thumbnail format and image data. 261 pub fn set_thumbnail<S: Into<String>, B: Into<Vec<u8>>>( 262 &mut self, 263 format: S, 264 thumbnail: B, 265 ) -> Result<&mut Self> { 266 let base_id = self 267 .label() 268 .unwrap_or_else(|| self.instance_id()) 269 .to_string(); 270 self.thumbnail = Some( 271 self.resources 272 .add_with(&base_id, &format.into(), thumbnail)?, 273 ); 274 Ok(self) 275 } 276 277 /// If set, the embed calls will create a sidecar .c2pa manifest file next to the output file 278 /// No change will be made to the output file 279 pub fn set_sidecar_manifest(&mut self) -> &mut Self { 280 self.remote_manifest = Some(RemoteManifest::SideCar); 281 self 282 } 283 284 /// If set, the embed calls will put the remote url into the output file xmp provenance 285 /// and create a c2pa manifest file next to the output file 286 pub fn set_remote_manifest<S: Into<String>>(&mut self, remote_url: S) -> &mut Self { 287 self.remote_manifest = Some(RemoteManifest::Remote(remote_url.into())); 288 self 289 } 290 291 /// If set, the embed calls will put the remote url into the output file xmp provenance 292 /// and will embed the manifest into the output file 293 pub fn set_embedded_manifest_with_remote_ref<S: Into<String>>( 294 &mut self, 295 remote_url: S, 296 ) -> &mut Self { 297 self.remote_manifest = Some(RemoteManifest::EmbedWithRemote(remote_url.into())); 298 self 299 } 300 301 pub const fn signature_info(&self) -> Option<&SignatureInfo> { 302 self.signature_info.as_ref() 303 } 304 305 /// Returns the parent ingredient if it exists 306 pub fn parent(&self) -> Option<&Ingredient> { 307 self.ingredients.iter().find(|i| i.is_parent()) 308 } 309 310 /// Sets the parent ingredient, assuring it is first and setting the is_parent flag 311 pub fn set_parent(&mut self, mut ingredient: Ingredient) -> Result<&mut Self> { 312 // there should only be one parent so return an error if we already have one 313 if self.parent().is_some() { 314 error!("parent already added"); 315 return Err(Error::BadParam("Parent parent already added".to_owned())); 316 } 317 ingredient.set_is_parent(); 318 self.ingredients.insert(0, ingredient); 319 320 Ok(self) 321 } 322 323 /// Add an ingredient removing duplicates (consumes the asset) 324 pub fn add_ingredient(&mut self, ingredient: Ingredient) -> &mut Self { 325 self.ingredients.push(ingredient); 326 self 327 } 328 329 /// Adds assertion using given label and any serde serializable 330 /// The data for predefined assertions must be in correct format 331 /// 332 /// # Example: Creating a custom assertion from a serde_json object. 333 ///``` 334 /// # use c2pa::Result; 335 /// use c2pa::Manifest; 336 /// use serde_json::json; 337 /// # fn main() -> Result<()> { 338 /// let mut manifest = Manifest::new("my_app"); 339 /// let value = json!({"my_tag": "Anything I want"}); 340 /// manifest.add_labeled_assertion("org.contentauth.foo", &value)?; 341 /// # Ok(()) 342 /// # } 343 /// ``` 344 pub fn add_labeled_assertion<S: Into<String>, T: Serialize>( 345 &mut self, 346 label: S, 347 data: &T, 348 ) -> Result<&mut Self> { 349 self.assertions 350 .push(ManifestAssertion::from_labeled_assertion(label, data)?); 351 Ok(self) 352 } 353 354 /// Adds ManifestAssertions from existing assertions 355 /// The data for standard assertions must be in correct format 356 /// 357 /// # Example: Creating a from an Actions object. 358 ///``` 359 /// # use c2pa::Result; 360 /// use c2pa::{ 361 /// assertions::{c2pa_action, Action, Actions}, 362 /// Manifest, 363 /// }; 364 /// # fn main() -> Result<()> { 365 /// let mut manifest = Manifest::new("my_app"); 366 /// let actions = Actions::new().add_action(Action::new(c2pa_action::EDITED)); 367 /// manifest.add_assertion(&actions)?; 368 /// # Ok(()) 369 /// # } 370 /// ``` 371 pub fn add_assertion<T: Serialize + AssertionBase>(&mut self, data: &T) -> Result<&mut Self> { 372 self.assertions 373 .push(ManifestAssertion::from_assertion(data)?); 374 Ok(self) 375 } 376 377 /// Retrieves an assertion by label if it exists or Error::NotFound 378 /// 379 /// Example: Find an Actions Assertion 380 /// ``` 381 /// # use c2pa::Result; 382 /// use c2pa::{ 383 /// assertions::{c2pa_action, Action, Actions}, 384 /// Manifest, 385 /// }; 386 /// # fn main() -> Result<()> { 387 /// let mut manifest = Manifest::new("my_app"); 388 /// let actions = Actions::new().add_action(Action::new(c2pa_action::EDITED)); 389 /// manifest.add_assertion(&actions)?; 390 /// 391 /// let actions: Actions = manifest.find_assertion(Actions::LABEL)?; 392 /// for action in actions.actions { 393 /// println!("{}", action.action()); 394 /// } 395 /// # Ok(()) 396 /// # } 397 /// ``` 398 pub fn find_assertion<T: DeserializeOwned>(&self, label: &str) -> Result<T> { 399 if let Some(manifest_assertion) = self.assertions.iter().find(|a| a.label() == label) { 400 manifest_assertion.to_assertion() 401 } else { 402 Err(Error::NotFound) 403 } 404 } 405 406 /// Retrieves an assertion by label and instance if it exists or Error::NotFound 407 pub fn find_assertion_with_instance<T: DeserializeOwned>( 408 &self, 409 label: &str, 410 instance: usize, 411 ) -> Result<T> { 412 if let Some(manifest_assertion) = self 413 .assertions 414 .iter() 415 .find(|a| a.label() == label && a.instance() == instance) 416 { 417 manifest_assertion.to_assertion() 418 } else { 419 Err(Error::NotFound) 420 } 421 } 422 423 /// Redacts an assertion from the parent [Ingredient] of this manifest using the provided 424 /// assertion label. 425 pub fn add_redaction<S: Into<String>>(&mut self, label: S) -> Result<&mut Self> { 426 // todo: any way to verify if this assertion exists in the parent claim here? 427 match self.redactions.as_mut() { 428 Some(redactions) => redactions.push(label.into()), 429 None => self.redactions = Some([label.into()].to_vec()), 430 } 431 Ok(self) 432 } 433 434 /// Add verifiable credentials 435 pub fn add_verifiable_credential<T: Serialize>(&mut self, data: &T) -> Result<&mut Self> { 436 let value = serde_json::to_value(data).map_err(|_err| Error::AssertionEncoding)?; 437 match self.credentials.as_mut() { 438 Some(credentials) => credentials.push(value), 439 None => self.credentials = Some([value].to_vec()), 440 } 441 Ok(self) 442 } 443 444 /// Returns the name of the signature issuer 445 pub fn issuer(&self) -> Option<String> { 446 self.signature_info.to_owned().and_then(|sig| sig.issuer) 447 } 448 449 /// Returns the time that the manifest was signed 450 pub fn time(&self) -> Option<String> { 451 self.signature_info.to_owned().and_then(|sig| sig.time) 452 } 453 454 /// Returns an iterator over [`ResourceRef`][ResourceRef]s. 455 pub fn iter_resources(&self) -> impl Iterator<Item = ResourceRef> + '_ { 456 self.resources 457 .resources() 458 .keys() 459 .map(|uri| ResourceRef::new(mime_from_uri(uri), uri.to_owned())) 460 } 461 462 /// Return an immutable reference to the manifest resources 463 pub const fn resources(&self) -> &ResourceStore { 464 &self.resources 465 } 466 467 /// Return a mutable reference to the manifest resources 468 pub fn resources_mut(&mut self) -> &mut ResourceStore { 469 &mut self.resources 470 } 471 472 /// Creates a Manifest from a JSON string formatted as a Manifest 473 pub fn from_json(json: &str) -> Result<Self> { 474 serde_json::from_slice(json.as_bytes()).map_err(Error::JsonError) 475 } 476 477 /// Setting a base path will make the manifest use resource files instead of memory buffers 478 /// 479 /// The files will be relative to the given base path 480 /// Ingredients resources will also be relative to this path 481 #[cfg(feature = "file_io")] 482 pub fn with_base_path<P: AsRef<Path>>(&mut self, base_path: P) -> Result<&Self> { 483 create_dir_all(&base_path)?; 484 self.resources.set_base_path(base_path.as_ref()); 485 for i in 0..self.ingredients.len() { 486 // todo: create different subpath for each ingredient? 487 self.ingredients[i].with_base_path(base_path.as_ref())?; 488 } 489 Ok(self) 490 } 491 492 // Generates a Manifest given a store and a manifest label 493 pub(crate) fn from_store( 494 store: &Store, 495 manifest_label: &str, 496 #[cfg(feature = "file_io")] resource_path: Option<&Path>, 497 ) -> Result<Self> { 498 let claim = store 499 .get_claim(manifest_label) 500 .ok_or_else(|| Error::ClaimMissing { 501 label: manifest_label.to_owned(), 502 })?; 503 504 // extract vendor from claim label 505 let claim_generator = claim.claim_generator().to_owned(); 506 507 let mut manifest = Manifest::new(claim_generator); 508 509 #[cfg(feature = "file_io")] 510 if let Some(base_path) = resource_path { 511 manifest.with_base_path(base_path)?; 512 } 513 514 if let Some(info_vec) = claim.claim_generator_info() { 515 let mut generators = Vec::new(); 516 for claim_info in info_vec { 517 let mut info = claim_info.to_owned(); 518 if let Some(icon) = claim_info.icon.as_ref() { 519 info.set_icon(icon.to_resource_ref(manifest.resources_mut(), claim)?); 520 } 521 generators.push(info); 522 } 523 manifest.claim_generator_info = Some(generators); 524 } 525 526 manifest.set_label(claim.label()); 527 manifest.resources.set_label(claim.label()); // default manifest for relative urls 528 manifest.claim_generator_hints = claim.get_claim_generator_hint_map().cloned(); 529 530 // get credentials converting from AssertionData to Value 531 let credentials: Vec<Value> = claim 532 .get_verifiable_credentials() 533 .iter() 534 .filter_map(|d| match d { 535 AssertionData::Json(s) => serde_json::from_str(s).ok(), 536 _ => None, 537 }) 538 .collect(); 539 540 if !credentials.is_empty() { 541 manifest.credentials = Some(credentials); 542 } 543 544 manifest.redactions = claim.redactions().map(|rs| { 545 rs.iter() 546 .filter_map(|r| jumbf::labels::assertion_label_from_uri(r)) 547 .collect() 548 }); 549 550 if let Some(title) = claim.title() { 551 manifest.set_title(title); 552 } 553 manifest.set_format(claim.format()); 554 manifest.set_instance_id(claim.instance_id()); 555 556 for assertion in claim.assertions() { 557 let claim_assertion = store.get_claim_assertion_from_uri( 558 &jumbf::labels::to_absolute_uri(claim.label(), &assertion.url()), 559 )?; 560 let assertion = claim_assertion.assertion(); 561 let label = claim_assertion.label(); 562 let base_label = assertion.label(); 563 debug!("assertion = {}", &label); 564 match base_label.as_ref() { 565 base if base.starts_with(labels::ACTIONS) => { 566 let mut actions = Actions::from_assertion(assertion)?; 567 568 for action in actions.actions_mut() { 569 if let Some(SoftwareAgent::ClaimGeneratorInfo(info)) = 570 action.software_agent_mut() 571 { 572 if let Some(icon) = info.icon.as_mut() { 573 let icon = icon.to_resource_ref(manifest.resources_mut(), claim)?; 574 info.set_icon(icon); 575 } 576 } 577 } 578 579 // convert icons in templates to resource refs 580 if let Some(templates) = actions.templates.as_mut() { 581 for template in templates { 582 // replace icon with resource ref 583 template.icon = match template.icon.take() { 584 Some(icon) => { 585 Some(icon.to_resource_ref(manifest.resources_mut(), claim)?) 586 } 587 None => None, 588 }; 589 590 // replace software agent with resource ref 591 template.software_agent = match template.software_agent.take() { 592 Some(SoftwareAgent::ClaimGeneratorInfo(mut info)) => { 593 if let Some(icon) = info.icon.as_mut() { 594 let icon = 595 icon.to_resource_ref(manifest.resources_mut(), claim)?; 596 info.set_icon(icon); 597 } 598 Some(SoftwareAgent::ClaimGeneratorInfo(info)) 599 } 600 agent => agent, 601 }; 602 } 603 } 604 let manifest_assertion = ManifestAssertion::from_assertion(&actions)? 605 .set_instance(claim_assertion.instance()); 606 manifest.assertions.push(manifest_assertion); 607 } 608 base if base.starts_with(labels::INGREDIENT) => { 609 // note that we use the original label here, not the base label 610 let assertion_uri = jumbf::labels::to_assertion_uri(claim.label(), &label); 611 let ingredient = Ingredient::from_ingredient_uri( 612 store, 613 manifest_label, 614 &assertion_uri, 615 #[cfg(feature = "file_io")] 616 resource_path, 617 )?; 618 manifest.add_ingredient(ingredient); 619 } 620 labels::DATA_HASH | labels::BMFF_HASH | labels::BOX_HASH => { 621 // do not include data hash when reading manifests 622 } 623 label if label.starts_with(labels::CLAIM_THUMBNAIL) => { 624 let thumbnail = Thumbnail::from_assertion(assertion)?; 625 let id = jumbf::labels::to_assertion_uri(claim.label(), label); 626 let id = jumbf::labels::to_relative_uri(&id); 627 manifest.thumbnail = Some(manifest.resources.add_uri( 628 &id, 629 &thumbnail.content_type, 630 thumbnail.data, 631 )?); 632 } 633 _ => { 634 // inject assertions for all other assertions 635 match assertion.decode_data() { 636 AssertionData::Cbor(_) => { 637 let value = assertion.as_json_object()?; 638 let ma = ManifestAssertion::new(base_label, value) 639 .set_instance(claim_assertion.instance()); 640 manifest.assertions.push(ma); 641 } 642 AssertionData::Json(_) => { 643 let value = assertion.as_json_object()?; 644 let ma = ManifestAssertion::new(base_label, value) 645 .set_instance(claim_assertion.instance()) 646 .set_kind(ManifestAssertionKind::Json); 647 648 manifest.assertions.push(ma); 649 } 650 651 // todo: support binary forms 652 AssertionData::Binary(_x) => {} 653 AssertionData::Uuid(_, _) => {} 654 } 655 } 656 } 657 } 658 659 manifest.signature_info = match claim.signature_info() { 660 Some(signature_info) => Some(SignatureInfo { 661 alg: signature_info.alg, 662 issuer: signature_info.issuer_org, 663 time: signature_info.date.map(|d| d.to_rfc3339()), 664 cert_serial_number: signature_info.cert_serial_number.map(|s| s.to_string()), 665 cert_chain: String::from_utf8(signature_info.cert_chain) 666 .map_err(|_e| Error::CoseInvalidCert)?, 667 revocation_status: signature_info.revocation_status, 668 }), 669 None => None, 670 }; 671 672 Ok(manifest) 673 } 674 675 /// Sets the asset field from data in a file 676 /// the information in the claim should reflect the state of the asset it is embedded in 677 /// this method can be used to ensure that data is correct 678 /// it will extract filename,format and xmp info and generate a thumbnail 679 #[cfg(feature = "file_io")] 680 pub fn set_asset_from_path<P: AsRef<Path>>(&mut self, path: P) -> Result<()> { 681 // Gather the information we need from the target path 682 let ingredient = Ingredient::from_file_info(path.as_ref()); 683 684 self.set_format(ingredient.format()); 685 self.set_instance_id(ingredient.instance_id()); 686 687 // if there is already an asset title preserve it 688 if self.title().is_none() { 689 self.set_title(ingredient.title()); 690 } 691 692 // if a thumbnail is not already defined, create one here 693 if self.thumbnail_ref().is_none() { 694 #[cfg(feature = "add_thumbnails")] 695 if let Ok((format, image)) = crate::utils::thumbnail::make_thumbnail(path.as_ref()) { 696 // Do not write this as a file when reading from files 697 let base_path = self.resources_mut().take_base_path(); 698 self.set_thumbnail(format, image)?; 699 if let Some(path) = base_path { 700 self.resources_mut().set_base_path(path) 701 } 702 } 703 } 704 Ok(()) 705 } 706 707 // Convert a Manifest into a Claim 708 pub(crate) fn to_claim(&self) -> Result<Claim> { 709 // add library identifier to claim_generator 710 let generator = format!( 711 "{} {}/{}", 712 &self.claim_generator, 713 crate::NAME, 714 crate::VERSION 715 ); 716 717 let mut claim = match self.label() { 718 Some(label) => Claim::new_with_user_guid(&generator, &label.to_string()), 719 None => Claim::new(&generator, self.vendor.as_deref()), 720 }; 721 722 if let Some(info_vec) = self.claim_generator_info.as_ref() { 723 for info in info_vec { 724 let mut claim_info = info.to_owned(); 725 if let Some(icon) = claim_info.icon.as_ref() { 726 claim_info.icon = Some(icon.to_hashed_uri(self.resources(), &mut claim)?); 727 } 728 claim.add_claim_generator_info(claim_info); 729 } 730 } 731 732 if let Some(remote_op) = &self.remote_manifest { 733 match remote_op { 734 RemoteManifest::NoRemote => (), 735 RemoteManifest::SideCar => claim.set_external_manifest(), 736 RemoteManifest::Remote(r) => claim.set_remote_manifest(r)?, 737 RemoteManifest::EmbedWithRemote(r) => claim.set_embed_remote_manifest(r)?, 738 }; 739 } 740 741 if let Some(title) = self.title() { 742 claim.set_title(Some(title.to_owned())); 743 } 744 self.format().clone_into(&mut claim.format); 745 self.instance_id().clone_into(&mut claim.instance_id); 746 747 if let Some(thumb_ref) = self.thumbnail_ref() { 748 // Setting the format to "none" will ensure that no claim thumbnail is added 749 if thumb_ref.format != "none" { 750 let data = self.resources.get(&thumb_ref.identifier)?; 751 claim.add_assertion(&Thumbnail::new( 752 &labels::add_thumbnail_format(labels::CLAIM_THUMBNAIL, &thumb_ref.format), 753 data.into_owned(), 754 ))?; 755 } 756 } 757 758 // add any verified credentials - needs to happen early so we can reference them 759 let mut vc_table = HashMap::new(); 760 if let Some(verified_credentials) = self.credentials.as_ref() { 761 for vc in verified_credentials { 762 let vc_str = &vc.to_string(); 763 let id = Claim::vc_id(vc_str)?; 764 vc_table.insert(id, claim.add_verifiable_credential(vc_str)?); 765 } 766 } 767 768 let mut ingredient_map = HashMap::new(); 769 // add all ingredients to the claim 770 for ingredient in &self.ingredients { 771 let uri = ingredient.add_to_claim(&mut claim, self.redactions.clone(), None)?; 772 ingredient_map.insert(ingredient.instance_id(), uri); 773 } 774 775 let salt = DefaultSalt::default(); 776 777 // add any additional assertions 778 for manifest_assertion in &self.assertions { 779 match manifest_assertion.label() { 780 l if l.starts_with(Actions::LABEL) => { 781 let version = labels::version(l); 782 783 let mut actions: Actions = manifest_assertion.to_assertion()?; 784 785 let ingredients_key = match version { 786 None | Some(1) => "ingredient", 787 Some(2) => "ingredients", 788 _ => return Err(Error::AssertionUnsupportedVersion), 789 }; 790 791 // fixup parameters field from instance_id to ingredient uri 792 let needs_ingredient: Vec<(usize, crate::assertions::Action)> = actions 793 .actions() 794 .iter() 795 .enumerate() 796 .filter_map(|(i, a)| { 797 if a.instance_id().is_some() 798 && a.get_parameter(ingredients_key).is_none() 799 { 800 Some((i, a.clone())) 801 } else { 802 None 803 } 804 }) 805 .collect(); 806 807 for (index, action) in needs_ingredient { 808 if let Some(id) = action.instance_id() { 809 if let Some(hash_url) = ingredient_map.get(id) { 810 let update = match ingredients_key { 811 "ingredient" => { 812 action.set_parameter(ingredients_key, hash_url.clone()) 813 } 814 _ => { 815 // we only support on instanceId for actions, so only one ingredient on writing 816 action.set_parameter(ingredients_key, [hash_url.clone()]) 817 } 818 }?; 819 actions = actions.update_action(index, update); 820 } 821 } 822 } 823 824 if let Some(templates) = actions.templates.as_mut() { 825 for template in templates { 826 // replace icon with hashed_uri 827 template.icon = match template.icon.take() { 828 Some(icon) => { 829 Some(icon.to_hashed_uri(self.resources(), &mut claim)?) 830 } 831 None => None, 832 }; 833 834 // replace software agent with hashed_uri 835 template.software_agent = match template.software_agent.take() { 836 Some(SoftwareAgent::ClaimGeneratorInfo(mut info)) => { 837 if let Some(icon) = info.icon.as_mut() { 838 let icon = 839 icon.to_hashed_uri(self.resources(), &mut claim)?; 840 info.set_icon(icon); 841 } 842 Some(SoftwareAgent::ClaimGeneratorInfo(info)) 843 } 844 agent => agent, 845 }; 846 } 847 } 848 849 // convert icons in software agents to hashed uris 850 let actions_mut = actions.actions_mut(); 851 #[allow(clippy::needless_range_loop)] 852 // clippy is wrong here, we reference index twice 853 for index in 0..actions_mut.len() { 854 let action = &actions_mut[index]; 855 if let Some(SoftwareAgent::ClaimGeneratorInfo(info)) = 856 action.software_agent() 857 { 858 if let Some(icon) = info.icon.as_ref() { 859 let mut info = info.to_owned(); 860 let icon_uri = icon.to_hashed_uri(self.resources(), &mut claim)?; 861 let update = info.set_icon(icon_uri); 862 let mut action = action.to_owned(); 863 action = action.set_software_agent(update.to_owned()); 864 actions_mut[index] = action; 865 } 866 } 867 } 868 869 claim.add_assertion(&actions) 870 } 871 CreativeWork::LABEL => { 872 let mut cw: CreativeWork = manifest_assertion.to_assertion()?; 873 // insert a credentials field if we have a vc that matches the identifier 874 // todo: this should apply to any person, not just author 875 if let Some(cw_authors) = cw.author() { 876 let mut authors = Vec::new(); 877 for a in cw_authors { 878 authors.push( 879 a.identifier() 880 .and_then(|i| { 881 vc_table 882 .get(&i) 883 .map(|uri| a.clone().add_credential(uri.clone())) 884 }) 885 .unwrap_or_else(|| Ok(a.clone()))?, 886 ); 887 } 888 cw = cw.set_author(&authors)?; 889 } 890 claim.add_assertion_with_salt(&cw, &salt) 891 } 892 Exif::LABEL => { 893 let exif: Exif = manifest_assertion.to_assertion()?; 894 claim.add_assertion_with_salt(&exif, &salt) 895 } 896 _ => match manifest_assertion.kind() { 897 ManifestAssertionKind::Cbor => claim.add_assertion_with_salt( 898 &UserCbor::new( 899 manifest_assertion.label(), 900 serde_cbor::to_vec(&manifest_assertion.value()?)?, 901 ), 902 &salt, 903 ), 904 ManifestAssertionKind::Json => claim.add_assertion_with_salt( 905 &User::new( 906 manifest_assertion.label(), 907 &serde_json::to_string(&manifest_assertion.value()?)?, 908 ), 909 &salt, 910 ), 911 ManifestAssertionKind::Binary => { 912 // todo: Support binary kinds 913 return Err(Error::AssertionEncoding); 914 } 915 ManifestAssertionKind::Uri => { 916 // todo: Support binary kinds 917 return Err(Error::AssertionEncoding); 918 } 919 }, 920 }?; 921 } 922 923 Ok(claim) 924 } 925 926 // Convert a Manifest into a Store 927 pub(crate) fn to_store(&self) -> Result<Store> { 928 let claim = self.to_claim()?; 929 // commit the claim 930 let mut store = Store::new(); 931 let _provenance = store.commit_claim(claim)?; 932 Ok(store) 933 } 934 935 // factor out this code to set up the destination path with a file 936 // so we can use set_asset_from_path to initialize the right fields in Manifest 937 #[cfg(feature = "file_io")] 938 fn embed_prep<P: AsRef<Path>>(&mut self, source_path: P, dest_path: P) -> Result<P> { 939 let mut copied = false; 940 941 if !source_path.as_ref().exists() { 942 let path = source_path.as_ref().to_string_lossy().into_owned(); 943 return Err(Error::FileNotFound(path)); 944 } 945 // we need to copy the source to target before setting the asset info 946 if !dest_path.as_ref().exists() { 947 // ensure the path to the file exists 948 if let Some(output_dir) = dest_path.as_ref().parent() { 949 create_dir_all(output_dir)?; 950 } 951 std::fs::copy(&source_path, &dest_path)?; 952 copied = true; 953 } 954 // first add the information about the target file 955 self.set_asset_from_path(dest_path.as_ref())?; 956 957 if copied { 958 Ok(dest_path) 959 } else { 960 Ok(source_path) 961 } 962 } 963 964 /// Embed a signed manifest into the target file using a supplied signer. 965 /// 966 /// # Example: Embed a manifest in a file 967 /// 968 /// ``` 969 /// # use c2pa::Result; 970 /// use c2pa::{create_signer, Manifest, SigningAlg}; 971 /// use serde::Serialize; 972 /// 973 /// #[derive(Serialize)] 974 /// struct Test { 975 /// my_tag: usize, 976 /// } 977 /// 978 /// # fn main() -> Result<()> { 979 /// let mut manifest = Manifest::new("my_app".to_owned()); 980 /// manifest.add_labeled_assertion("org.contentauth.test", &Test { my_tag: 42 })?; 981 /// 982 /// // Create a PS256 signer using certs and public key files. 983 /// let signcert_path = "tests/fixtures/certs/ps256.pub"; 984 /// let pkey_path = "tests/fixtures/certs/ps256.pem"; 985 /// let signer = create_signer::from_files(signcert_path, pkey_path, SigningAlg::Ps256, None)?; 986 /// 987 /// // Embed a manifest using the signer. 988 /// manifest.embed("tests/fixtures/C.jpg", "../target/test_file.jpg", &*signer)?; 989 /// # Ok(()) 990 /// # } 991 /// ``` 992 #[cfg(feature = "file_io")] 993 pub fn embed<P: AsRef<Path>>( 994 &mut self, 995 source_path: P, 996 dest_path: P, 997 signer: &dyn Signer, 998 ) -> Result<()> { 999 // Add manifest info for this target file 1000 let source_path = self.embed_prep(source_path.as_ref(), dest_path.as_ref())?; 1001 1002 // convert the manifest to a store 1003 let mut store = self.to_store()?; 1004 1005 // sign and write our store to the output image file 1006 store.save_to_asset(source_path.as_ref(), signer, dest_path.as_ref())?; 1007 Ok(()) 1008 } 1009 1010 /// Embed a signed manifest into a stream using a supplied signer. 1011 /// returns the bytes of the manifest that was embedded 1012 #[async_generic(async_signature( 1013 &mut self, 1014 format: &str, 1015 asset: &[u8], 1016 signer: &dyn AsyncSigner, 1017 ))] 1018 pub fn embed_from_memory( 1019 &mut self, 1020 format: &str, 1021 asset: &[u8], 1022 signer: &dyn Signer, 1023 ) -> Result<Vec<u8>> { 1024 // first make a copy of the asset that will contain our modified result 1025 // todo:: see if we can pass a trait with to_vec support like we to for Strings 1026 let asset = asset.to_vec(); 1027 let mut stream = std::io::Cursor::new(asset); 1028 let mut output_stream = Cursor::new(Vec::new()); 1029 if _sync { 1030 self.embed_to_stream(format, &mut stream, &mut output_stream, signer)?; 1031 } else { 1032 self.embed_to_stream_async(format, &mut stream, &mut output_stream, signer) 1033 .await?; 1034 } 1035 Ok(output_stream.into_inner()) 1036 } 1037 1038 /// Embed a signed manifest into a stream using a supplied signer. 1039 /// 1040 /// Returns the bytes of the new asset 1041 #[deprecated(since = "0.27.2", note = "use embed_to_stream instead")] 1042 pub fn embed_stream( 1043 &mut self, 1044 format: &str, 1045 stream: &mut dyn CAIRead, 1046 signer: &dyn Signer, 1047 ) -> Result<Vec<u8>> { 1048 // sign and write our store to to the output image file 1049 let output_vec: Vec<u8> = Vec::new(); 1050 let mut output_stream = Cursor::new(output_vec); 1051 1052 self.embed_to_stream(format, stream, &mut output_stream, signer)?; 1053 1054 Ok(output_stream.into_inner()) 1055 } 1056 1057 /// Embed a signed manifest into a stream using a supplied signer. 1058 /// 1059 /// Returns the bytes of c2pa_manifest that was embedded. 1060 #[async_generic(async_signature( 1061 &mut self, 1062 format: &str, 1063 source: &mut dyn CAIRead, 1064 dest: &mut dyn CAIReadWrite, 1065 signer: &dyn AsyncSigner, 1066 ))] 1067 pub fn embed_to_stream( 1068 &mut self, 1069 format: &str, 1070 source: &mut dyn CAIRead, 1071 dest: &mut dyn CAIReadWrite, 1072 signer: &dyn Signer, 1073 ) -> Result<Vec<u8>> { 1074 self.set_format(format); 1075 // todo:: read instance_id from xmp from stream 1076 self.set_instance_id(format!("xmp:iid:{}", Uuid::new_v4())); 1077 1078 // generate thumbnail if we don't already have one 1079 #[cfg(feature = "add_thumbnails")] 1080 { 1081 if self.thumbnail_ref().is_none() { 1082 if let Ok((format, image)) = 1083 crate::utils::thumbnail::make_thumbnail_from_stream(format, source) 1084 { 1085 self.set_thumbnail(format, image)?; 1086 } 1087 } 1088 } 1089 1090 // convert the manifest to a store 1091 let mut store = self.to_store()?; 1092 1093 // sign and write our store to to the output image file 1094 if _sync { 1095 store.save_to_stream(format, source, dest, signer) 1096 } else { 1097 store 1098 .save_to_stream_async(format, source, dest, signer) 1099 .await 1100 } 1101 } 1102 1103 /// Embed a signed manifest into a stream using a supplied signer. 1104 /// returns the asset generated and bytes of the manifest that was embedded 1105 //#[cfg(feature = "remote_wasm_sign")] 1106 pub async fn embed_from_memory_remote_signed( 1107 &mut self, 1108 format: &str, 1109 asset: &[u8], 1110 signer: &dyn RemoteSigner, 1111 ) -> Result<(Vec<u8>, Vec<u8>)> { 1112 self.set_format(format); 1113 // todo:: read instance_id from xmp from stream 1114 self.set_instance_id(format!("xmp:iid:{}", Uuid::new_v4())); 1115 1116 // generate thumbnail if we don't already have one 1117 #[allow(unused_mut)] // so that this builds with WASM 1118 let mut stream = std::io::Cursor::new(asset); 1119 #[cfg(feature = "add_thumbnails")] 1120 { 1121 if self.thumbnail_ref().is_none() { 1122 if let Ok((format, image)) = 1123 crate::utils::thumbnail::make_thumbnail_from_stream(format, &mut stream) 1124 { 1125 self.set_thumbnail(format, image)?; 1126 } 1127 } 1128 } 1129 let asset = stream.into_inner(); 1130 1131 // convert the manifest to a store 1132 let mut store = self.to_store()?; 1133 1134 // sign and write our store to to the output image file 1135 let (output_asset, output_manifest) = store 1136 .save_to_memory_remote_signed(format, asset, signer) 1137 .await?; 1138 1139 Ok((output_asset, output_manifest)) 1140 } 1141 1142 /// Embed a signed manifest into the target file using a supplied [`AsyncSigner`]. 1143 #[cfg(feature = "file_io")] 1144 pub async fn embed_async_signed<P: AsRef<Path>>( 1145 &mut self, 1146 source_path: P, 1147 dest_path: P, 1148 signer: &dyn AsyncSigner, 1149 ) -> Result<Vec<u8>> { 1150 // Add manifest info for this target file 1151 let source_path = self.embed_prep(source_path.as_ref(), dest_path.as_ref())?; 1152 // convert the manifest to a store 1153 let mut store = self.to_store()?; 1154 // sign and write our store to to the output image file 1155 store 1156 .save_to_asset_async(source_path.as_ref(), signer, dest_path.as_ref()) 1157 .await 1158 } 1159 1160 /// Embed a signed manifest into the target file using a supplied [`RemoteSigner`]. 1161 #[cfg(feature = "file_io")] 1162 pub async fn embed_remote_signed<P: AsRef<Path>>( 1163 &mut self, 1164 source_path: P, 1165 dest_path: P, 1166 signer: &dyn RemoteSigner, 1167 ) -> Result<Vec<u8>> { 1168 // Add manifest info for this target file 1169 let source_path = self.embed_prep(source_path.as_ref(), dest_path.as_ref())?; 1170 // convert the manifest to a store 1171 let mut store = self.to_store()?; 1172 // sign and write our store to to the output image file 1173 store 1174 .save_to_asset_remote_signed(source_path.as_ref(), signer, dest_path.as_ref()) 1175 .await 1176 } 1177 1178 /// Removes any existing manifest from a file 1179 /// 1180 /// This should only be used for special cases, such as converting an embedded manifest 1181 /// to a cloud manifest 1182 #[cfg(feature = "file_io")] 1183 pub fn remove_manifest<P: AsRef<Path>>(asset_path: P) -> Result<()> { 1184 use crate::jumbf_io::remove_jumbf_from_file; 1185 remove_jumbf_from_file(asset_path.as_ref()) 1186 } 1187 1188 /// Generates a data hashed placeholder manifest for a file 1189 /// 1190 /// The return value is pre-formatted for insertion into a file of the given format 1191 /// For JPEG it is a series of App11 JPEG segments containing space for a manifest 1192 /// This is used to create a properly formatted file ready for signing. 1193 /// The reserve_size is the amount of space to reserve for the signature box. This 1194 /// value is fixed once set and must be sufficient to hold the completed signature 1195 pub fn data_hash_placeholder(&mut self, reserve_size: usize, format: &str) -> Result<Vec<u8>> { 1196 let dh: Result<DataHash> = self.find_assertion(DataHash::LABEL); 1197 if dh.is_err() { 1198 let mut ph = DataHash::new("jumbf manifest", "sha256"); 1199 for _ in 0..10 { 1200 ph.add_exclusion(HashRange::new(0, 2)); 1201 } 1202 self.add_assertion(&ph)?; 1203 } 1204 1205 let mut store = self.to_store()?; 1206 let placeholder = store.get_data_hashed_manifest_placeholder(reserve_size, format)?; 1207 Ok(placeholder) 1208 } 1209 1210 /// Generates an data hashed embeddable manifest for a file 1211 /// 1212 /// The return value is pre-formatted for insertion into a file of the given format 1213 /// For JPEG it is a series of App11 JPEG segments containing a signed manifest 1214 /// This can directly replace a placeholder manifest to create a properly signed asset 1215 /// The data hash must contain exclusions and may contain pre-calculated hashes 1216 /// if an asset reader is provided, it will be used to calculate the data hash 1217 #[async_generic(async_signature( 1218 &mut self, 1219 dh: &DataHash, 1220 signer: &dyn AsyncSigner, 1221 format: &str, 1222 mut asset_reader: Option<&mut dyn CAIRead>, 1223 ))] 1224 pub fn data_hash_embeddable_manifest( 1225 &mut self, 1226 dh: &DataHash, 1227 signer: &dyn Signer, 1228 format: &str, 1229 mut asset_reader: Option<&mut dyn CAIRead>, 1230 ) -> Result<Vec<u8>> { 1231 let mut store = self.to_store()?; 1232 if let Some(asset_reader) = asset_reader.as_deref_mut() { 1233 asset_reader.rewind()?; 1234 } 1235 if _sync { 1236 store.get_data_hashed_embeddable_manifest(dh, signer, format, asset_reader) 1237 } else { 1238 store 1239 .get_data_hashed_embeddable_manifest_async(dh, signer, format, asset_reader) 1240 .await 1241 } 1242 } 1243 1244 /// Generates an data hashed embeddable manifest for a file 1245 /// 1246 /// The return value is pre-formatted for insertion into a file of the given format 1247 /// For JPEG it is a series of App11 JPEG segments containing a signed manifest 1248 /// This can directly replace a placeholder manifest to create a properly signed asset 1249 /// The data hash must contain exclusions and may contain pre-calculated hashes 1250 /// if an asset reader is provided, it will be used to calculate the data hash 1251 pub async fn data_hash_embeddable_manifest_remote( 1252 &mut self, 1253 dh: &DataHash, 1254 signer: &dyn RemoteSigner, 1255 format: &str, 1256 mut asset_reader: Option<&mut dyn CAIRead>, 1257 ) -> Result<Vec<u8>> { 1258 let mut store = self.to_store()?; 1259 if let Some(asset_reader) = asset_reader.as_deref_mut() { 1260 asset_reader.rewind()?; 1261 } 1262 store 1263 .get_data_hashed_embeddable_manifest_remote(dh, signer, format, asset_reader) 1264 .await 1265 } 1266 1267 /// Generates a signed box hashed manifest, optionally preformatted for embedding 1268 /// 1269 /// The manifest must include a box hash assertion with correct hashes 1270 #[async_generic(async_signature( 1271 &mut self, 1272 signer: &dyn AsyncSigner, 1273 format: Option<&str>, 1274 ))] 1275 pub fn box_hash_embeddable_manifest( 1276 &mut self, 1277 signer: &dyn Signer, 1278 format: Option<&str>, 1279 ) -> Result<Vec<u8>> { 1280 let mut store = self.to_store()?; 1281 let mut cm = if _sync { 1282 store.get_box_hashed_embeddable_manifest(signer) 1283 } else { 1284 store.get_box_hashed_embeddable_manifest_async(signer).await 1285 }?; 1286 if let Some(format) = format { 1287 cm = Store::get_composed_manifest(&cm, format)?; 1288 } 1289 Ok(cm) 1290 } 1291 1292 /// Formats a signed manifest for embedding in the given format 1293 /// 1294 /// For instance, this would return one or JPEG App11 segments containing the manifest 1295 pub fn composed_manifest(manifest_bytes: &[u8], format: &str) -> Result<Vec<u8>> { 1296 Store::get_composed_manifest(manifest_bytes, format) 1297 } 1298 } 1299 1300 impl std::fmt::Display for Manifest { 1301 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 1302 let json = serde_json::to_string_pretty(self).unwrap_or_default(); 1303 f.write_str(&json) 1304 } 1305 } 1306 #[derive(Clone, Debug, Deserialize, Serialize)] 1307 #[cfg_attr(feature = "json_schema", derive(JsonSchema))] 1308 /// Holds information about a signature 1309 pub struct SignatureInfo { 1310 /// human readable issuing authority for this signature 1311 #[serde(skip_serializing_if = "Option::is_none")] 1312 alg: Option<SigningAlg>, 1313 /// human readable issuing authority for this signature 1314 #[serde(skip_serializing_if = "Option::is_none")] 1315 issuer: Option<String>, 1316 1317 /// The serial number of the certificate 1318 #[serde(skip_serializing_if = "Option::is_none")] 1319 cert_serial_number: Option<String>, 1320 1321 /// the time the signature was created 1322 #[serde(skip_serializing_if = "Option::is_none")] 1323 time: Option<String>, 1324 1325 /// the cert chain for this claim 1326 #[serde(skip)] // don't serialize this, let someone ask for it 1327 cert_chain: String, 1328 1329 /// revocation status of the certificate 1330 #[serde(skip_serializing_if = "Option::is_none")] 1331 revocation_status: Option<bool>, 1332 } 1333 1334 impl SignatureInfo { 1335 // returns the cert chain for this signature 1336 pub fn cert_chain(&self) -> &str { 1337 &self.cert_chain 1338 } 1339 } 1340 1341 #[cfg(test)] 1342 pub(crate) mod tests { 1343 #![allow(clippy::expect_used)] 1344 #![allow(clippy::unwrap_used)] 1345 1346 use std::io::Cursor; 1347 1348 #[cfg(feature = "file_io")] 1349 use tempfile::tempdir; 1350 #[cfg(target_arch = "wasm32")] 1351 use wasm_bindgen_test::*; 1352 1353 #[cfg(target_arch = "wasm32")] 1354 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); 1355 1356 use crate::{ 1357 assertions::{c2pa_action, Action, Actions}, 1358 ingredient::Ingredient, 1359 reader::Reader, 1360 utils::test::{temp_remote_signer, temp_signer, TEST_VC}, 1361 Manifest, Result, 1362 }; 1363 #[cfg(feature = "file_io")] 1364 use crate::{ 1365 assertions::{labels::ACTIONS, DataHash}, 1366 error::Error, 1367 hash_utils::HashRange, 1368 resource_store::ResourceRef, 1369 status_tracker::{DetailedStatusTracker, StatusTracker}, 1370 store::Store, 1371 utils::test::{ 1372 fixture_path, temp_dir_path, temp_fixture_path, write_jpeg_placeholder_file, 1373 TEST_SMALL_JPEG, 1374 }, 1375 validation_status, 1376 }; 1377 1378 // example of random data structure as an assertion 1379 #[derive(serde::Serialize)] 1380 #[allow(dead_code)] // this here for wasm builds to pass clippy (todo: remove) 1381 struct MyStruct { 1382 l1: String, 1383 l2: u32, 1384 } 1385 1386 fn test_manifest() -> Manifest { 1387 Manifest::new("test".to_owned()) 1388 } 1389 1390 #[test] 1391 #[cfg(feature = "file_io")] 1392 fn from_file() { 1393 let mut manifest = test_manifest(); 1394 let source_path = fixture_path(TEST_SMALL_JPEG); 1395 manifest 1396 .set_vendor("vendor".to_owned()) 1397 .set_parent(Ingredient::from_file(&source_path).expect("from_file")) 1398 .expect("set_parent"); 1399 1400 let vc: serde_json::Value = serde_json::from_str(TEST_VC).unwrap(); 1401 manifest 1402 .add_verifiable_credential(&vc) 1403 .expect("verifiable_credential"); 1404 1405 manifest 1406 .add_labeled_assertion( 1407 "my.assertion", 1408 &MyStruct { 1409 l1: "some data".to_owned(), 1410 l2: 5, 1411 }, 1412 ) 1413 .expect("add_assertion"); 1414 1415 let actions = Actions::new().add_action( 1416 Action::new(c2pa_action::EDITED) 1417 .set_parameter("name".to_owned(), "gaussian_blur") 1418 .unwrap(), 1419 ); 1420 1421 manifest.add_assertion(&actions).expect("add_assertion"); 1422 1423 manifest.add_ingredient(Ingredient::from_file(&source_path).expect("from_file")); 1424 1425 // generate json and omit binary thumbnails for printout 1426 let mut json = serde_json::to_string_pretty(&manifest).expect("error to json"); 1427 while let Some(index) = json.find("\"thumbnail\": [") { 1428 if let Some(idx2) = json[index..].find(']') { 1429 json = format!( 1430 "{}\"thumbnail\": \"<omitted>\"{}", 1431 &json[..index], 1432 &json[index + idx2 + 1..] 1433 ); 1434 } 1435 } 1436 1437 // copy an image to use as our target 1438 let dir = tempdir().expect("temp dir"); 1439 let test_output = dir.path().join("wc_embed_test.jpg"); 1440 1441 //embed a claim generated from this manifest 1442 let signer = temp_signer(); 1443 1444 let _store = manifest 1445 .embed(&source_path, &test_output, signer.as_ref()) 1446 .expect("embed"); 1447 1448 assert_eq!(manifest.format(), "image/jpeg"); 1449 assert_eq!(manifest.title(), Some("wc_embed_test.jpg")); 1450 if cfg!(feature = "add_thumbnails") { 1451 assert!(manifest.thumbnail().is_some()); 1452 } else { 1453 assert!(manifest.thumbnail().is_none()); 1454 } 1455 let ingredient = Ingredient::from_file(&test_output).expect("load_from_asset"); 1456 assert!(ingredient.active_manifest().is_some()); 1457 } 1458 1459 #[test] 1460 #[cfg(feature = "file_io")] 1461 /// test assertion validation on actions, should generate an error 1462 fn ws_bad_assertion() { 1463 // copy an image to use as our target for embedding 1464 let ap = fixture_path(TEST_SMALL_JPEG); 1465 let temp_dir = tempdir().expect("temp dir"); 1466 let test_output = temp_dir_path(&temp_dir, "ws_bad_assertion.jpg"); 1467 std::fs::copy(ap, test_output).expect("copy"); 1468 1469 let mut manifest = test_manifest(); 1470 1471 manifest 1472 .add_labeled_assertion( 1473 "c2pa.actions", 1474 &MyStruct { 1475 // add something that isn't an actions struct 1476 l1: "some data".to_owned(), 1477 l2: 5, 1478 }, 1479 ) 1480 .expect("add_assertion"); 1481 1482 // convert to store 1483 let result = manifest.to_store(); 1484 1485 println!("{result:?}"); 1486 assert!(result.is_err()) 1487 } 1488 1489 #[test] 1490 #[cfg(feature = "file_io")] 1491 /// test assertion validation on actions, should generate an error 1492 fn ws_valid_labeled_assertion() { 1493 // copy an image to use as our target for embedding 1494 let ap = fixture_path(TEST_SMALL_JPEG); 1495 let temp_dir = tempdir().expect("temp dir"); 1496 let test_output = temp_dir_path(&temp_dir, "ws_bad_assertion.jpg"); 1497 std::fs::copy(ap, test_output).expect("copy"); 1498 1499 let mut manifest = test_manifest(); 1500 1501 manifest 1502 .add_labeled_assertion( 1503 "c2pa.actions", 1504 &serde_json::json!({ 1505 "actions": [ 1506 { 1507 "action": "c2pa.edited", 1508 "parameters": { 1509 "description": "gradient", 1510 "name": "any value" 1511 }, 1512 "softwareAgent": "TestApp" 1513 }, 1514 { 1515 "action": "c2pa.dubbed", 1516 "changes": [ 1517 { 1518 "description": "translated to klingon", 1519 "region": [ 1520 { 1521 "type": "temporal", 1522 "time": {} 1523 }, 1524 { 1525 "type": "identified", 1526 "item": { 1527 "identifier": "https://bioportal.bioontology.org/ontologies/FMA", 1528 "value": "lips" 1529 } 1530 } 1531 ] 1532 } 1533 ] 1534 } 1535 ] 1536 }), 1537 ) 1538 .expect("add_assertion"); 1539 1540 // convert to store 1541 let store = manifest.to_store().expect("valid action to_store"); 1542 let m2 = Manifest::from_store(&store, &store.provenance_label().unwrap(), None) 1543 .expect("from_store"); 1544 let actions: Actions = m2 1545 .find_assertion("c2pa.actions.v2") 1546 .expect("find_assertion"); 1547 assert_eq!(actions.actions()[0].action(), "c2pa.edited"); 1548 assert_eq!(actions.actions()[1].action(), "c2pa.dubbed"); 1549 } 1550 1551 #[test] 1552 fn test_verifiable_credential() { 1553 let mut manifest = test_manifest(); 1554 let vc: serde_json::Value = serde_json::from_str(TEST_VC).unwrap(); 1555 manifest 1556 .add_verifiable_credential(&vc) 1557 .expect("verifiable_credential"); 1558 let store = manifest.to_store().expect("to_store"); 1559 let claim = store.provenance_claim().unwrap(); 1560 assert!(!claim.get_verifiable_credentials().is_empty()); 1561 } 1562 1563 #[test] 1564 fn test_assertion_user_cbor() { 1565 use crate::{assertions::UserCbor, Manifest}; 1566 1567 const LABEL: &str = "org.cai.test"; 1568 const DATA: &str = r#"{ "l1":"some data", "l2":"some other data" }"#; 1569 let json: serde_json::Value = serde_json::from_str(DATA).unwrap(); 1570 let data = serde_cbor::to_vec(&json).unwrap(); 1571 let cbor = UserCbor::new(LABEL, data); 1572 let mut manifest = test_manifest(); 1573 manifest.add_assertion(&cbor).expect("add_assertion"); 1574 manifest.add_assertion(&cbor).expect("add_assertion"); 1575 let store = manifest.to_store().expect("to_store"); 1576 1577 let _manifest2 = Manifest::from_store( 1578 &store, 1579 &store.provenance_label().unwrap(), 1580 #[cfg(feature = "file_io")] 1581 None, 1582 ) 1583 .expect("from_store"); 1584 println!("{store}"); 1585 println!("{_manifest2:?}"); 1586 let cbor2: UserCbor = manifest.find_assertion(LABEL).expect("get_assertion"); 1587 assert_eq!(cbor, cbor2); 1588 } 1589 1590 #[test] 1591 #[cfg(feature = "file_io")] 1592 fn test_redaction() { 1593 const ASSERTION_LABEL: &str = "stds.schema-org.CreativeWork"; 1594 1595 let temp_dir = tempdir().expect("temp dir"); 1596 let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG); 1597 let output2 = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG); 1598 1599 let mut manifest = test_manifest(); 1600 1601 manifest 1602 .add_labeled_assertion( 1603 ASSERTION_LABEL, 1604 &serde_json::json! ( 1605 { 1606 "@context": "https://schema.org", 1607 "@type": "CreativeWork", 1608 "author": [ 1609 { 1610 "@type": "Person", 1611 "name": "Joe Bloggs" 1612 }, 1613 1614 ] 1615 }), 1616 ) 1617 .expect("add_assertion"); 1618 1619 let signer = temp_signer(); 1620 1621 let c2pa_data = manifest 1622 .embed(&output, &output, signer.as_ref()) 1623 .expect("embed"); 1624 let mut validation_log = DetailedStatusTracker::new(); 1625 1626 let store1 = Store::load_from_memory("c2pa", &c2pa_data, true, &mut validation_log) 1627 .expect("load from memory"); 1628 let claim1_label = store1.provenance_label().unwrap(); 1629 let claim = store1.provenance_claim().unwrap(); 1630 assert!(claim.get_claim_assertion(ASSERTION_LABEL, 0).is_some()); // verify the assertion is there 1631 1632 // create a new claim and make the previous file a parent 1633 let mut manifest2 = test_manifest(); 1634 manifest2 1635 .set_parent(Ingredient::from_file(&output).expect("from_file")) 1636 .expect("set_parent"); 1637 1638 // redact the assertion 1639 manifest2 1640 .add_redaction(ASSERTION_LABEL) 1641 .expect("add_redaction"); 1642 1643 //embed a claim in output2 1644 let signer = temp_signer(); 1645 let _store2 = manifest2 1646 .embed(&output2, &output2, signer.as_ref()) 1647 .expect("embed"); 1648 1649 let mut report = DetailedStatusTracker::new(); 1650 let store3 = Store::load_from_asset(&output2, true, &mut report).unwrap(); 1651 let claim2 = store3.provenance_claim().unwrap(); 1652 1653 // assert!(!claim2.get_verifiable_credentials().is_empty()); 1654 1655 // test that the redaction is in the new claim and the assertion is removed from the first one 1656 1657 assert!(claim2.redactions().is_some()); 1658 assert!(!claim2.redactions().unwrap().is_empty()); 1659 assert!(!report.get_log().is_empty()); 1660 let redacted_uri = &claim2.redactions().unwrap()[0]; 1661 1662 let claim1 = store3.get_claim(&claim1_label).unwrap(); 1663 assert!(claim1.get_claim_assertion(redacted_uri, 0).is_none()); 1664 } 1665 1666 #[test] 1667 #[cfg(feature = "file_io")] 1668 fn test_action_assertion_redaction_error() { 1669 let temp_dir = tempdir().expect("temp dir"); 1670 let parent_output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG); 1671 1672 // Create parent with a c2pa_action type assertion. 1673 let mut parent_manifest = test_manifest(); 1674 let actions = Actions::new().add_action( 1675 Action::new(c2pa_action::FILTERED) 1676 .set_parameter("name".to_owned(), "gaussian blur") 1677 .unwrap() 1678 .set_when("2015-06-26T16:43:23+0200"), 1679 ); 1680 parent_manifest 1681 .add_assertion(&actions) 1682 .expect("add_assertion"); 1683 1684 let signer = temp_signer(); 1685 parent_manifest 1686 .embed(&parent_output, &parent_output, signer.as_ref()) 1687 .expect("embed"); 1688 1689 // Add parent_manifest as an ingredient of the new manifest and redact the assertion `c2pa.actions`. 1690 let mut manifest = test_manifest(); 1691 manifest 1692 .set_parent(Ingredient::from_file(&parent_output).expect("from_file")) 1693 .expect("set_parent"); 1694 assert!(manifest.add_redaction(ACTIONS).is_ok()); 1695 1696 // Attempt embedding the manifest with the invalid redaction. 1697 let redact_output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG); 1698 let embed_result = manifest.embed(&redact_output, &redact_output, signer.as_ref()); 1699 assert!(matches!( 1700 embed_result.err().unwrap(), 1701 Error::AssertionInvalidRedaction 1702 )); 1703 } 1704 1705 #[test] 1706 fn manifest_assertion_instances() { 1707 let mut manifest = Manifest::new("test".to_owned()); 1708 let actions = Actions::new().add_action(Action::new(c2pa_action::EDITED)); 1709 // add three assertions with the same label 1710 manifest.add_assertion(&actions).expect("add_assertion"); 1711 manifest.add_assertion(&actions).expect("add_assertion"); 1712 manifest.add_assertion(&actions).expect("add_assertion"); 1713 1714 // convert to a store and read back again 1715 let store = manifest.to_store().expect("to_store"); 1716 println!("{store}"); 1717 let active_label = store.provenance_label().unwrap(); 1718 1719 let manifest2 = Manifest::from_store( 1720 &store, 1721 &active_label, 1722 #[cfg(feature = "file_io")] 1723 None, 1724 ) 1725 .expect("from_store"); 1726 println!("{manifest2}"); 1727 1728 // now check to see if we have three separate assertions with different instances 1729 let action2: Result<Actions> = manifest2.find_assertion_with_instance(Actions::LABEL, 2); 1730 assert!(action2.is_ok()); 1731 assert_eq!(action2.unwrap().actions()[0].action(), c2pa_action::EDITED); 1732 } 1733 1734 #[cfg(all(feature = "file_io", feature = "openssl_sign"))] 1735 #[actix::test] 1736 async fn test_embed_async_sign() { 1737 let temp_dir = tempdir().expect("temp dir"); 1738 let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG); 1739 1740 let async_signer = 1741 crate::openssl::temp_signer_async::AsyncSignerAdapter::new(crate::SigningAlg::Ps256); 1742 1743 let mut manifest = test_manifest(); 1744 manifest 1745 .embed_async_signed(&output, &output, &async_signer) 1746 .await 1747 .expect("embed"); 1748 let reader = Reader::from_file(&output).expect("from_file"); 1749 assert_eq!( 1750 reader.active_manifest().unwrap().title().unwrap(), 1751 TEST_SMALL_JPEG 1752 ); 1753 } 1754 1755 #[cfg(all(feature = "file_io", feature = "openssl_sign"))] 1756 #[actix::test] 1757 async fn test_embed_remote_sign() { 1758 let temp_dir = tempdir().expect("temp dir"); 1759 let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG); 1760 1761 let remote_signer = temp_remote_signer(); 1762 1763 let mut manifest = test_manifest(); 1764 manifest 1765 .embed_remote_signed(&output, &output, remote_signer.as_ref()) 1766 .await 1767 .expect("embed"); 1768 let manifest_store = Reader::from_file(&output).expect("from_file"); 1769 assert_eq!( 1770 manifest_store.active_manifest().unwrap().title().unwrap(), 1771 TEST_SMALL_JPEG 1772 ); 1773 } 1774 1775 #[cfg(feature = "file_io")] 1776 #[test] 1777 fn test_embed_user_label() { 1778 let temp_dir = tempdir().expect("temp dir"); 1779 let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG); 1780 1781 let signer = temp_signer(); 1782 1783 let mut manifest = test_manifest(); 1784 manifest.set_label("MyLabel"); 1785 manifest 1786 .embed(&output, &output, signer.as_ref()) 1787 .expect("embed"); 1788 1789 let reader = Reader::from_file(&output).expect("from_file"); 1790 assert_eq!( 1791 reader.active_manifest().unwrap().title().unwrap(), 1792 TEST_SMALL_JPEG 1793 ); 1794 } 1795 1796 #[cfg(feature = "file_io")] 1797 #[test] 1798 fn test_embed_sidecar_user_label() { 1799 let temp_dir = tempdir().expect("temp dir"); 1800 let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG); 1801 let sidecar = output.with_extension("c2pa"); 1802 let fp = format!("file:/{}", sidecar.to_str().unwrap()); 1803 let url = url::Url::parse(&fp).unwrap(); 1804 1805 let signer = temp_signer(); 1806 1807 let mut manifest = test_manifest(); 1808 manifest.set_label("MyLabel"); 1809 manifest.set_remote_manifest(url); 1810 let c2pa_data = manifest 1811 .embed(&output, &output, signer.as_ref()) 1812 .expect("embed"); 1813 1814 let manifest_store = 1815 Reader::from_stream("application/c2pa", Cursor::new(c2pa_data)).expect("from_bytes"); 1816 assert_eq!( 1817 manifest_store.active_manifest().unwrap().title().unwrap(), 1818 TEST_SMALL_JPEG 1819 ); 1820 } 1821 1822 #[cfg_attr(not(target_arch = "wasm32"), actix::test)] 1823 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] 1824 async fn test_embed_jpeg_stream_wasm() { 1825 use crate::assertions::User; 1826 let image = include_bytes!("../tests/fixtures/earth_apollo17.jpg"); 1827 // convert buffer to cursor with Read/Write/Seek capability 1828 1829 let mut manifest = Manifest::new("my_app".to_owned()); 1830 manifest.set_title("EmbedStream"); 1831 manifest 1832 .add_assertion(&User::new( 1833 "org.contentauth.mylabel", 1834 r#"{"my_tag":"Anything I want"}"#, 1835 )) 1836 .unwrap(); 1837 1838 // add a parent ingredient 1839 let mut ingredient = Ingredient::from_memory_async("jpeg", image) 1840 .await 1841 .expect("from_stream_async"); 1842 ingredient.set_title("parent.jpg"); 1843 manifest.set_parent(ingredient).expect("set_parent"); 1844 1845 let signer = temp_remote_signer(); 1846 1847 // Embed a manifest using the signer. 1848 let (out_vec, _out_manifest) = manifest 1849 .embed_from_memory_remote_signed("jpeg", image, signer.as_ref()) 1850 .await 1851 .expect("embed_stream"); 1852 1853 // try to load the image 1854 let manifest_store = Reader::from_stream("image/jpeg", Cursor::new(out_vec)).unwrap(); 1855 1856 /* to be enabled later 1857 // try to load the manifest 1858 let mut validation_log = DetailedStatusTracker::new(); 1859 Store::from_jumbf(&out_manifest, &mut validation_log).expect("manifest_load_error"); 1860 */ 1861 println!("It worked: {manifest_store}\n"); 1862 } 1863 1864 #[cfg_attr(not(target_arch = "wasm32"), actix::test)] 1865 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] 1866 async fn test_embed_png_stream_wasm() { 1867 use crate::assertions::User; 1868 let image = include_bytes!("../tests/fixtures/libpng-test.png"); 1869 // convert buffer to cursor with Read/Write/Seek capability 1870 1871 let mut manifest = Manifest::new("my_app".to_owned()); 1872 manifest.set_title("EmbedStream"); 1873 manifest 1874 .add_assertion(&User::new( 1875 "org.contentauth.mylabel", 1876 r#"{"my_tag":"Anything I want"}"#, 1877 )) 1878 .unwrap(); 1879 1880 let signer = temp_remote_signer(); 1881 1882 // Embed a manifest using the signer. 1883 let (out_vec, _out_manifest) = manifest 1884 .embed_from_memory_remote_signed("png", image, signer.as_ref()) 1885 .await 1886 .expect("embed_stream"); 1887 1888 // try to load the image 1889 let manifest_store = Reader::from_stream("image/png", Cursor::new(out_vec)).unwrap(); 1890 1891 /* to be enabled later 1892 // try to load the manifest 1893 let mut validation_log = DetailedStatusTracker::new(); 1894 Store::from_jumbf(&out_manifest, &mut validation_log).expect("manifest_load_error"); 1895 */ 1896 1897 println!("It worked: {manifest_store}\n"); 1898 } 1899 1900 #[cfg_attr(not(target_arch = "wasm32"), actix::test)] 1901 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] 1902 async fn test_embed_webp_stream_wasm() { 1903 use crate::assertions::User; 1904 let image = include_bytes!("../tests/fixtures/mars.webp"); 1905 // convert buffer to cursor with Read/Write/Seek capability 1906 1907 let mut manifest = Manifest::new("my_app".to_owned()); 1908 manifest.set_title("EmbedStream"); 1909 manifest 1910 .add_assertion(&User::new( 1911 "org.contentauth.mylabel", 1912 r#"{"my_tag":"Anything I want"}"#, 1913 )) 1914 .unwrap(); 1915 1916 let signer = temp_remote_signer(); 1917 1918 // Embed a manifest using the signer. 1919 let (out_vec, _out_manifest) = manifest 1920 .embed_from_memory_remote_signed("image/webp", image, signer.as_ref()) 1921 .await 1922 .expect("embed_stream"); 1923 1924 // try to load the image 1925 let manifest_store = Reader::from_stream("image/webp", Cursor::new(out_vec)).unwrap(); 1926 1927 /* to be enabled later 1928 // try to load the manifest 1929 let mut validation_log = DetailedStatusTracker::new(); 1930 Store::from_jumbf(&out_manifest, &mut validation_log).expect("manifest_load_error"); 1931 */ 1932 1933 println!("It worked: {manifest_store}\n"); 1934 } 1935 1936 #[test] 1937 fn test_embed_stream() { 1938 use crate::assertions::User; 1939 let image = include_bytes!("../tests/fixtures/earth_apollo17.jpg"); 1940 // convert buffer to cursor with Read/Write/Seek capability 1941 let mut stream = std::io::Cursor::new(image.to_vec()); 1942 // let mut image = image.to_vec(); 1943 // let mut stream = std::io::Cursor::new(image.as_mut_slice()); 1944 1945 let mut manifest = Manifest::new("my_app".to_owned()); 1946 manifest.set_title("EmbedStream"); 1947 manifest 1948 .add_assertion(&User::new( 1949 "org.contentauth.mylabel", 1950 r#"{"my_tag":"Anything I want"}"#, 1951 )) 1952 .unwrap(); 1953 1954 let signer = temp_signer(); 1955 let mut output = Cursor::new(Vec::new()); 1956 // Embed a manifest using the signer. 1957 manifest 1958 .embed_to_stream("jpeg", &mut stream, &mut output, signer.as_ref()) 1959 .expect("embed_stream"); 1960 1961 stream.set_position(0); 1962 let reader = Reader::from_stream("jpeg", &mut output).expect("from_bytes"); 1963 assert_eq!( 1964 reader.active_manifest().unwrap().title().unwrap(), 1965 "EmbedStream" 1966 ); 1967 #[cfg(feature = "add_thumbnails")] 1968 assert!(reader.active_manifest().unwrap().thumbnail().is_some()); 1969 //println!("{manifest_store}");main 1970 } 1971 1972 #[cfg(any(target_arch = "wasm32", feature = "openssl_sign"))] 1973 #[cfg_attr(feature = "openssl_sign", actix::test)] 1974 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] 1975 async fn test_embed_from_memory_async() { 1976 use crate::{assertions::User, utils::test::temp_async_signer}; 1977 let image = include_bytes!("../tests/fixtures/earth_apollo17.jpg"); 1978 // convert buffer to cursor with Read/Write/Seek capability 1979 let mut stream = std::io::Cursor::new(image.to_vec()); 1980 // let mut image = image.to_vec(); 1981 // let mut stream = std::io::Cursor::new(image.as_mut_slice()); 1982 1983 let mut manifest = Manifest::new("my_app".to_owned()); 1984 manifest.set_title("EmbedStream"); 1985 manifest 1986 .add_assertion(&User::new( 1987 "org.contentauth.mylabel", 1988 r#"{"my_tag":"Anything I want"}"#, 1989 )) 1990 .unwrap(); 1991 1992 let signer = temp_async_signer(); 1993 let mut output = Cursor::new(Vec::new()); 1994 // Embed a manifest using the signer. 1995 manifest 1996 .embed_to_stream_async("jpeg", &mut stream, &mut output, signer.as_ref()) 1997 .await 1998 .expect("embed_stream"); 1999 2000 let manifest_store = crate::ManifestStore::from_bytes("jpeg", &output.into_inner(), true) 2001 .expect("from_bytes"); 2002 assert_eq!( 2003 manifest_store.get_active().unwrap().title().unwrap(), 2004 "EmbedStream" 2005 ); 2006 #[cfg(feature = "add_thumbnails")] 2007 assert!(manifest_store.get_active().unwrap().thumbnail().is_some()); 2008 //println!("{manifest_store}");main 2009 } 2010 2011 #[cfg(feature = "file_io")] 2012 #[actix::test] 2013 /// Verify that an ingredient with error is reported on the ingredient and not on the manifest_store 2014 async fn test_embed_with_ingredient_error() { 2015 let temp_dir = tempdir().expect("temp dir"); 2016 let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG); 2017 2018 let signer = temp_signer(); 2019 2020 let mut manifest = test_manifest(); 2021 let ingredient = 2022 Ingredient::from_file(fixture_path("XCA.jpg")).expect("getting ingredient"); 2023 assert!(ingredient.validation_status().is_some()); 2024 assert_eq!( 2025 ingredient.validation_status().unwrap()[0].code(), 2026 validation_status::ASSERTION_DATAHASH_MISMATCH 2027 ); 2028 manifest.add_ingredient(ingredient); 2029 manifest 2030 .embed(&output, &output, signer.as_ref()) 2031 .expect("embed"); 2032 let manifest_store = Reader::from_file(&output).expect("from_file"); 2033 println!("{manifest_store}"); 2034 let manifest = manifest_store.active_manifest().unwrap(); 2035 let ingredient_status = manifest.ingredients()[0].validation_status(); 2036 assert_eq!( 2037 ingredient_status.unwrap()[0].code(), 2038 validation_status::ASSERTION_DATAHASH_MISMATCH 2039 ); 2040 assert_eq!(manifest.title().unwrap(), TEST_SMALL_JPEG); 2041 assert!(manifest_store.validation_status().is_none()) 2042 } 2043 2044 #[cfg(feature = "file_io")] 2045 #[test] 2046 fn test_embed_sidecar_with_parent_manifest() { 2047 let temp_dir = tempdir().expect("temp dir"); 2048 let source = fixture_path("XCA.jpg"); 2049 let output = temp_dir.path().join("XCAplus.jpg"); 2050 let sidecar = output.with_extension("c2pa"); 2051 let fp = format!("file:/{}", sidecar.to_str().unwrap()); 2052 let url = url::Url::parse(&fp).unwrap(); 2053 2054 let signer = temp_signer(); 2055 2056 let parent = Ingredient::from_file(fixture_path("XCA.jpg")).expect("getting parent"); 2057 let mut manifest = test_manifest(); 2058 manifest.set_parent(parent).expect("setting parent"); 2059 manifest.set_remote_manifest(url.clone()); 2060 let _c2pa_data = manifest 2061 .embed(&source, &output, signer.as_ref()) 2062 .expect("embed"); 2063 2064 assert_eq!(manifest.remote_manifest_url().unwrap(), url.to_string()); 2065 2066 //let manifest_store = crate::ManifestStore::from_file(&sidecar).expect("from_file"); 2067 let manifest_store = Reader::from_file(&output).expect("from_file"); 2068 assert_eq!( 2069 manifest_store.active_manifest().unwrap().title().unwrap(), 2070 "XCAplus.jpg" 2071 ); 2072 } 2073 2074 #[cfg(feature = "file_io")] 2075 #[test] 2076 fn test_embed_user_thumbnail() { 2077 let temp_dir = tempdir().expect("temp dir"); 2078 let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG); 2079 2080 let signer = temp_signer(); 2081 2082 let mut manifest = test_manifest(); 2083 let thumb_data = vec![1, 2, 3]; 2084 manifest 2085 .set_thumbnail("image/jpeg", thumb_data.clone()) 2086 .expect("set_thumbnail"); 2087 manifest 2088 .embed(&output, &output, signer.as_ref()) 2089 .expect("embed"); 2090 let manifest_store = Reader::from_file(&output).expect("from_file"); 2091 let active_manifest = manifest_store.active_manifest().unwrap(); 2092 let (format, image) = active_manifest.thumbnail().unwrap(); 2093 assert_eq!(format, "image/jpeg"); 2094 assert_eq!(image.into_owned(), thumb_data); 2095 } 2096 2097 #[cfg(feature = "file_io")] 2098 const MANIFEST_JSON: &str = r#"{ 2099 "claim_generator": "test", 2100 "claim_generator_info": [ 2101 { 2102 "name": "test", 2103 "version": "1.0", 2104 "icon": { 2105 "format": "image/svg+xml", 2106 "identifier": "sample1.svg" 2107 } 2108 } 2109 ], 2110 "format" : "image/jpeg", 2111 "thumbnail": { 2112 "format": "image/jpeg", 2113 "identifier": "IMG_0003.jpg" 2114 }, 2115 "assertions": [ 2116 { 2117 "label": "c2pa.actions.v2", 2118 "data": { 2119 "actions": [ 2120 { 2121 "action": "c2pa.opened", 2122 "instanceId": "xmp.iid:7b57930e-2f23-47fc-affe-0400d70b738d", 2123 "parameters": { 2124 "description": "import" 2125 }, 2126 "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia", 2127 "softwareAgent": { 2128 "name": "TestApp", 2129 "version": "1.0", 2130 "icon": { 2131 "format": "image/svg+xml", 2132 "identifier": "sample1.svg" 2133 }, 2134 "something": "else" 2135 }, 2136 "changes": [ 2137 { 2138 "region" : [ 2139 { 2140 "type" : "temporal", 2141 "time" : {} 2142 }, 2143 { 2144 "type" : "identified", 2145 "item" : { 2146 "identifier" : "https://bioportal.bioontology.org/ontologies/FMA", 2147 "value" : "lips" 2148 } 2149 } 2150 ], 2151 "description": "lip synced area" 2152 } 2153 ] 2154 } 2155 ], 2156 "templates": [ 2157 { 2158 "action": "c2pa.opened", 2159 "softwareAgent": { 2160 "name": "TestApp", 2161 "version": "1.0", 2162 "icon": { 2163 "format": "image/svg+xml", 2164 "identifier": "sample1.svg" 2165 }, 2166 "something": "else" 2167 }, 2168 "icon": { 2169 "format": "image/svg+xml", 2170 "identifier": "sample1.svg" 2171 } 2172 } 2173 ] 2174 } 2175 } 2176 ], 2177 "ingredients": [{ 2178 "title": "A.jpg", 2179 "format": "image/jpeg", 2180 "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb", 2181 "relationship": "parentOf", 2182 "thumbnail": { 2183 "format": "image/png", 2184 "identifier": "exp-test1.png" 2185 } 2186 }, 2187 { 2188 "title": "prompt", 2189 "format": "text/plain", 2190 "relationship": "inputTo", 2191 "data": { 2192 "format": "text/plain", 2193 "identifier": "prompt.txt", 2194 "data_types": [ 2195 { 2196 "type": "c2pa.types.generator.prompt" 2197 } 2198 ] 2199 } 2200 } 2201 ] 2202 }"#; 2203 2204 #[test] 2205 #[cfg(feature = "openssl_sign")] 2206 /// tests and illustrates how to add assets to a non-file based manifest by using a stream 2207 fn from_json_with_stream() { 2208 use crate::assertions::Relationship; 2209 2210 let mut manifest = Manifest::from_json(MANIFEST_JSON).unwrap(); 2211 // add binary resources to manifest and ingredients giving matching the identifiers given in JSON 2212 manifest 2213 .resources_mut() 2214 .add("IMG_0003.jpg", *b"my value") 2215 .unwrap() 2216 .add("sample1.svg", *b"my value") 2217 .expect("add resource"); 2218 manifest.ingredients_mut()[0] 2219 .resources_mut() 2220 .add("exp-test1.png", *b"my value") 2221 .expect("add_resource"); 2222 manifest.ingredients_mut()[1] 2223 .resources_mut() 2224 .add("prompt.txt", *b"pirate with bird on shoulder") 2225 .expect("add_resource"); 2226 2227 println!("{manifest}"); 2228 2229 let image = include_bytes!("../tests/fixtures/earth_apollo17.jpg"); 2230 // convert buffer to cursor with Read/Write/Seek capability 2231 let mut input = std::io::Cursor::new(image.to_vec()); 2232 2233 let signer = temp_signer(); 2234 // Embed a manifest using the signer. 2235 let mut output = Cursor::new(Vec::new()); 2236 manifest 2237 .embed_to_stream("jpeg", &mut input, &mut output, signer.as_ref()) 2238 .expect("embed_stream"); 2239 2240 output.set_position(0); 2241 let reader = Reader::from_stream("jpeg", &mut output).expect("from_bytes"); 2242 println!("manifest_store = {reader}"); 2243 let m = reader.active_manifest().unwrap(); 2244 2245 //println!("after = {m}"); 2246 2247 assert!(m.thumbnail().is_some()); 2248 let (format, image) = m.thumbnail().unwrap(); 2249 assert_eq!(format, "image/jpeg"); 2250 assert_eq!(image.to_vec(), b"my value"); 2251 assert_eq!(m.ingredients().len(), 2); 2252 assert_eq!(m.ingredients()[1].relationship(), &Relationship::InputTo); 2253 assert!(m.ingredients()[1].data_ref().is_some()); 2254 assert_eq!(m.ingredients()[1].data_ref().unwrap().format, "text/plain"); 2255 let id = m.ingredients()[1].data_ref().unwrap().identifier.as_str(); 2256 assert_eq!( 2257 m.ingredients()[1].resources().get(id).unwrap().into_owned(), 2258 b"pirate with bird on shoulder" 2259 ); 2260 // println!("{manifest_store}"); 2261 } 2262 2263 #[test] 2264 #[cfg(feature = "openssl_sign")] 2265 /// tests and illustrates how to add assets to a non-file based manifest by using a memory buffer 2266 fn from_json_with_memory() { 2267 use crate::assertions::Relationship; 2268 2269 let mut manifest = Manifest::from_json(MANIFEST_JSON).unwrap(); 2270 // add binary resources to manifest and ingredients giving matching the identifiers given in JSON 2271 manifest 2272 .resources_mut() 2273 .add("IMG_0003.jpg", *b"my value") 2274 .unwrap() 2275 .add("sample1.svg", *b"my value") 2276 .expect("add resource"); 2277 manifest.ingredients_mut()[0] 2278 .resources_mut() 2279 .add("exp-test1.png", *b"my value") 2280 .expect("add_resource"); 2281 manifest.ingredients_mut()[1] 2282 .resources_mut() 2283 .add("prompt.txt", *b"pirate with bird on shoulder") 2284 .expect("add_resource"); 2285 2286 println!("{manifest}"); 2287 2288 let image = include_bytes!("../tests/fixtures/earth_apollo17.jpg"); 2289 2290 let signer = temp_signer(); 2291 // Embed a manifest using the signer. 2292 let output_image = manifest 2293 .embed_from_memory("jpeg", image, signer.as_ref()) 2294 .expect("embed_stream"); 2295 2296 let reader = Reader::from_stream("jpeg", Cursor::new(output_image)).expect("from_bytes"); 2297 println!("manifest_store = {reader}"); 2298 let m = reader.active_manifest().unwrap(); 2299 2300 assert!(m.thumbnail().is_some()); 2301 let (format, image) = m.thumbnail().unwrap(); 2302 assert_eq!(format, "image/jpeg"); 2303 assert_eq!(image.to_vec(), b"my value"); 2304 assert_eq!(m.ingredients().len(), 2); 2305 assert_eq!(m.ingredients()[1].relationship(), &Relationship::InputTo); 2306 assert!(m.ingredients()[1].data_ref().is_some()); 2307 assert_eq!(m.ingredients()[1].data_ref().unwrap().format, "text/plain"); 2308 let id = m.ingredients()[1].data_ref().unwrap().identifier.as_str(); 2309 assert_eq!( 2310 m.ingredients()[1].resources().get(id).unwrap().into_owned(), 2311 b"pirate with bird on shoulder" 2312 ); 2313 // println!("{manifest_store}"); 2314 } 2315 2316 #[test] 2317 #[cfg(feature = "file_io")] 2318 fn from_json_with_files() { 2319 let mut manifest = Manifest::from_json(MANIFEST_JSON).unwrap(); 2320 let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); 2321 path.push("tests/fixtures"); // the path we want to read files from 2322 manifest.with_base_path(path).expect("with_files"); 2323 // convert the manifest to a store 2324 let store = manifest.to_store().expect("to store"); 2325 let mut resource_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); 2326 resource_path.push("../target/tmp/manifest"); 2327 let m2 = Manifest::from_store( 2328 &store, 2329 &store.provenance_label().unwrap(), 2330 Some(&resource_path), 2331 ) 2332 .expect("from store"); 2333 println!("{m2}"); 2334 assert!(m2.thumbnail().is_some()); 2335 assert!(m2.ingredients()[0].thumbnail().is_some()); 2336 } 2337 2338 #[cfg(feature = "file_io")] 2339 #[test] 2340 fn test_embed_from_json() { 2341 let mut fixtures = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); 2342 fixtures.push("tests/fixtures"); // the path we want to read files from 2343 2344 let temp_dir = tempdir().expect("temp dir"); 2345 let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG); 2346 2347 let signer = temp_signer(); 2348 2349 let mut manifest = Manifest::from_json(MANIFEST_JSON).expect("from_json"); 2350 manifest.with_base_path(fixtures).expect("with_base"); 2351 manifest 2352 .embed(&output, &output, signer.as_ref()) 2353 .expect("embed"); 2354 2355 let reader = Reader::from_file(&output).expect("from_file"); 2356 println!("{reader}"); 2357 let active_manifest = reader.active_manifest().unwrap(); 2358 let (format, _) = active_manifest.thumbnail().unwrap(); 2359 assert_eq!(format, "image/jpeg"); 2360 } 2361 2362 #[cfg(feature = "file_io")] 2363 #[test] 2364 fn test_embed_webp_from_json() { 2365 use crate::utils::test::TEST_WEBP; 2366 2367 let mut fixtures = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); 2368 fixtures.push("tests/fixtures"); // the path we want to read files from 2369 2370 let temp_dir = tempdir().expect("temp dir"); 2371 let output = temp_fixture_path(&temp_dir, TEST_WEBP); 2372 2373 let signer = temp_signer(); 2374 2375 let mut manifest = Manifest::from_json(MANIFEST_JSON).expect("from_json"); 2376 manifest.with_base_path(fixtures).expect("with_base"); 2377 manifest 2378 .embed(&output, &output, signer.as_ref()) 2379 .expect("embed"); 2380 2381 let manifest_store = Reader::from_file(&output).expect("from_file"); 2382 println!("{manifest_store}"); 2383 let active_manifest = manifest_store.active_manifest().unwrap(); 2384 let (format, _) = active_manifest.thumbnail().unwrap(); 2385 assert_eq!(format, "image/jpeg"); 2386 } 2387 2388 #[test] 2389 #[cfg(feature = "file_io")] 2390 fn test_create_file_based_ingredient() { 2391 let mut fixtures = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); 2392 fixtures.push("tests/fixtures"); 2393 2394 let temp_dir = tempdir().expect("temp dir"); 2395 let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG); 2396 2397 let mut manifest = Manifest::new("claim_generator"); 2398 manifest.with_base_path(fixtures).expect("with_base"); 2399 // verify we can't set a references that don't exist 2400 assert!(manifest 2401 .set_thumbnail_ref(ResourceRef::new("image/jpg", "foo")) 2402 .is_err()); 2403 assert!(manifest.thumbnail_ref().is_none()); 2404 // verify we can set a references that do exist 2405 assert!(manifest 2406 .set_thumbnail_ref(ResourceRef::new("image/jpeg", "C.jpg")) 2407 .is_ok()); 2408 assert!(manifest.thumbnail_ref().is_some()); 2409 2410 let signer = temp_signer(); 2411 manifest 2412 .embed(&output, &output, signer.as_ref()) 2413 .expect("embed"); 2414 } 2415 2416 #[test] 2417 #[cfg(all(feature = "file_io", feature = "add_thumbnails"))] 2418 fn test_create_no_claim_thumbnail() { 2419 let mut fixtures = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); 2420 fixtures.push("tests/fixtures"); 2421 2422 let temp_dir = tempdir().expect("temp dir"); 2423 let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG); 2424 2425 let mut manifest = Manifest::new("claim_generator"); 2426 2427 // Set format to none to force no claim thumbnail generated 2428 assert!(manifest 2429 .set_thumbnail_ref(ResourceRef::new("none", "none")) 2430 .is_ok()); 2431 // verify there is a thumbnail ref 2432 assert!(manifest.thumbnail_ref().is_some()); 2433 // verify there is no thumbnail 2434 assert!(manifest.thumbnail().is_none()); 2435 2436 let signer = temp_signer(); 2437 manifest 2438 .embed(&output, &output, signer.as_ref()) 2439 .expect("embed"); 2440 2441 let manifest_store = Reader::from_file(&output).expect("from_file"); 2442 println!("{manifest_store}"); 2443 let active_manifest = manifest_store.active_manifest().unwrap(); 2444 assert!(active_manifest.thumbnail_ref().is_none()); 2445 assert!(active_manifest.thumbnail().is_none()); 2446 } 2447 2448 #[test] 2449 fn test_missing_thumbnail() { 2450 const MANIFEST_JSON: &str = r#" 2451 { 2452 "claim_generator": "test", 2453 "format" : "image/jpeg", 2454 "thumbnail": { 2455 "format": "image/jpeg", 2456 "identifier": "does_not_exist.jpg" 2457 } 2458 } 2459 "#; 2460 2461 let mut manifest = Manifest::from_json(MANIFEST_JSON).expect("from_json"); 2462 2463 let mut source = std::io::Cursor::new(vec![1, 2, 3]); 2464 let mut dest = std::io::Cursor::new(Vec::new()); 2465 let signer = temp_signer(); 2466 let result = 2467 manifest.embed_to_stream("image/jpeg", &mut source, &mut dest, signer.as_ref()); 2468 assert!(result.is_err()); 2469 assert!(result 2470 .unwrap_err() 2471 .to_string() 2472 .contains("resource not found: does_not_exist.jpg")); 2473 } 2474 2475 #[test] 2476 #[cfg(feature = "file_io")] 2477 fn test_data_hash_embeddable_manifest() { 2478 let ap = fixture_path("cloud.jpg"); 2479 2480 let signer = temp_signer(); 2481 2482 let mut manifest = Manifest::new("claim_generator"); 2483 2484 // get a placeholder the manifest 2485 let placeholder = manifest 2486 .data_hash_placeholder(signer.reserve_size(), "jpeg") 2487 .unwrap(); 2488 2489 let temp_dir = tempfile::tempdir().unwrap(); 2490 let output = temp_dir_path(&temp_dir, "boxhash-out.jpg"); 2491 let mut output_file = std::fs::OpenOptions::new() 2492 .read(true) 2493 .write(true) 2494 .create(true) 2495 .truncate(true) 2496 .open(&output) 2497 .unwrap(); 2498 2499 // write a jpeg file with a placeholder for the manifest (returns offset of the placeholder) 2500 let offset = 2501 write_jpeg_placeholder_file(&placeholder, &ap, &mut output_file, None).unwrap(); 2502 2503 // build manifest to insert in the hole 2504 2505 // create an hash exclusion for the manifest 2506 let exclusion = HashRange::new(offset, placeholder.len()); 2507 let exclusions = vec![exclusion]; 2508 2509 let mut dh = DataHash::new("source_hash", "sha256"); 2510 dh.exclusions = Some(exclusions); 2511 2512 let signed_manifest = manifest 2513 .data_hash_embeddable_manifest( 2514 &dh, 2515 signer.as_ref(), 2516 "image/jpeg", 2517 Some(&mut output_file), 2518 ) 2519 .unwrap(); 2520 2521 use std::io::{Seek, SeekFrom, Write}; 2522 2523 // path in new composed manifest 2524 output_file.seek(SeekFrom::Start(offset as u64)).unwrap(); 2525 output_file.write_all(&signed_manifest).unwrap(); 2526 2527 let manifest_store = Reader::from_file(&output).expect("from_file"); 2528 println!("{manifest_store}"); 2529 assert!(manifest_store.validation_status().is_none()); 2530 } 2531 2532 #[cfg(all(feature = "file_io", feature = "openssl_sign"))] 2533 #[actix::test] 2534 async fn test_data_hash_embeddable_manifest_remote_signed() { 2535 let ap = fixture_path("cloud.jpg"); 2536 2537 let signer = temp_remote_signer(); 2538 2539 let mut manifest = Manifest::new("claim_generator"); 2540 2541 // get a placeholder the manifest 2542 let placeholder = manifest 2543 .data_hash_placeholder(signer.reserve_size(), "jpeg") 2544 .unwrap(); 2545 2546 let temp_dir = tempfile::tempdir().unwrap(); 2547 let output = temp_dir_path(&temp_dir, "boxhash-out.jpg"); 2548 let mut output_file = std::fs::OpenOptions::new() 2549 .read(true) 2550 .write(true) 2551 .create(true) 2552 .truncate(true) 2553 .open(&output) 2554 .unwrap(); 2555 2556 // write a jpeg file with a placeholder for the manifest (returns offset of the placeholder) 2557 let offset = 2558 write_jpeg_placeholder_file(&placeholder, &ap, &mut output_file, None).unwrap(); 2559 2560 // build manifest to insert in the hole 2561 2562 // create an hash exclusion for the manifest 2563 let exclusion = HashRange::new(offset, placeholder.len()); 2564 let exclusions = vec![exclusion]; 2565 2566 let mut dh = DataHash::new("source_hash", "sha256"); 2567 dh.exclusions = Some(exclusions); 2568 2569 let signed_manifest = manifest 2570 .data_hash_embeddable_manifest_remote( 2571 &dh, 2572 signer.as_ref(), 2573 "c2pa", // force an uncomposed manifest - you could send this to the cloud 2574 Some(&mut output_file), 2575 ) 2576 .await 2577 .unwrap(); 2578 2579 // test composed manifest here to ensure it works 2580 let signed_manifest = 2581 Manifest::composed_manifest(&signed_manifest, "image/jpeg").expect("composed_manifest"); 2582 use std::io::{Seek, SeekFrom, Write}; 2583 2584 // path in new composed manifest 2585 output_file.seek(SeekFrom::Start(offset as u64)).unwrap(); 2586 output_file.write_all(&signed_manifest).unwrap(); 2587 2588 let manifest_store = Reader::from_file(&output).expect("from_file"); 2589 println!("{manifest_store}"); 2590 assert!(manifest_store.validation_status().is_none()); 2591 } 2592 2593 #[test] 2594 #[cfg(feature = "file_io")] 2595 fn test_box_hash_embeddable_manifest() { 2596 let asset_bytes = include_bytes!("../tests/fixtures/boxhash.jpg"); 2597 let box_hash_data = include_bytes!("../tests/fixtures/boxhash.json"); 2598 let box_hash: crate::assertions::BoxHash = serde_json::from_slice(box_hash_data).unwrap(); 2599 2600 let mut manifest = Manifest::new("test_app".to_owned()); 2601 manifest.set_title("BoxHashTest").set_format("image/jpeg"); 2602 2603 manifest 2604 .add_labeled_assertion(crate::assertions::labels::BOX_HASH, &box_hash) 2605 .unwrap(); 2606 2607 let signer = temp_signer(); 2608 2609 let embeddable = manifest 2610 .box_hash_embeddable_manifest(signer.as_ref(), None) 2611 .expect("embeddable_manifest"); 2612 2613 // Validate the embeddable manifest against the asset bytes 2614 let reader = Reader::from_manifest_data_and_stream( 2615 &embeddable, 2616 "image/jpeg", 2617 Cursor::new(asset_bytes), 2618 ) 2619 .unwrap(); 2620 println!("{reader}"); 2621 assert!(reader.active_manifest().is_some()); 2622 assert!(reader.validation_status().is_none()); 2623 } 2624 }