c2pa-rs

A fork of https://github.com/contentauth/c2pa-rs/
git clone git://archive.git.mtrnord.blog/mtrnords-photography-manager/c2pa-rs.git
Log | Files | Refs | README

commit 9da7a0785e5b0e0bd0a66f592ddf69ab25c55f30
parent aeb4a32d2928d32bf63332346e2a7f713e839f42
Author: Gavin  Peacock <gpeacock@adobe.com>
Date:   Tue,  7 Jun 2022 20:09:47 -0700

(MINOR) Update ManifestAssertion supporting instances (#34)

* Added support for ManifestAssertion instances and kinds
ManifestAssertion is a new file
Added ManifestAsssertionKind
ManifestAssertion getters & setters and make private
Changed Actions assertion getters to mut self pattern
* Add value and binary getters for manifest_assertion
* Integrations with new signer from main
* respond to review and add docs
Diffstat:
Mc2patool/Cargo.toml | 2+-
Mc2patool/src/main.rs | 8+++++---
Mc2patool/src/signer.rs | 40+++++++---------------------------------
Mc2patool/tests/integration.rs | 2+-
Mmake_test_images/src/make_test_images.rs | 15+++++++--------
Msdk/examples/client/client.rs | 6+++---
Msdk/src/assertions/actions.rs | 7+++----
Msdk/src/assertions/creative_work.rs | 4++--
Msdk/src/lib.rs | 10++++------
Msdk/src/manifest.rs | 254++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Asdk/src/manifest_assertion.rs | 233+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msdk/src/store.rs | 3+--
Msdk/src/utils/test.rs | 3+--
Msdk/tests/integration.rs | 6+++---
14 files changed, 433 insertions(+), 160 deletions(-)

diff --git a/c2patool/Cargo.toml b/c2patool/Cargo.toml @@ -8,7 +8,7 @@ rust-version = "1.58.0" [dependencies] anyhow = "1.0" -c2pa = { version = "0.2", features = ["file_io"] } +c2pa = { path = "../sdk", features = ["file_io"] } env_logger = "0.9" log = "0.4" serde = { version = "1.0", features = ["derive"] } diff --git a/c2patool/src/main.rs b/c2patool/src/main.rs @@ -33,7 +33,7 @@ use tempfile::tempdir; pub mod config; use config::Config; mod signer; -use signer::get_signer; +use signer::get_c2pa_signer; // define the command line options #[derive(Debug, StructOpt)] @@ -85,12 +85,14 @@ fn handle_config( ) -> Result<()> { let config: Config = serde_json::from_str(json)?; + // if the config has a base path, use it for relative paths in the config + // otherwise set the base path to the location of the config file let base_path = match &config.base_path { Some(path) => PathBuf::from(path), None => PathBuf::from(base_dir), }; - let signer = get_signer(&config, &base_path)?; + let signer = get_c2pa_signer(&config, &base_path)?; let claim_generator = match config.claim_generator { Some(claim_generator) => claim_generator, @@ -143,7 +145,7 @@ fn handle_config( // add any assertions for assertion in config.assertions { - manifest.add_labeled_assertion(&assertion.label, &assertion.data)?; + manifest.add_labeled_assertion(assertion.label(), &assertion.value()?)?; } // if we have an output option, then we must have a source image to add a claim to diff --git a/c2patool/src/signer.rs b/c2patool/src/signer.rs @@ -11,15 +11,13 @@ // specific language governing permissions and limitations under // each license. -use crate::{config::Config, fix_relative_path}; -use anyhow::{Context, Result}; /// Provides a method to read configured certs and generate a singer /// -use c2pa::{ - openssl::{EcSigner, EdSigner, RsaSigner}, - signer::ConfigurableSigner, - signer::Signer, -}; +use crate::{config::Config, fix_relative_path}; + +use anyhow::{Context, Result}; +use c2pa::{get_signer, Signer}; + use std::{env, path::Path, process::exit}; pub fn get_ta_url() -> Option<String> { @@ -30,7 +28,7 @@ pub fn get_ta_url() -> Option<String> { /// keys can be directly in environment variables /// or in a folder referenced by CAI_KEY_PATH /// also supports default dev environment keys -pub fn get_signer(config: &Config, base_path: &Path) -> Result<Box<dyn Signer>> { +pub fn get_c2pa_signer(config: &Config, base_path: &Path) -> Result<Box<dyn Signer>> { let alg = config.alg.as_deref().unwrap_or("ps256").to_lowercase(); let tsa_url = config.ta_url.clone().or_else(get_ta_url); @@ -60,31 +58,7 @@ pub fn get_signer(config: &Config, base_path: &Path) -> Result<Box<dyn Signer>> if let Some(private_key) = private_key { if let Some(sign_cert) = sign_cert { - let signer: Box<dyn Signer> = match alg.as_str() { - "ps256" | "ps384" | "ps512" => Box::new(RsaSigner::from_signcert_and_pkey( - &sign_cert, - &private_key, - alg, - tsa_url, - )?), - "es256" | "es384" | "es512" => Box::new(EcSigner::from_signcert_and_pkey( - &sign_cert, - &private_key, - alg, - tsa_url, - )?), - "ed25519" => Box::new(EdSigner::from_signcert_and_pkey( - &sign_cert, - &private_key, - alg.to_owned(), - tsa_url, - )?), - _ => { - eprintln!("Unsupported signing algorithm, must be one of [ ps256 | ps384 | ps512 | es256 | es384 | es512 | ed25519 ]"); - exit(2); - } - }; - + let signer = get_signer(&sign_cert, &private_key, &alg, tsa_url)?; return Ok(signer); } } diff --git a/c2patool/tests/integration.rs b/c2patool/tests/integration.rs @@ -117,7 +117,7 @@ mod integration { if !(priv_key_path.exists() && sign_cert_path.exists()) { // Creating the signer (which we don't use) has the side effect of // creating temporary private key and signing certificate. - c2pa::openssl::temp_signer::get_signer(&x509_path); + c2pa::get_temp_signer(&x509_path); } } } diff --git a/make_test_images/src/make_test_images.rs b/make_test_images/src/make_test_images.rs @@ -177,7 +177,7 @@ impl MakeTestImages { let src_path = &self.make_path(src); let parent = Ingredient::from_file_with_options(src_path, &options)?; - actions.add_action( + actions = actions.add_action( Action::new(c2pa_action::OPENED) .set_parameter("identifier".to_owned(), parent.instance_id().to_owned())?, ); @@ -189,7 +189,7 @@ impl MakeTestImages { // adjust brightness to show we made an edit img = img.brighten(30); - actions.add_action( + actions = actions.add_action( Action::new(c2pa_action::COLOR_ADJUSTMENTS) .set_parameter("name".to_owned(), "brightnesscontrast")?, ); @@ -206,7 +206,7 @@ impl MakeTestImages { *pixel = image::Rgb([r, 100, b]); } } - actions + actions = actions .add_action(Action::new(c2pa_action::CREATED)) .add_action( Action::new(c2pa_action::DRAWING) @@ -238,18 +238,17 @@ impl MakeTestImages { // create and add the ingredient let ingredient = Ingredient::from_file_with_options(ing_path, &options)?; - actions.add_action( - Action::new(c2pa_action::PLACED).set_parameter( + actions = + actions.add_action(Action::new(c2pa_action::PLACED).set_parameter( "identifier".to_owned(), ingredient.instance_id().to_owned(), - )?, - ); + )?); manifest.add_ingredient(ingredient); x += width; } // record what we did as an action (only need to record this once) - actions.add_action(Action::new(c2pa_action::RESIZED)); + actions = actions.add_action(Action::new(c2pa_action::RESIZED)); } // save the changes to the image as our target file diff --git a/sdk/examples/client/client.rs b/sdk/examples/client/client.rs @@ -44,7 +44,8 @@ fn show_manifest(manifest_store: &ManifestStore, manifest_label: &str, level: us } for assertion in manifest.assertions().iter() { - match assertion.label.as_str() { + println!("{}", assertion.label_with_instance()); + match assertion.label() { labels::ACTIONS => { let actions: Actions = assertion.to_assertion()?; for action in actions.actions { @@ -95,8 +96,7 @@ pub fn main() -> Result<()> { let source = PathBuf::from(&args[1]); // create an action assertion stating that we imported this file - let mut actions = Actions::new(); - actions.add_action( + let actions = Actions::new().add_action( Action::new(c2pa_action::PLACED) .set_parameter("identifier".to_owned(), parent.instance_id().to_owned())?, ); diff --git a/sdk/src/assertions/actions.rs b/sdk/src/assertions/actions.rs @@ -259,13 +259,13 @@ impl Actions { } /// Adds an [`Action`] to this assertion's list of actions. - pub fn add_action(&mut self, action: Action) -> &mut Self { + pub fn add_action(mut self, action: Action) -> Self { self.actions.push(action); self } /// Sets [`Metadata`] for the action. - pub fn add_metadata(&mut self, metadata: Metadata) -> &mut Self { + pub fn add_metadata(mut self, metadata: Metadata) -> Self { self.metadata = Some(metadata); self } @@ -347,8 +347,7 @@ pub mod tests { #[test] fn assertion_actions() { - let mut original = Actions::new(); - original + let original = Actions::new() .add_action(make_action1()) .add_action( Action::new("c2pa.filtered") diff --git a/sdk/src/assertions/creative_work.rs b/sdk/src/assertions/creative_work.rs @@ -47,8 +47,8 @@ impl CreativeWork { } /// insert key / value pair - pub fn insert<T: Serialize>(self, key: String, value: T) -> Result<Self> { - self.0.insert(key, value).map(Self) + pub fn insert<S: Into<String>, T: Serialize>(self, key: S, value: T) -> Result<Self> { + self.0.insert(key.into(), value).map(Self) } /// get creative work from json string diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs @@ -78,7 +78,9 @@ mod ingredient; pub use ingredient::{Ingredient, IngredientOptions}; pub mod jumbf_io; mod manifest; -pub use manifest::{Manifest, ManifestAssertion}; +pub use manifest::Manifest; +mod manifest_assertion; +pub use manifest_assertion::{ManifestAssertion, ManifestAssertionKind}; mod manifest_store; pub use manifest_store::ManifestStore; @@ -108,15 +110,10 @@ pub(crate) mod assertion; pub(crate) mod asset_handlers; pub(crate) mod asset_io; pub(crate) mod claim; -pub mod validation_status; -// TODO: Make this a private module again once we no longer need -// access to this from claims signer. #[cfg(feature = "file_io")] pub(crate) mod cose_sign; - #[cfg(feature = "file_io")] pub(crate) mod embedded_xmp; - pub(crate) mod hashed_uri; #[allow(dead_code)] pub(crate) mod jumbf; @@ -125,6 +122,7 @@ pub(crate) mod status_tracker; pub(crate) mod store; pub(crate) mod time_stamp; pub(crate) mod utils; +pub mod validation_status; pub(crate) use utils::cbor_types; pub(crate) use utils::hash_utils; pub(crate) use utils::xmp_inmemory_utils; diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs @@ -14,13 +14,14 @@ #[cfg(feature = "file_io")] use crate::utils::thumbnail::make_thumbnail; use crate::{ - assertion::{AssertionBase, AssertionData, AssertionDecodeError}, - assertions::{labels, Actions, CreativeWork, SchemaDotOrg, Thumbnail, UserCbor}, + assertion::{AssertionBase, AssertionData}, + assertions::{labels, Actions, CreativeWork, Thumbnail, User, UserCbor}, claim::Claim, error::{Error, Result}, jumbf, + salt::DefaultSalt, store::Store, - Ingredient, + Ingredient, ManifestAssertion, ManifestAssertionKind, }; #[cfg(feature = "file_io")] @@ -75,10 +76,10 @@ pub struct Manifest { impl Manifest { /// Create a new Manifest /// requires a claim_generator string (User Agent)) - pub fn new(claim_generator: String) -> Self { + pub fn new<S: Into<String>>(claim_generator: S) -> Self { Self { vendor: None, - claim_generator, + claim_generator: claim_generator.into(), claim_generator_hints: None, asset: None, ingredients: Vec::new(), @@ -117,14 +118,14 @@ impl Manifest { /// Sets the vendor prefix to be used when generating manifest labels /// Optional prefix added to the generated Manifest Label /// This is typically a lower case Internet domain name for the vendor (i.e. `adobe`) - pub fn set_vendor(&mut self, vendor: String) -> &mut Self { - self.vendor = Some(vendor); + pub fn set_vendor<S: Into<String>>(&mut self, vendor: S) -> &mut Self { + self.vendor = Some(vendor.into()); self } /// Sets a human readable name for the product that created this manifest - pub fn set_claim_generator(&mut self, generator: String) -> &mut Self { - self.claim_generator = generator; + pub fn set_claim_generator<S: Into<String>>(&mut self, generator: S) -> &mut Self { + self.claim_generator = generator.into(); self } @@ -180,10 +181,24 @@ impl Manifest { self } - /// Adds assertion using given label - the data for predefined assertions must be in correct format - pub fn add_labeled_assertion<T: Serialize>( + /// Adds assertion using given label and any serde serializable + /// The data for predefined assertions must be in correct format + /// + /// # Example: Creating a custom assertion from a serde_json object. + ///``` + /// # use c2pa::Result; + /// use c2pa::Manifest; + /// use serde_json::json; + /// # fn main() -> Result<()> { + /// let mut manifest = Manifest::new("my_app"); + /// let value = json!({"my_tag": "Anything I want"}); + /// manifest.add_labeled_assertion("org.contentauth.foo", &value)?; + /// # Ok(()) + /// # } + /// ``` + pub fn add_labeled_assertion<S: Into<String>, T: Serialize>( &mut self, - label: &str, + label: S, data: &T, ) -> Result<&mut Self> { self.assertions @@ -191,7 +206,23 @@ impl Manifest { Ok(self) } - /// Adds assertions, data for predefined assertions must be in correct format + /// Adds ManifestAssertions from existing assertions + /// The data for standard assertions must be in correct format + /// + /// # Example: Creating a from an Actions object. + ///``` + /// # use c2pa::Result; + /// use c2pa::{ + /// assertions::{Actions, Action, c2pa_action}, + /// Manifest + /// }; + /// # fn main() -> Result<()> { + /// let mut manifest = Manifest::new("my_app"); + /// let actions = Actions::new().add_action(Action::new(c2pa_action::EDITED)); + /// manifest.add_assertion(&actions)?; + /// # Ok(()) + /// # } + /// ``` pub fn add_assertion<T: Serialize + AssertionBase>(&mut self, data: &T) -> Result<&mut Self> { self.assertions .push(ManifestAssertion::from_assertion(data)?); @@ -199,8 +230,46 @@ impl Manifest { } /// Retrieves an assertion by label if it exists or Error::NotFound + /// + /// Example: Find an Actions Assertion + /// ``` + /// # use c2pa::Result; + /// use c2pa::{ + /// assertions::{Actions, Action, c2pa_action}, + /// Manifest + /// }; + /// # fn main() -> Result<()> { + /// let mut manifest = Manifest::new("my_app"); + /// let actions = Actions::new().add_action(Action::new(c2pa_action::EDITED)); + /// manifest.add_assertion(&actions)?; + /// + /// let actions: Actions = manifest.find_assertion(Actions::LABEL)?; + /// for action in actions.actions { + /// println!("{}", action.action()); + /// } + /// # Ok(()) + /// # } + /// ``` pub fn find_assertion<T: DeserializeOwned>(&self, label: &str) -> Result<T> { - if let Some(manifest_assertion) = self.assertions.iter().find(|a| a.label == label) { + if let Some(manifest_assertion) = self.assertions.iter().find(|a| a.label() == label) { + manifest_assertion.to_assertion() + } else { + Err(Error::NotFound) + } + } + + /// Retrieves an assertion by label and instance if it exists or Error::NotFound + /// + pub fn find_assertion_with_instance<T: DeserializeOwned>( + &self, + label: &str, + instance: usize, + ) -> Result<T> { + if let Some(manifest_assertion) = self + .assertions + .iter() + .find(|a| a.label() == label && a.instance() == instance) + { manifest_assertion.to_assertion() } else { Err(Error::NotFound) @@ -209,11 +278,11 @@ impl Manifest { // keep this private until we support it externally #[allow(dead_code)] - pub(crate) fn add_redaction(&mut self, label: &str) -> Result<&mut Self> { + pub(crate) fn add_redaction<S: Into<String>>(&mut self, label: S) -> Result<&mut Self> { // todo: any way to verify if this assertion exists in the parent claim here? match self.redactions.as_mut() { - Some(redactions) => redactions.push(label.to_string()), - None => self.redactions = Some([label.to_string()].to_vec()), + Some(redactions) => redactions.push(label.into()), + None => self.redactions = Some([label.into()].to_vec()), } Ok(self) } @@ -229,7 +298,7 @@ impl Manifest { } /// Sets the signature information for the report - pub fn set_signature(&mut self, issuer: Option<&String>, time: Option<&String>) -> &mut Self { + fn set_signature(&mut self, issuer: Option<&String>, time: Option<&String>) -> &mut Self { self.signature_info = Some(SignatureInfo { issuer: issuer.cloned(), time: time.cloned(), @@ -295,29 +364,29 @@ impl Manifest { let ingredient = Ingredient::from_ingredient_uri(store, &assertion_uri)?; manifest.add_ingredient(ingredient); } - Actions::LABEL => { - let actions = Actions::from_assertion(assertion)?; - manifest.add_assertion(&actions)?; // assertion.as_json_object()?)?; - } label if label.starts_with(labels::CLAIM_THUMBNAIL) => { let thumbnail = Thumbnail::from_assertion(assertion)?; asset.set_thumbnail(thumbnail.content_type, thumbnail.data); } _ => { - // inject assertions for all json data + // inject assertions for all other assertions match assertion.decode_data() { AssertionData::Json(_x) => { let value = assertion.as_json_object()?; - manifest.add_labeled_assertion(&label, &value)?; + let ma = ManifestAssertion::new(label, value) + .set_instance(claim_assertion.instance()) + .set_kind(ManifestAssertionKind::Json); + manifest.assertions.push(ma); } AssertionData::Cbor(_x) => { let value = assertion.as_json_object()?; //todo: should this be cbor? - manifest.add_labeled_assertion(&label, &value)?; - } - AssertionData::Binary(_x) => { - //let _value = Value::String("<omitted>".to_owned()); - // claim_report.add_assertion(&label, &value)?; + let ma = ManifestAssertion::new(label, value) + .set_instance(claim_assertion.instance()); + + manifest.assertions.push(ma); } + // todo: support binary forms + AssertionData::Binary(_x) => {} AssertionData::Uuid(_, _) => {} } } @@ -407,17 +476,19 @@ impl Manifest { let lib_hint = format!("\"{}\";v=\"{}\"", crate::NAME, crate::VERSION); claim.add_claim_generator_hint(GH_UA, Value::from(lib_hint)); + let salt = DefaultSalt::default(); + // add any additional assertions - for assertion in &self.assertions { - match assertion.label.as_str() { + for manifest_assertion in &self.assertions { + match manifest_assertion.label() { Actions::LABEL => { + let actions: Actions = manifest_assertion.to_assertion()?; // todo: fixup parameters field from instance_id to ingredient uri for // c2pa.transcoded, c2pa.repackaged, and c2pa.placed action - claim.add_assertion(&Actions::from_json_value(&assertion.data)?) + claim.add_assertion(&actions) } CreativeWork::LABEL => { - let mut cw = CreativeWork::from_json_str(&assertion.data.to_string())?; - + let mut cw: CreativeWork = manifest_assertion.to_assertion()?; // insert a credentials field if we have a vc that matches the identifier // todo: this should apply to any person, not just author if let Some(cw_authors) = cw.author() { @@ -435,20 +506,32 @@ impl Manifest { } cw = cw.set_author(&authors)?; } - claim.add_assertion(&cw) - } - labels::CLAIM_REVIEW => { - claim.add_assertion(&SchemaDotOrg::from_json_str(&assertion.data.to_string())?) - } - _ => { - // default to creating UserCbor assertions - claim.add_assertion(&UserCbor::new( - &assertion.label, - serde_cbor::to_vec(&assertion.data)?, - )) - // todo: add option to use json - //claim.add_assertion(&User::new(&assertion.label, &assertion.data.to_string()), &NoSalt{})?; + claim.add_assertion_with_salt(&cw, &salt) } + _ => match manifest_assertion.kind() { + ManifestAssertionKind::Cbor => claim.add_assertion_with_salt( + &UserCbor::new( + manifest_assertion.label(), + serde_cbor::to_vec(&manifest_assertion.value()?)?, + ), + &salt, + ), + ManifestAssertionKind::Json => claim.add_assertion_with_salt( + &User::new( + manifest_assertion.label(), + &serde_json::to_string(&manifest_assertion.value()?)?, + ), + &salt, + ), + ManifestAssertionKind::Binary => { + // todo: Support binary kinds + return Err(Error::AssertionEncoding); + } + ManifestAssertionKind::Uri => { + // todo: Support binary kinds + return Err(Error::AssertionEncoding); + } + }, }?; } @@ -514,42 +597,6 @@ impl std::fmt::Display for Manifest { f.write_str(&json) } } -#[derive(Debug, Deserialize, Serialize, Clone)] -/// A labeled container for an Assertion value in a Manifest -pub struct ManifestAssertion { - /// An assertion label in reverse domain format - pub label: String, - /// The data of the assertion as Value - pub data: Value, -} - -impl ManifestAssertion { - pub fn from_labeled_assertion<T: Serialize>(label: &str, data: &T) -> Result<Self> { - Ok(Self { - label: label.to_owned(), - data: serde_json::to_value(data).map_err(|_err| Error::AssertionEncoding)?, - }) - } - - pub fn from_assertion<T: Serialize + AssertionBase>(data: &T) -> Result<Self> { - Ok(Self { - label: data.label().to_owned(), - data: serde_json::to_value(data).map_err(|_err| Error::AssertionEncoding)?, - }) - } - - pub fn to_assertion<T: DeserializeOwned>(&self) -> Result<T> { - serde_json::from_value(self.data.clone()).map_err(|e| { - Error::AssertionDecoding(AssertionDecodeError::from_json_err( - self.label.to_owned(), - None, - "application/json".to_owned(), - e, - )) - }) - } -} - #[derive(Clone, Debug, Deserialize, Serialize)] /// Holds information about a signature pub struct SignatureInfo { @@ -561,20 +608,26 @@ pub struct SignatureInfo { time: Option<String>, } #[cfg(test)] -#[cfg(feature = "file_io")] pub(crate) mod tests { #![allow(clippy::expect_used)] #![allow(clippy::unwrap_used)] - use super::{Ingredient, Manifest, Store}; - use crate::{ assertions::{c2pa_action, Action, Actions}, + utils::test::TEST_VC, + Manifest, Result, + }; + + #[cfg(feature = "file_io")] + use crate::{ openssl::temp_signer::get_temp_signer, status_tracker::{DetailedStatusTracker, StatusTracker}, - utils::test::{fixture_path, temp_dir_path, temp_fixture_path, TEST_SMALL_JPEG, TEST_VC}, + store::Store, + utils::test::{fixture_path, temp_dir_path, temp_fixture_path, TEST_SMALL_JPEG}, + Ingredient, }; + #[cfg(feature = "file_io")] use tempfile::tempdir; // example of random data structure as an assertion @@ -613,9 +666,7 @@ pub(crate) mod tests { ) .expect("add_assertion"); - let mut actions = Actions::new(); - - actions.add_action( + let actions = Actions::new().add_action( Action::new(c2pa_action::EDITED) .set_parameter("name".to_owned(), "gaussian_blur") .unwrap(), @@ -683,7 +734,6 @@ pub(crate) mod tests { } #[test] - #[cfg(feature = "file_io")] fn test_verifiable_credential() { let mut manifest = test_manifest(); let vc: serde_json::Value = serde_json::from_str(TEST_VC).unwrap(); @@ -696,7 +746,6 @@ pub(crate) mod tests { } #[test] - #[cfg(feature = "file_io")] fn test_assertion_user_cbor() { use crate::assertions::UserCbor; use crate::Manifest; @@ -792,4 +841,25 @@ pub(crate) mod tests { let claim1 = store3.get_claim(&claim1_label).unwrap(); assert!(claim1.get_claim_assertion(redacted_uri, 0).is_none()); } + + #[test] + fn manifest_assertion_instances() { + let mut manifest = Manifest::new("test".to_owned()); + let actions = Actions::new().add_action(Action::new(c2pa_action::EDITED)); + // add three assertions with the same label + manifest.add_assertion(&actions).expect("add_assertion"); + manifest.add_assertion(&actions).expect("add_assertion"); + manifest.add_assertion(&actions).expect("add_assertion"); + + // convert to a store and read back again + let store = manifest.to_store().expect("to_store"); + println!("{}", store); + let active_label = store.provenance_label().unwrap(); + let manifest2 = Manifest::from_store(&store, &active_label).expect("from_store"); + println!("{}", manifest2); + // now check to see if we have three separate assertions with different instances + let action2: Result<Actions> = manifest2.find_assertion_with_instance(Actions::LABEL, 2); + assert!(action2.is_ok()); + assert_eq!(action2.unwrap().actions()[0].action(), c2pa_action::EDITED); + } } diff --git a/sdk/src/manifest_assertion.rs b/sdk/src/manifest_assertion.rs @@ -0,0 +1,233 @@ +use crate::{ + assertion::{AssertionBase, AssertionDecodeError}, + error::{Error, Result}, +}; + +use serde::{de::DeserializeOwned, Deserialize, Serialize}; //, Deserializer, Serializer}; +use serde_json::Value; + +/// Assertions in C2PA can be stored in several formats +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub enum ManifestAssertionKind { + Cbor, + Json, + Binary, + Uri, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(untagged)] +enum ManifestData { + Json(Value), // { label: String, instance: usize, data: Value }, + Binary(Vec<u8>), // ) { label: String, instance: usize, data: Value }, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +/// A labeled container for an Assertion value in a Manifest +pub struct ManifestAssertion { + /// An assertion label in reverse domain format + label: String, + /// The data of the assertion as Value + data: ManifestData, + /// There can be more than one assertion for any label + #[serde(skip_serializing_if = "Option::is_none")] + instance: Option<usize>, + /// The [ManifestAssertionKind] for this assertion (as stored in c2pa content) + #[serde(skip_serializing_if = "Option::is_none")] + kind: Option<ManifestAssertionKind>, +} + +impl ManifestAssertion { + /// Create with label and value + pub fn new(label: String, data: Value) -> Self { + Self { + label, + data: ManifestData::Json(data), + instance: None, + kind: None, + } + } + + /// An assertion label in reverse domain format + pub fn label(&self) -> &str { + &self.label + } + + /// An assertion label in reverse domain format with appended instance number + /// The instance number follows two underscores and is only added when the instance is > 1 + /// This is a c2pa spec internal standard format + pub fn label_with_instance(&self) -> String { + match self.instance { + Some(i) if i > 1 => format!("{}__{}", self.label, i), + _ => self.label.to_owned(), + } + } + + /// The data of the assertion as a serde_Json::Value + /// This will return UnsupportedType if the assertion data is binary + pub fn value(&self) -> Result<&Value> { + match &self.data { + ManifestData::Json(d) => Ok(d), + ManifestData::Binary(_) => Err(Error::UnsupportedType), + } + } + + /// The data of the assertion as u8 binary vector + /// This will return UnsupportedType if the assertion data is Json/String + pub fn binary(&self) -> Result<&[u8]> { + match &self.data { + ManifestData::Json(_) => Err(Error::UnsupportedType), + ManifestData::Binary(b) => Ok(b), + } + } + + /// The instance number of this assertion + /// If the same label is used for multiple assertions, incremental instances are added + /// The first instance is always 1 and increased by 1 per duplicated label + pub fn instance(&self) -> usize { + self.instance.unwrap_or(1) + } + + /// The ManifestAssertionKind for this assertion + /// This refers to how the format of the assertion inside a C2PA manifest + /// The default is ManifestAssertionKind::Cbor + pub fn kind(&self) -> &ManifestAssertionKind { + match self.kind.as_ref() { + Some(kind) => kind, + None => &ManifestAssertionKind::Cbor, + } + } + + /// This can be used to set an instance number, but generally should not be used + /// Instance numbers will be assigned automatically when the assertions are embedded + pub(crate) fn set_instance(mut self, instance: usize) -> Self { + self.instance = if instance > 1 { Some(instance) } else { None }; + self + } + + /// Allows overriding the default [ManifestAssertionKind] to Json + /// For assertions like Schema.org that require being stored in Json format + pub fn set_kind(mut self, kind: ManifestAssertionKind) -> Self { + self.kind = Some(kind); + self + } + + /// Creates a ManifestAssertion with the given label and any serde serializable object + /// + /// # Example: Creating a custom assertion from a serde_json object. + /// + ///``` + /// # use c2pa::Result; + /// use c2pa::ManifestAssertion; + /// use serde_json::json; + /// # fn main() -> Result<()> { + /// let value = json!({"my_tag": "Anything I want"}); + /// let _ma = ManifestAssertion::from_labeled_assertion("org.contentauth.foo", &value)?; + /// # Ok(()) + /// # } + /// ``` + pub fn from_labeled_assertion<S: Into<String>, T: Serialize>( + label: S, + data: &T, + ) -> Result<Self> { + Ok(Self::new( + label.into(), + serde_json::to_value(data).map_err(|_err| Error::AssertionEncoding)?, + )) + } + + /// Creates a ManifestAssertion from an AssertionBase object + /// + /// # Example: Creating a custom assertion an Action assertion + /// + ///``` + /// # use c2pa::Result; + /// use c2pa::{ + /// assertions::{Actions, Action, c2pa_action}, + /// ManifestAssertion + /// }; + /// # fn main() -> Result<()> { + /// let actions = Actions::new().add_action(Action::new(c2pa_action::EDITED)); + /// let _ma = ManifestAssertion::from_assertion(&actions)?; + /// # Ok(()) + /// # } + /// ``` + pub fn from_assertion<T: Serialize + AssertionBase>(data: &T) -> Result<Self> { + Ok(Self::new( + data.label().to_owned(), + serde_json::to_value(data).map_err(|_err| Error::AssertionEncoding)?, + )) + } + + /// Creates an Assertion object from a ManifestAssertion + /// + /// # Example: extracting an Actions Assertion + /// ``` + /// # use c2pa::Result; + /// use c2pa::{ + /// assertions::{Actions, Action, c2pa_action}, + /// ManifestAssertion + /// }; + /// # fn main() -> Result<()> { + /// let actions = Actions::new().add_action(Action::new(c2pa_action::EDITED)); + /// let manifest_assertion = ManifestAssertion::from_assertion(&actions)?; + /// + /// let actions: Actions = manifest_assertion.to_assertion()?; + /// for action in actions.actions { + /// println!("{}", action.action()); + /// } + /// # Ok(()) + /// # } + /// ``` + pub fn to_assertion<T: DeserializeOwned>(&self) -> Result<T> { + serde_json::from_value(self.value()?.to_owned()).map_err(|e| { + Error::AssertionDecoding(AssertionDecodeError::from_json_err( + self.label.to_owned(), + None, + "application/json".to_owned(), + e, + )) + }) + } +} + +#[cfg(test)] +pub(crate) mod tests { + #![allow(clippy::expect_used)] + #![allow(clippy::unwrap_used)] + + use super::*; + use crate::assertions::{c2pa_action, Action, Actions}; + + #[test] + fn test_from_labeled() { + let data = serde_json::json!({"mytag": "mydata"}); + let ma = ManifestAssertion::from_labeled_assertion("org.contentauth.foo", &data) + .expect("from_labeled_assertion"); + assert_eq!(ma.label(), "org.contentauth.foo"); + assert!(ma.value().is_ok()); + } + + #[test] + fn test_manifest_assertion() { + let actions = Actions::new().add_action(Action::new(c2pa_action::EDITED)); + let value = serde_json::to_value(actions).unwrap(); + let mut ma = ManifestAssertion::new(Actions::LABEL.to_owned(), value); + assert_eq!(ma.label(), Actions::LABEL); + + ma = ma.set_instance(1); + assert_eq!(ma.instance, None); + ma = ma.set_instance(2); + assert_eq!(ma.instance(), 2); + assert_eq!(ma.kind(), &ManifestAssertionKind::Cbor); + ma = ma.set_kind(ManifestAssertionKind::Json); + assert_eq!(ma.kind(), &ManifestAssertionKind::Json); + + let actions = Actions::new().add_action(Action::new(c2pa_action::EDITED)); + let ma2 = ManifestAssertion::from_assertion(&actions).expect("from_assertion"); + let actions2: Actions = ma2.to_assertion().expect("to_assertion"); + let actions3 = ManifestAssertion::from_labeled_assertion("foo".to_owned(), &actions2) + .expect("from_labeled_assertion"); + assert_eq!(actions3.label(), "foo"); + } +} diff --git a/sdk/src/store.rs b/sdk/src/store.rs @@ -1715,8 +1715,7 @@ pub mod tests { } fn create_capture_claim(claim: &mut Claim) -> Result<&mut Claim> { - let mut actions = Actions::new(); - actions.add_action(Action::new("c2pa.created")); + let actions = Actions::new().add_action(Action::new("c2pa.created")); claim.add_assertion(&actions)?; diff --git a/sdk/src/utils/test.rs b/sdk/src/utils/test.rs @@ -58,8 +58,7 @@ pub fn create_test_claim() -> Result<Claim> { let _hu = claim.add_verifiable_credential(TEST_VC)?; // Add assertions. - let mut actions = Actions::new(); - actions + let actions = Actions::new() .add_action( Action::new("c2pa.cropped") .set_parameter( diff --git a/sdk/tests/integration.rs b/sdk/tests/integration.rs @@ -45,7 +45,7 @@ mod integration_1 { // add a parent ingredient let parent = Ingredient::from_file(&parent_path)?; // add an action assertion stating that we imported this file - actions.add_action( + actions = actions.add_action( Action::new(c2pa_action::EDITED) .set_parameter("name".to_owned(), "import")? .set_parameter("identifier".to_owned(), parent.instance_id().to_owned())?, @@ -58,7 +58,7 @@ mod integration_1 { let mut img = image::open(&parent_path)?; img = img.brighten(50); // brighten the image - actions.add_action( + actions = actions.add_action( Action::new("c2pa.edit").set_parameter("name".to_owned(), "brightnesscontrast")?, ); @@ -71,7 +71,7 @@ mod integration_1 { image::imageops::overlay(&mut img, &img_small, 0, 0); // add an action assertion stating that we imported this file - actions.add_action( + actions = actions.add_action( Action::new(c2pa_action::EDITED) .set_parameter("name".to_owned(), "import")? .set_parameter("identifier".to_owned(), ingredient.instance_id().to_owned())?,