Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions sdk/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2355,7 +2355,7 @@ mod tests {
let manifest_store = Reader::from_file(&dest).expect("from_bytes");

println!("{manifest_store}");
assert_ne!(manifest_store.validation_state(), ValidationState::Invalid);
assert_eq!(manifest_store.validation_state(), ValidationState::Trusted);
assert_eq!(manifest_store.validation_status(), None);
assert_eq!(
manifest_store.active_manifest().unwrap().title().unwrap(),
Expand Down Expand Up @@ -2413,7 +2413,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::Invalid);
assert_eq!(manifest_store.validation_state(), ValidationState::Trusted);
}
assert_eq!(
manifest_store.active_manifest().unwrap().title().unwrap(),
Expand Down Expand Up @@ -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());

Expand Down Expand Up @@ -2671,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::Invalid);
assert_eq!(reader.validation_state(), ValidationState::Trusted);
assert_eq!(reader.validation_status(), None);
assert_eq!(
reader
Expand Down
106 changes: 74 additions & 32 deletions sdk/src/validation_results.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -21,30 +23,48 @@ 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},
};

/// 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))]
/// Indicates if the manifest store is valid and trusted.
///
/// 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.
/// 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,
/// No errors were found in validation, but the active signature is not trusted.
/// 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 store is valid and the active signature is trusted.
/// 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].
///
/// [§14.3.6. Trusted Manifest]: https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_trusted_manifest
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<ValidationStatus>, // an array of validation success codes. May be empty.
pub informational: Vec<ValidationStatus>, // an array of validation informational codes. May be empty.
pub failure: Vec<ValidationStatus>, // an array of validation failure codes. May be empty.
/// An array of validation success codes. May be empty.
pub success: Vec<ValidationStatus>,
/// An array of validation informational codes. May be empty.
pub informational: Vec<ValidationStatus>,
// An array of validation failure codes. May be empty.
pub failure: Vec<ValidationStatus>,
}

impl StatusCodes {
Expand Down Expand Up @@ -85,18 +105,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<StatusCodes>, // 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<StatusCodes>,

/// 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<Vec<IngredientDeltaValidationResult>>, // 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<Vec<IngredientDeltaValidationResult>>,
}

impl ValidationResults {
Expand Down Expand Up @@ -180,29 +204,47 @@ impl ValidationResults {
}

/// Returns the [ValidationState] of the manifest store 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 = 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())
});
}
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;
}

// 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()
&& !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)
&& failure_codes.is_empty()
&& is_valid;

if is_trusted {
return ValidationState::Trusted;
} else if is_valid {
return ValidationState::Valid;
}
}
if is_trusted {
ValidationState::Trusted
} else {
ValidationState::Valid
// 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work? I'm not sure if there will always be no active manifest in validation_results if we did not validate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/// 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<StatusCodes>,

I think it's a safe assumption that if there's no active_manifest then it hasn't been validated, or at the very least it couldn't be determined. The only problem is that we populate the validation results on a Reader::post_validate, so it's possible that normal validation is disabled but since it was post validated the state shows as invalid (or possibly even valid?).

// is an active manifest?
return ValidationState::Unknown;
}

ValidationState::Invalid
}

/// Returns a list of all validation errors in [ValidationResults].
Expand Down
2 changes: 1 addition & 1 deletion sdk/tests/known_good/XCA.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,4 @@
]
},
"validation_state": "Invalid"
}
}
2 changes: 1 addition & 1 deletion sdk/tests/test_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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_eq!(reader.validation_state(), ValidationState::Trusted);
//println!("reader: {}", reader);
assert_eq!(
reader.active_manifest().unwrap().ingredients()[0]
Expand Down