diff --git a/sdk/src/validation_results.rs b/sdk/src/validation_results.rs index 497f4d9d2..82cb02ff5 100644 --- a/sdk/src/validation_results.rs +++ b/sdk/src/validation_results.rs @@ -11,8 +11,6 @@ // specific language governing permissions and limitations under // each license. -use std::collections::HashSet; - #[cfg(feature = "json_schema")] use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -210,30 +208,46 @@ impl ValidationResults { /// [ยง14.3. Validation states]: https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_validation_states pub fn validation_state(&self) -> ValidationState { if let Some(active_manifest) = self.active_manifest.as_ref() { - let success_codes: HashSet<&str> = active_manifest + // https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_valid_manifest + let is_valid = active_manifest + // First check if the claim is valid and the certificate hasn't expired. .success() .iter() - .map(|status| status.code()) - .collect(); - let failure_codes = active_manifest.failure(); - let ingredient_failure = self.ingredient_deltas.as_ref().is_some_and(|deltas| { - deltas - .iter() - .any(|idv| !idv.validation_deltas().failure().is_empty()) - }); - - // https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_valid_manifest - let is_valid = success_codes.contains(validation_status::CLAIM_SIGNATURE_VALIDATED) - && success_codes.contains(validation_status::CLAIM_SIGNATURE_INSIDE_VALIDITY) - && (failure_codes.is_empty() - || failure_codes.iter().all(|status| { + .any(|status| status.code() == validation_status::CLAIM_SIGNATURE_VALIDATED) + && active_manifest.success().iter().any(|status| { + status.code() == validation_status::CLAIM_SIGNATURE_INSIDE_VALIDITY + }) + // Then check if the manifest contains either no failures or that it's only untrusted. + && (active_manifest.failure().is_empty() + || active_manifest.failure().iter().all(|status| { status.code() == validation_status::SIGNING_CREDENTIAL_UNTRUSTED })) - && !ingredient_failure; + // Finally check if the ingredients contain either no failures or the only failure is + // that the ingredient is untrusted. + && self.ingredient_deltas.as_ref().iter().all(|deltas| { + deltas.iter().all(|idv| { + let deltas = idv.validation_deltas(); + deltas.failure().is_empty() + || deltas.failure().iter().all(|status| { + status.code() == validation_status::SIGNING_CREDENTIAL_UNTRUSTED + }) + }) + }); // https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_trusted_manifest - let is_trusted = success_codes.contains(validation_status::SIGNING_CREDENTIAL_TRUSTED) - && failure_codes.is_empty() + let is_trusted = active_manifest + // First check if the signing certificate is trusted. + .success() + .iter() + .any(|status| status.code() == validation_status::SIGNING_CREDENTIAL_TRUSTED) + // Then check that there are no errors. + && active_manifest.failure().is_empty() + // Finally check if the ingredients contain no failures. + && self.ingredient_deltas.as_ref().iter().all(|deltas| { + deltas.iter().all(|idv| { + idv.validation_deltas().failure().is_empty() + }) + }) && is_valid; if is_trusted { @@ -950,3 +964,229 @@ pub mod validation_codes { } } } + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::validation_status::{ + ASSERTION_DATAHASH_MISMATCH, CLAIM_MALFORMED, CLAIM_SIGNATURE_INSIDE_VALIDITY, + CLAIM_SIGNATURE_VALIDATED, SIGNING_CREDENTIAL_TRUSTED, SIGNING_CREDENTIAL_UNTRUSTED, + }; + + #[test] + fn trusted_state() { + let mut validation_results = ValidationResults::default(); + + validation_results.add_status( + ValidationStatus::new(CLAIM_SIGNATURE_VALIDATED).set_kind(LogKind::Success), + ); + validation_results.add_status( + ValidationStatus::new(CLAIM_SIGNATURE_INSIDE_VALIDITY).set_kind(LogKind::Success), + ); + validation_results.add_status( + ValidationStatus::new(SIGNING_CREDENTIAL_TRUSTED).set_kind(LogKind::Success), + ); + + assert_eq!( + validation_results.validation_state(), + ValidationState::Trusted + ); + } + + #[test] + fn not_trusted_state_with_failure() { + let mut validation_results = ValidationResults::default(); + + validation_results.add_status( + ValidationStatus::new(CLAIM_SIGNATURE_VALIDATED).set_kind(LogKind::Success), + ); + validation_results.add_status( + ValidationStatus::new(CLAIM_SIGNATURE_INSIDE_VALIDITY).set_kind(LogKind::Success), + ); + validation_results.add_status( + ValidationStatus::new(SIGNING_CREDENTIAL_TRUSTED).set_kind(LogKind::Success), + ); + + validation_results.add_status(ValidationStatus::new_failure(SIGNING_CREDENTIAL_UNTRUSTED)); + + assert_eq!( + validation_results.validation_state(), + ValidationState::Valid + ); + } + + #[test] + fn not_trusted_state_with_failure_delta() { + let mut validation_results = ValidationResults::default(); + + validation_results.add_status( + ValidationStatus::new(CLAIM_SIGNATURE_VALIDATED).set_kind(LogKind::Success), + ); + validation_results.add_status( + ValidationStatus::new(CLAIM_SIGNATURE_INSIDE_VALIDITY).set_kind(LogKind::Success), + ); + validation_results.add_status( + ValidationStatus::new(SIGNING_CREDENTIAL_TRUSTED).set_kind(LogKind::Success), + ); + + validation_results.add_status( + ValidationStatus::new_failure(SIGNING_CREDENTIAL_UNTRUSTED).set_ingredient_uri("1"), + ); + + assert_eq!( + validation_results.validation_state(), + ValidationState::Valid + ); + } + + #[test] + fn valid_state() { + let mut validation_results = ValidationResults::default(); + + validation_results.add_status( + ValidationStatus::new(CLAIM_SIGNATURE_VALIDATED).set_kind(LogKind::Success), + ); + validation_results.add_status( + ValidationStatus::new(CLAIM_SIGNATURE_INSIDE_VALIDITY).set_kind(LogKind::Success), + ); + + assert_eq!( + validation_results.validation_state(), + ValidationState::Valid + ); + } + + #[test] + fn valid_state_with_untrusted_delta() { + let mut validation_results = ValidationResults::default(); + + validation_results.add_status( + ValidationStatus::new(CLAIM_SIGNATURE_VALIDATED).set_kind(LogKind::Success), + ); + validation_results.add_status( + ValidationStatus::new(CLAIM_SIGNATURE_INSIDE_VALIDITY).set_kind(LogKind::Success), + ); + + validation_results.add_status( + ValidationStatus::new_failure(SIGNING_CREDENTIAL_UNTRUSTED).set_ingredient_uri("1"), + ); + + assert_eq!( + validation_results.validation_state(), + ValidationState::Valid + ); + } + + #[test] + fn not_valid_state_with_failure() { + let mut validation_results = ValidationResults::default(); + + validation_results.add_status( + ValidationStatus::new(CLAIM_SIGNATURE_VALIDATED).set_kind(LogKind::Success), + ); + validation_results.add_status( + ValidationStatus::new(CLAIM_SIGNATURE_INSIDE_VALIDITY).set_kind(LogKind::Success), + ); + + validation_results.add_status(ValidationStatus::new_failure(CLAIM_MALFORMED)); + + assert_eq!( + validation_results.validation_state(), + ValidationState::Invalid + ); + } + + #[test] + fn valid_state_with_failure_delta_and_untrusted_delta() { + let mut validation_results = ValidationResults::default(); + + validation_results.add_status( + ValidationStatus::new(CLAIM_SIGNATURE_VALIDATED).set_kind(LogKind::Success), + ); + validation_results.add_status( + ValidationStatus::new(CLAIM_SIGNATURE_INSIDE_VALIDITY).set_kind(LogKind::Success), + ); + + validation_results.add_status( + ValidationStatus::new_failure(SIGNING_CREDENTIAL_UNTRUSTED).set_ingredient_uri("1"), + ); + validation_results.add_status( + ValidationStatus::new_failure(ASSERTION_DATAHASH_MISMATCH).set_ingredient_uri("1"), + ); + + assert_eq!( + validation_results.validation_state(), + ValidationState::Invalid + ); + } + + #[test] + fn not_valid_state_with_failure_delta() { + let mut validation_results = ValidationResults::default(); + + validation_results.add_status( + ValidationStatus::new(CLAIM_SIGNATURE_VALIDATED).set_kind(LogKind::Success), + ); + validation_results.add_status( + ValidationStatus::new(CLAIM_SIGNATURE_INSIDE_VALIDITY).set_kind(LogKind::Success), + ); + + validation_results.add_status( + ValidationStatus::new_failure(ASSERTION_DATAHASH_MISMATCH).set_ingredient_uri("1"), + ); + + assert_eq!( + validation_results.validation_state(), + ValidationState::Invalid + ); + } + + #[test] + fn not_valid_state_with_no_inside_validity() { + let mut validation_results = ValidationResults::default(); + + validation_results.add_status( + ValidationStatus::new(CLAIM_SIGNATURE_VALIDATED).set_kind(LogKind::Success), + ); + + assert_eq!( + validation_results.validation_state(), + ValidationState::Invalid + ); + } + + #[test] + fn not_valid_state_with_no_validated() { + let mut validation_results = ValidationResults::default(); + + validation_results.add_status( + ValidationStatus::new(CLAIM_SIGNATURE_INSIDE_VALIDITY).set_kind(LogKind::Success), + ); + + assert_eq!( + validation_results.validation_state(), + ValidationState::Invalid + ); + } + + #[test] + fn invalid_state() { + let mut validation_results = ValidationResults::default(); + + validation_results.add_status(ValidationStatus::new_failure(ASSERTION_DATAHASH_MISMATCH)); + + assert_eq!( + validation_results.validation_state(), + ValidationState::Invalid + ); + } + + #[test] + fn invalid_state_with_nothing() { + let validation_results = ValidationResults::default(); + assert_eq!( + validation_results.validation_state(), + ValidationState::Invalid + ); + } +}