diff --git a/export_schema/src/main.rs b/export_schema/src/main.rs index a1136ddec..b1bdb25f9 100644 --- a/export_schema/src/main.rs +++ b/export_schema/src/main.rs @@ -1,7 +1,7 @@ use std::{fs, path::Path}; use anyhow::Result; -use c2pa::{settings::Settings, ManifestDefinition, ManifestStore}; +use c2pa::{settings::Settings, Builder, ManifestDefinition, ManifestStore}; use schemars::{schema::RootSchema, schema_for}; fn write_schema(schema: &RootSchema, name: &str) { @@ -15,6 +15,9 @@ fn write_schema(schema: &RootSchema, name: &str) { } fn main() -> Result<()> { + let builder = schema_for!(Builder); + write_schema(&builder, "Builder"); + let manifest_definition = schema_for!(ManifestDefinition); write_schema(&manifest_definition, "ManifestDefinition"); diff --git a/make_test_images/src/make_test_images.rs b/make_test_images/src/make_test_images.rs index 3af2281f1..303b84e24 100644 --- a/make_test_images/src/make_test_images.rs +++ b/make_test_images/src/make_test_images.rs @@ -23,7 +23,7 @@ use anyhow::{Context, Result}; use c2pa::{ create_signer, jumbf_io::{get_supported_types, load_jumbf_from_stream, save_jumbf_to_stream}, - Builder, Error, Reader, Signer, SigningAlg, + Builder, Error, Ingredient, Reader, Relationship, Signer, SigningAlg, }; use memchr::memmem; use nom::AsBytes; @@ -208,7 +208,7 @@ impl MakeTestImages { fn add_ingredient_from_file( builder: &mut Builder, path: &Path, - relationship: &str, + relationship: Relationship, ) -> Result { let mut source = fs::File::open(path).context("opening ingredient")?; let name = path @@ -222,20 +222,18 @@ impl MakeTestImages { .into_owned(); let format = extension_to_mime(&extension).unwrap_or("image/jpeg"); - let json = json!({ - "title": name, - "relationship": relationship, - }) - .to_string(); - - let ingredient = builder.add_ingredient(&json, format, &mut source)?; - if ingredient.thumbnail_ref().is_none() { + let mut parent = Ingredient::from_stream(format, &mut source)?; + parent.set_relationship(relationship); + parent.set_title(name); + if parent.thumbnail_ref().is_none() { source.rewind()?; let (format, thumbnail) = make_thumbnail_from_stream(format, &mut source).context("making thumbnail")?; - ingredient.set_thumbnail(format, thumbnail)?; + parent.set_thumbnail(format, thumbnail)?; } + builder.add_ingredient(parent); + Ok( builder.definition.ingredients[builder.definition.ingredients.len() - 1] .instance_id() @@ -300,7 +298,7 @@ impl MakeTestImages { let src_path = &self.make_path(src); let instance_id = - Self::add_ingredient_from_file(&mut builder, src_path, "parentOf")?; + Self::add_ingredient_from_file(&mut builder, src_path, Relationship::ParentOf)?; actions.push(json!( { @@ -376,8 +374,11 @@ impl MakeTestImages { let instance_id = match ingredient_table.get(ing.as_str()) { Some(id) => id.to_string(), None => { - let instance_id = - Self::add_ingredient_from_file(&mut builder, ing_path, "componentOf")?; + let instance_id = Self::add_ingredient_from_file( + &mut builder, + ing_path, + Relationship::ComponentOf, + )?; ingredient_table.insert(ing, instance_id.clone()); instance_id } @@ -490,7 +491,7 @@ impl MakeTestImages { let mut builder = Builder::from_json(&json)?; let parent_name = file_name(&dst_path).ok_or(Error::BadParam("no filename".to_string()))?; - builder.add_ingredient( + builder.add_ingredient_from_stream( json!({ "title": parent_name, "relationship": "parentOf" diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 81da4618b..d2e5e0f30 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -89,6 +89,7 @@ config = { version = "0.14.0", default-features = false, features = [ conv = "0.3.3" coset = "0.3.1" extfmt = "0.1.1" +ed25519-dalek = "2.1.1" fast-xml = "0.23.1" hex = "0.4.3" # Version 1.13.0 doesn't compile under Rust < 1.75, pinning to 1.12.0 @@ -141,7 +142,6 @@ openssl = { version = "0.10.61", features = ["vendored"], optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] console_log = { version = "1.0.0", features = ["color"] } -ed25519-dalek = "2.1.1" getrandom = { version = "0.2.7", features = ["js"] } # We need to use the `inaccurate` flag here to ensure usage of the JavaScript Date API # to handle certificate timestamp checking correctly. @@ -176,5 +176,4 @@ wasm-bindgen-test = "0.3.31" [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] actix = "0.13.1" -ed25519-dalek = "2.1.1" tokio = { version = "1.36.0", features = ["full"] } diff --git a/sdk/examples/client/client.rs b/sdk/examples/client/client.rs index 7dba38c23..7f2b7e834 100644 --- a/sdk/examples/client/client.rs +++ b/sdk/examples/client/client.rs @@ -18,18 +18,18 @@ use std::path::PathBuf; use anyhow::Result; use c2pa::{ assertions::{c2pa_action, labels, Action, Actions, CreativeWork, Exif, SchemaDotOrgPerson}, - create_signer, Ingredient, Manifest, Reader as ManifestStore, SigningAlg, + create_signer, Builder, ClaimGeneratorInfo, Ingredient, Reader, Relationship, SigningAlg, }; const GENERATOR: &str = "test_app/0.1"; const INDENT_SPACE: usize = 2; // Example for reading the contents of a manifest store, recursively showing nested manifests -fn show_manifest(manifest_store: &ManifestStore, manifest_label: &str, level: usize) -> Result<()> { +fn show_manifest(reader: &Reader, manifest_label: &str, level: usize) -> Result<()> { let indent = " ".repeat(level * INDENT_SPACE); println!("{indent}manifest_label: {manifest_label}"); - if let Some(manifest) = manifest_store.get_manifest(manifest_label) { + if let Some(manifest) = reader.get_manifest(manifest_label) { println!( "{}title: {} , format: {}, instance_id: {}", indent, @@ -66,8 +66,17 @@ fn show_manifest(manifest_store: &ManifestStore, manifest_label: &str, level: us for ingredient in manifest.ingredients().iter() { println!("{}Ingredient title:{}", indent, ingredient.title()); + if let Some(validation_status) = ingredient.validation_status() { + for status in validation_status { + println!( + "Ingredient validation status: {}: {}", + status.code(), + status.explanation().unwrap_or_default() + ); + } + } if let Some(label) = ingredient.active_manifest() { - show_manifest(manifest_store, label, level + 1)?; + show_manifest(reader, label, level + 1)?; } } } @@ -88,11 +97,11 @@ pub fn main() -> Result<()> { let source = PathBuf::from(src); let dest = PathBuf::from(dst); // if a filepath was provided on the command line, read it as a parent file - let parent = Ingredient::from_file(source.as_path())?; - + let mut parent = Ingredient::from_file(source.as_path())?; + parent.set_relationship(Relationship::ParentOf); // create an action assertion stating that we imported this file let actions = Actions::new().add_action( - Action::new(c2pa_action::PLACED) + Action::new(c2pa_action::OPENED) .set_parameter("identifier", parent.instance_id().to_owned())?, ); @@ -115,31 +124,38 @@ pub fn main() -> Result<()> { )?; // create a new Manifest - let mut manifest = Manifest::new(GENERATOR.to_owned()); - // add parent and assertions - manifest - .set_parent(parent)? - .add_assertion(&actions)? - .add_assertion(&creative_work)? - .add_assertion(&exif)?; + let mut builder = Builder::new(); + builder + .set_claim_generator_info(ClaimGeneratorInfo::new(GENERATOR)) + .add_ingredient(parent) + .add_assertion(Actions::LABEL, &actions)? + .add_assertion(CreativeWork::LABEL, &creative_work)? + .add_assertion(Exif::LABEL, &exif)?; // sign and embed into the target file let signcert_path = "sdk/tests/fixtures/certs/es256.pub"; let pkey_path = "sdk/tests/fixtures/certs/es256.pem"; let signer = create_signer::from_files(signcert_path, pkey_path, SigningAlg::Es256, None)?; - manifest.embed(&source, &dest, &*signer)?; + builder.sign_file(&*signer, &source, &dest)?; - let manifest_store = ManifestStore::from_file(&dest)?; + let reader = Reader::from_file(&dest)?; // example of how to print out the whole manifest as json - println!("{manifest_store}\n"); - - // walk through the manifest and access data. + println!("{reader}\n"); - if let Some(manifest_label) = manifest_store.active_label() { - show_manifest(&manifest_store, manifest_label, 0)?; + // walk through the manifests and show the contents + if let Some(manifest_label) = reader.active_label() { + show_manifest(&reader, manifest_label, 0)?; + } + if let Some(validation_status) = reader.validation_status() { + for status in validation_status { + println!( + "Validation status: {}: {}", + status.code(), + status.explanation().unwrap_or_default() + ); + } } - Ok(()) } diff --git a/sdk/examples/data_hash.rs b/sdk/examples/data_hash.rs index 5bdcf31bc..3f2825e34 100644 --- a/sdk/examples/data_hash.rs +++ b/sdk/examples/data_hash.rs @@ -16,68 +16,44 @@ #[cfg(not(target_arch = "wasm32"))] use std::{ - io::{Read, Seek, Write}, - path::PathBuf, + io::{Cursor, Read, Seek, Write}, + path::{Path, PathBuf}, }; #[cfg(not(target_arch = "wasm32"))] use c2pa::{ - assertions::{c2pa_action, Action, Actions, CreativeWork, DataHash, Exif, SchemaDotOrgPerson}, - create_signer, hash_stream_by_alg, HashRange, Ingredient, Manifest, ManifestStore, SigningAlg, + assertions::{ + c2pa_action, labels::*, Action, Actions, CreativeWork, DataHash, Exif, SchemaDotOrgPerson, + }, + create_signer, hash_stream_by_alg, Builder, ClaimGeneratorInfo, HashRange, Ingredient, Reader, + Relationship, Result, SigningAlg, }; -fn main() { +fn main() -> std::result::Result<(), Box> { println!("DataHash demo"); #[cfg(not(target_arch = "wasm32"))] - user_data_hash_with_sdk_hashing(); - + user_data_hash_with_sdk_hashing()?; + println!("Done with SDK hashing1"); #[cfg(not(target_arch = "wasm32"))] - user_data_hash_with_user_hashing(); + user_data_hash_with_user_hashing()?; + println!("Done with SDK hashing2"); + Ok(()) } #[cfg(not(target_arch = "wasm32"))] -fn user_data_hash_with_sdk_hashing() { - const GENERATOR: &str = "test_app/0.1"; - - // You will often implement your own Signer trait to perform on device signing - let signcert_path = "sdk/tests/fixtures/certs/es256.pub"; - let pkey_path = "sdk/tests/fixtures/certs/es256.pem"; - let signer = - create_signer::from_files(signcert_path, pkey_path, SigningAlg::Es256, None).unwrap(); - - let src = "sdk/tests/fixtures/earth_apollo17.jpg"; - let dst = "target/tmp/output.jpg"; - - let source = PathBuf::from(src); - let dest = PathBuf::from(dst); - - let mut input_file = std::fs::OpenOptions::new() - .read(true) - .open(&source) - .unwrap(); - - let mut output_file = std::fs::OpenOptions::new() - .read(true) - .write(true) - .create(true) - .truncate(true) - .open(&dest) - .unwrap(); - - let parent = Ingredient::from_file(source.as_path()).unwrap(); - +fn builder_from_source>(source: S) -> Result { + let mut parent = Ingredient::from_file(source.as_ref())?; + parent.set_relationship(Relationship::ParentOf); // create an action assertion stating that we imported this file let actions = Actions::new().add_action( Action::new(c2pa_action::PLACED) - .set_parameter("identifier", parent.instance_id().to_owned()) - .unwrap(), + .set_parameter("identifier", parent.instance_id().to_owned())?, ); // build a creative work assertion - let creative_work = CreativeWork::new() - .add_author(SchemaDotOrgPerson::new().set_name("me").unwrap()) - .unwrap(); + let creative_work = + CreativeWork::new().add_author(SchemaDotOrgPerson::new().set_name("me")?)?; let exif = Exif::from_json_str( r#"{ @@ -91,161 +67,125 @@ fn user_data_hash_with_sdk_hashing() { "exif:GPSAltitude": "100963/29890", "exif:GPSTimeStamp": "2019-09-22T18:22:57Z" }"#, - ) - .unwrap(); - - // create a new Manifest - let mut manifest = Manifest::new(GENERATOR.to_owned()); - // add parent and assertions - manifest - .set_parent(parent) - .unwrap() - .add_assertion(&actions) - .unwrap() - .add_assertion(&creative_work) - .unwrap() - .add_assertion(&exif) - .unwrap(); + )?; - // get the composed manifest ready to insert into a file (returns manifest of same length as finished manifest) - let unfinished_manifest = manifest - .data_hash_placeholder(signer.reserve_size(), "jpg") - .unwrap(); + let mut builder = Builder::default(); - // Figure out where you want to put the manifest, let's put it at the beginning of the JPEG as first segment - // generate new file inserting unfinished manifest into file - input_file.rewind().unwrap(); - let mut before = vec![0u8; 2]; - input_file.read_exact(before.as_mut_slice()).unwrap(); + let mut claim_generator = ClaimGeneratorInfo::new("test_app".to_string()); + claim_generator.set_version("0.1"); - output_file.write_all(&before).unwrap(); + builder + .set_claim_generator_info(claim_generator) + .add_ingredient(parent) + .add_assertion(ACTIONS, &actions)? + .add_assertion_json(CREATIVE_WORK, &creative_work)? + .add_assertion_json(EXIF, &exif)?; - // write completed final manifest - output_file.write_all(&unfinished_manifest).unwrap(); + Ok(builder) +} - // write bytes after - let mut after_buf = Vec::new(); - input_file.read_to_end(&mut after_buf).unwrap(); - output_file.write_all(&after_buf).unwrap(); +#[cfg(not(target_arch = "wasm32"))] +fn user_data_hash_with_sdk_hashing() -> Result<()> { + // You will often implement your own Signer trait to perform on device signing + let signcert_path = "sdk/tests/fixtures/certs/es256.pub"; + let pkey_path = "sdk/tests/fixtures/certs/es256.pem"; + let signer = create_signer::from_files(signcert_path, pkey_path, SigningAlg::Es256, None)?; + + let src = "sdk/tests/fixtures/earth_apollo17.jpg"; + + let source = PathBuf::from(src); + + let mut builder = builder_from_source(&source)?; // c2pa::Builder::from_manifest_definition(manifest_definition(&source)?); + + let placeholder_manifest = + builder.data_hashed_placeholder(signer.reserve_size(), "image/jpeg")?; + + let bytes = std::fs::read(&source)?; + let mut output: Vec = Vec::with_capacity(bytes.len() + placeholder_manifest.len()); + + // Generate new file inserting unfinished manifest into file. + // Figure out where you want to put the manifest. + // Here we put it at the beginning of the JPEG as first segment after the 2 byte SOI marker. + let manifest_pos = 2; + output.extend_from_slice(&bytes[0..manifest_pos]); + output.extend_from_slice(&placeholder_manifest); + output.extend_from_slice(&bytes[manifest_pos..]); + + // make a stream from the output bytes + let mut output_stream = Cursor::new(output); // we need to add a data hash that excludes the manifest let mut dh = DataHash::new("my_manifest", "sha265"); - let hr = HashRange::new(2, unfinished_manifest.len()); - dh.add_exclusion(hr); + let hr = HashRange::new(manifest_pos, placeholder_manifest.len()); + dh.add_exclusion(hr.clone()); + + // Hash the bytes excluding the manifest we inserted + let hash = hash_stream_by_alg("sha256", &mut output_stream, Some([hr].to_vec()), true)?; + dh.set_hash(hash); // tell SDK to fill in the hash and sign to complete the manifest - output_file.rewind().unwrap(); - let final_manifest = manifest - .data_hash_embeddable_manifest(&dh, signer.as_ref(), "jpg", Some(&mut output_file)) - .unwrap(); + let final_manifest = builder.sign_data_hashed_embeddable(signer.as_ref(), &dh, "image/jpeg")?; // replace temporary manifest with final signed manifest // move to location where we inserted manifest, // note: temporary manifest and final manifest will be the same size - output_file.seek(std::io::SeekFrom::Start(2)).unwrap(); + output_stream.seek(std::io::SeekFrom::Start(2))?; // write completed final manifest bytes over temporary bytes - output_file.write_all(&final_manifest).unwrap(); + output_stream.write_all(&final_manifest)?; - // make sure the output file is correct - let manifest_store = ManifestStore::from_file(&dest).unwrap(); + output_stream.rewind()?; + // make sure the output stream is correct + let reader = Reader::from_stream("image/jpeg", &mut output_stream)?; // example of how to print out the whole manifest as json - println!("{manifest_store}\n"); + println!("{reader}\n"); + + Ok(()) } #[cfg(not(target_arch = "wasm32"))] -fn user_data_hash_with_user_hashing() { - const GENERATOR: &str = "test_app/0.1"; - +fn user_data_hash_with_user_hashing() -> Result<()> { // You will often implement your own Signer trait to perform on device signing let signcert_path = "sdk/tests/fixtures/certs/es256.pub"; let pkey_path = "sdk/tests/fixtures/certs/es256.pem"; - let signer = - create_signer::from_files(signcert_path, pkey_path, SigningAlg::Es256, None).unwrap(); + let signer = create_signer::from_files(signcert_path, pkey_path, SigningAlg::Es256, None)?; let src = "sdk/tests/fixtures/earth_apollo17.jpg"; - let dst = "target/tmp/output.jpg"; + let dst = "target/tmp/output_hashed.jpg"; let source = PathBuf::from(src); let dest = PathBuf::from(dst); - let mut input_file = std::fs::OpenOptions::new() - .read(true) - .open(&source) - .unwrap(); + let mut input_file = std::fs::OpenOptions::new().read(true).open(&source)?; let mut output_file = std::fs::OpenOptions::new() .read(true) .write(true) .create(true) .truncate(true) - .open(&dest) - .unwrap(); - - let parent = Ingredient::from_file(source.as_path()).unwrap(); - - // create an action assertion stating that we imported this file - let actions = Actions::new().add_action( - Action::new(c2pa_action::PLACED) - .set_parameter("identifier", parent.instance_id().to_owned()) - .unwrap(), - ); - - // build a creative work assertion - let creative_work = CreativeWork::new() - .add_author(SchemaDotOrgPerson::new().set_name("me").unwrap()) - .unwrap(); - - let exif = Exif::from_json_str( - r#"{ - "@context" : { - "exif": "http://ns.adobe.com/exif/1.0/" - }, - "exif:GPSVersionID": "2.2.0.0", - "exif:GPSLatitude": "39,21.102N", - "exif:GPSLongitude": "74,26.5737W", - "exif:GPSAltitudeRef": 0, - "exif:GPSAltitude": "100963/29890", - "exif:GPSTimeStamp": "2019-09-22T18:22:57Z" - }"#, - ) - .unwrap(); - - // create a new Manifest - let mut manifest = Manifest::new(GENERATOR.to_owned()); - // add parent and assertions - manifest - .set_parent(parent) - .unwrap() - .add_assertion(&actions) - .unwrap() - .add_assertion(&creative_work) - .unwrap() - .add_assertion(&exif) - .unwrap(); + .open(&dest)?; + let mut builder = builder_from_source(&source)?; // get the composed manifest ready to insert into a file (returns manifest of same length as finished manifest) - let unfinished_manifest = manifest - .data_hash_placeholder(signer.reserve_size(), "jpg") - .unwrap(); + let placeholder_manifest = + builder.data_hashed_placeholder(signer.reserve_size(), "image/jpeg")?; // Figure out where you want to put the manifest, let's put it at the beginning of the JPEG as first segment // we will need to add a data hash that excludes the manifest let mut dh = DataHash::new("my_manifest", "sha265"); - let hr = HashRange::new(2, unfinished_manifest.len()); + let hr = HashRange::new(2, placeholder_manifest.len()); dh.add_exclusion(hr); // since the only thing we are excluding in this example is the manifest we can just hash all the bytes // if you have additional exclusions you can add them to the DataHash and pass them to this function to be ' // excluded from the hash generation - let hash = hash_stream_by_alg("sha256", &mut input_file, None, true).unwrap(); + let hash = hash_stream_by_alg("sha256", &mut input_file, None, true)?; dh.set_hash(hash); - // tell SDK to fill we will provide the hash and sign to complete the manifest - let final_manifest = manifest - .data_hash_embeddable_manifest(&dh, signer.as_ref(), "jpg", None) - .unwrap(); + // tell SDK to fill in the hash and sign to complete the manifest + let final_manifest: Vec = + builder.sign_data_hashed_embeddable(signer.as_ref(), &dh, "image/jpeg")?; // generate new file inserting final manifest into file input_file.rewind().unwrap(); @@ -263,8 +203,11 @@ fn user_data_hash_with_user_hashing() { output_file.write_all(&after_buf).unwrap(); // make sure the output file is correct - let manifest_store = ManifestStore::from_file(&dest).unwrap(); + output_file.rewind()?; + let reader = Reader::from_stream("image/jpeg", output_file)?; // example of how to print out the whole manifest as json - println!("{manifest_store}\n"); + println!("{reader}\n"); + + Ok(()) } diff --git a/sdk/examples/v2api.rs b/sdk/examples/v2api.rs index 4577c5c97..98fd203d3 100644 --- a/sdk/examples/v2api.rs +++ b/sdk/examples/v2api.rs @@ -87,7 +87,7 @@ fn main() -> Result<()> { let json = manifest_def(title, format); let mut builder = Builder::from_json(&json)?; - builder.add_ingredient( + builder.add_ingredient_from_stream( json!({ "title": parent_name, "relationship": "parentOf" @@ -122,7 +122,8 @@ fn main() -> Result<()> { // unzip the manifest builder from the zipped stream zipped.rewind()?; - let ed_signer = |_context: *const (), data: &[u8]| ed_sign(data, PRIVATE_KEY); + let ed_signer = + |_context: *const (), data: &[u8]| CallbackSigner::ed25519_sign(data, PRIVATE_KEY); let signer = CallbackSigner::new(ed_signer, SigningAlg::Ed25519, CERTS); let mut builder = Builder::from_archive(&mut zipped)?; @@ -155,23 +156,6 @@ fn main() -> Result<()> { Ok(()) } -// Sign the data using the Ed25519 algorithm -fn ed_sign(data: &[u8], private_key: &[u8]) -> c2pa::Result> { - use ed25519_dalek::{Signature, Signer, SigningKey}; - use pem::parse; - - // Parse the PEM data to get the private key - let pem = parse(private_key).map_err(|e| c2pa::Error::OtherError(Box::new(e)))?; - // For Ed25519, the key is 32 bytes long, so we skip the first 16 bytes of the PEM data - let key_bytes = &pem.contents()[16..]; - let signing_key = - SigningKey::try_from(key_bytes).map_err(|e| c2pa::Error::OtherError(Box::new(e)))?; - // Sign the data - let signature: Signature = signing_key.sign(data); - - Ok(signature.to_bytes().to_vec()) -} - // #[cfg(feature = "openssl")] // use openssl::{error::ErrorStack, pkey::PKey}; // #[cfg(feature = "openssl")] diff --git a/sdk/src/assertions/exif.rs b/sdk/src/assertions/exif.rs index fe2eca008..c406de81a 100644 --- a/sdk/src/assertions/exif.rs +++ b/sdk/src/assertions/exif.rs @@ -36,6 +36,9 @@ pub struct Exif { } impl Exif { + // A label for our assertion, use reverse domain name syntax + pub const LABEL: &'static str = labels::EXIF; + pub fn new() -> Self { Self { object_context: Some(json!({ diff --git a/sdk/src/assertions/mod.rs b/sdk/src/assertions/mod.rs index 570e28ad9..67865ae53 100644 --- a/sdk/src/assertions/mod.rs +++ b/sdk/src/assertions/mod.rs @@ -33,7 +33,8 @@ pub use exif::Exif; #[allow(dead_code)] // will become public later mod ingredient; -pub(crate) use ingredient::{Ingredient, Relationship}; +pub(crate) use ingredient::Ingredient; +pub use ingredient::Relationship; pub mod labels; diff --git a/sdk/src/builder.rs b/sdk/src/builder.rs index eacec53c0..80ac24980 100644 --- a/sdk/src/builder.rs +++ b/sdk/src/builder.rs @@ -21,25 +21,29 @@ use std::{ use async_generic::async_generic; #[cfg(feature = "json_schema")] use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_with::skip_serializing_none; use uuid::Uuid; use zip::{write::FileOptions, ZipArchive, ZipWriter}; use crate::{ - assertion::AssertionBase, + assertion::AssertionDecodeError, assertions::{ labels, Actions, CreativeWork, DataHash, Exif, Metadata, SoftwareAgent, Thumbnail, User, UserCbor, }, claim::Claim, error::{Error, Result}, - ingredient::Ingredient, + // manifest_definition::{AssertionData, AssertionDefinition, ManifestDefinition}, resource_store::{ResourceRef, ResourceResolver, ResourceStore}, salt::DefaultSalt, store::Store, utils::mime::format_to_mime, - AsyncSigner, ClaimGeneratorInfo, HashRange, Signer, + AsyncSigner, + ClaimGeneratorInfo, + HashRange, + Ingredient, + Signer, }; /// Version of the Builder Archive file @@ -124,10 +128,6 @@ pub struct AssertionDefinition { pub label: String, pub data: AssertionData, } - -use serde::de::DeserializeOwned; - -use crate::assertion::AssertionDecodeError; impl AssertionDefinition { pub(crate) fn to_assertion(&self) -> Result { match &self.data { @@ -206,6 +206,7 @@ impl AssertionDefinition { /// ``` #[skip_serializing_none] #[derive(Debug, Default, Deserialize, Serialize)] +#[cfg_attr(feature = "json_schema", derive(JsonSchema))] pub struct Builder { #[serde(flatten)] pub definition: ManifestDefinition, @@ -232,6 +233,13 @@ impl AsRef for Builder { } impl Builder { + /// Creates a new builder. + /// # Returns + /// * A new [`Builder`]. + pub fn new() -> Self { + Default::default() + } + /// Creates a new builder from a JSON [`ManifestDefinition`] string. /// /// # Arguments @@ -245,6 +253,15 @@ impl Builder { }) } + /// Sets the ClaimGeneratorInfo for this [`Builder`] + pub fn set_claim_generator_info(&mut self, claim_generator_info: I) -> &mut Self + where + I: Into, + { + self.definition.claim_generator_info = [claim_generator_info.into()].to_vec(); + self + } + /// Sets the MIME format for this [`Builder`]. /// /// # Arguments @@ -349,7 +366,7 @@ impl Builder { Ok(self) } - /// Adds an [`Ingredient`] to the manifest + /// Adds an [`Ingredient`] to the manifest with JSON and a stream. /// # Arguments /// * `ingredient_json` - A JSON string representing the [`Ingredient`]. /// * `format` - The format of the [`Ingredient`]. @@ -359,7 +376,7 @@ impl Builder { /// # Errors /// * If the [`Ingredient`] is not valid #[async_generic()] - pub fn add_ingredient<'a, T, R>( + pub fn add_ingredient_from_stream<'a, T, R>( &'a mut self, ingredient_json: T, format: &str, @@ -380,6 +397,15 @@ impl Builder { Ok(self.definition.ingredients.last_mut().unwrap()) // ok since we just added it } + /// Adds an [`Ingredient`] to the manifest from an existing Ingredient. + pub fn add_ingredient(&mut self, ingredient: I) -> &mut Self + where + I: Into, + { + self.definition.ingredients.push(ingredient.into()); + self + } + /// Adds a resource to the manifest. /// The id should match up with an identifier in the manifest. /// # Arguments @@ -811,6 +837,8 @@ impl Builder { } self.add_assertion(labels::DATA_HASH, &ph)?; } + self.definition.format = format.to_string(); + self.definition.instance_id = format!("xmp:iid:{}", Uuid::new_v4()); let mut store = self.to_store()?; let placeholder = store.get_data_hashed_manifest_placeholder(reserve_size, format)?; Ok(placeholder) @@ -835,30 +863,19 @@ impl Builder { signer: &dyn AsyncSigner, data_hash: &DataHash, format: &str, - source: Option<&mut R>, ))] - pub fn sign_data_hashed_embeddable( + pub fn sign_data_hashed_embeddable( &mut self, signer: &dyn Signer, data_hash: &DataHash, format: &str, - source: Option<&mut R>, - ) -> Result> - where - R: Read + Seek + Send, - { - let format = format_to_mime(format); - self.definition.format.clone_from(&format); - // todo:: read instance_id from xmp from stream ? - self.definition.instance_id = format!("xmp:iid:{}", Uuid::new_v4()); - + ) -> Result> { let mut store = self.to_store()?; - let source: Option<&mut dyn crate::CAIRead> = source.map(|s| s as &mut dyn crate::CAIRead); if _sync { - store.get_data_hashed_embeddable_manifest(data_hash, signer, &format, source) + store.get_data_hashed_embeddable_manifest(data_hash, signer, format, None) } else { store - .get_data_hashed_embeddable_manifest_async(data_hash, signer, &format, source) + .get_data_hashed_embeddable_manifest_async(data_hash, signer, format, None) .await } } @@ -986,8 +1003,12 @@ impl Builder { "Destination file already exists".to_string(), )); }; + if self.definition.title.is_none() { + if let Some(title) = dest.file_name() { + self.definition.title = Some(title.to_string_lossy().to_string()); + } + } let mut dest = std::fs::File::create(dest)?; - self.sign(signer, &format, &mut source, &mut dest) } } @@ -1006,6 +1027,7 @@ mod tests { use crate::{ assertions::BoxHash, asset_handlers::jpeg_io::JpegIO, + hash_stream_by_alg, utils::test::{temp_signer, write_jpeg_placeholder_stream}, Reader, }; @@ -1104,7 +1126,7 @@ mod tests { }; builder - .add_ingredient(parent_json(), "image/jpeg", &mut image) + .add_ingredient_from_stream(parent_json(), "image/jpeg", &mut image) .unwrap(); builder @@ -1186,7 +1208,7 @@ mod tests { let mut builder = Builder::from_json(&manifest_json()).unwrap(); builder - .add_ingredient(parent_json().to_string(), format, &mut source) + .add_ingredient_from_stream(parent_json().to_string(), format, &mut source) .unwrap(); builder @@ -1294,7 +1316,7 @@ mod tests { let mut builder = Builder::from_json(&manifest_json()).unwrap(); builder - .add_ingredient(parent_json(), format, &mut source) + .add_ingredient_from_stream(parent_json(), format, &mut source) .unwrap(); builder @@ -1341,7 +1363,7 @@ mod tests { let mut builder = Builder::from_json(&manifest_json()).unwrap(); builder - .add_ingredient(parent_json(), format, &mut source) + .add_ingredient_from_stream(parent_json(), format, &mut source) .unwrap(); builder @@ -1438,15 +1460,15 @@ mod tests { let mut dh = DataHash::new("source_hash", "sha256"); dh.exclusions = Some(exclusions); - // get the embeddable manifest, letting API do the hashing + // Hash the bytes excluding the manifest we inserted output_stream.rewind().unwrap(); + let hash = + hash_stream_by_alg("sha256", &mut output_stream, dh.exclusions.clone(), true).unwrap(); + dh.set_hash(hash); + + // get the embeddable manifest, letting API do the hashing let signed_manifest: Vec = builder - .sign_data_hashed_embeddable( - signer.as_ref(), - &dh, - "image/jpeg", - Some(&mut output_stream), - ) + .sign_data_hashed_embeddable(signer.as_ref(), &dh, "image/jpeg") .unwrap(); use std::io::{Seek, SeekFrom, Write}; diff --git a/sdk/src/callback_signer.rs b/sdk/src/callback_signer.rs index 5ad36368e..b3f4a6112 100644 --- a/sdk/src/callback_signer.rs +++ b/sdk/src/callback_signer.rs @@ -88,6 +88,38 @@ impl CallbackSigner { self.context = context; self } + + /// Sign data using an Ed25519 private key. + /// This static function is provided for testing with [`CallbackSigner`]. + /// For a released product the private key should be stored securely. + /// The signing should be done in a secure environment. + /// The private key should not be exposed to the client. + /// Example: (only for testing) + /// ``` + /// use c2pa::{CallbackSigner, SigningAlg}; + /// + /// const CERTS: &[u8] = include_bytes!("../tests/fixtures/certs/ed25519.pub"); + /// const PRIVATE_KEY: &[u8] = include_bytes!("../tests/fixtures/certs/ed25519.pem"); + /// + /// let ed_signer = + /// |_context: *const _, data: &[u8]| CallbackSigner::ed25519_sign(data, PRIVATE_KEY); + /// let signer = CallbackSigner::new(ed_signer, SigningAlg::Ed25519, CERTS); + /// ``` + pub fn ed25519_sign(data: &[u8], private_key: &[u8]) -> Result> { + use ed25519_dalek::{Signature, Signer, SigningKey}; + use pem::parse; + + // Parse the PEM data to get the private key + let pem = parse(private_key).map_err(|e| Error::OtherError(Box::new(e)))?; + // For Ed25519, the key is 32 bytes long, so we skip the first 16 bytes of the PEM data + let key_bytes = &pem.contents()[16..]; + let signing_key = + SigningKey::try_from(key_bytes).map_err(|e| Error::OtherError(Box::new(e)))?; + // Sign the data + let signature: Signature = signing_key.sign(data); + + Ok(signature.to_bytes().to_vec()) + } } // This default is only intended for struct completion, do not use on its own. diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 5c7156c84..e6b14f97e 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -113,10 +113,11 @@ pub mod validation_status; pub mod wasm; // Public exports +pub use assertions::Relationship; #[cfg(feature = "v1_api")] pub use asset_io::{CAIRead, CAIReadWrite}; #[cfg(feature = "unstable_api")] -pub use builder::{AssertionDefinition, Builder, ManifestDefinition}; +pub use builder::{Builder, ManifestDefinition}; pub use callback_signer::{CallbackFunc, CallbackSigner}; pub use claim_generator_info::ClaimGeneratorInfo; pub use error::{Error, Result}; diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs index e4c5127e1..1e88d815b 100644 --- a/sdk/src/manifest.rs +++ b/sdk/src/manifest.rs @@ -1050,6 +1050,7 @@ impl Manifest { /// # } /// ``` #[cfg(feature = "file_io")] + #[deprecated(since = "0.35.0", note = "use Builder.sign_file instead")] pub fn embed>( &mut self, source_path: P, @@ -1068,6 +1069,8 @@ impl Manifest { /// Embed a signed manifest into a stream using a supplied signer. /// returns the bytes of the manifest that was embedded + #[allow(deprecated)] + #[deprecated(since = "0.35.0", note = "use Builder.sign with Cursor instead")] #[async_generic(async_signature( &mut self, format: &str, @@ -1097,7 +1100,7 @@ impl Manifest { /// Embed a signed manifest into a stream using a supplied signer. /// /// Returns the bytes of the new asset - #[deprecated(since = "0.27.2", note = "use embed_to_stream instead")] + #[deprecated(since = "0.35.0", note = "obsolete test")] pub fn embed_stream( &mut self, format: &str, @@ -1116,6 +1119,7 @@ impl Manifest { /// Embed a signed manifest into a stream using a supplied signer. /// /// Returns the bytes of c2pa_manifest that was embedded. + #[allow(deprecated)] #[async_generic(async_signature( &mut self, format: &str, @@ -1162,6 +1166,10 @@ impl Manifest { /// Embed a signed manifest into a stream using a supplied signer. /// returns the asset generated and bytes of the manifest that was embedded //#[cfg(feature = "remote_wasm_sign")] + #[deprecated( + since = "0.35.0", + note = "use Builder.sign with memory Cursor and direct_cose_handling signer instead" + )] pub async fn embed_from_memory_remote_signed( &mut self, format: &str, @@ -1200,6 +1208,7 @@ impl Manifest { /// Embed a signed manifest into the target file using a supplied [`AsyncSigner`]. #[cfg(feature = "file_io")] + #[deprecated(since = "0.35.0", note = "use Builder.sign_file_async instead")] pub async fn embed_async_signed>( &mut self, source_path: P, @@ -1218,6 +1227,10 @@ impl Manifest { /// Embed a signed manifest into the target file using a supplied [`RemoteSigner`]. #[cfg(feature = "file_io")] + #[deprecated( + since = "0.35.0", + note = "use Builder.sign file with cose_handling enabled." + )] pub async fn embed_remote_signed>( &mut self, source_path: P, @@ -1274,6 +1287,10 @@ impl Manifest { /// This is used to create a properly formatted file ready for signing. /// The reserve_size is the amount of space to reserve for the signature box. This /// value is fixed once set and must be sufficient to hold the completed signature + #[deprecated( + since = "0.35.0", + note = "use Builder.sign_data_hashed_placeholder instead" + )] pub fn data_hash_placeholder(&mut self, reserve_size: usize, format: &str) -> Result> { let dh: Result = self.find_assertion(DataHash::LABEL); if dh.is_err() { @@ -1296,6 +1313,10 @@ impl Manifest { /// This can directly replace a placeholder manifest to create a properly signed asset /// The data hash must contain exclusions and may contain pre-calculated hashes /// if an asset reader is provided, it will be used to calculate the data hash + #[deprecated( + since = "0.35.0", + note = "use Builder.sign_data_hashed_embeddable instead" + )] #[async_generic(async_signature( &mut self, dh: &DataHash, @@ -1330,6 +1351,10 @@ impl Manifest { /// This can directly replace a placeholder manifest to create a properly signed asset /// The data hash must contain exclusions and may contain pre-calculated hashes /// if an asset reader is provided, it will be used to calculate the data hash + #[deprecated( + since = "0.35.0", + note = "use Builder.sign_data_hashed_embeddable instead" + )] pub async fn data_hash_embeddable_manifest_remote( &mut self, dh: &DataHash, @@ -1349,6 +1374,10 @@ impl Manifest { /// Generates a signed box hashed manifest, optionally preformatted for embedding /// /// The manifest must include a box hash assertion with correct hashes + #[deprecated( + since = "0.35.0", + note = "use Builder.sign_box_hashed_embeddable instead" + )] #[async_generic(async_signature( &mut self, signer: &dyn AsyncSigner, @@ -1514,6 +1543,7 @@ pub(crate) mod tests { #[test] #[cfg(feature = "file_io")] + #[allow(deprecated)] fn from_file() { let mut manifest = test_manifest(); let source_path = fixture_path(TEST_SMALL_JPEG); @@ -1707,6 +1737,7 @@ pub(crate) mod tests { #[test] #[cfg(feature = "file_io")] + #[allow(deprecated)] fn test_redaction() { const ASSERTION_LABEL: &str = "stds.schema-org.CreativeWork"; @@ -1783,6 +1814,7 @@ pub(crate) mod tests { #[test] #[cfg(feature = "file_io")] + #[allow(deprecated)] fn test_action_assertion_redaction_error() { let temp_dir = tempdir().expect("temp dir"); let parent_output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG); @@ -1851,6 +1883,7 @@ pub(crate) mod tests { #[cfg(all(feature = "file_io", feature = "openssl_sign"))] #[actix::test] + #[allow(deprecated)] async fn test_embed_async_sign() { let temp_dir = tempdir().expect("temp dir"); let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG); @@ -1872,6 +1905,7 @@ pub(crate) mod tests { #[cfg(all(feature = "file_io", feature = "openssl_sign"))] #[actix::test] + #[allow(deprecated)] async fn test_embed_remote_sign() { let temp_dir = tempdir().expect("temp dir"); let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG); @@ -1892,6 +1926,7 @@ pub(crate) mod tests { #[cfg(feature = "file_io")] #[test] + #[allow(deprecated)] fn test_embed_user_label() { let temp_dir = tempdir().expect("temp dir"); let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG); @@ -1913,6 +1948,7 @@ pub(crate) mod tests { #[cfg(feature = "file_io")] #[test] + #[allow(deprecated)] fn test_embed_sidecar_user_label() { let temp_dir = tempdir().expect("temp dir"); let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG); @@ -1939,6 +1975,7 @@ pub(crate) mod tests { #[cfg_attr(not(target_arch = "wasm32"), actix::test)] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[allow(deprecated)] async fn test_embed_jpeg_stream_wasm() { use crate::assertions::User; let image = include_bytes!("../tests/fixtures/earth_apollo17.jpg"); @@ -1978,6 +2015,7 @@ pub(crate) mod tests { #[cfg_attr(not(target_arch = "wasm32"), actix::test)] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[allow(deprecated)] async fn test_embed_png_stream_wasm() { use crate::assertions::User; let image = include_bytes!("../tests/fixtures/libpng-test.png"); @@ -2010,6 +2048,7 @@ pub(crate) mod tests { #[cfg_attr(not(target_arch = "wasm32"), actix::test)] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[allow(deprecated)] async fn test_embed_webp_stream_wasm() { use crate::assertions::User; let image = include_bytes!("../tests/fixtures/mars.webp"); @@ -2119,6 +2158,7 @@ pub(crate) mod tests { #[cfg(feature = "file_io")] #[actix::test] + #[allow(deprecated)] /// Verify that an ingredient with error is reported on the ingredient and not on the manifest_store async fn test_embed_with_ingredient_error() { let temp_dir = tempdir().expect("temp dir"); @@ -2152,6 +2192,7 @@ pub(crate) mod tests { #[cfg(feature = "file_io")] #[test] + #[allow(deprecated)] fn test_embed_sidecar_with_parent_manifest() { let temp_dir = tempdir().expect("temp dir"); let source = fixture_path("XCA.jpg"); @@ -2182,6 +2223,7 @@ pub(crate) mod tests { #[cfg(feature = "file_io")] #[test] + #[allow(deprecated)] fn test_embed_user_thumbnail() { let temp_dir = tempdir().expect("temp dir"); let output = temp_fixture_path(&temp_dir, TEST_SMALL_JPEG); @@ -2389,6 +2431,7 @@ pub(crate) mod tests { #[test] #[cfg(feature = "openssl_sign")] + #[allow(deprecated)] /// tests and illustrates how to add assets to a non-file based manifest by using a memory buffer fn from_json_with_memory() { use crate::assertions::Relationship; @@ -2471,6 +2514,7 @@ pub(crate) mod tests { #[cfg(feature = "file_io")] #[test] + #[allow(deprecated)] fn test_embed_from_json() { let mut fixtures = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); fixtures.push("tests/fixtures"); // the path we want to read files from @@ -2495,6 +2539,7 @@ pub(crate) mod tests { #[cfg(feature = "file_io")] #[test] + #[allow(deprecated)] fn test_embed_webp_from_json() { use crate::utils::test::TEST_WEBP; @@ -2521,6 +2566,7 @@ pub(crate) mod tests { #[test] #[cfg(feature = "file_io")] + #[allow(deprecated)] fn test_create_file_based_ingredient() { let mut fixtures = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); fixtures.push("tests/fixtures"); @@ -2549,6 +2595,7 @@ pub(crate) mod tests { #[test] #[cfg(all(feature = "file_io", feature = "add_thumbnails"))] + #[allow(deprecated)] fn test_create_no_claim_thumbnail() { let mut fixtures = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); fixtures.push("tests/fixtures"); @@ -2608,6 +2655,7 @@ pub(crate) mod tests { #[test] #[cfg(feature = "file_io")] + #[allow(deprecated)] fn test_data_hash_embeddable_manifest() { let ap = fixture_path("cloud.jpg"); @@ -2665,6 +2713,7 @@ pub(crate) mod tests { #[cfg(all(feature = "file_io", feature = "openssl_sign"))] #[actix::test] + #[allow(deprecated)] async fn test_data_hash_embeddable_manifest_remote_signed() { let ap = fixture_path("cloud.jpg"); @@ -2723,6 +2772,7 @@ pub(crate) mod tests { #[test] #[cfg(feature = "file_io")] + #[allow(deprecated)] fn test_box_hash_embeddable_manifest() { let asset_bytes = include_bytes!("../tests/fixtures/boxhash.jpg"); let box_hash_data = include_bytes!("../tests/fixtures/boxhash.json"); diff --git a/sdk/src/reader.rs b/sdk/src/reader.rs index 8e7e6205b..59efab706 100644 --- a/sdk/src/reader.rs +++ b/sdk/src/reader.rs @@ -97,15 +97,28 @@ impl Reader { } else { Self::from_stream_async(&format, &mut file).await }; - if let Err(Error::JumbfNotFound) = result { - // if not embedded or cloud, check for sidecar first and load if it exists - let potential_sidecar_path = path.with_extension("c2pa"); - if potential_sidecar_path.exists() { - let manifest_data = read(potential_sidecar_path)?; - return Self::from_manifest_data_and_stream(&manifest_data, &format, &mut file); + match result { + Err(Error::JumbfNotFound) => { + // if not embedded or cloud, check for sidecar first and load if it exists + let potential_sidecar_path = path.with_extension("c2pa"); + if potential_sidecar_path.exists() { + let manifest_data = read(potential_sidecar_path)?; + if _sync { + Self::from_manifest_data_and_stream(&manifest_data, &format, &mut file) + } else { + Self::from_manifest_data_and_stream_async( + &manifest_data, + &format, + &mut file, + ) + .await + } + } else { + Err(Error::JumbfNotFound) + } } + _ => result, } - result } /// Create a manifest store [`Reader`]` from a JSON string. diff --git a/sdk/tests/integration.rs b/sdk/tests/integration.rs index e2b834e23..0e0067432 100644 --- a/sdk/tests/integration.rs +++ b/sdk/tests/integration.rs @@ -21,12 +21,11 @@ mod integration_1 { assertions::{c2pa_action, Action, Actions}, create_signer, settings::load_settings_from_str, - Error, Ingredient, Manifest, ManifestPatchCallback, ManifestStore, Result, Signer, - SigningAlg, + Builder, ClaimGeneratorInfo, Ingredient, Reader, Result, Signer, SigningAlg, }; use tempfile::tempdir; - const GENERATOR: &str = "app"; + //const GENERATOR: &str = "app"; // prevent tests from polluting the results of each other because of Rust unit test concurrency static PROTECT: std::sync::Mutex = std::sync::Mutex::new(1); @@ -110,14 +109,17 @@ mod integration_1 { Some(String::from_utf8_lossy(config).to_string()), )?; + let generator = ClaimGeneratorInfo::new("app"); // create a new Manifest - let mut manifest = Manifest::new(GENERATOR.to_owned()); + let mut builder = Builder::new(); + builder.set_claim_generator_info(generator); // allocate actions so we can add them let mut actions = Actions::new(); // add a parent ingredient - let parent = Ingredient::from_file(&parent_path)?; + let mut parent = Ingredient::from_file(&parent_path)?; + parent.set_is_parent(); // add an action assertion stating that we imported this file actions = actions.add_action( Action::new(c2pa_action::EDITED) @@ -127,7 +129,7 @@ mod integration_1 { ); // set the parent ingredient - manifest.set_parent(parent)?; + builder.add_ingredient(parent); actions = actions.add_action( Action::new("c2pa.edit").set_parameter("name".to_owned(), "brightnesscontrast")?, @@ -144,21 +146,21 @@ mod integration_1 { // could add other parameters for position and size here ); - manifest.add_ingredient(ingredient); + builder.add_ingredient(ingredient); - manifest.add_assertion(&actions)?; + builder.add_assertion(Actions::LABEL, &actions)?; // sign and embed into the target file let signer = get_temp_signer(); - manifest.embed(&parent_path, &output_path, signer.as_ref())?; + builder.sign_file(signer.as_ref(), &parent_path, &output_path)?; // read our new file with embedded manifest - let manifest_store = ManifestStore::from_file(&output_path)?; + let reader = Reader::from_file(&output_path)?; - println!("{manifest_store}"); + println!("{reader}"); - assert!(manifest_store.get_active().is_some()); - if let Some(manifest) = manifest_store.get_active() { + assert!(reader.active_manifest().is_some()); + if let Some(manifest) = reader.active_manifest() { assert!(manifest.title().is_some()); assert_eq!(manifest.ingredients().len(), 2); } else { @@ -184,21 +186,21 @@ mod integration_1 { let json = std::fs::read_to_string(manifest_path)?; - let mut manifest = Manifest::from_json(&json)?; - manifest.with_base_path(fixture_path.canonicalize()?)?; + let mut builder = Builder::from_json(&json)?; + builder.base_path = Some(fixture_path.canonicalize()?); // sign and embed into the target file let signer = get_temp_signer(); - manifest.embed(&parent_path, &output_path, signer.as_ref())?; + builder.sign_file(signer.as_ref(), &parent_path, &output_path)?; // read our new file with embedded manifest - let manifest_store = ManifestStore::from_file(&output_path)?; + let reader = Reader::from_file(&output_path)?; - println!("{manifest_store}"); + println!("{reader}"); // std::fs::copy(&output_path, "test_file.jpg")?; // for debugging to get copy of the file - assert!(manifest_store.get_active().is_some()); - if let Some(manifest) = manifest_store.get_active() { + assert!(reader.active_manifest().is_some()); + if let Some(manifest) = reader.active_manifest() { assert!(manifest.title().is_some()); assert_eq!(manifest.ingredients().len(), 2); } else { @@ -211,6 +213,8 @@ mod integration_1 { path: String, } + use c2pa::{Error, Manifest, ManifestPatchCallback}; + impl ManifestPatchCallback for PlacedCallback { fn patch_manifest(&self, manifest_store: &[u8]) -> Result> { use ::jumbf::parser::SuperBox; diff --git a/sdk/tests/v2_api_integration.rs b/sdk/tests/v2_api_integration.rs index 4c5964a27..0e2b51b1f 100644 --- a/sdk/tests/v2_api_integration.rs +++ b/sdk/tests/v2_api_integration.rs @@ -90,7 +90,7 @@ mod integration_v2 { // don't try to verify on wasm since it doesn't support ed25519 yet let mut builder = Builder::from_json(&json)?; - builder.add_ingredient(PARENT_JSON, format, &mut source)?; + builder.add_ingredient_from_stream(PARENT_JSON, format, &mut source)?; // add a manifest thumbnail ( just reuse the image for now ) source.rewind()?;