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 85e7223d319f72b99b7f18294b4b4209e66ae03d
parent fa2d15a9481142cb48b1051da12afc6f8f9dbcd1
Author: mauricefisher64 <92736594+mauricefisher64@users.noreply.github.com>
Date:   Fri, 13 Jan 2023 11:07:50 -0500

Visualizations (#163)

* Hack work

* Add cert chain dumping

* Interface refinements

* add watermarks

* Steganography support

* Back out stego

* fmt and clippy cleanup

* Get visualizations ready for release

* Merge fixes

* Add comment

* WASM fixes

* Format fixes

* build fixes

* Fix unit test

* Change feature flags for cert dump

* Format

* Clippy fixes

Co-authored-by: Gavin Peacock <gpeacock@adobe.com>
Co-authored-by: Eric Scouten <scouten@adobe.com>
Diffstat:
Mmake_test_images/tests.json | 2+-
Msdk/Cargo.toml | 1+
Msdk/src/claim.rs | 11+++++++++++
Msdk/src/cose_validator.rs | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Msdk/src/manifest.rs | 6+++++-
Msdk/src/manifest_store_report.rs | 149+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msdk/src/store.rs | 13+++++++++++--
Msdk/src/validator.rs | 3++-
8 files changed, 236 insertions(+), 21 deletions(-)

diff --git a/make_test_images/tests.json b/make_test_images/tests.json @@ -3,7 +3,7 @@ "tsa_url": "http://timestamp.digicert.com", "output_path": "target/images", "default_ext": "jpg", - "author": "Gavin Peacock", + "author": "Adobe make_test", "recipes": [ { "op": "copy", "parent": "sdk/tests/fixtures/IMG_0003.jpg", "output": "A.jpg" }, { "op": "copy", "parent": "sdk/tests/fixtures/P1000827.jpg", "output": "I.jpg" }, diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml @@ -80,6 +80,7 @@ serde-transcode = "1.1.1" sha2 = "0.10.2" tempfile = "3.1.0" thiserror = ">= 1.0.20, < 1.0.38" +treeline = "0.1.0" twoway = "0.2.1" url = "2.2.2" uuid = { version = "0.8.1", features = ["serde", "v4", "wasm-bindgen"] } diff --git a/sdk/src/claim.rs b/sdk/src/claim.rs @@ -886,6 +886,17 @@ impl Claim { Claim::verify_internal(claim, asset_data, is_provenance, verified, validation_log) } + /// Get the signing certificate chain as PEM bytes + pub fn get_cert_chain(&self) -> Result<Vec<u8>> { + let sig = self.signature_val(); + let data = self.data()?; + let mut validation_log = OneShotStatusTracker::new(); + + let vi = get_signing_info(sig, &data, &mut validation_log); + + Ok(vi.cert_chain) + } + fn verify_internal<'a>( claim: &Claim, asset_data: &ClaimAssetData<'a>, diff --git a/sdk/src/cose_validator.rs b/sdk/src/cose_validator.rs @@ -567,20 +567,31 @@ fn get_sign_certs(sign1: &coset::CoseSign1) -> Result<Vec<Vec<u8>>> { } // internal util function to dump the cert chain in PEM format -#[cfg(not(target_arch = "wasm32"))] -#[cfg(feature = "file_io")] -#[allow(dead_code)] -fn dump_cert_chain(certs: &[Vec<u8>], output_path: &std::path::Path) -> Result<()> { - let mut out_buf: Vec<u8> = Vec::new(); +#[allow(unused_variables)] +fn dump_cert_chain(certs: &[Vec<u8>], output_path: Option<&std::path::Path>) -> Result<Vec<u8>> { + #[cfg(feature = "sign")] + { + let mut out_buf: Vec<u8> = Vec::new(); + + for der_bytes in certs { + let c = + openssl::x509::X509::from_der(der_bytes).map_err(|_e| Error::UnsupportedType)?; + let mut c_pem = c.to_pem().map_err(|_e| Error::UnsupportedType)?; - for der_bytes in certs { - let c = openssl::x509::X509::from_der(der_bytes).map_err(|_e| Error::UnsupportedType)?; - let mut c_pem = c.to_pem().map_err(|_e| Error::UnsupportedType)?; + out_buf.append(&mut c_pem); + } - out_buf.append(&mut c_pem); + if let Some(op) = output_path { + std::fs::write(op, &out_buf).map_err(Error::IoError)?; + } + Ok(out_buf) } - std::fs::write(output_path, &out_buf).map_err(Error::IoError) + #[cfg(not(feature = "sign"))] + { + let out_buf: Vec<u8> = Vec::new(); + Ok(out_buf) + } } // Note: this function is only used to get the display string and not for cert validation. @@ -731,11 +742,15 @@ pub async fn verify_cose_async( // parse the temp time for now util we have TA result.date = get_signing_time(&sign1, &data); + + // return cert chain + result.cert_chain = dump_cert_chain(&get_sign_certs(&sign1)?, None)?; } Ok(result) } +#[allow(unused_variables)] pub fn get_signing_info( cose_bytes: &[u8], data: &[u8], @@ -745,7 +760,7 @@ pub fn get_signing_info( let mut issuer_org = None; let mut alg: Option<SigningAlg> = None; - let _ = get_cose_sign1(cose_bytes, data, validation_log).and_then(|sign1| { + let sign1 = get_cose_sign1(cose_bytes, data, validation_log).and_then(|sign1| { // get the public key der let der_bytes = get_sign_cert(&sign1)?; @@ -762,11 +777,33 @@ pub fn get_signing_info( Ok(sign1) }); - ValidationInfo { - issuer_org, - date, - alg, - validated: false, + #[cfg(target_arch = "wasm32")] + { + ValidationInfo { + issuer_org, + date, + alg, + validated: false, + cert_chain: Vec::new(), + } + } + #[cfg(not(target_arch = "wasm32"))] + { + let certs = match sign1 { + Ok(s) => match get_sign_certs(&s) { + Ok(c) => dump_cert_chain(&c, None).unwrap_or_default(), + Err(_) => Vec::new(), + }, + Err(_e) => Vec::new(), + }; + + ValidationInfo { + issuer_org, + date, + alg, + validated: false, + cert_chain: certs, + } } } @@ -861,6 +898,9 @@ pub fn verify_cose( // parse the temp time for now util we have TA result.date = get_signing_time(&sign1, data); + + // return cert chain + result.cert_chain = dump_cert_chain(&certs, None)?; } // Note: not adding validation_log entry here since caller will supply claim specific info to log Ok(()) diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs @@ -115,7 +115,6 @@ impl Manifest { remote_manifest: None, } } - /// Returns a User Agent formatted string identifying the software/hardware/system produced this claim pub fn claim_generator(&self) -> &str { self.claim_generator.as_str() @@ -852,6 +851,11 @@ impl Manifest { .save_to_asset_remote_signed(source_path.as_ref(), signer, dest_path.as_ref()) .await } + #[cfg(feature = "file_io")] + pub fn remove_manifest<P: AsRef<Path>>(asset_path: P) -> Result<()> { + use crate::jumbf_io::remove_jumbf_from_file; + remove_jumbf_from_file(asset_path.as_ref()) + } } impl std::fmt::Display for Manifest { diff --git a/sdk/src/manifest_store_report.rs b/sdk/src/manifest_store_report.rs @@ -15,6 +15,8 @@ use std::collections::HashMap; #[cfg(feature = "file_io")] use std::path::Path; +use atree::{Arena, Token}; +use extfmt::Hexlify; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -53,6 +55,56 @@ impl ManifestStoreReport { }) } + /// Prints tree view of manifest store + #[cfg(feature = "file_io")] + pub fn dump_tree<P: AsRef<Path>>(path: P) -> Result<()> { + let mut validation_log = crate::status_tracker::DetailedStatusTracker::new(); + let store = crate::store::Store::load_from_asset(path.as_ref(), true, &mut validation_log)?; + + let claim = store.provenance_claim().ok_or(crate::Error::ClaimMissing { + label: "None".to_string(), + })?; + + let os_filename = path + .as_ref() + .file_name() + .ok_or_else(|| crate::Error::BadParam("bad filename".to_string()))?; + let asset_name = os_filename.to_string_lossy().into_owned(); + + let (tree, root_token) = ManifestStoreReport::to_tree(&store, claim, &asset_name, false)?; + fn walk_tree(tree: &Arena<String>, token: &Token) -> treeline::Tree<String> { + let result = token.children_tokens(tree).fold( + treeline::Tree::root(tree[*token].data.clone()), + |mut root, entry_token| { + if entry_token.is_leaf(tree) { + root.push(treeline::Tree::root(tree[entry_token].data.clone())); + } else { + root.push(walk_tree(tree, &entry_token)); + } + root + }, + ); + + result + } + + // print tree + println!("Tree View:\n {}", walk_tree(&tree, &root_token)); + + Ok(()) + } + + /// Prints the certificate chain use to sign the active manifest + #[cfg(feature = "file_io")] + pub fn dump_cert_chain<P: AsRef<Path>>(path: P) -> Result<()> { + let mut validation_log = crate::status_tracker::DetailedStatusTracker::new(); + let store = crate::store::Store::load_from_asset(path.as_ref(), true, &mut validation_log)?; + + let cert_str = store.get_provenance_cert_chain()?; + println!("{}", cert_str); + Ok(()) + } + /// Creates a ManifestStoreReport from an existing Store and a validation log pub(crate) fn from_store_with_log( store: &Store, @@ -101,6 +153,85 @@ impl ManifestStoreReport { json } + + #[allow(dead_code)] + fn populate_node( + tree: &mut Arena<String>, + store: &Store, + claim: &Claim, + current_token: &Token, + name_only: bool, + ) -> Result<()> { + let claim_assertions = claim.claim_assertion_store(); + for claim_assertion in claim_assertions.iter() { + let hashlink = claim_assertion.label(); + let (label, instance) = Claim::assertion_label_from_link(&hashlink); + let label = Claim::label_with_instance(&label, instance); + + current_token.append(tree, format!("Assertion:{}", label)); + } + + // recurse down ingredients + for i in claim.ingredient_assertions() { + let ingredient_assertion = + <crate::assertions::Ingredient as crate::AssertionBase>::from_assertion(i)?; + + // is this an ingredient + if let Some(ref c2pa_manifest) = &ingredient_assertion.c2pa_manifest { + let label = Store::manifest_label_from_path(&c2pa_manifest.url()); + let hash = &c2pa_manifest.hash()[..5]; + + if let Some(ingredient_claim) = store.get_claim(&label) { + // create new node + let data = if name_only { + format!("{}_{}", ingredient_assertion.title, Hexlify(hash)) + } else { + format!("Asset:{}, Manifest:{}", ingredient_assertion.title, label) + }; + let new_token = tree.new_node(data); + current_token.append_node(tree, new_token).map_err(|_err| { + crate::Error::InvalidAsset("Bad Manifest graph".to_string()) + })?; + + ManifestStoreReport::populate_node( + tree, + store, + ingredient_claim, + &new_token, + name_only, + )?; + } + } else { + let asset_name = &ingredient_assertion.title; + let data = if name_only { + asset_name.to_string() + } else { + format!("Asset:{}", asset_name) + }; + current_token.append(tree, data); + } + } + + Ok(()) + } + + #[allow(dead_code)] + fn to_tree( + store: &Store, + claim: &Claim, + asset_name: &str, + name_only: bool, + ) -> Result<(Arena<String>, Token)> { + let data = if name_only { + asset_name.to_string() + } else { + format!("Asset:{}, Manifest:{}", asset_name, claim.label()) + }; + + let (mut tree, root_token) = Arena::with_data(data); + ManifestStoreReport::populate_node(&mut tree, store, claim, &root_token, name_only)?; + Ok((tree, root_token)) + } } impl std::fmt::Display for ManifestStoreReport { @@ -243,4 +374,22 @@ mod tests { let report = ManifestStoreReport::from_file(path).expect("load_from_asset"); println!("{}", report); } + + #[test] + #[cfg(feature = "file_io")] + fn manifest_dump_tree() { + let asset_name = "CA.jpg"; + let path = fixture_path(asset_name); + + ManifestStoreReport::dump_tree(path).expect("dump_tree"); + } + + #[test] + #[cfg(feature = "file_io")] + fn manifest_dump_certchain() { + let asset_name = "CA.jpg"; + let path = fixture_path(asset_name); + + ManifestStoreReport::dump_cert_chain(path).expect("dump certs"); + } } diff --git a/sdk/src/store.rs b/sdk/src/store.rs @@ -335,6 +335,16 @@ impl Store { placeholder } + /// Return certificate chain for the provenance claim + pub fn get_provenance_cert_chain(&self) -> Result<String> { + let claim = self.provenance_claim().ok_or(Error::ProvenanceMissing)?; + + match claim.get_cert_chain() { + Ok(chain) => String::from_utf8(chain).map_err(|_e| Error::CoseInvalidCert), + Err(e) => Err(e), + } + } + /// Sign the claim and return signature. #[cfg(feature = "sign")] pub fn sign_claim( @@ -1716,8 +1726,6 @@ impl Store { dest_path: &Path, reserve_size: usize, ) -> Result<Vec<u8>> { - // clone the source to working copy if requested - // force generate external manifests for unknown types let ext = match get_supported_file_extension(dest_path) { Some(ext) => ext, @@ -1728,6 +1736,7 @@ impl Store { } }; + // clone the source to working copy if requested if asset_path != dest_path { fs::copy(asset_path, dest_path).map_err(Error::IoError)?; } diff --git a/sdk/src/validator.rs b/sdk/src/validator.rs @@ -22,7 +22,8 @@ pub struct ValidationInfo { pub alg: Option<SigningAlg>, // validation algorithm pub date: Option<DateTime<Utc>>, pub issuer_org: Option<String>, - pub validated: bool, // claim signature is valid + pub validated: bool, // claim signature is valid + pub cert_chain: Vec<u8>, // certificate chain used to validate signature } /// Trait to support validating a signature against the provided data