use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::c2pa_core::spec::canonicalize_mime;
use crate::c2pa_core::{ComplianceLevel, EngineProfile, OperatingMode, SpecVersion};
use crate::c2pa_formats::AssetFormat;
use crate::c2pa_validate::{
binding_target as kernel_binding_target, verify_detached_safe,
verify_fragmented_with_cawg_options_and_expected_seeks_safe,
verify_identity_with_caller_verified_binding, verify_store_binding,
verify_with_cawg_options_and_graph_inputs_safe, BindingTarget, CallerBinding, CawgTrustInputs,
StoreBinding, VerifyOutput,
};
use crate::{
copy_status, finish_store_only_validation, map_validate_error, parse_validation_time,
report_from_output, require_embedded_mime, resolve_detached_mime, resolve_mime,
resolve_profile, Error, GraphInputs, ResolvedOptions, StoreOnlyValidation, ValidationResults,
VerificationReport, VerifyOptions, MAX_MANIFEST_STORE_BYTES,
};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CawgProfile {
profile: EngineProfile,
}
impl CawgProfile {
pub fn new(
spec_version: &str,
conformance_mode: bool,
compliance_program: bool,
) -> Result<Self, Error> {
let version = SpecVersion::from_str(spec_version).ok_or_else(|| {
Error::Verification(format!(
"spec_version {spec_version:?} is not one of {}",
SpecVersion::ALL
.iter()
.map(|version| version.version_str())
.collect::<Vec<_>>()
.join(", ")
))
})?;
if version == SpecVersion::V2_5 && compliance_program {
return Err(Error::Verification(
"spec_version \"2.5\" has no Conformance Program profile; compliance_program must be false".into(),
));
}
let mode = if conformance_mode {
OperatingMode::Conformance
} else {
OperatingMode::Regular
};
let compliance = if compliance_program {
ComplianceLevel::ConformanceProgram
} else {
ComplianceLevel::CoreSpec
};
Ok(Self {
profile: EngineProfile::new(version, mode).with_compliance(compliance),
})
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CawgStoreHost {
Asset,
HostLess,
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub enum CawgEvaluation {
#[non_exhaustive]
Evaluated {
manifest_label: String,
store_manifest_sha256: Vec<(String, [u8; 32])>,
validation_results: ValidationResults,
},
#[non_exhaustive]
StoreGateClosed {
manifest_label: Option<String>,
code: String,
},
}
pub struct CawgEvaluator {
resolved: ResolvedOptions,
did_documents: Option<HashMap<String, serde_json::Value>>,
ica_trusted_issuers: Option<Vec<String>>,
ica_trust_anchors: Option<Vec<String>>,
ica_status_lists: Option<HashMap<String, String>>,
}
impl std::fmt::Debug for CawgEvaluator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CawgEvaluator")
.field("profile", &self.resolved.profile)
.finish_non_exhaustive()
}
}
impl CawgEvaluator {
pub fn new(options: &VerifyOptions, profile: &CawgProfile) -> Result<Self, Error> {
if options.strict_conformance {
return Err(Error::Verification(
"strict_conformance must be false: the evaluator takes its posture from CawgProfile"
.into(),
));
}
if options.spec_version.is_some() {
return Err(Error::Verification(
"spec_version must be None: the evaluator takes its spec revision from CawgProfile"
.into(),
));
}
if options.online == Some(true) {
return Err(Error::Verification(
"online must not be Some(true): the evaluator never fetches".into(),
));
}
if options
.external_data
.as_ref()
.is_some_and(|data| !data.is_empty())
{
return Err(Error::Verification(
"external_data must be empty: the evaluator never reads external content".into(),
));
}
if options.validation_time.is_none() {
return Err(Error::Verification(
"validation_time is required: the evaluator judges every evaluation at one fixed instant"
.into(),
));
}
let mut resolved = ResolvedOptions::resolve(options)?;
resolved.profile = profile.profile;
Ok(Self {
resolved,
did_documents: options.cawg_did_documents.clone(),
ica_trusted_issuers: options.cawg_ica_trusted_issuers.clone(),
ica_trust_anchors: options.cawg_ica_trust_anchors.clone(),
ica_status_lists: options.cawg_ica_status_lists.clone(),
})
}
pub fn evaluate_with_caller_verified_binding(
&self,
manifest_store: &[u8],
host: CawgStoreHost,
) -> Result<CawgEvaluation, Error> {
if manifest_store.len() > MAX_MANIFEST_STORE_BYTES {
return Err(Error::Verification(format!(
"manifest store exceeds the {MAX_MANIFEST_STORE_BYTES} byte limit"
)));
}
let input = self.resolved.input(&[], "");
let cawg_inputs = self.cawg_inputs(GraphInputs::default());
let output = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
verify_identity_with_caller_verified_binding(
manifest_store,
host == CawgStoreHost::HostLess,
&input,
cawg_inputs,
)
}))
.map_err(|_| Error::Verification("CAWG evaluation panicked".into()))?
.map_err(map_validate_error)?;
Ok(match (output.identity, output.manifest_label) {
(Some(identity), Some(manifest_label)) => CawgEvaluation::Evaluated {
manifest_label,
store_manifest_sha256: output
.store_manifest_sha256
.into_iter()
.map(|digest| (digest.label, digest.sha256))
.collect(),
validation_results: ValidationResults {
success: identity.success.iter().map(copy_status).collect(),
informational: identity.informational.iter().map(copy_status).collect(),
failure: identity.failure.iter().map(copy_status).collect(),
trust_list_sources: identity.trust_list_sources.clone(),
},
},
(_, manifest_label) => CawgEvaluation::StoreGateClosed {
manifest_label,
code: output.gate_code.ok_or_else(|| {
Error::Verification(
"the identity step neither ran nor recorded a gate status".into(),
)
})?,
},
})
}
}
impl CawgEvaluator {
pub fn profile(&self) -> CawgProfile {
CawgProfile {
profile: self.resolved.profile,
}
}
fn cawg_inputs<'a>(&'a self, graph_inputs: GraphInputs<'a>) -> CawgTrustInputs<'a> {
CawgTrustInputs::new(
self.resolved.cawg_trust(),
self.resolved.cawg_allowed_certs(),
self.resolved.cawg_document_signing_require_anchor,
self.did_documents.as_ref(),
self.ica_trusted_issuers.as_deref(),
self.ica_trust_anchors.as_deref(),
self.ica_status_lists.as_ref(),
graph_inputs,
)
}
fn pass_options(&self, options: &VerifyOptions) -> Result<ResolvedOptions, Error> {
let profile = self.resolved.profile;
if let Some(version) = options.spec_version.as_deref() {
if version != profile.version_str() {
return Err(Error::Verification(format!(
"spec_version {version:?} differs from the evaluator's revision {:?}",
profile.version_str()
)));
}
}
if options.strict_conformance {
let expected_compliance = if profile.version == SpecVersion::V2_5 {
ComplianceLevel::CoreSpec
} else {
ComplianceLevel::ConformanceProgram
};
if profile.mode != OperatingMode::Conformance
|| profile.compliance != expected_compliance
{
return Err(Error::Verification(
"strict_conformance does not match the evaluator's exact technical posture"
.into(),
));
}
}
let cawg_fields_set = options.cawg_trust_pem.is_some()
|| options.cawg_allowed_certs_pem.is_some()
|| options.cawg_document_signing_require_anchor.is_some()
|| options.cawg_trust_configurations.is_some()
|| options.cawg_did_documents.is_some()
|| options.cawg_ica_trusted_issuers.is_some()
|| options.cawg_ica_trust_anchors.is_some()
|| options.cawg_ica_status_lists.is_some()
|| options.cawg_strict_encoding;
if cawg_fields_set {
return Err(Error::Verification(
"CAWG settings come from the evaluator; options must not set any cawg_* field"
.into(),
));
}
if options.online == Some(true) {
return Err(Error::Verification(
"online must not be Some(true): evaluator-driven verification never fetches".into(),
));
}
if options.validation_time.is_some()
&& parse_validation_time(options.validation_time.as_deref())?
!= self.resolved.validation_time
{
return Err(Error::Verification(
"validation_time differs from the evaluator's fixed validation instant".into(),
));
}
let c2pa_only = VerifyOptions {
spec_version: None,
strict_conformance: false,
validation_time: None,
..options.clone()
};
let mut resolved = ResolvedOptions::resolve(&c2pa_only)?;
resolved.profile = profile.with_debug(profile.debug || options.strict_conformance);
resolved.validation_time = self.resolved.validation_time;
resolved.validation_time_text = self.resolved.validation_time_text.clone();
resolved.cawg_strict_encoding = self.resolved.cawg_strict_encoding;
Ok(resolved)
}
}
fn contained(
run: impl FnOnce() -> Result<VerifyOutput, crate::c2pa_validate::ValidateError>,
) -> Result<VerifyOutput, Error> {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(run))
.map_err(|_| Error::Verification("verification panicked".into()))?
.map_err(map_validate_error)
}
fn check_store_size(manifest_store: &[u8]) -> Result<(), Error> {
if manifest_store.is_empty() {
return Err(Error::Verification("manifest store is empty".into()));
}
if manifest_store.len() > MAX_MANIFEST_STORE_BYTES {
return Err(Error::Verification(format!(
"manifest store exceeds the {MAX_MANIFEST_STORE_BYTES} byte limit"
)));
}
Ok(())
}
pub fn verify_manifest_store_only_with_cawg(
manifest_store: &[u8],
options: &VerifyOptions,
evaluator: &CawgEvaluator,
graph_inputs: GraphInputs<'_>,
) -> Result<StoreOnlyValidation, Error> {
check_store_size(manifest_store)?;
let resolved = evaluator.pass_options(options)?;
let input = resolved.input(&[], "application/c2pa");
let output = contained(|| {
verify_store_binding(
manifest_store,
StoreBinding::StoreOnly,
&input,
evaluator.cawg_inputs(graph_inputs),
)
})?;
Ok(finish_store_only_validation(
manifest_store,
output,
&resolved,
))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ManifestBindingTarget {
pub manifest_store_sha256: [u8; 32],
pub active_manifest_label: String,
pub active_manifest_sha256: [u8; 32],
pub binding_manifest_label: String,
pub binding_assertion_label: String,
pub binding_reference_alg: String,
pub binding_reference_hash: Vec<u8>,
}
impl From<BindingTarget> for ManifestBindingTarget {
fn from(target: BindingTarget) -> Self {
Self {
manifest_store_sha256: target.manifest_store_sha256,
active_manifest_label: target.active_manifest_label,
active_manifest_sha256: target.active_manifest_sha256,
binding_manifest_label: target.binding_manifest_label,
binding_assertion_label: target.binding_assertion_label,
binding_reference_alg: target.binding_reference_alg,
binding_reference_hash: target.binding_reference_hash,
}
}
}
impl From<&ManifestBindingTarget> for BindingTarget {
fn from(target: &ManifestBindingTarget) -> Self {
Self {
manifest_store_sha256: target.manifest_store_sha256,
active_manifest_label: target.active_manifest_label.clone(),
active_manifest_sha256: target.active_manifest_sha256,
binding_manifest_label: target.binding_manifest_label.clone(),
binding_assertion_label: target.binding_assertion_label.clone(),
binding_reference_alg: target.binding_reference_alg.clone(),
binding_reference_hash: target.binding_reference_hash.clone(),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum CallerBindingVerdict {
Match,
Mismatch {
explanation: String,
},
}
#[derive(Debug, Clone)]
pub struct CallerBindingEvidence {
target: ManifestBindingTarget,
verdict: CallerBindingVerdict,
engine: String,
}
impl CallerBindingEvidence {
pub fn new(
target: ManifestBindingTarget,
verdict: CallerBindingVerdict,
engine: &str,
) -> Result<Self, Error> {
if engine.is_empty()
|| engine.len() > 128
|| !engine
.bytes()
.all(|byte| byte.is_ascii_graphic() || byte == b' ')
{
return Err(Error::Verification(
"engine must be 1-128 printable ASCII characters".into(),
));
}
Ok(Self {
target,
verdict,
engine: engine.to_string(),
})
}
pub fn target(&self) -> &ManifestBindingTarget {
&self.target
}
pub fn verdict(&self) -> &CallerBindingVerdict {
&self.verdict
}
fn kernel(&self) -> CallerBinding {
CallerBinding {
target: BindingTarget::from(&self.target),
mismatch: match &self.verdict {
CallerBindingVerdict::Match => None,
CallerBindingVerdict::Mismatch { explanation } => Some(explanation.clone()),
},
engine: self.engine.clone(),
}
}
}
pub fn binding_target(
manifest_store: &[u8],
host: CawgStoreHost,
options: &VerifyOptions,
) -> Result<Option<ManifestBindingTarget>, Error> {
let profile = resolve_profile(options.spec_version.as_deref(), options.strict_conformance)?;
check_store_size(manifest_store)?;
let target = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
kernel_binding_target(manifest_store, host == CawgStoreHost::HostLess, profile)
}))
.map_err(|_| Error::Verification("binding target derivation panicked".into()))?
.map_err(map_validate_error)?;
Ok(target.map(ManifestBindingTarget::from))
}
fn content_format(content_mime: &str) -> (String, Option<AssetFormat>) {
let mime = canonicalize_mime(content_mime);
let format = AssetFormat::from_canonical_mime(&mime);
(mime, format)
}
fn verify_caller_binding(
manifest_store: &[u8],
content_mime: &str,
host: CawgStoreHost,
evidence: &CallerBindingEvidence,
resolved: &ResolvedOptions,
cawg_inputs: CawgTrustInputs<'_>,
) -> Result<VerificationReport, Error> {
check_store_size(manifest_store)?;
let (mime, format) = content_format(content_mime);
let kernel_evidence = evidence.kernel();
let input = resolved.input(&[], &mime);
let output = contained(|| {
verify_store_binding(
manifest_store,
StoreBinding::Caller {
evidence: &kernel_evidence,
host_less: host == CawgStoreHost::HostLess,
format,
},
&input,
cawg_inputs,
)
})?;
Ok(report_from_output(output, mime, resolved))
}
pub fn verify_with_caller_verified_binding(
manifest_store: &[u8],
content_mime: &str,
host: CawgStoreHost,
evidence: &CallerBindingEvidence,
options: &VerifyOptions,
graph_inputs: GraphInputs<'_>,
) -> Result<VerificationReport, Error> {
if options.online == Some(true) {
return Err(Error::Verification(
"online must not be Some(true): caller-verified binding never fetches".into(),
));
}
let resolved = ResolvedOptions::resolve(options)?;
let cawg_inputs = CawgTrustInputs::new(
resolved.cawg_trust(),
resolved.cawg_allowed_certs(),
resolved.cawg_document_signing_require_anchor,
options.cawg_did_documents.as_ref(),
options.cawg_ica_trusted_issuers.as_deref(),
options.cawg_ica_trust_anchors.as_deref(),
options.cawg_ica_status_lists.as_ref(),
graph_inputs,
);
verify_caller_binding(
manifest_store,
content_mime,
host,
evidence,
&resolved,
cawg_inputs,
)
}
pub fn verify_with_caller_verified_binding_and_cawg(
manifest_store: &[u8],
content_mime: &str,
host: CawgStoreHost,
evidence: &CallerBindingEvidence,
options: &VerifyOptions,
evaluator: &CawgEvaluator,
graph_inputs: GraphInputs<'_>,
) -> Result<VerificationReport, Error> {
let resolved = evaluator.pass_options(options)?;
verify_caller_binding(
manifest_store,
content_mime,
host,
evidence,
&resolved,
evaluator.cawg_inputs(graph_inputs),
)
}
pub fn verify_with_cawg(
data: &[u8],
mime_type: &str,
options: &VerifyOptions,
evaluator: &CawgEvaluator,
graph_inputs: GraphInputs<'_>,
) -> Result<VerificationReport, Error> {
let resolved = evaluator.pass_options(options)?;
let mime = resolve_mime(mime_type, resolved.profile)?;
require_embedded_mime(&mime, resolved.profile)?;
let input = resolved.input(data, &mime);
let output = contained(|| {
verify_with_cawg_options_and_graph_inputs_safe(
&input,
evaluator.resolved.cawg_trust(),
evaluator.resolved.cawg_allowed_certs(),
evaluator.resolved.cawg_document_signing_require_anchor,
evaluator.did_documents.as_ref(),
evaluator.ica_trusted_issuers.as_deref(),
evaluator.ica_trust_anchors.as_deref(),
evaluator.ica_status_lists.as_ref(),
graph_inputs,
)
})?;
Ok(report_from_output(output, mime, &resolved))
}
pub fn verify_with_manifest_store_and_cawg(
asset: &[u8],
manifest_store: &[u8],
mime_type: &str,
options: &VerifyOptions,
evaluator: &CawgEvaluator,
graph_inputs: GraphInputs<'_>,
) -> Result<VerificationReport, Error> {
let resolved = evaluator.pass_options(options)?;
let mime = resolve_detached_mime(mime_type, resolved.profile)?;
check_store_size(manifest_store)?;
let input = resolved.input(asset, &mime);
let output = contained(|| {
verify_detached_safe(
manifest_store,
asset,
&mime,
&input,
evaluator.resolved.cawg_trust(),
evaluator.resolved.cawg_allowed_certs(),
evaluator.resolved.cawg_document_signing_require_anchor,
evaluator.did_documents.as_ref(),
evaluator.ica_trusted_issuers.as_deref(),
evaluator.ica_trust_anchors.as_deref(),
evaluator.ica_status_lists.as_ref(),
graph_inputs,
)
})?;
Ok(report_from_output(output, mime, &resolved))
}
pub fn verify_fragmented_with_cawg(
init_segment: &[u8],
fragments: &[&[u8]],
mime_type: &str,
options: &VerifyOptions,
evaluator: &CawgEvaluator,
graph_inputs: GraphInputs<'_>,
) -> Result<VerificationReport, Error> {
if options.stream_presentation_times.is_some() {
return Err(Error::InvalidValidationTime(
"stream_presentation_times: per-segment presentation times apply only to \
session-key live streams verified with verify_stream_with_options; \
Merkle-bound fragments are assessed at validation_time"
.into(),
));
}
let resolved = evaluator.pass_options(options)?;
let mime = resolve_mime(mime_type, resolved.profile)?;
if AssetFormat::from_canonical_mime(&mime) != Some(AssetFormat::Bmff) {
return Err(Error::UnsupportedMime(mime));
}
let input = resolved.input(init_segment, &mime);
let output = contained(|| {
verify_fragmented_with_cawg_options_and_expected_seeks_safe(
&input,
fragments,
&options.expected_seek_positions,
evaluator.resolved.cawg_trust(),
evaluator.resolved.cawg_allowed_certs(),
evaluator.resolved.cawg_document_signing_require_anchor,
evaluator.did_documents.as_ref(),
evaluator.ica_trusted_issuers.as_deref(),
evaluator.ica_trust_anchors.as_deref(),
evaluator.ica_status_lists.as_ref(),
graph_inputs,
)
})?;
Ok(report_from_output(output, mime, &resolved))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cawg_profile_accepts_strict_25_core_and_refuses_program() {
let strict_core = CawgProfile::new("2.5", true, false).unwrap();
assert_eq!(strict_core.profile.mode, OperatingMode::Conformance);
assert_eq!(strict_core.profile.compliance, ComplianceLevel::CoreSpec);
assert!(CawgProfile::new("2.5", true, true).is_err());
assert!(CawgProfile::new("2.5", false, true).is_err());
}
}