use std::borrow::Cow;
use thiserror::Error;
const UUID_SUFFIX: [u8; 12] = [
0x00, 0x11, 0x00, 0x10, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
];
const fn type_uuid(prefix: [u8; 4]) -> [u8; 16] {
let mut u = [0u8; 16];
u[0] = prefix[0];
u[1] = prefix[1];
u[2] = prefix[2];
u[3] = prefix[3];
let mut i = 0;
while i < 12 {
u[4 + i] = UUID_SUFFIX[i];
i += 1;
}
u
}
pub const UUID_MANIFEST_STORE: [u8; 16] = type_uuid(*b"c2pa");
pub const UUID_MANIFEST: [u8; 16] = type_uuid(*b"c2ma");
pub const UUID_LEGACY_MANIFEST: [u8; 16] = type_uuid(*b"c2md");
pub const UUID_UPDATE_MANIFEST: [u8; 16] = type_uuid(*b"c2um");
pub const UUID_COMPRESSED_MANIFEST: [u8; 16] = type_uuid(*b"c2cm");
pub const UUID_ASSERTION_STORE: [u8; 16] = type_uuid(*b"c2as");
pub const UUID_CLAIM: [u8; 16] = type_uuid(*b"c2cl");
pub const UUID_CLAIM_SIGNATURE: [u8; 16] = type_uuid(*b"c2cs");
pub const UUID_CBOR_CONTENT: [u8; 16] = type_uuid(*b"cbor");
pub const UUID_JSON_CONTENT: [u8; 16] = type_uuid(*b"json");
pub const UUID_REDACTION: [u8; 16] = [
0xCA, 0xA9, 0x8E, 0xEE, 0x9D, 0x4D, 0xF8, 0x0E, 0x86, 0xAD, 0x4D, 0xFF, 0xCA, 0x26, 0x39, 0x73,
];
const TYPE_JUMB: &[u8; 4] = b"jumb";
const TYPE_JUMD: &[u8; 4] = b"jumd";
const TYPE_CBOR: &[u8; 4] = b"cbor";
const TYPE_JSON: &[u8; 4] = b"json";
const TYPE_BROB: &[u8; 4] = b"brob";
const TYPE_FREE: &[u8; 4] = b"free";
const TYPE_UUID: &[u8; 4] = b"uuid";
const TYPE_SALT: &[u8; 4] = b"c2sh";
const LABEL_MANIFEST_STORE: &str = "c2pa";
const LABEL_ASSERTION_STORE: &str = "c2pa.assertions";
const LABEL_CLAIM_V1: &str = "c2pa.claim";
const LABEL_CLAIM_V2: &str = "c2pa.claim.v2";
const LABEL_CLAIM_SIGNATURE: &str = "c2pa.signature";
const MAX_MANIFESTS_PER_STORE: usize = 1_024;
const MAX_ASSERTIONS_PER_MANIFEST: usize = 4_096;
const MAX_CHILD_BOXES_PER_SUPERBOX: usize = MAX_ASSERTIONS_PER_MANIFEST;
const MAX_LABEL_BYTES: usize = 1_024;
const TOGGLE_REQUESTABLE: u8 = 0x01;
const TOGGLE_LABEL_PRESENT: u8 = 0x02;
const TOGGLE_PRIVATE: u8 = 0x10;
#[derive(Debug, Error, PartialEq)]
pub enum JumbfError {
#[error("truncated box header at offset {0}")]
TruncatedHeader(usize),
#[error("box length {0} overruns buffer")]
LengthOverrun(u64),
#[error("expected box type {expected:?}, found {found:?}")]
UnexpectedType {
expected: [u8; 4],
found: [u8; 4],
},
#[error("unexpected superbox uuid")]
UnexpectedUuid,
#[error("brotli decompression failed")]
DecompressionFailed,
#[error("decompressed manifest exceeds {MAX_DECOMPRESSED_MANIFEST_BYTES} bytes")]
DecompressedTooLarge,
#[error("malformed compressed manifest: {0}")]
CompressedManifestShape(&'static str),
#[error(
"compressed manifest label {compressed:?} does not match carried manifest {manifest:?}"
)]
CompressedManifestLabelMismatch {
compressed: String,
manifest: String,
},
#[error("not a C2PA manifest superbox")]
NotAManifestSuperbox,
#[error("compressed manifest {label:?} is invalid: {reason}")]
CompressedManifestInvalid {
label: String,
reason: Box<JumbfError>,
},
#[error("{0} trailing bytes after top-level JUMBF box")]
TrailingBytes(usize),
#[error("{0}")]
ResourceLimit(&'static str),
#[error("ambiguous JUMBF label: {0}")]
AmbiguousLabel(String),
#[error("expected JUMBF label {expected}, found {found}")]
UnexpectedLabel {
expected: &'static str,
found: String,
},
#[error("invalid C2PA JUMBF structure: {0}")]
InvalidStructure(&'static str),
#[error("unsupported C2PA manifest type: {0}")]
UnsupportedManifestType(&'static str),
}
fn box_bytes(box_type: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let total = 8 + payload.len();
let mut out = Vec::with_capacity(total + 8);
if (total as u64) < (1u64 << 32) {
out.extend_from_slice(&(total as u32).to_be_bytes());
out.extend_from_slice(box_type);
out.extend_from_slice(payload);
} else {
out.extend_from_slice(&1u32.to_be_bytes());
out.extend_from_slice(box_type);
out.extend_from_slice(&((total + 8) as u64).to_be_bytes());
out.extend_from_slice(payload);
}
out
}
#[cfg(test)]
fn description_box(type_uuid: &[u8; 16], label: &str, salt: Option<&[u8]>) -> Vec<u8> {
let mut toggles = TOGGLE_REQUESTABLE | TOGGLE_LABEL_PRESENT;
if salt.is_some() {
toggles |= TOGGLE_PRIVATE;
}
let mut payload = Vec::new();
payload.extend_from_slice(type_uuid);
payload.push(toggles);
payload.extend_from_slice(label.as_bytes());
payload.push(0x00);
if let Some(s) = salt {
payload.extend_from_slice(&box_bytes(b"c2sh", s));
}
box_bytes(TYPE_JUMD, &payload)
}
#[cfg(test)]
pub fn superbox(
type_uuid: &[u8; 16],
label: &str,
content: &[Vec<u8>],
salt: Option<&[u8]>,
) -> Vec<u8> {
let mut inner = description_box(type_uuid, label, salt);
for c in content {
inner.extend_from_slice(c);
}
box_bytes(TYPE_JUMB, &inner)
}
#[cfg(test)]
pub fn cbor_box(cbor: &[u8]) -> Vec<u8> {
box_bytes(TYPE_CBOR, cbor)
}
#[cfg(test)]
pub fn assertion_box(label: &str, cbor: &[u8], salt: Option<&[u8]>) -> Vec<u8> {
superbox(&UUID_CBOR_CONTENT, label, &[cbor_box(cbor)], salt)
}
#[cfg(test)]
pub fn redacted_assertion_box(label: &str, zeros: usize) -> Vec<u8> {
let mut content = UUID_REDACTION.to_vec();
content.resize(UUID_REDACTION.len() + zeros, 0);
superbox(
&UUID_CBOR_CONTENT,
label,
&[box_bytes(TYPE_UUID, &content)],
None,
)
}
pub fn superbox_content(superbox_bytes: &[u8]) -> Result<&[u8], JumbfError> {
let parsed = parse_box(superbox_bytes, 0)?;
if &parsed.box_type != TYPE_JUMB {
return Err(JumbfError::UnexpectedType {
expected: *TYPE_JUMB,
found: parsed.box_type,
});
}
Ok(parsed.payload)
}
#[cfg(test)]
pub fn build_manifest(
manifest_label: &str,
assertion_boxes: &[Vec<u8>],
claim_cbor: &[u8],
signature_cose: &[u8],
) -> Vec<u8> {
build_manifest_with_claim_label(
manifest_label,
assertion_boxes,
claim_cbor,
signature_cose,
"c2pa.claim.v2",
)
}
#[cfg(test)]
pub fn build_manifest_with_claim_label(
manifest_label: &str,
assertion_boxes: &[Vec<u8>],
claim_cbor: &[u8],
signature_cose: &[u8],
claim_label: &str,
) -> Vec<u8> {
let assertion_store = superbox(
&UUID_ASSERTION_STORE,
"c2pa.assertions",
assertion_boxes,
None,
);
let claim = superbox(&UUID_CLAIM, claim_label, &[cbor_box(claim_cbor)], None);
let sig = superbox(
&UUID_CLAIM_SIGNATURE,
"c2pa.signature",
&[cbor_box(signature_cose)],
None,
);
superbox(
&UUID_MANIFEST,
manifest_label,
&[assertion_store, claim, sig],
None,
)
}
#[cfg(test)]
pub fn build_manifest_store(manifests: &[Vec<u8>]) -> Vec<u8> {
superbox(&UUID_MANIFEST_STORE, "c2pa", manifests, None)
}
pub fn manifest_superboxes_from_store(data: &[u8]) -> Result<Vec<&[u8]>, JumbfError> {
let top = parse_manifest_store_box(data)?;
if &top.box_type != TYPE_JUMB {
return Err(JumbfError::UnexpectedType {
expected: *TYPE_JUMB,
found: top.box_type,
});
}
let store = parse_superbox_description(top.payload)?;
if store.type_uuid != UUID_MANIFEST_STORE {
return Err(JumbfError::UnexpectedUuid);
}
if store.label != LABEL_MANIFEST_STORE {
return Err(JumbfError::UnexpectedLabel {
expected: LABEL_MANIFEST_STORE,
found: store.label,
});
}
let mut pos = store.content_offset;
let mut child_count = 0usize;
let mut manifests = Vec::new();
while pos < top.payload.len() {
if child_count >= MAX_CHILD_BOXES_PER_SUPERBOX {
return Err(JumbfError::ResourceLimit(
"superbox child count exceeds verifier bound",
));
}
child_count += 1;
let b = parse_box(top.payload, pos)?;
if b.box_type == *TYPE_JUMB {
let child = parse_superbox_description(b.payload)?;
if matches!(
child.type_uuid,
UUID_MANIFEST | UUID_LEGACY_MANIFEST | UUID_UPDATE_MANIFEST
) {
if manifests.len() >= MAX_MANIFESTS_PER_STORE {
return Err(JumbfError::ResourceLimit(
"manifest count exceeds verifier bound",
));
}
manifests.push(&top.payload[pos..b.next]);
} else if child.type_uuid == UUID_COMPRESSED_MANIFEST {
return Err(JumbfError::UnsupportedManifestType("c2cm"));
}
}
pos = b.next;
}
Ok(manifests)
}
pub fn merge_manifest_stores(base: &[u8], update: &[u8]) -> Result<Vec<u8>, JumbfError> {
let base_store = parse_manifest_store_box(base)?;
let appended = manifest_superboxes_from_store(update)?;
let total = appended
.iter()
.try_fold(base_store.payload.len(), |total, manifest| {
total.checked_add(manifest.len())
})
.filter(|total| *total <= crate::MAX_MANIFEST_STORE_BYTES)
.ok_or(JumbfError::ResourceLimit(
"merged manifest store exceeds verifier byte bound",
))?;
let mut payload = Vec::with_capacity(total);
payload.extend_from_slice(base_store.payload);
for manifest in appended {
payload.extend_from_slice(manifest);
}
Ok(box_bytes(TYPE_JUMB, &payload))
}
pub const MAX_DECOMPRESSED_MANIFEST_BYTES: usize = 16 * 1024 * 1024;
struct BoundedSink {
bytes: Vec<u8>,
overflowed: bool,
}
impl std::io::Write for BoundedSink {
fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
if data.len() > MAX_DECOMPRESSED_MANIFEST_BYTES - self.bytes.len() {
self.overflowed = true;
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"decompressed manifest exceeds bound",
));
}
self.bytes.extend_from_slice(data);
Ok(data.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn brotli_decompress_bounded(stream: &[u8]) -> Result<Vec<u8>, JumbfError> {
let mut sink = BoundedSink {
bytes: Vec::new(),
overflowed: false,
};
match brotli::BrotliDecompress(&mut std::io::Cursor::new(stream), &mut sink) {
Ok(()) => Ok(sink.bytes),
Err(_) if sink.overflowed => Err(JumbfError::DecompressedTooLarge),
Err(_) => Err(JumbfError::DecompressionFailed),
}
}
#[cfg(test)]
fn brotli_compress(data: &[u8]) -> Vec<u8> {
let mut compressed = Vec::new();
let params = brotli::enc::BrotliEncoderParams::default();
brotli::BrotliCompress(&mut std::io::Cursor::new(data), &mut compressed, ¶ms)
.expect("fixture compression");
compressed
}
#[cfg(test)]
pub fn compress_manifest(manifest_superbox: &[u8]) -> Result<Vec<u8>, JumbfError> {
let parsed = parse_superbox(manifest_superbox)?;
if !matches!(parsed.type_uuid, UUID_MANIFEST | UUID_LEGACY_MANIFEST) {
return Err(JumbfError::NotAManifestSuperbox);
}
Ok(superbox(
&UUID_COMPRESSED_MANIFEST,
&parsed.label,
&[box_bytes(TYPE_BROB, &brotli_compress(manifest_superbox))],
None,
))
}
#[cfg(test)]
fn compress_manifest_kind(manifest_superbox: &[u8]) -> Result<Vec<u8>, JumbfError> {
let parsed = parse_superbox(manifest_superbox)?;
Ok(superbox(
&UUID_COMPRESSED_MANIFEST,
&parsed.label,
&[box_bytes(TYPE_BROB, &brotli_compress(manifest_superbox))],
None,
))
}
fn decompress_manifest_superbox(parsed: &Superbox<'_>) -> Result<(Vec<u8>, [u8; 16]), JumbfError> {
if parsed.type_uuid != UUID_COMPRESSED_MANIFEST {
return Err(JumbfError::UnexpectedUuid);
}
let [(box_type, stream)] = parsed.content.as_slice() else {
return Err(JumbfError::CompressedManifestShape(
"expected exactly one brob content box",
));
};
if box_type != TYPE_BROB {
return Err(JumbfError::UnexpectedType {
expected: *TYPE_BROB,
found: *box_type,
});
}
if stream.is_empty() {
return Err(JumbfError::CompressedManifestShape("brob box is empty"));
}
let manifest = brotli_decompress_bounded(stream)?;
let inner_type_uuid = {
let inner = parse_superbox(&manifest)?;
if !matches!(
inner.type_uuid,
UUID_MANIFEST | UUID_LEGACY_MANIFEST | UUID_UPDATE_MANIFEST
) {
return Err(JumbfError::NotAManifestSuperbox);
}
if inner.label != parsed.label {
return Err(JumbfError::CompressedManifestLabelMismatch {
compressed: parsed.label.clone(),
manifest: inner.label,
});
}
inner.type_uuid
};
Ok((manifest, inner_type_uuid))
}
pub fn store_has_compressed_manifest(store: &[u8]) -> bool {
let Ok(top) = parse_manifest_store_box(store) else {
return false;
};
let Ok(description) = parse_superbox_description(top.payload) else {
return false;
};
if description.type_uuid != UUID_MANIFEST_STORE {
return false;
}
let mut position = description.content_offset;
while position < top.payload.len() {
let Ok(child_box) = parse_box(top.payload, position) else {
return false;
};
if child_box.box_type == *TYPE_JUMB
&& parse_superbox_payload(child_box.payload)
.is_ok_and(|child| child.type_uuid == UUID_COMPRESSED_MANIFEST)
{
return true;
}
position = child_box.next;
}
false
}
pub fn store_carries_manifest(store: &[u8]) -> bool {
parse_manifest_store(store).is_ok_and(|parsed| !parsed.manifests.is_empty())
|| store_has_compressed_manifest(store)
}
pub struct ExpandedStore<'a> {
bytes: Cow<'a, [u8]>,
stored_contents: Vec<Option<&'a [u8]>>,
}
impl<'a> ExpandedStore<'a> {
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
pub fn expanded(&self) -> bool {
matches!(self.bytes, Cow::Owned(_))
}
pub fn stored_content(&self, index: usize) -> Option<&'a [u8]> {
self.stored_contents.get(index).copied().flatten()
}
}
pub fn expand_store(store: &[u8]) -> Result<ExpandedStore<'_>, JumbfError> {
let top = parse_manifest_store_box(store)?;
let description = parse_superbox_description(top.payload)?;
if description.type_uuid != UUID_MANIFEST_STORE {
return Err(JumbfError::UnexpectedUuid);
}
if description.label != LABEL_MANIFEST_STORE {
return Err(JumbfError::UnexpectedLabel {
expected: LABEL_MANIFEST_STORE,
found: description.label,
});
}
let mut compressed_seen = false;
let mut rebuilt_payload = top.payload[..description.content_offset].to_vec();
let mut stored_contents = Vec::new();
let mut position = description.content_offset;
while position < top.payload.len() {
let child_box = parse_box(top.payload, position)?;
if child_box.box_type != *TYPE_JUMB {
rebuilt_payload.extend_from_slice(&top.payload[position..child_box.next]);
position = child_box.next;
continue;
}
let child = parse_superbox_payload(child_box.payload)?;
if child.type_uuid == UUID_COMPRESSED_MANIFEST {
compressed_seen = true;
let (manifest, manifest_type) =
decompress_manifest_superbox(&child).map_err(|reason| {
JumbfError::CompressedManifestInvalid {
label: child.label.clone(),
reason: Box::new(reason),
}
})?;
if matches!(
manifest_type,
UUID_MANIFEST | UUID_LEGACY_MANIFEST | UUID_UPDATE_MANIFEST
) {
stored_contents.push(Some(child_box.payload));
}
rebuilt_payload.extend_from_slice(&manifest);
} else {
if matches!(
child.type_uuid,
UUID_MANIFEST | UUID_LEGACY_MANIFEST | UUID_UPDATE_MANIFEST
) {
stored_contents.push(None);
}
rebuilt_payload.extend_from_slice(&top.payload[position..child_box.next]);
}
position = child_box.next;
}
if !compressed_seen {
return Ok(ExpandedStore {
bytes: Cow::Borrowed(store),
stored_contents: Vec::new(),
});
}
Ok(ExpandedStore {
bytes: Cow::Owned(box_bytes(TYPE_JUMB, &rebuilt_payload)),
stored_contents,
})
}
struct ParsedBox<'a> {
box_type: [u8; 4],
payload: &'a [u8],
next: usize,
}
fn parse_box(data: &[u8], offset: usize) -> Result<ParsedBox<'_>, JumbfError> {
if offset + 8 > data.len() {
return Err(JumbfError::TruncatedHeader(offset));
}
let size = u32::from_be_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]);
let mut box_type = [0u8; 4];
box_type.copy_from_slice(&data[offset + 4..offset + 8]);
if size == 1 {
if offset + 16 > data.len() {
return Err(JumbfError::TruncatedHeader(offset));
}
let xl = u64::from_be_bytes([
data[offset + 8],
data[offset + 9],
data[offset + 10],
data[offset + 11],
data[offset + 12],
data[offset + 13],
data[offset + 14],
data[offset + 15],
]);
let xl_size = usize::try_from(xl).map_err(|_| JumbfError::LengthOverrun(xl))?;
let end = offset
.checked_add(xl_size)
.ok_or(JumbfError::LengthOverrun(xl))?;
if end > data.len() || xl_size < 16 {
return Err(JumbfError::LengthOverrun(xl));
}
Ok(ParsedBox {
box_type,
payload: &data[offset + 16..end],
next: end,
})
} else if size == 0 {
Ok(ParsedBox {
box_type,
payload: &data[offset + 8..],
next: data.len(),
})
} else {
let end = offset
.checked_add(size as usize)
.ok_or(JumbfError::LengthOverrun(size as u64))?;
if end > data.len() || (size as usize) < 8 {
return Err(JumbfError::LengthOverrun(size as u64));
}
Ok(ParsedBox {
box_type,
payload: &data[offset + 8..end],
next: end,
})
}
}
fn parse_manifest_store_box(data: &[u8]) -> Result<ParsedBox<'_>, JumbfError> {
let top = parse_box(data, 0)?;
if &top.box_type != TYPE_JUMB {
return Err(JumbfError::UnexpectedType {
expected: *TYPE_JUMB,
found: top.box_type,
});
}
if top.next == data.len() {
return Ok(top);
}
let trailing =
parse_box(data, top.next).map_err(|_| JumbfError::TrailingBytes(data.len() - top.next))?;
if trailing.box_type != *TYPE_FREE || trailing.next != data.len() {
return Err(JumbfError::TrailingBytes(data.len() - top.next));
}
Ok(top)
}
struct ParsedSuperboxDescription {
type_uuid: [u8; 16],
label: String,
content_offset: usize,
}
fn parse_superbox_description(payload: &[u8]) -> Result<ParsedSuperboxDescription, JumbfError> {
let desc = parse_box(payload, 0)?;
if &desc.box_type != TYPE_JUMD {
return Err(JumbfError::UnexpectedType {
expected: *TYPE_JUMD,
found: desc.box_type,
});
}
if desc.payload.len() < 18 {
return Err(JumbfError::TruncatedHeader(0));
}
let mut type_uuid = [0u8; 16];
type_uuid.copy_from_slice(&desc.payload[..16]);
let toggles = desc.payload[16];
if toggles & (TOGGLE_REQUESTABLE | TOGGLE_LABEL_PRESENT)
!= TOGGLE_REQUESTABLE | TOGGLE_LABEL_PRESENT
{
return Err(JumbfError::InvalidStructure(
"C2PA JUMBF description is not labelled and requestable",
));
}
let label_bytes = &desc.payload[17..];
let null = label_bytes
.iter()
.position(|&b| b == 0)
.ok_or(JumbfError::InvalidStructure(
"JUMBF label is not null-terminated",
))?;
if null > MAX_LABEL_BYTES {
return Err(JumbfError::ResourceLimit(
"JUMBF label exceeds verifier bound",
));
}
let label = std::str::from_utf8(&label_bytes[..null])
.map_err(|_| JumbfError::InvalidStructure("JUMBF label is not valid UTF-8"))?;
if label.is_empty()
|| label.chars().any(|character| {
character <= '\u{001f}'
|| ('\u{007f}'..='\u{009f}').contains(&character)
|| matches!(character, '/' | ';' | '?' | '#' | '\u{feff}' | '\u{ffff}')
})
{
return Err(JumbfError::InvalidStructure(
"JUMBF label contains a forbidden character",
));
}
if toggles & TOGGLE_PRIVATE != 0 {
let salt_offset = 17 + null + 1;
let salt = parse_box(desc.payload, salt_offset).map_err(|_| {
JumbfError::InvalidStructure(
"private JUMBF description requires one 16- or 32-byte c2sh box",
)
})?;
if salt.box_type != *TYPE_SALT
|| salt.next != desc.payload.len()
|| !matches!(salt.payload.len(), 16 | 32)
{
return Err(JumbfError::InvalidStructure(
"private JUMBF description requires one 16- or 32-byte c2sh box",
));
}
}
Ok(ParsedSuperboxDescription {
type_uuid,
label: label.to_owned(),
content_offset: desc.next,
})
}
pub struct Superbox<'a> {
pub type_uuid: [u8; 16],
pub label: String,
pub content: Vec<([u8; 4], &'a [u8])>,
}
fn parse_superbox_payload(payload: &[u8]) -> Result<Superbox<'_>, JumbfError> {
let description = parse_superbox_description(payload)?;
let mut content = Vec::new();
let mut pos = description.content_offset;
while pos < payload.len() {
if content.len() >= MAX_CHILD_BOXES_PER_SUPERBOX {
return Err(JumbfError::ResourceLimit(
"superbox child count exceeds verifier bound",
));
}
let b = parse_box(payload, pos)?;
content.push((b.box_type, b.payload));
pos = b.next;
}
Ok(Superbox {
type_uuid: description.type_uuid,
label: description.label,
content,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AssertionContentType {
Cbor,
Json,
}
pub fn parse_superbox(data: &[u8]) -> Result<Superbox<'_>, JumbfError> {
let b = parse_box(data, 0)?;
if &b.box_type != TYPE_JUMB {
return Err(JumbfError::UnexpectedType {
expected: *TYPE_JUMB,
found: b.box_type,
});
}
if b.next != data.len() {
return Err(JumbfError::TrailingBytes(data.len() - b.next));
}
parse_superbox_payload(b.payload)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ManifestKind {
Standard,
Update,
}
pub struct ParsedManifest<'a> {
pub label: String,
pub manifest_jumbf: &'a [u8],
pub assertions: Vec<(String, &'a [u8])>,
pub assertion_jumbf: Vec<(String, &'a [u8])>,
pub claim_cbor: Option<&'a [u8]>,
pub signature_cose: Option<&'a [u8]>,
pub claim_count: usize,
pub claim_box_label: Option<String>,
}
pub struct ParsedStore<'a> {
pub manifests: Vec<ParsedManifest<'a>>,
}
pub fn parse_manifest_store(data: &[u8]) -> Result<ParsedStore<'_>, JumbfError> {
if data.len() > crate::MAX_MANIFEST_STORE_BYTES {
return Err(JumbfError::ResourceLimit(
"manifest store exceeds verifier byte bound",
));
}
let top = parse_manifest_store_box(data)?;
let store = parse_superbox_payload(top.payload)?;
if store.type_uuid != UUID_MANIFEST_STORE {
return Err(JumbfError::UnexpectedUuid);
}
if store.label != LABEL_MANIFEST_STORE {
return Err(JumbfError::UnexpectedLabel {
expected: LABEL_MANIFEST_STORE,
found: store.label,
});
}
let mut manifests = Vec::new();
let mut manifest_labels = std::collections::BTreeSet::new();
for (ctype, cpayload) in &store.content {
if ctype == TYPE_JUMB {
if let Some(manifest) = parse_manifest(cpayload)? {
if manifests.len() >= MAX_MANIFESTS_PER_STORE {
return Err(JumbfError::ResourceLimit(
"manifest count exceeds verifier bound",
));
}
if !manifest_labels.insert(manifest.label.clone()) {
return Err(JumbfError::AmbiguousLabel(manifest.label));
}
manifests.push(manifest);
}
}
}
Ok(ParsedStore { manifests })
}
impl ParsedManifest<'_> {
pub fn kind(&self) -> ManifestKind {
parse_superbox_payload(self.manifest_jumbf)
.ok()
.filter(|manifest| manifest.type_uuid == UUID_UPDATE_MANIFEST)
.map_or(ManifestKind::Standard, |_| ManifestKind::Update)
}
pub fn assertion_content_type(&self, label: &str) -> Option<AssertionContentType> {
let (_, payload) = self
.assertion_jumbf
.iter()
.find(|(candidate, _)| candidate == label)?;
let assertion = parse_superbox_payload(payload).ok()?;
match assertion.type_uuid {
UUID_CBOR_CONTENT => Some(AssertionContentType::Cbor),
UUID_JSON_CONTENT => Some(AssertionContentType::Json),
_ => None,
}
}
pub fn assertion_content_is_zeroed(&self, label: &str) -> Option<bool> {
let (_, payload) = self
.assertion_jumbf
.iter()
.find(|(candidate, _)| candidate == label)?;
let Ok(assertion) = parse_superbox_payload(payload) else {
return Some(false);
};
Some(assertion.content.iter().all(|(box_type, content)| {
let data = match (box_type == TYPE_UUID, content.split_at_checked(16)) {
(true, Some((id, rest))) if id == UUID_REDACTION => rest,
_ => *content,
};
data.iter().all(|byte| *byte == 0)
}))
}
}
fn parse_manifest(payload: &[u8]) -> Result<Option<ParsedManifest<'_>>, JumbfError> {
let parsed = parse_superbox_payload(payload)?;
if parsed.type_uuid == UUID_COMPRESSED_MANIFEST {
return Err(JumbfError::UnsupportedManifestType("c2cm"));
}
if !matches!(
parsed.type_uuid,
UUID_MANIFEST | UUID_LEGACY_MANIFEST | UUID_UPDATE_MANIFEST
) {
return Ok(None);
}
let mut manifest = ParsedManifest {
label: parsed.label.clone(),
manifest_jumbf: payload,
assertions: Vec::new(),
assertion_jumbf: Vec::new(),
claim_cbor: None,
signature_cose: None,
claim_count: 0,
claim_box_label: None,
};
let mut assertion_labels = std::collections::BTreeSet::new();
let mut direct_child_labels = std::collections::BTreeSet::new();
let mut assertion_store_count = 0usize;
for (content_type, content_payload) in &parsed.content {
if content_type != TYPE_JUMB {
continue;
}
let inner = parse_superbox_payload(content_payload)?;
if inner.type_uuid == UUID_ASSERTION_STORE {
assertion_store_count += 1;
if assertion_store_count > 1 {
return Err(JumbfError::InvalidStructure(
"manifest contains multiple assertion store superboxes",
));
}
if inner.label != LABEL_ASSERTION_STORE {
return Err(JumbfError::UnexpectedLabel {
expected: LABEL_ASSERTION_STORE,
found: inner.label,
});
}
} else if inner.type_uuid == UUID_CLAIM
&& inner.label != LABEL_CLAIM_V1
&& inner.label != LABEL_CLAIM_V2
{
return Err(JumbfError::UnexpectedLabel {
expected: "c2pa.claim or c2pa.claim.v2",
found: inner.label,
});
} else if inner.type_uuid == UUID_CLAIM_SIGNATURE && inner.label != LABEL_CLAIM_SIGNATURE {
return Err(JumbfError::UnexpectedLabel {
expected: LABEL_CLAIM_SIGNATURE,
found: inner.label,
});
}
if !direct_child_labels.insert(inner.label.clone()) {
return Err(JumbfError::AmbiguousLabel(inner.label));
}
if inner.type_uuid == UUID_ASSERTION_STORE {
for (assertion_type, assertion_payload) in &inner.content {
if assertion_type != TYPE_JUMB {
continue;
}
let assertion = parse_superbox_payload(assertion_payload)?;
if manifest.assertion_jumbf.len() >= MAX_ASSERTIONS_PER_MANIFEST {
return Err(JumbfError::ResourceLimit(
"assertion count exceeds verifier bound",
));
}
if !assertion_labels.insert(assertion.label.clone()) {
return Err(JumbfError::AmbiguousLabel(assertion.label));
}
manifest
.assertion_jumbf
.push((assertion.label.clone(), assertion_payload));
let expected_content_type = match assertion.type_uuid {
UUID_CBOR_CONTENT => Some(TYPE_CBOR),
UUID_JSON_CONTENT => Some(TYPE_JSON),
_ => None,
};
if let Some(expected_content_type) = expected_content_type {
if let Some((_, payload)) = assertion
.content
.iter()
.find(|(box_type, _)| box_type == expected_content_type)
{
manifest.assertions.push((assertion.label.clone(), payload));
}
}
}
} else if inner.type_uuid == UUID_CLAIM {
manifest.claim_count += 1;
if inner.content.len() != 1 || inner.content[0].0 != *TYPE_CBOR {
return Err(JumbfError::InvalidStructure(
"claim superbox must contain exactly one CBOR content box",
));
}
if manifest.claim_box_label.is_none() {
manifest.claim_cbor = Some(inner.content[0].1);
manifest.claim_box_label = Some(inner.label.clone());
}
} else if inner.type_uuid == UUID_CLAIM_SIGNATURE {
if inner.content.len() != 1 || inner.content[0].0 != *TYPE_CBOR {
return Err(JumbfError::InvalidStructure(
"claim signature superbox must contain exactly one CBOR content box",
));
}
manifest.signature_cose = Some(inner.content[0].1);
}
}
if assertion_store_count == 0 {
return Err(JumbfError::InvalidStructure(
"manifest is missing required c2pa.assertions assertion store",
));
}
Ok(Some(manifest))
}
#[cfg(test)]
mod tests {
use super::*;
fn extended_box_bytes(ordinary: &[u8]) -> Vec<u8> {
assert_eq!(
u32::from_be_bytes(ordinary[..4].try_into().unwrap()) as usize,
ordinary.len()
);
let extended_size = u64::try_from(ordinary.len().checked_add(8).unwrap()).unwrap();
let mut extended = Vec::with_capacity(ordinary.len() + 8);
extended.extend_from_slice(&1u32.to_be_bytes());
extended.extend_from_slice(&ordinary[4..8]);
extended.extend_from_slice(&extended_size.to_be_bytes());
extended.extend_from_slice(&ordinary[8..]);
extended
}
fn relabel_unique(data: &mut [u8], from: &str, to: &str) {
assert_eq!(from.len(), to.len());
let offset = {
let mut matches = data
.windows(from.len())
.enumerate()
.filter_map(|(offset, candidate)| (candidate == from.as_bytes()).then_some(offset));
let offset = matches.next().expect("source label must be present");
assert!(matches.next().is_none(), "source label must be unique");
offset
};
data[offset..offset + to.len()].copy_from_slice(to.as_bytes());
}
#[test]
fn superbox_content_parses_ordinary_extended_and_size_to_end() {
let payload = b"manifest payload";
let ordinary = box_bytes(TYPE_JUMB, payload);
assert_eq!(superbox_content(&ordinary).unwrap(), payload);
let mut ordinary_with_trailing_box = ordinary.clone();
ordinary_with_trailing_box.extend_from_slice(b"ignored");
assert_eq!(
superbox_content(&ordinary_with_trailing_box).unwrap(),
payload
);
let extended = extended_box_bytes(&ordinary);
assert_eq!(superbox_content(&extended).unwrap(), payload);
let mut extended_with_trailing_box = extended.clone();
extended_with_trailing_box.extend_from_slice(b"ignored");
assert_eq!(
superbox_content(&extended_with_trailing_box).unwrap(),
payload
);
let mut size_to_end = Vec::with_capacity(payload.len() + 8);
size_to_end.extend_from_slice(&0u32.to_be_bytes());
size_to_end.extend_from_slice(TYPE_JUMB);
size_to_end.extend_from_slice(payload);
assert_eq!(superbox_content(&size_to_end).unwrap(), payload);
}
#[test]
fn superbox_content_rejects_malformed_headers_and_lengths() {
assert!(matches!(
superbox_content(&[0, 0, 0]),
Err(JumbfError::TruncatedHeader(0))
));
let wrong_type = box_bytes(b"free", b"payload");
assert!(matches!(
superbox_content(&wrong_type),
Err(JumbfError::UnexpectedType { .. })
));
let ordinary_overrun = [0, 0, 0, 9, b'j', b'u', b'm', b'b'];
assert!(matches!(
superbox_content(&ordinary_overrun),
Err(JumbfError::LengthOverrun(9))
));
let mut extended_overrun = Vec::new();
extended_overrun.extend_from_slice(&1u32.to_be_bytes());
extended_overrun.extend_from_slice(TYPE_JUMB);
extended_overrun.extend_from_slice(&17u64.to_be_bytes());
assert!(matches!(
superbox_content(&extended_overrun),
Err(JumbfError::LengthOverrun(17))
));
}
#[test]
fn manifest_superboxes_borrow_store_bytes() {
let manifest = build_manifest("urn:c2pa:borrowed", &[], &[0xa0], &[0xd2, 0x84]);
let store = build_manifest_store(std::slice::from_ref(&manifest));
let spans = manifest_superboxes_from_store(&store).unwrap();
assert_eq!(spans.as_slice(), &[manifest.as_slice()]);
let store_start = store.as_ptr() as usize;
let store_end = store_start + store.len();
let span_start = spans[0].as_ptr() as usize;
let span_end = span_start + spans[0].len();
assert!(span_start >= store_start && span_end <= store_end);
}
#[test]
fn superbox_parser_rejects_excessive_label() {
let label = "x".repeat(MAX_LABEL_BYTES + 1);
let store = superbox(&UUID_MANIFEST_STORE, &label, &[], None);
assert!(matches!(
parse_manifest_store(&store),
Err(JumbfError::ResourceLimit(
"JUMBF label exceeds verifier bound"
))
));
}
#[test]
fn manifest_superboxes_enforce_manifest_count() {
let manifests = (0..=MAX_MANIFESTS_PER_STORE)
.map(|index| {
superbox(
&UUID_MANIFEST,
&format!("urn:c2pa:manifest-{index}"),
&[],
None,
)
})
.collect::<Vec<_>>();
let store = build_manifest_store(&manifests);
assert!(matches!(
manifest_superboxes_from_store(&store),
Err(JumbfError::ResourceLimit(
"manifest count exceeds verifier bound"
))
));
}
#[test]
fn superbox_parser_rejects_excessive_children() {
let child = box_bytes(b"free", &[]);
let children = vec![child; MAX_CHILD_BOXES_PER_SUPERBOX + 1];
let store = superbox(&UUID_MANIFEST_STORE, "c2pa", &children, None);
assert!(matches!(
parse_manifest_store(&store),
Err(JumbfError::ResourceLimit(
"superbox child count exceeds verifier bound"
))
));
}
#[test]
fn roundtrip_manifest_store() {
let assertion = assertion_box("c2pa.actions.v2", &[0xa0], None); let claim = &[0xbf, 0xff]; let sig = &[0xd2, 0x84]; let manifest = build_manifest("urn:c2pa:test", &[assertion], claim, sig);
let store = build_manifest_store(&[manifest]);
let parsed = parse_manifest_store(&store).unwrap();
assert_eq!(parsed.manifests.len(), 1);
let m = &parsed.manifests[0];
assert_eq!(m.label, "urn:c2pa:test");
assert_eq!(m.assertions.len(), 1);
assert_eq!(m.assertions[0].0, "c2pa.actions.v2");
assert_eq!(m.claim_cbor, Some(&[0xbf, 0xff][..]));
assert_eq!(m.signature_cose, Some(&[0xd2, 0x84][..]));
}
#[test]
fn manifest_store_requires_exact_top_level_framing() {
let manifest = build_manifest("urn:c2pa:framing", &[], &[0xa0], &[0xd2, 0x84]);
let ordinary = build_manifest_store(&[manifest]);
let extended = extended_box_bytes(&ordinary);
assert_eq!(parse_manifest_store(&extended).unwrap().manifests.len(), 1);
for mut framed in [ordinary.clone(), extended] {
framed.extend_from_slice(b"suffix");
assert!(matches!(
parse_manifest_store(&framed),
Err(JumbfError::TrailingBytes(6))
));
}
let mut size_to_end = ordinary;
size_to_end[..4].copy_from_slice(&0u32.to_be_bytes());
assert_eq!(
parse_manifest_store(&size_to_end).unwrap().manifests.len(),
1
);
size_to_end.extend_from_slice(b"suffix");
assert!(parse_manifest_store(&size_to_end).is_err());
}
#[test]
fn rejects_relabelled_manifest_store_root() {
let manifest = build_manifest("urn:c2pa:root-label", &[], &[0xa0], &[0xd2, 0x84]);
let store = superbox(&UUID_MANIFEST_STORE, "evil", &[manifest], None);
assert!(matches!(
parse_manifest_store(&store),
Err(JumbfError::UnexpectedLabel {
expected: LABEL_MANIFEST_STORE,
found,
}) if found == "evil"
));
assert!(matches!(
manifest_superboxes_from_store(&store),
Err(JumbfError::UnexpectedLabel {
expected: LABEL_MANIFEST_STORE,
found,
}) if found == "evil"
));
}
#[test]
fn rejects_relabelled_assertion_store() {
let manifest = build_manifest("urn:c2pa:assertion-label", &[], &[0xa0], &[0xd2, 0x84]);
let mut store = build_manifest_store(&[manifest]);
relabel_unique(&mut store, LABEL_ASSERTION_STORE, "evil.assertions");
assert!(matches!(
parse_manifest_store(&store),
Err(JumbfError::UnexpectedLabel {
expected: LABEL_ASSERTION_STORE,
found,
}) if found == "evil.assertions"
));
}
#[test]
fn rejects_absent_assertion_store() {
let claim = superbox(&UUID_CLAIM, LABEL_CLAIM_V2, &[cbor_box(&[0xa0])], None);
let signature = superbox(
&UUID_CLAIM_SIGNATURE,
LABEL_CLAIM_SIGNATURE,
&[cbor_box(&[0xd2, 0x84])],
None,
);
let manifest = superbox(
&UUID_MANIFEST,
"urn:c2pa:missing-assertion-store",
&[claim, signature],
None,
);
let store = build_manifest_store(&[manifest]);
assert!(matches!(
parse_manifest_store(&store),
Err(JumbfError::InvalidStructure(
"manifest is missing required c2pa.assertions assertion store"
))
));
}
#[test]
fn rejects_multiple_assertion_stores() {
let first = superbox(&UUID_ASSERTION_STORE, LABEL_ASSERTION_STORE, &[], None);
let second = superbox(&UUID_ASSERTION_STORE, LABEL_ASSERTION_STORE, &[], None);
let claim = superbox(&UUID_CLAIM, LABEL_CLAIM_V2, &[cbor_box(&[0xa0])], None);
let signature = superbox(
&UUID_CLAIM_SIGNATURE,
LABEL_CLAIM_SIGNATURE,
&[cbor_box(&[0xd2, 0x84])],
None,
);
let manifest = superbox(
&UUID_MANIFEST,
"urn:c2pa:multiple-assertion-stores",
&[first, second, claim, signature],
None,
);
let store = build_manifest_store(&[manifest]);
assert!(matches!(
parse_manifest_store(&store),
Err(JumbfError::InvalidStructure(
"manifest contains multiple assertion store superboxes"
))
));
}
#[test]
fn rejects_relabelled_claim() {
let manifest = build_manifest("urn:c2pa:claim-label", &[], &[0xa0], &[0xd2, 0x84]);
let mut store = build_manifest_store(&[manifest]);
relabel_unique(&mut store, LABEL_CLAIM_V2, "evil.claim.v2");
assert!(matches!(
parse_manifest_store(&store),
Err(JumbfError::UnexpectedLabel {
expected: "c2pa.claim or c2pa.claim.v2",
found,
}) if found == "evil.claim.v2"
));
}
#[test]
fn rejects_relabelled_claim_signature() {
let manifest = build_manifest("urn:c2pa:signature-label", &[], &[0xa0], &[0xd2, 0x84]);
let mut store = build_manifest_store(&[manifest]);
relabel_unique(&mut store, LABEL_CLAIM_SIGNATURE, "evil.signature");
assert!(matches!(
parse_manifest_store(&store),
Err(JumbfError::UnexpectedLabel {
expected: LABEL_CLAIM_SIGNATURE,
found,
}) if found == "evil.signature"
));
}
#[test]
fn parses_canonical_v1_and_v2_claim_boxes() {
for claim_label in [LABEL_CLAIM_V1, LABEL_CLAIM_V2] {
let manifest = build_manifest_with_claim_label(
&format!("urn:c2pa:{claim_label}"),
&[],
&[0xa0],
&[0xd2, 0x84],
claim_label,
);
let store = build_manifest_store(&[manifest]);
let parsed = parse_manifest_store(&store).unwrap();
let manifest = &parsed.manifests[0];
assert_eq!(manifest.claim_count, 1);
assert_eq!(manifest.claim_box_label.as_deref(), Some(claim_label));
assert_eq!(manifest.claim_cbor, Some(&[0xa0][..]));
assert_eq!(manifest.signature_cose, Some(&[0xd2, 0x84][..]));
}
}
#[test]
fn rejects_duplicate_signatures_in_any_order() {
let assertion_store = superbox(&UUID_ASSERTION_STORE, "c2pa.assertions", &[], None);
let claim = superbox(&UUID_CLAIM, "c2pa.claim.v2", &[cbor_box(&[0xa0])], None);
let injected = superbox(
&UUID_CLAIM_SIGNATURE,
"c2pa.signature",
&[cbor_box(&[0x01])],
None,
);
let authentic = superbox(
&UUID_CLAIM_SIGNATURE,
"c2pa.signature",
&[cbor_box(&[0xd2, 0x84])],
None,
);
for (first, second) in [(injected.clone(), authentic.clone()), (authentic, injected)] {
let manifest = superbox(
&UUID_MANIFEST,
"urn:c2pa:duplicate-signature",
&[assertion_store.clone(), claim.clone(), first, second],
None,
);
let store = build_manifest_store(&[manifest]);
assert!(matches!(
parse_manifest_store(&store),
Err(JumbfError::AmbiguousLabel(label)) if label == "c2pa.signature"
));
}
}
#[test]
fn rejects_duplicate_direct_child_labels() {
let assertion_store = superbox(&UUID_ASSERTION_STORE, "c2pa.assertions", &[], None);
let claim = superbox(&UUID_CLAIM, "c2pa.claim.v2", &[cbor_box(&[0xa0])], None);
let signature = superbox(
&UUID_CLAIM_SIGNATURE,
"c2pa.signature",
&[cbor_box(&[0xd2, 0x84])],
None,
);
let shared_first = superbox(&UUID_CBOR_CONTENT, "shared", &[], None);
let shared_second = superbox(&UUID_CBOR_CONTENT, "shared", &[], None);
let cases = [
(
"c2pa.claim.v2",
vec![assertion_store.clone(), claim.clone(), claim, signature],
),
("shared", vec![shared_first, shared_second]),
];
for (duplicate_label, children) in cases {
let manifest = superbox(&UUID_MANIFEST, "urn:c2pa:duplicate-child", &children, None);
let store = build_manifest_store(&[manifest]);
assert!(matches!(
parse_manifest_store(&store),
Err(JumbfError::AmbiguousLabel(label)) if label == duplicate_label
));
}
}
#[test]
fn accepts_legacy_standard_manifest_uuid() {
let standard = build_manifest("urn:c2pa:legacy", &[], &[0xa0], &[0xd2, 0x84]);
let payload = parse_box(&standard, 0).unwrap().payload;
let parsed = parse_superbox_payload(payload).unwrap();
let assertion_store = superbox(&UUID_ASSERTION_STORE, LABEL_ASSERTION_STORE, &[], None);
let claim = superbox(&UUID_CLAIM, LABEL_CLAIM_V2, &[cbor_box(&[0xa0])], None);
let signature = superbox(
&UUID_CLAIM_SIGNATURE,
LABEL_CLAIM_SIGNATURE,
&[cbor_box(&[0xd2, 0x84])],
None,
);
assert_eq!(parsed.label, "urn:c2pa:legacy");
let legacy = superbox(
&UUID_LEGACY_MANIFEST,
"urn:c2pa:legacy",
&[assertion_store, claim, signature],
None,
);
let store = build_manifest_store(&[legacy]);
assert_eq!(parse_manifest_store(&store).unwrap().manifests.len(), 1);
}
#[test]
fn parses_update_manifest_and_retains_its_kind() {
let standard = build_manifest("urn:c2pa:update", &[], &[0xa0], &[0xd2, 0x84]);
let payload = parse_box(&standard, 0).unwrap().payload;
let parsed = parse_superbox_payload(payload).unwrap();
let update = superbox(
&UUID_UPDATE_MANIFEST,
"urn:c2pa:update",
&parsed
.content
.iter()
.map(|(box_type, payload)| box_bytes(box_type, payload))
.collect::<Vec<_>>(),
None,
);
let store = build_manifest_store(&[update]);
let parsed = parse_manifest_store(&store).unwrap();
assert_eq!(parsed.manifests.len(), 1);
assert_eq!(parsed.manifests[0].kind(), ManifestKind::Update);
}
#[test]
fn rejects_unexpanded_compressed_manifest() {
let manifest = superbox(&UUID_COMPRESSED_MANIFEST, "urn:c2pa:unsupported", &[], None);
let store = build_manifest_store(&[manifest]);
assert!(matches!(
parse_manifest_store(&store),
Err(JumbfError::UnsupportedManifestType("c2cm"))
));
}
#[test]
fn rejects_non_requestable_or_illegal_labels() {
let manifest = build_manifest("urn:c2pa:toggles", &[], &[0xa0], &[0xd2, 0x84]);
let mut store = build_manifest_store(&[manifest]);
let label_offset = store
.windows(LABEL_ASSERTION_STORE.len())
.position(|bytes| bytes == LABEL_ASSERTION_STORE.as_bytes())
.unwrap();
store[label_offset - 1] &= !TOGGLE_REQUESTABLE;
assert!(matches!(
parse_manifest_store(&store),
Err(JumbfError::InvalidStructure(
"C2PA JUMBF description is not labelled and requestable"
))
));
let manifest = build_manifest("urn:c2pa:illegal", &[], &[0xa0], &[0xd2, 0x84]);
let mut store = build_manifest_store(&[manifest]);
relabel_unique(&mut store, "urn:c2pa:illegal", "urn/c2pa:illegal");
assert!(matches!(
parse_manifest_store(&store),
Err(JumbfError::InvalidStructure(
"JUMBF label contains a forbidden character"
))
));
}
#[test]
fn rejects_multiple_claim_or_signature_content_boxes() {
let assertion_store = superbox(&UUID_ASSERTION_STORE, LABEL_ASSERTION_STORE, &[], None);
for (uuid, label, expected) in [
(
UUID_CLAIM,
LABEL_CLAIM_V2,
"claim superbox must contain exactly one CBOR content box",
),
(
UUID_CLAIM_SIGNATURE,
LABEL_CLAIM_SIGNATURE,
"claim signature superbox must contain exactly one CBOR content box",
),
] {
let malformed = superbox(&uuid, label, &[cbor_box(&[0xa0]), cbor_box(&[0xa1])], None);
let counterpart = if uuid == UUID_CLAIM {
superbox(
&UUID_CLAIM_SIGNATURE,
LABEL_CLAIM_SIGNATURE,
&[cbor_box(&[0xd2, 0x84])],
None,
)
} else {
superbox(&UUID_CLAIM, LABEL_CLAIM_V2, &[cbor_box(&[0xa0])], None)
};
let manifest = superbox(
&UUID_MANIFEST,
"urn:c2pa:extra-content",
&[assertion_store.clone(), malformed, counterpart],
None,
);
let store = build_manifest_store(&[manifest]);
assert!(matches!(
parse_manifest_store(&store),
Err(JumbfError::InvalidStructure(detail)) if detail == expected
));
}
}
#[test]
fn uuid_prefixes_correct() {
assert_eq!(&UUID_MANIFEST_STORE[..4], b"c2pa");
assert_eq!(&UUID_MANIFEST[..4], b"c2ma");
assert_eq!(&UUID_CLAIM[..4], b"c2cl");
assert_eq!(&UUID_MANIFEST_STORE[4..], &UUID_SUFFIX);
}
#[test]
fn rejects_truncated() {
assert!(matches!(
parse_box(&[0, 0, 0], 0),
Err(JumbfError::TruncatedHeader(_))
));
}
#[test]
fn rejects_length_overrun() {
let data = [0x00, 0x00, 0xff, 0xff, b'j', b'u', b'm', b'b'];
assert!(matches!(
parse_box(&data, 0),
Err(JumbfError::LengthOverrun(_))
));
}
#[test]
fn rejects_duplicate_manifest_labels() {
let claim = &[0xa0];
let sig = &[0xd2, 0x84];
let first = build_manifest("urn:c2pa:duplicate", &[], claim, sig);
let second = build_manifest("urn:c2pa:duplicate", &[], claim, sig);
let store = build_manifest_store(&[first, second]);
assert!(matches!(
parse_manifest_store(&store),
Err(JumbfError::AmbiguousLabel(label)) if label == "urn:c2pa:duplicate"
));
}
#[test]
fn rejects_duplicate_assertion_labels() {
let first = assertion_box("c2pa.actions.v2", &[0xa0], None);
let second = assertion_box("c2pa.actions.v2", &[0xa0], None);
let manifest = build_manifest("urn:c2pa:test", &[first, second], &[0xa0], &[0xd2, 0x84]);
let store = build_manifest_store(&[manifest]);
assert!(matches!(
parse_manifest_store(&store),
Err(JumbfError::AmbiguousLabel(label)) if label == "c2pa.actions.v2"
));
}
#[test]
fn manifest_store_accepts_exactly_one_trailing_free_box() {
let manifest = build_manifest("urn:c2pa:padded", &[], &[0xa0], &[0xd2, 0x84]);
let store = build_manifest_store(&[manifest]);
let mut padded = store.clone();
padded.extend_from_slice(&8u32.to_be_bytes());
padded.extend_from_slice(b"free");
assert_eq!(parse_manifest_store(&padded).unwrap().manifests.len(), 1);
let mut wrong_type = store.clone();
wrong_type.extend_from_slice(&8u32.to_be_bytes());
wrong_type.extend_from_slice(b"skip");
assert!(parse_manifest_store(&wrong_type).is_err());
let mut two_free = padded;
two_free.extend_from_slice(&8u32.to_be_bytes());
two_free.extend_from_slice(b"free");
assert!(parse_manifest_store(&two_free).is_err());
}
#[test]
fn private_description_requires_one_valid_salt_box() {
for salt_len in [0, 15, 17, 31, 33] {
let salt = vec![7u8; salt_len];
let assertion = assertion_box("c2pa.actions.v2", &[0xa0], Some(&salt));
let manifest =
build_manifest("urn:c2pa:bad-salt", &[assertion], &[0xa0], &[0xd2, 0x84]);
let store = build_manifest_store(&[manifest]);
assert!(
matches!(
parse_manifest_store(&store),
Err(JumbfError::InvalidStructure(_))
),
"salt length {salt_len} must be rejected"
);
}
for salt_len in [16, 32] {
let salt = vec![7u8; salt_len];
let assertion = assertion_box("c2pa.actions.v2", &[0xa0], Some(&salt));
let manifest =
build_manifest("urn:c2pa:good-salt", &[assertion], &[0xa0], &[0xd2, 0x84]);
let store = build_manifest_store(&[manifest]);
assert!(
parse_manifest_store(&store).is_ok(),
"salt length {salt_len} must be accepted"
);
}
let assertion = assertion_box("c2pa.actions.v2", &[0xa0], None);
let manifest = build_manifest(
"urn:c2pa:missing-salt",
&[assertion],
&[0xa0],
&[0xd2, 0x84],
);
let mut store = build_manifest_store(&[manifest]);
let label_offset = store
.windows(b"c2pa.actions.v2".len())
.position(|bytes| bytes == b"c2pa.actions.v2")
.unwrap();
store[label_offset - 1] |= TOGGLE_PRIVATE;
assert!(matches!(
parse_manifest_store(&store),
Err(JumbfError::InvalidStructure(_))
));
}
#[test]
fn json_assertion_content_is_exposed_with_its_type() {
let json = br#"{"@context":"https://schema.org"}"#;
let assertion = superbox(
&UUID_JSON_CONTENT,
"c2pa.metadata",
&[box_bytes(TYPE_JSON, json)],
None,
);
let manifest = build_manifest("urn:c2pa:json", &[assertion], &[0xa0], &[0xd2, 0x84]);
let store = build_manifest_store(&[manifest]);
let parsed = parse_manifest_store(&store).unwrap();
let manifest = &parsed.manifests[0];
assert_eq!(manifest.assertions[0].1, json);
assert_eq!(
manifest.assertion_content_type("c2pa.metadata"),
Some(AssertionContentType::Json)
);
}
#[test]
fn compressed_store_expands_raw_brotli_and_preserves_hash_domains() {
let manifest = build_manifest("urn:c2pa:compressed", &[], &[0xa0], &[0xd2, 0x84]);
let compressed = compress_manifest(&manifest).unwrap();
let stored_content = superbox_content(&compressed).unwrap().to_vec();
let store = build_manifest_store(&[compressed]);
let expanded = expand_store(&store).unwrap();
assert!(expanded.expanded());
assert_eq!(
parse_manifest_store(expanded.bytes()).unwrap().manifests[0].label,
"urn:c2pa:compressed"
);
assert_eq!(expanded.stored_content(0), Some(stored_content.as_slice()));
}
#[test]
fn compressed_update_follows_the_plain_update_path() {
let update = superbox(
&UUID_UPDATE_MANIFEST,
"urn:c2pa:update",
&[superbox(
&UUID_ASSERTION_STORE,
LABEL_ASSERTION_STORE,
&[],
None,
)],
None,
);
let compressed = compress_manifest_kind(&update).unwrap();
let stored_content = superbox_content(&compressed).unwrap().to_vec();
let store = build_manifest_store(&[compressed]);
let expanded = expand_store(&store).unwrap();
let parsed = parse_manifest_store(expanded.bytes()).unwrap();
assert_eq!(parsed.manifests[0].kind(), ManifestKind::Update);
assert_eq!(expanded.stored_content(0), Some(stored_content.as_slice()));
let plain_store = build_manifest_store(&[update]);
let plain = parse_manifest_store(&plain_store).unwrap();
assert_eq!(plain.manifests[0].kind(), ManifestKind::Update);
}
#[test]
fn corrupt_or_recursive_compressed_manifest_is_rejected_without_recursion() {
let corrupt = superbox(
&UUID_COMPRESSED_MANIFEST,
"urn:c2pa:corrupt",
&[box_bytes(TYPE_BROB, b"not a brotli stream")],
None,
);
assert!(matches!(
expand_store(&build_manifest_store(&[corrupt])),
Err(JumbfError::CompressedManifestInvalid { .. })
));
let inner = superbox(
&UUID_COMPRESSED_MANIFEST,
"urn:c2pa:recursive",
&[box_bytes(TYPE_BROB, b"nested")],
None,
);
let recursive = compress_manifest_kind(&inner).unwrap();
assert!(matches!(
expand_store(&build_manifest_store(&[recursive])),
Err(JumbfError::CompressedManifestInvalid { .. })
));
}
#[test]
fn compressed_manifest_inflate_is_bounded_and_label_bound() {
let oversized = vec![0u8; MAX_DECOMPRESSED_MANIFEST_BYTES + 1];
let compressed = superbox(
&UUID_COMPRESSED_MANIFEST,
"urn:c2pa:oversized",
&[box_bytes(TYPE_BROB, &brotli_compress(&oversized))],
None,
);
let error = match expand_store(&build_manifest_store(&[compressed])) {
Err(error) => error,
Ok(_) => panic!("oversized expansion must fail"),
};
assert!(matches!(
error,
JumbfError::CompressedManifestInvalid { reason, .. }
if matches!(reason.as_ref(), JumbfError::DecompressedTooLarge)
));
let manifest = build_manifest("urn:c2pa:inner", &[], &[0xa0], &[0xd2, 0x84]);
let mismatched = superbox(
&UUID_COMPRESSED_MANIFEST,
"urn:c2pa:outer",
&[box_bytes(TYPE_BROB, &brotli_compress(&manifest))],
None,
);
let error = match expand_store(&build_manifest_store(&[mismatched])) {
Err(error) => error,
Ok(_) => panic!("mismatched labels must fail"),
};
assert!(matches!(
error,
JumbfError::CompressedManifestInvalid { reason, .. }
if matches!(
reason.as_ref(),
JumbfError::CompressedManifestLabelMismatch { .. }
)
));
}
}