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 }