#![forbid(unsafe_code)]
#[path = "c2pa-cbor/lib.rs"]
#[allow(dead_code, reason = "production kernel mirror")]
mod c2pa_cbor;
#[path = "c2pa-core/lib.rs"]
#[allow(dead_code, unused_imports, reason = "production kernel mirror")]
mod c2pa_core;
#[path = "c2pa-crypto/lib.rs"]
#[allow(dead_code, unused_imports, reason = "production kernel mirror")]
mod c2pa_crypto;
#[path = "c2pa-formats/lib.rs"]
#[allow(dead_code, unused_imports, reason = "production kernel mirror")]
mod c2pa_formats;
#[path = "c2pa-trust/lib.rs"]
#[allow(dead_code, unused_imports, reason = "production kernel mirror")]
mod c2pa_trust;
#[path = "c2pa-validate/lib.rs"]
#[allow(dead_code, unused_imports, reason = "production kernel mirror")]
mod c2pa_validate;
mod config_dir;
mod default_trust;
mod online;
mod online_consent;
mod stream;
pub use stream::{
verify_stream, verify_stream_with_options, SegmentReport, StreamEncapsulation, StreamMethod,
StreamVerificationReport,
};
#[cfg(feature = "caller-verified-binding")]
mod caller_verified_binding;
mod telemetry;
mod telemetry_consent;
#[cfg(feature = "caller-verified-binding")]
pub use caller_verified_binding::{CawgEvaluation, CawgEvaluator, CawgProfile, CawgStoreHost};
pub use config_dir::config_directory;
pub use default_trust::SNAPSHOT_DATE as DEFAULT_TRUST_SNAPSHOT_DATE;
pub use online::{NetworkReport, NetworkRequest};
pub use online_consent::{
allow_interactive_online_consent, online_preference, set_online_preference, OnlinePreference,
OnlinePreferenceError,
};
pub use telemetry::{
validation_failure_telemetry, TelemetryOptions, ValidationFailureTelemetry,
DEFAULT_TELEMETRY_ENDPOINT,
};
pub use telemetry_consent::{
prompt_for_telemetry_consent, set_telemetry_enabled, telemetry_preference,
TelemetryPreferenceError,
};
use std::collections::HashMap;
use std::fs::{self, OpenOptions};
use std::io::{self, Read};
#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt;
use std::path::Path;
use crate::c2pa_core::{
spec::{canonicalize_mime, mimes_for_version},
EngineProfile, SpecVersion,
};
use crate::c2pa_trust::{AnchorPurpose, CawgTrustSource, TrustList};
use crate::c2pa_validate::{
verify_fragmented_with_cawg_options_and_expected_seeks_safe as verify_fragmented_safe,
verify_with_cawg_options_safe as verify_safe, StatusCode as CoreStatus,
ValidationResults as CoreResults, VerifyInput, ASSERTION_BMFF_HASH_MALFORMED,
ASSERTION_BMFF_HASH_MATCH, ASSERTION_BMFF_HASH_MISMATCH, ASSERTION_BOXES_HASH_MALFORMED,
ASSERTION_BOXES_HASH_MATCH, ASSERTION_BOXES_HASH_MISMATCH, ASSERTION_COLLECTION_HASH_MALFORMED,
ASSERTION_COLLECTION_HASH_MATCH, ASSERTION_COLLECTION_HASH_MISMATCH, ASSERTION_DATA_HASH_MATCH,
ASSERTION_DATA_HASH_MISMATCH, ASSERTION_MULTI_ASSET_HASH_MALFORMED,
ASSERTION_MULTI_ASSET_HASH_MATCH, ASSERTION_MULTI_ASSET_HASH_MISMATCH,
CLAIM_HARD_BINDINGS_MISSING, CLAIM_SIGNATURE_MISMATCH, CLAIM_SIGNATURE_MISSING,
CLAIM_SIGNATURE_VALIDATED, SIGNING_CREDENTIAL_INVALID, SIGNING_CREDENTIAL_OCSP_NOT_REVOKED,
SIGNING_CREDENTIAL_OCSP_REVOKED, SIGNING_CREDENTIAL_TRUSTED, SIGNING_CREDENTIAL_UNTRUSTED,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
pub const REPORT_SCHEMA_VERSION: &str = "1.0";
pub const C2PA_PROFILE: &str = "c2pa-2.4";
const MAX_MANIFEST_STORE_BYTES: usize = 64 * 1024 * 1024;
const MAX_PATH_ASSET_BYTES: u64 = 128 * 1024 * 1024;
const MAX_OCSP_EVIDENCE_BYTES: usize = 64 * 1024;
const MAX_EXTERNAL_DATA_EVIDENCE_BYTES: usize = 64 * 1024 * 1024;
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct VerifyOptions {
pub trust_pem: Option<String>,
pub tsa_trust_pem: Option<String>,
pub allowed_list_pem: Option<String>,
pub cawg_trust_pem: Option<String>,
pub cawg_allowed_certs_pem: Option<String>,
pub cawg_document_signing_require_anchor: Option<bool>,
pub cawg_trust_configurations: Option<Vec<CawgTrustConfiguration>>,
pub trust_anchor_not_before: Option<String>,
pub trust_anchor_not_after: Option<String>,
pub no_default_trust: bool,
pub cawg_did_documents: Option<HashMap<String, Value>>,
pub cawg_ica_trusted_issuers: Option<Vec<String>>,
pub cawg_ica_trust_anchors: Option<Vec<String>>,
pub cawg_ica_status_lists: Option<HashMap<String, String>>,
pub ocsp_responses: Option<HashMap<String, String>>,
pub ocsp_unreachable: Option<Vec<String>>,
pub external_data: Option<HashMap<String, String>>,
pub expected_seek_positions: Vec<usize>,
pub strict_conformance: bool,
pub cawg_strict_encoding: bool,
pub validation_time: Option<String>,
pub stream_presentation_times: Option<Vec<String>>,
pub telemetry: TelemetryOptions,
pub online: Option<bool>,
pub online_allow_private_networks: bool,
}
impl VerifyOptions {
pub fn validate(&self) -> Result<(), Error> {
ResolvedOptions::resolve(self)?;
for (index, raw) in self.stream_presentation_times.iter().flatten().enumerate() {
parse_stream_presentation_time(raw, index)?;
}
Ok(())
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CawgTrustConfiguration {
pub profile: CawgTrustProfile,
pub certificates_pem: String,
#[serde(default)]
pub not_before: Option<String>,
#[serde(default)]
pub not_after: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CawgTrustProfile {
Base,
SmimeInterim,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct VerificationStatus {
pub code: String,
pub url: String,
pub explanation: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub details: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ValidationResults {
pub success: Vec<VerificationStatus>,
pub informational: Vec<VerificationStatus>,
pub failure: Vec<VerificationStatus>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RevocationReport {
pub status: String,
pub source: String,
pub responder_signature: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FreshnessReport {
pub status: String,
pub as_of: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TrustReport {
pub status: String,
pub basis: String,
pub validation_time: String,
pub revocation: RevocationReport,
pub freshness: FreshnessReport,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerificationReport {
pub schema_version: String,
pub profile: String,
pub mime_type: String,
pub present: bool,
pub integrity: String,
pub signature: String,
pub hard_binding: String,
pub trust: TrustReport,
pub policy: Option<Value>,
pub managed_receipt: Option<Value>,
pub validation_state: String,
pub validation_results: ValidationResults,
pub manifest_report: Value,
pub content_credentials: Option<Value>,
#[serde(default)]
pub network: NetworkReport,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DetachedManifestEvidence {
pub manifest_store: Vec<u8>,
pub manifest_store_sha256: String,
pub carrier: Vec<u8>,
}
#[derive(Debug, Clone, Serialize)]
pub struct LocalEvidence {
pub report: VerificationReport,
#[serde(serialize_with = "serialize_optional_bytes")]
pub manifest_store: Option<Vec<u8>>,
pub manifest_store_sha256: Option<String>,
pub asset_sha256: String,
pub hard_binding: LocalHardBinding,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct LocalHardBinding {
pub algorithm: Option<String>,
pub status: String,
}
fn serialize_optional_bytes<S: serde::Serializer>(
value: &Option<Vec<u8>>,
serializer: S,
) -> Result<S::Ok, S::Error> {
match value {
Some(bytes) => serializer.serialize_bytes(bytes),
None => serializer.serialize_none(),
}
}
#[derive(Default)]
struct PassCapture {
manifest_store: Option<Vec<u8>>,
hard_binding_label: Option<String>,
}
impl PassCapture {
fn take(output: &mut crate::c2pa_validate::VerifyOutput) -> Self {
Self {
manifest_store: output.manifest_store.take(),
hard_binding_label: output.hard_binding_label.take(),
}
}
}
impl VerificationReport {
pub fn to_json(&self) -> Result<String, Error> {
serde_json::to_string(self).map_err(Error::Serialize)
}
pub fn to_pretty_json(&self) -> Result<String, Error> {
serde_json::to_string_pretty(self).map_err(Error::Serialize)
}
pub fn cawg_statuses(&self) -> Vec<&VerificationStatus> {
self.validation_results
.success
.iter()
.chain(&self.validation_results.informational)
.chain(&self.validation_results.failure)
.filter(|status| status.code.starts_with("cawg."))
.collect()
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("unsupported MIME type: {0}")]
UnsupportedMime(String),
#[error("invalid trust material: {0}")]
InvalidTrust(String),
#[error("invalid validation time: {0}")]
InvalidValidationTime(String),
#[error("verification failed: {0}")]
Verification(String),
#[error("could not read asset: {0}")]
Io(#[from] std::io::Error),
#[error(transparent)]
TelemetryPreference(#[from] TelemetryPreferenceError),
#[error(transparent)]
OnlinePreference(#[from] OnlinePreferenceError),
#[error("could not serialize report: {0}")]
Serialize(serde_json::Error),
}
impl Error {
pub fn code(&self) -> &'static str {
match self {
Self::UnsupportedMime(_) => "unsupported_mime",
Self::InvalidTrust(_) => "invalid_trust_material",
Self::InvalidValidationTime(_) => "invalid_validation_time",
Self::Verification(_) => "verification_error",
Self::Io(_) => "io_error",
Self::OnlinePreference(_) => "online_preference_error",
Self::TelemetryPreference(_) => "telemetry_preference_error",
Self::Serialize(_) => "serialization_error",
}
}
}
pub fn verify(data: &[u8], mime_type: &str) -> Result<VerificationReport, Error> {
verify_with_options(data, mime_type, &VerifyOptions::default())
}
pub fn verify_fragmented(
init_segment: &[u8],
fragments: &[&[u8]],
mime_type: &str,
) -> Result<VerificationReport, Error> {
verify_fragmented_with_options(
init_segment,
fragments,
mime_type,
&VerifyOptions::default(),
)
}
pub fn detached_manifest_evidence(
data: &[u8],
mime_type: &str,
) -> Result<Option<DetachedManifestEvidence>, Error> {
let mime = canonicalize_mime(mime_type);
let format = crate::c2pa_formats::AssetFormat::from_mime(&mime)
.ok_or_else(|| Error::UnsupportedMime(mime.clone()))?;
if !crate::c2pa_formats::supports_hash_mode(&mime) {
return Ok(None);
}
let Some(manifest_store) = crate::c2pa_formats::extract_manifest(format, data)
.map_err(|error| Error::Verification(error.to_string()))?
else {
return Ok(None);
};
let spans = crate::c2pa_formats::compute_data_hash_exclusions(format, data)
.map_err(|error| Error::Verification(error.to_string()))?;
let [span] = spans.as_slice() else {
return Ok(None);
};
let end = span
.start
.checked_add(span.length)
.filter(|end| *end <= data.len())
.ok_or_else(|| Error::Verification("manifest carrier exceeds asset bounds".into()))?;
let manifest_store_sha256 = hex::encode(Sha256::digest(&manifest_store));
Ok(Some(DetachedManifestEvidence {
manifest_store,
manifest_store_sha256,
carrier: data[span.start..end].to_vec(),
}))
}
pub fn verify_with_manifest_store(
asset: &[u8],
manifest_store: &[u8],
mime_type: &str,
options: &VerifyOptions,
) -> Result<VerificationReport, Error> {
let telemetry_enabled = telemetry_consent::resolve_telemetry_enabled(options.telemetry.enabled);
let result = verify_with_manifest_store_inner(asset, manifest_store, mime_type, options);
enqueue_failure_telemetry(mime_type, &result, options, telemetry_enabled);
result
}
fn verify_with_manifest_store_inner(
asset: &[u8],
manifest_store: &[u8],
mime_type: &str,
options: &VerifyOptions,
) -> Result<VerificationReport, Error> {
let mime = crate::c2pa_formats::canonicalize_detached_mime(mime_type);
let format = crate::c2pa_formats::AssetFormat::from_detached_content(&mime, asset)
.map_err(|error| Error::Verification(error.to_string()))?;
let mime = if format == Some(crate::c2pa_formats::AssetFormat::ExternalMedia) {
mime
} else {
resolve_mime(mime_type)?
};
let (mut report, needs, _) = detached_pass(asset, manifest_store, &mime, options)?;
let (network, evidence) = online::gather(options, &needs);
if evidence.is_empty() {
report.network = network;
return Ok(report);
}
let next = online::apply_evidence(options, &evidence);
let (mut report, _, _) = detached_pass(asset, manifest_store, &mime, &next)?;
report.network = network;
Ok(report)
}
fn detached_pass(
asset: &[u8],
manifest_store: &[u8],
mime: &str,
options: &VerifyOptions,
) -> Result<(VerificationReport, Vec<online::NetworkNeed>, PassCapture), 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"
)));
}
let resolved = ResolvedOptions::resolve(options)?;
let input = resolved.input(asset, mime);
let mut output = crate::c2pa_validate::verify_detached_safe(
manifest_store,
asset,
mime,
&input,
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(),
)
.map_err(map_validate_error)?;
let needs = online::network_needs(&output);
let capture = PassCapture::take(&mut output);
Ok((
report_from_output(output, mime.to_string(), &resolved),
needs,
capture,
))
}
pub fn verify_with_options(
data: &[u8],
mime_type: &str,
options: &VerifyOptions,
) -> Result<VerificationReport, Error> {
let resolved = ResolvedOptions::resolve(options)?;
verify_with_resolved_options(data, None, mime_type, options, &resolved)
}
pub fn local_evidence_with_options(
data: &[u8],
mime_type: &str,
options: &VerifyOptions,
) -> Result<LocalEvidence, Error> {
let resolved = ResolvedOptions::resolve(options)?;
let telemetry_enabled = telemetry_consent::resolve_telemetry_enabled(options.telemetry.enabled);
let (result, capture) =
match verify_with_options_inner(data, None, mime_type, options, &resolved) {
Ok((report, capture)) => (Ok(report), capture),
Err(error) => (Err(error), PassCapture::default()),
};
enqueue_failure_telemetry(mime_type, &result, options, telemetry_enabled);
let report = result?;
let status = match report.hard_binding.as_str() {
"match" => "match",
"mismatch" => "mismatch",
_ => "unknown",
};
Ok(LocalEvidence {
manifest_store_sha256: capture
.manifest_store
.as_deref()
.map(|store| hex::encode(Sha256::digest(store))),
manifest_store: capture.manifest_store,
asset_sha256: hex::encode(Sha256::digest(data)),
hard_binding: LocalHardBinding {
algorithm: capture.hard_binding_label,
status: status.to_string(),
},
report,
})
}
fn verify_with_resolved_options(
data: &[u8],
fragments: Option<&[&[u8]]>,
mime_type: &str,
options: &VerifyOptions,
resolved: &ResolvedOptions,
) -> Result<VerificationReport, Error> {
let telemetry_enabled = telemetry_consent::resolve_telemetry_enabled(options.telemetry.enabled);
let result = verify_with_options_inner(data, fragments, mime_type, options, resolved)
.map(|(report, _)| report);
enqueue_failure_telemetry(mime_type, &result, options, telemetry_enabled);
result
}
fn enqueue_failure_telemetry(
mime_type: &str,
result: &Result<VerificationReport, Error>,
options: &VerifyOptions,
enabled: bool,
) {
if let Some(event) = telemetry::validation_failure_telemetry_with_enabled(
mime_type,
result,
&options.telemetry,
enabled,
) {
telemetry::enqueue(options.telemetry.endpoint(), event);
}
}
pub fn verify_fragmented_with_options(
init_segment: &[u8],
fragments: &[&[u8]],
mime_type: &str,
options: &VerifyOptions,
) -> 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 = ResolvedOptions::resolve(options)?;
let telemetry_enabled = telemetry_consent::resolve_telemetry_enabled(options.telemetry.enabled);
let mime = canonicalize_mime(mime_type);
let result = if crate::c2pa_formats::AssetFormat::from_mime(&mime)
== Some(crate::c2pa_formats::AssetFormat::Bmff)
{
verify_with_options_inner(init_segment, Some(fragments), &mime, options, &resolved)
.map(|(report, _)| report)
} else {
Err(Error::UnsupportedMime(mime))
};
enqueue_failure_telemetry(mime_type, &result, options, telemetry_enabled);
result
}
pub(crate) struct ResolvedOptions {
claim_trust: Option<ResolvedTrust>,
tsa_trust: Option<ResolvedTrust>,
allowed_certs: Option<ResolvedTrust>,
cawg_trust: Option<ResolvedTrust>,
cawg_allowed_certs: Option<ResolvedTrust>,
validation_time: OffsetDateTime,
validation_time_text: String,
trust_basis: &'static str,
profile: EngineProfile,
cawg_strict_encoding: bool,
cawg_document_signing_require_anchor: bool,
ocsp_responses: HashMap<String, Vec<u8>>,
ocsp_unreachable: Vec<String>,
external_data: HashMap<String, Vec<u8>>,
}
impl ResolvedOptions {
pub(crate) fn resolve(options: &VerifyOptions) -> Result<Self, Error> {
let use_defaults = !options.no_default_trust;
let validation_time = parse_validation_time(options.validation_time.as_deref())?;
let custom_claim_trust = options.trust_pem.is_some() || options.allowed_list_pem.is_some();
let bounds = (
parse_optional_instant(
options.trust_anchor_not_before.as_deref(),
"trust_anchor_not_before",
)?,
parse_optional_instant(
options.trust_anchor_not_after.as_deref(),
"trust_anchor_not_after",
)?,
);
let (cawg_anchors, cawg_direct) =
resolve_cawg_configurations(options.cawg_trust_configurations.as_deref())?;
Ok(Self {
claim_trust: resolve_trust(
options.trust_pem.as_deref(),
use_defaults.then(default_trust::claim_signing),
AnchorPurpose::ClaimSigning,
bounds,
)?,
tsa_trust: resolve_trust(
options.tsa_trust_pem.as_deref(),
use_defaults.then(default_trust::timestamp_authorities),
AnchorPurpose::TimeStamping,
bounds,
)?,
allowed_certs: resolve_trust(
options.allowed_list_pem.as_deref(),
use_defaults.then(default_trust::allowed_claim_signers),
AnchorPurpose::ClaimSigning,
bounds,
)?,
cawg_trust: extend_trust(
resolve_trust(
options.cawg_trust_pem.as_deref(),
use_defaults.then(default_trust::cawg_identity),
AnchorPurpose::CawgIdentity,
bounds,
)?,
cawg_anchors,
),
cawg_allowed_certs: extend_trust(
resolve_trust(
options.cawg_allowed_certs_pem.as_deref(),
use_defaults.then(default_trust::cawg_allowed_identities),
AnchorPurpose::CawgIdentity,
bounds,
)?,
cawg_direct,
),
validation_time,
validation_time_text: validation_time
.format(&Rfc3339)
.map_err(|error| Error::InvalidValidationTime(error.to_string()))?,
trust_basis: match (use_defaults, custom_claim_trust) {
(true, true) => "bundled_and_caller_supplied_static_material",
(true, false) => "bundled_static_material",
(false, true) => "caller_supplied_static_material",
(false, false) => "none",
},
profile: if options.strict_conformance {
EngineProfile::strict(SpecVersion::V2_4)
} else {
EngineProfile::GENEROUS
},
cawg_strict_encoding: options.cawg_strict_encoding,
cawg_document_signing_require_anchor: options
.cawg_document_signing_require_anchor
.unwrap_or(true),
ocsp_responses: decode_base64_map(
options.ocsp_responses.as_ref(),
"ocsp_responses",
MAX_OCSP_EVIDENCE_BYTES,
)?,
ocsp_unreachable: options.ocsp_unreachable.clone().unwrap_or_default(),
external_data: decode_base64_map(
options.external_data.as_ref(),
"external_data",
MAX_EXTERNAL_DATA_EVIDENCE_BYTES,
)?,
})
}
pub(crate) fn input<'a>(&'a self, data: &'a [u8], mime: &'a str) -> VerifyInput<'a> {
VerifyInput {
data,
mime,
claim_signer_trust: self.claim_trust.as_ref().map(ResolvedTrust::get),
tsa_trust: self.tsa_trust.as_ref().map(ResolvedTrust::get),
allowed_certs: self.allowed_certs.as_ref().map(ResolvedTrust::get),
validation_time: Some(self.validation_time),
profile: self.profile,
cawg_strict_encoding: self.cawg_strict_encoding,
evidence: crate::c2pa_validate::OnlineEvidence {
ocsp_responses: (!self.ocsp_responses.is_empty()).then_some(&self.ocsp_responses),
ocsp_unreachable: (!self.ocsp_unreachable.is_empty())
.then_some(self.ocsp_unreachable.as_slice()),
external_data: (!self.external_data.is_empty()).then_some(&self.external_data),
},
}
}
pub(crate) fn cawg_trust(&self) -> Option<&TrustList> {
self.cawg_trust.as_ref().map(ResolvedTrust::get)
}
pub(crate) fn cawg_allowed_certs(&self) -> Option<&TrustList> {
self.cawg_allowed_certs.as_ref().map(ResolvedTrust::get)
}
}
fn decode_base64_map(
values: Option<&HashMap<String, String>>,
field: &str,
max_bytes: usize,
) -> Result<HashMap<String, Vec<u8>>, Error> {
let Some(values) = values else {
return Ok(HashMap::new());
};
let mut decoded = HashMap::with_capacity(values.len());
for (key, encoded) in values {
let bytes = crate::c2pa_formats::util::base64_decode(encoded)
.ok_or_else(|| Error::Verification(format!("{field}[{key}] is not standard base64")))?;
if bytes.len() > max_bytes {
return Err(Error::Verification(format!(
"{field}[{key}] exceeds the {max_bytes} byte limit"
)));
}
decoded.insert(key.clone(), bytes);
}
Ok(decoded)
}
pub(crate) fn resolve_mime(mime_type: &str) -> Result<String, Error> {
let mime = canonicalize_mime(mime_type);
if !mimes_for_version(SpecVersion::V2_4).contains(&mime.as_str())
|| crate::c2pa_formats::AssetFormat::from_mime(&mime).is_none()
{
return Err(Error::UnsupportedMime(mime));
}
Ok(mime)
}
pub(crate) fn map_validate_error(error: crate::c2pa_validate::ValidateError) -> Error {
match error {
crate::c2pa_validate::ValidateError::UnsupportedMime(value) => {
Error::UnsupportedMime(value)
}
other => Error::Verification(other.to_string()),
}
}
pub(crate) fn report_from_output(
output: crate::c2pa_validate::VerifyOutput,
mime: String,
resolved: &ResolvedOptions,
) -> VerificationReport {
let present = output
.report_json
.pointer("/provenance_verdict/present")
.and_then(Value::as_bool)
.unwrap_or(false);
let integrity = output
.report_json
.pointer("/provenance_verdict/integrity")
.and_then(Value::as_str)
.unwrap_or(if present { "invalid" } else { "absent" })
.to_string();
let trust = trust_report(
&output.results,
present,
resolved.trust_basis,
resolved.validation_time_text.clone(),
);
VerificationReport {
schema_version: REPORT_SCHEMA_VERSION.to_string(),
profile: C2PA_PROFILE.to_string(),
mime_type: mime,
present,
integrity,
signature: signature_status(&output.results),
hard_binding: hard_binding_status(&output.results),
trust,
policy: None,
managed_receipt: None,
validation_state: output.validation_state.as_str().to_string(),
validation_results: copy_results(&output.results),
manifest_report: output.report_json,
content_credentials: output.crjson,
network: NetworkReport::default(),
}
}
fn verify_with_options_inner(
data: &[u8],
fragments: Option<&[&[u8]]>,
mime_type: &str,
options: &VerifyOptions,
resolved: &ResolvedOptions,
) -> Result<(VerificationReport, PassCapture), Error> {
let mime = resolve_mime(mime_type)?;
let (mut report, needs, capture) =
embedded_pass_resolved(data, fragments, &mime, options, resolved)?;
let (mut network, evidence) = online::gather(options, &needs);
if evidence.is_empty() {
report.network = network;
return Ok((report, capture));
}
let next = online::apply_evidence(options, &evidence);
let second = match (evidence.manifest_store.as_deref(), fragments) {
(Some(store), None) => detached_pass(data, store, &mime, &next),
_ => embedded_pass(data, fragments, &mime, &next),
};
match second {
Ok((mut second, _, second_capture)) => {
second.network = network;
Ok((second, second_capture))
}
Err(error) => {
network.mark_unusable("remote_manifest", error.to_string());
report.network = network;
Ok((report, capture))
}
}
}
fn embedded_pass(
data: &[u8],
fragments: Option<&[&[u8]]>,
mime: &str,
options: &VerifyOptions,
) -> Result<(VerificationReport, Vec<online::NetworkNeed>, PassCapture), Error> {
let resolved = ResolvedOptions::resolve(options)?;
embedded_pass_resolved(data, fragments, mime, options, &resolved)
}
fn embedded_pass_resolved(
data: &[u8],
fragments: Option<&[&[u8]]>,
mime: &str,
options: &VerifyOptions,
resolved: &ResolvedOptions,
) -> Result<(VerificationReport, Vec<online::NetworkNeed>, PassCapture), Error> {
let input = resolved.input(data, mime);
let mut output = match fragments {
Some(fragments) => verify_fragmented_safe(
&input,
fragments,
&options.expected_seek_positions,
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(),
),
None => verify_safe(
&input,
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(),
),
}
.map_err(map_validate_error)?;
let needs = online::network_needs(&output);
let capture = PassCapture::take(&mut output);
Ok((
report_from_output(output, mime.to_string(), resolved),
needs,
capture,
))
}
pub fn verify_file(
path: impl AsRef<Path>,
mime_type: Option<&str>,
options: &VerifyOptions,
) -> Result<VerificationReport, Error> {
let resolved = ResolvedOptions::resolve(options)?;
let path = path.as_ref();
let mime = match mime_type {
Some(value) => value.to_string(),
None => mime_from_path(path)
.ok_or_else(|| Error::UnsupportedMime(path.display().to_string()))?
.to_string(),
};
let data = read_path_asset(path, MAX_PATH_ASSET_BYTES)?;
verify_with_resolved_options(&data, None, &mime, options, &resolved)
}
fn read_path_asset(path: &Path, limit: u64) -> io::Result<Vec<u8>> {
let mut options = OpenOptions::new();
options.read(true);
#[cfg(unix)]
options.custom_flags(libc::O_NONBLOCK | libc::O_CLOEXEC);
let mut file = options.open(path)?;
let opened_metadata = file.metadata()?;
validate_path_asset(path, &opened_metadata, limit)?;
read_bounded_file(&mut file, opened_metadata.len(), limit)
}
fn validate_path_asset(path: &Path, metadata: &fs::Metadata, limit: u64) -> io::Result<()> {
if !metadata.is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("asset path is not a regular file: {}", path.display()),
));
}
if metadata.len() > limit {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("asset exceeds the 128 MiB path limit: {}", path.display()),
));
}
Ok(())
}
fn read_bounded_file<R: Read>(file: &mut R, expected_len: u64, limit: u64) -> io::Result<Vec<u8>> {
if expected_len > limit {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"asset exceeds the path size limit",
));
}
let expected_len = usize::try_from(expected_len)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "asset size is not addressable"))?;
let mut data = vec![0_u8; expected_len + 1];
let mut used = 0;
while used < expected_len {
let read = file.read(&mut data[used..expected_len])?;
if read == 0 {
break;
}
used += read;
}
if used == expected_len && file.read(&mut data[expected_len..expected_len + 1])? != 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"asset grew while being read",
));
}
data.truncate(used);
Ok(data)
}
pub fn supported_mime_types() -> Vec<&'static str> {
let mut mimes: Vec<_> = mimes_for_version(SpecVersion::V2_4)
.into_iter()
.filter(|mime| crate::c2pa_formats::AssetFormat::from_mime(mime).is_some())
.collect();
mimes.sort_unstable();
mimes.dedup();
mimes
}
pub const SUPPORTED_EXTENSIONS: &[(&str, &str)] = &[
("jpg", "image/jpeg"),
("jpeg", "image/jpeg"),
("png", "image/png"),
("webp", "image/webp"),
("gif", "image/gif"),
("tif", "image/tiff"),
("tiff", "image/tiff"),
("dng", "image/x-adobe-dng"),
("heic", "image/heic"),
("heics", "image/heic-sequence"),
("heif", "image/heif"),
("heifs", "image/heif-sequence"),
("avif", "image/avif"),
("jxl", "image/jxl"),
("svg", "image/svg+xml"),
("mp4", "video/mp4"),
("m4v", "video/mp4"),
("mov", "video/quicktime"),
("avi", "video/x-msvideo"),
("wav", "audio/wav"),
("mp3", "audio/mpeg"),
("m4a", "audio/mp4"),
("aac", "audio/aac"),
("flac", "audio/flac"),
("ogg", "audio/ogg"),
("oga", "audio/ogg"),
("pdf", "application/pdf"),
("zip", "application/zip"),
("sketch", "application/zip"),
("epub", "application/epub+zip"),
(
"docx",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
),
(
"xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
),
(
"pptx",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
),
(
"dotx",
"application/vnd.openxmlformats-officedocument.wordprocessingml.template",
),
("docm", "application/vnd.ms-word.document.macroenabled.12"),
("dotm", "application/vnd.ms-word.template.macroenabled.12"),
(
"xltx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.template",
),
("xlsm", "application/vnd.ms-excel.sheet.macroenabled.12"),
("xltm", "application/vnd.ms-excel.template.macroenabled.12"),
(
"xlsb",
"application/vnd.ms-excel.sheet.binary.macroenabled.12",
),
(
"ppsx",
"application/vnd.openxmlformats-officedocument.presentationml.slideshow",
),
(
"potx",
"application/vnd.openxmlformats-officedocument.presentationml.template",
),
(
"pptm",
"application/vnd.ms-powerpoint.presentation.macroenabled.12",
),
(
"ppsm",
"application/vnd.ms-powerpoint.slideshow.macroenabled.12",
),
(
"potm",
"application/vnd.ms-powerpoint.template.macroenabled.12",
),
("vsdx", "application/vnd.ms-visio.drawing"),
("vsdm", "application/vnd.ms-visio.drawing.macroenabled.12"),
("vssx", "application/vnd.ms-visio.stencil"),
("vssm", "application/vnd.ms-visio.stencil.macroenabled.12"),
("vstx", "application/vnd.ms-visio.template"),
("vstm", "application/vnd.ms-visio.template.macroenabled.12"),
("oxps", "application/oxps"),
("xps", "application/vnd.ms-xpsdocument"),
("odt", "application/vnd.oasis.opendocument.text"),
("ods", "application/vnd.oasis.opendocument.spreadsheet"),
("odp", "application/vnd.oasis.opendocument.presentation"),
("odg", "application/vnd.oasis.opendocument.graphics"),
("ttf", "font/ttf"),
("otf", "font/otf"),
("sfnt", "font/sfnt"),
("txt", "text/plain"),
("tsv", "text/tab-separated-values"),
("csv", "text/csv"),
("md", "text/markdown"),
("markdown", "text/markdown"),
("html", "text/html"),
("htm", "text/html"),
("xhtml", "application/xhtml+xml"),
("xml", "application/xml"),
("css", "text/css"),
("js", "application/javascript"),
("mjs", "application/javascript"),
("json", "application/json"),
("yaml", "application/yaml"),
("yml", "application/yaml"),
("toml", "application/toml"),
("py", "text/x-python"),
];
pub fn mime_from_path(path: &Path) -> Option<&'static str> {
let extension = path.extension()?.to_str()?.to_ascii_lowercase();
SUPPORTED_EXTENSIONS
.iter()
.find(|(candidate, _)| *candidate == extension)
.map(|(_, mime)| *mime)
}
enum ResolvedTrust {
Bundled(&'static TrustList),
Owned(TrustList),
}
impl ResolvedTrust {
fn get(&self) -> &TrustList {
match self {
Self::Bundled(trust) => trust,
Self::Owned(trust) => trust,
}
}
}
fn resolve_trust(
custom_pem: Option<&str>,
bundled: Option<&'static TrustList>,
purpose: AnchorPurpose,
bounds: (Option<OffsetDateTime>, Option<OffsetDateTime>),
) -> Result<Option<ResolvedTrust>, Error> {
let custom = custom_pem
.map(|pem| TrustList::from_pem_for(purpose, pem))
.transpose()
.map_err(|error| Error::InvalidTrust(error.to_string()))?
.map(|trust| trust.with_bounds(bounds.0, bounds.1))
.map(|trust| match purpose {
AnchorPurpose::CawgIdentity => trust.with_cawg_source(CawgTrustSource::CallerSupplied),
_ => trust,
});
match (bundled, custom) {
(None, None) => Ok(None),
(Some(trust), None) => Ok(Some(ResolvedTrust::Bundled(trust))),
(None, Some(trust)) => Ok(Some(ResolvedTrust::Owned(trust))),
(Some(bundled), Some(custom)) => {
let mut merged = bundled.clone();
merged.anchors.extend(custom.anchors);
Ok(Some(ResolvedTrust::Owned(merged)))
}
}
}
fn resolve_cawg_configurations(
configurations: Option<&[CawgTrustConfiguration]>,
) -> Result<(TrustList, TrustList), Error> {
let mut anchors = TrustList::default();
let mut direct = TrustList::default();
for (index, configuration) in configurations.unwrap_or_default().iter().enumerate() {
let invalid = |field: &str, detail: &dyn std::fmt::Display| {
Error::InvalidTrust(format!(
"cawg_trust_configurations[{index}]{field}: {detail}"
))
};
let instant = |value: Option<&str>, field| {
value
.map(|raw| {
OffsetDateTime::parse(raw, &Rfc3339).map_err(|error| invalid(field, &error))
})
.transpose()
};
let not_before = instant(configuration.not_before.as_deref(), ".not_before")?;
let not_after = instant(configuration.not_after.as_deref(), ".not_after")?;
if let (Some(start), Some(end)) = (not_before, not_after) {
if start > end {
return Err(invalid("", &"not_before is later than not_after"));
}
}
let source = match configuration.profile {
CawgTrustProfile::Base => CawgTrustSource::CallerSupplied,
CawgTrustProfile::SmimeInterim => CawgTrustSource::SmimeInterim,
};
let (entry_anchors, entry_direct) =
TrustList::from_pem_for(AnchorPurpose::CawgIdentity, &configuration.certificates_pem)
.map_err(|error| invalid(".certificates_pem", &error))?
.with_bounds(not_before, not_after)
.with_cawg_source(source)
.split_certificate_authorities();
anchors.anchors.extend(entry_anchors.anchors);
direct.anchors.extend(entry_direct.anchors);
}
Ok((anchors, direct))
}
fn extend_trust(resolved: Option<ResolvedTrust>, extra: TrustList) -> Option<ResolvedTrust> {
if extra.anchors.is_empty() {
return resolved;
}
let mut merged = match resolved {
None => return Some(ResolvedTrust::Owned(extra)),
Some(ResolvedTrust::Bundled(bundled)) => bundled.clone(),
Some(ResolvedTrust::Owned(owned)) => owned,
};
merged.anchors.extend(extra.anchors);
Some(ResolvedTrust::Owned(merged))
}
fn parse_validation_time(value: Option<&str>) -> Result<OffsetDateTime, Error> {
match value {
Some(raw) => OffsetDateTime::parse(raw, &Rfc3339)
.map_err(|error| Error::InvalidValidationTime(error.to_string())),
None => Ok(OffsetDateTime::now_utc()),
}
}
fn parse_optional_instant(
value: Option<&str>,
field: &str,
) -> Result<Option<OffsetDateTime>, Error> {
value
.map(|raw| {
OffsetDateTime::parse(raw, &Rfc3339)
.map_err(|error| Error::InvalidValidationTime(format!("{field}: {error}")))
})
.transpose()
}
fn parse_stream_presentation_time(raw: &str, index: usize) -> Result<OffsetDateTime, Error> {
OffsetDateTime::parse(raw, &Rfc3339).map_err(|error| {
Error::InvalidValidationTime(format!("stream_presentation_times[{index}]: {error}"))
})
}
pub(crate) fn parse_stream_presentation_times(
values: Option<&[String]>,
) -> Result<Option<Vec<OffsetDateTime>>, Error> {
values
.map(|values| {
values
.iter()
.enumerate()
.map(|(index, raw)| parse_stream_presentation_time(raw, index))
.collect()
})
.transpose()
}
fn copy_status(status: &CoreStatus) -> VerificationStatus {
VerificationStatus {
code: status.code.clone(),
url: status.url.clone(),
explanation: status.explanation.clone(),
details: status.details.clone(),
}
}
fn copy_results(results: &CoreResults) -> ValidationResults {
ValidationResults {
success: results.success.iter().map(copy_status).collect(),
informational: results.informational.iter().map(copy_status).collect(),
failure: results.failure.iter().map(copy_status).collect(),
}
}
fn signature_status(results: &CoreResults) -> String {
if results.has_success(CLAIM_SIGNATURE_VALIDATED) {
"valid"
} else if results.has_failure(CLAIM_SIGNATURE_MISMATCH) {
"invalid"
} else if results.has_failure(CLAIM_SIGNATURE_MISSING) {
"missing"
} else {
"unknown"
}
.to_string()
}
fn hard_binding_status(results: &CoreResults) -> String {
const MATCHES: &[&str] = &[
ASSERTION_DATA_HASH_MATCH,
ASSERTION_BMFF_HASH_MATCH,
ASSERTION_BOXES_HASH_MATCH,
ASSERTION_COLLECTION_HASH_MATCH,
ASSERTION_MULTI_ASSET_HASH_MATCH,
];
const FAILURES: &[&str] = &[
ASSERTION_DATA_HASH_MISMATCH,
ASSERTION_BMFF_HASH_MISMATCH,
ASSERTION_BMFF_HASH_MALFORMED,
ASSERTION_BOXES_HASH_MISMATCH,
ASSERTION_BOXES_HASH_MALFORMED,
ASSERTION_COLLECTION_HASH_MISMATCH,
ASSERTION_COLLECTION_HASH_MALFORMED,
ASSERTION_MULTI_ASSET_HASH_MISMATCH,
ASSERTION_MULTI_ASSET_HASH_MALFORMED,
];
if results.has_failure(crate::c2pa_validate::live_video::LIVEVIDEO_SEGMENT_INVALID) {
"mismatch"
} else if MATCHES.iter().any(|code| results.has_success(code)) {
"match"
} else if FAILURES.iter().any(|code| results.has_failure(code)) {
"mismatch"
} else if results.has_failure(CLAIM_HARD_BINDINGS_MISSING) {
"missing"
} else {
"unknown"
}
.to_string()
}
fn trust_report(
results: &CoreResults,
present: bool,
basis: &str,
validation_time: String,
) -> TrustReport {
let supplied = basis != "none";
let trusted = results.has_success(SIGNING_CREDENTIAL_TRUSTED);
let rejected = results.has_failure(SIGNING_CREDENTIAL_UNTRUSTED)
|| results.has_failure(SIGNING_CREDENTIAL_INVALID);
let revoked = results.has_failure(SIGNING_CREDENTIAL_OCSP_REVOKED);
let not_revoked = results.has_success(SIGNING_CREDENTIAL_OCSP_NOT_REVOKED);
TrustReport {
status: if trusted && !revoked {
"valid_for_supplied_material"
} else if present && supplied && (rejected || revoked) {
"not_valid_for_supplied_material"
} else {
"not_evaluated"
}
.to_string(),
basis: basis.to_string(),
validation_time,
revocation: RevocationReport {
status: if revoked {
"revoked"
} else if not_revoked {
"not_revoked"
} else {
"not_checked"
}
.to_string(),
source: if !(revoked || not_revoked) {
"none"
} else if results
.success
.iter()
.chain(&results.failure)
.filter(|status| {
status.code == SIGNING_CREDENTIAL_OCSP_REVOKED
|| status.code == SIGNING_CREDENTIAL_OCSP_NOT_REVOKED
})
.any(|status| {
status
.details
.as_ref()
.and_then(|details| details.get("source"))
.and_then(Value::as_str)
== Some("online_ocsp")
})
{
"online_ocsp"
} else {
"embedded_ocsp"
}
.to_string(),
responder_signature: if revoked || not_revoked {
"valid"
} else {
"not_applicable"
}
.to_string(),
},
freshness: FreshnessReport {
status: "unknown".to_string(),
as_of: None,
},
}
}
#[cfg(test)]
mod tests {
use super::{
mime_from_path, read_bounded_file, supported_mime_types, verify, verify_file,
verify_fragmented, verify_with_options, CawgTrustConfiguration, CawgTrustProfile, Error,
VerifyOptions,
};
use std::{io::Cursor, path::Path};
#[test]
fn known_filename_maps_to_mime() {
assert_eq!(
mime_from_path(Path::new("composition.MP4")),
Some("video/mp4")
);
assert_eq!(
mime_from_path(Path::new("drawing.odg")),
Some("application/vnd.oasis.opendocument.graphics")
);
assert_eq!(
mime_from_path(Path::new("data.tsv")),
Some("text/tab-separated-values")
);
assert_eq!(
mime_from_path(Path::new("archive.ZIP")),
Some("application/zip")
);
assert_eq!(
mime_from_path(Path::new("design.sketch")),
Some("application/zip")
);
}
#[test]
fn every_supported_format_is_reachable_from_an_extension() {
const ALIASES: &[&str] = &[
"application/font-sfnt",
"application/x-font-ttf",
"application/mp4",
"video/x-m4v",
"text/xml",
"application/x-zip-based",
];
let unreachable: Vec<_> = supported_mime_types()
.into_iter()
.filter(|mime| !ALIASES.contains(mime))
.filter(|mime| !super::SUPPORTED_EXTENSIONS.iter().any(|(_, m)| m == mime))
.collect();
assert!(
unreachable.is_empty(),
"no extension maps to {unreachable:?}"
);
assert_eq!(
mime_from_path(Path::new("report.OXPS")),
Some("application/oxps")
);
}
#[test]
fn revocation_source_distinguishes_online_from_embedded_ocsp() {
use crate::c2pa_validate::{StatusCode, ValidationResults};
let status = |source: Option<&str>| StatusCode {
code: crate::c2pa_validate::SIGNING_CREDENTIAL_OCSP_NOT_REVOKED.into(),
url: "self#jumbf=/c2pa/urn:c2pa:x/c2pa.signature".into(),
explanation: "not revoked".into(),
details: source.map(|source| serde_json::json!({ "source": source })),
};
let report = |source: Option<&str>| {
let results = ValidationResults {
success: vec![status(source)],
..ValidationResults::default()
};
super::trust_report(&results, true, "bundled_static_material", String::new())
.revocation
.source
};
assert_eq!(report(Some("online_ocsp")), "online_ocsp");
assert_eq!(report(None), "embedded_ocsp");
}
#[test]
fn format_list_is_sorted_and_contains_composition_formats() {
let formats = supported_mime_types();
assert_eq!(formats.len(), 73);
assert!(formats.windows(2).all(|pair| pair[0] < pair[1]));
assert!(formats.contains(&"video/mp4"));
assert!(formats.contains(&"image/jpeg"));
assert!(formats.contains(&"text/tab-separated-values"));
assert!(formats.contains(&"application/vnd.oasis.opendocument.graphics"));
assert!(formats.contains(&"application/zip"));
assert!(formats.contains(&"application/x-zip-based"));
}
#[test]
fn input_alias_is_reported_as_canonical_mime() {
let asset = b"\x00\x00\x00\x10ftypisom\x00\x00\x00\x00";
let report = verify(asset, "audio/aac; codecs=mp4a.40.2").unwrap();
assert_eq!(report.mime_type, "audio/mp4");
assert!(!report.present);
}
#[test]
fn strict_conformance_option_selects_the_program_profile() {
let asset = include_bytes!("../../../tests/fixtures/signed_test.jpg");
let report = verify_with_options(
asset,
"image/jpeg",
&VerifyOptions {
strict_conformance: true,
..VerifyOptions::default()
},
)
.unwrap();
assert_eq!(
report.manifest_report["engine_profile"]["operating_mode"],
"conformance"
);
assert_eq!(
report.manifest_report["engine_profile"]["compliance_level"],
"conformance-program"
);
}
#[test]
fn fragmented_entry_point_rejects_non_bmff_mime() {
let error = verify_fragmented(
include_bytes!("../../../tests/fixtures/signed_test.jpg"),
&[b"ignored fragment"],
"image/jpeg",
)
.unwrap_err();
assert!(matches!(error, Error::UnsupportedMime(mime) if mime == "image/jpeg"));
}
#[test]
fn unratified_hostless_store_is_not_in_public_profile() {
let error = verify(b"jumb", "application/c2pa").unwrap_err();
assert!(matches!(error, Error::UnsupportedMime(_)));
}
#[test]
fn bounded_reader_accepts_exact_limit_without_large_allocation() {
let mut input = Cursor::new(b"1234");
assert_eq!(read_bounded_file(&mut input, 4, 4).unwrap(), b"1234");
}
#[test]
fn bounded_reader_detects_growth_at_limit_plus_one() {
let mut input = Cursor::new(b"12345");
let error = read_bounded_file(&mut input, 4, 4).unwrap_err();
assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
assert!(error.to_string().contains("grew while being read"));
}
#[test]
fn unsupported_mime_has_stable_error_code() {
let error = verify(b"not an asset", "application/x-unknown").unwrap_err();
assert!(matches!(error, Error::UnsupportedMime(_)));
assert_eq!(error.code(), "unsupported_mime");
}
#[test]
fn verify_file_resolves_options_before_opening_the_asset() {
let path = std::env::temp_dir().join(format!(
"encypher-c2pa-invalid-options-missing-{}.jpg",
std::process::id()
));
let options = VerifyOptions {
no_default_trust: true,
cawg_trust_configurations: Some(vec![CawgTrustConfiguration {
profile: CawgTrustProfile::Base,
certificates_pem: String::new(),
not_before: None,
not_after: None,
}]),
..VerifyOptions::default()
};
let error = verify_file(path, Some("image/jpeg"), &options)
.expect_err("invalid trust material must win over a missing asset");
assert_eq!(error.code(), "invalid_trust_material");
assert!(
error
.to_string()
.contains("cawg_trust_configurations[0].certificates_pem"),
"{error}"
);
}
#[test]
fn a_malformed_cawg_trust_configuration_fails_the_call_naming_its_entry() {
let root = include_str!("default_trust/encypher-identity-root.pem");
let resolve = |entry: serde_json::Value| {
let options: VerifyOptions = serde_json::from_value(serde_json::json!({
"no_default_trust": true,
"cawg_trust_configurations": [
{ "profile": "base", "certificates_pem": root },
entry,
],
}))
.map_err(|error| error.to_string())?;
super::ResolvedOptions::resolve(&options)
.map(|_| ())
.map_err(|error| format!("{}: {error}", error.code()))
};
for (entry, field) in [
(
serde_json::json!({ "profile": "smime_interim", "certificates_pem": "" }),
"cawg_trust_configurations[1].certificates_pem",
),
(
serde_json::json!({ "profile": "smime_interim", "certificates_pem": root, "not_after": "next spring" }),
"cawg_trust_configurations[1].not_after",
),
(
serde_json::json!({
"profile": "smime_interim",
"certificates_pem": root,
"not_before": "2027-01-02T00:00:00Z",
"not_after": "2027-01-01T00:00:00Z",
}),
"cawg_trust_configurations[1]",
),
] {
let error = resolve(entry).expect_err("malformed configuration");
assert!(error.starts_with("invalid_trust_material: "), "{error}");
assert!(error.contains(field), "{error}");
}
let unknown =
resolve(serde_json::json!({ "profile": "trusted", "certificates_pem": root }))
.expect_err("unknown profile");
assert!(unknown.contains("unknown variant `trusted`"), "{unknown}");
assert_eq!(
resolve(serde_json::json!({ "profile": "smime_interim", "certificates_pem": root })),
Ok(())
);
}
}