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:
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