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:
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)
}