1#![forbid(unsafe_code)]
11#[path = "c2pa-cbor/lib.rs"]
50#[allow(dead_code, reason = "production kernel mirror")]
51mod c2pa_cbor;
52#[path = "c2pa-core/lib.rs"]
53#[allow(dead_code, unused_imports, reason = "production kernel mirror")]
54mod c2pa_core;
55#[path = "c2pa-crypto/lib.rs"]
56#[allow(dead_code, unused_imports, reason = "production kernel mirror")]
57mod c2pa_crypto;
58#[path = "c2pa-formats/lib.rs"]
59#[allow(dead_code, unused_imports, reason = "production kernel mirror")]
60mod c2pa_formats;
61#[path = "c2pa-trust/lib.rs"]
62#[allow(dead_code, unused_imports, reason = "production kernel mirror")]
63mod c2pa_trust;
64#[path = "c2pa-validate/lib.rs"]
65#[allow(dead_code, unused_imports, reason = "production kernel mirror")]
66mod c2pa_validate;
67mod config_dir;
68mod default_trust;
69mod online;
70mod online_consent;
71mod stream;
72pub use stream::{
73 verify_stream, verify_stream_with_options, SegmentReport, StreamEncapsulation, StreamMethod,
74 StreamVerificationReport,
75};
76mod telemetry;
77mod telemetry_consent;
78pub use config_dir::config_directory;
79pub use default_trust::SNAPSHOT_DATE as DEFAULT_TRUST_SNAPSHOT_DATE;
80
81pub use online::{NetworkReport, NetworkRequest};
82pub use online_consent::{
83 allow_interactive_online_consent, online_preference, set_online_preference, OnlinePreference,
84 OnlinePreferenceError,
85};
86pub use telemetry::{
87 validation_failure_telemetry, TelemetryOptions, ValidationFailureTelemetry,
88 DEFAULT_TELEMETRY_ENDPOINT,
89};
90pub use telemetry_consent::{
91 prompt_for_telemetry_consent, set_telemetry_enabled, telemetry_preference,
92 TelemetryPreferenceError,
93};
94
95use std::collections::HashMap;
96use std::fs::{self, OpenOptions};
97use std::io::{self, Read};
98#[cfg(unix)]
99use std::os::unix::fs::OpenOptionsExt;
100use std::path::Path;
101
102use crate::c2pa_core::{
103 spec::{canonicalize_mime, mimes_for_version},
104 EngineProfile, SpecVersion,
105};
106use crate::c2pa_trust::{AnchorPurpose, CawgTrustSource, TrustList};
107use crate::c2pa_validate::{
108 verify_fragmented_with_cawg_options_and_expected_seeks_safe as verify_fragmented_safe,
109 verify_with_cawg_options_safe as verify_safe, StatusCode as CoreStatus,
110 ValidationResults as CoreResults, VerifyInput, ASSERTION_BMFF_HASH_MALFORMED,
111 ASSERTION_BMFF_HASH_MATCH, ASSERTION_BMFF_HASH_MISMATCH, ASSERTION_BOXES_HASH_MALFORMED,
112 ASSERTION_BOXES_HASH_MATCH, ASSERTION_BOXES_HASH_MISMATCH, ASSERTION_COLLECTION_HASH_MALFORMED,
113 ASSERTION_COLLECTION_HASH_MATCH, ASSERTION_COLLECTION_HASH_MISMATCH, ASSERTION_DATA_HASH_MATCH,
114 ASSERTION_DATA_HASH_MISMATCH, ASSERTION_MULTI_ASSET_HASH_MALFORMED,
115 ASSERTION_MULTI_ASSET_HASH_MATCH, ASSERTION_MULTI_ASSET_HASH_MISMATCH,
116 CLAIM_HARD_BINDINGS_MISSING, CLAIM_SIGNATURE_MISMATCH, CLAIM_SIGNATURE_MISSING,
117 CLAIM_SIGNATURE_VALIDATED, SIGNING_CREDENTIAL_INVALID, SIGNING_CREDENTIAL_OCSP_NOT_REVOKED,
118 SIGNING_CREDENTIAL_OCSP_REVOKED, SIGNING_CREDENTIAL_TRUSTED, SIGNING_CREDENTIAL_UNTRUSTED,
119};
120use serde::{Deserialize, Serialize};
121use serde_json::Value;
122use sha2::{Digest, Sha256};
123use time::{format_description::well_known::Rfc3339, OffsetDateTime};
124
125pub const REPORT_SCHEMA_VERSION: &str = "1.0";
126pub const C2PA_PROFILE: &str = "c2pa-2.4";
127const MAX_MANIFEST_STORE_BYTES: usize = 64 * 1024 * 1024;
128const MAX_PATH_ASSET_BYTES: u64 = 128 * 1024 * 1024;
129const MAX_OCSP_EVIDENCE_BYTES: usize = 64 * 1024;
131const MAX_EXTERNAL_DATA_EVIDENCE_BYTES: usize = 64 * 1024 * 1024;
133
134#[derive(Debug, Clone, Default, Deserialize)]
135#[serde(default, deny_unknown_fields)]
136pub struct VerifyOptions {
137 pub trust_pem: Option<String>,
139 pub tsa_trust_pem: Option<String>,
141 pub allowed_list_pem: Option<String>,
143 pub cawg_trust_pem: Option<String>,
147 pub cawg_allowed_certs_pem: Option<String>,
149 pub trust_anchor_not_before: Option<String>,
154 pub trust_anchor_not_after: Option<String>,
158 pub no_default_trust: bool,
162 pub cawg_did_documents: Option<HashMap<String, Value>>,
165 pub cawg_ica_trusted_issuers: Option<Vec<String>>,
167 pub cawg_ica_trust_anchors: Option<Vec<String>>,
170 pub cawg_ica_status_lists: Option<HashMap<String, String>>,
174 pub ocsp_responses: Option<HashMap<String, String>>,
184 pub ocsp_unreachable: Option<Vec<String>>,
191 pub external_data: Option<HashMap<String, String>>,
197 pub expected_seek_positions: Vec<usize>,
201 pub strict_conformance: bool,
208 pub cawg_strict_encoding: bool,
214 pub validation_time: Option<String>,
216 pub telemetry: TelemetryOptions,
218 pub online: Option<bool>,
229 pub online_allow_private_networks: bool,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
243pub struct VerificationStatus {
244 pub code: String,
245 pub url: String,
246 pub explanation: String,
247 #[serde(default, skip_serializing_if = "Option::is_none")]
250 pub details: Option<Value>,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
254pub struct ValidationResults {
255 pub success: Vec<VerificationStatus>,
256 pub informational: Vec<VerificationStatus>,
257 pub failure: Vec<VerificationStatus>,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
261pub struct RevocationReport {
262 pub status: String,
263 pub source: String,
264 pub responder_signature: String,
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
268pub struct FreshnessReport {
269 pub status: String,
270 pub as_of: Option<String>,
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
274pub struct TrustReport {
275 pub status: String,
276 pub basis: String,
277 pub validation_time: String,
278 pub revocation: RevocationReport,
279 pub freshness: FreshnessReport,
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct VerificationReport {
284 pub schema_version: String,
285 pub profile: String,
286 pub mime_type: String,
287 pub present: bool,
288 pub integrity: String,
289 pub signature: String,
290 pub hard_binding: String,
291 pub trust: TrustReport,
292 pub policy: Option<Value>,
293 pub managed_receipt: Option<Value>,
294 pub validation_state: String,
295 pub validation_results: ValidationResults,
296 pub manifest_report: Value,
297 pub content_credentials: Option<Value>,
298 #[serde(default)]
300 pub network: NetworkReport,
301}
302#[derive(Debug, Clone, PartialEq, Eq)]
309pub struct DetachedManifestEvidence {
310 pub manifest_store: Vec<u8>,
311 pub manifest_store_sha256: String,
312 pub carrier: Vec<u8>,
313}
314
315impl VerificationReport {
316 pub fn to_json(&self) -> Result<String, Error> {
317 serde_json::to_string(self).map_err(Error::Serialize)
318 }
319
320 pub fn to_pretty_json(&self) -> Result<String, Error> {
321 serde_json::to_string_pretty(self).map_err(Error::Serialize)
322 }
323
324 pub fn cawg_statuses(&self) -> Vec<&VerificationStatus> {
328 self.validation_results
329 .success
330 .iter()
331 .chain(&self.validation_results.informational)
332 .chain(&self.validation_results.failure)
333 .filter(|status| status.code.starts_with("cawg."))
334 .collect()
335 }
336}
337
338#[derive(Debug, thiserror::Error)]
339pub enum Error {
340 #[error("unsupported MIME type: {0}")]
341 UnsupportedMime(String),
342 #[error("invalid trust material: {0}")]
343 InvalidTrust(String),
344 #[error("invalid validation time: {0}")]
345 InvalidValidationTime(String),
346 #[error("verification failed: {0}")]
347 Verification(String),
348 #[error("could not read asset: {0}")]
349 Io(#[from] std::io::Error),
350 #[error(transparent)]
351 TelemetryPreference(#[from] TelemetryPreferenceError),
352 #[error(transparent)]
353 OnlinePreference(#[from] OnlinePreferenceError),
354 #[error("could not serialize report: {0}")]
355 Serialize(serde_json::Error),
356}
357
358impl Error {
359 pub fn code(&self) -> &'static str {
360 match self {
361 Self::UnsupportedMime(_) => "unsupported_mime",
362 Self::InvalidTrust(_) => "invalid_trust_material",
363 Self::InvalidValidationTime(_) => "invalid_validation_time",
364 Self::Verification(_) => "verification_error",
365 Self::Io(_) => "io_error",
366 Self::OnlinePreference(_) => "online_preference_error",
367 Self::TelemetryPreference(_) => "telemetry_preference_error",
368 Self::Serialize(_) => "serialization_error",
369 }
370 }
371}
372
373pub fn verify(data: &[u8], mime_type: &str) -> Result<VerificationReport, Error> {
378 verify_with_options(data, mime_type, &VerifyOptions::default())
379}
380
381pub fn verify_fragmented(
387 init_segment: &[u8],
388 fragments: &[&[u8]],
389 mime_type: &str,
390) -> Result<VerificationReport, Error> {
391 verify_fragmented_with_options(
392 init_segment,
393 fragments,
394 mime_type,
395 &VerifyOptions::default(),
396 )
397}
398pub fn detached_manifest_evidence(
405 data: &[u8],
406 mime_type: &str,
407) -> Result<Option<DetachedManifestEvidence>, Error> {
408 let mime = canonicalize_mime(mime_type);
409 let format = crate::c2pa_formats::AssetFormat::from_mime(&mime)
410 .ok_or_else(|| Error::UnsupportedMime(mime.clone()))?;
411 if !crate::c2pa_formats::supports_hash_mode(&mime) {
412 return Ok(None);
413 }
414 let Some(manifest_store) = crate::c2pa_formats::extract_manifest(format, data)
415 .map_err(|error| Error::Verification(error.to_string()))?
416 else {
417 return Ok(None);
418 };
419 let spans = crate::c2pa_formats::compute_data_hash_exclusions(format, data)
420 .map_err(|error| Error::Verification(error.to_string()))?;
421 let [span] = spans.as_slice() else {
422 return Ok(None);
423 };
424 let end = span
425 .start
426 .checked_add(span.length)
427 .filter(|end| *end <= data.len())
428 .ok_or_else(|| Error::Verification("manifest carrier exceeds asset bounds".into()))?;
429 let manifest_store_sha256 = hex::encode(Sha256::digest(&manifest_store));
430 Ok(Some(DetachedManifestEvidence {
431 manifest_store,
432 manifest_store_sha256,
433 carrier: data[span.start..end].to_vec(),
434 }))
435}
436
437pub fn verify_with_manifest_store(
454 asset: &[u8],
455 manifest_store: &[u8],
456 mime_type: &str,
457 options: &VerifyOptions,
458) -> Result<VerificationReport, Error> {
459 let telemetry_enabled = telemetry_consent::resolve_telemetry_enabled(options.telemetry.enabled);
460 let result = verify_with_manifest_store_inner(asset, manifest_store, mime_type, options);
461 if let Some(event) = telemetry::validation_failure_telemetry_with_enabled(
462 mime_type,
463 &result,
464 &options.telemetry,
465 telemetry_enabled,
466 ) {
467 telemetry::enqueue(options.telemetry.endpoint(), event);
468 }
469 result
470}
471
472fn verify_with_manifest_store_inner(
473 asset: &[u8],
474 manifest_store: &[u8],
475 mime_type: &str,
476 options: &VerifyOptions,
477) -> Result<VerificationReport, Error> {
478 let mime = resolve_mime(mime_type)?;
479 let (mut report, needs) = detached_pass(asset, manifest_store, &mime, options)?;
480 let (network, evidence) = online::gather(options, &needs);
481 if evidence.is_empty() {
482 report.network = network;
483 return Ok(report);
484 }
485 let next = online::apply_evidence(options, &evidence);
488 let (mut report, _) = detached_pass(asset, manifest_store, &mime, &next)?;
489 report.network = network;
490 Ok(report)
491}
492
493fn detached_pass(
496 asset: &[u8],
497 manifest_store: &[u8],
498 mime: &str,
499 options: &VerifyOptions,
500) -> Result<(VerificationReport, Vec<online::NetworkNeed>), Error> {
501 if manifest_store.is_empty() {
502 return Err(Error::Verification("manifest store is empty".into()));
503 }
504 if manifest_store.len() > MAX_MANIFEST_STORE_BYTES {
505 return Err(Error::Verification(format!(
506 "manifest store exceeds the {MAX_MANIFEST_STORE_BYTES} byte limit"
507 )));
508 }
509 let resolved = ResolvedOptions::resolve(options)?;
510 let input = resolved.input(asset, mime);
511 let output = crate::c2pa_validate::verify_detached_safe(
512 manifest_store,
513 asset,
514 mime,
515 &input,
516 resolved.cawg_trust(),
517 resolved.cawg_allowed_certs(),
518 true,
519 options.cawg_did_documents.as_ref(),
520 options.cawg_ica_trusted_issuers.as_deref(),
521 options.cawg_ica_trust_anchors.as_deref(),
522 options.cawg_ica_status_lists.as_ref(),
523 )
524 .map_err(map_validate_error)?;
525 let needs = online::network_needs(&output);
526 Ok((
527 report_from_output(output, mime.to_string(), &resolved),
528 needs,
529 ))
530}
531
532pub fn verify_with_options(
536 data: &[u8],
537 mime_type: &str,
538 options: &VerifyOptions,
539) -> Result<VerificationReport, Error> {
540 let telemetry_enabled = telemetry_consent::resolve_telemetry_enabled(options.telemetry.enabled);
541 let result = verify_with_options_inner(data, None, mime_type, options);
542 if let Some(event) = telemetry::validation_failure_telemetry_with_enabled(
543 mime_type,
544 &result,
545 &options.telemetry,
546 telemetry_enabled,
547 ) {
548 telemetry::enqueue(options.telemetry.endpoint(), event);
549 }
550 result
551}
552
553pub fn verify_fragmented_with_options(
561 init_segment: &[u8],
562 fragments: &[&[u8]],
563 mime_type: &str,
564 options: &VerifyOptions,
565) -> Result<VerificationReport, Error> {
566 let telemetry_enabled = telemetry_consent::resolve_telemetry_enabled(options.telemetry.enabled);
567 let mime = canonicalize_mime(mime_type);
568 let result = if crate::c2pa_formats::AssetFormat::from_mime(&mime)
569 == Some(crate::c2pa_formats::AssetFormat::Bmff)
570 {
571 verify_with_options_inner(init_segment, Some(fragments), &mime, options)
572 } else {
573 Err(Error::UnsupportedMime(mime))
574 };
575 if let Some(event) = telemetry::validation_failure_telemetry_with_enabled(
576 mime_type,
577 &result,
578 &options.telemetry,
579 telemetry_enabled,
580 ) {
581 telemetry::enqueue(options.telemetry.endpoint(), event);
582 }
583 result
584}
585
586pub(crate) struct ResolvedOptions {
593 claim_trust: Option<ResolvedTrust>,
594 tsa_trust: Option<ResolvedTrust>,
595 allowed_certs: Option<ResolvedTrust>,
596 cawg_trust: Option<ResolvedTrust>,
597 cawg_allowed_certs: Option<ResolvedTrust>,
598 validation_time: OffsetDateTime,
599 validation_time_text: String,
600 trust_basis: &'static str,
601 profile: EngineProfile,
602 cawg_strict_encoding: bool,
603 ocsp_responses: HashMap<String, Vec<u8>>,
605 ocsp_unreachable: Vec<String>,
606 external_data: HashMap<String, Vec<u8>>,
607}
608
609impl ResolvedOptions {
610 pub(crate) fn resolve(options: &VerifyOptions) -> Result<Self, Error> {
611 let use_defaults = !options.no_default_trust;
612 let validation_time = parse_validation_time(options.validation_time.as_deref())?;
613 let custom_claim_trust = options.trust_pem.is_some() || options.allowed_list_pem.is_some();
614 let bounds = (
615 parse_optional_instant(
616 options.trust_anchor_not_before.as_deref(),
617 "trust_anchor_not_before",
618 )?,
619 parse_optional_instant(
620 options.trust_anchor_not_after.as_deref(),
621 "trust_anchor_not_after",
622 )?,
623 );
624 Ok(Self {
625 claim_trust: resolve_trust(
626 options.trust_pem.as_deref(),
627 use_defaults.then(default_trust::claim_signing),
628 AnchorPurpose::ClaimSigning,
629 bounds,
630 )?,
631 tsa_trust: resolve_trust(
632 options.tsa_trust_pem.as_deref(),
633 use_defaults.then(default_trust::timestamp_authorities),
634 AnchorPurpose::TimeStamping,
635 bounds,
636 )?,
637 allowed_certs: resolve_trust(
638 options.allowed_list_pem.as_deref(),
639 use_defaults.then(default_trust::allowed_claim_signers),
640 AnchorPurpose::ClaimSigning,
641 bounds,
642 )?,
643 cawg_trust: resolve_trust(
644 options.cawg_trust_pem.as_deref(),
645 use_defaults.then(default_trust::cawg_identity),
646 AnchorPurpose::CawgIdentity,
647 bounds,
648 )?,
649 cawg_allowed_certs: resolve_trust(
650 options.cawg_allowed_certs_pem.as_deref(),
651 use_defaults.then(default_trust::cawg_allowed_identities),
652 AnchorPurpose::CawgIdentity,
653 bounds,
654 )?,
655 validation_time,
656 validation_time_text: validation_time
657 .format(&Rfc3339)
658 .map_err(|error| Error::InvalidValidationTime(error.to_string()))?,
659 trust_basis: match (use_defaults, custom_claim_trust) {
660 (true, true) => "bundled_and_caller_supplied_static_material",
661 (true, false) => "bundled_static_material",
662 (false, true) => "caller_supplied_static_material",
663 (false, false) => "none",
664 },
665 profile: if options.strict_conformance {
666 EngineProfile::strict(SpecVersion::V2_4)
667 } else {
668 EngineProfile::GENEROUS
669 },
670 cawg_strict_encoding: options.cawg_strict_encoding,
671 ocsp_responses: decode_base64_map(
672 options.ocsp_responses.as_ref(),
673 "ocsp_responses",
674 MAX_OCSP_EVIDENCE_BYTES,
675 )?,
676 ocsp_unreachable: options.ocsp_unreachable.clone().unwrap_or_default(),
677 external_data: decode_base64_map(
678 options.external_data.as_ref(),
679 "external_data",
680 MAX_EXTERNAL_DATA_EVIDENCE_BYTES,
681 )?,
682 })
683 }
684
685 pub(crate) fn input<'a>(&'a self, data: &'a [u8], mime: &'a str) -> VerifyInput<'a> {
686 VerifyInput {
687 data,
688 mime,
689 claim_signer_trust: self.claim_trust.as_ref().map(ResolvedTrust::get),
690 tsa_trust: self.tsa_trust.as_ref().map(ResolvedTrust::get),
691 allowed_certs: self.allowed_certs.as_ref().map(ResolvedTrust::get),
692 validation_time: Some(self.validation_time),
693 profile: self.profile,
694 cawg_strict_encoding: self.cawg_strict_encoding,
695 evidence: crate::c2pa_validate::OnlineEvidence {
696 ocsp_responses: (!self.ocsp_responses.is_empty()).then_some(&self.ocsp_responses),
697 ocsp_unreachable: (!self.ocsp_unreachable.is_empty())
698 .then_some(self.ocsp_unreachable.as_slice()),
699 external_data: (!self.external_data.is_empty()).then_some(&self.external_data),
700 },
701 }
702 }
703
704 pub(crate) fn cawg_trust(&self) -> Option<&TrustList> {
705 self.cawg_trust.as_ref().map(ResolvedTrust::get)
706 }
707
708 pub(crate) fn cawg_allowed_certs(&self) -> Option<&TrustList> {
709 self.cawg_allowed_certs.as_ref().map(ResolvedTrust::get)
710 }
711}
712
713fn decode_base64_map(
719 values: Option<&HashMap<String, String>>,
720 field: &str,
721 max_bytes: usize,
722) -> Result<HashMap<String, Vec<u8>>, Error> {
723 let Some(values) = values else {
724 return Ok(HashMap::new());
725 };
726 let mut decoded = HashMap::with_capacity(values.len());
727 for (key, encoded) in values {
728 let bytes = crate::c2pa_formats::util::base64_decode(encoded)
729 .ok_or_else(|| Error::Verification(format!("{field}[{key}] is not standard base64")))?;
730 if bytes.len() > max_bytes {
731 return Err(Error::Verification(format!(
732 "{field}[{key}] exceeds the {max_bytes} byte limit"
733 )));
734 }
735 decoded.insert(key.clone(), bytes);
736 }
737 Ok(decoded)
738}
739
740pub(crate) fn resolve_mime(mime_type: &str) -> Result<String, Error> {
742 let mime = canonicalize_mime(mime_type);
743 if !mimes_for_version(SpecVersion::V2_4).contains(&mime.as_str())
744 || crate::c2pa_formats::AssetFormat::from_mime(&mime).is_none()
745 {
746 return Err(Error::UnsupportedMime(mime));
747 }
748 Ok(mime)
749}
750
751pub(crate) fn map_validate_error(error: crate::c2pa_validate::ValidateError) -> Error {
752 match error {
753 crate::c2pa_validate::ValidateError::UnsupportedMime(value) => {
754 Error::UnsupportedMime(value)
755 }
756 other => Error::Verification(other.to_string()),
757 }
758}
759
760pub(crate) fn report_from_output(
762 output: crate::c2pa_validate::VerifyOutput,
763 mime: String,
764 resolved: &ResolvedOptions,
765) -> VerificationReport {
766 let present = output
767 .report_json
768 .pointer("/provenance_verdict/present")
769 .and_then(Value::as_bool)
770 .unwrap_or(false);
771 let integrity = output
772 .report_json
773 .pointer("/provenance_verdict/integrity")
774 .and_then(Value::as_str)
775 .unwrap_or(if present { "invalid" } else { "absent" })
776 .to_string();
777 let trust = trust_report(
778 &output.results,
779 present,
780 resolved.trust_basis,
781 resolved.validation_time_text.clone(),
782 );
783 VerificationReport {
784 schema_version: REPORT_SCHEMA_VERSION.to_string(),
785 profile: C2PA_PROFILE.to_string(),
786 mime_type: mime,
787 present,
788 integrity,
789 signature: signature_status(&output.results),
790 hard_binding: hard_binding_status(&output.results),
791 trust,
792 policy: None,
793 managed_receipt: None,
794 validation_state: output.validation_state.as_str().to_string(),
795 validation_results: copy_results(&output.results),
796 manifest_report: output.report_json,
797 content_credentials: output.crjson,
798 network: NetworkReport::default(),
799 }
800}
801
802fn verify_with_options_inner(
803 data: &[u8],
804 fragments: Option<&[&[u8]]>,
805 mime_type: &str,
806 options: &VerifyOptions,
807) -> Result<VerificationReport, Error> {
808 let mime = resolve_mime(mime_type)?;
809 let (mut report, needs) = embedded_pass(data, fragments, &mime, options)?;
810 let (mut network, evidence) = online::gather(options, &needs);
811 if evidence.is_empty() {
812 report.network = network;
813 return Ok(report);
814 }
815 let next = online::apply_evidence(options, &evidence);
816 let second = match (evidence.manifest_store.as_deref(), fragments) {
817 (Some(store), None) => detached_pass(data, store, &mime, &next),
821 _ => embedded_pass(data, fragments, &mime, &next),
822 };
823 match second {
824 Ok((mut second, _)) => {
825 second.network = network;
826 Ok(second)
827 }
828 Err(error) => {
832 network.mark_unusable("remote_manifest", error.to_string());
833 report.network = network;
834 Ok(report)
835 }
836 }
837}
838
839fn embedded_pass(
842 data: &[u8],
843 fragments: Option<&[&[u8]]>,
844 mime: &str,
845 options: &VerifyOptions,
846) -> Result<(VerificationReport, Vec<online::NetworkNeed>), Error> {
847 let resolved = ResolvedOptions::resolve(options)?;
848 let input = resolved.input(data, mime);
849 let output = match fragments {
850 Some(fragments) => verify_fragmented_safe(
851 &input,
852 fragments,
853 &options.expected_seek_positions,
854 resolved.cawg_trust(),
855 resolved.cawg_allowed_certs(),
856 true,
857 options.cawg_did_documents.as_ref(),
858 options.cawg_ica_trusted_issuers.as_deref(),
859 options.cawg_ica_trust_anchors.as_deref(),
860 options.cawg_ica_status_lists.as_ref(),
861 ),
862 None => verify_safe(
863 &input,
864 resolved.cawg_trust(),
865 resolved.cawg_allowed_certs(),
866 true,
867 options.cawg_did_documents.as_ref(),
868 options.cawg_ica_trusted_issuers.as_deref(),
869 options.cawg_ica_trust_anchors.as_deref(),
870 options.cawg_ica_status_lists.as_ref(),
871 ),
872 }
873 .map_err(map_validate_error)?;
874 let needs = online::network_needs(&output);
875 Ok((
876 report_from_output(output, mime.to_string(), &resolved),
877 needs,
878 ))
879}
880
881pub fn verify_file(
886 path: impl AsRef<Path>,
887 mime_type: Option<&str>,
888 options: &VerifyOptions,
889) -> Result<VerificationReport, Error> {
890 let path = path.as_ref();
891 let mime = match mime_type {
892 Some(value) => value.to_string(),
893 None => mime_from_path(path)
894 .ok_or_else(|| Error::UnsupportedMime(path.display().to_string()))?
895 .to_string(),
896 };
897 let data = read_path_asset(path, MAX_PATH_ASSET_BYTES)?;
898 verify_with_options(&data, &mime, options)
899}
900
901fn read_path_asset(path: &Path, limit: u64) -> io::Result<Vec<u8>> {
902 let mut options = OpenOptions::new();
903 options.read(true);
904 #[cfg(unix)]
905 options.custom_flags(libc::O_NONBLOCK | libc::O_CLOEXEC);
906
907 let mut file = options.open(path)?;
910 let opened_metadata = file.metadata()?;
911 validate_path_asset(path, &opened_metadata, limit)?;
912 read_bounded_file(&mut file, opened_metadata.len(), limit)
913}
914
915fn validate_path_asset(path: &Path, metadata: &fs::Metadata, limit: u64) -> io::Result<()> {
916 if !metadata.is_file() {
917 return Err(io::Error::new(
918 io::ErrorKind::InvalidInput,
919 format!("asset path is not a regular file: {}", path.display()),
920 ));
921 }
922 if metadata.len() > limit {
923 return Err(io::Error::new(
924 io::ErrorKind::InvalidData,
925 format!("asset exceeds the 128 MiB path limit: {}", path.display()),
926 ));
927 }
928 Ok(())
929}
930
931fn read_bounded_file<R: Read>(file: &mut R, expected_len: u64, limit: u64) -> io::Result<Vec<u8>> {
932 if expected_len > limit {
933 return Err(io::Error::new(
934 io::ErrorKind::InvalidData,
935 "asset exceeds the path size limit",
936 ));
937 }
938 let expected_len = usize::try_from(expected_len)
939 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "asset size is not addressable"))?;
940 let mut data = vec![0_u8; expected_len + 1];
941 let mut used = 0;
942 while used < expected_len {
943 let read = file.read(&mut data[used..expected_len])?;
944 if read == 0 {
945 break;
946 }
947 used += read;
948 }
949 if used == expected_len && file.read(&mut data[expected_len..expected_len + 1])? != 0 {
950 return Err(io::Error::new(
951 io::ErrorKind::InvalidData,
952 "asset grew while being read",
953 ));
954 }
955 data.truncate(used);
956 Ok(data)
957}
958
959pub fn supported_mime_types() -> Vec<&'static str> {
961 let mut mimes: Vec<_> = mimes_for_version(SpecVersion::V2_4)
962 .into_iter()
963 .filter(|mime| crate::c2pa_formats::AssetFormat::from_mime(mime).is_some())
964 .collect();
965 mimes.sort_unstable();
966 mimes.dedup();
967 mimes
968}
969
970pub const SUPPORTED_EXTENSIONS: &[(&str, &str)] = &[
978 ("jpg", "image/jpeg"),
979 ("jpeg", "image/jpeg"),
980 ("png", "image/png"),
981 ("webp", "image/webp"),
982 ("gif", "image/gif"),
983 ("tif", "image/tiff"),
984 ("tiff", "image/tiff"),
985 ("dng", "image/x-adobe-dng"),
986 ("heic", "image/heic"),
987 ("heics", "image/heic-sequence"),
988 ("heif", "image/heif"),
989 ("heifs", "image/heif-sequence"),
990 ("avif", "image/avif"),
991 ("jxl", "image/jxl"),
992 ("svg", "image/svg+xml"),
993 ("mp4", "video/mp4"),
994 ("m4v", "video/mp4"),
995 ("mov", "video/quicktime"),
996 ("avi", "video/x-msvideo"),
997 ("wav", "audio/wav"),
998 ("mp3", "audio/mpeg"),
999 ("m4a", "audio/mp4"),
1000 ("aac", "audio/aac"),
1001 ("flac", "audio/flac"),
1002 ("ogg", "audio/ogg"),
1003 ("oga", "audio/ogg"),
1004 ("pdf", "application/pdf"),
1005 ("epub", "application/epub+zip"),
1006 (
1007 "docx",
1008 "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1009 ),
1010 (
1011 "xlsx",
1012 "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1013 ),
1014 (
1015 "pptx",
1016 "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1017 ),
1018 (
1019 "dotx",
1020 "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
1021 ),
1022 ("docm", "application/vnd.ms-word.document.macroenabled.12"),
1023 ("dotm", "application/vnd.ms-word.template.macroenabled.12"),
1024 (
1025 "xltx",
1026 "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
1027 ),
1028 ("xlsm", "application/vnd.ms-excel.sheet.macroenabled.12"),
1029 ("xltm", "application/vnd.ms-excel.template.macroenabled.12"),
1030 (
1031 "xlsb",
1032 "application/vnd.ms-excel.sheet.binary.macroenabled.12",
1033 ),
1034 (
1035 "ppsx",
1036 "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
1037 ),
1038 (
1039 "potx",
1040 "application/vnd.openxmlformats-officedocument.presentationml.template",
1041 ),
1042 (
1043 "pptm",
1044 "application/vnd.ms-powerpoint.presentation.macroenabled.12",
1045 ),
1046 (
1047 "ppsm",
1048 "application/vnd.ms-powerpoint.slideshow.macroenabled.12",
1049 ),
1050 (
1051 "potm",
1052 "application/vnd.ms-powerpoint.template.macroenabled.12",
1053 ),
1054 ("vsdx", "application/vnd.ms-visio.drawing"),
1055 ("vsdm", "application/vnd.ms-visio.drawing.macroenabled.12"),
1056 ("vssx", "application/vnd.ms-visio.stencil"),
1057 ("vssm", "application/vnd.ms-visio.stencil.macroenabled.12"),
1058 ("vstx", "application/vnd.ms-visio.template"),
1059 ("vstm", "application/vnd.ms-visio.template.macroenabled.12"),
1060 ("oxps", "application/oxps"),
1061 ("xps", "application/vnd.ms-xpsdocument"),
1062 ("odt", "application/vnd.oasis.opendocument.text"),
1063 ("ods", "application/vnd.oasis.opendocument.spreadsheet"),
1064 ("odp", "application/vnd.oasis.opendocument.presentation"),
1065 ("odg", "application/vnd.oasis.opendocument.graphics"),
1066 ("ttf", "font/ttf"),
1067 ("otf", "font/otf"),
1068 ("sfnt", "font/sfnt"),
1069 ("txt", "text/plain"),
1070 ("tsv", "text/tab-separated-values"),
1071 ("csv", "text/csv"),
1072 ("md", "text/markdown"),
1073 ("markdown", "text/markdown"),
1074 ("html", "text/html"),
1075 ("htm", "text/html"),
1076 ("xhtml", "application/xhtml+xml"),
1077 ("xml", "application/xml"),
1078 ("css", "text/css"),
1079 ("js", "application/javascript"),
1080 ("mjs", "application/javascript"),
1081 ("json", "application/json"),
1082 ("yaml", "application/yaml"),
1083 ("yml", "application/yaml"),
1084 ("toml", "application/toml"),
1085 ("py", "text/x-python"),
1086];
1087
1088pub fn mime_from_path(path: &Path) -> Option<&'static str> {
1090 let extension = path.extension()?.to_str()?.to_ascii_lowercase();
1091 SUPPORTED_EXTENSIONS
1092 .iter()
1093 .find(|(candidate, _)| *candidate == extension)
1094 .map(|(_, mime)| *mime)
1095}
1096
1097enum ResolvedTrust {
1098 Bundled(&'static TrustList),
1099 Owned(TrustList),
1100}
1101
1102impl ResolvedTrust {
1103 fn get(&self) -> &TrustList {
1104 match self {
1105 Self::Bundled(trust) => trust,
1106 Self::Owned(trust) => trust,
1107 }
1108 }
1109}
1110
1111fn resolve_trust(
1122 custom_pem: Option<&str>,
1123 bundled: Option<&'static TrustList>,
1124 purpose: AnchorPurpose,
1125 bounds: (Option<OffsetDateTime>, Option<OffsetDateTime>),
1126) -> Result<Option<ResolvedTrust>, Error> {
1127 let custom = custom_pem
1128 .map(|pem| TrustList::from_pem_for(purpose, pem))
1129 .transpose()
1130 .map_err(|error| Error::InvalidTrust(error.to_string()))?
1131 .map(|trust| trust.with_bounds(bounds.0, bounds.1))
1132 .map(|trust| match purpose {
1133 AnchorPurpose::CawgIdentity => trust.with_cawg_source(CawgTrustSource::CallerSupplied),
1134 _ => trust,
1135 });
1136 match (bundled, custom) {
1137 (None, None) => Ok(None),
1138 (Some(trust), None) => Ok(Some(ResolvedTrust::Bundled(trust))),
1139 (None, Some(trust)) => Ok(Some(ResolvedTrust::Owned(trust))),
1140 (Some(bundled), Some(custom)) => {
1141 let mut merged = bundled.clone();
1142 merged.anchors.extend(custom.anchors);
1143 Ok(Some(ResolvedTrust::Owned(merged)))
1144 }
1145 }
1146}
1147
1148fn parse_validation_time(value: Option<&str>) -> Result<OffsetDateTime, Error> {
1149 match value {
1150 Some(raw) => OffsetDateTime::parse(raw, &Rfc3339)
1151 .map_err(|error| Error::InvalidValidationTime(error.to_string())),
1152 None => Ok(OffsetDateTime::now_utc()),
1153 }
1154}
1155
1156fn parse_optional_instant(
1159 value: Option<&str>,
1160 field: &str,
1161) -> Result<Option<OffsetDateTime>, Error> {
1162 value
1163 .map(|raw| {
1164 OffsetDateTime::parse(raw, &Rfc3339)
1165 .map_err(|error| Error::InvalidValidationTime(format!("{field}: {error}")))
1166 })
1167 .transpose()
1168}
1169
1170fn copy_status(status: &CoreStatus) -> VerificationStatus {
1171 VerificationStatus {
1172 code: status.code.clone(),
1173 url: status.url.clone(),
1174 explanation: status.explanation.clone(),
1175 details: status.details.clone(),
1176 }
1177}
1178
1179fn copy_results(results: &CoreResults) -> ValidationResults {
1180 ValidationResults {
1181 success: results.success.iter().map(copy_status).collect(),
1182 informational: results.informational.iter().map(copy_status).collect(),
1183 failure: results.failure.iter().map(copy_status).collect(),
1184 }
1185}
1186
1187fn signature_status(results: &CoreResults) -> String {
1188 if results.has_success(CLAIM_SIGNATURE_VALIDATED) {
1189 "valid"
1190 } else if results.has_failure(CLAIM_SIGNATURE_MISMATCH) {
1191 "invalid"
1192 } else if results.has_failure(CLAIM_SIGNATURE_MISSING) {
1193 "missing"
1194 } else {
1195 "unknown"
1196 }
1197 .to_string()
1198}
1199
1200fn hard_binding_status(results: &CoreResults) -> String {
1201 const MATCHES: &[&str] = &[
1202 ASSERTION_DATA_HASH_MATCH,
1203 ASSERTION_BMFF_HASH_MATCH,
1204 ASSERTION_BOXES_HASH_MATCH,
1205 ASSERTION_COLLECTION_HASH_MATCH,
1206 ASSERTION_MULTI_ASSET_HASH_MATCH,
1207 ];
1208 const FAILURES: &[&str] = &[
1209 ASSERTION_DATA_HASH_MISMATCH,
1210 ASSERTION_BMFF_HASH_MISMATCH,
1211 ASSERTION_BMFF_HASH_MALFORMED,
1212 ASSERTION_BOXES_HASH_MISMATCH,
1213 ASSERTION_BOXES_HASH_MALFORMED,
1214 ASSERTION_COLLECTION_HASH_MISMATCH,
1215 ASSERTION_COLLECTION_HASH_MALFORMED,
1216 ASSERTION_MULTI_ASSET_HASH_MISMATCH,
1217 ASSERTION_MULTI_ASSET_HASH_MALFORMED,
1218 ];
1219 if results.has_failure(crate::c2pa_validate::live_video::LIVEVIDEO_SEGMENT_INVALID) {
1226 "mismatch"
1227 } else if MATCHES.iter().any(|code| results.has_success(code)) {
1228 "match"
1229 } else if FAILURES.iter().any(|code| results.has_failure(code)) {
1230 "mismatch"
1231 } else if results.has_failure(CLAIM_HARD_BINDINGS_MISSING) {
1232 "missing"
1233 } else {
1234 "unknown"
1235 }
1236 .to_string()
1237}
1238
1239fn trust_report(
1240 results: &CoreResults,
1241 present: bool,
1242 basis: &str,
1243 validation_time: String,
1244) -> TrustReport {
1245 let supplied = basis != "none";
1246 let trusted = results.has_success(SIGNING_CREDENTIAL_TRUSTED);
1247 let rejected = results.has_failure(SIGNING_CREDENTIAL_UNTRUSTED)
1248 || results.has_failure(SIGNING_CREDENTIAL_INVALID);
1249 let revoked = results.has_failure(SIGNING_CREDENTIAL_OCSP_REVOKED);
1250 let not_revoked = results.has_success(SIGNING_CREDENTIAL_OCSP_NOT_REVOKED);
1251 TrustReport {
1252 status: if trusted && !revoked {
1253 "valid_for_supplied_material"
1254 } else if present && supplied && (rejected || revoked) {
1255 "not_valid_for_supplied_material"
1256 } else {
1257 "not_evaluated"
1258 }
1259 .to_string(),
1260 basis: basis.to_string(),
1261 validation_time,
1262 revocation: RevocationReport {
1263 status: if revoked {
1264 "revoked"
1265 } else if not_revoked {
1266 "not_revoked"
1267 } else {
1268 "not_checked"
1269 }
1270 .to_string(),
1271 source: if !(revoked || not_revoked) {
1272 "none"
1273 } else if results
1274 .success
1275 .iter()
1276 .chain(&results.failure)
1277 .filter(|status| {
1278 status.code == SIGNING_CREDENTIAL_OCSP_REVOKED
1279 || status.code == SIGNING_CREDENTIAL_OCSP_NOT_REVOKED
1280 })
1281 .any(|status| {
1282 status
1283 .details
1284 .as_ref()
1285 .and_then(|details| details.get("source"))
1286 .and_then(Value::as_str)
1287 == Some("online_ocsp")
1288 })
1289 {
1290 "online_ocsp"
1291 } else {
1292 "embedded_ocsp"
1293 }
1294 .to_string(),
1295 responder_signature: if revoked || not_revoked {
1296 "valid"
1297 } else {
1298 "not_applicable"
1299 }
1300 .to_string(),
1301 },
1302 freshness: FreshnessReport {
1303 status: "unknown".to_string(),
1304 as_of: None,
1305 },
1306 }
1307}
1308
1309#[cfg(test)]
1310mod tests {
1311 use super::{
1312 mime_from_path, read_bounded_file, supported_mime_types, verify, verify_fragmented,
1313 verify_with_options, Error, VerifyOptions,
1314 };
1315 use std::{io::Cursor, path::Path};
1316
1317 #[test]
1318 fn known_filename_maps_to_mime() {
1319 assert_eq!(
1320 mime_from_path(Path::new("composition.MP4")),
1321 Some("video/mp4")
1322 );
1323
1324 assert_eq!(
1325 mime_from_path(Path::new("drawing.odg")),
1326 Some("application/vnd.oasis.opendocument.graphics")
1327 );
1328 assert_eq!(
1329 mime_from_path(Path::new("data.tsv")),
1330 Some("text/tab-separated-values")
1331 );
1332 }
1333
1334 #[test]
1338 fn every_supported_format_is_reachable_from_an_extension() {
1339 const ALIASES: &[&str] = &[
1340 "application/font-sfnt",
1341 "application/x-font-ttf",
1342 "application/mp4",
1343 "video/x-m4v",
1344 "text/xml",
1345 ];
1346 let unreachable: Vec<_> = supported_mime_types()
1347 .into_iter()
1348 .filter(|mime| !ALIASES.contains(mime))
1349 .filter(|mime| !super::SUPPORTED_EXTENSIONS.iter().any(|(_, m)| m == mime))
1350 .collect();
1351 assert!(
1352 unreachable.is_empty(),
1353 "no extension maps to {unreachable:?}"
1354 );
1355 assert_eq!(
1356 mime_from_path(Path::new("report.OXPS")),
1357 Some("application/oxps")
1358 );
1359 }
1360
1361 #[test]
1364 fn revocation_source_distinguishes_online_from_embedded_ocsp() {
1365 use crate::c2pa_validate::{StatusCode, ValidationResults};
1366 let status = |source: Option<&str>| StatusCode {
1367 code: crate::c2pa_validate::SIGNING_CREDENTIAL_OCSP_NOT_REVOKED.into(),
1368 url: "self#jumbf=/c2pa/urn:c2pa:x/c2pa.signature".into(),
1369 explanation: "not revoked".into(),
1370 details: source.map(|source| serde_json::json!({ "source": source })),
1371 };
1372 let report = |source: Option<&str>| {
1373 let results = ValidationResults {
1374 success: vec![status(source)],
1375 ..ValidationResults::default()
1376 };
1377 super::trust_report(&results, true, "bundled_static_material", String::new())
1378 .revocation
1379 .source
1380 };
1381 assert_eq!(report(Some("online_ocsp")), "online_ocsp");
1382 assert_eq!(report(None), "embedded_ocsp");
1383 }
1384
1385 #[test]
1386 fn format_list_is_sorted_and_contains_composition_formats() {
1387 let formats = supported_mime_types();
1388 assert_eq!(formats.len(), 71);
1389 assert!(formats.windows(2).all(|pair| pair[0] < pair[1]));
1390 assert!(formats.contains(&"video/mp4"));
1391 assert!(formats.contains(&"image/jpeg"));
1392 assert!(formats.contains(&"text/tab-separated-values"));
1393 assert!(formats.contains(&"application/vnd.oasis.opendocument.graphics"));
1394 }
1395
1396 #[test]
1397 fn input_alias_is_reported_as_canonical_mime() {
1398 let asset = b"\x00\x00\x00\x10ftypisom\x00\x00\x00\x00";
1399 let report = verify(asset, "audio/aac; codecs=mp4a.40.2").unwrap();
1400 assert_eq!(report.mime_type, "audio/mp4");
1401 assert!(!report.present);
1402 }
1403
1404 #[test]
1405 fn strict_conformance_option_selects_the_program_profile() {
1406 let asset = include_bytes!("../../../tests/fixtures/signed_test.jpg");
1407 let report = verify_with_options(
1408 asset,
1409 "image/jpeg",
1410 &VerifyOptions {
1411 strict_conformance: true,
1412 ..VerifyOptions::default()
1413 },
1414 )
1415 .unwrap();
1416 assert_eq!(
1417 report.manifest_report["engine_profile"]["operating_mode"],
1418 "conformance"
1419 );
1420 assert_eq!(
1421 report.manifest_report["engine_profile"]["compliance_level"],
1422 "conformance-program"
1423 );
1424 }
1425
1426 #[test]
1427 fn fragmented_entry_point_rejects_non_bmff_mime() {
1428 let error = verify_fragmented(
1429 include_bytes!("../../../tests/fixtures/signed_test.jpg"),
1430 &[b"ignored fragment"],
1431 "image/jpeg",
1432 )
1433 .unwrap_err();
1434 assert!(matches!(error, Error::UnsupportedMime(mime) if mime == "image/jpeg"));
1435 }
1436
1437 #[test]
1438 fn unratified_hostless_store_is_not_in_public_profile() {
1439 let error = verify(b"jumb", "application/c2pa").unwrap_err();
1440 assert!(matches!(error, Error::UnsupportedMime(_)));
1441 }
1442
1443 #[test]
1444 fn bounded_reader_accepts_exact_limit_without_large_allocation() {
1445 let mut input = Cursor::new(b"1234");
1446 assert_eq!(read_bounded_file(&mut input, 4, 4).unwrap(), b"1234");
1447 }
1448
1449 #[test]
1450 fn bounded_reader_detects_growth_at_limit_plus_one() {
1451 let mut input = Cursor::new(b"12345");
1452 let error = read_bounded_file(&mut input, 4, 4).unwrap_err();
1453 assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
1454 assert!(error.to_string().contains("grew while being read"));
1455 }
1456
1457 #[test]
1458 fn unsupported_mime_has_stable_error_code() {
1459 let error = verify(b"not an asset", "application/x-unknown").unwrap_err();
1460 assert!(matches!(error, Error::UnsupportedMime(_)));
1461 assert_eq!(error.code(), "unsupported_mime");
1462 }
1463}