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

commit 9023ce53a52ee4bccd62c8f634b27c95b6257cbb
parent 55b09b3dc2233f2669a79ca5c91372f53d1ebe42
Author: mauricefisher64 <92736594+mauricefisher64@users.noreply.github.com>
Date:   Wed,  4 Oct 2023 15:28:18 -0400

Support for validating JPEGs that contain MPF (multi-picture format). (#317)

* Support for validating JPEGs that contain MPF (multi-picture format).
Allow box hashing to contain arbitrary data between segments if they are contained within a range.

* Add temporary exception allowing unmaintained `json` crate to pass validation

See tracking issue #318.

---------

Co-authored-by: Eric Scouten <scouten@adobe.com>
Diffstat:
Mdeny.toml | 1+
Msdk/Cargo.toml | 1+
Msdk/src/assertions/box_hash.rs | 5++++-
Msdk/src/asset_handlers/jpeg_io.rs | 488++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
4 files changed, 313 insertions(+), 182 deletions(-)

diff --git a/deny.toml b/deny.toml @@ -22,6 +22,7 @@ ignore = [ "RUSTSEC-2020-0071", # time 0.1.45 (see https://github.com/indygreg/cryptography-rs/issues/10) "RUSTSEC-2021-0127", # serde_cbor "RUSTSEC-2021-0146", # twoway (see https://github.com/contentauth/c2pa-rs/issues/234) + "RUSTSEC-2022-0081", # json (see https://github.com/contentauth/c2pa-rs/issues/318) ] # Deny multiple versions unless explicitly skipped. diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml @@ -78,6 +78,7 @@ fast-xml = "0.23.1" hex = "0.4.3" id3 = "1.7.0" img-parts = "0.3.0" +jfifdump = "0.4.0" log = "0.4.8" lopdf = { version = "0.31.0", optional = true } lazy_static = "1.4.0" diff --git a/sdk/src/assertions/box_hash.rs b/sdk/src/assertions/box_hash.rs @@ -125,8 +125,11 @@ impl BoxHash { skip_c2pa = true; } } else { + // count any unknown data between named segments + let len_to_this_seg = + next_source_bm.range_start - inclusion.start(); // update item - inclusion.set_length(inclusion.length() + next_source_bm.range_len); + inclusion.set_length(len_to_this_seg + next_source_bm.range_len); } } else { return Err(Error::HashMismatch( diff --git a/sdk/src/asset_handlers/jpeg_io.rs b/sdk/src/asset_handlers/jpeg_io.rs @@ -22,7 +22,7 @@ use std::{ use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use img_parts::{ jpeg::{ - markers::{self, P, RST0, RST7, Z}, + markers::{self, APP0, APP15, COM, DQT, DRI, P, RST0, RST7, SOF0, SOF15, SOS, Z}, Jpeg, JpegSegment, }, Bytes, DynImage, @@ -592,7 +592,8 @@ fn in_entropy(marker: u8) -> bool { // img-parts does not correctly return the true size of the SOS segment. This utility // finds the correct break point for single image JPEGs. We will need a new JPEG decoder // to handle those. Also this function can be removed if img-parts ever addresses this issue -fn get_entropy_size(input_stream: &mut dyn CAIRead, projected_len: usize) -> Result<usize> { +// and support MPF JPEGs. +fn get_entropy_size(input_stream: &mut dyn CAIRead) -> Result<usize> { // Search the entropy data looking for non entropy segment marker. The first valid seg marker before we hit // end of the file. @@ -616,191 +617,163 @@ fn get_entropy_size(input_stream: &mut dyn CAIRead, projected_len: usize) -> Res } Err(e) => return Err(Error::IoError(e)), } - if size > projected_len { - break; - } } Ok(size) } -impl AssetBoxHash for JpegIO { - fn get_box_map(&self, input_stream: &mut dyn CAIRead) -> Result<Vec<BoxMap>> { - let segment_names = HashMap::from([ - (0xe0u8, "APP0"), - (0xe1u8, "APP1"), - (0xe2u8, "APP2"), - (0xe3u8, "APP3"), - (0xe4u8, "APP4"), - (0xe5u8, "APP5"), - (0xe6u8, "APP6"), - (0xe7u8, "APP7"), - (0xe8u8, "APP8"), - (0xe9u8, "APP9"), - (0xeau8, "APP10"), - (0xebu8, "APP11"), - (0xecu8, "APP12"), - (0xedu8, "APP13"), - (0xeeu8, "APP14"), - (0xefu8, "APP15"), - (0xfeu8, "COM"), - (0xc4u8, "DHT"), - (0xdbu8, "DQT"), - (0xddu8, "DRI"), - (0xd9u8, "EOI"), - (0xd0u8, "RST0"), - (0xd1u8, "RST1"), - (0xd2u8, "RST2"), - (0xd3u8, "RST3"), - (0xd4u8, "RST4"), - (0xd5u8, "RST5"), - (0xd6u8, "RST6"), - (0xd7u8, "RST7"), - (0xc0u8, "SOF0"), - (0xc2u8, "SOF2"), - (0xd8u8, "SOI"), - (0xdau8, "SOS"), - (0xf0u8, "JPG0"), - (0xf1u8, "JPG1"), - (0xf2u8, "JPG2"), - (0xf3u8, "JPG3"), - (0xf4u8, "JPG4"), - (0xf5u8, "JPG5"), - (0xf6u8, "JPG6"), - (0xf7u8, "JPG7"), - (0xf8u8, "JPG8"), - (0xf9u8, "JPG9"), - (0xfau8, "JPG10"), - (0xfbu8, "JPG11"), - (0xfcu8, "JPG12"), - (0xfdu8, "JPG13"), - ]); - - let mut cai_en: Vec<u8> = Vec::new(); - let mut cai_seg_cnt: u32 = 0; - - let mut box_maps = Vec::new(); - let mut curr_offset = 2; // start after JPEG marker - - // load the bytes - let mut buf: Vec<u8> = Vec::new(); - input_stream.rewind()?; - input_stream.read_to_end(&mut buf).map_err(Error::IoError)?; - - // add first Map object contain SOI - let soi_bm = BoxMap { - names: vec!["SOI".to_string()], - alg: None, - hash: ByteBuf::from(Vec::new()), - pad: ByteBuf::from(Vec::new()), - range_start: 0, - range_len: 2, - }; - box_maps.push(soi_bm); - - let mut c2pa_bm_index = 0; - - let dimg = DynImage::from_bytes(buf.into()) - .map_err(|e| Error::OtherError(Box::new(e)))? - .ok_or(Error::UnsupportedType)?; - - match dimg { - DynImage::Jpeg(jpeg) => { - for seg in jpeg.segments() { - match seg.marker() { - markers::APP11 => { - // JUMBF marker - let raw_bytes = seg.contents(); - - if raw_bytes.len() > 16 { - // we need at least 16 bytes in each segment for CAI - let mut raw_vec = raw_bytes.to_vec(); - let _ci = raw_vec.as_mut_slice()[0..2].to_vec(); - let en = raw_vec.as_mut_slice()[2..4].to_vec(); - - let is_cai_continuation = vec_compare(&cai_en, &en); - - if cai_seg_cnt > 0 && is_cai_continuation { - cai_seg_cnt += 1; - - let c2pa_bm = box_maps.get_mut(c2pa_bm_index).ok_or( - Error::InvalidAsset("Invalid C2PA segment".to_owned()), - )?; +fn has_length(marker: u8) -> bool { + matches!(marker, RST0..=RST7 | APP0..=APP15 | SOF0..=SOF15 | SOS | COM | DQT | DRI) +} - // update c2pa box map - c2pa_bm.range_len += seg.len_with_entropy(); - } else { - // check if this is a CAI JUMBF block - let jumb_type = raw_vec.as_mut_slice()[24..28].to_vec(); - let is_cai = vec_compare(&C2PA_MARKER, &jumb_type); - if is_cai { - cai_seg_cnt = 1; - cai_en = en.clone(); // store the identifier +fn get_seg_size(input_stream: &mut dyn CAIRead) -> Result<usize> { + let p = input_stream.read_u8()?; + let marker = if p == P { + input_stream.read_u8()? + } else { + return Err(Error::InvalidAsset( + "Cannot read segment marker".to_string(), + )); + }; + + if has_length(marker) { + let val: usize = input_stream.read_u16::<BigEndian>()? as usize; + Ok(val + 2) + } else { + Ok(2) + } +} - let c2pa_bm = BoxMap { - names: vec![C2PA_BOXHASH.to_string()], - alg: None, - hash: ByteBuf::from(Vec::new()), - pad: ByteBuf::from(Vec::new()), - range_start: curr_offset, - range_len: seg.len_with_entropy(), - }; +fn make_box_maps(input_stream: &mut dyn CAIRead) -> Result<Vec<BoxMap>> { + let segment_names = HashMap::from([ + (0xe0u8, "APP0"), + (0xe1u8, "APP1"), + (0xe2u8, "APP2"), + (0xe3u8, "APP3"), + (0xe4u8, "APP4"), + (0xe5u8, "APP5"), + (0xe6u8, "APP6"), + (0xe7u8, "APP7"), + (0xe8u8, "APP8"), + (0xe9u8, "APP9"), + (0xeau8, "APP10"), + (0xebu8, "APP11"), + (0xecu8, "APP12"), + (0xedu8, "APP13"), + (0xeeu8, "APP14"), + (0xefu8, "APP15"), + (0xfeu8, "COM"), + (0xc4u8, "DHT"), + (0xdbu8, "DQT"), + (0xddu8, "DRI"), + (0xd9u8, "EOI"), + (0xd0u8, "RST0"), + (0xd1u8, "RST1"), + (0xd2u8, "RST2"), + (0xd3u8, "RST3"), + (0xd4u8, "RST4"), + (0xd5u8, "RST5"), + (0xd6u8, "RST6"), + (0xd7u8, "RST7"), + (0xc0u8, "SOF0"), + (0xc1u8, "SOF1"), + (0xc2u8, "SOF2"), + (0xd8u8, "SOI"), + (0xdau8, "SOS"), + (0xf0u8, "JPG0"), + (0xf1u8, "JPG1"), + (0xf2u8, "JPG2"), + (0xf3u8, "JPG3"), + (0xf4u8, "JPG4"), + (0xf5u8, "JPG5"), + (0xf6u8, "JPG6"), + (0xf7u8, "JPG7"), + (0xf8u8, "JPG8"), + (0xf9u8, "JPG9"), + (0xfau8, "JPG10"), + (0xfbu8, "JPG11"), + (0xfcu8, "JPG12"), + (0xfdu8, "JPG13"), + ]); + + let mut box_maps = Vec::new(); + let mut cai_en: Vec<u8> = Vec::new(); + let mut cai_seg_cnt: u32 = 0; + let mut cai_index = 0; - box_maps.push(c2pa_bm); + input_stream.rewind()?; - c2pa_bm_index = box_maps.len() - 1; - } else { - let name = segment_names.get(&seg.marker()).ok_or( - Error::InvalidAsset( - "Unknown segment marker".to_owned(), - ), - )?; - - let bm = BoxMap { - names: vec![name.to_string()], - alg: None, - hash: ByteBuf::from(Vec::new()), - pad: ByteBuf::from(Vec::new()), - range_start: curr_offset, - range_len: seg.len_with_entropy(), - }; + let buf_reader = BufReader::new(input_stream); + let mut reader = jfifdump::Reader::new(buf_reader) + .map_err(|_e| Error::InvalidAsset("could not read JPEG segments".to_string()))?; + + while let Ok(seg) = reader.next_segment() { + match seg.kind { + jfifdump::SegmentKind::Eoi => { + let bm = BoxMap { + names: vec!["EOI".to_string()], + alg: None, + hash: ByteBuf::from(Vec::new()), + pad: ByteBuf::from(Vec::new()), + range_start: seg.position - 2, + range_len: 0, + }; + + box_maps.push(bm); + } + jfifdump::SegmentKind::Soi => { + let bm = BoxMap { + names: vec!["SOI".to_string()], + alg: None, + hash: ByteBuf::from(Vec::new()), + pad: ByteBuf::from(Vec::new()), + range_start: seg.position - 2, + range_len: 0, + }; + + box_maps.push(bm); + } + jfifdump::SegmentKind::App { nr, data } if nr == 0x0b => { + let nr = nr | 0xe0; - box_maps.push(bm); - } - } - } - } - markers::SOS => { - // workaround for img-parts returning wrong segment len when entropy is present + // JUMBF marker + let raw_bytes = data; - // move pointer to beginning of segment - input_stream - .seek(std::io::SeekFrom::Start((curr_offset + seg.len()) as u64))?; + if raw_bytes.len() > 16 { + // we need at least 16 bytes in each segment for CAI + let mut raw_vec = raw_bytes.to_vec(); + let _ci = raw_vec.as_mut_slice()[0..2].to_vec(); + let en = raw_vec.as_mut_slice()[2..4].to_vec(); - let size = - get_entropy_size(input_stream, seg.len_with_entropy() - seg.len())? - + seg.len(); + let is_cai_continuation = vec_compare(&cai_en, &en); - let name = segment_names - .get(&seg.marker()) - .ok_or(Error::InvalidAsset("Unknown segment marker".to_owned()))?; + if cai_seg_cnt > 0 && is_cai_continuation { + cai_seg_cnt += 1; - let bm = BoxMap { - names: vec![name.to_string()], + let cai_bm = &mut box_maps[cai_index]; + cai_bm.range_len += raw_bytes.len() + 4; + } else { + // check if this is a CAI JUMBF block + let jumb_type = raw_vec.as_mut_slice()[24..28].to_vec(); + let is_cai = vec_compare(&C2PA_MARKER, &jumb_type); + if is_cai { + cai_seg_cnt = 1; + cai_en = en.clone(); // store the identifier + + let c2pa_bm = BoxMap { + names: vec![C2PA_BOXHASH.to_string()], alg: None, hash: ByteBuf::from(Vec::new()), pad: ByteBuf::from(Vec::new()), - range_start: curr_offset, - range_len: size, + range_start: seg.position - 2, + range_len: raw_bytes.len() + 4, }; - box_maps.push(bm); - curr_offset += size; - continue; - } - _ => { + box_maps.push(c2pa_bm); + cai_index = box_maps.len() - 1; + } else { let name = segment_names - .get(&seg.marker()) + .get(&nr) .ok_or(Error::InvalidAsset("Unknown segment marker".to_owned()))?; let bm = BoxMap { @@ -808,29 +781,182 @@ impl AssetBoxHash for JpegIO { alg: None, hash: ByteBuf::from(Vec::new()), pad: ByteBuf::from(Vec::new()), - range_start: curr_offset, - range_len: seg.len_with_entropy(), + range_start: seg.position - 2, + range_len: 0, }; box_maps.push(bm); } } - curr_offset += seg.len_with_entropy(); } } - _ => return Err(Error::InvalidAsset("Unknown image format".to_owned())), + jfifdump::SegmentKind::App { nr, data } => { + let nr = nr | 0xe0; + let _data = data; + + let name = segment_names + .get(&nr) + .ok_or(Error::InvalidAsset("Unknown segment marker".to_owned()))?; + + let bm = BoxMap { + names: vec![name.to_string()], + alg: None, + hash: ByteBuf::from(Vec::new()), + pad: ByteBuf::from(Vec::new()), + range_start: seg.position - 2, + range_len: 0, + }; + + box_maps.push(bm); + } + jfifdump::SegmentKind::App0Jfif(_) => { + let bm = BoxMap { + names: vec!["APP0".to_string()], + alg: None, + hash: ByteBuf::from(Vec::new()), + pad: ByteBuf::from(Vec::new()), + range_start: seg.position - 2, + range_len: 0, + }; + + box_maps.push(bm); + } + jfifdump::SegmentKind::Dqt(_) => { + let bm = BoxMap { + names: vec!["DQT".to_string()], + alg: None, + hash: ByteBuf::from(Vec::new()), + pad: ByteBuf::from(Vec::new()), + range_start: seg.position - 2, + range_len: 0, + }; + + box_maps.push(bm); + } + jfifdump::SegmentKind::Dht(_) => { + let bm = BoxMap { + names: vec!["DHT".to_string()], + alg: None, + hash: ByteBuf::from(Vec::new()), + pad: ByteBuf::from(Vec::new()), + range_start: seg.position - 2, + range_len: 0, + }; + + box_maps.push(bm); + } + jfifdump::SegmentKind::Dac(_) => { + let bm = BoxMap { + names: vec!["DAC".to_string()], + alg: None, + hash: ByteBuf::from(Vec::new()), + pad: ByteBuf::from(Vec::new()), + range_start: seg.position - 2, + range_len: 0, + }; + + box_maps.push(bm); + } + jfifdump::SegmentKind::Frame(f) => { + let name = segment_names + .get(&f.sof) + .ok_or(Error::InvalidAsset("Unknown segment marker".to_owned()))?; + + let bm = BoxMap { + names: vec![name.to_string()], + alg: None, + hash: ByteBuf::from(Vec::new()), + pad: ByteBuf::from(Vec::new()), + range_start: seg.position - 2, + range_len: 0, + }; + + box_maps.push(bm); + } + jfifdump::SegmentKind::Scan(_s) => { + let bm = BoxMap { + names: vec!["SOS".to_string()], + alg: None, + hash: ByteBuf::from(Vec::new()), + pad: ByteBuf::from(Vec::new()), + range_start: seg.position - 2, + range_len: 0, + }; + + box_maps.push(bm); + } + jfifdump::SegmentKind::Dri(_) => { + let bm = BoxMap { + names: vec!["DRI".to_string()], + alg: None, + hash: ByteBuf::from(Vec::new()), + pad: ByteBuf::from(Vec::new()), + range_start: seg.position - 2, + range_len: 0, + }; + + box_maps.push(bm); + } + jfifdump::SegmentKind::Rst(_r) => (), + jfifdump::SegmentKind::Comment(_) => { + let bm = BoxMap { + names: vec!["COM".to_string()], + alg: None, + hash: ByteBuf::from(Vec::new()), + pad: ByteBuf::from(Vec::new()), + range_start: seg.position - 2, + range_len: 0, + }; + + box_maps.push(bm); + } + jfifdump::SegmentKind::Unknown { marker, data: _ } => { + let name = segment_names + .get(&marker) + .ok_or(Error::InvalidAsset("Unknown segment marker".to_owned()))?; + + let bm = BoxMap { + names: vec![name.to_string()], + alg: None, + hash: ByteBuf::from(Vec::new()), + pad: ByteBuf::from(Vec::new()), + range_start: seg.position - 2, + range_len: 0, + }; + + box_maps.push(bm); + } } + } + + Ok(box_maps) +} + +impl AssetBoxHash for JpegIO { + fn get_box_map(&self, input_stream: &mut dyn CAIRead) -> Result<Vec<BoxMap>> { + let mut box_maps = make_box_maps(input_stream)?; + + for bm in box_maps.iter_mut() { + if bm.names[0] == C2PA_BOXHASH { + continue; + } + + input_stream.seek(std::io::SeekFrom::Start(bm.range_start as u64))?; + + let size = if bm.names[0] == "SOS" { + let mut size = get_seg_size(input_stream)?; - // add last segment - let eoi_bm = BoxMap { - names: vec!["EOI".to_string()], - alg: None, - hash: ByteBuf::from(Vec::new()), - pad: ByteBuf::from(Vec::new()), - range_start: curr_offset, - range_len: 2, - }; - box_maps.push(eoi_bm); + input_stream.seek(std::io::SeekFrom::Start((bm.range_start + size) as u64))?; + + size += get_entropy_size(input_stream)?; + + size + } else { + get_seg_size(input_stream)? + }; + + bm.range_len = size; + } Ok(box_maps) }