use time::OffsetDateTime;
use crate::c2pa_cbor::{decode, Value};
use crate::c2pa_core::jumbf::{
manifest_superboxes_from_store, parse_manifest_store, superbox_content, ParsedManifest,
};
use crate::c2pa_formats::{
bmff_check_stream_brand, AssetFormat, StreamEncapsulation, StreamFileRole, StreamMethod,
};
use super::live_video::{
validate_emsg_segment, validate_init_segment, validate_live_video_assertion,
validate_segment_manifest, validate_session_keys, LiveVideoApplicability,
LiveVideoManifestState, LIVEVIDEO_CONTINUITY_METHOD_INVALID, LIVEVIDEO_SEGMENT_INVALID,
LIVEVIDEO_SESSION_KEY_INVALID,
};
use super::{
hash_bytes, CawgTrustInputs, ClaimGeneration, Json, ValidateError, ValidationResults,
ValidationState, VerifyInput, VerifyOutput, ASSERTION_BMFF_HASH_MATCH,
ASSERTION_BMFF_HASH_MISMATCH,
};
pub const BMFF_FRAGMENTS_NOT_EVALUATED: &str = "com.encypher.bmffHash.fragmentsNotEvaluated";
const LIVE_VIDEO_SEGMENT_LABEL: &str = "com.encypher.livevideo.segment";
const SESSION_KEYS_LABEL: &str = "c2pa.session-keys";
const INGREDIENT_ASSERTION_LABEL: &str = "c2pa.ingredient.v3";
const CHAIN_HASH_ALG: &str = "sha256";
pub struct SegmentVerification {
pub sequence_number: usize,
pub manifest_label: String,
pub previous_manifest_label: Option<String>,
pub output: VerifyOutput,
}
pub struct PerSegmentVerifyOutput {
pub segments: Vec<SegmentVerification>,
pub chain_valid: bool,
pub chain_failures: Vec<String>,
}
pub enum StreamVerifyOutput {
VerifiableSegmentInfo(Box<VerifyOutput>),
PerSegment(PerSegmentVerifyOutput),
}
impl StreamVerifyOutput {
pub const fn method(&self) -> StreamMethod {
match self {
Self::VerifiableSegmentInfo(_) => StreamMethod::VerifiableSegmentInfo,
Self::PerSegment(_) => StreamMethod::PerSegment,
}
}
}
pub fn verify_stream<'a>(
input: &VerifyInput<'a>,
init_segment: &'a [u8],
segments: &[&'a [u8]],
encapsulation: StreamEncapsulation,
method: StreamMethod,
cawg_inputs: CawgTrustInputs<'_>,
) -> Result<StreamVerifyOutput, ValidateError> {
verify_stream_with_expected_seeks(
input,
init_segment,
segments,
&[],
encapsulation,
method,
cawg_inputs,
)
}
fn verify_stream_with_expected_seeks<'a>(
input: &VerifyInput<'a>,
init_segment: &'a [u8],
segments: &[&'a [u8]],
expected_seek_positions: &[usize],
encapsulation: StreamEncapsulation,
method: StreamMethod,
cawg_inputs: CawgTrustInputs<'_>,
) -> Result<StreamVerifyOutput, ValidateError> {
validate_expected_seek_positions(expected_seek_positions, segments.len())?;
match method {
StreamMethod::VerifiableSegmentInfo => {
match session_key_delivery(init_segment, input.mime)? {
Some(delivery) => {
check_stream_brands(init_segment, segments, encapsulation)?;
verify_delivered_live_session(
input,
init_segment,
segments,
expected_seek_positions,
&delivery,
cawg_inputs,
)
}
None => verify_fragmented_with_encapsulation_and_expected_seeks(
&input_for(input, init_segment),
segments,
expected_seek_positions,
encapsulation,
cawg_inputs,
),
}
.map(|out| StreamVerifyOutput::VerifiableSegmentInfo(Box::new(out)))
}
StreamMethod::PerSegment => verify_per_segment_with_expected_seeks(
input,
init_segment,
segments,
expected_seek_positions,
encapsulation,
cawg_inputs,
)
.map(StreamVerifyOutput::PerSegment),
}
}
#[allow(clippy::too_many_arguments)]
pub fn verify_stream_safe<'a>(
input: &VerifyInput<'a>,
init_segment: &'a [u8],
segments: &[&'a [u8]],
encapsulation: StreamEncapsulation,
method: StreamMethod,
cawg_trust: Option<&crate::c2pa_trust::TrustList>,
cawg_allowed_certs: Option<&crate::c2pa_trust::TrustList>,
document_signing_require_anchor: bool,
cawg_did_documents: Option<&std::collections::HashMap<String, Json>>,
cawg_ica_trusted_issuers: Option<&[String]>,
cawg_ica_trust_anchors: Option<&[String]>,
cawg_ica_status_lists: Option<&std::collections::HashMap<String, String>>,
) -> Result<StreamVerifyOutput, ValidateError> {
verify_stream_safe_with_expected_seeks(
input,
init_segment,
segments,
&[],
encapsulation,
method,
cawg_trust,
cawg_allowed_certs,
document_signing_require_anchor,
cawg_did_documents,
cawg_ica_trusted_issuers,
cawg_ica_trust_anchors,
cawg_ica_status_lists,
)
}
#[allow(clippy::too_many_arguments)]
pub fn verify_stream_safe_with_expected_seeks<'a>(
input: &VerifyInput<'a>,
init_segment: &'a [u8],
segments: &[&'a [u8]],
expected_seek_positions: &[usize],
encapsulation: StreamEncapsulation,
method: StreamMethod,
cawg_trust: Option<&crate::c2pa_trust::TrustList>,
cawg_allowed_certs: Option<&crate::c2pa_trust::TrustList>,
document_signing_require_anchor: bool,
cawg_did_documents: Option<&std::collections::HashMap<String, Json>>,
cawg_ica_trusted_issuers: Option<&[String]>,
cawg_ica_trust_anchors: Option<&[String]>,
cawg_ica_status_lists: Option<&std::collections::HashMap<String, String>>,
) -> Result<StreamVerifyOutput, ValidateError> {
let cawg_inputs = CawgTrustInputs {
trust: cawg_trust,
allowed_certs: cawg_allowed_certs,
document_signing_require_anchor,
did_documents: cawg_did_documents,
ica_trusted_issuers: cawg_ica_trusted_issuers,
ica_trust_anchors: cawg_ica_trust_anchors,
ica_status_lists: cawg_ica_status_lists,
};
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
verify_stream_with_expected_seeks(
input,
init_segment,
segments,
expected_seek_positions,
encapsulation,
method,
cawg_inputs,
)
})) {
Ok(result) => result,
Err(_) => Err(ValidateError::Panic),
}
}
pub fn verify_live_session(
input: &VerifyInput<'_>,
init_segment: &[u8],
segments: &[&[u8]],
encapsulation: StreamEncapsulation,
cawg_inputs: CawgTrustInputs<'_>,
) -> Result<VerifyOutput, ValidateError> {
check_stream_brands(init_segment, segments, encapsulation)?;
let delivery = session_key_delivery(init_segment, input.mime)?.ok_or_else(|| {
ValidateError::Stream(
"init segment delivers no claim-bound session key, so it is not a live session".into(),
)
})?;
verify_delivered_live_session(input, init_segment, segments, &[], &delivery, cawg_inputs)
}
fn verify_delivered_live_session(
input: &VerifyInput<'_>,
init_segment: &[u8],
segments: &[&[u8]],
expected_seek_positions: &[usize],
delivery: &SessionKeyDelivery,
cawg_inputs: CawgTrustInputs<'_>,
) -> Result<VerifyOutput, ValidateError> {
let applicability = LiveVideoApplicability::new(StreamMethod::VerifiableSegmentInfo);
let mut out = super::verify_with_fragments(&input_for(input, init_segment), &[], cawg_inputs)?;
let manifest_id = &delivery.manifest_label;
validate_segment_manifest(
applicability,
out.validation_state != ValidationState::Invalid,
&mut out.results,
&format!("self#jumbf=/c2pa/{manifest_id}"),
);
validate_init_segment(
applicability,
init_segment,
&mut out.results,
"self#init-segment",
);
let keys_url = format!("self#jumbf=/c2pa/{manifest_id}/c2pa.assertions/{SESSION_KEYS_LABEL}");
let keys = match &delivery.session_keys {
Ok(assertion_cbor) => validate_session_keys(
applicability,
assertion_cbor,
&delivery.signer_chain_der,
&mut out.results,
&keys_url,
),
Err(defect) => {
out.results
.push_failure(LIVEVIDEO_SESSION_KEY_INVALID, keys_url, (*defect).into());
Vec::new()
}
};
let validation_time = input
.validation_time
.unwrap_or_else(OffsetDateTime::now_utc);
let mut next_sequence_number = Some(1u64);
for (index, segment) in segments.iter().enumerate() {
let expected_seek = expected_seek_positions.binary_search(&index).is_ok();
let expected_sequence_number = if expected_seek {
None
} else {
next_sequence_number
};
let url = format!("self#segment={}", index + 1);
if !expected_seek && expected_sequence_number.is_none() {
out.results.push_failure(
LIVEVIDEO_SEGMENT_INVALID,
url.clone(),
"unexpected sequence discontinuity: predecessor sequenceNumber cannot be incremented"
.into(),
);
}
if let Some(info) = validate_emsg_segment(
applicability,
segment,
expected_sequence_number,
manifest_id,
&keys,
validation_time,
&mut out.results,
&url,
) {
next_sequence_number = info.sequence_number.checked_add(1);
}
}
restate_results(&mut out, input.profile);
Ok(out)
}
struct SessionKeyDelivery {
manifest_label: String,
session_keys: Result<Vec<u8>, &'static str>,
signer_chain_der: Vec<Vec<u8>>,
}
fn session_key_delivery(
init_segment: &[u8],
mime: &str,
) -> Result<Option<SessionKeyDelivery>, ValidateError> {
let format = AssetFormat::from_mime(mime)
.ok_or_else(|| ValidateError::UnsupportedMime(mime.to_string()))?;
let Some(store) = crate::c2pa_formats::extract_manifest(format, init_segment)? else {
return Ok(None);
};
let parsed = parse_manifest_store(&store)?;
let Some(active) = parsed.manifests.last() else {
return Ok(None);
};
let found: Vec<_> = active
.assertions
.iter()
.filter(|(label, _)| label == SESSION_KEYS_LABEL)
.collect();
let session_keys = match found.as_slice() {
[] => return Ok(None),
[(label, cbor)] => {
if !claim_references_assertion(active, label) {
return Ok(None);
}
Ok((*cbor).to_vec())
}
_ => Err(
"init manifest carries more than one session-key delivery assertion, \
so none of its keys can be trusted",
),
};
Ok(Some(SessionKeyDelivery {
manifest_label: active.label.clone(),
session_keys,
signer_chain_der: active
.signature_cose
.and_then(|cose| crate::c2pa_crypto::extract_x5chain(cose).ok())
.unwrap_or_default(),
}))
}
pub fn verify_fragmented_with_encapsulation(
input: &VerifyInput,
fragments: &[&[u8]],
encapsulation: StreamEncapsulation,
cawg_inputs: CawgTrustInputs<'_>,
) -> Result<VerifyOutput, ValidateError> {
verify_fragmented_with_encapsulation_and_expected_seeks(
input,
fragments,
&[],
encapsulation,
cawg_inputs,
)
}
fn verify_fragmented_with_encapsulation_and_expected_seeks(
input: &VerifyInput,
fragments: &[&[u8]],
expected_seek_positions: &[usize],
encapsulation: StreamEncapsulation,
cawg_inputs: CawgTrustInputs<'_>,
) -> Result<VerifyOutput, ValidateError> {
check_stream_brands(input.data, fragments, encapsulation)?;
super::verify_with_fragments_and_expected_seeks(
input,
fragments,
expected_seek_positions,
cawg_inputs,
)
}
pub struct VerifiedBmffPortion<'a> {
bytes: &'a [u8],
pub verification: VerifyOutput,
}
impl<'a> VerifiedBmffPortion<'a> {
pub fn release(self) -> &'a [u8] {
self.bytes
}
}
pub fn verify_bmff_portion_before_release<'a>(
input: &VerifyInput<'_>,
portion: &'a [u8],
encapsulation: StreamEncapsulation,
cawg_inputs: CawgTrustInputs<'_>,
) -> Result<VerifiedBmffPortion<'a>, ValidateError> {
let verification =
verify_fragmented_with_encapsulation(input, &[portion], encapsulation, cawg_inputs)?;
let bmff_successes = verification
.results
.success
.iter()
.filter(|status| status.code == ASSERTION_BMFF_HASH_MATCH)
.count();
if verification.validation_state == ValidationState::Invalid || bmff_successes < 2 {
return Err(ValidateError::Stream(
"BMFF portion was not Merkle-verified; release refused".into(),
));
}
Ok(VerifiedBmffPortion {
bytes: portion,
verification,
})
}
pub fn verify_per_segment<'a>(
input: &VerifyInput<'a>,
init_segment: &'a [u8],
segments: &[&'a [u8]],
encapsulation: StreamEncapsulation,
cawg_inputs: CawgTrustInputs<'_>,
) -> Result<PerSegmentVerifyOutput, ValidateError> {
verify_per_segment_with_expected_seeks(
input,
init_segment,
segments,
&[],
encapsulation,
cawg_inputs,
)
}
fn verify_per_segment_with_expected_seeks<'a>(
input: &VerifyInput<'a>,
init_segment: &'a [u8],
segments: &[&'a [u8]],
expected_seek_positions: &[usize],
encapsulation: StreamEncapsulation,
cawg_inputs: CawgTrustInputs<'_>,
) -> Result<PerSegmentVerifyOutput, ValidateError> {
check_stream_brands(init_segment, segments, encapsulation)?;
let applicability = LiveVideoApplicability::new(StreamMethod::PerSegment);
let mut verifications = Vec::with_capacity(segments.len() + 1);
let mut failures = Vec::new();
let mut previous: Option<(String, Vec<u8>)> = None;
let mut previous_live: Option<LiveVideoManifestState> = None;
let mut next_sequence_number = Some(0i128);
let mut stream_id: Option<String> = None;
for (position, segment) in std::iter::once(init_segment)
.chain(segments.iter().copied())
.enumerate()
{
let expected_seek = position > 0
&& expected_seek_positions
.binary_search(&(position - 1))
.is_ok();
let mut verified =
super::verify_with_fragments(&input_for(input, segment), &[], cawg_inputs)?;
let facts = segment_facts(segment, input.mime)?;
let (manifest_label, active_manifest_hash) = match &facts {
Some(facts) => (
facts.manifest_label.clone(),
Some(facts.active_manifest_hash.clone()),
),
None => {
failures.push(format!("segment {position}: carries no C2PA manifest"));
(String::new(), None)
}
};
if verified.validation_state == ValidationState::Invalid {
failures.push(format!(
"segment {position}: standalone validation state is Invalid"
));
}
let declared = match facts.as_ref().map(|facts| &facts.declared) {
Some(Ok(declared)) => Some(declared),
Some(Err(defect)) => {
failures.push(format!("segment {position}: {defect}"));
None
}
None => None,
};
if let Some(declared) = declared {
check_declaration(
position,
declared,
if expected_seek {
None
} else {
next_sequence_number
},
expected_seek,
encapsulation,
&mut stream_id,
&mut failures,
);
next_sequence_number = declared
.sequence_number
.filter(|sequence| *sequence >= 0)
.and_then(|sequence| sequence.checked_add(1));
} else {
next_sequence_number = None;
}
let previous_manifest_label =
declared.and_then(|declared| declared.previous_manifest_id.clone());
let continuity_previous = if expected_seek {
None
} else {
previous_live.as_ref()
};
if previous_live.is_some() || expected_seek {
if let Some(facts) = facts.as_ref() {
let already_filed = verified.results.failure.len();
let accepted = validate_live_video_assertion(
applicability,
&facts.declaration_cbor,
&facts.manifest_label,
continuity_previous,
&mut verified.results,
&format!(
"self#jumbf=/c2pa/{}/c2pa.assertions/{LIVE_VIDEO_SEGMENT_LABEL}",
facts.manifest_label
),
)
.is_some();
if !accepted {
let expected_label = previous_live
.as_ref()
.map(|state| state.manifest_id.as_str())
.unwrap_or("");
for status in &verified.results.failure[already_filed..] {
failures.push(live_chain_failure(
position,
&status.code,
expected_label,
previous_manifest_label.as_deref(),
));
}
restate_results(&mut verified, input.profile);
}
}
} else if !expected_seek {
if let (None, Some(claimed)) = (&previous, &previous_manifest_label) {
failures.push(format!(
"segment {position}: declares previousManifestId {claimed:?} but has no predecessor"
));
}
}
let parent_link = facts.as_ref().map(|facts| &facts.parent_link);
match (&previous, parent_link) {
(_, Some(Err(defect))) => failures.push(format!("segment {position}: {defect}")),
(Some((expected_label, _)), Some(Ok(None))) => failures.push(format!(
"segment {position}: no parentOf {INGREDIENT_ASSERTION_LABEL} assertion \
linking to manifest {expected_label:?}"
)),
(Some((_, expected_hash)), Some(Ok(Some(linked))))
if !expected_seek && linked != expected_hash =>
{
failures.push(format!(
"segment {position}: ingredient activeManifest hash does not match the \
preceding segment's manifest"
));
}
(None, Some(Ok(Some(_)))) => failures.push(format!(
"segment {position}: carries a parentOf {INGREDIENT_ASSERTION_LABEL} assertion \
but has no predecessor"
)),
_ => {}
}
previous_live = facts
.as_ref()
.zip(declared)
.and_then(|(facts, declared)| declared_live_state(declared, &facts.manifest_label));
verifications.push(SegmentVerification {
sequence_number: position,
manifest_label: manifest_label.clone(),
previous_manifest_label,
output: verified,
});
previous = active_manifest_hash.map(|hash| (manifest_label, hash));
}
Ok(PerSegmentVerifyOutput {
chain_valid: failures.is_empty(),
chain_failures: failures,
segments: verifications,
})
}
pub(crate) fn require_fragments_bound(
out: &mut VerifyOutput,
manifest: &ParsedManifest<'_>,
fragments: &[&[u8]],
profile: crate::c2pa_core::EngineProfile,
) {
if fragments.is_empty() || declares_fragment_merkle(manifest) {
return;
}
out.results.push_failure(
LIVEVIDEO_SEGMENT_INVALID,
format!("self#jumbf=/c2pa/{}", manifest.label),
format!(
"{} caller-supplied fragment(s) were not bound: the active manifest declares no \
fragment merkle tree. Verify this stream with the stream verifier, which selects \
the session-key or per-segment binding from the manifest",
fragments.len()
),
);
restate_results(out, profile);
}
pub(crate) fn validate_fragment_sequence(
out: &mut VerifyOutput,
manifest: &ParsedManifest<'_>,
fragments: &[&[u8]],
expected_seek_positions: &[usize],
profile: crate::c2pa_core::EngineProfile,
) {
if fragments.is_empty() {
return;
}
let Some(binding_label) = fragment_merkle_label(manifest) else {
return;
};
let mut failed = false;
let mut last_locations =
std::collections::HashMap::<(i128, i128), usize>::with_capacity(fragments.len());
for (position, fragment) in fragments.iter().enumerate() {
let expected_seek = expected_seek_positions.binary_search(&position).is_ok();
if expected_seek {
last_locations.clear();
}
let Ok(mut boxes) = crate::c2pa_formats::bmff_merkle_boxes(fragment) else {
continue;
};
if boxes.len() != 1 {
continue;
}
let Some(Ok(merkle_box)) = boxes.pop() else {
continue;
};
let key = (merkle_box.unique_id, merkle_box.local_id);
let Some(previous) = last_locations.insert(key, merkle_box.location) else {
continue;
};
if previous
.checked_add(1)
.is_some_and(|expected| merkle_box.location == expected)
{
continue;
}
failed = true;
let explanation = format!(
"fragment {position}: unexpected discontinuity at playback location {} after \
{previous} for uniqueId={} localId={}",
merkle_box.location, merkle_box.unique_id, merkle_box.local_id
);
out.results.push_failure(
ASSERTION_BMFF_HASH_MISMATCH,
format!(
"self#jumbf=/c2pa/{}/c2pa.assertions/{binding_label}",
manifest.label
),
explanation,
);
}
if failed {
restate_results(out, profile);
}
}
fn fragment_merkle_label<'a>(manifest: &'a ParsedManifest<'_>) -> Option<&'a str> {
manifest.assertions.iter().find_map(|(label, cbor)| {
if !super::is_supported_bmff_hash_label(label) {
return None;
}
let assertion = decode(cbor).ok()?;
match assertion.get("merkle") {
Some(Value::Array(entries))
if entries.iter().any(|entry| entry.get("initHash").is_some()) =>
{
Some(label.as_str())
}
_ => None,
}
})
}
fn declares_fragment_merkle(manifest: &ParsedManifest<'_>) -> bool {
fragment_merkle_label(manifest).is_some()
}
pub(crate) fn restate_results(out: &mut VerifyOutput, profile: crate::c2pa_core::EngineProfile) {
out.validation_state = super::compute_state(&out.results, profile);
let Some(object) = out.report_json.as_object_mut() else {
return;
};
object.insert(
"validation_status".to_string(),
super::status_array(&out.results.failure),
);
super::restate_validation_results(object, &out.results);
object.insert(
"validation_state".to_string(),
Json::String(out.validation_state.as_str().to_string()),
);
let present = object
.get("provenance_verdict")
.and_then(|verdict| verdict.get("present"))
.and_then(Json::as_bool)
.unwrap_or(true);
object.insert(
"provenance_verdict".to_string(),
super::provenance_verdict_json(&out.results, present, profile),
);
}
fn claim_references_assertion(manifest: &ParsedManifest<'_>, alabel: &str) -> bool {
let Some(claim) = manifest.claim_cbor.and_then(|bytes| decode(bytes).ok()) else {
return false;
};
[ClaimGeneration::V1, ClaimGeneration::V2]
.into_iter()
.flat_map(super::ref_fields)
.filter_map(|field| match claim.get(field) {
Some(Value::Array(items)) => Some(items.clone()),
_ => None,
})
.flatten()
.filter_map(|item| item.get("url").and_then(Value::as_text).map(str::to_string))
.any(|url| url.rsplit("c2pa.assertions/").next() == Some(alabel))
}
fn declared_live_state(
declared: &DeclaredSegment,
manifest_label: &str,
) -> Option<LiveVideoManifestState> {
Some(LiveVideoManifestState {
sequence_number: u64::try_from(declared.sequence_number?).ok()?,
stream_id: declared.stream_id.clone()?,
manifest_id: manifest_label.to_owned(),
})
}
fn live_chain_failure(
position: usize,
code: &str,
expected_label: &str,
claimed: Option<&str>,
) -> String {
match code {
LIVEVIDEO_SEGMENT_INVALID => format!(
"segment {position}: previousManifestId {claimed:?} does not name the \
preceding manifest {expected_label:?}"
),
LIVEVIDEO_CONTINUITY_METHOD_INVALID => format!(
"segment {position}: declares no usable manifest-ID continuity; expected \
previousManifestId {expected_label:?}"
),
_ => format!(
"segment {position}: declared live-video sequence or stream does not follow \
the preceding segment"
),
}
}
fn check_declaration(
position: usize,
declared: &DeclaredSegment,
expected_sequence_number: Option<i128>,
expected_seek: bool,
encapsulation: StreamEncapsulation,
stream_id: &mut Option<String>,
failures: &mut Vec<String>,
) {
match (
declared.sequence_number,
expected_sequence_number,
expected_seek,
) {
(Some(sequence), _, true) if sequence >= 0 => {}
(Some(sequence), Some(expected), false) if sequence == expected => {}
(Some(sequence), Some(expected), false) => failures.push(format!(
"segment {position}: unexpected sequence discontinuity, declares sequenceNumber \
{sequence}, expected {expected}"
)),
(Some(sequence), None, false) => failures.push(format!(
"segment {position}: sequenceNumber {sequence} cannot follow an exhausted sequence"
)),
(Some(sequence), _, _) => failures.push(format!(
"segment {position}: declares invalid sequenceNumber {sequence}"
)),
(None, _, _) => failures.push(format!(
"segment {position}: {LIVE_VIDEO_SEGMENT_LABEL} has no sequenceNumber"
)),
}
match (&declared.stream_id, &stream_id) {
(Some(declared_id), None) => *stream_id = Some(declared_id.clone()),
(Some(declared_id), Some(expected)) if declared_id != expected => failures.push(format!(
"segment {position}: declares streamId {declared_id:?}, expected {expected:?}"
)),
(Some(_), Some(_)) => {}
(None, _) => failures.push(format!(
"segment {position}: {LIVE_VIDEO_SEGMENT_LABEL} has no streamId"
)),
}
if declared.encapsulation.as_deref() != Some(encapsulation.token()) {
failures.push(format!(
"segment {position}: declares encapsulation {:?}, verified as {}",
declared.encapsulation, encapsulation
));
}
if declared.method.as_deref() != Some(StreamMethod::PerSegment.token()) {
failures.push(format!(
"segment {position}: declares method {:?}, verified as {}",
declared.method,
StreamMethod::PerSegment
));
}
}
struct DeclaredSegment {
sequence_number: Option<i128>,
stream_id: Option<String>,
encapsulation: Option<String>,
method: Option<String>,
previous_manifest_id: Option<String>,
}
struct SegmentFacts {
manifest_label: String,
active_manifest_hash: Vec<u8>,
declared: Result<DeclaredSegment, &'static str>,
declaration_cbor: Vec<u8>,
parent_link: Result<Option<Vec<u8>>, &'static str>,
}
fn segment_facts(segment: &[u8], mime: &str) -> Result<Option<SegmentFacts>, ValidateError> {
let format = AssetFormat::from_mime(mime)
.ok_or_else(|| ValidateError::UnsupportedMime(mime.to_string()))?;
let Some(store) = crate::c2pa_formats::extract_manifest(format, segment)? else {
return Ok(None);
};
let parsed = parse_manifest_store(&store)?;
let Some(active) = parsed.manifests.last() else {
return Ok(None);
};
let active_manifest_hash = manifest_superboxes_from_store(&store)?
.last()
.and_then(|superbox| superbox_content(superbox).ok())
.and_then(|content| hash_bytes(CHAIN_HASH_ALG, content))
.ok_or_else(|| {
ValidateError::Stream("segment manifest store has no hashable active manifest".into())
})?;
let (declared, declaration_cbor) = match read_declaration(active) {
Ok((declared, cbor)) => (Ok(declared), cbor),
Err(defect) => (Err(defect), Vec::new()),
};
Ok(Some(SegmentFacts {
manifest_label: active.label.clone(),
active_manifest_hash,
declared,
declaration_cbor,
parent_link: read_parent_link(active),
}))
}
fn read_declaration(
active: &ParsedManifest<'_>,
) -> Result<(DeclaredSegment, Vec<u8>), &'static str> {
let found: Vec<_> = active
.assertions
.iter()
.filter(|(label, _)| label == LIVE_VIDEO_SEGMENT_LABEL)
.collect();
let [(label, cbor)] = found.as_slice() else {
return Err(if found.is_empty() {
"missing segment-identity assertion"
} else {
"carries more than one segment-identity assertion, so its declared \
ordering cannot be trusted"
});
};
if !claim_references_assertion(active, label) {
return Err("segment-identity assertion is not referenced by the claim");
}
let value = decode(cbor).map_err(|_| "segment-identity assertion is not decodable CBOR")?;
Ok((
DeclaredSegment {
sequence_number: match value.get("sequenceNumber") {
Some(Value::Integer(sequence)) => Some(*sequence),
_ => None,
},
stream_id: text_field(&value, "streamId"),
encapsulation: text_field(&value, "encapsulation"),
method: text_field(&value, "method"),
previous_manifest_id: text_field(&value, "previousManifestId"),
},
(*cbor).to_vec(),
))
}
fn read_parent_link(active: &ParsedManifest<'_>) -> Result<Option<Vec<u8>>, &'static str> {
let parents: Vec<_> = active
.assertions
.iter()
.filter(|(label, _)| is_ingredient_label(label))
.filter_map(|(label, cbor)| decode(cbor).ok().map(|value| (label, value)))
.filter(|(_, value)| value.get("relationship").and_then(Value::as_text) == Some("parentOf"))
.collect();
let [(label, value)] = parents.as_slice() else {
if parents.is_empty() {
return Ok(None);
}
return Err(
"carries more than one parentOf ingredient assertion, so its chain link \
cannot be trusted",
);
};
if !claim_references_assertion(active, label) {
return Err("parentOf ingredient assertion is not referenced by the claim");
}
value
.get("activeManifest")
.and_then(|manifest| manifest.get("hash"))
.and_then(Value::as_bytes)
.map(<[u8]>::to_vec)
.map(Some)
.ok_or("parentOf ingredient assertion carries no activeManifest hash")
}
fn text_field(value: &Value, key: &str) -> Option<String> {
value.get(key).and_then(Value::as_text).map(str::to_owned)
}
fn is_ingredient_label(label: &str) -> bool {
label == INGREDIENT_ASSERTION_LABEL
|| label
.strip_prefix(INGREDIENT_ASSERTION_LABEL)
.and_then(|suffix| suffix.strip_prefix("__"))
.and_then(|instance| instance.parse::<usize>().ok())
.is_some_and(|instance| instance > 0)
}
pub(crate) fn validate_expected_seek_positions(
expected_seek_positions: &[usize],
segment_count: usize,
) -> Result<(), ValidateError> {
let mut previous = None;
for &position in expected_seek_positions {
if position >= segment_count {
return Err(ValidateError::Stream(format!(
"expected seek position {position} is outside {segment_count} presented segment(s)"
)));
}
if previous.is_some_and(|prior| prior >= position) {
return Err(ValidateError::Stream(
"expected seek positions must be unique and strictly increasing".into(),
));
}
previous = Some(position);
}
Ok(())
}
fn check_stream_brands(
init_segment: &[u8],
segments: &[&[u8]],
encapsulation: StreamEncapsulation,
) -> Result<(), ValidateError> {
bmff_check_stream_brand(init_segment, StreamFileRole::Init, encapsulation)
.map_err(|e| ValidateError::Stream(format!("{encapsulation} init segment: {e}")))?;
for (index, segment) in segments.iter().enumerate() {
bmff_check_stream_brand(segment, StreamFileRole::Segment, encapsulation)
.map_err(|e| ValidateError::Stream(format!("{encapsulation} segment {index}: {e}")))?;
}
Ok(())
}
fn input_for<'a>(input: &VerifyInput<'a>, data: &'a [u8]) -> VerifyInput<'a> {
VerifyInput { data, ..*input }
}