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

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 }