builder.rs (42370B)
1 // Copyright 2024 Adobe. All rights reserved. 2 // This file is licensed to you under the Apache License, 3 // Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 4 // or the MIT license (http://opensource.org/licenses/MIT), 5 // at your option. 6 7 // Unless required by applicable law or agreed to in writing, 8 // this software is distributed on an "AS IS" BASIS, WITHOUT 9 // WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or 10 // implied. See the LICENSE-MIT and LICENSE-APACHE files for the 11 // specific language governing permissions and limitations under 12 // each license. 13 14 use std::{ 15 collections::HashMap, 16 io::{Read, Seek, Write}, 17 }; 18 19 use async_generic::async_generic; 20 use serde::{de::DeserializeOwned, Deserialize, Serialize}; 21 use serde_with::skip_serializing_none; 22 use uuid::Uuid; 23 use zip::{write::SimpleFileOptions, ZipArchive, ZipWriter}; 24 25 use crate::{ 26 assertion::{AssertionBase, AssertionDecodeError}, 27 assertions::{labels, Actions, CreativeWork, Exif, SoftwareAgent, Thumbnail, User, UserCbor}, 28 claim::Claim, 29 error::{Error, Result}, 30 ingredient::Ingredient, 31 resource_store::{ResourceRef, ResourceResolver, ResourceStore}, 32 salt::DefaultSalt, 33 store::Store, 34 utils::mime::format_to_mime, 35 AsyncSigner, ClaimGeneratorInfo, Signer, 36 }; 37 38 /// A Manifest Definition 39 /// This is used to define a manifest and is used to build a ManifestStore 40 /// A Manifest is a collection of ingredients and assertions 41 /// It is used to define a claim that can be signed and embedded into a file 42 #[skip_serializing_none] 43 #[derive(Debug, Default, Deserialize, Serialize)] 44 #[non_exhaustive] 45 pub struct ManifestDefinition { 46 /// Optional prefix added to the generated Manifest Label 47 /// This is typically Internet domain name for the vendor (i.e. `adobe`) 48 pub vendor: Option<String>, 49 50 /// Clam Generator Info is always required with at least one entry 51 #[serde(default = "default_claim_generator_info")] 52 pub claim_generator_info: Vec<ClaimGeneratorInfo>, 53 54 /// A human-readable title, generally source filename. 55 pub title: Option<String>, 56 57 /// The format of the source file as a MIME type. 58 #[serde(default = "default_format")] 59 pub format: String, 60 61 /// Instance ID from `xmpMM:InstanceID` in XMP metadata. 62 #[serde(default = "default_instance_id")] 63 pub instance_id: String, 64 65 pub thumbnail: Option<ResourceRef>, 66 67 /// A List of ingredients 68 #[serde(default = "default_vec::<Ingredient>")] 69 pub ingredients: Vec<Ingredient>, 70 71 /// A list of assertions 72 #[serde(default = "default_vec::<AssertionDefinition>")] 73 pub assertions: Vec<AssertionDefinition>, 74 75 /// A list of redactions - URIs to a redacted assertions 76 pub redactions: Option<Vec<String>>, 77 78 pub label: Option<String>, 79 } 80 81 fn default_instance_id() -> String { 82 format!("xmp:iid:{}", Uuid::new_v4()) 83 } 84 85 fn default_claim_generator_info() -> Vec<ClaimGeneratorInfo> { 86 [ClaimGeneratorInfo::default()].to_vec() 87 } 88 89 fn default_format() -> String { 90 "application/octet-stream".to_owned() 91 } 92 93 const fn default_vec<T>() -> Vec<T> { 94 Vec::new() 95 } 96 97 #[derive(Debug, Deserialize, Serialize, Clone)] 98 #[serde(untagged)] 99 pub enum AssertionData { 100 Cbor(serde_cbor::Value), 101 Json(serde_json::Value), 102 } 103 104 #[derive(Debug, Deserialize, Serialize, Clone)] 105 #[non_exhaustive] 106 pub struct AssertionDefinition { 107 pub label: String, 108 pub data: AssertionData, 109 } 110 111 impl AssertionDefinition { 112 pub(crate) fn to_assertion<T: DeserializeOwned>(&self) -> Result<T> { 113 match &self.data { 114 AssertionData::Json(value) => serde_json::from_value(value.clone()).map_err(|e| { 115 Error::AssertionDecoding(AssertionDecodeError::from_err( 116 self.label.to_owned(), 117 None, 118 "application/json".to_owned(), 119 e, 120 )) 121 }), 122 AssertionData::Cbor(value) => { 123 serde_cbor::value::from_value(value.clone()).map_err(|e| { 124 Error::AssertionDecoding(AssertionDecodeError::from_err( 125 self.label.to_owned(), 126 None, 127 "application/cbor".to_owned(), 128 e, 129 )) 130 }) 131 } 132 } 133 } 134 } 135 136 /// A Builder is used to add a signed manifest to an asset. 137 /// 138 /// # Example: Building and signing a manifest 139 /// 140 /// ``` 141 /// # use c2pa::Result; 142 /// use std::path::PathBuf; 143 /// 144 /// use c2pa::{create_signer, Builder, SigningAlg}; 145 /// use serde::Serialize; 146 /// use serde_json::json; 147 /// use tempfile::tempdir; 148 /// 149 /// #[derive(Serialize)] 150 /// struct Test { 151 /// my_tag: usize, 152 /// } 153 /// 154 /// # fn main() -> Result<()> { 155 /// let manifest_json = json!({ 156 /// "claim_generator_info": [ 157 /// { 158 /// "name": "c2pa_test", 159 /// "version": "1.0.0" 160 /// } 161 /// ], 162 /// "title": "Test_Manifest" 163 /// }).to_string(); 164 /// 165 /// let mut builder = Builder::from_json(&manifest_json)?; 166 /// builder.add_assertion("org.contentauth.test", &Test { my_tag: 42 })?; 167 /// 168 /// let source = PathBuf::from("tests/fixtures/C.jpg"); 169 /// let dir = tempdir()?; 170 /// let dest = dir.path().join("test_file.jpg"); 171 /// 172 /// // Create a ps256 signer using certs and key files 173 /// let signcert_path = "tests/fixtures/certs/ps256.pub"; 174 /// let pkey_path = "tests/fixtures/certs/ps256.pem"; 175 /// let signer = create_signer::from_files(signcert_path, pkey_path, SigningAlg::Ps256, None)?; 176 /// 177 /// // embed a manifest using the signer 178 /// builder.sign( 179 /// signer.as_ref(), 180 /// "image/jpeg", 181 /// &mut std::fs::File::open(&source)?, 182 /// &mut std::fs::File::create(&dest)?, 183 /// )?; 184 /// # Ok(()) 185 /// # } 186 /// ``` 187 #[skip_serializing_none] 188 #[derive(Debug, Default, Deserialize, Serialize)] 189 pub struct Builder { 190 #[serde(flatten)] 191 pub definition: ManifestDefinition, 192 193 /// Optional remote URL for the manifest 194 pub remote_url: Option<String>, 195 196 // If true, the manifest store will not be embedded in the asset on sign 197 pub no_embed: bool, 198 199 /// container for binary assets (like thumbnails) 200 #[serde(skip)] 201 resources: ResourceStore, 202 } 203 204 impl AsRef<Builder> for Builder { 205 fn as_ref(&self) -> &Self { 206 self 207 } 208 } 209 210 impl Builder { 211 /// Creates a new builder from a JSON [`ManifestDefinition`] string. 212 /// 213 /// # Arguments 214 /// * `json` - A JSON string representing the [`ManifestDefinition`]. 215 /// # Returns 216 /// * A new [`Builder`]. 217 pub fn from_json(json: &str) -> Result<Self> { 218 Ok(Self { 219 definition: serde_json::from_str(json).map_err(Error::JsonError)?, 220 ..Default::default() 221 }) 222 } 223 224 /// Sets the MIME format for this [`Builder`]. 225 /// 226 /// # Arguments 227 /// * `format` - The format of the asset associated with this [`Builder`]. 228 /// # Returns 229 /// * A mutable reference to the [`Builder`]. 230 pub fn set_format(&mut self, format: &str) -> &mut Self { 231 self.definition.format = format.to_string(); 232 self 233 } 234 235 /// Sets a thumbnail for the [`Builder`]. 236 /// 237 /// The thumbnail should represent the associated asset for this [`Builder`]. 238 /// 239 /// # Arguments 240 /// * `format` - The format of the thumbnail. 241 /// * `stream` - A stream to read the thumbnail from. 242 /// # Returns 243 /// * A mutable reference to the [`Builder`]. 244 /// # Errors 245 /// * If the thumbnail is not valid. 246 pub fn set_thumbnail<R>(&mut self, format: &str, stream: &mut R) -> Result<&mut Self> 247 where 248 R: Read + Seek + ?Sized, 249 { 250 // just read into a buffer until resource store handles reading streams 251 let mut resource = Vec::new(); 252 stream.read_to_end(&mut resource)?; 253 // add the resource and set the resource reference 254 self.resources 255 .add(self.definition.instance_id.clone(), resource)?; 256 self.definition.thumbnail = Some(ResourceRef::new( 257 format, 258 self.definition.instance_id.clone(), 259 )); 260 Ok(self) 261 } 262 263 /// Adds a CBOR assertion to the manifest. 264 /// # Arguments 265 /// * `label` - A label for the assertion. 266 /// * `data` - The data for the assertion. The data is any Serde Serializable type. 267 /// # Returns 268 /// * A mutable reference to the [`Builder`]. 269 /// # Errors 270 /// * If the assertion is not valid. 271 pub fn add_assertion<S, T>(&mut self, label: S, data: &T) -> Result<&mut Self> 272 where 273 S: Into<String>, 274 T: Serialize, 275 { 276 self.definition.assertions.push(AssertionDefinition { 277 label: label.into(), 278 data: AssertionData::Cbor(serde_cbor::value::to_value(data)?), 279 }); 280 Ok(self) 281 } 282 283 /// Adds a Json assertion to the manifest. 284 /// # Arguments 285 /// * `label` - A label for the assertion. 286 /// * `data` - The data for the assertion. The data is any Serde Serializable type. 287 /// # Returns 288 /// * A mutable reference to the [`Builder`]. 289 /// # Errors 290 /// * If the assertion is not valid. 291 pub fn add_assertion_json<S, T>(&mut self, label: S, data: &T) -> Result<&mut Self> 292 where 293 S: Into<String>, 294 T: Serialize, 295 { 296 self.definition.assertions.push(AssertionDefinition { 297 label: label.into(), 298 data: AssertionData::Json(serde_json::to_value(data)?), 299 }); 300 Ok(self) 301 } 302 303 /// Adds an [`Ingredient`] to the manifest 304 /// # Arguments 305 /// * `ingredient_json` - A JSON string representing the [`Ingredient`]. 306 /// * `format` - The format of the [`Ingredient`]. 307 /// * `stream` - A stream to read the [`Ingredient`] from. 308 /// # Returns 309 /// * A mutable reference to the [`Ingredient`]. 310 /// # Errors 311 /// * If the [`Ingredient`] is not valid 312 pub fn add_ingredient<'a, T, R>( 313 &'a mut self, 314 ingredient_json: T, 315 format: &str, 316 stream: &mut R, 317 ) -> Result<&'a mut Ingredient> 318 where 319 T: Into<String>, 320 R: Read + Seek + Send, 321 { 322 let ingredient: Ingredient = Ingredient::from_json(&ingredient_json.into())?; 323 let ingredient = ingredient.with_stream(format, stream)?; 324 self.definition.ingredients.push(ingredient); 325 #[allow(clippy::unwrap_used)] 326 Ok(self.definition.ingredients.last_mut().unwrap()) // ok since we just added it 327 } 328 329 /// Adds a resource to the manifest. 330 /// The id should match up with an identifier in the manifest. 331 /// # Arguments 332 /// * `id` - The identifier for the resource. 333 /// * `stream` - A stream to read the resource from. 334 /// # Returns 335 /// * A mutable reference to the builder. 336 /// # Errors 337 /// * If the resource is not valid. 338 pub fn add_resource( 339 &mut self, 340 id: &str, 341 mut stream: impl Read + Seek + Send, 342 ) -> Result<&mut Self> { 343 if self.resources.exists(id) { 344 return Err(Error::BadParam(id.to_string())); // todo add specific error 345 } 346 let mut buf = Vec::new(); 347 let _size = stream.read_to_end(&mut buf)?; 348 self.resources.add(id, buf)?; 349 Ok(self) 350 } 351 352 /// Convert the Builder into a archive formatted stream. 353 /// 354 /// The archive is a zip formatted stream containing the manifest.json, resources, and ingredients. 355 /// # Arguments 356 /// * `stream` - A stream to write the zip into. 357 /// # Errors 358 /// * If the archive cannot be written. 359 pub fn to_archive(&mut self, stream: impl Write + Seek) -> Result<()> { 360 drop( 361 // this drop seems to be required to force a flush before reading back. 362 { 363 let mut zip = ZipWriter::new(stream); 364 let options = 365 SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored); 366 zip.start_file("manifest.json", options) 367 .map_err(|e| Error::OtherError(Box::new(e)))?; 368 zip.write_all(&serde_json::to_vec(self)?)?; 369 // add a folder to the zip file 370 zip.start_file("resources/", options) 371 .map_err(|e| Error::OtherError(Box::new(e)))?; 372 for (id, data) in self.resources.resources() { 373 zip.start_file(format!("resources/{}", id), options) 374 .map_err(|e| Error::OtherError(Box::new(e)))?; 375 zip.write_all(data)?; 376 } 377 for (index, ingredient) in self.definition.ingredients.iter().enumerate() { 378 zip.start_file(format!("ingredients/{}/", index), options) 379 .map_err(|e| Error::OtherError(Box::new(e)))?; 380 for (id, data) in ingredient.resources().resources() { 381 //println!("adding ingredient {}/{}", index, id); 382 zip.start_file(format!("ingredients/{}/{}", index, id), options) 383 .map_err(|e| Error::OtherError(Box::new(e)))?; 384 zip.write_all(data)?; 385 } 386 } 387 zip.finish() 388 } 389 .map_err(|e| Error::OtherError(Box::new(e)))?, 390 ); 391 Ok(()) 392 } 393 394 /// Unpacks an archive stream into a Builder. 395 /// # Arguments 396 /// * `stream` - A stream to read the archive from. 397 /// # Returns 398 /// * A new Builder. 399 /// # Errors 400 /// * If the archive cannot be read. 401 pub fn from_archive(stream: impl Read + Seek) -> Result<Self> { 402 let mut zip = ZipArchive::new(stream).map_err(|e| Error::OtherError(Box::new(e)))?; 403 let mut manifest = zip 404 .by_name("manifest.json") 405 .map_err(|e| Error::OtherError(Box::new(e)))?; 406 let mut manifest_json = Vec::new(); 407 manifest.read_to_end(&mut manifest_json)?; 408 let mut builder: Builder = 409 serde_json::from_slice(&manifest_json).map_err(|e| Error::OtherError(Box::new(e)))?; 410 drop(manifest); 411 for i in 0..zip.len() { 412 let mut file = zip 413 .by_index(i) 414 .map_err(|e| Error::OtherError(Box::new(e)))?; 415 416 if file.name().starts_with("resources/") && file.name() != "resources/" { 417 let mut data = Vec::new(); 418 file.read_to_end(&mut data)?; 419 let id = file 420 .name() 421 .split('/') 422 .nth(1) 423 .ok_or(Error::BadParam("Invalid resource path".to_string()))?; 424 //println!("adding resource {}", id); 425 builder.resources.add(id, data)?; 426 } 427 if file.name().starts_with("ingredients/") && file.name() != "ingredients/" { 428 let mut data = Vec::new(); 429 file.read_to_end(&mut data)?; 430 let index: usize = file 431 .name() 432 .split('/') 433 .nth(1) 434 .ok_or_else(|| Error::BadParam("Invalid ingredient path".to_string()))? 435 .parse::<usize>() 436 .map_err(|_| Error::BadParam("Invalid ingredient path".to_string()))?; 437 let id = file.name().split('/').nth(2).unwrap_or_default(); 438 if index >= builder.definition.ingredients.len() { 439 return Err(Error::OtherError(Box::new(std::io::Error::new( 440 std::io::ErrorKind::Other, 441 format!("Invalid ingredient index {}", index), 442 ))))?; // todo add specific error 443 } 444 builder.definition.ingredients[index] 445 .resources_mut() 446 .add(id, data)?; 447 } 448 } 449 Ok(builder) 450 } 451 452 // Convert a Manifest into a Claim 453 fn to_claim(&self) -> Result<Claim> { 454 let definition = &self.definition; 455 let mut claim_generator_info = definition.claim_generator_info.clone(); 456 // add the default claim generator info for this library 457 claim_generator_info.push(ClaimGeneratorInfo::default()); 458 459 // build the claim_generator string since this is required 460 let claim_generator: String = claim_generator_info 461 .iter() 462 .map(|s| { 463 let name = s.name.replace(' ', "_"); 464 if let Some(version) = s.version.as_deref() { 465 format!("{}/{}", name.to_lowercase(), version) 466 } else { 467 name 468 } 469 }) 470 .collect::<Vec<String>>() 471 .join(" "); 472 473 let mut claim = match definition.label.as_ref() { 474 Some(label) => Claim::new_with_user_guid(&claim_generator, &label.to_string()), 475 None => Claim::new(&claim_generator, definition.vendor.as_deref()), 476 }; 477 478 // add claim generator info to claim resolving icons 479 for info in &claim_generator_info { 480 let mut claim_info = info.to_owned(); 481 if let Some(icon) = claim_info.icon.as_ref() { 482 claim_info.icon = Some(icon.to_hashed_uri(&self.resources, &mut claim)?); 483 } 484 claim.add_claim_generator_info(claim_info); 485 } 486 487 if let Some(remote_url) = &self.remote_url { 488 if self.no_embed { 489 claim.set_remote_manifest(remote_url)?; 490 } else { 491 claim.set_embed_remote_manifest(remote_url)?; 492 } 493 } else if self.no_embed { 494 claim.set_external_manifest() 495 } 496 497 if let Some(title) = definition.title.as_ref() { 498 claim.set_title(Some(title.to_owned())); 499 } 500 definition.format.clone_into(&mut claim.format); 501 definition.instance_id.clone_into(&mut claim.instance_id); 502 503 if let Some(thumb_ref) = definition.thumbnail.as_ref() { 504 // Setting the format to "none" will ensure that no claim thumbnail is added 505 if thumb_ref.format != "none" { 506 //let data = self.resources.get(&thumb_ref.identifier)?; 507 let mut stream = self.resources.open(thumb_ref)?; 508 let mut data = Vec::new(); 509 stream.read_to_end(&mut data)?; 510 claim.add_assertion(&Thumbnail::new( 511 &labels::add_thumbnail_format(labels::CLAIM_THUMBNAIL, &thumb_ref.format), 512 data, 513 ))?; 514 } 515 } 516 517 let mut ingredient_map = HashMap::new(); 518 // add all ingredients to the claim 519 for ingredient in &definition.ingredients { 520 //let ingredient = ingredient_builder.build(self)?; 521 let uri = ingredient.add_to_claim( 522 &mut claim, 523 definition.redactions.clone(), 524 Some(&self.resources), 525 )?; 526 ingredient_map.insert(ingredient.instance_id().to_string(), uri); 527 } 528 529 let salt = DefaultSalt::default(); 530 531 // add any additional assertions 532 for manifest_assertion in &definition.assertions { 533 match manifest_assertion.label.as_str() { 534 l if l.starts_with(Actions::LABEL) => { 535 let version = labels::version(l); 536 537 let mut actions: Actions = manifest_assertion.to_assertion()?; 538 539 let ingredients_key = match version { 540 None | Some(1) => "ingredient", 541 Some(2) => "ingredients", 542 _ => return Err(Error::AssertionUnsupportedVersion), 543 }; 544 545 // fixup parameters field from instance_id to ingredient uri 546 let needs_ingredient: Vec<(usize, crate::assertions::Action)> = actions 547 .actions() 548 .iter() 549 .enumerate() 550 .filter_map(|(i, a)| { 551 if a.instance_id().is_some() 552 && a.get_parameter(ingredients_key).is_none() 553 { 554 Some((i, a.clone())) 555 } else { 556 None 557 } 558 }) 559 .collect(); 560 561 for (index, action) in needs_ingredient { 562 if let Some(id) = action.instance_id() { 563 if let Some(hash_url) = ingredient_map.get(id) { 564 let update = match ingredients_key { 565 "ingredient" => { 566 action.set_parameter(ingredients_key, hash_url.clone()) 567 } 568 _ => { 569 // we only support on instanceId for actions, so only one ingredient on writing 570 action.set_parameter(ingredients_key, [hash_url.clone()]) 571 } 572 }?; 573 actions = actions.update_action(index, update); 574 } 575 } 576 } 577 578 if let Some(templates) = actions.templates.as_mut() { 579 for template in templates { 580 // replace icon with hashed_uri 581 template.icon = match template.icon.take() { 582 Some(icon) => { 583 Some(icon.to_hashed_uri(&self.resources, &mut claim)?) 584 } 585 None => None, 586 }; 587 588 // replace software agent with hashed_uri 589 template.software_agent = match template.software_agent.take() { 590 Some(SoftwareAgent::ClaimGeneratorInfo(mut info)) => { 591 if let Some(icon) = info.icon.as_mut() { 592 let icon = 593 icon.to_hashed_uri(&self.resources, &mut claim)?; 594 info.set_icon(icon); 595 } 596 Some(SoftwareAgent::ClaimGeneratorInfo(info)) 597 } 598 agent => agent, 599 }; 600 } 601 } 602 603 // convert icons in software agents to hashed uris 604 let actions_mut = actions.actions_mut(); 605 #[allow(clippy::needless_range_loop)] 606 // clippy is wrong here, we reference index twice 607 for index in 0..actions_mut.len() { 608 let action = &actions_mut[index]; 609 if let Some(SoftwareAgent::ClaimGeneratorInfo(info)) = 610 action.software_agent() 611 { 612 if let Some(icon) = info.icon.as_ref() { 613 let mut info = info.to_owned(); 614 let icon_uri = icon.to_hashed_uri(&self.resources, &mut claim)?; 615 let update = info.set_icon(icon_uri); 616 let mut action = action.to_owned(); 617 action = action.set_software_agent(update.to_owned()); 618 actions_mut[index] = action; 619 } 620 } 621 } 622 623 claim.add_assertion(&actions) 624 } 625 CreativeWork::LABEL => { 626 let cw: CreativeWork = manifest_assertion.to_assertion()?; 627 628 claim.add_assertion_with_salt(&cw, &salt) 629 } 630 Exif::LABEL => { 631 let exif: Exif = manifest_assertion.to_assertion()?; 632 claim.add_assertion_with_salt(&exif, &salt) 633 } 634 _ => match &manifest_assertion.data { 635 AssertionData::Json(value) => claim.add_assertion_with_salt( 636 &User::new(&manifest_assertion.label, &serde_json::to_string(&value)?), 637 &salt, 638 ), 639 AssertionData::Cbor(value) => claim.add_assertion_with_salt( 640 &UserCbor::new(&manifest_assertion.label, serde_cbor::to_vec(value)?), 641 &salt, 642 ), 643 }, 644 }?; 645 } 646 647 Ok(claim) 648 } 649 650 // Convert a Manifest into a Store 651 fn to_store(&self) -> Result<Store> { 652 let claim = self.to_claim()?; 653 // commit the claim 654 let mut store = Store::new(); 655 let _provenance = store.commit_claim(claim)?; 656 Ok(store) 657 } 658 659 #[cfg(feature = "add_thumbnails")] 660 fn maybe_add_thumbnail<R>(&mut self, format: &str, stream: &mut R) -> Result<&mut Self> 661 where 662 R: Read + Seek + ?Sized, 663 { 664 // check settings to see if we should auto generate a thumbnail 665 let auto_thumbnail = crate::settings::get_settings_value::<bool>("builder.auto_thumbnail")?; 666 if self.definition.thumbnail.is_none() && auto_thumbnail { 667 stream.rewind()?; 668 if let Ok((format, image)) = 669 crate::utils::thumbnail::make_thumbnail_from_stream(format, stream) 670 { 671 stream.rewind()?; 672 self.resources 673 .add(self.definition.instance_id.clone(), image)?; 674 self.definition.thumbnail = Some(ResourceRef::new( 675 format, 676 self.definition.instance_id.clone(), 677 )); 678 } 679 } 680 Ok(self) 681 } 682 683 /// Embed a signed manifest into a stream using a supplied signer. 684 /// # Arguments 685 /// * `format` - The format of the stream 686 /// * `source` - The stream to read from 687 /// * `dest` - The stream to write to 688 /// * `signer` - The signer to use 689 /// # Returns 690 /// * The bytes of c2pa_manifest that was embedded. 691 /// # Errors 692 /// * If the manifest cannot be signed. 693 #[async_generic(async_signature( 694 &mut self, 695 signer: &dyn AsyncSigner, 696 format: &str, 697 source: &mut R, 698 dest: &mut W, 699 ))] 700 pub fn sign<R, W>( 701 &mut self, 702 signer: &dyn Signer, 703 format: &str, 704 source: &mut R, 705 dest: &mut W, 706 ) -> Result<Vec<u8>> 707 where 708 R: Read + Seek + Send, 709 W: Write + Read + Seek + Send, 710 { 711 let format = format_to_mime(format); 712 self.definition.format.clone_from(&format); 713 // todo:: read instance_id from xmp from stream ? 714 self.definition.instance_id = format!("xmp:iid:{}", Uuid::new_v4()); 715 716 // generate thumbnail if we don't already have one 717 #[cfg(feature = "add_thumbnails")] 718 self.maybe_add_thumbnail(&format, source)?; 719 720 // convert the manifest to a store 721 let mut store = self.to_store()?; 722 723 // sign and write our store to to the output image file 724 if _sync { 725 store.save_to_stream(&format, source, dest, signer) 726 } else { 727 store 728 .save_to_stream_async(&format, source, dest, signer) 729 .await 730 } 731 } 732 733 #[cfg(feature = "file_io")] 734 /// Sign a file using a supplied signer. 735 /// # Arguments 736 /// * `source` - The path to the file to read from. 737 /// * `dest` - The path to the file to write to (this must not already exist). 738 /// * `signer` - The signer to use. 739 /// # Returns 740 /// * The bytes of c2pa_manifest that was created. 741 /// # Errors 742 /// * If the manifest cannot be signed. 743 pub fn sign_file<S, D>(&mut self, signer: &dyn Signer, source: S, dest: D) -> Result<Vec<u8>> 744 where 745 S: AsRef<std::path::Path>, 746 D: AsRef<std::path::Path>, 747 { 748 let source = source.as_ref(); 749 let dest = dest.as_ref(); 750 // formats must match but allow extensions to be slightly different (i.e. .jpeg vs .jpg)s 751 let format = crate::format_from_path(source).ok_or(crate::Error::UnsupportedType)?; 752 let format_dest = crate::format_from_path(dest).ok_or(crate::Error::UnsupportedType)?; 753 if format != format_dest { 754 return Err(crate::Error::BadParam( 755 "Source and destination file formats must match".to_string(), 756 )); 757 } 758 let mut source = std::fs::File::open(source)?; 759 if !dest.exists() { 760 // ensure the path to the file exists 761 if let Some(output_dir) = dest.parent() { 762 std::fs::create_dir_all(output_dir)?; 763 } 764 } else { 765 // if the file exists, we need to remove it to avoid appending to it 766 return Err(crate::Error::BadParam( 767 "Destination file already exists".to_string(), 768 )); 769 }; 770 let mut dest = std::fs::File::create(dest)?; 771 772 self.sign(signer, &format, &mut source, &mut dest) 773 } 774 } 775 776 #[cfg(test)] 777 mod tests { 778 #![allow(clippy::expect_used)] 779 #![allow(clippy::unwrap_used)] 780 781 use std::io::Cursor; 782 783 use serde_json::json; 784 #[cfg(target_arch = "wasm32")] 785 use wasm_bindgen_test::*; 786 787 use super::*; 788 use crate::{utils::test::temp_signer, Reader}; 789 790 #[cfg(target_arch = "wasm32")] 791 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); 792 793 fn parent_json() -> String { 794 json!({ 795 "title": "Parent Test", 796 "format": "image/jpeg", 797 "instance_id": "12345", 798 "relationship": "parentOf" 799 }) 800 .to_string() 801 } 802 803 fn manifest_json() -> String { 804 json!({ 805 "vendor": "test", 806 "claim_generator_info": [ 807 { 808 "name": "c2pa_test", 809 "version": "1.0.0" 810 } 811 ], 812 "title": "Test_Manifest", 813 "format": "image/tiff", 814 "instance_id": "1234", 815 "thumbnail": { 816 "format": "image/jpeg", 817 "identifier": "thumbnail1.jpg" 818 }, 819 "ingredients": [ 820 { 821 "title": "Test", 822 "format": "image/jpeg", 823 "instance_id": "12345", 824 "relationship": "componentOf" 825 } 826 ], 827 "assertions": [ 828 { 829 "label": "org.test.assertion", 830 "data": "assertion" 831 } 832 ] 833 }) 834 .to_string() 835 } 836 837 #[cfg(not(target_arch = "wasm32"))] 838 const TEST_IMAGE_CLEAN: &[u8] = include_bytes!("../tests/fixtures/IMG_0003.jpg"); 839 const TEST_IMAGE: &[u8] = include_bytes!("../tests/fixtures/CA.jpg"); 840 841 #[test] 842 /// example of creating a builder directly with a [`ManifestDefinition`] 843 fn test_manifest_store_builder() { 844 let mut image = Cursor::new(TEST_IMAGE); 845 846 let thumbnail_ref = ResourceRef::new("ingredient/jpeg", "5678"); 847 848 let definition = ManifestDefinition { 849 vendor: Some("test".to_string()), 850 claim_generator_info: [ClaimGeneratorInfo::default()].to_vec(), 851 format: "image/tiff".to_string(), 852 title: Some("Test_Manifest".to_string()), 853 instance_id: "1234".to_string(), 854 thumbnail: Some(thumbnail_ref.clone()), 855 label: Some("ABCDE".to_string()), 856 ..Default::default() 857 }; 858 859 let mut builder = Builder { 860 definition, 861 ..Default::default() 862 }; 863 864 builder 865 .add_ingredient(parent_json(), "image/jpeg", &mut image) 866 .unwrap(); 867 868 builder 869 .add_assertion("org.test.assertion", &"assertion".to_string()) 870 .unwrap(); 871 872 builder 873 .add_resource(&thumbnail_ref.identifier, Cursor::new(b"12345")) 874 .unwrap(); 875 876 let definition = &builder.definition; 877 assert_eq!(definition.vendor, Some("test".to_string())); 878 assert_eq!(definition.title, Some("Test_Manifest".to_string())); 879 assert_eq!(definition.format, "image/tiff".to_string()); 880 assert_eq!(definition.instance_id, "1234".to_string()); 881 assert_eq!(definition.thumbnail, Some(thumbnail_ref)); 882 assert_eq!(definition.ingredients[0].title(), "Parent Test".to_string()); 883 assert_eq!( 884 definition.assertions[0].label, 885 "org.test.assertion".to_string() 886 ); 887 assert_eq!(definition.label, Some("ABCDE".to_string())); 888 assert_eq!( 889 builder 890 .resources 891 .get(&builder.definition.thumbnail.unwrap().identifier) 892 .unwrap() 893 .into_owned(), 894 b"12345" 895 ); 896 } 897 898 #[test] 899 fn test_from_json() { 900 // strip whitespace so we can compare later 901 let mut stripped_json = manifest_json(); 902 stripped_json.retain(|c| !c.is_whitespace()); 903 let mut builder = Builder::from_json(&stripped_json).unwrap(); 904 builder.resources.add("5678", "12345").unwrap(); 905 let definition = &builder.definition; 906 assert_eq!(definition.vendor, Some("test".to_string())); 907 assert_eq!(definition.title, Some("Test_Manifest".to_string())); 908 assert_eq!(definition.format, "image/tiff".to_string()); 909 assert_eq!(definition.instance_id, "1234".to_string()); 910 assert_eq!( 911 definition.thumbnail.clone().unwrap().identifier.as_str(), 912 "thumbnail1.jpg" 913 ); 914 assert_eq!(definition.ingredients[0].title(), "Test".to_string()); 915 assert_eq!( 916 definition.assertions[0].label, 917 "org.test.assertion".to_string() 918 ); 919 920 // convert back to json and compare to original 921 let builder_json = serde_json::to_string(&builder.definition).unwrap(); 922 assert_eq!(builder_json, stripped_json); 923 } 924 925 #[test] 926 fn test_builder_sign() { 927 #[derive(Serialize, Deserialize)] 928 struct TestAssertion { 929 answer: usize, 930 } 931 let format = "image/jpeg"; 932 let mut source = Cursor::new(TEST_IMAGE); 933 let mut dest = Cursor::new(Vec::new()); 934 935 let mut builder = Builder::from_json(&manifest_json()).unwrap(); 936 builder 937 .add_ingredient(parent_json().to_string(), format, &mut source) 938 .unwrap(); 939 940 builder 941 .resources 942 .add("thumbnail1.jpg", TEST_IMAGE.to_vec()) 943 .unwrap(); 944 945 builder 946 .add_assertion("org.life.meaning", &TestAssertion { answer: 42 }) 947 .unwrap(); 948 949 builder 950 .add_assertion_json("org.life.meaning.json", &TestAssertion { answer: 42 }) 951 .unwrap(); 952 953 // write the manifest builder to a zipped stream 954 let mut zipped = Cursor::new(Vec::new()); 955 builder.to_archive(&mut zipped).unwrap(); 956 957 // write the zipped stream to a file for debugging 958 std::fs::write("../target/test.zip", zipped.get_ref()).unwrap(); 959 960 // unzip the manifest builder from the zipped stream 961 zipped.rewind().unwrap(); 962 let mut _builder = Builder::from_archive(&mut zipped).unwrap(); 963 964 // sign and write to the output stream 965 let signer = temp_signer(); 966 builder 967 .sign(signer.as_ref(), format, &mut source, &mut dest) 968 .unwrap(); 969 970 // read and validate the signed manifest store 971 dest.rewind().unwrap(); 972 let manifest_store = Reader::from_stream(format, &mut dest).expect("from_bytes"); 973 974 println!("{}", manifest_store); 975 assert!(manifest_store.validation_status().is_none()); 976 assert!(manifest_store.active_manifest().is_some()); 977 let manifest = manifest_store.active_manifest().unwrap(); 978 assert_eq!(manifest.title().unwrap(), "Test_Manifest"); 979 let test_assertion: TestAssertion = manifest.find_assertion("org.life.meaning").unwrap(); 980 assert_eq!(test_assertion.answer, 42); 981 } 982 983 #[test] 984 #[cfg(feature = "file_io")] 985 fn test_builder_sign_file() { 986 let source = "tests/fixtures/CA.jpg"; 987 let dir = tempfile::tempdir().unwrap(); 988 let dest = dir.path().join("test_file.jpg"); 989 990 let mut builder = Builder::from_json(&manifest_json()).unwrap(); 991 992 builder 993 .add_resource("thumbnail1.jpg", Cursor::new(TEST_IMAGE)) 994 .unwrap(); 995 996 // sign and write to the output stream 997 let signer = temp_signer(); 998 builder.sign_file(signer.as_ref(), source, &dest).unwrap(); 999 1000 // read and validate the signed manifest store 1001 let manifest_store = Reader::from_file(&dest).expect("from_bytes"); 1002 1003 println!("{}", manifest_store); 1004 assert!(manifest_store.validation_status().is_none()); 1005 assert_eq!( 1006 manifest_store.active_manifest().unwrap().title().unwrap(), 1007 "Test_Manifest" 1008 ); 1009 } 1010 1011 #[test] 1012 #[cfg(feature = "file_io")] 1013 fn test_builder_sign_assets() { 1014 const TESTFILES: &[&str] = &[ 1015 "IMG_0003.jpg", 1016 "sample1.png", 1017 "sample1.webp", 1018 "TUSCANY.TIF", 1019 "sample1.svg", 1020 "sample1.wav", 1021 "test.avi", 1022 "sample1.mp3", 1023 "sample1.avif", 1024 "sample1.heic", 1025 "sample1.heif", 1026 "video1.mp4", 1027 "cloud_manifest.c2pa", 1028 ]; 1029 for file_name in TESTFILES { 1030 let extension = file_name.split('.').last().unwrap(); 1031 let format = extension; 1032 1033 let path = format!("tests/fixtures/{}", file_name); 1034 println!("path: {}", path); 1035 let mut source = std::fs::File::open(path).unwrap(); 1036 let mut dest = Cursor::new(Vec::new()); 1037 1038 let mut builder = Builder::from_json(&manifest_json()).unwrap(); 1039 builder 1040 .add_ingredient(parent_json(), format, &mut source) 1041 .unwrap(); 1042 1043 builder 1044 .add_resource("thumbnail1.jpg", Cursor::new(TEST_IMAGE)) 1045 .unwrap(); 1046 1047 // sign and write to the output stream 1048 let signer = temp_signer(); 1049 builder 1050 .sign(signer.as_ref(), format, &mut source, &mut dest) 1051 .unwrap(); 1052 1053 // read and validate the signed manifest store 1054 dest.rewind().unwrap(); 1055 let manifest_store = Reader::from_stream(format, &mut dest).expect("from_bytes"); 1056 1057 println!("{}", manifest_store); 1058 if format != "c2pa" { 1059 // c2pa files will not validate since they have no associated asset 1060 assert!(manifest_store.validation_status().is_none()); 1061 } 1062 assert_eq!( 1063 manifest_store.active_manifest().unwrap().title().unwrap(), 1064 "Test_Manifest" 1065 ); 1066 1067 // enable to write the signed manifests to a file for debugging 1068 // let dest_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) 1069 // .join("../target") 1070 // .join("signed") 1071 // .join(file_name); 1072 1073 // std::fs::create_dir_all(dest_path.parent().unwrap()).unwrap(); 1074 // std::fs::write(&dest_path, dest.get_ref()).unwrap(); 1075 } 1076 } 1077 1078 #[cfg_attr(not(target_arch = "wasm32"), actix::test)] 1079 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] 1080 async fn test_builder_remote_sign() { 1081 let format = "image/jpeg"; 1082 let mut source = Cursor::new(TEST_IMAGE); 1083 let mut dest = Cursor::new(Vec::new()); 1084 1085 let mut builder = Builder::from_json(&manifest_json()).unwrap(); 1086 builder 1087 .add_ingredient(&parent_json(), format, &mut source) 1088 .unwrap(); 1089 1090 builder 1091 .resources 1092 .add("thumbnail1.jpg", TEST_IMAGE.to_vec()) 1093 .unwrap(); 1094 1095 // sign the ManifestStoreBuilder and write it to the output stream 1096 let signer = crate::utils::test::temp_async_remote_signer(); 1097 builder 1098 .sign_async(signer.as_ref(), format, &mut source, &mut dest) 1099 .await 1100 .unwrap(); 1101 1102 // read and validate the signed manifest store 1103 dest.rewind().unwrap(); 1104 let manifest_store = Reader::from_stream(format, &mut dest).expect("from_bytes"); 1105 1106 println!("{}", manifest_store); 1107 #[cfg(not(target_arch = "wasm32"))] // skip this until we get wasm async signing working 1108 assert!(manifest_store.validation_status().is_none()); 1109 assert_eq!( 1110 manifest_store.active_manifest().unwrap().title().unwrap(), 1111 "Test_Manifest" 1112 ); 1113 } 1114 1115 #[test] 1116 #[cfg(not(target_arch = "wasm32"))] 1117 fn test_builder_remote_url() { 1118 let mut source = Cursor::new(TEST_IMAGE_CLEAN); 1119 let mut dest = Cursor::new(Vec::new()); 1120 1121 let mut builder = Builder::from_json(&manifest_json()).unwrap(); 1122 builder.remote_url = Some("http://my_remote_url".to_string()); 1123 builder.no_embed = true; 1124 1125 builder 1126 .add_resource("thumbnail1.jpg", Cursor::new(TEST_IMAGE)) 1127 .unwrap(); 1128 1129 // sign the ManifestStoreBuilder and write it to the output stream 1130 let signer = temp_signer(); 1131 let manifest_data = builder 1132 .sign(signer.as_ref(), "image/jpeg", &mut source, &mut dest) 1133 .unwrap(); 1134 1135 // check to make sure we have a remote url and no manifest data 1136 dest.set_position(0); 1137 let _err = c2pa::Reader::from_stream("image/jpeg", &mut dest).expect_err("from_bytes"); 1138 1139 // now validate the manifest against the written asset 1140 dest.set_position(0); 1141 let reader = 1142 c2pa::Reader::from_manifest_data_and_stream(&manifest_data, "image/jpeg", &mut dest) 1143 .expect("from_bytes"); 1144 1145 println!("{}", reader.json()); 1146 assert!(reader.validation_status().is_none()); 1147 } 1148 }