From a6d38237636f3812e5e6f75787f3bdf9d626a08b Mon Sep 17 00:00:00 2001 From: ok-nick Date: Thu, 16 Oct 2025 09:46:33 -0400 Subject: [PATCH 01/12] feat: add well-formed validation state --- sdk/src/validation_results.rs | 107 ++++++++++++++++++++++++++-------- 1 file changed, 84 insertions(+), 23 deletions(-) diff --git a/sdk/src/validation_results.rs b/sdk/src/validation_results.rs index fdfa0daff..a2c204985 100644 --- a/sdk/src/validation_results.rs +++ b/sdk/src/validation_results.rs @@ -11,6 +11,8 @@ // specific language governing permissions and limitations under // each license. +use std::collections::HashSet; + #[cfg(feature = "json_schema")] use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -21,7 +23,7 @@ use crate::{ jumbf::labels::manifest_label_from_uri, status_tracker::{LogKind, StatusTracker}, store::Store, - validation_status::{log_kind, ValidationStatus}, + validation_status::{self, log_kind, ValidationStatus}, }; #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -30,11 +32,28 @@ use crate::{ /// /// The Trusted state implies the manifest store is valid and the active signature is trusted. pub enum ValidationState { - /// Errors were found in the manifest store. + // REVIEW-NOTE: A "WellFormed" manifest is invalid, should we rename this to "Malformed?" + /// The manfiest fails to meet [ValidationState::WellFormed] requirements, meaning it cannot + /// even be parsed or its basic structure is non-compliant. Invalid, - /// No errors were found in validation, but the active signature is not trusted. + /// The manifest follows all required structural and syntactic rules in the C2PA spec. + /// + /// See [§14.3.4. Well-Formed Manifest]. + /// + /// [§14.3.4. Well-Formed Manifest]: https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_well_formed_manifest + WellFormed, + /// The manifest is well-formed and the cryptographic integrity checks succeed. + /// + /// See [§14.3.5. Valid Manifest]. + /// + /// [§14.3.5. Valid Manifest]: https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_valid_manifest Valid, - /// The manifest store is valid and the active signature is trusted. + /// The manifest is valid and signed by a certificate that chains up to a trusted root or known + /// authority in the trust list. + /// + /// See [§14.3.6. Trusted Manifest]. + /// + /// [§14.3.6. Trusted Manifest]: https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_trusted_manifest Trusted, } @@ -179,30 +198,72 @@ impl ValidationResults { results } - /// Returns the [ValidationState] of the manifest store based on the validation results. + /// Returns the [ValidationState] of the manifest based on the validation results. + /// + /// See [§14.3. Validation states]. + /// + /// [§14.3. Validation states]: https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_validation_states pub fn validation_state(&self) -> ValidationState { - let mut is_trusted = true; // Assume the state is trusted until proven otherwise if let Some(active_manifest) = self.active_manifest.as_ref() { - if !active_manifest.failure().is_empty() { - return ValidationState::Invalid; - } - // There must be a trusted credential in the active manifest for the state to be trusted - is_trusted = active_manifest.success().iter().any(|status| { - status.code() == crate::validation_status::SIGNING_CREDENTIAL_TRUSTED + let success_codes: HashSet<&str> = active_manifest + .success() + .iter() + .map(|status| status.code()) + .collect(); + let failure_codes: HashSet<&str> = active_manifest + .failure() + .iter() + .map(|status| status.code()) + .collect(); + // let ingredient_failure_codes: HashSet<&str> = self + // .ingredient_deltas + // .as_ref() + // .map(|deltas| { + // deltas + // .iter() + // .flat_map(|idv| idv.validation_deltas().failure()) + // .map(|status| status.code()) + // .collect() + // }) + // .unwrap_or_default(); + let ingredient_failure = self.ingredient_deltas.as_ref().is_some_and(|deltas| { + deltas + .iter() + .any(|idv| !idv.validation_deltas().failure().is_empty()) }); - } - if let Some(ingredient_deltas) = self.ingredient_deltas.as_ref() { - for idv in ingredient_deltas.iter() { - if !idv.validation_deltas().failure().is_empty() { - return ValidationState::Invalid; - } + + let is_trusted = success_codes.contains(validation_status::SIGNING_CREDENTIAL_TRUSTED) + && success_codes.contains(validation_status::CLAIM_SIGNATURE_VALIDATED) + && success_codes.contains(validation_status::CLAIM_SIGNATURE_INSIDE_VALIDITY) + && failure_codes.is_empty() + && !ingredient_failure; + if is_trusted { + return ValidationState::Trusted; + } + + let is_valid = success_codes.contains(validation_status::CLAIM_SIGNATURE_VALIDATED) + && success_codes.contains(validation_status::CLAIM_SIGNATURE_INSIDE_VALIDITY) + && failure_codes.len() == 1 + && failure_codes.contains(validation_status::SIGNING_CREDENTIAL_UNTRUSTED) + && !ingredient_failure; + if is_valid { + return ValidationState::Valid; + } + + let is_well_formed = (failure_codes.is_empty() + || failure_codes.iter().all(|&code| { + code == validation_status::SIGNING_CREDENTIAL_OCSP_UNKNOWN + || code == validation_status::SIGNING_CREDENTIAL_REVOKED + || code == validation_status::SIGNING_CREDENTIAL_UNTRUSTED + })) + // TODO: what codes to ignore here? + && !ingredient_failure; + if is_well_formed { + return ValidationState::WellFormed; } } - if is_trusted { - ValidationState::Trusted - } else { - ValidationState::Valid - } + + ValidationState::Invalid } /// Returns a list of all validation errors in [ValidationResults]. From 75bc108f3cdfbc200e8b8230e8b3eee538da3979 Mon Sep 17 00:00:00 2001 From: ok-nick Date: Thu, 16 Oct 2025 10:05:37 -0400 Subject: [PATCH 02/12] docs: clarify validation state and manifest stores --- sdk/src/validation_results.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/sdk/src/validation_results.rs b/sdk/src/validation_results.rs index a2c204985..a18b896fd 100644 --- a/sdk/src/validation_results.rs +++ b/sdk/src/validation_results.rs @@ -28,27 +28,30 @@ use crate::{ #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "json_schema", derive(JsonSchema))] -/// Indicates if the manifest store is valid and trusted. +/// Represents the levels of assurance a manifest store achives when evaluated against the C2PA +/// specifications structural, cryptographic, and trust requirements. /// -/// The Trusted state implies the manifest store is valid and the active signature is trusted. +/// See [§14.3. Validation states]. +/// +/// [§14.3. Validation states]: https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_validation_states pub enum ValidationState { // REVIEW-NOTE: A "WellFormed" manifest is invalid, should we rename this to "Malformed?" - /// The manfiest fails to meet [ValidationState::WellFormed] requirements, meaning it cannot + /// The manifest store fails to meet [ValidationState::WellFormed] requirements, meaning it cannot /// even be parsed or its basic structure is non-compliant. Invalid, - /// The manifest follows all required structural and syntactic rules in the C2PA spec. + /// The manifest store follows all required structural and syntactic rules in the C2PA specification. /// /// See [§14.3.4. Well-Formed Manifest]. /// /// [§14.3.4. Well-Formed Manifest]: https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_well_formed_manifest WellFormed, - /// The manifest is well-formed and the cryptographic integrity checks succeed. + /// The manifest store is well-formed and the cryptographic integrity checks succeed. /// /// See [§14.3.5. Valid Manifest]. /// /// [§14.3.5. Valid Manifest]: https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_valid_manifest Valid, - /// The manifest is valid and signed by a certificate that chains up to a trusted root or known + /// The manifest store is valid and signed by a certificate that chains up to a trusted root or known /// authority in the trust list. /// /// See [§14.3.6. Trusted Manifest]. @@ -198,7 +201,7 @@ impl ValidationResults { results } - /// Returns the [ValidationState] of the manifest based on the validation results. + /// Returns the [ValidationState] of the manifest store based on the validation results. /// /// See [§14.3. Validation states]. /// From 10f0679d93c6b499fcb4c8896763985efa375c2c Mon Sep 17 00:00:00 2001 From: ok-nick Date: Thu, 16 Oct 2025 16:03:23 -0400 Subject: [PATCH 03/12] fix: rename invalid to malformed --- sdk/src/builder.rs | 17 +++++++++++++---- sdk/src/ingredient.rs | 2 +- sdk/src/reader.rs | 4 ++-- sdk/src/store.rs | 2 +- sdk/src/validation_results.rs | 5 ++--- sdk/tests/test_builder.rs | 2 +- 6 files changed, 20 insertions(+), 12 deletions(-) diff --git a/sdk/src/builder.rs b/sdk/src/builder.rs index 38bc88671..23992b969 100644 --- a/sdk/src/builder.rs +++ b/sdk/src/builder.rs @@ -1895,7 +1895,10 @@ mod tests { let manifest_store = Reader::from_stream(format, &mut dest).expect("from_bytes"); println!("{manifest_store}"); - assert_ne!(manifest_store.validation_state(), ValidationState::Invalid); + assert_ne!( + manifest_store.validation_state(), + ValidationState::Malformed + ); assert!(manifest_store.active_manifest().is_some()); let manifest = manifest_store.active_manifest().unwrap(); assert_eq!(manifest.title().unwrap(), "Test_Manifest"); @@ -2355,7 +2358,10 @@ mod tests { let manifest_store = Reader::from_file(&dest).expect("from_bytes"); println!("{manifest_store}"); - assert_ne!(manifest_store.validation_state(), ValidationState::Invalid); + assert_ne!( + manifest_store.validation_state(), + ValidationState::Malformed + ); assert_eq!(manifest_store.validation_status(), None); assert_eq!( manifest_store.active_manifest().unwrap().title().unwrap(), @@ -2413,7 +2419,10 @@ mod tests { //println!("{}", manifest_store); if format != "c2pa" { // c2pa files will not validate since they have no associated asset - assert_ne!(manifest_store.validation_state(), ValidationState::Invalid); + assert_ne!( + manifest_store.validation_state(), + ValidationState::Malformed + ); } assert_eq!( manifest_store.active_manifest().unwrap().title().unwrap(), @@ -2671,7 +2680,7 @@ mod tests { let reader = Reader::from_stream("image/jpeg", &mut dest).expect("from_bytes"); //println!("{}", reader); - assert_ne!(reader.validation_state(), ValidationState::Invalid); + assert_ne!(reader.validation_state(), ValidationState::Malformed); assert_eq!(reader.validation_status(), None); assert_eq!( reader diff --git a/sdk/src/ingredient.rs b/sdk/src/ingredient.rs index fc61d47d8..38510e76e 100644 --- a/sdk/src/ingredient.rs +++ b/sdk/src/ingredient.rs @@ -1229,7 +1229,7 @@ impl Ingredient { // if there are validations and they have all passed, then use the parent claim thumbnail if available if let Some(validation_results) = self.validation_results() { - if validation_results.validation_state() != crate::ValidationState::Invalid { + if validation_results.validation_state() != crate::ValidationState::Malformed { thumbnail = ingredient_active_claim .assertions() .iter() diff --git a/sdk/src/reader.rs b/sdk/src/reader.rs index 72089eaed..c87043581 100644 --- a/sdk/src/reader.rs +++ b/sdk/src/reader.rs @@ -568,7 +568,7 @@ impl Reader { .iter() .any(|s| s.code() != crate::validation_status::SIGNING_CREDENTIAL_UNTRUSTED); if errs { - ValidationState::Invalid + ValidationState::Malformed } else if verify_trust { // If we verified trust and didn't get an error, we can assume it is trusted ValidationState::Trusted @@ -1015,7 +1015,7 @@ pub mod tests { reader.validation_status().unwrap()[0].code(), crate::validation_status::ASSERTION_DATAHASH_MISMATCH ); - assert_eq!(reader.validation_state(), ValidationState::Invalid); + assert_eq!(reader.validation_state(), ValidationState::Malformed); Ok(()) } diff --git a/sdk/src/store.rs b/sdk/src/store.rs index c4d8489ba..859c351f0 100644 --- a/sdk/src/store.rs +++ b/sdk/src/store.rs @@ -8579,7 +8579,7 @@ pub mod tests { let reader = crate::Reader::from_stream("image/png", &mut dst).unwrap(); - assert_eq!(reader.validation_state(), crate::ValidationState::Invalid); + assert_eq!(reader.validation_state(), crate::ValidationState::Malformed); } #[test] diff --git a/sdk/src/validation_results.rs b/sdk/src/validation_results.rs index a18b896fd..bad60996f 100644 --- a/sdk/src/validation_results.rs +++ b/sdk/src/validation_results.rs @@ -35,10 +35,9 @@ use crate::{ /// /// [§14.3. Validation states]: https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_validation_states pub enum ValidationState { - // REVIEW-NOTE: A "WellFormed" manifest is invalid, should we rename this to "Malformed?" /// The manifest store fails to meet [ValidationState::WellFormed] requirements, meaning it cannot /// even be parsed or its basic structure is non-compliant. - Invalid, + Malformed, /// The manifest store follows all required structural and syntactic rules in the C2PA specification. /// /// See [§14.3.4. Well-Formed Manifest]. @@ -266,7 +265,7 @@ impl ValidationResults { } } - ValidationState::Invalid + ValidationState::Malformed } /// Returns a list of all validation errors in [ValidationResults]. diff --git a/sdk/tests/test_builder.rs b/sdk/tests/test_builder.rs index 67ff96ae2..efc05303c 100644 --- a/sdk/tests/test_builder.rs +++ b/sdk/tests/test_builder.rs @@ -196,7 +196,7 @@ fn test_builder_embedded_v1_otgp() -> Result<()> { dest.set_position(0); let reader = Reader::from_stream(format, &mut dest)?; // check that the v1 OTGP is embedded and we catch it correct with validation_results - assert_ne!(reader.validation_state(), ValidationState::Invalid); + assert_ne!(reader.validation_state(), ValidationState::Malformed); //println!("reader: {}", reader); assert_eq!( reader.active_manifest().unwrap().ingredients()[0] From b839a509b405aa866585c7230f6382647c9592c9 Mon Sep 17 00:00:00 2001 From: ok-nick Date: Fri, 17 Oct 2025 12:56:38 -0400 Subject: [PATCH 04/12] fix: add more checks to validation state --- sdk/src/validation_results.rs | 58 +++++++++++++++-------------------- 1 file changed, 24 insertions(+), 34 deletions(-) diff --git a/sdk/src/validation_results.rs b/sdk/src/validation_results.rs index bad60996f..8a3cde8a5 100644 --- a/sdk/src/validation_results.rs +++ b/sdk/src/validation_results.rs @@ -23,17 +23,17 @@ use crate::{ jumbf::labels::manifest_label_from_uri, status_tracker::{LogKind, StatusTracker}, store::Store, - validation_status::{self, log_kind, ValidationStatus}, + validation_status::{self, log_kind, ValidationStatus, ASSERTION_ACTION_MALFORMED}, }; -#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[cfg_attr(feature = "json_schema", derive(JsonSchema))] /// Represents the levels of assurance a manifest store achives when evaluated against the C2PA /// specifications structural, cryptographic, and trust requirements. /// /// See [§14.3. Validation states]. /// /// [§14.3. Validation states]: https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_validation_states +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "json_schema", derive(JsonSchema))] pub enum ValidationState { /// The manifest store fails to meet [ValidationState::WellFormed] requirements, meaning it cannot /// even be parsed or its basic structure is non-compliant. @@ -217,50 +217,40 @@ impl ValidationResults { .iter() .map(|status| status.code()) .collect(); - // let ingredient_failure_codes: HashSet<&str> = self - // .ingredient_deltas - // .as_ref() - // .map(|deltas| { - // deltas - // .iter() - // .flat_map(|idv| idv.validation_deltas().failure()) - // .map(|status| status.code()) - // .collect() - // }) - // .unwrap_or_default(); let ingredient_failure = self.ingredient_deltas.as_ref().is_some_and(|deltas| { deltas .iter() .any(|idv| !idv.validation_deltas().failure().is_empty()) }); - let is_trusted = success_codes.contains(validation_status::SIGNING_CREDENTIAL_TRUSTED) - && success_codes.contains(validation_status::CLAIM_SIGNATURE_VALIDATED) - && success_codes.contains(validation_status::CLAIM_SIGNATURE_INSIDE_VALIDITY) - && failure_codes.is_empty() - && !ingredient_failure; - if is_trusted { - return ValidationState::Trusted; - } - - let is_valid = success_codes.contains(validation_status::CLAIM_SIGNATURE_VALIDATED) - && success_codes.contains(validation_status::CLAIM_SIGNATURE_INSIDE_VALIDITY) - && failure_codes.len() == 1 - && failure_codes.contains(validation_status::SIGNING_CREDENTIAL_UNTRUSTED) - && !ingredient_failure; - if is_valid { - return ValidationState::Valid; - } - + // https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_well_formed_manifest let is_well_formed = (failure_codes.is_empty() + // We allow any codes that the spec states CANNOT occur to make it "valid." || failure_codes.iter().all(|&code| { code == validation_status::SIGNING_CREDENTIAL_OCSP_UNKNOWN || code == validation_status::SIGNING_CREDENTIAL_REVOKED || code == validation_status::SIGNING_CREDENTIAL_UNTRUSTED + || code == validation_status::CLAIM_SIGNATURE_OUTSIDE_VALIDITY })) - // TODO: what codes to ignore here? + // Note that as of this writing, an ingredient delta failure only occurs in the SDK if there is a hash mismatch on + // the ingredient's active manifest, thus signaling it was modified. && !ingredient_failure; - if is_well_formed { + + // 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) + && is_well_formed; + + // 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() + && is_valid; + + if is_trusted { + return ValidationState::Trusted; + } else if is_valid { + return ValidationState::Valid; + } else if is_well_formed { return ValidationState::WellFormed; } } From 3fc7e910f410072cf35d9b0112498c2e9d177d53 Mon Sep 17 00:00:00 2001 From: ok-nick Date: Fri, 17 Oct 2025 13:10:44 -0400 Subject: [PATCH 05/12] fix: add unknown validation state --- sdk/src/validation_results.rs | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/sdk/src/validation_results.rs b/sdk/src/validation_results.rs index 8a3cde8a5..3374243a1 100644 --- a/sdk/src/validation_results.rs +++ b/sdk/src/validation_results.rs @@ -23,7 +23,7 @@ use crate::{ jumbf::labels::manifest_label_from_uri, status_tracker::{LogKind, StatusTracker}, store::Store, - validation_status::{self, log_kind, ValidationStatus, ASSERTION_ACTION_MALFORMED}, + validation_status::{self, log_kind, ValidationStatus}, }; /// Represents the levels of assurance a manifest store achives when evaluated against the C2PA @@ -35,6 +35,8 @@ use crate::{ #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "json_schema", derive(JsonSchema))] pub enum ValidationState { + /// Validation is disabled in the SDK and the [ValidationState] is unable to be determined. + Unknown, /// The manifest store fails to meet [ValidationState::WellFormed] requirements, meaning it cannot /// even be parsed or its basic structure is non-compliant. Malformed, @@ -59,13 +61,16 @@ pub enum ValidationState { Trusted, } +/// Contains a set of success, informational, and failure validation status codes. #[derive(Clone, Serialize, Default, Deserialize, Debug, PartialEq, Eq)] #[cfg_attr(feature = "json_schema", derive(JsonSchema))] -/// Contains a set of success, informational, and failure validation status codes. pub struct StatusCodes { - pub success: Vec, // an array of validation success codes. May be empty. - pub informational: Vec, // an array of validation informational codes. May be empty. - pub failure: Vec, // an array of validation failure codes. May be empty. + /// An array of validation success codes. May be empty. + pub success: Vec, + /// An array of validation informational codes. May be empty. + pub informational: Vec, + // An array of validation failure codes. May be empty. + pub failure: Vec, } impl StatusCodes { @@ -106,18 +111,22 @@ impl StatusCodes { } } -#[derive(Clone, Serialize, Default, Deserialize, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "json_schema", derive(JsonSchema))] /// A map of validation results for a manifest store. /// /// The map contains the validation results for the active manifest and any ingredient deltas. /// It is normal for there to be many +#[derive(Clone, Serialize, Default, Deserialize, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "json_schema", derive(JsonSchema))] pub struct ValidationResults { + /// Validation status codes for the ingredient's active manifest. Present if ingredient is a C2PA + /// asset. Not present if the ingredient is not a C2PA asset. #[serde(rename = "activeManifest", skip_serializing_if = "Option::is_none")] - active_manifest: Option, // Validation status codes for the ingredient's active manifest. Present if ingredient is a C2PA asset. Not present if the ingredient is not a C2PA asset. + active_manifest: Option, + /// List of any changes/deltas between the current and previous validation results for each ingredient's + /// manifest. Present if the the ingredient is a C2PA asset. #[serde(rename = "ingredientDeltas", skip_serializing_if = "Option::is_none")] - ingredient_deltas: Option>, // List of any changes/deltas between the current and previous validation results for each ingredient's manifest. Present if the the ingredient is a C2PA asset. + ingredient_deltas: Option>, } impl ValidationResults { @@ -207,6 +216,12 @@ 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() { + // If there are no success codes and no failure codes, we assume validation was disabled in the SDK + // and the state is unknown. + if active_manifest.success().is_empty() && active_manifest.failure().is_empty() { + return ValidationState::Unknown; + } + let success_codes: HashSet<&str> = active_manifest .success() .iter() From 3f725719d22a19213cf2c9546674391447a2ba85 Mon Sep 17 00:00:00 2001 From: ok-nick Date: Wed, 22 Oct 2025 13:13:16 -0400 Subject: [PATCH 06/12] test: check for ==Trusted instead of !=Malformed --- sdk/src/builder.rs | 12 +++--------- sdk/tests/test_builder.rs | 2 +- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/sdk/src/builder.rs b/sdk/src/builder.rs index 23992b969..36d341324 100644 --- a/sdk/src/builder.rs +++ b/sdk/src/builder.rs @@ -2358,10 +2358,7 @@ mod tests { let manifest_store = Reader::from_file(&dest).expect("from_bytes"); println!("{manifest_store}"); - assert_ne!( - manifest_store.validation_state(), - ValidationState::Malformed - ); + assert_eq!(manifest_store.validation_state(), ValidationState::Trusted); assert_eq!(manifest_store.validation_status(), None); assert_eq!( manifest_store.active_manifest().unwrap().title().unwrap(), @@ -2419,10 +2416,7 @@ mod tests { //println!("{}", manifest_store); if format != "c2pa" { // c2pa files will not validate since they have no associated asset - assert_ne!( - manifest_store.validation_state(), - ValidationState::Malformed - ); + assert_eq!(manifest_store.validation_state(), ValidationState::Trusted); } assert_eq!( manifest_store.active_manifest().unwrap().title().unwrap(), @@ -2680,7 +2674,7 @@ mod tests { let reader = Reader::from_stream("image/jpeg", &mut dest).expect("from_bytes"); //println!("{}", reader); - assert_ne!(reader.validation_state(), ValidationState::Malformed); + assert_eq!(reader.validation_state(), ValidationState::Trusted); assert_eq!(reader.validation_status(), None); assert_eq!( reader diff --git a/sdk/tests/test_builder.rs b/sdk/tests/test_builder.rs index efc05303c..1fa09ec4a 100644 --- a/sdk/tests/test_builder.rs +++ b/sdk/tests/test_builder.rs @@ -196,7 +196,7 @@ fn test_builder_embedded_v1_otgp() -> Result<()> { dest.set_position(0); let reader = Reader::from_stream(format, &mut dest)?; // check that the v1 OTGP is embedded and we catch it correct with validation_results - assert_ne!(reader.validation_state(), ValidationState::Malformed); + assert_eq!(reader.validation_state(), ValidationState::Trusted); //println!("reader: {}", reader); assert_eq!( reader.active_manifest().unwrap().ingredients()[0] From 628683f5ea5ede0637142febf0e9e9a27b2ff640 Mon Sep 17 00:00:00 2001 From: ok-nick Date: Wed, 22 Oct 2025 13:16:25 -0400 Subject: [PATCH 07/12] test: mark XCA.json as Malformed instead of Invalid --- sdk/tests/known_good/XCA.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/tests/known_good/XCA.json b/sdk/tests/known_good/XCA.json index 30658a76c..46e9578aa 100644 --- a/sdk/tests/known_good/XCA.json +++ b/sdk/tests/known_good/XCA.json @@ -170,5 +170,5 @@ } ] }, - "validation_state": "Invalid" -} \ No newline at end of file + "validation_state": "Malformed" +} From e6351cf3945e29f0b9bf4d910a70330826c50fcc Mon Sep 17 00:00:00 2001 From: ok-nick Date: Thu, 30 Oct 2025 10:26:43 -0400 Subject: [PATCH 08/12] feat: make Valid state more strict and remove WellFormed state --- sdk/src/builder.rs | 5 +---- sdk/src/ingredient.rs | 2 +- sdk/src/reader.rs | 4 ++-- sdk/src/store.rs | 2 +- sdk/src/validation_results.rs | 28 ++++------------------------ sdk/tests/known_good/XCA.json | 2 +- 6 files changed, 10 insertions(+), 33 deletions(-) diff --git a/sdk/src/builder.rs b/sdk/src/builder.rs index 36d341324..e92904b8e 100644 --- a/sdk/src/builder.rs +++ b/sdk/src/builder.rs @@ -1895,10 +1895,7 @@ mod tests { let manifest_store = Reader::from_stream(format, &mut dest).expect("from_bytes"); println!("{manifest_store}"); - assert_ne!( - manifest_store.validation_state(), - ValidationState::Malformed - ); + assert_ne!(manifest_store.validation_state(), ValidationState::Invalid); assert!(manifest_store.active_manifest().is_some()); let manifest = manifest_store.active_manifest().unwrap(); assert_eq!(manifest.title().unwrap(), "Test_Manifest"); diff --git a/sdk/src/ingredient.rs b/sdk/src/ingredient.rs index 38510e76e..fc61d47d8 100644 --- a/sdk/src/ingredient.rs +++ b/sdk/src/ingredient.rs @@ -1229,7 +1229,7 @@ impl Ingredient { // if there are validations and they have all passed, then use the parent claim thumbnail if available if let Some(validation_results) = self.validation_results() { - if validation_results.validation_state() != crate::ValidationState::Malformed { + if validation_results.validation_state() != crate::ValidationState::Invalid { thumbnail = ingredient_active_claim .assertions() .iter() diff --git a/sdk/src/reader.rs b/sdk/src/reader.rs index c87043581..72089eaed 100644 --- a/sdk/src/reader.rs +++ b/sdk/src/reader.rs @@ -568,7 +568,7 @@ impl Reader { .iter() .any(|s| s.code() != crate::validation_status::SIGNING_CREDENTIAL_UNTRUSTED); if errs { - ValidationState::Malformed + ValidationState::Invalid } else if verify_trust { // If we verified trust and didn't get an error, we can assume it is trusted ValidationState::Trusted @@ -1015,7 +1015,7 @@ pub mod tests { reader.validation_status().unwrap()[0].code(), crate::validation_status::ASSERTION_DATAHASH_MISMATCH ); - assert_eq!(reader.validation_state(), ValidationState::Malformed); + assert_eq!(reader.validation_state(), ValidationState::Invalid); Ok(()) } diff --git a/sdk/src/store.rs b/sdk/src/store.rs index 859c351f0..c4d8489ba 100644 --- a/sdk/src/store.rs +++ b/sdk/src/store.rs @@ -8579,7 +8579,7 @@ pub mod tests { let reader = crate::Reader::from_stream("image/png", &mut dst).unwrap(); - assert_eq!(reader.validation_state(), crate::ValidationState::Malformed); + assert_eq!(reader.validation_state(), crate::ValidationState::Invalid); } #[test] diff --git a/sdk/src/validation_results.rs b/sdk/src/validation_results.rs index 3374243a1..e07540095 100644 --- a/sdk/src/validation_results.rs +++ b/sdk/src/validation_results.rs @@ -39,13 +39,7 @@ pub enum ValidationState { Unknown, /// The manifest store fails to meet [ValidationState::WellFormed] requirements, meaning it cannot /// even be parsed or its basic structure is non-compliant. - Malformed, - /// The manifest store follows all required structural and syntactic rules in the C2PA specification. - /// - /// See [§14.3.4. Well-Formed Manifest]. - /// - /// [§14.3.4. Well-Formed Manifest]: https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_well_formed_manifest - WellFormed, + Invalid, /// The manifest store is well-formed and the cryptographic integrity checks succeed. /// /// See [§14.3.5. Valid Manifest]. @@ -238,23 +232,11 @@ impl ValidationResults { .any(|idv| !idv.validation_deltas().failure().is_empty()) }); - // https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_well_formed_manifest - let is_well_formed = (failure_codes.is_empty() - // We allow any codes that the spec states CANNOT occur to make it "valid." - || failure_codes.iter().all(|&code| { - code == validation_status::SIGNING_CREDENTIAL_OCSP_UNKNOWN - || code == validation_status::SIGNING_CREDENTIAL_REVOKED - || code == validation_status::SIGNING_CREDENTIAL_UNTRUSTED - || code == validation_status::CLAIM_SIGNATURE_OUTSIDE_VALIDITY - })) - // Note that as of this writing, an ingredient delta failure only occurs in the SDK if there is a hash mismatch on - // the ingredient's active manifest, thus signaling it was modified. - && !ingredient_failure; - // 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) - && is_well_formed; + && failure_codes.is_empty() + && !ingredient_failure; // 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) @@ -265,12 +247,10 @@ impl ValidationResults { return ValidationState::Trusted; } else if is_valid { return ValidationState::Valid; - } else if is_well_formed { - return ValidationState::WellFormed; } } - ValidationState::Malformed + ValidationState::Invalid } /// Returns a list of all validation errors in [ValidationResults]. diff --git a/sdk/tests/known_good/XCA.json b/sdk/tests/known_good/XCA.json index 46e9578aa..d9d7799e7 100644 --- a/sdk/tests/known_good/XCA.json +++ b/sdk/tests/known_good/XCA.json @@ -170,5 +170,5 @@ } ] }, - "validation_state": "Malformed" + "validation_state": "Invalid" } From 307d865bffe18f2341612569c8d9cc91a7a50ec6 Mon Sep 17 00:00:00 2001 From: ok-nick Date: Thu, 30 Oct 2025 11:06:13 -0400 Subject: [PATCH 09/12] test: reset settings before running test_builder_base_path --- sdk/src/builder.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sdk/src/builder.rs b/sdk/src/builder.rs index e92904b8e..99e8f33e8 100644 --- a/sdk/src/builder.rs +++ b/sdk/src/builder.rs @@ -2643,6 +2643,9 @@ mod tests { #[cfg(feature = "file_io")] #[test] fn test_builder_base_path() { + #[cfg(target_os = "wasi")] + Settings::reset().unwrap(); + let mut source = Cursor::new(TEST_IMAGE_CLEAN); let mut dest = Cursor::new(Vec::new()); From 38fdd24d5bbbff3c9135262215c9344754df392d Mon Sep 17 00:00:00 2001 From: ok-nick Date: Thu, 30 Oct 2025 11:12:00 -0400 Subject: [PATCH 10/12] fix: if there is no active manifest, assume it wasn't validated --- sdk/src/validation_results.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/sdk/src/validation_results.rs b/sdk/src/validation_results.rs index e07540095..fe1a8fb6d 100644 --- a/sdk/src/validation_results.rs +++ b/sdk/src/validation_results.rs @@ -210,12 +210,6 @@ 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() { - // If there are no success codes and no failure codes, we assume validation was disabled in the SDK - // and the state is unknown. - if active_manifest.success().is_empty() && active_manifest.failure().is_empty() { - return ValidationState::Unknown; - } - let success_codes: HashSet<&str> = active_manifest .success() .iter() @@ -248,6 +242,10 @@ impl ValidationResults { } else if is_valid { return ValidationState::Valid; } + } else { + // REVIEW-NOTE: is this the best way to detect that it wasn't validated? should we also check if success/failure is empty if there + // is an active manifest? + return ValidationState::Unknown; } ValidationState::Invalid From 2f8d88f260e67ca996d5e632c57656e44f98fd87 Mon Sep 17 00:00:00 2001 From: ok-nick Date: Thu, 30 Oct 2025 13:21:47 -0400 Subject: [PATCH 11/12] fix: remove unknown validation state for now --- sdk/src/validation_results.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/sdk/src/validation_results.rs b/sdk/src/validation_results.rs index fe1a8fb6d..348792558 100644 --- a/sdk/src/validation_results.rs +++ b/sdk/src/validation_results.rs @@ -35,8 +35,6 @@ use crate::{ #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "json_schema", derive(JsonSchema))] pub enum ValidationState { - /// Validation is disabled in the SDK and the [ValidationState] is unable to be determined. - Unknown, /// The manifest store fails to meet [ValidationState::WellFormed] requirements, meaning it cannot /// even be parsed or its basic structure is non-compliant. Invalid, @@ -215,11 +213,7 @@ impl ValidationResults { .iter() .map(|status| status.code()) .collect(); - let failure_codes: HashSet<&str> = active_manifest - .failure() - .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() @@ -242,10 +236,6 @@ impl ValidationResults { } else if is_valid { return ValidationState::Valid; } - } else { - // REVIEW-NOTE: is this the best way to detect that it wasn't validated? should we also check if success/failure is empty if there - // is an active manifest? - return ValidationState::Unknown; } ValidationState::Invalid From 3c4ba9aea6d773e9bf70c431b0bb6f1e88e7c219 Mon Sep 17 00:00:00 2001 From: ok-nick Date: Thu, 30 Oct 2025 13:22:26 -0400 Subject: [PATCH 12/12] feat: add Unknown validation state --- sdk/src/validation_results.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sdk/src/validation_results.rs b/sdk/src/validation_results.rs index 348792558..1d7f15d3f 100644 --- a/sdk/src/validation_results.rs +++ b/sdk/src/validation_results.rs @@ -35,6 +35,8 @@ use crate::{ #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "json_schema", derive(JsonSchema))] pub enum ValidationState { + /// Validation is disabled in the SDK and the [ValidationState] is unable to be determined. + Unknown, /// The manifest store fails to meet [ValidationState::WellFormed] requirements, meaning it cannot /// even be parsed or its basic structure is non-compliant. Invalid, @@ -236,6 +238,10 @@ impl ValidationResults { } else if is_valid { return ValidationState::Valid; } + } else { + // REVIEW-NOTE: is this the best way to detect that it wasn't validated? should we also check if success/failure is empty if there + // is an active manifest? + return ValidationState::Unknown; } ValidationState::Invalid