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;
67pub use c2pa_validate::external_discovery::{
68 locate_external_resources, locate_external_resources_with_manifest_store,
69 ExternalDidDocumentRequest, ExternalResourceKind, ExternalResourcePlan,
70 ExternalResourceRequest, ExternalResourceStopReason, GraphLimitKind,
71};
72pub use c2pa_validate::ingredient_graph::{GraphInputs, GraphLimits};
73pub use c2pa_validate::refs::{ExternalResource, ExternalResources};
74mod config_dir;
75mod default_trust;
76mod ertmp;
77mod ertmp_wire;
78mod native_online;
79mod online;
80mod online_consent;
81pub use ertmp::{
82 verify_ertmp, verify_ertmp_with_options, ErtmpEvent, ErtmpInput, ErtmpManifestReport,
83 ErtmpMediaType, ErtmpSegmentReport, ErtmpTrack, ErtmpVerificationReport,
84};
85pub use ertmp_wire::{
86 ertmp_input_from_flv, ertmp_input_from_rtmp_connections, ErtmpConnectionContext,
87 ErtmpWireError, ErtmpWireEventContext, ErtmpWireInput, ErtmpWireLimits, FlvWireOptions,
88 RtmpWireConnection,
89};
90mod soft_binding;
91pub use soft_binding::{
92 request_soft_binding_receipt_verification, resolve_soft_binding, RecoveredSoftBindingManifest,
93 SoftBindingAlgorithm, SoftBindingAlgorithms, SoftBindingCapabilities,
94 SoftBindingDisclosureConsent, SoftBindingDiscovery, SoftBindingFetchRequest,
95 SoftBindingHttpMethod, SoftBindingMatch, SoftBindingQuery, SoftBindingReceiptEvidence,
96 SoftBindingReceiptVerificationSession, SoftBindingResolution, SoftBindingResolverOptions,
97 SoftBindingResolverSession,
98};
99mod stream;
100pub use stream::{
101 verify_stream, verify_stream_with_options, SegmentReport, StreamEncapsulation, StreamMethod,
102 StreamVerificationReport,
103};
104#[cfg(feature = "caller-verified-binding")]
105mod caller_verified_binding;
106mod telemetry;
107mod telemetry_consent;
108pub use c2pa_trust::TrustSource;
109#[cfg(feature = "caller-verified-binding")]
110pub use caller_verified_binding::{
111 binding_target, verify_fragmented_with_cawg, verify_manifest_store_only_with_cawg,
112 verify_with_caller_verified_binding, verify_with_caller_verified_binding_and_cawg,
113 verify_with_cawg, verify_with_manifest_store_and_cawg, CallerBindingEvidence,
114 CallerBindingVerdict, CawgEvaluation, CawgEvaluator, CawgProfile, CawgStoreHost,
115 ManifestBindingTarget,
116};
117pub use config_dir::config_directory;
118pub use default_trust::SNAPSHOT_DATE as DEFAULT_TRUST_SNAPSHOT_DATE;
119
120pub use online::{NetworkReport, NetworkRequest};
121pub use online_consent::{
122 allow_interactive_online_consent, online_preference, set_online_preference, OnlinePreference,
123 OnlinePreferenceError,
124};
125pub use telemetry::{
126 validation_failure_telemetry, TelemetryOptions, ValidationFailureTelemetry,
127 DEFAULT_TELEMETRY_ENDPOINT,
128};
129pub use telemetry_consent::{
130 prompt_for_telemetry_consent, set_telemetry_enabled, telemetry_preference,
131 TelemetryPreferenceError,
132};
133
134use std::collections::HashMap;
135use std::fs::{self, OpenOptions};
136use std::io::{self, Read};
137#[cfg(unix)]
138use std::os::unix::fs::OpenOptionsExt;
139use std::path::Path;
140
141use crate::c2pa_core::{
142 spec::{canonicalize_mime, mimes_for_version},
143 ComplianceLevel, EngineProfile, OperatingMode, SpecVersion,
144};
145use crate::c2pa_trust::{AnchorPurpose, CawgTrustSource, TrustList};
146use crate::c2pa_validate::{
147 verify_fragmented_with_cawg_options_and_expected_seeks_safe as verify_fragmented_safe,
148 verify_with_cawg_options_and_graph_inputs_safe as verify_graph_safe,
149 verify_with_cawg_options_safe as verify_safe, StatusCode as CoreStatus,
150 ValidationResults as CoreResults, VerifyInput, ASSERTION_BMFF_HASH_MALFORMED,
151 ASSERTION_BMFF_HASH_MATCH, ASSERTION_BMFF_HASH_MISMATCH, ASSERTION_BOXES_HASH_MALFORMED,
152 ASSERTION_BOXES_HASH_MATCH, ASSERTION_BOXES_HASH_MISMATCH, ASSERTION_COLLECTION_HASH_MALFORMED,
153 ASSERTION_COLLECTION_HASH_MATCH, ASSERTION_COLLECTION_HASH_MISMATCH, ASSERTION_DATA_HASH_MATCH,
154 ASSERTION_DATA_HASH_MISMATCH, ASSERTION_MULTI_ASSET_HASH_MALFORMED,
155 ASSERTION_MULTI_ASSET_HASH_MATCH, ASSERTION_MULTI_ASSET_HASH_MISMATCH,
156 CLAIM_HARD_BINDINGS_MISSING, CLAIM_SIGNATURE_MISMATCH, CLAIM_SIGNATURE_MISSING,
157 CLAIM_SIGNATURE_VALIDATED, SIGNING_CREDENTIAL_INVALID, SIGNING_CREDENTIAL_OCSP_NOT_REVOKED,
158 SIGNING_CREDENTIAL_OCSP_REVOKED, SIGNING_CREDENTIAL_TRUSTED, SIGNING_CREDENTIAL_UNTRUSTED,
159};
160use serde::{Deserialize, Serialize};
161use serde_json::Value;
162use sha2::{Digest, Sha256};
163use time::{format_description::well_known::Rfc3339, OffsetDateTime};
164
165pub const REPORT_SCHEMA_VERSION: &str = "1.0";
166pub const C2PA_PROFILE_2_2: &str = "c2pa-2.2";
168pub const C2PA_PROFILE: &str = "c2pa-2.4";
170pub const C2PA_PROFILE_2_5: &str = "c2pa-2.5";
172pub const SPEC_VERSIONS: &[&str] = &SpecVersion::PUBLIC_POLICY_STRINGS;
174const MAX_MANIFEST_STORE_BYTES: usize = 64 * 1024 * 1024;
175const MAX_PATH_ASSET_BYTES: u64 = 128 * 1024 * 1024;
176const MAX_OCSP_EVIDENCE_BYTES: usize = 64 * 1024;
178const MAX_EXTERNAL_DATA_EVIDENCE_BYTES: usize = 64 * 1024 * 1024;
180
181#[derive(Debug, Clone, Default, Deserialize)]
182#[serde(default, deny_unknown_fields)]
183pub struct VerifyOptions {
184 pub trust_pem: Option<String>,
186 pub claim_trust_sources: Option<Vec<TrustListSource>>,
189 pub tsa_trust_pem: Option<String>,
191 pub tsa_trust_sources: Option<Vec<TrustListSource>>,
194 pub allowed_list_pem: Option<String>,
196 pub cawg_trust_pem: Option<String>,
200 pub cawg_allowed_certs_pem: Option<String>,
202 pub cawg_document_signing_require_anchor: Option<bool>,
206 pub cawg_trust_configurations: Option<Vec<CawgTrustConfiguration>>,
210 pub trust_anchor_not_before: Option<String>,
215 pub trust_anchor_not_after: Option<String>,
219 pub no_default_trust: bool,
223 pub cawg_did_documents: Option<HashMap<String, Value>>,
226 pub cawg_ica_trusted_issuers: Option<Vec<String>>,
228 pub cawg_ica_trust_anchors: Option<Vec<String>>,
231 pub cawg_ica_status_lists: Option<HashMap<String, String>>,
235 pub ocsp_responses: Option<HashMap<String, String>>,
247 pub ocsp_unreachable: Option<Vec<String>>,
255 pub external_data: Option<HashMap<String, String>>,
261 pub external_data_media_types: Option<HashMap<String, String>>,
265 pub external_data_unreachable: Option<Vec<String>>,
269 pub expected_seek_positions: Vec<usize>,
273 pub spec_version: Option<String>,
294 pub strict_conformance: bool,
303 pub cawg_strict_encoding: bool,
309 pub validation_time: Option<String>,
311 pub stream_presentation_times: Option<Vec<String>>,
337 pub telemetry: TelemetryOptions,
339 pub online: Option<bool>,
350 pub online_allow_private_networks: bool,
361}
362
363#[derive(Debug, Default)]
369pub struct ValidatedExternalData {
370 pub data: HashMap<String, Vec<u8>>,
372 pub media_types: HashMap<String, String>,
374 pub unreachable: Vec<String>,
376}
377
378impl VerifyOptions {
379 pub fn validate(&self) -> Result<(), Error> {
386 self.resolve_and_validate()?;
387 Ok(())
388 }
389
390 pub fn validate_with_external_data(&self) -> Result<ValidatedExternalData, Error> {
394 let resolved = self.resolve_and_validate()?;
395 Ok(ValidatedExternalData {
396 data: resolved.external_data,
397 media_types: resolved.external_data_media_types,
398 unreachable: resolved.external_data_unreachable,
399 })
400 }
401
402 fn resolve_and_validate(&self) -> Result<ResolvedOptions, Error> {
403 let resolved = ResolvedOptions::resolve(self)?;
404 for (index, raw) in self.stream_presentation_times.iter().flatten().enumerate() {
405 parse_stream_presentation_time(raw, index)?;
406 }
407 Ok(resolved)
408 }
409}
410
411#[derive(Debug, Clone, Deserialize)]
417#[serde(deny_unknown_fields)]
418pub struct TrustListSource {
419 pub uri: String,
421 #[serde(rename = "type")]
423 pub source_type: String,
424 #[serde(default)]
426 pub identifier: Option<String>,
427 pub certificates_pem: String,
429 #[serde(default)]
431 pub not_before: Option<String>,
432 #[serde(default)]
434 pub not_after: Option<String>,
435}
436
437#[derive(Debug, Clone, Deserialize)]
444#[serde(deny_unknown_fields)]
445pub struct CawgTrustConfiguration {
446 pub profile: CawgTrustProfile,
448 pub certificates_pem: String,
451 #[serde(default)]
454 pub not_before: Option<String>,
455 #[serde(default)]
458 pub not_after: Option<String>,
459}
460
461#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
463#[serde(rename_all = "snake_case")]
464pub enum CawgTrustProfile {
465 Base,
473 SmimeInterim,
481}
482
483#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
484pub struct VerificationStatus {
485 pub code: String,
486 #[serde(default)]
487 pub url: String,
488 #[serde(default, rename = "resourceUrl")]
490 pub resource_url: Option<String>,
491 pub explanation: String,
492 #[serde(default, skip_serializing_if = "Option::is_none")]
495 pub details: Option<Value>,
496}
497
498impl Serialize for VerificationStatus {
499 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
500 crate::c2pa_validate::serialize_status(
501 serializer,
502 &self.code,
503 &self.url,
504 self.resource_url.as_deref(),
505 &self.explanation,
506 self.details.as_ref(),
507 )
508 }
509}
510
511#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
512pub struct ValidationResults {
513 pub success: Vec<VerificationStatus>,
514 pub informational: Vec<VerificationStatus>,
515 pub failure: Vec<VerificationStatus>,
516 #[serde(default, skip_serializing_if = "Vec::is_empty")]
518 pub trust_list_sources: Vec<TrustSource>,
519}
520
521#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
522pub struct RevocationReport {
523 pub status: String,
524 pub source: String,
525 pub responder_signature: String,
526}
527
528#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
529pub struct FreshnessReport {
530 pub status: String,
531 pub as_of: Option<String>,
532}
533
534#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
535pub struct TrustReport {
536 pub status: String,
537 pub basis: String,
538 pub validation_time: String,
539 pub revocation: RevocationReport,
540 pub freshness: FreshnessReport,
541}
542
543#[derive(Debug, Clone, Serialize, Deserialize)]
544pub struct VerificationReport {
545 pub schema_version: String,
546 pub profile: String,
547 pub mime_type: String,
548 pub present: bool,
549 pub integrity: String,
550 pub signature: String,
551 pub hard_binding: String,
552 pub trust: TrustReport,
553 pub policy: Option<Value>,
554 pub managed_receipt: Option<Value>,
555 pub validation_state: String,
556 pub validation_results: ValidationResults,
557 pub manifest_report: Value,
558 pub content_credentials: Option<Value>,
559 #[serde(default)]
561 pub network: NetworkReport,
562}
563#[derive(Debug, Clone, PartialEq, Eq)]
570pub struct DetachedManifestEvidence {
571 pub manifest_store: Vec<u8>,
572 pub manifest_store_sha256: String,
573 pub carrier: Vec<u8>,
574}
575
576#[derive(Debug, Clone, Serialize)]
583pub struct LocalEvidence {
584 pub report: VerificationReport,
586 #[serde(serialize_with = "serialize_optional_bytes")]
590 pub manifest_store: Option<Vec<u8>>,
591 pub manifest_store_sha256: Option<String>,
593 pub asset_sha256: String,
595 pub hard_binding: LocalHardBinding,
597 pub active_manifest_label: Option<String>,
599 pub store_manifests: Vec<ManifestDigest>,
603}
604
605#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
608pub struct ManifestDigest {
609 pub label: String,
611 pub sha256: String,
613}
614
615fn manifest_identity(manifest_store: Option<&[u8]>) -> (Option<String>, Vec<ManifestDigest>) {
618 let Some(store) = manifest_store else {
619 return (None, Vec::new());
620 };
621 match std::panic::catch_unwind(|| crate::c2pa_validate::store_manifest_identity(store)) {
622 Ok(Ok((active, digests))) => (
623 active,
624 digests
625 .into_iter()
626 .map(|digest| ManifestDigest {
627 label: digest.label,
628 sha256: hex::encode(digest.sha256),
629 })
630 .collect(),
631 ),
632 _ => (None, Vec::new()),
633 }
634}
635
636#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
638pub struct LocalHardBinding {
639 pub algorithm: Option<String>,
644 pub status: String,
646}
647
648#[derive(Debug, Clone, Serialize)]
658pub struct StoreOnlyValidation {
659 pub report: VerificationReport,
661 pub active_manifest_label: Option<String>,
663 pub store_manifests: Vec<ManifestDigest>,
665}
666
667fn serialize_optional_bytes<S: serde::Serializer>(
670 value: &Option<Vec<u8>>,
671 serializer: S,
672) -> Result<S::Ok, S::Error> {
673 match value {
674 Some(bytes) => serializer.serialize_bytes(bytes),
675 None => serializer.serialize_none(),
676 }
677}
678
679#[derive(Default)]
681struct PassCapture {
682 manifest_store: Option<Vec<u8>>,
683 hard_binding_label: Option<String>,
684}
685
686impl PassCapture {
687 fn take(output: &mut crate::c2pa_validate::VerifyOutput) -> Self {
688 Self {
689 manifest_store: output.manifest_store.take(),
690 hard_binding_label: output.hard_binding_label.take(),
691 }
692 }
693}
694
695impl VerificationReport {
696 pub fn to_json(&self) -> Result<String, Error> {
697 serde_json::to_string(self).map_err(Error::Serialize)
698 }
699
700 pub fn to_pretty_json(&self) -> Result<String, Error> {
701 serde_json::to_string_pretty(self).map_err(Error::Serialize)
702 }
703
704 pub fn cawg_statuses(&self) -> Vec<&VerificationStatus> {
708 self.validation_results
709 .success
710 .iter()
711 .chain(&self.validation_results.informational)
712 .chain(&self.validation_results.failure)
713 .filter(|status| status.code.starts_with("cawg."))
714 .collect()
715 }
716}
717
718#[derive(Debug, thiserror::Error)]
719pub enum Error {
720 #[error("unsupported MIME type: {0}")]
721 UnsupportedMime(String),
722 #[error("invalid trust material: {0}")]
723 InvalidTrust(String),
724 #[error("invalid validation time: {0}")]
725 InvalidValidationTime(String),
726 #[error("unsupported spec version: {0}")]
727 UnsupportedSpecVersion(String),
728 #[error("verification failed: {0}")]
729 Verification(String),
730 #[error("could not read asset: {0}")]
731 Io(#[from] std::io::Error),
732 #[error(transparent)]
733 TelemetryPreference(#[from] TelemetryPreferenceError),
734 #[error(transparent)]
735 OnlinePreference(#[from] OnlinePreferenceError),
736 #[error("could not serialize report: {0}")]
737 Serialize(serde_json::Error),
738}
739
740impl Error {
741 pub fn code(&self) -> &'static str {
742 match self {
743 Self::UnsupportedMime(_) => "unsupported_mime",
744 Self::InvalidTrust(_) => "invalid_trust_material",
745 Self::InvalidValidationTime(_) => "invalid_validation_time",
746 Self::UnsupportedSpecVersion(_) => "unsupported_spec_version",
747 Self::Verification(_) => "verification_error",
748 Self::Io(_) => "io_error",
749 Self::OnlinePreference(_) => "online_preference_error",
750 Self::TelemetryPreference(_) => "telemetry_preference_error",
751 Self::Serialize(_) => "serialization_error",
752 }
753 }
754}
755
756pub fn verify(data: &[u8], mime_type: &str) -> Result<VerificationReport, Error> {
761 verify_with_options(data, mime_type, &VerifyOptions::default())
762}
763
764pub fn verify_fragmented(
770 init_segment: &[u8],
771 fragments: &[&[u8]],
772 mime_type: &str,
773) -> Result<VerificationReport, Error> {
774 verify_fragmented_with_options(
775 init_segment,
776 fragments,
777 mime_type,
778 &VerifyOptions::default(),
779 )
780}
781pub fn detached_manifest_evidence(
788 data: &[u8],
789 mime_type: &str,
790) -> Result<Option<DetachedManifestEvidence>, Error> {
791 detached_manifest_evidence_with_options(data, mime_type, &VerifyOptions::default())
792}
793
794pub fn detached_manifest_evidence_with_options(
801 data: &[u8],
802 mime_type: &str,
803 options: &VerifyOptions,
804) -> Result<Option<DetachedManifestEvidence>, Error> {
805 let profile = resolve_profile(options.spec_version.as_deref(), options.strict_conformance)?;
806 let mime = canonicalize_mime(mime_type);
807 require_embedded_mime(&mime, profile)?;
808 let format = crate::c2pa_formats::AssetFormat::from_canonical_mime(&mime)
809 .ok_or_else(|| Error::UnsupportedMime(mime.clone()))?;
810 if !crate::c2pa_formats::supports_hash_mode(&mime) {
811 return Ok(None);
812 }
813 let Some(manifest_store) =
814 crate::c2pa_formats::extract_manifest_for_spec(format, data, profile.version)
815 .map_err(|error| Error::Verification(error.to_string()))?
816 else {
817 return Ok(None);
818 };
819 let spans =
820 crate::c2pa_formats::compute_data_hash_exclusions_for_spec(format, data, profile.version)
821 .map_err(|error| Error::Verification(error.to_string()))?;
822 let [span] = spans.as_slice() else {
823 return Ok(None);
824 };
825 let end = span
826 .start
827 .checked_add(span.length)
828 .filter(|end| *end <= data.len())
829 .ok_or_else(|| Error::Verification("manifest carrier exceeds asset bounds".into()))?;
830 let manifest_store_sha256 = hex::encode(Sha256::digest(&manifest_store));
831 Ok(Some(DetachedManifestEvidence {
832 manifest_store,
833 manifest_store_sha256,
834 carrier: data[span.start..end].to_vec(),
835 }))
836}
837
838pub fn verify_with_manifest_store(
857 asset: &[u8],
858 manifest_store: &[u8],
859 mime_type: &str,
860 options: &VerifyOptions,
861) -> Result<VerificationReport, Error> {
862 let telemetry_enabled = telemetry_consent::resolve_telemetry_enabled(options.telemetry.enabled);
863 let result = verify_with_manifest_store_inner(asset, manifest_store, mime_type, options);
864 enqueue_failure_telemetry(mime_type, &result, options, telemetry_enabled);
865 result
866}
867
868pub fn verify_with_manifest_store_and_graph_inputs(
873 asset: &[u8],
874 manifest_store: &[u8],
875 mime_type: &str,
876 options: &VerifyOptions,
877 graph_inputs: GraphInputs<'_>,
878) -> Result<VerificationReport, Error> {
879 let telemetry_enabled = telemetry_consent::resolve_telemetry_enabled(options.telemetry.enabled);
880 let result = (|| {
881 let profile = resolve_profile(options.spec_version.as_deref(), options.strict_conformance)?;
882 let mime = resolve_detached_mime(mime_type, profile)?;
883 detached_pass(asset, manifest_store, &mime, options, graph_inputs)
884 .map(|(report, _, _)| report)
885 })();
886 enqueue_failure_telemetry(mime_type, &result, options, telemetry_enabled);
887 result
888}
889
890fn verify_with_manifest_store_inner(
891 asset: &[u8],
892 manifest_store: &[u8],
893 mime_type: &str,
894 options: &VerifyOptions,
895) -> Result<VerificationReport, Error> {
896 let profile = resolve_profile(options.spec_version.as_deref(), options.strict_conformance)?;
897 let mime = resolve_detached_mime(mime_type, profile)?;
898 if profile.version == SpecVersion::V2_5 {
899 let resolved = ResolvedOptions::resolve(options)?;
900 return verify_detached_online_v25(asset, manifest_store, &mime, options, resolved);
901 }
902 let (mut report, needs, _) = detached_pass(
903 asset,
904 manifest_store,
905 &mime,
906 options,
907 GraphInputs::default(),
908 )?;
909 let (network, evidence) = online::gather(options, &needs);
910 if evidence.is_empty() {
911 report.network = network;
912 return Ok(report);
913 }
914 let next = online::apply_evidence(options, &evidence);
917 let (mut report, _, _) =
918 detached_pass(asset, manifest_store, &mime, &next, GraphInputs::default())?;
919 report.network = network;
920 Ok(report)
921}
922
923fn detached_pass(
926 asset: &[u8],
927 manifest_store: &[u8],
928 mime: &str,
929 options: &VerifyOptions,
930 graph_inputs: GraphInputs<'_>,
931) -> Result<(VerificationReport, Vec<online::NetworkNeed>, PassCapture), Error> {
932 let resolved = ResolvedOptions::resolve(options)?;
933 detached_pass_resolved(
934 asset,
935 manifest_store,
936 mime,
937 options,
938 &resolved,
939 options.cawg_did_documents.as_ref(),
940 graph_inputs,
941 )
942}
943
944fn detached_pass_resolved(
945 asset: &[u8],
946 manifest_store: &[u8],
947 mime: &str,
948 options: &VerifyOptions,
949 resolved: &ResolvedOptions,
950 did_documents: Option<&HashMap<String, Value>>,
951 graph_inputs: GraphInputs<'_>,
952) -> Result<(VerificationReport, Vec<online::NetworkNeed>, PassCapture), Error> {
953 if manifest_store.is_empty() {
954 return Err(Error::Verification("manifest store is empty".into()));
955 }
956 if manifest_store.len() > MAX_MANIFEST_STORE_BYTES {
957 return Err(Error::Verification(format!(
958 "manifest store exceeds the {MAX_MANIFEST_STORE_BYTES} byte limit"
959 )));
960 }
961 let input = resolved.input(asset, mime);
962 let mut output = crate::c2pa_validate::verify_detached_safe(
963 manifest_store,
964 asset,
965 mime,
966 &input,
967 resolved.cawg_trust(),
968 resolved.cawg_allowed_certs(),
969 resolved.cawg_document_signing_require_anchor,
970 did_documents,
971 options.cawg_ica_trusted_issuers.as_deref(),
972 options.cawg_ica_trust_anchors.as_deref(),
973 options.cawg_ica_status_lists.as_ref(),
974 graph_inputs,
975 )
976 .map_err(map_validate_error)?;
977 let needs = online::network_needs(&output);
978 let capture = PassCapture::take(&mut output);
979 Ok((
980 report_from_output(output, mime.to_string(), resolved),
981 needs,
982 capture,
983 ))
984}
985
986pub fn verify_manifest_store_only(
1000 manifest_store: &[u8],
1001 options: &VerifyOptions,
1002) -> Result<StoreOnlyValidation, Error> {
1003 verify_manifest_store_only_with_graph_inputs(manifest_store, options, GraphInputs::default())
1004}
1005
1006pub fn verify_manifest_store_only_with_graph_inputs(
1010 manifest_store: &[u8],
1011 options: &VerifyOptions,
1012 graph_inputs: GraphInputs<'_>,
1013) -> Result<StoreOnlyValidation, Error> {
1014 if options.online == Some(true) {
1015 return Err(Error::Verification(
1016 "online must not be Some(true): store-only validation never fetches".into(),
1017 ));
1018 }
1019 let resolved = ResolvedOptions::resolve(options)?;
1020 if manifest_store.is_empty() {
1021 return Err(Error::Verification("manifest store is empty".into()));
1022 }
1023 if manifest_store.len() > MAX_MANIFEST_STORE_BYTES {
1024 return Err(Error::Verification(format!(
1025 "manifest store exceeds the {MAX_MANIFEST_STORE_BYTES} byte limit"
1026 )));
1027 }
1028 let input = resolved.input(&[], "application/c2pa");
1029 let cawg_inputs = crate::c2pa_validate::CawgTrustInputs::new(
1030 resolved.cawg_trust(),
1031 resolved.cawg_allowed_certs(),
1032 resolved.cawg_document_signing_require_anchor,
1033 options.cawg_did_documents.as_ref(),
1034 options.cawg_ica_trusted_issuers.as_deref(),
1035 options.cawg_ica_trust_anchors.as_deref(),
1036 options.cawg_ica_status_lists.as_ref(),
1037 graph_inputs,
1038 );
1039 let output = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1040 crate::c2pa_validate::verify_store_binding(
1041 manifest_store,
1042 crate::c2pa_validate::StoreBinding::StoreOnly,
1043 &input,
1044 cawg_inputs,
1045 )
1046 }))
1047 .map_err(|_| Error::Verification("verification panicked".into()))?
1048 .map_err(map_validate_error)?;
1049 Ok(finish_store_only_validation(
1050 manifest_store,
1051 output,
1052 &resolved,
1053 ))
1054}
1055
1056fn finish_store_only_validation(
1057 manifest_store: &[u8],
1058 output: crate::c2pa_validate::VerifyOutput,
1059 resolved: &ResolvedOptions,
1060) -> StoreOnlyValidation {
1061 let mut report = report_from_output(output, "application/c2pa".to_string(), resolved);
1062 report.hard_binding = "not_evaluated".to_string();
1063 if report.integrity == "valid" {
1064 report.integrity = "unbound".to_string();
1065 if let Some(verdict) = report
1066 .manifest_report
1067 .pointer_mut("/provenance_verdict")
1068 .and_then(Value::as_object_mut)
1069 {
1070 verdict.insert("integrity".to_string(), Value::from("unbound"));
1071 }
1072 }
1073 let (active_manifest_label, store_manifests) = manifest_identity(Some(manifest_store));
1074 StoreOnlyValidation {
1075 report,
1076 active_manifest_label,
1077 store_manifests,
1078 }
1079}
1080
1081pub fn verify_with_graph_inputs(
1085 data: &[u8],
1086 mime_type: &str,
1087 options: &VerifyOptions,
1088 graph_inputs: GraphInputs<'_>,
1089) -> Result<VerificationReport, Error> {
1090 let telemetry_enabled = telemetry_consent::resolve_telemetry_enabled(options.telemetry.enabled);
1091 let result = (|| {
1092 let resolved = ResolvedOptions::resolve(options)?;
1093 embedded_graph_pass(data, None, mime_type, options, &resolved, graph_inputs)
1094 })();
1095 enqueue_failure_telemetry(mime_type, &result, options, telemetry_enabled);
1096 result
1097}
1098
1099pub fn verify_with_options(
1103 data: &[u8],
1104 mime_type: &str,
1105 options: &VerifyOptions,
1106) -> Result<VerificationReport, Error> {
1107 let resolved = ResolvedOptions::resolve(options)?;
1108 verify_with_resolved_options(data, None, mime_type, options, resolved)
1109}
1110
1111pub fn local_evidence_with_options(
1120 data: &[u8],
1121 mime_type: &str,
1122 options: &VerifyOptions,
1123) -> Result<LocalEvidence, Error> {
1124 let resolved = ResolvedOptions::resolve(options)?;
1125 let telemetry_enabled = telemetry_consent::resolve_telemetry_enabled(options.telemetry.enabled);
1126 let (result, capture) =
1127 match verify_with_options_inner(data, None, mime_type, options, resolved) {
1128 Ok((report, capture)) => (Ok(report), capture),
1129 Err(error) => (Err(error), PassCapture::default()),
1130 };
1131 enqueue_failure_telemetry(mime_type, &result, options, telemetry_enabled);
1132 let report = result?;
1133 let status = match report.hard_binding.as_str() {
1134 "match" => "match",
1135 "mismatch" => "mismatch",
1136 _ => "unknown",
1137 };
1138 let (active_manifest_label, store_manifests) =
1139 manifest_identity(capture.manifest_store.as_deref());
1140 Ok(LocalEvidence {
1141 manifest_store_sha256: capture
1142 .manifest_store
1143 .as_deref()
1144 .map(|store| hex::encode(Sha256::digest(store))),
1145 manifest_store: capture.manifest_store,
1146 asset_sha256: hex::encode(Sha256::digest(data)),
1147 hard_binding: LocalHardBinding {
1148 algorithm: capture.hard_binding_label,
1149 status: status.to_string(),
1150 },
1151 active_manifest_label,
1152 store_manifests,
1153 report,
1154 })
1155}
1156
1157fn verify_with_resolved_options(
1158 data: &[u8],
1159 fragments: Option<&[&[u8]]>,
1160 mime_type: &str,
1161 options: &VerifyOptions,
1162 resolved: ResolvedOptions,
1163) -> Result<VerificationReport, Error> {
1164 let telemetry_enabled = telemetry_consent::resolve_telemetry_enabled(options.telemetry.enabled);
1165 let result = verify_with_options_inner(data, fragments, mime_type, options, resolved)
1166 .map(|(report, _)| report);
1167 enqueue_failure_telemetry(mime_type, &result, options, telemetry_enabled);
1168 result
1169}
1170
1171fn enqueue_failure_telemetry(
1172 mime_type: &str,
1173 result: &Result<VerificationReport, Error>,
1174 options: &VerifyOptions,
1175 enabled: bool,
1176) {
1177 if let Some(event) = telemetry::validation_failure_telemetry_with_enabled(
1178 mime_type,
1179 result,
1180 &options.telemetry,
1181 enabled,
1182 ) {
1183 telemetry::enqueue(options.telemetry.endpoint(), event);
1184 }
1185}
1186
1187pub fn verify_fragmented_with_options(
1195 init_segment: &[u8],
1196 fragments: &[&[u8]],
1197 mime_type: &str,
1198 options: &VerifyOptions,
1199) -> Result<VerificationReport, Error> {
1200 if options.stream_presentation_times.is_some() {
1201 return Err(Error::InvalidValidationTime(
1202 "stream_presentation_times: per-segment presentation times apply only to \
1203 session-key live streams verified with verify_stream_with_options; \
1204 Merkle-bound fragments are assessed at validation_time"
1205 .into(),
1206 ));
1207 }
1208 let resolved = ResolvedOptions::resolve(options)?;
1209 let telemetry_enabled = telemetry_consent::resolve_telemetry_enabled(options.telemetry.enabled);
1210 let mime = canonicalize_mime(mime_type);
1211 let result = if crate::c2pa_formats::AssetFormat::from_canonical_mime(&mime)
1212 == Some(crate::c2pa_formats::AssetFormat::Bmff)
1213 {
1214 verify_with_options_inner(init_segment, Some(fragments), &mime, options, resolved)
1215 .map(|(report, _)| report)
1216 } else {
1217 Err(Error::UnsupportedMime(mime))
1218 };
1219 enqueue_failure_telemetry(mime_type, &result, options, telemetry_enabled);
1220 result
1221}
1222
1223pub fn verify_fragmented_with_graph_inputs(
1227 init_segment: &[u8],
1228 fragments: &[&[u8]],
1229 mime_type: &str,
1230 options: &VerifyOptions,
1231 graph_inputs: GraphInputs<'_>,
1232) -> Result<VerificationReport, Error> {
1233 if options.stream_presentation_times.is_some() {
1234 return Err(Error::InvalidValidationTime(
1235 "stream_presentation_times: per-segment presentation times apply only to \
1236 session-key live streams verified with verify_stream_with_options; \
1237 Merkle-bound fragments are assessed at validation_time"
1238 .into(),
1239 ));
1240 }
1241 let resolved = ResolvedOptions::resolve(options)?;
1242 let telemetry_enabled = telemetry_consent::resolve_telemetry_enabled(options.telemetry.enabled);
1243 let mime = canonicalize_mime(mime_type);
1244 let result = if crate::c2pa_formats::AssetFormat::from_canonical_mime(&mime)
1245 == Some(crate::c2pa_formats::AssetFormat::Bmff)
1246 {
1247 embedded_graph_pass(
1248 init_segment,
1249 Some(fragments),
1250 &mime,
1251 options,
1252 &resolved,
1253 graph_inputs,
1254 )
1255 } else {
1256 Err(Error::UnsupportedMime(mime))
1257 };
1258 enqueue_failure_telemetry(mime_type, &result, options, telemetry_enabled);
1259 result
1260}
1261
1262pub(crate) struct ResolvedOptions {
1269 claim_trust: Option<ResolvedTrust>,
1270 tsa_trust: Option<ResolvedTrust>,
1271 allowed_certs: Option<ResolvedTrust>,
1272 cawg_trust: Option<ResolvedTrust>,
1273 cawg_allowed_certs: Option<ResolvedTrust>,
1274 validation_time: OffsetDateTime,
1275 validation_time_text: String,
1276 trust_basis: &'static str,
1277 profile: EngineProfile,
1278 cawg_strict_encoding: bool,
1279 cawg_document_signing_require_anchor: bool,
1280 ocsp_responses: HashMap<String, Vec<u8>>,
1282 ocsp_unreachable: Vec<String>,
1283 external_data: HashMap<String, Vec<u8>>,
1284 external_data_media_types: HashMap<String, String>,
1285 external_data_unreachable: Vec<String>,
1286}
1287
1288impl ResolvedOptions {
1289 pub(crate) fn resolve(options: &VerifyOptions) -> Result<Self, Error> {
1290 let profile = resolve_profile(options.spec_version.as_deref(), options.strict_conformance)?;
1293 let use_defaults = !options.no_default_trust;
1294 let validation_time = parse_validation_time(options.validation_time.as_deref())?;
1295 let custom_claim_trust = options.trust_pem.is_some()
1296 || options
1297 .claim_trust_sources
1298 .as_ref()
1299 .is_some_and(|sources| !sources.is_empty())
1300 || options.allowed_list_pem.is_some();
1301 let bounds = (
1302 parse_optional_instant(
1303 options.trust_anchor_not_before.as_deref(),
1304 "trust_anchor_not_before",
1305 )?,
1306 parse_optional_instant(
1307 options.trust_anchor_not_after.as_deref(),
1308 "trust_anchor_not_after",
1309 )?,
1310 );
1311 let (cawg_anchors, cawg_direct) =
1312 resolve_cawg_configurations(options.cawg_trust_configurations.as_deref())?;
1313 Ok(Self {
1314 claim_trust: resolve_trust(
1315 options.trust_pem.as_deref(),
1316 options.claim_trust_sources.as_deref(),
1317 use_defaults.then(default_trust::claim_signing),
1318 AnchorPurpose::ClaimSigning,
1319 bounds,
1320 )?,
1321 tsa_trust: resolve_trust(
1322 options.tsa_trust_pem.as_deref(),
1323 options.tsa_trust_sources.as_deref(),
1324 use_defaults.then(default_trust::timestamp_authorities),
1325 AnchorPurpose::TimeStamping,
1326 bounds,
1327 )?,
1328 allowed_certs: resolve_trust(
1329 options.allowed_list_pem.as_deref(),
1330 None,
1331 use_defaults.then(default_trust::allowed_claim_signers),
1332 AnchorPurpose::ClaimSigning,
1333 bounds,
1334 )?,
1335 cawg_trust: extend_trust(
1336 resolve_trust(
1337 options.cawg_trust_pem.as_deref(),
1338 None,
1339 use_defaults.then(default_trust::cawg_identity),
1340 AnchorPurpose::CawgIdentity,
1341 bounds,
1342 )?,
1343 cawg_anchors,
1344 ),
1345 cawg_allowed_certs: extend_trust(
1346 resolve_trust(
1347 options.cawg_allowed_certs_pem.as_deref(),
1348 None,
1349 use_defaults.then(default_trust::cawg_allowed_identities),
1350 AnchorPurpose::CawgIdentity,
1351 bounds,
1352 )?,
1353 cawg_direct,
1354 ),
1355 validation_time,
1356 validation_time_text: validation_time
1357 .format(&Rfc3339)
1358 .map_err(|error| Error::InvalidValidationTime(error.to_string()))?,
1359 trust_basis: match (use_defaults, custom_claim_trust) {
1360 (true, true) => "bundled_and_caller_supplied_static_material",
1361 (true, false) => "bundled_static_material",
1362 (false, true) => "caller_supplied_static_material",
1363 (false, false) => "none",
1364 },
1365 profile,
1366 cawg_strict_encoding: options.cawg_strict_encoding,
1367 cawg_document_signing_require_anchor: options
1368 .cawg_document_signing_require_anchor
1369 .unwrap_or(true),
1370 ocsp_responses: decode_base64_map(
1371 options.ocsp_responses.as_ref(),
1372 "ocsp_responses",
1373 MAX_OCSP_EVIDENCE_BYTES,
1374 )?,
1375 ocsp_unreachable: options.ocsp_unreachable.clone().unwrap_or_default(),
1376 external_data: decode_base64_map(
1377 options.external_data.as_ref(),
1378 "external_data",
1379 MAX_EXTERNAL_DATA_EVIDENCE_BYTES,
1380 )?,
1381 external_data_media_types: options
1382 .external_data_media_types
1383 .clone()
1384 .unwrap_or_default(),
1385 external_data_unreachable: options
1386 .external_data_unreachable
1387 .clone()
1388 .unwrap_or_default(),
1389 })
1390 }
1391
1392 fn take_external_data(&mut self) -> ValidatedExternalData {
1393 ValidatedExternalData {
1394 data: std::mem::take(&mut self.external_data),
1395 media_types: std::mem::take(&mut self.external_data_media_types),
1396 unreachable: std::mem::take(&mut self.external_data_unreachable),
1397 }
1398 }
1399
1400 fn apply_ocsp_update(&mut self, update: native_online::OcspUpdate) {
1401 for (certificate, response) in update.responses {
1402 self.ocsp_responses.entry(certificate).or_insert(response);
1403 }
1404 for certificate in update.unreachable {
1405 if !self.ocsp_unreachable.contains(&certificate) {
1406 self.ocsp_unreachable.push(certificate);
1407 }
1408 }
1409 }
1410
1411 pub(crate) fn input<'a>(&'a self, data: &'a [u8], mime: &'a str) -> VerifyInput<'a> {
1412 VerifyInput {
1413 data,
1414 mime,
1415 claim_signer_trust: self.claim_trust.as_ref().map(ResolvedTrust::get),
1416 tsa_trust: self.tsa_trust.as_ref().map(ResolvedTrust::get),
1417 allowed_certs: self.allowed_certs.as_ref().map(ResolvedTrust::get),
1418 validation_time: Some(self.validation_time),
1419 profile: self.profile,
1420 cawg_strict_encoding: self.cawg_strict_encoding,
1421 evidence: crate::c2pa_validate::OnlineEvidence {
1422 ocsp_responses: (!self.ocsp_responses.is_empty()).then_some(&self.ocsp_responses),
1423 ocsp_unreachable: (!self.ocsp_unreachable.is_empty())
1424 .then_some(self.ocsp_unreachable.as_slice()),
1425 external_data: (!self.external_data.is_empty()).then_some(&self.external_data),
1426 external_data_media_types: (!self.external_data_media_types.is_empty())
1427 .then_some(&self.external_data_media_types),
1428 external_data_unreachable: (!self.external_data_unreachable.is_empty())
1429 .then_some(self.external_data_unreachable.as_slice()),
1430 },
1431 }
1432 }
1433
1434 pub(crate) fn cawg_trust(&self) -> Option<&TrustList> {
1435 self.cawg_trust.as_ref().map(ResolvedTrust::get)
1436 }
1437
1438 pub(crate) fn cawg_allowed_certs(&self) -> Option<&TrustList> {
1439 self.cawg_allowed_certs.as_ref().map(ResolvedTrust::get)
1440 }
1441}
1442
1443fn resolve_profile(spec_version: Option<&str>, strict: bool) -> Result<EngineProfile, Error> {
1451 let version = match spec_version {
1452 None => SpecVersion::V2_4,
1453 Some(value) => SpecVersion::public_policy_from_str(value).ok_or_else(|| {
1454 Error::UnsupportedSpecVersion(format!(
1455 "spec_version {value:?} is not one of {}",
1456 SpecVersion::PUBLIC_POLICIES
1457 .iter()
1458 .map(|version| format!("{:?}", version.version_str()))
1459 .collect::<Vec<_>>()
1460 .join(", ")
1461 ))
1462 })?,
1463 };
1464 match (version, strict) {
1465 (SpecVersion::V2_2, true) => Ok(EngineProfile::strict(SpecVersion::V2_2)),
1466 (SpecVersion::V2_2, false) => Ok(EngineProfile {
1467 version,
1468 ..EngineProfile::GENEROUS
1469 }),
1470 (SpecVersion::V2_4, true) => Ok(EngineProfile::strict(SpecVersion::V2_4)),
1471 (SpecVersion::V2_4, false) => Ok(EngineProfile::GENEROUS),
1472 (SpecVersion::V2_5, true) => Ok(EngineProfile::new(
1473 SpecVersion::V2_5,
1474 OperatingMode::Conformance,
1475 )
1476 .with_compliance(ComplianceLevel::CoreSpec)
1477 .with_debug(true)),
1478 (SpecVersion::V2_5, false) => Ok(EngineProfile {
1479 version,
1480 ..EngineProfile::GENEROUS
1481 }),
1482 _ => Err(Error::UnsupportedSpecVersion(format!(
1483 "spec_version {:?} is registered but has no public verification policy",
1484 version.version_str()
1485 ))),
1486 }
1487}
1488
1489fn report_profile(profile: EngineProfile) -> &'static str {
1491 match profile.version {
1492 SpecVersion::V2_5 => C2PA_PROFILE_2_5,
1493 SpecVersion::V2_2 => C2PA_PROFILE_2_2,
1494 _ => C2PA_PROFILE,
1495 }
1496}
1497
1498fn decode_base64_map(
1504 values: Option<&HashMap<String, String>>,
1505 field: &str,
1506 max_bytes: usize,
1507) -> Result<HashMap<String, Vec<u8>>, Error> {
1508 let Some(values) = values else {
1509 return Ok(HashMap::new());
1510 };
1511 let mut decoded = HashMap::with_capacity(values.len());
1512 for (key, encoded) in values {
1513 let bytes = crate::c2pa_formats::util::base64_decode(encoded)
1514 .ok_or_else(|| Error::Verification(format!("{field}[{key}] is not standard base64")))?;
1515 if bytes.len() > max_bytes {
1516 return Err(Error::Verification(format!(
1517 "{field}[{key}] exceeds the {max_bytes} byte limit"
1518 )));
1519 }
1520 decoded.insert(key.clone(), bytes);
1521 }
1522 Ok(decoded)
1523}
1524
1525pub(crate) fn resolve_mime(mime_type: &str, profile: EngineProfile) -> Result<String, Error> {
1530 let mime = canonicalize_mime(mime_type);
1531 if !crate::c2pa_core::spec::reader_permits(&mime, profile.version)
1532 || crate::c2pa_formats::AssetFormat::from_canonical_mime(&mime).is_none()
1533 {
1534 return Err(Error::UnsupportedMime(mime));
1535 }
1536 Ok(mime)
1537}
1538
1539fn resolve_detached_mime(mime_type: &str, profile: EngineProfile) -> Result<String, Error> {
1546 let mime = crate::c2pa_formats::canonicalize_detached_mime(mime_type);
1547 if !crate::c2pa_formats::detached_mime_permitted(&mime, profile.version) {
1548 return Err(Error::UnsupportedMime(mime));
1549 }
1550 Ok(mime)
1551}
1552
1553fn is_xml_comment_carrier(mime: &str) -> bool {
1556 (matches!(mime, "application/xml" | "text/xml") || mime.ends_with("+xml"))
1557 && matches!(
1558 crate::c2pa_formats::AssetFormat::from_canonical_mime(mime),
1559 Some(crate::c2pa_formats::AssetFormat::TextStructured {
1560 comment_prefix: "<!--",
1561 ..
1562 })
1563 )
1564}
1565
1566fn require_embedded_mime(mime: &str, profile: EngineProfile) -> Result<(), Error> {
1567 if profile.version == SpecVersion::V2_5 && is_xml_comment_carrier(mime) {
1574 return Err(Error::UnsupportedMime(mime.to_owned()));
1575 }
1576 Ok(())
1577}
1578
1579pub(crate) fn map_validate_error(error: crate::c2pa_validate::ValidateError) -> Error {
1580 match error {
1581 crate::c2pa_validate::ValidateError::UnsupportedMime(value) => {
1582 Error::UnsupportedMime(value)
1583 }
1584 other => Error::Verification(other.to_string()),
1585 }
1586}
1587
1588pub(crate) fn report_from_output(
1590 output: crate::c2pa_validate::VerifyOutput,
1591 mime: String,
1592 resolved: &ResolvedOptions,
1593) -> VerificationReport {
1594 let present = output
1595 .report_json
1596 .pointer("/provenance_verdict/present")
1597 .and_then(Value::as_bool)
1598 .unwrap_or(false);
1599 let integrity = output
1600 .report_json
1601 .pointer("/provenance_verdict/integrity")
1602 .and_then(Value::as_str)
1603 .unwrap_or(if present { "invalid" } else { "absent" })
1604 .to_string();
1605 let trust = trust_report(
1606 &output.results,
1607 present,
1608 resolved.trust_basis,
1609 resolved.validation_time_text.clone(),
1610 );
1611 VerificationReport {
1612 schema_version: REPORT_SCHEMA_VERSION.to_string(),
1613 profile: report_profile(resolved.profile).to_string(),
1614 mime_type: mime,
1615 present,
1616 integrity,
1617 signature: signature_status(&output.results),
1618 hard_binding: hard_binding_status(&output.results),
1619 trust,
1620 policy: None,
1621 managed_receipt: None,
1622 validation_state: output.validation_state.as_str().to_string(),
1623 validation_results: copy_results(&output.results),
1624 manifest_report: output.report_json,
1625 content_credentials: output.crjson,
1626 network: NetworkReport::default(),
1627 }
1628}
1629
1630fn verify_with_options_inner(
1633 data: &[u8],
1634 fragments: Option<&[&[u8]]>,
1635 mime_type: &str,
1636 options: &VerifyOptions,
1637 resolved: ResolvedOptions,
1638) -> Result<(VerificationReport, PassCapture), Error> {
1639 let mime = resolve_mime(mime_type, resolved.profile)?;
1640 require_embedded_mime(&mime, resolved.profile)?;
1641 if resolved.profile.version == SpecVersion::V2_5 {
1642 return verify_embedded_online_v25(data, fragments, &mime, options, resolved);
1643 }
1644
1645 let (mut report, needs, capture) =
1646 embedded_pass_resolved(data, fragments, &mime, options, &resolved)?;
1647 let (mut network, evidence) = online::gather(options, &needs);
1648 if evidence.is_empty() {
1649 report.network = network;
1650 return Ok((report, capture));
1651 }
1652 let next = online::apply_evidence(options, &evidence);
1653 let second = match (evidence.manifest_store.as_deref(), fragments) {
1654 (Some(store), None) => detached_pass(data, store, &mime, &next, GraphInputs::default()),
1658 _ => embedded_pass(data, fragments, &mime, &next),
1659 };
1660 match second {
1661 Ok((mut second, _, second_capture)) => {
1662 second.network = network;
1663 Ok((second, second_capture))
1664 }
1665 Err(error) => {
1668 network.mark_unusable("remote_manifest", error.to_string());
1669 report.network = network;
1670 Ok((report, capture))
1671 }
1672 }
1673}
1674
1675fn merged_did_documents(
1676 pinned: Option<&HashMap<String, Value>>,
1677 fetched: HashMap<String, Value>,
1678) -> Option<HashMap<String, Value>> {
1679 if fetched.is_empty() {
1680 return None;
1681 }
1682 let mut merged = pinned.cloned().unwrap_or_default();
1683 for (did, document) in fetched {
1684 merged.entry(did).or_insert(document);
1685 }
1686 Some(merged)
1687}
1688
1689fn verify_embedded_online_v25(
1690 data: &[u8],
1691 fragments: Option<&[&[u8]]>,
1692 mime: &str,
1693 options: &VerifyOptions,
1694 mut resolved: ResolvedOptions,
1695) -> Result<(VerificationReport, PassCapture), Error> {
1696 let mut plan = locate_external_resources(data, mime, options, GraphLimits::default())?;
1697 let terminal_uri = (!plan.manifest_store_present)
1698 .then(|| {
1699 plan.resources
1700 .iter()
1701 .find(|request| request.kind == ExternalResourceKind::SegmentManifestStore)
1702 .map(|request| request.uri.clone())
1703 })
1704 .flatten();
1705 let mut online = native_online::NativeOnline::new(resolved.take_external_data(), options);
1706
1707 while !plan.resources.is_empty() {
1708 let frontier = plan.resources.clone();
1709 for request in &frontier {
1710 online.ensure_graph_resource(
1711 options,
1712 request,
1713 terminal_uri.as_deref() == Some(request.uri.as_str()),
1714 );
1715 }
1716 plan.continue_with(online.resources())?;
1717 }
1718 online.mark_discovery_stop(plan.stop_reason.as_ref());
1719
1720 let fetched_dids = online.resolve_did_documents(
1721 options,
1722 plan.did_documents(),
1723 options.cawg_did_documents.as_ref(),
1724 );
1725 let merged_dids = merged_did_documents(options.cawg_did_documents.as_ref(), fetched_dids);
1726 let did_documents = merged_dids.as_ref().or(options.cawg_did_documents.as_ref());
1727 let graph_inputs = GraphInputs {
1728 external: Some(online.resources()),
1729 limits: GraphLimits::default(),
1730 };
1731 let (mut report, mut needs, mut capture) = embedded_graph_pass_resolved(
1732 data,
1733 fragments,
1734 mime,
1735 options,
1736 &resolved,
1737 did_documents,
1738 graph_inputs,
1739 )?;
1740
1741 let ocsp = online.fetch_ocsp(options, &needs);
1742 if ocsp.has_evidence() {
1743 resolved.apply_ocsp_update(ocsp);
1744 let graph_inputs = GraphInputs {
1745 external: Some(online.resources()),
1746 limits: GraphLimits::default(),
1747 };
1748 (report, needs, capture) = embedded_graph_pass_resolved(
1749 data,
1750 fragments,
1751 mime,
1752 options,
1753 &resolved,
1754 did_documents,
1755 graph_inputs,
1756 )?;
1757 }
1758 report.network = online.finish(&needs);
1759 Ok((report, capture))
1760}
1761
1762fn verify_detached_online_v25(
1763 asset: &[u8],
1764 manifest_store: &[u8],
1765 mime: &str,
1766 options: &VerifyOptions,
1767 mut resolved: ResolvedOptions,
1768) -> Result<VerificationReport, Error> {
1769 let mut plan =
1770 c2pa_validate::external_discovery::locate_external_resources_with_canonical_manifest_store(
1771 asset,
1772 manifest_store,
1773 mime,
1774 resolved.profile,
1775 GraphLimits::default(),
1776 )?;
1777 let mut online = native_online::NativeOnline::new(resolved.take_external_data(), options);
1778 while !plan.resources.is_empty() {
1779 let frontier = plan.resources.clone();
1780 for request in &frontier {
1781 online.ensure_graph_resource(options, request, false);
1782 }
1783 plan.continue_with(online.resources())?;
1784 }
1785 online.mark_discovery_stop(plan.stop_reason.as_ref());
1786
1787 let fetched_dids = online.resolve_did_documents(
1788 options,
1789 plan.did_documents(),
1790 options.cawg_did_documents.as_ref(),
1791 );
1792 let merged_dids = merged_did_documents(options.cawg_did_documents.as_ref(), fetched_dids);
1793 let did_documents = merged_dids.as_ref().or(options.cawg_did_documents.as_ref());
1794 let graph_inputs = GraphInputs {
1795 external: Some(online.resources()),
1796 limits: GraphLimits::default(),
1797 };
1798 let (mut report, mut needs, _) = detached_pass_resolved(
1799 asset,
1800 manifest_store,
1801 mime,
1802 options,
1803 &resolved,
1804 did_documents,
1805 graph_inputs,
1806 )?;
1807
1808 let ocsp = online.fetch_ocsp(options, &needs);
1809 if ocsp.has_evidence() {
1810 resolved.apply_ocsp_update(ocsp);
1811 let graph_inputs = GraphInputs {
1812 external: Some(online.resources()),
1813 limits: GraphLimits::default(),
1814 };
1815 (report, needs, _) = detached_pass_resolved(
1816 asset,
1817 manifest_store,
1818 mime,
1819 options,
1820 &resolved,
1821 did_documents,
1822 graph_inputs,
1823 )?;
1824 }
1825 report.network = online.finish(&needs);
1826 Ok(report)
1827}
1828
1829fn embedded_pass(
1832 data: &[u8],
1833 fragments: Option<&[&[u8]]>,
1834 mime: &str,
1835 options: &VerifyOptions,
1836) -> Result<(VerificationReport, Vec<online::NetworkNeed>, PassCapture), Error> {
1837 let resolved = ResolvedOptions::resolve(options)?;
1838 embedded_pass_resolved(data, fragments, mime, options, &resolved)
1839}
1840
1841fn embedded_pass_resolved(
1842 data: &[u8],
1843 fragments: Option<&[&[u8]]>,
1844 mime: &str,
1845 options: &VerifyOptions,
1846 resolved: &ResolvedOptions,
1847) -> Result<(VerificationReport, Vec<online::NetworkNeed>, PassCapture), Error> {
1848 let input = resolved.input(data, mime);
1849 let mut output = match fragments {
1850 Some(fragments) => verify_fragmented_safe(
1851 &input,
1852 fragments,
1853 &options.expected_seek_positions,
1854 resolved.cawg_trust(),
1855 resolved.cawg_allowed_certs(),
1856 resolved.cawg_document_signing_require_anchor,
1857 options.cawg_did_documents.as_ref(),
1858 options.cawg_ica_trusted_issuers.as_deref(),
1859 options.cawg_ica_trust_anchors.as_deref(),
1860 options.cawg_ica_status_lists.as_ref(),
1861 GraphInputs::default(),
1862 ),
1863 None => verify_safe(
1864 &input,
1865 resolved.cawg_trust(),
1866 resolved.cawg_allowed_certs(),
1867 resolved.cawg_document_signing_require_anchor,
1868 options.cawg_did_documents.as_ref(),
1869 options.cawg_ica_trusted_issuers.as_deref(),
1870 options.cawg_ica_trust_anchors.as_deref(),
1871 options.cawg_ica_status_lists.as_ref(),
1872 ),
1873 }
1874 .map_err(map_validate_error)?;
1875 let needs = online::network_needs(&output);
1876 let capture = PassCapture::take(&mut output);
1877 Ok((
1878 report_from_output(output, mime.to_string(), resolved),
1879 needs,
1880 capture,
1881 ))
1882}
1883
1884fn embedded_graph_pass(
1885 data: &[u8],
1886 fragments: Option<&[&[u8]]>,
1887 mime_type: &str,
1888 options: &VerifyOptions,
1889 resolved: &ResolvedOptions,
1890 graph_inputs: GraphInputs<'_>,
1891) -> Result<VerificationReport, Error> {
1892 let mime = resolve_mime(mime_type, resolved.profile)?;
1893 require_embedded_mime(&mime, resolved.profile)?;
1894 embedded_graph_pass_resolved(
1895 data,
1896 fragments,
1897 &mime,
1898 options,
1899 resolved,
1900 options.cawg_did_documents.as_ref(),
1901 graph_inputs,
1902 )
1903 .map(|(report, _, _)| report)
1904}
1905
1906#[allow(clippy::too_many_arguments)]
1907fn embedded_graph_pass_resolved(
1908 data: &[u8],
1909 fragments: Option<&[&[u8]]>,
1910 mime: &str,
1911 options: &VerifyOptions,
1912 resolved: &ResolvedOptions,
1913 did_documents: Option<&HashMap<String, Value>>,
1914 graph_inputs: GraphInputs<'_>,
1915) -> Result<(VerificationReport, Vec<online::NetworkNeed>, PassCapture), Error> {
1916 let input = resolved.input(data, mime);
1917 let mut output = match fragments {
1918 Some(fragments) => verify_fragmented_safe(
1919 &input,
1920 fragments,
1921 &options.expected_seek_positions,
1922 resolved.cawg_trust(),
1923 resolved.cawg_allowed_certs(),
1924 resolved.cawg_document_signing_require_anchor,
1925 did_documents,
1926 options.cawg_ica_trusted_issuers.as_deref(),
1927 options.cawg_ica_trust_anchors.as_deref(),
1928 options.cawg_ica_status_lists.as_ref(),
1929 graph_inputs,
1930 ),
1931 None => verify_graph_safe(
1932 &input,
1933 resolved.cawg_trust(),
1934 resolved.cawg_allowed_certs(),
1935 resolved.cawg_document_signing_require_anchor,
1936 did_documents,
1937 options.cawg_ica_trusted_issuers.as_deref(),
1938 options.cawg_ica_trust_anchors.as_deref(),
1939 options.cawg_ica_status_lists.as_ref(),
1940 graph_inputs,
1941 ),
1942 }
1943 .map_err(map_validate_error)?;
1944 let needs = online::network_needs(&output);
1945 let capture = PassCapture::take(&mut output);
1946 Ok((
1947 report_from_output(output, mime.to_string(), resolved),
1948 needs,
1949 capture,
1950 ))
1951}
1952
1953pub fn verify_file(
1958 path: impl AsRef<Path>,
1959 mime_type: Option<&str>,
1960 options: &VerifyOptions,
1961) -> Result<VerificationReport, Error> {
1962 let resolved = ResolvedOptions::resolve(options)?;
1963 let path = path.as_ref();
1964 let mime = match mime_type {
1965 Some(value) => value.to_string(),
1966 None => mime_from_path(path)
1967 .ok_or_else(|| Error::UnsupportedMime(path.display().to_string()))?
1968 .to_string(),
1969 };
1970 let data = read_path_asset(path, MAX_PATH_ASSET_BYTES)?;
1971 verify_with_resolved_options(&data, None, &mime, options, resolved)
1972}
1973
1974fn read_path_asset(path: &Path, limit: u64) -> io::Result<Vec<u8>> {
1975 let mut options = OpenOptions::new();
1976 options.read(true);
1977 #[cfg(unix)]
1978 options.custom_flags(libc::O_NONBLOCK | libc::O_CLOEXEC);
1979
1980 let mut file = options.open(path)?;
1983 let opened_metadata = file.metadata()?;
1984 validate_path_asset(path, &opened_metadata, limit)?;
1985 read_bounded_file(&mut file, opened_metadata.len(), limit)
1986}
1987
1988fn validate_path_asset(path: &Path, metadata: &fs::Metadata, limit: u64) -> io::Result<()> {
1989 if !metadata.is_file() {
1990 return Err(io::Error::new(
1991 io::ErrorKind::InvalidInput,
1992 format!("asset path is not a regular file: {}", path.display()),
1993 ));
1994 }
1995 if metadata.len() > limit {
1996 return Err(io::Error::new(
1997 io::ErrorKind::InvalidData,
1998 format!("asset exceeds the 128 MiB path limit: {}", path.display()),
1999 ));
2000 }
2001 Ok(())
2002}
2003
2004fn read_bounded_file<R: Read>(file: &mut R, expected_len: u64, limit: u64) -> io::Result<Vec<u8>> {
2005 if expected_len > limit {
2006 return Err(io::Error::new(
2007 io::ErrorKind::InvalidData,
2008 "asset exceeds the path size limit",
2009 ));
2010 }
2011 let expected_len = usize::try_from(expected_len)
2012 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "asset size is not addressable"))?;
2013 let mut data = vec![0_u8; expected_len + 1];
2014 let mut used = 0;
2015 while used < expected_len {
2016 let read = file.read(&mut data[used..expected_len])?;
2017 if read == 0 {
2018 break;
2019 }
2020 used += read;
2021 }
2022 if used == expected_len && file.read(&mut data[expected_len..expected_len + 1])? != 0 {
2023 return Err(io::Error::new(
2024 io::ErrorKind::InvalidData,
2025 "asset grew while being read",
2026 ));
2027 }
2028 data.truncate(used);
2029 Ok(data)
2030}
2031
2032pub fn supported_mime_types() -> Vec<&'static str> {
2034 let mut mimes: Vec<_> = mimes_for_version(SpecVersion::V2_4)
2035 .into_iter()
2036 .filter(|mime| crate::c2pa_formats::AssetFormat::from_mime(mime).is_some())
2037 .collect();
2038 mimes.sort_unstable();
2039 mimes.dedup();
2040 mimes
2041}
2042
2043pub fn supported_mime_types_for(spec_version: Option<&str>) -> Result<Vec<&'static str>, Error> {
2055 let profile = resolve_profile(spec_version, false)?;
2056 let registry_version = match profile.version {
2057 SpecVersion::V2_5 => SpecVersion::V2_4,
2058 version => version,
2059 };
2060 let mut mimes: Vec<_> = mimes_for_version(registry_version)
2061 .into_iter()
2062 .chain(
2063 (profile.version == SpecVersion::V2_5)
2064 .then_some(crate::c2pa_core::spec::V2_5_READER_MIMES)
2065 .into_iter()
2066 .flatten()
2067 .copied(),
2068 )
2069 .filter(|mime| crate::c2pa_formats::AssetFormat::from_mime(mime).is_some())
2070 .filter(|mime| require_embedded_mime(mime, profile).is_ok())
2071 .collect();
2072 mimes.sort_unstable();
2073 mimes.dedup();
2074 Ok(mimes)
2075}
2076
2077pub const SUPPORTED_EXTENSIONS: &[(&str, &str)] = &[
2085 ("jpg", "image/jpeg"),
2086 ("jpeg", "image/jpeg"),
2087 ("png", "image/png"),
2088 ("webp", "image/webp"),
2089 ("gif", "image/gif"),
2090 ("tif", "image/tiff"),
2091 ("tiff", "image/tiff"),
2092 ("dng", "image/x-adobe-dng"),
2093 ("heic", "image/heic"),
2094 ("heics", "image/heic-sequence"),
2095 ("heif", "image/heif"),
2096 ("heifs", "image/heif-sequence"),
2097 ("avif", "image/avif"),
2098 ("jxl", "image/jxl"),
2099 ("svg", "image/svg+xml"),
2100 ("mp4", "video/mp4"),
2101 ("m4v", "video/mp4"),
2102 ("mov", "video/quicktime"),
2103 ("avi", "video/x-msvideo"),
2104 ("wav", "audio/wav"),
2105 ("mp3", "audio/mpeg"),
2106 ("m4a", "audio/mp4"),
2107 ("aac", "audio/aac"),
2108 ("flac", "audio/flac"),
2109 ("ogg", "audio/ogg"),
2110 ("oga", "audio/ogg"),
2111 ("pdf", "application/pdf"),
2112 ("zip", "application/zip"),
2113 ("sketch", "application/zip"),
2114 ("epub", "application/epub+zip"),
2115 (
2116 "docx",
2117 "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
2118 ),
2119 (
2120 "xlsx",
2121 "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
2122 ),
2123 (
2124 "pptx",
2125 "application/vnd.openxmlformats-officedocument.presentationml.presentation",
2126 ),
2127 (
2128 "dotx",
2129 "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
2130 ),
2131 ("docm", "application/vnd.ms-word.document.macroenabled.12"),
2132 ("dotm", "application/vnd.ms-word.template.macroenabled.12"),
2133 (
2134 "xltx",
2135 "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
2136 ),
2137 ("xlsm", "application/vnd.ms-excel.sheet.macroenabled.12"),
2138 ("xltm", "application/vnd.ms-excel.template.macroenabled.12"),
2139 (
2140 "xlsb",
2141 "application/vnd.ms-excel.sheet.binary.macroenabled.12",
2142 ),
2143 (
2144 "ppsx",
2145 "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
2146 ),
2147 (
2148 "potx",
2149 "application/vnd.openxmlformats-officedocument.presentationml.template",
2150 ),
2151 (
2152 "pptm",
2153 "application/vnd.ms-powerpoint.presentation.macroenabled.12",
2154 ),
2155 (
2156 "ppsm",
2157 "application/vnd.ms-powerpoint.slideshow.macroenabled.12",
2158 ),
2159 (
2160 "potm",
2161 "application/vnd.ms-powerpoint.template.macroenabled.12",
2162 ),
2163 ("vsdx", "application/vnd.ms-visio.drawing"),
2164 ("vsdm", "application/vnd.ms-visio.drawing.macroenabled.12"),
2165 ("vssx", "application/vnd.ms-visio.stencil"),
2166 ("vssm", "application/vnd.ms-visio.stencil.macroenabled.12"),
2167 ("vstx", "application/vnd.ms-visio.template"),
2168 ("vstm", "application/vnd.ms-visio.template.macroenabled.12"),
2169 ("oxps", "application/oxps"),
2170 ("xps", "application/vnd.ms-xpsdocument"),
2171 ("odt", "application/vnd.oasis.opendocument.text"),
2172 ("ods", "application/vnd.oasis.opendocument.spreadsheet"),
2173 ("odp", "application/vnd.oasis.opendocument.presentation"),
2174 ("odg", "application/vnd.oasis.opendocument.graphics"),
2175 ("ttf", "font/ttf"),
2176 ("otf", "font/otf"),
2177 ("sfnt", "font/sfnt"),
2178 ("txt", "text/plain"),
2179 ("tsv", "text/tab-separated-values"),
2180 ("csv", "text/csv"),
2181 ("md", "text/markdown"),
2182 ("markdown", "text/markdown"),
2183 ("html", "text/html"),
2184 ("htm", "text/html"),
2185 ("xhtml", "application/xhtml+xml"),
2186 ("xml", "application/xml"),
2187 ("css", "text/css"),
2188 ("js", "application/javascript"),
2189 ("mjs", "application/javascript"),
2190 ("json", "application/json"),
2191 ("yaml", "application/yaml"),
2192 ("yml", "application/yaml"),
2193 ("toml", "application/toml"),
2194 ("py", "text/x-python"),
2195 ("warc", "application/warc"),
2198 ("mid", "audio/midi"),
2199 ("midi", "audio/midi"),
2200 ("ttml", "application/ttml+xml"),
2201 ("onnx", "application/onnx"),
2202 ("safetensors", "application/safetensors"),
2203 ("parquet", "application/vnd.apache.parquet"),
2204 ("facti", "image/vnd.blockfact.facti"),
2205 ("facta", "audio/vnd.blockfact.facta"),
2206 ("factv", "video/vnd.blockfact.factv"),
2207 ("jsonl", "application/jsonl"),
2208 ("ndjson", "application/jsonl"),
2209 ("vtt", "text/vtt"),
2210];
2211
2212pub fn mime_from_path(path: &Path) -> Option<&'static str> {
2214 let extension = path.extension()?.to_str()?.to_ascii_lowercase();
2215 SUPPORTED_EXTENSIONS
2216 .iter()
2217 .find(|(candidate, _)| *candidate == extension)
2218 .map(|(_, mime)| *mime)
2219}
2220
2221enum ResolvedTrust {
2222 Bundled(&'static TrustList),
2223 Owned(TrustList),
2224}
2225
2226impl ResolvedTrust {
2227 fn get(&self) -> &TrustList {
2228 match self {
2229 Self::Bundled(trust) => trust,
2230 Self::Owned(trust) => trust,
2231 }
2232 }
2233}
2234
2235fn resolve_trust(
2246 custom_pem: Option<&str>,
2247 sources: Option<&[TrustListSource]>,
2248 bundled: Option<&'static TrustList>,
2249 purpose: AnchorPurpose,
2250 bounds: (Option<OffsetDateTime>, Option<OffsetDateTime>),
2251) -> Result<Option<ResolvedTrust>, Error> {
2252 let mut custom = custom_pem
2253 .map(|pem| TrustList::from_pem_for(purpose, pem))
2254 .transpose()
2255 .map_err(|error| Error::InvalidTrust(error.to_string()))?
2256 .map(|trust| trust.with_bounds(bounds.0, bounds.1))
2257 .map(|trust| match purpose {
2258 AnchorPurpose::CawgIdentity => trust.with_cawg_source(CawgTrustSource::CallerSupplied),
2259 _ => trust,
2260 });
2261 let mut windowed = TrustList::default();
2262 for (index, source) in sources.unwrap_or_default().iter().enumerate() {
2263 if source.uri.trim().is_empty() || url::Url::parse(&source.uri).is_err() {
2264 return Err(Error::InvalidTrust(format!(
2265 "trust source {index} has an invalid uri"
2266 )));
2267 }
2268 if source.source_type.trim().is_empty() {
2269 return Err(Error::InvalidTrust(format!(
2270 "trust source {index} has an empty type"
2271 )));
2272 }
2273 if source
2274 .identifier
2275 .as_ref()
2276 .is_some_and(|value| value.trim().is_empty())
2277 {
2278 return Err(Error::InvalidTrust(format!(
2279 "trust source {index} has an empty identifier"
2280 )));
2281 }
2282 let instant = |value: Option<&str>, field: &str| {
2283 value
2284 .map(|raw| {
2285 OffsetDateTime::parse(raw, &Rfc3339).map_err(|error| {
2286 Error::InvalidTrust(format!("trust source {index}.{field}: {error}"))
2287 })
2288 })
2289 .transpose()
2290 };
2291 let not_before = instant(source.not_before.as_deref(), "not_before")?;
2292 let not_after = instant(source.not_after.as_deref(), "not_after")?;
2293 if let (Some(start), Some(end)) = (not_before, not_after) {
2294 if start > end {
2295 return Err(Error::InvalidTrust(format!(
2296 "trust source {index}: not_before is later than not_after"
2297 )));
2298 }
2299 }
2300 let not_before = not_before.into_iter().chain(bounds.0).max();
2303 let not_after = not_after.into_iter().chain(bounds.1).min();
2304 let mut metadata =
2305 crate::c2pa_trust::TrustSource::new(source.uri.clone(), source.source_type.clone());
2306 if let Some(identifier) = &source.identifier {
2307 metadata = metadata.with_identifier(identifier.clone());
2308 }
2309 let sourced = TrustList::from_pem_for(purpose, &source.certificates_pem)
2310 .map_err(|error| Error::InvalidTrust(format!("trust source {index}: {error}")))?
2311 .with_bounds(not_before, not_after)
2312 .with_source(metadata);
2313 if source.not_before.is_some() || source.not_after.is_some() {
2314 for anchor in sourced.anchors {
2315 if let Some(existing) = windowed
2316 .anchors
2317 .iter_mut()
2318 .find(|existing| existing.certificate == anchor.certificate)
2319 {
2320 existing.not_before = existing
2321 .not_before
2322 .into_iter()
2323 .chain(anchor.not_before)
2324 .max();
2325 existing.not_after =
2326 existing.not_after.into_iter().chain(anchor.not_after).min();
2327 } else {
2328 windowed.anchors.push(anchor);
2329 }
2330 }
2331 } else {
2332 match &mut custom {
2333 Some(custom) => custom.anchors.extend(sourced.anchors),
2334 None => custom = Some(sourced),
2335 }
2336 }
2337 }
2338 if !windowed.anchors.is_empty() {
2339 let mut merged = bundled.cloned().unwrap_or_default();
2340 if let Some(custom) = custom {
2341 merged.anchors.extend(custom.anchors);
2342 }
2343 merged.anchors.retain(|anchor| {
2347 !windowed
2348 .anchors
2349 .iter()
2350 .any(|bounded| bounded.certificate == anchor.certificate)
2351 });
2352 merged.anchors.extend(windowed.anchors);
2353 return Ok(Some(ResolvedTrust::Owned(merged)));
2354 }
2355 match (bundled, custom) {
2356 (None, None) => Ok(None),
2357 (Some(trust), None) => Ok(Some(ResolvedTrust::Bundled(trust))),
2358 (None, Some(trust)) => Ok(Some(ResolvedTrust::Owned(trust))),
2359 (Some(bundled), Some(custom)) => {
2360 let mut merged = bundled.clone();
2361 merged.anchors.extend(custom.anchors);
2362 Ok(Some(ResolvedTrust::Owned(merged)))
2363 }
2364 }
2365}
2366
2367fn resolve_cawg_configurations(
2375 configurations: Option<&[CawgTrustConfiguration]>,
2376) -> Result<(TrustList, TrustList), Error> {
2377 let mut anchors = TrustList::default();
2378 let mut direct = TrustList::default();
2379 for (index, configuration) in configurations.unwrap_or_default().iter().enumerate() {
2380 let invalid = |field: &str, detail: &dyn std::fmt::Display| {
2381 Error::InvalidTrust(format!(
2382 "cawg_trust_configurations[{index}]{field}: {detail}"
2383 ))
2384 };
2385 let instant = |value: Option<&str>, field| {
2386 value
2387 .map(|raw| {
2388 OffsetDateTime::parse(raw, &Rfc3339).map_err(|error| invalid(field, &error))
2389 })
2390 .transpose()
2391 };
2392 let not_before = instant(configuration.not_before.as_deref(), ".not_before")?;
2393 let not_after = instant(configuration.not_after.as_deref(), ".not_after")?;
2394 if let (Some(start), Some(end)) = (not_before, not_after) {
2395 if start > end {
2396 return Err(invalid("", &"not_before is later than not_after"));
2397 }
2398 }
2399 let source = match configuration.profile {
2400 CawgTrustProfile::Base => CawgTrustSource::CallerSupplied,
2401 CawgTrustProfile::SmimeInterim => CawgTrustSource::SmimeInterim,
2402 };
2403 let (entry_anchors, entry_direct) =
2404 TrustList::from_pem_for(AnchorPurpose::CawgIdentity, &configuration.certificates_pem)
2405 .map_err(|error| invalid(".certificates_pem", &error))?
2406 .with_bounds(not_before, not_after)
2407 .with_cawg_source(source)
2408 .split_certificate_authorities();
2409 anchors.anchors.extend(entry_anchors.anchors);
2410 direct.anchors.extend(entry_direct.anchors);
2411 }
2412 Ok((anchors, direct))
2413}
2414
2415fn extend_trust(resolved: Option<ResolvedTrust>, extra: TrustList) -> Option<ResolvedTrust> {
2417 if extra.anchors.is_empty() {
2418 return resolved;
2419 }
2420 let mut merged = match resolved {
2421 None => return Some(ResolvedTrust::Owned(extra)),
2422 Some(ResolvedTrust::Bundled(bundled)) => bundled.clone(),
2423 Some(ResolvedTrust::Owned(owned)) => owned,
2424 };
2425 merged.anchors.extend(extra.anchors);
2426 Some(ResolvedTrust::Owned(merged))
2427}
2428
2429fn parse_validation_time(value: Option<&str>) -> Result<OffsetDateTime, Error> {
2430 match value {
2431 Some(raw) => OffsetDateTime::parse(raw, &Rfc3339)
2432 .map_err(|error| Error::InvalidValidationTime(error.to_string())),
2433 None => Ok(OffsetDateTime::now_utc()),
2434 }
2435}
2436
2437fn parse_optional_instant(
2440 value: Option<&str>,
2441 field: &str,
2442) -> Result<Option<OffsetDateTime>, Error> {
2443 value
2444 .map(|raw| {
2445 OffsetDateTime::parse(raw, &Rfc3339)
2446 .map_err(|error| Error::InvalidValidationTime(format!("{field}: {error}")))
2447 })
2448 .transpose()
2449}
2450
2451fn parse_stream_presentation_time(raw: &str, index: usize) -> Result<OffsetDateTime, Error> {
2452 OffsetDateTime::parse(raw, &Rfc3339).map_err(|error| {
2453 Error::InvalidValidationTime(format!("stream_presentation_times[{index}]: {error}"))
2454 })
2455}
2456
2457pub(crate) fn parse_stream_presentation_times(
2461 values: Option<&[String]>,
2462) -> Result<Option<Vec<OffsetDateTime>>, Error> {
2463 values
2464 .map(|values| {
2465 values
2466 .iter()
2467 .enumerate()
2468 .map(|(index, raw)| parse_stream_presentation_time(raw, index))
2469 .collect()
2470 })
2471 .transpose()
2472}
2473
2474fn copy_status(status: &CoreStatus) -> VerificationStatus {
2475 VerificationStatus {
2476 code: status.code.clone(),
2477 url: status.url.clone(),
2478 resource_url: status.resource_url.clone(),
2479 explanation: status.explanation.clone(),
2480 details: status.details.clone(),
2481 }
2482}
2483
2484fn copy_results(results: &CoreResults) -> ValidationResults {
2485 ValidationResults {
2486 success: results.success.iter().map(copy_status).collect(),
2487 informational: results.informational.iter().map(copy_status).collect(),
2488 failure: results.failure.iter().map(copy_status).collect(),
2489 trust_list_sources: results.trust_list_sources.clone(),
2490 }
2491}
2492
2493fn signature_status(results: &CoreResults) -> String {
2494 if results.has_success(CLAIM_SIGNATURE_VALIDATED) {
2495 "valid"
2496 } else if results.has_failure(CLAIM_SIGNATURE_MISMATCH) {
2497 "invalid"
2498 } else if results.has_failure(CLAIM_SIGNATURE_MISSING) {
2499 "missing"
2500 } else {
2501 "unknown"
2502 }
2503 .to_string()
2504}
2505
2506fn hard_binding_status(results: &CoreResults) -> String {
2507 const MATCHES: &[&str] = &[
2508 ASSERTION_DATA_HASH_MATCH,
2509 ASSERTION_BMFF_HASH_MATCH,
2510 ASSERTION_BOXES_HASH_MATCH,
2511 ASSERTION_COLLECTION_HASH_MATCH,
2512 ASSERTION_MULTI_ASSET_HASH_MATCH,
2513 crate::c2pa_validate::ASSERTION_SEGMENT_HASH_MATCH,
2514 ];
2515 const FAILURES: &[&str] = &[
2516 ASSERTION_DATA_HASH_MISMATCH,
2517 ASSERTION_BMFF_HASH_MISMATCH,
2518 ASSERTION_BMFF_HASH_MALFORMED,
2519 ASSERTION_BOXES_HASH_MISMATCH,
2520 ASSERTION_BOXES_HASH_MALFORMED,
2521 ASSERTION_COLLECTION_HASH_MISMATCH,
2522 ASSERTION_COLLECTION_HASH_MALFORMED,
2523 ASSERTION_MULTI_ASSET_HASH_MISMATCH,
2524 ASSERTION_MULTI_ASSET_HASH_MALFORMED,
2525 crate::c2pa_validate::ASSERTION_SEGMENT_HASH_MISMATCH,
2526 crate::c2pa_validate::ASSERTION_SEGMENT_HASH_MALFORMED,
2527 crate::c2pa_validate::ASSERTION_SEGMENT_HASH_PARTIAL_COVERAGE,
2528 crate::c2pa_validate::ASSERTION_SEGMENT_HASH_PREDECESSOR_MISMATCH,
2529 ];
2530 const UNBOUND: &[&str] = &[
2539 crate::c2pa_validate::live_video::LIVEVIDEO_SEGMENT_INVALID,
2540 crate::c2pa_validate::ASSERTION_SEGMENT_HASH_MISMATCH,
2541 crate::c2pa_validate::ASSERTION_SEGMENT_HASH_MALFORMED,
2542 crate::c2pa_validate::ASSERTION_SEGMENT_HASH_PARTIAL_COVERAGE,
2543 crate::c2pa_validate::ASSERTION_SEGMENT_HASH_PREDECESSOR_MISMATCH,
2544 crate::c2pa_validate::HARD_BINDING_CALLER_EVIDENCE_MISMATCH,
2545 ];
2546 if UNBOUND.iter().any(|code| results.has_failure(code)) {
2547 "mismatch"
2548 } else if MATCHES.iter().any(|code| results.has_success(code)) {
2549 "match"
2550 } else if FAILURES.iter().any(|code| results.has_failure(code)) {
2551 "mismatch"
2552 } else if results.has_failure(CLAIM_HARD_BINDINGS_MISSING) {
2553 "missing"
2554 } else {
2555 "unknown"
2556 }
2557 .to_string()
2558}
2559
2560fn trust_report(
2561 results: &CoreResults,
2562 present: bool,
2563 basis: &str,
2564 validation_time: String,
2565) -> TrustReport {
2566 let supplied = basis != "none";
2567 let trusted = results.has_success(SIGNING_CREDENTIAL_TRUSTED);
2568 let rejected = results.has_failure(SIGNING_CREDENTIAL_UNTRUSTED)
2569 || results.has_failure(SIGNING_CREDENTIAL_INVALID);
2570 let revoked = results.has_failure(SIGNING_CREDENTIAL_OCSP_REVOKED);
2571 let not_revoked = results.has_success(SIGNING_CREDENTIAL_OCSP_NOT_REVOKED);
2572 TrustReport {
2573 status: if trusted && !revoked {
2574 "valid_for_supplied_material"
2575 } else if present && supplied && (rejected || revoked) {
2576 "not_valid_for_supplied_material"
2577 } else {
2578 "not_evaluated"
2579 }
2580 .to_string(),
2581 basis: basis.to_string(),
2582 validation_time,
2583 revocation: RevocationReport {
2584 status: if revoked {
2585 "revoked"
2586 } else if not_revoked {
2587 "not_revoked"
2588 } else {
2589 "not_checked"
2590 }
2591 .to_string(),
2592 source: if !(revoked || not_revoked) {
2593 "none"
2594 } else if results
2595 .success
2596 .iter()
2597 .chain(&results.failure)
2598 .filter(|status| {
2599 status.code == SIGNING_CREDENTIAL_OCSP_REVOKED
2600 || status.code == SIGNING_CREDENTIAL_OCSP_NOT_REVOKED
2601 })
2602 .any(|status| {
2603 status
2604 .details
2605 .as_ref()
2606 .and_then(|details| details.get("source"))
2607 .and_then(Value::as_str)
2608 == Some("online_ocsp")
2609 })
2610 {
2611 "online_ocsp"
2612 } else {
2613 "embedded_ocsp"
2614 }
2615 .to_string(),
2616 responder_signature: if revoked || not_revoked {
2617 "valid"
2618 } else {
2619 "not_applicable"
2620 }
2621 .to_string(),
2622 },
2623 freshness: FreshnessReport {
2624 status: "unknown".to_string(),
2625 as_of: None,
2626 },
2627 }
2628}
2629
2630#[cfg(test)]
2631mod tests {
2632 use super::{
2633 detached_manifest_evidence, mime_from_path, read_bounded_file, supported_mime_types,
2634 verify, verify_file, verify_fragmented, verify_fragmented_with_graph_inputs,
2635 verify_manifest_store_only_with_graph_inputs, verify_with_manifest_store_and_graph_inputs,
2636 verify_with_options, CawgTrustConfiguration, CawgTrustProfile, Error, GraphInputs,
2637 GraphLimits, VerificationReport, VerifyOptions,
2638 };
2639 use std::{io::Cursor, path::Path};
2640
2641 #[test]
2642 fn windowed_claim_sources_override_pem_duplicates_and_intersect() {
2643 use crate::c2pa_crypto::extract_x5chain;
2644 use crate::c2pa_trust::{validate_chain_with, AnchorPurpose, CertificateProfile};
2645 use crate::c2pa_validate::signature_conformance_tests::Signer;
2646 use time::macros::datetime;
2647
2648 let signer = Signer::conformant();
2649 let other = Signer::conformant();
2650 let pem = format!("{}{}", signer.root_pem(), other.root_pem());
2651 let chain = extract_x5chain(&signer.sign(b"claim")).unwrap();
2652 let other_chain = extract_x5chain(&other.sign(b"claim")).unwrap();
2653 let source = |uri: &str, start: Option<&str>, end: Option<&str>| {
2654 serde_json::from_value::<super::TrustListSource>(serde_json::json!({
2655 "uri": uri, "type": "c2pa.trustList", "certificates_pem": signer.root_pem(),
2656 "not_before": start, "not_after": end,
2657 }))
2658 .unwrap()
2659 };
2660 for reverse in [false, true] {
2661 let mut sources = vec![
2662 source("urn:fixture:start", Some("2026-06-01T00:00:00Z"), None),
2663 source("urn:fixture:end", None, Some("2026-06-30T00:00:00Z")),
2664 source("urn:fixture:unbounded", None, None),
2665 ];
2666 if reverse {
2667 sources.reverse();
2668 }
2669 let trust = super::resolve_trust(
2670 Some(&pem),
2671 Some(&sources),
2672 None,
2673 AnchorPurpose::ClaimSigning,
2674 (None, None),
2675 )
2676 .unwrap()
2677 .unwrap();
2678 assert_eq!(trust.get().anchors.len(), 2);
2679 let bounded = trust
2680 .get()
2681 .anchors
2682 .iter()
2683 .find(|anchor| anchor.certificate == *chain.last().unwrap())
2684 .unwrap();
2685 assert_eq!(bounded.not_before, Some(datetime!(2026-06-01 0:00 UTC)));
2686 assert_eq!(bounded.not_after, Some(datetime!(2026-06-30 0:00 UTC)));
2687 assert!(bounded.source.as_ref().unwrap().uri != "urn:fixture:unbounded");
2688 for profile in [
2689 CertificateProfile::V2_2,
2690 CertificateProfile::V2_4,
2691 CertificateProfile::V2_5,
2692 ] {
2693 for (at, expected) in [
2694 (datetime!(2026-05-31 23:59:59 UTC), false),
2695 (datetime!(2026-06-01 0:00 UTC), true),
2696 (datetime!(2026-06-30 0:00 UTC), true),
2697 (datetime!(2026-06-30 0:00:01 UTC), false),
2698 ] {
2699 let result = validate_chain_with(
2700 &chain[0],
2701 &chain[1..],
2702 trust.get(),
2703 AnchorPurpose::ClaimSigning,
2704 Some(at),
2705 profile,
2706 );
2707 assert_eq!(result.trusted, expected, "{at} {profile:?}: {result:?}");
2708 let unaffected = validate_chain_with(
2709 &other_chain[0],
2710 &other_chain[1..],
2711 trust.get(),
2712 AnchorPurpose::ClaimSigning,
2713 Some(at),
2714 profile,
2715 );
2716 assert!(
2717 unaffected.trusted,
2718 "PEM-only anchor must stay trusted: {unaffected:?}"
2719 );
2720 }
2721 }
2722 }
2723 }
2724
2725 #[test]
2726 fn source_windows_are_validated_before_asset_io() {
2727 for field in ["not_before", "not_after"] {
2728 for list in ["claim_trust_sources", "tsa_trust_sources"] {
2729 let mut source = serde_json::json!({
2730 "uri": "urn:fixture:list",
2731 "type": "c2pa.tsaTrustList",
2732 "certificates_pem": include_str!("c2pa-trust/tests/fixtures/rfc3161_root.pem")
2733 });
2734 source[field] = serde_json::json!("not-an-rfc3339-instant");
2735 let mut value = serde_json::json!({"no_default_trust": true});
2736 value[list] = serde_json::json!([source]);
2737 let options: VerifyOptions = serde_json::from_value(value).unwrap();
2738 let error = options.validate().unwrap_err();
2739 assert_eq!(error.code(), "invalid_trust_material");
2740 assert!(error
2741 .to_string()
2742 .contains(&format!("trust source 0.{field}")));
2743 }
2744 }
2745 }
2746
2747 #[test]
2748 fn source_windows_preserve_global_bounds_and_refuse_reversed_dates() {
2749 let source = |start: &str, end: &str| {
2750 serde_json::from_value::<super::TrustListSource>(serde_json::json!({
2751 "uri": "urn:fixture:list", "type": "c2pa.tsaTrustList",
2752 "certificates_pem": include_str!("c2pa-trust/tests/fixtures/rfc3161_root.pem"),
2753 "not_before": start, "not_after": end
2754 }))
2755 .unwrap()
2756 };
2757 let global_start = time::macros::datetime!(2026-01-01 0:00 UTC);
2758 let global_end = time::macros::datetime!(2027-01-01 0:00 UTC);
2759 let sources = [source("2025-01-01T00:00:00Z", "2028-01-01T00:00:00Z")];
2760 let trust = super::resolve_trust(
2761 None,
2762 Some(&sources),
2763 None,
2764 super::AnchorPurpose::TimeStamping,
2765 (Some(global_start), Some(global_end)),
2766 )
2767 .unwrap()
2768 .unwrap();
2769 assert_eq!(trust.get().anchors[0].not_before, Some(global_start));
2770 assert_eq!(trust.get().anchors[0].not_after, Some(global_end));
2771 let options = VerifyOptions {
2772 tsa_trust_sources: Some(vec![source("2028-01-01T00:00:00Z", "2025-01-01T00:00:00Z")]),
2773 ..Default::default()
2774 };
2775 assert!(options
2776 .validate()
2777 .unwrap_err()
2778 .to_string()
2779 .contains("not_before is later than not_after"));
2780 }
2781
2782 #[test]
2783 fn known_filename_maps_to_mime() {
2784 assert_eq!(
2785 mime_from_path(Path::new("composition.MP4")),
2786 Some("video/mp4")
2787 );
2788
2789 assert_eq!(
2790 mime_from_path(Path::new("drawing.odg")),
2791 Some("application/vnd.oasis.opendocument.graphics")
2792 );
2793 assert_eq!(
2794 mime_from_path(Path::new("data.tsv")),
2795 Some("text/tab-separated-values")
2796 );
2797 assert_eq!(
2798 mime_from_path(Path::new("archive.ZIP")),
2799 Some("application/zip")
2800 );
2801 assert_eq!(
2802 mime_from_path(Path::new("design.sketch")),
2803 Some("application/zip")
2804 );
2805 }
2806
2807 #[test]
2811 fn every_supported_format_is_reachable_from_an_extension() {
2812 const ALIASES: &[&str] = &[
2813 "application/font-sfnt",
2814 "application/x-font-ttf",
2815 "application/mp4",
2816 "video/x-m4v",
2817 "text/xml",
2818 "application/x-zip-based",
2819 ];
2820 let unreachable: Vec<_> = supported_mime_types()
2821 .into_iter()
2822 .filter(|mime| !ALIASES.contains(mime))
2823 .filter(|mime| !super::SUPPORTED_EXTENSIONS.iter().any(|(_, m)| m == mime))
2824 .collect();
2825 assert!(
2826 unreachable.is_empty(),
2827 "no extension maps to {unreachable:?}"
2828 );
2829 assert_eq!(
2830 mime_from_path(Path::new("report.OXPS")),
2831 Some("application/oxps")
2832 );
2833 }
2834
2835 #[test]
2838 fn revocation_source_distinguishes_online_from_embedded_ocsp() {
2839 use crate::c2pa_validate::{StatusCode, ValidationResults};
2840 let status = |source: Option<&str>| StatusCode {
2841 code: crate::c2pa_validate::SIGNING_CREDENTIAL_OCSP_NOT_REVOKED.into(),
2842 url: "self#jumbf=/c2pa/urn:c2pa:x/c2pa.signature".into(),
2843 resource_url: None,
2844 explanation: "not revoked".into(),
2845 details: source.map(|source| serde_json::json!({ "source": source })),
2846 };
2847 let report = |source: Option<&str>| {
2848 let results = ValidationResults {
2849 success: vec![status(source)],
2850 ..ValidationResults::default()
2851 };
2852 super::trust_report(&results, true, "bundled_static_material", String::new())
2853 .revocation
2854 .source
2855 };
2856 assert_eq!(report(Some("online_ocsp")), "online_ocsp");
2857 assert_eq!(report(None), "embedded_ocsp");
2858 }
2859
2860 #[test]
2861 fn format_list_is_sorted_and_contains_composition_formats() {
2862 let formats = supported_mime_types();
2863 assert_eq!(formats.len(), 73);
2864 assert!(formats.windows(2).all(|pair| pair[0] < pair[1]));
2865 assert!(formats.contains(&"video/mp4"));
2866 assert!(formats.contains(&"image/jpeg"));
2867 assert!(formats.contains(&"text/tab-separated-values"));
2868 assert!(formats.contains(&"application/vnd.oasis.opendocument.graphics"));
2869 assert!(formats.contains(&"application/zip"));
2870 assert!(formats.contains(&"application/x-zip-based"));
2871 }
2872
2873 #[test]
2874 fn input_alias_is_reported_as_canonical_mime() {
2875 let asset = b"\x00\x00\x00\x10ftypisom\x00\x00\x00\x00";
2876 let report = verify(asset, "audio/aac; codecs=mp4a.40.2").unwrap();
2877 assert_eq!(report.mime_type, "audio/mp4");
2878 assert!(!report.present);
2879 }
2880
2881 #[test]
2882 fn strict_conformance_option_selects_the_program_profile() {
2883 let asset = include_bytes!("../../../tests/fixtures/signed_test.jpg");
2884 let report = verify_with_options(
2885 asset,
2886 "image/jpeg",
2887 &VerifyOptions {
2888 strict_conformance: true,
2889 ..VerifyOptions::default()
2890 },
2891 )
2892 .unwrap();
2893 assert_eq!(
2894 report.manifest_report["engine_profile"]["operating_mode"],
2895 "conformance"
2896 );
2897 assert_eq!(
2898 report.manifest_report["engine_profile"]["compliance_level"],
2899 "conformance-program"
2900 );
2901 }
2902
2903 #[test]
2904 fn fragmented_entry_point_rejects_non_bmff_mime() {
2905 let error = verify_fragmented(
2906 include_bytes!("../../../tests/fixtures/signed_test.jpg"),
2907 &[b"ignored fragment"],
2908 "image/jpeg",
2909 )
2910 .unwrap_err();
2911 assert!(matches!(error, Error::UnsupportedMime(mime) if mime == "image/jpeg"));
2912 }
2913
2914 #[test]
2915 fn detached_fragmented_and_store_only_routes_honor_one_graph_budget() {
2916 let limited = || GraphInputs {
2917 external: None,
2918 limits: GraphLimits::lowered(32, 0, 16_384, 64 * 1024 * 1024),
2919 };
2920 let has_node_limit = |report: &VerificationReport| {
2921 report
2922 .validation_results
2923 .informational
2924 .iter()
2925 .any(|status| {
2926 status.code == "com.encypher.resource.graphLimitExceeded"
2927 && status
2928 .details
2929 .as_ref()
2930 .and_then(|details| details.get("limit"))
2931 .and_then(serde_json::Value::as_str)
2932 == Some("nodes")
2933 })
2934 };
2935
2936 let jpeg = include_bytes!("../../../tests/fixtures/signed_test.jpg");
2937 let store = detached_manifest_evidence(jpeg, "image/jpeg")
2938 .expect("extract detached evidence")
2939 .expect("signed JPEG manifest")
2940 .manifest_store;
2941 let detached = verify_with_manifest_store_and_graph_inputs(
2942 jpeg,
2943 &store,
2944 "image/jpeg",
2945 &VerifyOptions::default(),
2946 limited(),
2947 )
2948 .expect("detached graph verification");
2949 assert!(
2950 has_node_limit(&detached),
2951 "{:?}",
2952 detached.validation_results
2953 );
2954
2955 let store_only = verify_manifest_store_only_with_graph_inputs(
2956 &store,
2957 &VerifyOptions::default(),
2958 limited(),
2959 )
2960 .expect("store-only graph verification");
2961 assert_eq!(store_only.report.hard_binding, "not_evaluated");
2962 assert!(has_node_limit(&store_only.report));
2963
2964 let mp4 = include_bytes!("../../../tests/fixtures/signed_test.mp4");
2965 let fragmented = verify_fragmented_with_graph_inputs(
2966 mp4,
2967 &[],
2968 "video/mp4",
2969 &VerifyOptions::default(),
2970 limited(),
2971 )
2972 .expect("fragmented graph verification");
2973 assert!(
2974 has_node_limit(&fragmented),
2975 "{:?}",
2976 fragmented.validation_results
2977 );
2978 }
2979
2980 #[test]
2981 fn unratified_hostless_store_is_not_in_public_profile() {
2982 let error = verify(b"jumb", "application/c2pa").unwrap_err();
2983 assert!(matches!(error, Error::UnsupportedMime(_)));
2984 }
2985
2986 #[test]
2987 fn bounded_reader_accepts_exact_limit_without_large_allocation() {
2988 let mut input = Cursor::new(b"1234");
2989 assert_eq!(read_bounded_file(&mut input, 4, 4).unwrap(), b"1234");
2990 }
2991
2992 #[test]
2993 fn bounded_reader_detects_growth_at_limit_plus_one() {
2994 let mut input = Cursor::new(b"12345");
2995 let error = read_bounded_file(&mut input, 4, 4).unwrap_err();
2996 assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
2997 assert!(error.to_string().contains("grew while being read"));
2998 }
2999
3000 #[test]
3001 fn unsupported_mime_has_stable_error_code() {
3002 let error = verify(b"not an asset", "application/x-unknown").unwrap_err();
3003 assert!(matches!(error, Error::UnsupportedMime(_)));
3004 assert_eq!(error.code(), "unsupported_mime");
3005 }
3006
3007 #[test]
3008 fn verify_file_resolves_options_before_opening_the_asset() {
3009 let path = std::env::temp_dir().join(format!(
3010 "encypher-c2pa-invalid-options-missing-{}.jpg",
3011 std::process::id()
3012 ));
3013 let options = VerifyOptions {
3014 no_default_trust: true,
3015 cawg_trust_configurations: Some(vec![CawgTrustConfiguration {
3016 profile: CawgTrustProfile::Base,
3017 certificates_pem: String::new(),
3018 not_before: None,
3019 not_after: None,
3020 }]),
3021 ..VerifyOptions::default()
3022 };
3023
3024 let error = verify_file(path, Some("image/jpeg"), &options)
3025 .expect_err("invalid trust material must win over a missing asset");
3026 assert_eq!(error.code(), "invalid_trust_material");
3027 assert!(
3028 error
3029 .to_string()
3030 .contains("cawg_trust_configurations[0].certificates_pem"),
3031 "{error}"
3032 );
3033 }
3034
3035 #[test]
3036 fn zip64_is_admitted_only_by_the_caller_selected_v2_5_policy() {
3037 let (archive, _) = crate::c2pa_formats::zip64_test_asset();
3038 for spec_version in [None, Some("2.4")] {
3039 let error = verify_with_options(
3040 &archive,
3041 "application/zip",
3042 &VerifyOptions {
3043 spec_version: spec_version.map(str::to_string),
3044 ..VerifyOptions::default()
3045 },
3046 )
3047 .expect_err("legacy policy must refuse ZIP64");
3048 assert!(
3049 matches!(error, Error::Verification(_)),
3050 "{spec_version:?} returned {error:?}"
3051 );
3052 }
3053
3054 let error = verify_with_options(
3055 &archive,
3056 "application/zip",
3057 &VerifyOptions {
3058 spec_version: Some("2.2".into()),
3059 ..VerifyOptions::default()
3060 },
3061 )
3062 .expect_err("2.2 must refuse a ZIP MIME outside its format set");
3063 assert!(
3064 matches!(&error, Error::UnsupportedMime(mime) if mime == "application/zip"),
3065 "2.2 returned {error:?}"
3066 );
3067
3068 let report = verify_with_options(
3069 &archive,
3070 "application/zip",
3071 &VerifyOptions {
3072 spec_version: Some("2.5".into()),
3073 ..VerifyOptions::default()
3074 },
3075 )
3076 .expect("2.5 ZIP64 verification returns a report");
3077 assert!(report.present, "2.5 did not reach the ZIP64 manifest");
3078 }
3079
3080 #[test]
3085 fn a_malformed_cawg_trust_configuration_fails_the_call_naming_its_entry() {
3086 let root = include_str!("default_trust/encypher-identity-root.pem");
3087 let resolve = |entry: serde_json::Value| {
3088 let options: VerifyOptions = serde_json::from_value(serde_json::json!({
3089 "no_default_trust": true,
3090 "cawg_trust_configurations": [
3091 { "profile": "base", "certificates_pem": root },
3092 entry,
3093 ],
3094 }))
3095 .map_err(|error| error.to_string())?;
3096 super::ResolvedOptions::resolve(&options)
3097 .map(|_| ())
3098 .map_err(|error| format!("{}: {error}", error.code()))
3099 };
3100
3101 for (entry, field) in [
3102 (
3103 serde_json::json!({ "profile": "smime_interim", "certificates_pem": "" }),
3104 "cawg_trust_configurations[1].certificates_pem",
3105 ),
3106 (
3107 serde_json::json!({ "profile": "smime_interim", "certificates_pem": root, "not_after": "next spring" }),
3108 "cawg_trust_configurations[1].not_after",
3109 ),
3110 (
3111 serde_json::json!({
3112 "profile": "smime_interim",
3113 "certificates_pem": root,
3114 "not_before": "2027-01-02T00:00:00Z",
3115 "not_after": "2027-01-01T00:00:00Z",
3116 }),
3117 "cawg_trust_configurations[1]",
3118 ),
3119 ] {
3120 let error = resolve(entry).expect_err("malformed configuration");
3121 assert!(error.starts_with("invalid_trust_material: "), "{error}");
3122 assert!(error.contains(field), "{error}");
3123 }
3124
3125 let unknown =
3126 resolve(serde_json::json!({ "profile": "trusted", "certificates_pem": root }))
3127 .expect_err("unknown profile");
3128 assert!(unknown.contains("unknown variant `trusted`"), "{unknown}");
3129 assert_eq!(
3130 resolve(serde_json::json!({ "profile": "smime_interim", "certificates_pem": root })),
3131 Ok(())
3132 );
3133 }
3134}