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

box_hash.rs (14629B)


      1 // Copyright 2023 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::{fs::File, io::Cursor, path::*};
     15 
     16 use serde::{Deserialize, Serialize};
     17 use serde_bytes::ByteBuf;
     18 
     19 use crate::{
     20     assertion::{Assertion, AssertionBase, AssertionCbor, AssertionJson},
     21     assertions::labels,
     22     asset_io::{AssetBoxHash, CAIRead},
     23     error::{Error, Result},
     24     utils::hash_utils::{hash_stream_by_alg, verify_stream_by_alg, HashRange},
     25     validation_status::ASSERTION_BOXHASH_UNKNOWN,
     26 };
     27 
     28 const ASSERTION_CREATION_VERSION: usize = 1;
     29 
     30 pub const C2PA_BOXHASH: &str = "C2PA";
     31 
     32 #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
     33 pub struct BoxMap {
     34     pub names: Vec<String>,
     35 
     36     #[serde(skip_serializing_if = "Option::is_none")]
     37     pub alg: Option<String>,
     38 
     39     pub hash: ByteBuf,
     40     pub pad: ByteBuf,
     41 
     42     #[serde(skip)]
     43     pub range_start: usize,
     44 
     45     #[serde(skip)]
     46     pub range_len: usize,
     47 }
     48 
     49 /// Helper class to create BoxHash assertion
     50 #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
     51 pub struct BoxHash {
     52     boxes: Vec<BoxMap>,
     53 }
     54 
     55 impl BoxHash {
     56     pub const LABEL: &'static str = labels::BOX_HASH;
     57 
     58     pub fn verify_hash(
     59         &self,
     60         asset_path: &Path,
     61         alg: Option<&str>,
     62         bhp: &dyn AssetBoxHash,
     63     ) -> Result<()> {
     64         let mut file = File::open(asset_path)?;
     65 
     66         self.verify_stream_hash(&mut file, alg, bhp)
     67     }
     68 
     69     pub fn verify_in_memory_hash(
     70         &self,
     71         data: &[u8],
     72         alg: Option<&str>,
     73         bhp: &dyn AssetBoxHash,
     74     ) -> Result<()> {
     75         let mut reader = Cursor::new(data);
     76 
     77         self.verify_stream_hash(&mut reader, alg, bhp)
     78     }
     79 
     80     pub fn verify_stream_hash(
     81         &self,
     82         reader: &mut dyn CAIRead,
     83         alg: Option<&str>,
     84         bhp: &dyn AssetBoxHash,
     85     ) -> Result<()> {
     86         // it is a failure if no hashes are listed
     87         if self.boxes.is_empty() {
     88             return Err(Error::HashMismatch("No box hash found".to_string()));
     89         }
     90 
     91         // get source box list
     92         let source_bms = bhp.get_box_map(reader)?;
     93         let mut source_index = 0;
     94 
     95         // check to see we source index starts at PNGh and skip if not included in the hash list
     96         if let Some(first_expected_bms) = source_bms.get(source_index) {
     97             if first_expected_bms.names[0] == "PNGh" && self.boxes[0].names[0] != "PNGh" {
     98                 source_index += 1;
     99             }
    100         } else {
    101             return Err(Error::HashMismatch("No data boxes found".to_string()));
    102         }
    103 
    104         for bm in &self.boxes {
    105             let mut inclusions = Vec::new();
    106 
    107             // build up current inclusion, consuming all names in this BoxMap
    108             let mut skip_c2pa = false;
    109             let mut inclusion = HashRange::new(0, 0);
    110             for name in &bm.names {
    111                 match source_bms.get(source_index) {
    112                     Some(next_source_bm) => {
    113                         if name == &next_source_bm.names[0] {
    114                             if inclusion.length() == 0 {
    115                                 // this is a new item
    116                                 inclusion.set_start(next_source_bm.range_start);
    117                                 inclusion.set_length(next_source_bm.range_len);
    118 
    119                                 if name == C2PA_BOXHASH {
    120                                     // there should only be 1 collapsed C2PA range
    121                                     if bm.names.len() != 1 {
    122                                         return Err(Error::HashMismatch(
    123                                             "Malformed C2PA box hash".to_owned(),
    124                                         ));
    125                                     }
    126                                     skip_c2pa = true;
    127                                 }
    128                             } else {
    129                                 // count any unknown data between named segments
    130                                 let len_to_this_seg =
    131                                     next_source_bm.range_start - inclusion.start();
    132                                 // update item
    133                                 inclusion.set_length(len_to_this_seg + next_source_bm.range_len);
    134                             }
    135                         } else {
    136                             return Err(Error::HashMismatch(ASSERTION_BOXHASH_UNKNOWN.to_owned()));
    137                         }
    138                     }
    139                     None => return Err(Error::HashMismatch(ASSERTION_BOXHASH_UNKNOWN.to_owned())),
    140                 }
    141                 source_index += 1;
    142             }
    143 
    144             // C2PA chunks are skipped for hashing purposes
    145             if skip_c2pa {
    146                 continue;
    147             }
    148 
    149             inclusions.push(inclusion);
    150 
    151             let curr_alg = match &bm.alg {
    152                 Some(a) => a.clone(),
    153                 None => match alg {
    154                     Some(a) => a.to_owned(),
    155                     None => return Err(Error::HashMismatch("No algorithm specified".to_string())),
    156                 },
    157             };
    158 
    159             if !verify_stream_by_alg(&curr_alg, &bm.hash, reader, Some(inclusions), false) {
    160                 return Err(Error::HashMismatch("Hashes do not match".to_owned()));
    161             }
    162         }
    163 
    164         Ok(())
    165     }
    166 
    167     #[allow(dead_code)]
    168     pub fn generate_box_hash_from_stream(
    169         &mut self,
    170         reader: &mut dyn CAIRead,
    171         alg: &str,
    172         bhp: &dyn AssetBoxHash,
    173         minimal_form: bool,
    174     ) -> Result<()> {
    175         // get source box list
    176         let source_bms = bhp.get_box_map(reader)?;
    177 
    178         if minimal_form {
    179             let mut before_c2pa = BoxMap {
    180                 names: Vec::new(),
    181                 alg: Some(alg.to_string()),
    182                 hash: ByteBuf::from(vec![]),
    183                 pad: ByteBuf::from(vec![]),
    184                 range_start: 0,
    185                 range_len: 0,
    186             };
    187 
    188             let mut c2pa_box = BoxMap {
    189                 names: Vec::new(),
    190                 alg: Some(alg.to_string()),
    191                 hash: ByteBuf::from(vec![]),
    192                 pad: ByteBuf::from(vec![]),
    193                 range_start: 0,
    194                 range_len: 0,
    195             };
    196 
    197             let mut after_c2pa = BoxMap {
    198                 names: Vec::new(),
    199                 alg: Some(alg.to_string()),
    200                 hash: ByteBuf::from(vec![]),
    201                 pad: ByteBuf::from(vec![]),
    202                 range_start: 0,
    203                 range_len: 0,
    204             };
    205 
    206             let mut is_before_c2pa = true;
    207 
    208             // collapse map list to minimal set
    209             for bm in source_bms.into_iter() {
    210                 if bm.names[0] == "C2PA" {
    211                     // there should only be 1 collapsed C2PA range
    212                     if bm.names.len() != 1 {
    213                         return Err(Error::HashMismatch("Malformed C2PA box hash".to_owned()));
    214                     }
    215 
    216                     c2pa_box = bm;
    217                     is_before_c2pa = false;
    218                     continue;
    219                 }
    220 
    221                 if is_before_c2pa {
    222                     before_c2pa.names.extend(bm.names);
    223                     if before_c2pa.range_len == 0 {
    224                         before_c2pa.range_start = bm.range_start;
    225                         before_c2pa.range_len = bm.range_len;
    226                     } else {
    227                         before_c2pa.range_len += bm.range_len;
    228                     }
    229                 } else {
    230                     after_c2pa.names.extend(bm.names);
    231                     if after_c2pa.range_len == 0 {
    232                         after_c2pa.range_start = bm.range_start;
    233                         after_c2pa.range_len = bm.range_len;
    234                     } else {
    235                         after_c2pa.range_len += bm.range_len;
    236                     }
    237                 }
    238             }
    239 
    240             self.boxes = vec![before_c2pa, c2pa_box, after_c2pa];
    241 
    242             // compute the hashes
    243             for bm in self.boxes.iter_mut() {
    244                 // skip c2pa box
    245                 if bm.names[0] == C2PA_BOXHASH {
    246                     continue;
    247                 }
    248 
    249                 let mut inclusions = Vec::new();
    250 
    251                 let inclusion = HashRange::new(bm.range_start, bm.range_len);
    252                 inclusions.push(inclusion);
    253 
    254                 bm.hash = ByteBuf::from(hash_stream_by_alg(alg, reader, Some(inclusions), false)?);
    255             }
    256         } else {
    257             for mut bm in source_bms {
    258                 if bm.names[0] == "C2PA" {
    259                     // there should only be 1 collapsed C2PA range
    260                     if bm.names.len() != 1 {
    261                         return Err(Error::HashMismatch("Malformed C2PA box hash".to_owned()));
    262                     }
    263                     bm.hash = ByteBuf::from(vec![0]);
    264                     bm.pad = ByteBuf::from(vec![]);
    265                     self.boxes.push(bm);
    266                     continue;
    267                 }
    268 
    269                 // this is a new item
    270                 let mut inclusions = Vec::new();
    271 
    272                 let inclusion = HashRange::new(bm.range_start, bm.range_len);
    273                 inclusions.push(inclusion);
    274 
    275                 bm.alg = Some(alg.to_string());
    276                 bm.hash = ByteBuf::from(hash_stream_by_alg(alg, reader, Some(inclusions), false)?);
    277                 bm.pad = ByteBuf::from(vec![]);
    278 
    279                 self.boxes.push(bm);
    280             }
    281         }
    282 
    283         Ok(())
    284     }
    285 }
    286 
    287 impl AssertionCbor for BoxHash {}
    288 
    289 impl AssertionJson for BoxHash {}
    290 
    291 impl AssertionBase for BoxHash {
    292     const LABEL: &'static str = Self::LABEL;
    293     const VERSION: Option<usize> = Some(ASSERTION_CREATION_VERSION);
    294 
    295     fn to_assertion(&self) -> crate::error::Result<Assertion> {
    296         Self::to_cbor_assertion(self)
    297     }
    298 
    299     fn from_assertion(assertion: &Assertion) -> crate::error::Result<Self> {
    300         Self::from_cbor_assertion(assertion)
    301     }
    302 }
    303 
    304 #[cfg(feature = "file_io")]
    305 #[cfg(test)]
    306 mod tests {
    307     #![allow(clippy::unwrap_used)]
    308 
    309     use super::*;
    310     #[cfg(test)]
    311     use crate::{jumbf_io::get_assetio_handler_from_path, utils::test::fixture_path};
    312 
    313     #[test]
    314     fn test_hash_verify_jpg() {
    315         let ap = fixture_path("CA.jpg");
    316 
    317         let bhp = get_assetio_handler_from_path(&ap)
    318             .unwrap()
    319             .asset_box_hash_ref()
    320             .unwrap();
    321 
    322         let mut input = File::open(&ap).unwrap();
    323 
    324         let mut bh = BoxHash { boxes: Vec::new() };
    325 
    326         // generate box hashes
    327         bh.generate_box_hash_from_stream(&mut input, "sha256", bhp, false)
    328             .unwrap();
    329 
    330         // see if they match reading
    331         bh.verify_stream_hash(&mut input, Some("sha256"), bhp)
    332             .unwrap();
    333     }
    334 
    335     #[test]
    336     fn test_hash_verify_jpg_reduced() {
    337         let ap = fixture_path("CA.jpg");
    338 
    339         let bhp = get_assetio_handler_from_path(&ap)
    340             .unwrap()
    341             .asset_box_hash_ref()
    342             .unwrap();
    343 
    344         let mut input = File::open(&ap).unwrap();
    345 
    346         let mut bh = BoxHash { boxes: Vec::new() };
    347 
    348         // generate box hashes
    349         bh.generate_box_hash_from_stream(&mut input, "sha256", bhp, true)
    350             .unwrap();
    351 
    352         // see if they match reading
    353         bh.verify_stream_hash(&mut input, Some("sha256"), bhp)
    354             .unwrap();
    355     }
    356 
    357     #[test]
    358     fn test_hash_verify_png() {
    359         let ap = fixture_path("libpng-test.png");
    360 
    361         let bhp = get_assetio_handler_from_path(&ap)
    362             .unwrap()
    363             .asset_box_hash_ref()
    364             .unwrap();
    365 
    366         let mut input = File::open(&ap).unwrap();
    367 
    368         let mut bh = BoxHash { boxes: Vec::new() };
    369 
    370         // generate box hashes
    371         bh.generate_box_hash_from_stream(&mut input, "sha256", bhp, false)
    372             .unwrap();
    373 
    374         // see if they match reading
    375         bh.verify_stream_hash(&mut input, Some("sha256"), bhp)
    376             .unwrap();
    377     }
    378 
    379     #[test]
    380     fn test_hash_verify_no_pngh() {
    381         let ap = fixture_path("libpng-test.png");
    382 
    383         let bhp = get_assetio_handler_from_path(&ap)
    384             .unwrap()
    385             .asset_box_hash_ref()
    386             .unwrap();
    387 
    388         let mut input = File::open(&ap).unwrap();
    389 
    390         let mut bh = BoxHash { boxes: Vec::new() };
    391 
    392         // generate box hashes
    393         bh.generate_box_hash_from_stream(&mut input, "sha256", bhp, false)
    394             .unwrap();
    395 
    396         bh.boxes.remove(0); // remove PNGh
    397 
    398         // see if they match reading
    399         bh.verify_stream_hash(&mut input, Some("sha256"), bhp)
    400             .unwrap();
    401     }
    402 
    403     #[test]
    404     fn test_json_round_trop() {
    405         let ap = fixture_path("CA.jpg");
    406 
    407         let bhp = get_assetio_handler_from_path(&ap)
    408             .unwrap()
    409             .asset_box_hash_ref()
    410             .unwrap();
    411 
    412         let mut input = File::open(&ap).unwrap();
    413 
    414         let mut bh = BoxHash { boxes: Vec::new() };
    415 
    416         // generate box hashes
    417         bh.generate_box_hash_from_stream(&mut input, "sha256", bhp, true)
    418             .unwrap();
    419 
    420         // save and reload JSON
    421         let bh_json_assertion = bh.to_json_assertion().unwrap();
    422         println!("Box hash json: {:?}", bh_json_assertion.decode_data());
    423 
    424         let reloaded_bh = BoxHash::from_json_assertion(&bh_json_assertion).unwrap();
    425 
    426         // see if they match reading
    427         reloaded_bh
    428             .verify_stream_hash(&mut input, Some("sha256"), bhp)
    429             .unwrap();
    430     }
    431 
    432     #[test]
    433     fn test_cbor_round_trop() {
    434         let ap = fixture_path("CA.jpg");
    435 
    436         let bhp = get_assetio_handler_from_path(&ap)
    437             .unwrap()
    438             .asset_box_hash_ref()
    439             .unwrap();
    440 
    441         let mut input = File::open(&ap).unwrap();
    442 
    443         let mut bh = BoxHash { boxes: Vec::new() };
    444 
    445         // generate box hashes
    446         bh.generate_box_hash_from_stream(&mut input, "sha256", bhp, true)
    447             .unwrap();
    448 
    449         // save and reload CBOR
    450         let bh_cbor_assertion = bh.to_cbor_assertion().unwrap();
    451         println!("Box hash cbor: {:?}", bh_cbor_assertion.decode_data());
    452 
    453         let reloaded_bh = BoxHash::from_cbor_assertion(&bh_cbor_assertion).unwrap();
    454 
    455         // see if they match reading
    456         reloaded_bh
    457             .verify_stream_hash(&mut input, Some("sha256"), bhp)
    458             .unwrap();
    459     }
    460 }