1#![forbid(unsafe_code)]
8#[path = "c2pa-cbor/lib.rs"]
47#[allow(dead_code, reason = "production kernel mirror")]
48mod c2pa_cbor;
49#[path = "c2pa-core/lib.rs"]
50#[allow(dead_code, unused_imports, reason = "production kernel mirror")]
51mod c2pa_core;
52#[path = "c2pa-crypto/lib.rs"]
53#[allow(dead_code, unused_imports, reason = "production kernel mirror")]
54mod c2pa_crypto;
55#[path = "c2pa-formats/lib.rs"]
56#[allow(dead_code, unused_imports, reason = "production kernel mirror")]
57mod c2pa_formats;
58#[path = "c2pa-trust/lib.rs"]
59#[allow(dead_code, unused_imports, reason = "production kernel mirror")]
60mod c2pa_trust;
61#[path = "c2pa-validate/lib.rs"]
62#[allow(dead_code, unused_imports, reason = "production kernel mirror")]
63mod c2pa_validate;
64mod default_trust;
65mod telemetry;
66mod telemetry_consent;
67pub use default_trust::SNAPSHOT_DATE as DEFAULT_TRUST_SNAPSHOT_DATE;
68
69pub use telemetry::{
70 validation_failure_telemetry, TelemetryOptions, ValidationFailureTelemetry,
71 DEFAULT_TELEMETRY_ENDPOINT,
72};
73pub use telemetry_consent::{
74 prompt_for_telemetry_consent, set_telemetry_enabled, telemetry_preference,
75 TelemetryPreferenceError,
76};
77
78use std::collections::HashMap;
79use std::fs::{self, OpenOptions};
80use std::io::{self, Read};
81#[cfg(unix)]
82use std::os::unix::fs::OpenOptionsExt;
83use std::path::Path;
84
85use crate::c2pa_core::{
86 spec::{canonicalize_mime, mimes_for_version},
87 EngineProfile, SpecVersion,
88};
89use crate::c2pa_trust::TrustList;
90use crate::c2pa_validate::{
91 verify_fragmented_with_cawg_trust_policy_did_documents_and_strict_encoding_safe as verify_fragmented_safe,
92 verify_with_cawg_trust_policy_did_documents_and_strict_encoding_safe as verify_safe,
93 StatusCode as CoreStatus, ValidationResults as CoreResults, VerifyInput,
94 ASSERTION_BMFF_HASH_MALFORMED, ASSERTION_BMFF_HASH_MATCH, ASSERTION_BMFF_HASH_MISMATCH,
95 ASSERTION_BOXES_HASH_MALFORMED, ASSERTION_BOXES_HASH_MATCH, ASSERTION_BOXES_HASH_MISMATCH,
96 ASSERTION_COLLECTION_HASH_MALFORMED, ASSERTION_COLLECTION_HASH_MATCH,
97 ASSERTION_COLLECTION_HASH_MISMATCH, ASSERTION_DATA_HASH_MATCH, ASSERTION_DATA_HASH_MISMATCH,
98 ASSERTION_MULTI_ASSET_HASH_MALFORMED, ASSERTION_MULTI_ASSET_HASH_MATCH,
99 ASSERTION_MULTI_ASSET_HASH_MISMATCH, CLAIM_HARD_BINDINGS_MISSING, CLAIM_SIGNATURE_MISMATCH,
100 CLAIM_SIGNATURE_MISSING, CLAIM_SIGNATURE_VALIDATED, SIGNING_CREDENTIAL_INVALID,
101 SIGNING_CREDENTIAL_OCSP_NOT_REVOKED, SIGNING_CREDENTIAL_OCSP_REVOKED,
102 SIGNING_CREDENTIAL_TRUSTED, SIGNING_CREDENTIAL_UNTRUSTED,
103};
104use serde::{Deserialize, Serialize};
105use serde_json::Value;
106use sha2::{Digest, Sha256};
107use time::{format_description::well_known::Rfc3339, OffsetDateTime};
108
109pub const REPORT_SCHEMA_VERSION: &str = "1.0";
110pub const C2PA_PROFILE: &str = "c2pa-2.4";
111const MAX_MANIFEST_STORE_BYTES: usize = 64 * 1024 * 1024;
112const MAX_PATH_ASSET_BYTES: u64 = 128 * 1024 * 1024;
113
114#[derive(Debug, Clone, Default, Deserialize)]
115#[serde(default, deny_unknown_fields)]
116pub struct VerifyOptions {
117 pub trust_pem: Option<String>,
119 pub tsa_trust_pem: Option<String>,
121 pub allowed_list_pem: Option<String>,
123 pub cawg_trust_pem: Option<String>,
127 pub cawg_allowed_certs_pem: Option<String>,
129 pub no_default_trust: bool,
133 pub cawg_did_documents: Option<HashMap<String, Value>>,
138 pub cawg_strict_encoding: bool,
144 pub strict_conformance: bool,
148 pub validation_time: Option<String>,
150 pub telemetry: TelemetryOptions,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
155pub struct VerificationStatus {
156 pub code: String,
157 pub url: String,
158 pub explanation: String,
159 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub details: Option<Value>,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
166pub struct ValidationResults {
167 pub success: Vec<VerificationStatus>,
168 pub informational: Vec<VerificationStatus>,
169 pub failure: Vec<VerificationStatus>,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
173pub struct RevocationReport {
174 pub status: String,
175 pub source: String,
176 pub responder_signature: String,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
180pub struct FreshnessReport {
181 pub status: String,
182 pub as_of: Option<String>,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
186pub struct TrustReport {
187 pub status: String,
188 pub basis: String,
189 pub validation_time: String,
190 pub revocation: RevocationReport,
191 pub freshness: FreshnessReport,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct VerificationReport {
196 pub schema_version: String,
197 pub profile: String,
198 pub mime_type: String,
199 pub present: bool,
200 pub integrity: String,
201 pub signature: String,
202 pub hard_binding: String,
203 pub trust: TrustReport,
204 pub policy: Option<Value>,
205 pub managed_receipt: Option<Value>,
206 pub validation_state: String,
207 pub validation_results: ValidationResults,
208 pub manifest_report: Value,
209 pub content_credentials: Option<Value>,
210}
211#[derive(Debug, Clone, PartialEq, Eq)]
218pub struct DetachedManifestEvidence {
219 pub manifest_store: Vec<u8>,
220 pub manifest_store_sha256: String,
221 pub carrier: Vec<u8>,
222}
223
224impl VerificationReport {
225 pub fn to_json(&self) -> Result<String, Error> {
226 serde_json::to_string(self).map_err(Error::Serialize)
227 }
228
229 pub fn to_pretty_json(&self) -> Result<String, Error> {
230 serde_json::to_string_pretty(self).map_err(Error::Serialize)
231 }
232
233 pub fn cawg_statuses(&self) -> Vec<&VerificationStatus> {
237 self.validation_results
238 .success
239 .iter()
240 .chain(&self.validation_results.informational)
241 .chain(&self.validation_results.failure)
242 .filter(|status| status.code.starts_with("cawg."))
243 .collect()
244 }
245}
246
247#[derive(Debug, thiserror::Error)]
248pub enum Error {
249 #[error("unsupported MIME type: {0}")]
250 UnsupportedMime(String),
251 #[error("invalid trust material: {0}")]
252 InvalidTrust(String),
253 #[error("invalid validation time: {0}")]
254 InvalidValidationTime(String),
255 #[error("verification failed: {0}")]
256 Verification(String),
257 #[error("could not read asset: {0}")]
258 Io(#[from] std::io::Error),
259 #[error(transparent)]
260 TelemetryPreference(#[from] TelemetryPreferenceError),
261 #[error("could not serialize report: {0}")]
262 Serialize(serde_json::Error),
263}
264
265impl Error {
266 pub fn code(&self) -> &'static str {
267 match self {
268 Self::UnsupportedMime(_) => "unsupported_mime",
269 Self::InvalidTrust(_) => "invalid_trust_material",
270 Self::InvalidValidationTime(_) => "invalid_validation_time",
271 Self::Verification(_) => "verification_error",
272 Self::Io(_) => "io_error",
273 Self::TelemetryPreference(_) => "telemetry_preference_error",
274 Self::Serialize(_) => "serialization_error",
275 }
276 }
277}
278
279pub fn verify(data: &[u8], mime_type: &str) -> Result<VerificationReport, Error> {
284 verify_with_options(data, mime_type, &VerifyOptions::default())
285}
286
287pub fn verify_fragmented(
293 init_segment: &[u8],
294 fragments: &[&[u8]],
295 mime_type: &str,
296) -> Result<VerificationReport, Error> {
297 verify_fragmented_with_options(
298 init_segment,
299 fragments,
300 mime_type,
301 &VerifyOptions::default(),
302 )
303}
304pub fn detached_manifest_evidence(
311 data: &[u8],
312 mime_type: &str,
313) -> Result<Option<DetachedManifestEvidence>, Error> {
314 let mime = canonicalize_mime(mime_type);
315 let format = crate::c2pa_formats::AssetFormat::from_mime(&mime)
316 .ok_or_else(|| Error::UnsupportedMime(mime.clone()))?;
317 if !crate::c2pa_formats::supports_hash_mode(&mime) {
318 return Ok(None);
319 }
320 let Some(manifest_store) = crate::c2pa_formats::extract_manifest(format, data)
321 .map_err(|error| Error::Verification(error.to_string()))?
322 else {
323 return Ok(None);
324 };
325 let spans = crate::c2pa_formats::compute_data_hash_exclusions(format, data)
326 .map_err(|error| Error::Verification(error.to_string()))?;
327 let [span] = spans.as_slice() else {
328 return Ok(None);
329 };
330 let end = span
331 .start
332 .checked_add(span.length)
333 .filter(|end| *end <= data.len())
334 .ok_or_else(|| Error::Verification("manifest carrier exceeds asset bounds".into()))?;
335 let manifest_store_sha256 = hex::encode(Sha256::digest(&manifest_store));
336 Ok(Some(DetachedManifestEvidence {
337 manifest_store,
338 manifest_store_sha256,
339 carrier: data[span.start..end].to_vec(),
340 }))
341}
342
343pub fn verify_with_options(
347 data: &[u8],
348 mime_type: &str,
349 options: &VerifyOptions,
350) -> Result<VerificationReport, Error> {
351 let telemetry_enabled = telemetry_consent::resolve_telemetry_enabled(options.telemetry.enabled);
352 let result = verify_with_options_inner(data, None, mime_type, options);
353 if let Some(event) = telemetry::validation_failure_telemetry_with_enabled(
354 mime_type,
355 &result,
356 &options.telemetry,
357 telemetry_enabled,
358 ) {
359 telemetry::enqueue(options.telemetry.endpoint(), event);
360 }
361 result
362}
363
364pub fn verify_fragmented_with_options(
370 init_segment: &[u8],
371 fragments: &[&[u8]],
372 mime_type: &str,
373 options: &VerifyOptions,
374) -> Result<VerificationReport, Error> {
375 let telemetry_enabled = telemetry_consent::resolve_telemetry_enabled(options.telemetry.enabled);
376 let mime = canonicalize_mime(mime_type);
377 let result = if crate::c2pa_formats::AssetFormat::from_mime(&mime)
378 == Some(crate::c2pa_formats::AssetFormat::Bmff)
379 {
380 verify_with_options_inner(init_segment, Some(fragments), &mime, options)
381 } else {
382 Err(Error::UnsupportedMime(mime))
383 };
384 if let Some(event) = telemetry::validation_failure_telemetry_with_enabled(
385 mime_type,
386 &result,
387 &options.telemetry,
388 telemetry_enabled,
389 ) {
390 telemetry::enqueue(options.telemetry.endpoint(), event);
391 }
392 result
393}
394
395fn verify_with_options_inner(
396 data: &[u8],
397 fragments: Option<&[&[u8]]>,
398 mime_type: &str,
399 options: &VerifyOptions,
400) -> Result<VerificationReport, Error> {
401 let mime = canonicalize_mime(mime_type);
402 if !mimes_for_version(SpecVersion::V2_4).contains(&mime.as_str())
403 || crate::c2pa_formats::AssetFormat::from_mime(&mime).is_none()
404 {
405 return Err(Error::UnsupportedMime(mime));
406 }
407
408 let use_defaults = !options.no_default_trust;
409 let claim_trust = resolve_trust(
410 options.trust_pem.as_deref(),
411 use_defaults.then(default_trust::claim_signing),
412 )?;
413 let tsa_trust = resolve_trust(
414 options.tsa_trust_pem.as_deref(),
415 use_defaults.then(default_trust::timestamp_authorities),
416 )?;
417 let allowed_certs = resolve_trust(
418 options.allowed_list_pem.as_deref(),
419 use_defaults.then(default_trust::allowed_claim_signers),
420 )?;
421 let cawg_trust = resolve_trust(
422 options.cawg_trust_pem.as_deref(),
423 use_defaults.then(default_trust::cawg_identity),
424 )?;
425 let cawg_allowed_certs = resolve_trust(
426 options.cawg_allowed_certs_pem.as_deref(),
427 use_defaults.then(default_trust::cawg_allowed_identities),
428 )?;
429 let validation_time = parse_validation_time(options.validation_time.as_deref())?;
430 let validation_time_text = validation_time
431 .format(&Rfc3339)
432 .map_err(|error| Error::InvalidValidationTime(error.to_string()))?;
433
434 let input = VerifyInput {
435 data,
436 mime: &mime,
437 claim_signer_trust: claim_trust.as_ref().map(ResolvedTrust::get),
438 tsa_trust: tsa_trust.as_ref().map(ResolvedTrust::get),
439 allowed_certs: allowed_certs.as_ref().map(ResolvedTrust::get),
440 validation_time: Some(validation_time),
441 profile: if options.strict_conformance {
442 EngineProfile::strict(SpecVersion::V2_4)
443 } else {
444 EngineProfile::GENEROUS
445 },
446 };
447 let cawg_trust = cawg_trust.as_ref().map(ResolvedTrust::get);
448 let cawg_allowed_certs = cawg_allowed_certs.as_ref().map(ResolvedTrust::get);
449 let output = match fragments {
450 Some(fragments) => verify_fragmented_safe(
451 &input,
452 fragments,
453 cawg_trust,
454 cawg_allowed_certs,
455 true,
456 options.cawg_did_documents.as_ref(),
457 options.cawg_strict_encoding,
458 ),
459 None => verify_safe(
460 &input,
461 cawg_trust,
462 cawg_allowed_certs,
463 true,
464 options.cawg_did_documents.as_ref(),
465 options.cawg_strict_encoding,
466 ),
467 }
468 .map_err(|error| match error {
469 crate::c2pa_validate::ValidateError::UnsupportedMime(value) => {
470 Error::UnsupportedMime(value)
471 }
472 other => Error::Verification(other.to_string()),
473 })?;
474
475 let present = output
476 .report_json
477 .pointer("/provenance_verdict/present")
478 .and_then(Value::as_bool)
479 .unwrap_or(false);
480 let integrity = output
481 .report_json
482 .pointer("/provenance_verdict/integrity")
483 .and_then(Value::as_str)
484 .unwrap_or(if present { "invalid" } else { "absent" })
485 .to_string();
486 let signature = signature_status(&output.results);
487 let hard_binding = hard_binding_status(&output.results);
488 let custom_claim_trust = options.trust_pem.is_some() || options.allowed_list_pem.is_some();
489 let trust_basis = match (use_defaults, custom_claim_trust) {
490 (true, true) => "bundled_and_caller_supplied_static_material",
491 (true, false) => "bundled_static_material",
492 (false, true) => "caller_supplied_static_material",
493 (false, false) => "none",
494 };
495 let trust = trust_report(&output.results, present, trust_basis, validation_time_text);
496
497 Ok(VerificationReport {
498 schema_version: REPORT_SCHEMA_VERSION.to_string(),
499 profile: C2PA_PROFILE.to_string(),
500 mime_type: mime,
501 present,
502 integrity,
503 signature,
504 hard_binding,
505 trust,
506 policy: None,
507 managed_receipt: None,
508 validation_state: output.validation_state.as_str().to_string(),
509 validation_results: copy_results(&output.results),
510 manifest_report: output.report_json,
511 content_credentials: output.crjson,
512 })
513}
514
515pub fn verify_file(
520 path: impl AsRef<Path>,
521 mime_type: Option<&str>,
522 options: &VerifyOptions,
523) -> Result<VerificationReport, Error> {
524 let path = path.as_ref();
525 let mime = match mime_type {
526 Some(value) => value.to_string(),
527 None => mime_from_path(path)
528 .ok_or_else(|| Error::UnsupportedMime(path.display().to_string()))?
529 .to_string(),
530 };
531 let data = read_path_asset(path, MAX_PATH_ASSET_BYTES)?;
532 verify_with_options(&data, &mime, options)
533}
534
535fn read_path_asset(path: &Path, limit: u64) -> io::Result<Vec<u8>> {
536 let mut options = OpenOptions::new();
537 options.read(true);
538 #[cfg(unix)]
539 options.custom_flags(libc::O_NONBLOCK | libc::O_CLOEXEC);
540
541 let mut file = options.open(path)?;
544 let opened_metadata = file.metadata()?;
545 validate_path_asset(path, &opened_metadata, limit)?;
546 read_bounded_file(&mut file, opened_metadata.len(), limit)
547}
548
549fn validate_path_asset(path: &Path, metadata: &fs::Metadata, limit: u64) -> io::Result<()> {
550 if !metadata.is_file() {
551 return Err(io::Error::new(
552 io::ErrorKind::InvalidInput,
553 format!("asset path is not a regular file: {}", path.display()),
554 ));
555 }
556 if metadata.len() > limit {
557 return Err(io::Error::new(
558 io::ErrorKind::InvalidData,
559 format!("asset exceeds the 128 MiB path limit: {}", path.display()),
560 ));
561 }
562 Ok(())
563}
564
565fn read_bounded_file<R: Read>(file: &mut R, expected_len: u64, limit: u64) -> io::Result<Vec<u8>> {
566 if expected_len > limit {
567 return Err(io::Error::new(
568 io::ErrorKind::InvalidData,
569 "asset exceeds the path size limit",
570 ));
571 }
572 let expected_len = usize::try_from(expected_len)
573 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "asset size is not addressable"))?;
574 let mut data = vec![0_u8; expected_len + 1];
575 let mut used = 0;
576 while used < expected_len {
577 let read = file.read(&mut data[used..expected_len])?;
578 if read == 0 {
579 break;
580 }
581 used += read;
582 }
583 if used == expected_len && file.read(&mut data[expected_len..expected_len + 1])? != 0 {
584 return Err(io::Error::new(
585 io::ErrorKind::InvalidData,
586 "asset grew while being read",
587 ));
588 }
589 data.truncate(used);
590 Ok(data)
591}
592
593pub fn supported_mime_types() -> Vec<&'static str> {
595 let mut mimes: Vec<_> = mimes_for_version(SpecVersion::V2_4)
596 .into_iter()
597 .filter(|mime| crate::c2pa_formats::AssetFormat::from_mime(mime).is_some())
598 .collect();
599 mimes.sort_unstable();
600 mimes.dedup();
601 mimes
602}
603
604pub const SUPPORTED_EXTENSIONS: &[(&str, &str)] = &[
612 ("jpg", "image/jpeg"),
613 ("jpeg", "image/jpeg"),
614 ("png", "image/png"),
615 ("webp", "image/webp"),
616 ("gif", "image/gif"),
617 ("tif", "image/tiff"),
618 ("tiff", "image/tiff"),
619 ("dng", "image/x-adobe-dng"),
620 ("heic", "image/heic"),
621 ("heif", "image/heif"),
622 ("avif", "image/avif"),
623 ("jxl", "image/jxl"),
624 ("svg", "image/svg+xml"),
625 ("mp4", "video/mp4"),
626 ("m4v", "video/mp4"),
627 ("mov", "video/quicktime"),
628 ("avi", "video/x-msvideo"),
629 ("wav", "audio/wav"),
630 ("mp3", "audio/mpeg"),
631 ("m4a", "audio/mp4"),
632 ("aac", "audio/aac"),
633 ("flac", "audio/flac"),
634 ("ogg", "audio/ogg"),
635 ("oga", "audio/ogg"),
636 ("pdf", "application/pdf"),
637 ("epub", "application/epub+zip"),
638 (
639 "docx",
640 "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
641 ),
642 (
643 "xlsx",
644 "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
645 ),
646 (
647 "pptx",
648 "application/vnd.openxmlformats-officedocument.presentationml.presentation",
649 ),
650 ("odt", "application/vnd.oasis.opendocument.text"),
651 ("odg", "application/vnd.oasis.opendocument.graphics"),
652 ("ttf", "font/ttf"),
653 ("otf", "font/otf"),
654 ("txt", "text/plain"),
655 ("tsv", "text/tab-separated-values"),
656];
657
658pub fn mime_from_path(path: &Path) -> Option<&'static str> {
660 let extension = path.extension()?.to_str()?.to_ascii_lowercase();
661 SUPPORTED_EXTENSIONS
662 .iter()
663 .find(|(candidate, _)| *candidate == extension)
664 .map(|(_, mime)| *mime)
665}
666
667enum ResolvedTrust {
668 Bundled(&'static TrustList),
669 Owned(TrustList),
670}
671
672impl ResolvedTrust {
673 fn get(&self) -> &TrustList {
674 match self {
675 Self::Bundled(trust) => trust,
676 Self::Owned(trust) => trust,
677 }
678 }
679}
680
681fn resolve_trust(
682 custom_pem: Option<&str>,
683 bundled: Option<&'static TrustList>,
684) -> Result<Option<ResolvedTrust>, Error> {
685 let custom = custom_pem
686 .map(TrustList::from_pem)
687 .transpose()
688 .map_err(|error| Error::InvalidTrust(error.to_string()))?;
689 match (bundled, custom) {
690 (None, None) => Ok(None),
691 (Some(trust), None) => Ok(Some(ResolvedTrust::Bundled(trust))),
692 (None, Some(trust)) => Ok(Some(ResolvedTrust::Owned(trust))),
693 (Some(bundled), Some(custom)) => {
694 let mut merged = bundled.clone();
695 merged.anchors.extend(custom.anchors);
696 Ok(Some(ResolvedTrust::Owned(merged)))
697 }
698 }
699}
700
701fn parse_validation_time(value: Option<&str>) -> Result<OffsetDateTime, Error> {
702 match value {
703 Some(raw) => OffsetDateTime::parse(raw, &Rfc3339)
704 .map_err(|error| Error::InvalidValidationTime(error.to_string())),
705 None => Ok(OffsetDateTime::now_utc()),
706 }
707}
708
709fn copy_status(status: &CoreStatus) -> VerificationStatus {
710 VerificationStatus {
711 code: status.code.clone(),
712 url: status.url.clone(),
713 explanation: status.explanation.clone(),
714 details: status.details.clone(),
715 }
716}
717
718fn copy_results(results: &CoreResults) -> ValidationResults {
719 ValidationResults {
720 success: results.success.iter().map(copy_status).collect(),
721 informational: results.informational.iter().map(copy_status).collect(),
722 failure: results.failure.iter().map(copy_status).collect(),
723 }
724}
725
726fn signature_status(results: &CoreResults) -> String {
727 if results.has_success(CLAIM_SIGNATURE_VALIDATED) {
728 "valid"
729 } else if results.has_failure(CLAIM_SIGNATURE_MISMATCH) {
730 "invalid"
731 } else if results.has_failure(CLAIM_SIGNATURE_MISSING) {
732 "missing"
733 } else {
734 "unknown"
735 }
736 .to_string()
737}
738
739fn hard_binding_status(results: &CoreResults) -> String {
740 const MATCHES: &[&str] = &[
741 ASSERTION_DATA_HASH_MATCH,
742 ASSERTION_BMFF_HASH_MATCH,
743 ASSERTION_BOXES_HASH_MATCH,
744 ASSERTION_COLLECTION_HASH_MATCH,
745 ASSERTION_MULTI_ASSET_HASH_MATCH,
746 ];
747 const FAILURES: &[&str] = &[
748 ASSERTION_DATA_HASH_MISMATCH,
749 ASSERTION_BMFF_HASH_MISMATCH,
750 ASSERTION_BMFF_HASH_MALFORMED,
751 ASSERTION_BOXES_HASH_MISMATCH,
752 ASSERTION_BOXES_HASH_MALFORMED,
753 ASSERTION_COLLECTION_HASH_MISMATCH,
754 ASSERTION_COLLECTION_HASH_MALFORMED,
755 ASSERTION_MULTI_ASSET_HASH_MISMATCH,
756 ASSERTION_MULTI_ASSET_HASH_MALFORMED,
757 ];
758 if MATCHES.iter().any(|code| results.has_success(code)) {
759 "match"
760 } else if FAILURES.iter().any(|code| results.has_failure(code)) {
761 "mismatch"
762 } else if results.has_failure(CLAIM_HARD_BINDINGS_MISSING) {
763 "missing"
764 } else {
765 "unknown"
766 }
767 .to_string()
768}
769
770fn trust_report(
771 results: &CoreResults,
772 present: bool,
773 basis: &str,
774 validation_time: String,
775) -> TrustReport {
776 let supplied = basis != "none";
777 let trusted = results.has_success(SIGNING_CREDENTIAL_TRUSTED);
778 let rejected = results.has_failure(SIGNING_CREDENTIAL_UNTRUSTED)
779 || results.has_failure(SIGNING_CREDENTIAL_INVALID);
780 let revoked = results.has_failure(SIGNING_CREDENTIAL_OCSP_REVOKED);
781 let not_revoked = results.has_success(SIGNING_CREDENTIAL_OCSP_NOT_REVOKED);
782 TrustReport {
783 status: if trusted && !revoked {
784 "valid_for_supplied_material"
785 } else if present && supplied && (rejected || revoked) {
786 "not_valid_for_supplied_material"
787 } else {
788 "not_evaluated"
789 }
790 .to_string(),
791 basis: basis.to_string(),
792 validation_time,
793 revocation: RevocationReport {
794 status: if revoked {
795 "revoked"
796 } else if not_revoked {
797 "not_revoked"
798 } else {
799 "not_checked"
800 }
801 .to_string(),
802 source: if revoked || not_revoked {
803 "embedded_ocsp"
804 } else {
805 "none"
806 }
807 .to_string(),
808 responder_signature: if revoked || not_revoked {
809 "valid"
810 } else {
811 "not_applicable"
812 }
813 .to_string(),
814 },
815 freshness: FreshnessReport {
816 status: "unknown".to_string(),
817 as_of: None,
818 },
819 }
820}
821
822#[cfg(test)]
823mod tests {
824 use super::{
825 mime_from_path, read_bounded_file, supported_mime_types, verify, verify_fragmented,
826 verify_with_options, Error, VerifyOptions,
827 };
828 use std::{io::Cursor, path::Path};
829
830 #[test]
831 fn known_filename_maps_to_mime() {
832 assert_eq!(
833 mime_from_path(Path::new("composition.MP4")),
834 Some("video/mp4")
835 );
836
837 assert_eq!(
838 mime_from_path(Path::new("drawing.odg")),
839 Some("application/vnd.oasis.opendocument.graphics")
840 );
841 assert_eq!(
842 mime_from_path(Path::new("data.tsv")),
843 Some("text/tab-separated-values")
844 );
845 }
846
847 #[test]
848 fn format_list_is_sorted_and_contains_composition_formats() {
849 let formats = supported_mime_types();
850 assert_eq!(formats.len(), 71);
851 assert!(formats.windows(2).all(|pair| pair[0] < pair[1]));
852 assert!(formats.contains(&"video/mp4"));
853 assert!(formats.contains(&"image/jpeg"));
854 assert!(formats.contains(&"text/tab-separated-values"));
855 assert!(formats.contains(&"application/vnd.oasis.opendocument.graphics"));
856 }
857
858 #[test]
859 fn input_alias_is_reported_as_canonical_mime() {
860 let asset = b"\x00\x00\x00\x10ftypisom\x00\x00\x00\x00";
861 let report = verify(asset, "audio/aac; codecs=mp4a.40.2").unwrap();
862 assert_eq!(report.mime_type, "audio/mp4");
863 assert!(!report.present);
864 }
865
866 #[test]
867 fn strict_conformance_option_selects_the_program_profile() {
868 let asset = include_bytes!("../../../tests/fixtures/signed_test.jpg");
869 let report = verify_with_options(
870 asset,
871 "image/jpeg",
872 &VerifyOptions {
873 strict_conformance: true,
874 ..VerifyOptions::default()
875 },
876 )
877 .unwrap();
878 assert_eq!(
879 report.manifest_report["engine_profile"]["operating_mode"],
880 "conformance"
881 );
882 assert_eq!(
883 report.manifest_report["engine_profile"]["compliance_level"],
884 "conformance-program"
885 );
886 }
887
888 #[test]
889 fn fragmented_entry_point_rejects_non_bmff_mime() {
890 let error = verify_fragmented(
891 include_bytes!("../../../tests/fixtures/signed_test.jpg"),
892 &[b"ignored fragment"],
893 "image/jpeg",
894 )
895 .unwrap_err();
896 assert!(matches!(error, Error::UnsupportedMime(mime) if mime == "image/jpeg"));
897 }
898
899 #[test]
900 fn unratified_hostless_store_is_not_in_public_profile() {
901 let error = verify(b"jumb", "application/c2pa").unwrap_err();
902 assert!(matches!(error, Error::UnsupportedMime(_)));
903 }
904
905 #[test]
906 fn bounded_reader_accepts_exact_limit_without_large_allocation() {
907 let mut input = Cursor::new(b"1234");
908 assert_eq!(read_bounded_file(&mut input, 4, 4).unwrap(), b"1234");
909 }
910
911 #[test]
912 fn bounded_reader_detects_growth_at_limit_plus_one() {
913 let mut input = Cursor::new(b"12345");
914 let error = read_bounded_file(&mut input, 4, 4).unwrap_err();
915 assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
916 assert!(error.to_string().contains("grew while being read"));
917 }
918
919 #[test]
920 fn unsupported_mime_has_stable_error_code() {
921 let error = verify(b"not an asset", "application/x-unknown").unwrap_err();
922 assert!(matches!(error, Error::UnsupportedMime(_)));
923 assert_eq!(error.code(), "unsupported_mime");
924 }
925}