manifest_store_report.rs (16518B)
1 // Copyright 2022 Adobe. All rights reserved. 2 // This file is licensed to you under the Apache License, 3 // Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 4 // or the MIT license (http://opensource.org/licenses/MIT), 5 // at your option. 6 7 // Unless required by applicable law or agreed to in writing, 8 // this software is distributed on an "AS IS" BASIS, WITHOUT 9 // WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or 10 // implied. See the LICENSE-MIT and LICENSE-APACHE files for the 11 // specific language governing permissions and limitations under 12 // each license. 13 14 use std::collections::HashMap; 15 #[cfg(feature = "file_io")] 16 #[cfg(feature = "v1_api")] 17 use std::path::Path; 18 19 use atree::{Arena, Token}; 20 use extfmt::Hexlify; 21 use serde::{Deserialize, Serialize}; 22 use serde_json::Value; 23 24 #[cfg(feature = "v1_api")] 25 use crate::status_tracker::{DetailedStatusTracker, StatusTracker}; 26 use crate::{ 27 assertion::AssertionData, claim::Claim, store::Store, utils::base64, 28 validation_status::ValidationStatus, Result, 29 }; 30 31 /// Low level JSON based representation of Manifest Store - used for debugging 32 #[non_exhaustive] 33 #[derive(Serialize, Deserialize, Debug, Default)] 34 pub struct ManifestStoreReport { 35 #[serde(skip_serializing_if = "Option::is_none")] 36 active_manifest: Option<String>, 37 manifests: HashMap<String, ManifestReport>, 38 #[serde(skip_serializing_if = "Option::is_none")] 39 validation_status: Option<Vec<ValidationStatus>>, 40 } 41 42 impl ManifestStoreReport { 43 /// Creates a ManifestStoreReport from an existing Store 44 pub(crate) fn from_store(store: &Store) -> Result<Self> { 45 let mut manifests = HashMap::<String, ManifestReport>::new(); 46 for claim in store.claims() { 47 manifests.insert(claim.label().to_owned(), ManifestReport::from_claim(claim)?); 48 } 49 50 Ok(ManifestStoreReport { 51 active_manifest: store.provenance_label(), 52 manifests, 53 validation_status: None, 54 }) 55 } 56 57 /// Prints tree view of manifest store 58 #[cfg(feature = "file_io")] 59 #[cfg(feature = "v1_api")] 60 pub fn dump_tree<P: AsRef<Path>>(path: P) -> Result<()> { 61 let mut validation_log = crate::status_tracker::DetailedStatusTracker::new(); 62 let store = crate::store::Store::load_from_asset(path.as_ref(), true, &mut validation_log)?; 63 64 let claim = store.provenance_claim().ok_or(crate::Error::ClaimMissing { 65 label: "None".to_string(), 66 })?; 67 68 let os_filename = path 69 .as_ref() 70 .file_name() 71 .ok_or_else(|| crate::Error::BadParam("bad filename".to_string()))?; 72 let asset_name = os_filename.to_string_lossy().into_owned(); 73 74 let (tree, root_token) = ManifestStoreReport::to_tree(&store, claim, &asset_name, false)?; 75 fn walk_tree(tree: &Arena<String>, token: &Token) -> treeline::Tree<String> { 76 let result = token.children_tokens(tree).fold( 77 treeline::Tree::root(tree[*token].data.clone()), 78 |mut root, entry_token| { 79 if entry_token.is_leaf(tree) { 80 root.push(treeline::Tree::root(tree[entry_token].data.clone())); 81 } else { 82 root.push(walk_tree(tree, &entry_token)); 83 } 84 root 85 }, 86 ); 87 88 result 89 } 90 91 // print tree 92 println!("Tree View:\n {}", walk_tree(&tree, &root_token)); 93 94 Ok(()) 95 } 96 97 /// Prints the certificate chain used to sign the active manifest. 98 #[cfg(feature = "file_io")] 99 #[cfg(feature = "v1_api")] 100 pub fn dump_cert_chain<P: AsRef<Path>>(path: P) -> Result<()> { 101 let mut validation_log = DetailedStatusTracker::new(); 102 let store = Store::load_from_asset(path.as_ref(), true, &mut validation_log)?; 103 104 let cert_str = store.get_provenance_cert_chain()?; 105 println!("{cert_str}\n\n"); 106 107 if let Some(ocsp_info) = store.get_ocsp_status() { 108 println!("{ocsp_info}"); 109 } 110 111 Ok(()) 112 } 113 114 /// Returns the certificate chain used to sign the active manifest. 115 #[cfg(feature = "file_io")] 116 #[cfg(feature = "v1_api")] 117 pub fn cert_chain<P: AsRef<Path>>(path: P) -> Result<String> { 118 let mut validation_log = DetailedStatusTracker::new(); 119 let store = Store::load_from_asset(path.as_ref(), true, &mut validation_log)?; 120 store.get_provenance_cert_chain() 121 } 122 123 /// Returns the certificate used to sign the active manifest. 124 #[cfg(feature = "v1_api")] 125 pub fn cert_chain_from_bytes(format: &str, bytes: &[u8]) -> Result<String> { 126 let mut validation_log = DetailedStatusTracker::new(); 127 let store = Store::load_from_memory(format, bytes, true, &mut validation_log)?; 128 store.get_provenance_cert_chain() 129 } 130 131 #[cfg(feature = "v1_api")] 132 /// Creates a ManifestStoreReport from an existing Store and a validation log 133 pub(crate) fn from_store_with_log( 134 store: &Store, 135 validation_log: &impl StatusTracker, 136 ) -> Result<Self> { 137 let mut report = Self::from_store(store)?; 138 139 // convert log items to ValidationStatus 140 let mut statuses = Vec::new(); 141 for item in validation_log.get_log() { 142 if let Some(status) = item.validation_status.as_ref() { 143 statuses.push( 144 ValidationStatus::new(status.to_string()) 145 .set_url(item.label.to_string()) 146 .set_explanation(item.description.to_string()), 147 ); 148 } 149 } 150 if !statuses.is_empty() { 151 report.validation_status = Some(statuses); 152 } 153 Ok(report) 154 } 155 156 #[cfg(feature = "v1_api")] 157 /// Creates a ManifestStoreReport from image bytes and a format 158 pub fn from_bytes(format: &str, image_bytes: &[u8]) -> Result<Self> { 159 let mut validation_log = DetailedStatusTracker::new(); 160 let store = Store::load_from_memory(format, image_bytes, true, &mut validation_log)?; 161 Self::from_store_with_log(&store, &validation_log) 162 } 163 164 #[cfg(feature = "v1_api")] 165 /// Creates a ManifestStoreReport from a file 166 #[cfg(feature = "file_io")] 167 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> { 168 let mut validation_log = DetailedStatusTracker::new(); 169 let store = Store::load_from_asset(path.as_ref(), true, &mut validation_log)?; 170 Self::from_store_with_log(&store, &validation_log) 171 } 172 173 /// create a json string representation of this structure, omitting binaries 174 fn to_json(&self) -> String { 175 let mut json = serde_json::to_string_pretty(self).unwrap_or_else(|e| e.to_string()); 176 177 json = b64_tag(json, "hash"); 178 json = omit_tag(json, "pad"); 179 180 json 181 } 182 183 #[allow(dead_code)] 184 fn populate_node( 185 tree: &mut Arena<String>, 186 store: &Store, 187 claim: &Claim, 188 current_token: &Token, 189 name_only: bool, 190 ) -> Result<()> { 191 let claim_assertions = claim.claim_assertion_store(); 192 for claim_assertion in claim_assertions.iter() { 193 let hashlink = claim_assertion.label(); 194 let (label, instance) = Claim::assertion_label_from_link(&hashlink); 195 let label = Claim::label_with_instance(&label, instance); 196 197 current_token.append(tree, format!("Assertion:{label}")); 198 } 199 200 // recurse down ingredients 201 for i in claim.ingredient_assertions() { 202 let ingredient_assertion = 203 <crate::assertions::Ingredient as crate::assertion::AssertionBase>::from_assertion( 204 i, 205 )?; 206 207 // is this an ingredient 208 if let Some(ref c2pa_manifest) = &ingredient_assertion.c2pa_manifest { 209 let label = Store::manifest_label_from_path(&c2pa_manifest.url()); 210 let hash = &c2pa_manifest.hash()[..5]; 211 212 if let Some(ingredient_claim) = store.get_claim(&label) { 213 // create new node 214 let data = if name_only { 215 format!("{}_{}", ingredient_assertion.title, Hexlify(hash)) 216 } else { 217 format!("Asset:{}, Manifest:{}", ingredient_assertion.title, label) 218 }; 219 220 let new_token = current_token.append(tree, data); 221 222 ManifestStoreReport::populate_node( 223 tree, 224 store, 225 ingredient_claim, 226 &new_token, 227 name_only, 228 )?; 229 } 230 } else { 231 let asset_name = &ingredient_assertion.title; 232 let data = if name_only { 233 asset_name.to_string() 234 } else { 235 format!("Asset:{asset_name}") 236 }; 237 current_token.append(tree, data); 238 } 239 } 240 241 Ok(()) 242 } 243 244 #[allow(dead_code)] 245 fn to_tree( 246 store: &Store, 247 claim: &Claim, 248 asset_name: &str, 249 name_only: bool, 250 ) -> Result<(Arena<String>, Token)> { 251 let data = if name_only { 252 asset_name.to_string() 253 } else { 254 format!("Asset:{}, Manifest:{}", asset_name, claim.label()) 255 }; 256 257 let (mut tree, root_token) = Arena::with_data(data); 258 ManifestStoreReport::populate_node(&mut tree, store, claim, &root_token, name_only)?; 259 Ok((tree, root_token)) 260 } 261 } 262 263 impl std::fmt::Display for ManifestStoreReport { 264 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 265 f.write_str(&self.to_json()) 266 } 267 } 268 269 #[derive(Serialize, Deserialize, Debug, Default)] 270 struct ManifestReport { 271 claim: Value, 272 assertion_store: HashMap<String, Value>, 273 #[serde(skip_serializing_if = "Option::is_none")] 274 credential_store: Option<Vec<Value>>, 275 signature: SignatureReport, 276 } 277 278 impl ManifestReport { 279 fn from_claim(claim: &Claim) -> Result<Self> { 280 let mut assertion_store = HashMap::<String, Value>::new(); 281 let claim_assertions = claim.claim_assertion_store(); 282 for claim_assertion in claim_assertions.iter() { 283 let hashlink = claim_assertion.label(); 284 let (label, instance) = Claim::assertion_label_from_link(&hashlink); 285 let label = Claim::label_with_instance(&label, instance); 286 let value = match claim_assertion.assertion().decode_data() { 287 AssertionData::Json(_) | AssertionData::Cbor(_) => { 288 claim_assertion.assertion().as_json_object()? // todo: this may cause data loss 289 } 290 AssertionData::Binary(x) => { 291 serde_json::to_value(format!("<omitted> len = {}", x.len()))? 292 } 293 AssertionData::Uuid(s, x) => { 294 serde_json::to_value(format!("uuid: {}, data: {}", s, base64::encode(x)))? 295 } 296 }; 297 assertion_store.insert(label, value); 298 } 299 300 // convert credential store to json values 301 let credential_store: Vec<Value> = claim 302 .get_verifiable_credentials() 303 .iter() 304 .filter_map(|d| match d { 305 AssertionData::Json(s) => serde_json::from_str(s).ok(), 306 _ => None, 307 }) 308 .collect(); 309 310 let signature = match claim.signature_info() { 311 Some(info) => SignatureReport { 312 alg: info.alg.map_or_else(String::new, |a| a.to_string()), 313 issuer: info.issuer_org, 314 time: info.date.map(|d| d.to_rfc3339()), 315 }, 316 None => SignatureReport::default(), 317 }; 318 Ok(Self { 319 claim: serde_json::to_value(claim)?, // todo: this will lose tagging info 320 assertion_store, 321 credential_store: if !credential_store.is_empty() { 322 Some(credential_store) 323 } else { 324 None 325 }, 326 signature, 327 }) 328 } 329 330 /// create a json string representation of this structure, omitting binaries 331 fn to_json(&self) -> String { 332 let mut json = serde_json::to_string_pretty(self).unwrap_or_else(|e| e.to_string()); 333 334 json = b64_tag(json, "hash"); 335 json = omit_tag(json, "pad"); 336 337 json 338 } 339 } 340 341 impl std::fmt::Display for ManifestReport { 342 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 343 f.write_str(&self.to_json()) 344 } 345 } 346 347 // used to report information from signature data 348 #[derive(Default, Debug, Deserialize, Serialize)] 349 struct SignatureReport { 350 alg: String, 351 // human readable issuing authority for this signature 352 #[serde(skip_serializing_if = "Option::is_none")] 353 issuer: Option<String>, 354 // the time the signature was created 355 #[serde(skip_serializing_if = "Option::is_none")] 356 time: Option<String>, 357 } 358 359 // replace the value of any field in the json string with a given key with the string <omitted> 360 fn omit_tag(mut json: String, tag: &str) -> String { 361 while let Some(index) = json.find(&format!("\"{tag}\": [")) { 362 if let Some(idx2) = json[index..].find(']') { 363 json = format!( 364 "{}\"{}\": \"<omitted>\"{}", 365 &json[..index], 366 tag, 367 &json[index + idx2 + 1..] 368 ); 369 } 370 } 371 json 372 } 373 374 // make a base64 hash from the value of any field in the json string with key base64 hash 375 fn b64_tag(mut json: String, tag: &str) -> String { 376 while let Some(index) = json.find(&format!("\"{tag}\": [")) { 377 if let Some(idx2) = json[index..].find(']') { 378 let idx3 = json[index..].find('[').unwrap_or_default(); // ok since we just found it 379 let bytes: Vec<u8> = 380 serde_json::from_slice(json[index + idx3..index + idx2 + 1].as_bytes()) 381 .unwrap_or_default(); 382 json = format!( 383 "{}\"{}\": \"{}\"{}", 384 &json[..index], 385 tag, 386 base64::encode(&bytes), 387 &json[index + idx2 + 1..] 388 ); 389 } 390 } 391 json 392 } 393 394 #[cfg(feature = "file_io")] 395 #[cfg(test)] 396 mod tests { 397 #![allow(clippy::expect_used)] 398 399 #[cfg(feature = "v1_api")] 400 use std::fs; 401 402 use crate::{manifest_store_report::ManifestStoreReport, utils::test::fixture_path}; 403 404 #[test] 405 fn manifest_store_report() { 406 let path = fixture_path("CIE-sig-CA.jpg"); 407 let report = ManifestStoreReport::from_file(path).expect("load_from_asset"); 408 println!("{report}"); 409 } 410 411 #[test] 412 #[cfg(feature = "v1_api")] 413 fn manifest_get_certchain_from_bytes() { 414 let bytes = fs::read(fixture_path("CA.jpg")).expect("missing test asset"); 415 assert!(ManifestStoreReport::cert_chain_from_bytes("jpg", &bytes).is_ok()) 416 } 417 418 #[test] 419 #[cfg(feature = "v1_api")] 420 fn manifest_get_certchain_from_bytes_no_manifest_err() { 421 let bytes = fs::read(fixture_path("no_manifest.jpg")).expect("missing test asset"); 422 assert!(matches!( 423 ManifestStoreReport::cert_chain_from_bytes("jpg", &bytes), 424 Err(crate::Error::JumbfNotFound) 425 )) 426 } 427 428 #[test] 429 #[cfg(feature = "file_io")] 430 #[cfg(feature = "v1_api")] 431 fn manifest_dump_tree() { 432 let asset_name = "CA.jpg"; 433 let path = fixture_path(asset_name); 434 435 ManifestStoreReport::dump_tree(path).expect("dump_tree"); 436 } 437 438 #[test] 439 #[cfg(feature = "file_io")] 440 #[cfg(feature = "v1_api")] 441 fn manifest_dump_certchain() { 442 let asset_name = "CA.jpg"; 443 let path = fixture_path(asset_name); 444 445 ManifestStoreReport::dump_cert_chain(path).expect("dump certs"); 446 } 447 448 #[test] 449 #[cfg(feature = "file_io")] 450 #[cfg(feature = "v1_api")] 451 fn manifest_get_certchain() { 452 let asset_name = "CA.jpg"; 453 let path = fixture_path(asset_name); 454 assert!(ManifestStoreReport::cert_chain(path).is_ok()) 455 } 456 457 #[test] 458 #[cfg(feature = "file_io")] 459 #[cfg(feature = "v1_api")] 460 fn manifest_get_certchain_no_manifest_err() { 461 let asset_name = "no_manifest.jpg"; 462 let path = fixture_path(asset_name); 463 assert!(matches!( 464 ManifestStoreReport::cert_chain(path), 465 Err(crate::Error::JumbfNotFound) 466 )) 467 } 468 }