// Copyright 2024 Adobe. All rights reserved. // This file is licensed to you under the Apache License, // Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) // or the MIT license (http://opensource.org/licenses/MIT), // at your option. // Unless required by applicable law or agreed to in writing, // this software is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or // implied. See the LICENSE-MIT and LICENSE-APACHE files for the // specific language governing permissions and limitations under // each license. use std::{ collections::HashSet, fmt::{self, Display}, }; use chrono::Utc; #[cfg(feature = "json_schema")] use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::{ assertion::AssertionBase, assertions::Ingredient, jumbf::labels::{self, manifest_label_from_uri}, spec_versions::C2PA_VALIDATOR_VERSION, status_tracker::{LogKind, StatusTracker}, store::Store, validation_status::{self, log_kind, ValidationStatus}, }; /// Represents the levels of assurance a manifest store achieves when evaluated against the C2PA /// specifications structural, cryptographic, and trust requirements. /// /// See [Validation states - C2PA Technical Specification](https://spec.c2pa.org/specifications/specifications/2.3/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. /// /// This case may also occur if validation is disabled in the SDK. Invalid, /// The manifest store is well-formed and the cryptographic integrity checks succeed. /// /// See [Valid Manifest - C2PA Technical Specification](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_valid_manifest). Valid, /// 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 [Trusted Manifest - C2PA Technical Specification](https://spec.c2pa.org/specifications/specifications/2.3/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))] pub struct StatusCodes { /// 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 { /// Adds a [ValidationStatus] to the StatusCodes. pub fn add_status(&mut self, status: ValidationStatus) { match status.kind() { LogKind::Success => self.success.push(status), LogKind::Informational => self.informational.push(status), LogKind::Failure => self.failure.push(status), } } pub fn add_success_val(mut self, sm: ValidationStatus) -> Self { self.success.push(sm); self } pub fn success(&self) -> &Vec { self.success.as_ref() } pub fn add_informational_val(mut self, sm: ValidationStatus) -> Self { self.informational.push(sm); self } pub fn informational(&self) -> &Vec { self.informational.as_ref() } pub fn add_failure_val(mut self, sm: ValidationStatus) -> Self { self.failure.push(sm); self } pub fn failure(&self) -> &Vec { self.failure.as_ref() } } /// 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, 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, /// 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>, /// The version of the specification against which the validation was performed (SemVer formatted string). #[serde(rename = "specVersion", skip_serializing_if = "Option::is_none")] spec_version: Option, /// URI to the trust list that was used to validate manifests signing certificate. #[serde(rename = "trustListUri", skip_serializing_if = "Option::is_none")] trust_list_uri: Option, /// URI to the trust list use to validate the time-stamp. #[serde(rename = "timestampTrustListUri", skip_serializing)] timestamp_trust_list_uri: Option, /// Time when the validation was performed (RFC 3339 date-time). Used only for document-level validationInfo; not serialized in validationResults (e.g. ingredient assertions). #[serde(rename = "validationTime", skip_serializing)] validation_time: Option, } impl Default for ValidationResults { fn default() -> Self { Self { active_manifest: None, ingredient_deltas: None, spec_version: Some(C2PA_VALIDATOR_VERSION.to_string()), trust_list_uri: None, validation_time: None, timestamp_trust_list_uri: None, } } } /// Prefix shared by every CAWG X.509 identity assertion status code (for /// example, `cawg.x509.credential.untrusted`, `cawg.x509.signature.mismatch`). const CAWG_X509_STATUS_PREFIX: &str = "cawg.x509."; /// Prefix shared by every CAWG identity assertion status code (for example, /// `cawg.identity.cbor.invalid`, `cawg.identity.sig_type.unknown`). const CAWG_IDENTITY_STATUS_PREFIX: &str = "cawg.identity."; /// Returns `true` if a failure with `code` is scoped to an individual credential /// or CAWG identity assertion and does not, by itself, invalidate the enclosing /// manifest. A CAWG validation failure's blast radius is only that assertion. /// /// See [`ValidationResults::validation_state`] and [`ValidationFailureSummary`]. fn is_tolerated_manifest_failure_code(code: &str) -> bool { code == validation_status::SIGNING_CREDENTIAL_UNTRUSTED || code.starts_with(CAWG_X509_STATUS_PREFIX) || code.starts_with(CAWG_IDENTITY_STATUS_PREFIX) } /// Labels of the active claim and every manifest transitively referenced by its /// ingredient assertions. Claims outside this set are in the store but not part of /// the verified provenance, so their ingredient assertions must not take part in /// validation-status reconciliation. fn referenced_claim_labels(store: &Store) -> HashSet { let mut referenced = HashSet::new(); let Some(active) = store.provenance_claim() else { return referenced; }; let mut queue = vec![active.label().to_string()]; referenced.insert(active.label().to_string()); while let Some(label) = queue.pop() { let Some(claim) = store.get_claim(&label) else { continue; }; for a in claim.ingredient_assertions() { let Ok(ingredient) = Ingredient::from_assertion(a.assertion()) else { continue; }; let ingredient_label = ingredient .active_manifest .as_ref() .or(ingredient.c2pa_manifest.as_ref()) .map(|m| m.url()) .and_then(|uri| manifest_label_from_uri(&uri)); if let Some(label) = ingredient_label { if referenced.insert(label.clone()) { queue.push(label); } } } } referenced } impl ValidationResults { pub(crate) fn from_store(store: &Store, validation_log: &StatusTracker) -> Self { let mut results = ValidationResults::default(); let mut statuses: Vec = validation_log .logged_items() .iter() .filter_map(ValidationStatus::from_log_item) .collect(); // Find out the trust list URI for trusted signing credentials or TSA for status in &statuses { if status.code() == validation_status::SIGNING_CREDENTIAL_TRUSTED { if let Some(trust_list_uri) = status.trust_list_uri() { if let Some(item_uri) = status.url() { // Filter out CAWG results if item_uri.ends_with(labels::SIGNATURE) { results.trust_list_uri = Some(trust_list_uri.into()); } } } } if status.code() == validation_status::TIMESTAMP_TRUSTED { if let Some(timestamp_trust_list_uri) = status.trust_list_uri() { results.timestamp_trust_list_uri = Some(timestamp_trust_list_uri.into()); } } } // Filter out any status that is already captured in an ingredient assertion. // There is always an active manifest in a manifest store; ensure active_manifest is set // so serialization (e.g. crJSON) always includes activeManifest when validationResults exist. if let Some(claim) = store.provenance_claim() { let _ = results .active_manifest .get_or_insert_with(StatusCodes::default); let active_manifest = Some(claim.label().to_string()); // Returns true if `uri` names an assertion (or box) inside the store's active // manifest, e.g. `self#jumbf=/c2pa//...`. let is_active_manifest = |uri: Option<&str>| { uri.is_some_and(|uri| manifest_label_from_uri(uri) == active_manifest) }; // Returns the ingredient's own manifest label (if any) together with a flat list of // its validation statuses, with absolute URIs. The own label is threaded through // separately (rather than re-derived from the flattened statuses later) so that a // `claimSignature.insideValidity` attestation can only ever be trusted as being // *about the ingredient that made it* -- not about some other, unrelated manifest // label that the ingredient's assertion content merely happens to name. let get_statuses = |i: Ingredient| { let own_label = i .active_manifest .as_ref() .or(i.c2pa_manifest.as_ref()) .map(|m| m.url()) .and_then(|uri| manifest_label_from_uri(&uri)); // Get a flat list of validation statuses from the ingredient. // If validation_results are present, use them, otherwise use the ingredient's validation_status. // // the `kind` field is `#[serde(skip)]` so it doesn't survive serialization // of the ingredient. we also need to fix up `kind` since the older validation // statuses don't have it set. let validation_status: Option> = i .validation_results .map(|v| v.validation_status()) .or(i.validation_status) .map(|statuses| { statuses .into_iter() .map(|s| { let kind = log_kind(s.code()); s.set_kind(kind) }) .collect() }); // Convert any relative manifest urls found in ingredient validation statuses to absolute. validation_status.map(|mut statuses| { if let Some(label) = &own_label { for status in &mut statuses { status.make_absolute(label) } } (own_label, statuses) }) }; // We only need to do the more detailed filtering if there are any statuses that // could actually be de-duplicated, i.e. statuses logged inside an ingredient scope // that are not about the active manifest. if statuses .iter() .any(|s| s.ingredient_uri().is_some() && !is_active_manifest(s.url())) { // Only reconcile against ingredient assertions from claims referenced by the // active manifest's ingredient tree; an unreferenced claim is never verified, so // it must not cancel findings about other manifests. Nested ingredients keep the // historical ingredient_uri, so statuses are matched by content, not by URI. let referenced = referenced_claim_labels(store); let mut ingredient_statuses: Vec = Vec::new(); // Trust a `claimSignature.insideValidity` attestation only when the ingredient // reported it for its own label (self-attested), so it can't vouch for another. let mut attested_inside_validity: HashSet = HashSet::new(); for (own_label, group_statuses) in store .claims() .iter() .filter(|c| referenced.contains(c.label())) .flat_map(|c| c.ingredient_assertions()) .filter_map(|a| Ingredient::from_assertion(a.assertion()).ok()) .filter_map(get_statuses) { if let Some(own_label) = &own_label { if group_statuses.iter().any(|status| { status.code() == validation_status::CLAIM_SIGNATURE_INSIDE_VALIDITY && status .url() .and_then(manifest_label_from_uri) .is_some_and(|label| &label == own_label) }) { attested_inside_validity.insert(own_label.clone()); } } ingredient_statuses.extend(group_statuses); } // Drop a status only if it re-reports what a referenced ingredient already // attested: it must be ingredient-scoped, not describe the active manifest, and // match an ingredient status. Active-manifest statuses (no ingredient_uri, or a // url inside the active manifest) are always kept so an ingredient assertion // can't cancel a genuine active-manifest failure. statuses.retain(|s| { let attested_historical_expiry = s.code() == validation_status::SIGNING_CREDENTIAL_EXPIRED && s.ingredient_uri().is_some() && !is_active_manifest(s.url()) && s.url() .and_then(manifest_label_from_uri) .is_some_and(|label| attested_inside_validity.contains(&label)); s.ingredient_uri().is_none() || is_active_manifest(s.url()) || (!ingredient_statuses.iter().any(|i| i == s) && !attested_historical_expiry) }) } for status in statuses { results.add_status(status); } } results.validation_time = Some(Utc::now().to_rfc3339()); results } /// Returns the [ValidationState] of the manifest store based on the validation results. /// /// See [Validation states - C2PA Technical Specification](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_validation_states). pub fn validation_state(&self) -> ValidationState { if let Some(active_manifest) = self.active_manifest.as_ref() { // NOTE: Changes here may impact the impl of [`ValidationFailureSummary::fmt`]. // Ensure changes are reciprocated in both locations. // // A failure to trust the issuer of a CAWG identity claims aggregation // (ICA) credential is scoped to that individual identity assertion and // does not affect the enclosing manifest's state. It is recorded with // the informational code `cawg.ica.untrusted_issuer` (and the // credential's `cawg.ica.credential_valid` success code is withheld), // so it never appears among the failures examined below. // // An untrusted C2PA claim signature credential // (`signingCredential.untrusted`), and every CAWG X.509 identity // assertion failure code, is likewise tolerated here -- see // [`is_tolerated_manifest_failure_code`]. // // 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() .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 they're // all tolerated failures. && (active_manifest.failure().is_empty() || active_manifest .failure() .iter() .all(|status| is_tolerated_manifest_failure_code(status.code()))) // Finally check if the ingredients contain either no failures or the only // failures are tolerated failures. && 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| is_tolerated_manifest_failure_code(status.code())) }) }); // https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_trusted_manifest 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 { return ValidationState::Trusted; } else if is_valid { return ValidationState::Valid; } } ValidationState::Invalid } /// Returns a list of all validation errors in [ValidationResults]. pub(crate) fn validation_errors(&self) -> Option> { let mut status_vec = Vec::new(); if let Some(active_manifest) = self.active_manifest.as_ref() { status_vec.extend(active_manifest.failure().to_vec()); } if let Some(ingredient_deltas) = self.ingredient_deltas.as_ref() { for idv in ingredient_deltas.iter() { status_vec.extend(idv.validation_deltas().failure().to_vec()); } } if status_vec.is_empty() { None } else { Some(status_vec) } } /// Returns a list of all validation status codes in [ValidationResults]. pub(crate) fn validation_status(&self) -> Vec { let mut status = Vec::new(); if let Some(active_manifest) = self.active_manifest.as_ref() { status.extend(active_manifest.success().to_vec()); status.extend(active_manifest.informational().to_vec()); status.extend(active_manifest.failure().to_vec()); } if let Some(ingredient_deltas) = self.ingredient_deltas.as_ref() { for idv in ingredient_deltas.iter() { status.extend(idv.validation_deltas().success().to_vec()); status.extend(idv.validation_deltas().informational().to_vec()); status.extend(idv.validation_deltas().failure().to_vec()); } } status } /// Adds a [ValidationStatus] to the [ValidationResults]. pub fn add_status(&mut self, status: ValidationStatus) -> &mut Self { match status.ingredient_uri() { None => { let scm = self .active_manifest .get_or_insert_with(StatusCodes::default); scm.add_status(status); } Some(ingredient_url) => { let ingredient_vec = self.ingredient_deltas.get_or_insert_with(Vec::new); match ingredient_vec .iter_mut() .find(|idv| idv.ingredient_assertion_uri() == ingredient_url) { Some(idv) => { idv.validation_deltas_mut().add_status(status); } None => { let mut idv = IngredientDeltaValidationResult::new( ingredient_url, StatusCodes::default(), ); idv.validation_deltas_mut().add_status(status); ingredient_vec.push(idv); } }; } } self } /// Returns the active manifest status codes, if present. pub fn active_manifest(&self) -> Option<&StatusCodes> { self.active_manifest.as_ref() } /// Returns the ingredient deltas, if present. pub fn ingredient_deltas(&self) -> Option<&Vec> { self.ingredient_deltas.as_ref() } /// Returns the time when validation was performed (RFC 3339), if set. pub fn validation_time(&self) -> Option<&str> { self.validation_time.as_deref() } pub fn add_active_manifest(mut self, scm: StatusCodes) -> Self { self.active_manifest = Some(scm); self } pub fn add_ingredient_delta(mut self, idv: IngredientDeltaValidationResult) -> Self { if let Some(id) = self.ingredient_deltas.as_mut() { id.push(idv); } else { self.ingredient_deltas = Some(vec![idv]); } self } /// Returns a summary of why validation failed. /// /// The `Display` impl outputs a human-readable view of the failures, suitable for error messages. /// If there are no failures, an empty string is output. pub fn failure_summary(&self) -> ValidationFailureSummary<'_> { ValidationFailureSummary(self) } pub(crate) fn format_status(status: &ValidationStatus, indent: &str) -> String { let mut line = format!("{indent}{}", status.code()); if let Some(explanation) = status.explanation() { line.push_str(&format!(": {explanation}")); } if let Some(url) = status.url() { line.push_str(&format!(" ({url})")); } line } } impl Display for ValidationResults { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let spec_version = self.spec_version.as_deref().unwrap_or("unknown"); writeln!(f, "spec validation version: {spec_version}")?; let state = self.validation_state(); if state == ValidationState::Trusted { writeln!( f, "state: {state:?}, trustListUri: {}", self.trust_list_uri.as_deref().unwrap_or("none") )?; } else { writeln!(f, "state: {state:?}")?; } if let Some(timestamp_trust_list_uri) = self.timestamp_trust_list_uri.as_deref() { writeln!(f, "timestamp trust list URI: {timestamp_trust_list_uri}")?; } if let Some(active_manifest) = self.active_manifest.as_ref() { if !active_manifest.success.is_empty() { let codes = active_manifest .success .iter() .map(|status| status.code()) .collect::>(); writeln!(f, " success: {}", codes.join(", "))?; } if !active_manifest.informational.is_empty() { writeln!(f, " informational:")?; for status in &active_manifest.informational { writeln!(f, "{}", ValidationResults::format_status(status, " "))?; } } if !active_manifest.failure.is_empty() { writeln!(f, " failure:")?; for status in &active_manifest.failure { writeln!(f, "{}", ValidationResults::format_status(status, " "))?; } } } if let Some(deltas) = self.ingredient_deltas.as_ref() { for delta in deltas { let d = delta.validation_deltas(); writeln!(f, " ingredient [{}]:", delta.ingredient_assertion_uri())?; if !d.success.is_empty() { let codes = d .success .iter() .map(|status| status.code()) .collect::>(); writeln!(f, " success: {}", codes.join(", "))?; } if !d.informational.is_empty() { writeln!(f, " informational:")?; for status in &d.informational { writeln!(f, "{}", ValidationResults::format_status(status, " "))?; } } if !d.failure.is_empty() { writeln!(f, " failure:")?; for status in &d.failure { writeln!(f, "{}", ValidationResults::format_status(status, " "))?; } } } } Ok(()) } } /// Concise display of why a [`ValidationResults`] is invalid (impls `Display`). pub struct ValidationFailureSummary<'a>(&'a ValidationResults); impl Display for ValidationFailureSummary<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let results = self.0; let mut output_lines = Vec::new(); if let Some(active_manifest) = results.active_manifest.as_ref() { let failures = active_manifest .failure .iter() .filter(|status| !is_tolerated_manifest_failure_code(status.code())) .collect::>(); if !failures.is_empty() { output_lines.push("failures:".to_string()); output_lines.extend( failures .iter() .map(|status| ValidationResults::format_status(status, " ")), ); } } if let Some(deltas) = results.ingredient_deltas.as_ref() { for delta in deltas { let failures = delta .validation_deltas() .failure .iter() .filter(|status| !is_tolerated_manifest_failure_code(status.code())) .collect::>(); if !failures.is_empty() { output_lines.push(format!( "ingredient [{}] failures:", delta.ingredient_assertion_uri() )); output_lines.extend( failures .iter() .map(|status| ValidationResults::format_status(status, " ")), ); } } } write!(f, "{}", output_lines.join("\n")) } } #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] #[cfg_attr(feature = "json_schema", derive(JsonSchema))] /// Represents any changes or deltas between the current and previous validation results for an ingredient's manifest. pub struct IngredientDeltaValidationResult { #[serde(rename = "ingredientAssertionURI")] /// JUMBF URI reference to the ingredient assertion ingredient_assertion_uri: String, #[serde(rename = "validationDeltas")] /// Validation results for the ingredient's active manifest validation_deltas: StatusCodes, } impl IngredientDeltaValidationResult { /// Creates a new [IngredientDeltaValidationResult] with the provided ingredient URI and validation deltas. pub fn new>( ingredient_assertion_uri: S, validation_deltas: StatusCodes, ) -> Self { IngredientDeltaValidationResult { ingredient_assertion_uri: ingredient_assertion_uri.into(), validation_deltas, } } pub fn ingredient_assertion_uri(&self) -> &str { self.ingredient_assertion_uri.as_str() } pub fn validation_deltas(&self) -> &StatusCodes { &self.validation_deltas } pub fn validation_deltas_mut(&mut self) -> &mut StatusCodes { &mut self.validation_deltas } } /// Implements validation status for specific parts of a manifest. /// /// See [Standard Status Codes - C2PA Technical Specification](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_standard_status_codes). pub mod validation_codes { use crate::status_tracker::LogKind; // -- success codes -- /// The claim signature referenced in the ingredient's claim validated. /// /// Any corresponding URL should point to a C2PA claim signature box. pub const CLAIM_SIGNATURE_VALIDATED: &str = "claimSignature.validated"; /// The claims signing certificate was valid at the time of signing. /// /// Any corresponding URL should point to a C2PA claim box. pub const CLAIM_SIGNATURE_INSIDE_VALIDITY: &str = "claimSignature.insideValidity"; /// The signing credential is listed on the validator's trust list. /// /// Any corresponding URL should point to a C2PA claim signature box. pub const SIGNING_CREDENTIAL_TRUSTED: &str = "signingCredential.trusted"; /// The signing credential for the manifest has not been revoked: /// /// Any corresponding URL should point to a C2PA claim pub const SIGNING_CREDENTIAL_NOT_REVOKED: &str = "signingCredential.ocsp.notRevoked"; /// The time-stamp credential is well-formed and message imprint and validity /// are correct. /// /// Any corresponding URL should point to a C2PA claim signature box. pub const TIMESTAMP_VALIDATED: &str = "timeStamp.validated"; /// The time-stamp credential is listed on the validator's trust list. /// /// Any corresponding URL should point to a C2PA claim signature box. pub const TIMESTAMP_TRUSTED: &str = "timeStamp.trusted"; /// The hash of the the referenced assertion in the ingredient's manifest /// matches the corresponding hash in the assertion's hashed URI in the claim. /// /// Any corresponding URL should point to a C2PA assertion. pub const ASSERTION_HASHEDURI_MATCH: &str = "assertion.hashedURI.match"; /// Hash of a byte range of the asset matches the hash declared in the /// data hash assertion. /// /// Any corresponding URL should point to a C2PA assertion. pub const ASSERTION_DATAHASH_MATCH: &str = "assertion.dataHash.match"; /// Additional exclusions are present in the data hash assertion. /// /// Any corresponding URL should point to a C2PA assertion. pub const ASSERTION_DATAHASH_ADDITIONAL_EXCLUSIONS: &str = "assertion.dataHash.additionalExclusionsPresent"; /// Hash of a box-based asset matches the hash declared in the BMFF /// hash assertion. /// /// Any corresponding URL should point to a C2PA assertion. pub const ASSERTION_BMFFHASH_MATCH: &str = "assertion.bmffHash.match"; /// Additional exclusions are present in the BMFF hash assertion. /// /// Any corresponding URL should point to a C2PA assertion. pub const ASSERTION_BMFFHASH_ADDITIONAL_EXCLUSIONS: &str = "assertion.bmffHash.additionalExclusionsPresent"; /// Hash of a box-based asset matches the hash declared in the General Box /// Hash assertion. /// /// Any corresponding URL should point to a C2PA assertion. pub const ASSERTION_BOXHASH_MATCH: &str = "assertion.boxesHash.match"; /// Additional exclusions are present in the General Box Hash assertion. /// /// Any corresponding URL should point to a C2PA assertion. pub const ASSERTION_BOXHASH_ADDITIONAL_EXCLUSIONS: &str = "assertion.boxesHash.additionalExclusionsPresent"; /// Hash of all assets contained in collection match hashes declared /// in Collection Data /// Hash assertion. /// /// Any corresponding URL should point to a C2PA assertion. pub const ASSERTION_COLLECTIONHASH_MATCH: &str = "assertion.collectionHash.match"; /// A non-embedded (remote) assertion was accessible at the time of /// validation. /// /// Any corresponding URL should point to a C2PA assertion. pub const ASSERTION_ACCESSIBLE: &str = "assertion.accessible"; /// Hash of the ingredient's C2PA manifest was successfully validated. /// /// Any corresponding URL should point to a C2PA assertion. pub const INGREDIENT_MANIFEST_VALIDATED: &str = "ingredient.manifest.validated"; /// Ingredient had no manifest. /// /// Any corresponding URL should point to a C2PA assertion. pub const INGREDIENT_PROVENANCE_UNKNOWN: &str = "ingredient.unknownProvenance"; /// Hash of the ingredient’s C2PA Claim Signature box was successfully validated /// /// Any corresponding URL should point to a C2PA assertion. pub const INGREDIENT_CLAIM_SIGNATURE_VALIDATED: &str = "ingredient.claimSignature.validated"; // -- informational codes -- /// The validator chose not to perform an online OCSP check. /// /// Any corresponding URL should point to a C2PA claim signature box. pub const SIGNING_CREDENTIAL_OCSP_SKIPPED: &str = "signingCredential.ocsp.skipped"; /// The validator attempted to perform an online OCSP check, but did not receive /// a response. /// /// Any corresponding URL should point to a C2PA claim signature box. pub const SIGNING_CREDENTIAL_OCSP_INACCESSIBLE: &str = "signingCredential.ocsp.inaccessible"; /// The time-stamp does not correspond to the contents of the claim. /// /// Any corresponding URL should point to a C2PA claim signature box. pub const TIMESTAMP_MISMATCH: &str = "timeStamp.mismatch"; /// The time-stamp does not correspond to the contents of the claim. /// /// Any corresponding URL should point to a C2PA claim signature box. pub const TIMESTAMP_MALFORMED: &str = "timeStamp.malformed"; /// The signed time-stamp attribute in the signature falls outside the /// validity window of the signing certificate or the TSA's certificate. /// /// Any corresponding URL should point to a C2PA claim signature box. pub const TIMESTAMP_OUTSIDE_VALIDITY: &str = "timeStamp.outsideValidity"; /// The time-stamp credential is not listed on the validator's trust list. /// /// Any corresponding URL should point to a C2PA claim signature box. pub const TIMESTAMP_UNTRUSTED: &str = "timeStamp.untrusted"; /// The asset manifest cannot be interpreted by this version of the SDK. /// /// Any corresponding URL should point to a C2PA assertion. pub const MANIFEST_UNKNOWN_PROVENANCE: &str = "manifest.unknownProvenance"; /// The manifest is not referenced via an ingredient assertion. /// /// Any corresponding URL should point to a C2PA assertion. pub const MANIFEST_UNREFERENCED: &str = "manifest.unreferenced"; /// The algorithm has been deprecated. /// /// Any corresponding URL should point to a C2PA claim signature box. pub const ALGORITHM_DEPRECATED: &str = "algorithm.deprecated"; /// The claimed time of signing (in the iat header of the signature) /// is within the validity period of the claim signer’s certificate /// chain and before the time in any corresponding trusted timestamp /// /// Any corresponding URL should point to a C2PA claim signature box. pub const TIME_OF_SIGNING_INSIDE_VALIDITY: &str = "timeOfSigning.insideValidity"; // -- failure codes -- /// The claim cbor is invalid /// /// Any corresponding URL should point to a C2PA claim box. pub const CLAIM_MALFORMED: &str = "claim.malformed"; /// The referenced claim in the ingredient's manifest cannot be found. /// /// Any corresponding URL should point to a C2PA claim box. pub const CLAIM_MISSING: &str = "claim.missing"; /// More than one claim box is present in the manifest. /// /// Any corresponding URL should point to a C2PA claim box. pub const CLAIM_MULTIPLE: &str = "claim.multiple"; /// No hard bindings are present in the claim. /// /// Any corresponding URL should point to a C2PA claim box. pub const HARD_BINDINGS_MISSING: &str = "claim.hardBindings.missing"; // Multiple hard bindings are present in the claim. /// /// Any corresponding URL should point to a C2PA assertion. pub const HARD_BINDINGS_MULTIPLE: &str = "assertion.multipleHardBindings"; /// A required field is not present in the claim. /// /// Any corresponding URL should point to a C2PA claim box. pub const CLAIM_REQUIRED_MISSING: &str = "claim.required.missing"; /// The cbor of the claim is not valid. /// /// Any corresponding URL should point to a C2PA claim box. pub const CLAIM_CBOR_INVALID: &str = "claim.cbor.invalid"; /// The hash of the the referenced ingredient claim in the manifest /// does not match the corresponding hash in the ingredient's hashed /// URI in the claim. /// /// Any corresponding URL should point to a C2PA assertion. pub const INGREDIENT_HASHEDURI_MISMATCH: &str = "ingredient.hashedURI.mismatch"; /// The claim signature referenced in the ingredient's claim /// cannot be found in its manifest. /// /// Any corresponding URL should point to a C2PA claim signature box. pub const CLAIM_SIGNATURE_MISSING: &str = "claimSignature.missing"; /// The claim signature referenced in the ingredient's claim /// failed to validate. /// /// Any corresponding URL should point to a C2PA claim signature box. pub const CLAIM_SIGNATURE_MISMATCH: &str = "claimSignature.mismatch"; /// If a manifest was documented to exist in a remote location, /// but is not present there, or the location is not currently available /// (such as in an offline scenario), /// the `manifest.inaccessible` error code shall be used to report the /// situation. /// /// `ValidationStatus.url()` URI reference to the C2PA Manifest that could not /// be accessed. pub const MANIFEST_INACCESSIBLE: &str = "manifest.inaccessible"; /// The manifest has more than one ingredient whose `relationship` /// is `parentOf`. /// /// Any corresponding URL should point to a C2PA claim box. pub const MANIFEST_MULTIPLE_PARENTS: &str = "manifest.multipleParents"; /// The manifest is an update manifest, but it contains hard binding /// or actions assertions. /// /// Any corresponding URL should point to a C2PA claim box. pub const MANIFEST_UPDATE_INVALID: &str = "manifest.update.invalid"; /// The manifest is an update manifest, but it contains either zero /// or multiple `parentOf` ingredients. /// /// Any corresponding URL should point to a C2PA claim box. pub const MANIFEST_UPDATE_WRONG_PARENTS: &str = "manifest.update.wrongParents"; /// The signing credential is not listed on the validator's trust list. /// /// Any corresponding URL should point to a C2PA claim signature box. pub const SIGNING_CREDENTIAL_UNTRUSTED: &str = "signingCredential.untrusted"; /// The issuer of a CAWG identity claims aggregation (ICA) credential is not /// listed on the validator's configured list of trusted ICA issuers. /// /// This is informational and scoped to the individual CAWG identity /// assertion: it does not by itself render the enclosing C2PA manifest /// invalid or untrusted. The credential's `cawg.ica.credential_valid` success /// code is withheld when this is reported, so consumers should treat the /// absence of `cawg.ica.credential_valid` as the signal that the identity was /// not validated. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_ICA_UNTRUSTED_ISSUER: &str = "cawg.ica.untrusted_issuer"; /// The CAWG identity assertion CBOR is not valid. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_IDENTITY_CBOR_INVALID: &str = "cawg.identity.cbor.invalid"; /// The CAWG identity assertion signature type is unknown. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_IDENTITY_SIG_TYPE_UNKNOWN: &str = "cawg.identity.sig_type.unknown"; /// The CAWG identity assertion padding is invalid. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_IDENTITY_PAD_INVALID: &str = "cawg.identity.pad.invalid"; /// The CAWG identity assertion is well formed. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_IDENTITY_WELL_FORMED: &str = "cawg.identity.well-formed"; /// A referenced CAWG identity assertion is missing or has a mismatched hash. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_IDENTITY_ASSERTION_MISMATCH: &str = "cawg.identity.assertion.mismatch"; /// A CAWG identity assertion does not reference a hard-binding assertion. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_IDENTITY_HARD_BINDING_MISSING: &str = "cawg.identity.hard_binding_missing"; /// A CAWG identity assertion references the same assertion more than once. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_IDENTITY_ASSERTION_DUPLICATE: &str = "cawg.identity.assertion.duplicate"; /// A CAWG identity claims aggregation credential was validated. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_ICA_CREDENTIAL_VALID: &str = "cawg.ica.credential_valid"; /// A CAWG identity claims aggregation COSE_Sign1 structure is invalid. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_ICA_INVALID_COSE_SIGN1: &str = "cawg.ica.invalid_cose_sign1"; /// A CAWG identity claims aggregation credential uses an invalid algorithm. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_ICA_INVALID_ALG: &str = "cawg.ica.invalid_alg"; /// A CAWG identity claims aggregation credential has an invalid content type. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_ICA_INVALID_CONTENT_TYPE: &str = "cawg.ica.invalid_content_type"; /// A CAWG identity claims aggregation verifiable credential is invalid. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_ICA_INVALID_VERIFIABLE_CREDENTIAL: &str = "cawg.ica.invalid_verifiable_credential"; /// A CAWG identity claims aggregation issuer is invalid. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_ICA_INVALID_ISSUER: &str = "cawg.ica.invalid_issuer"; /// The DID document for a CAWG identity claims aggregation credential is unavailable. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_ICA_DID_UNAVAILABLE: &str = "cawg.ica.did_unavailable"; /// The DID document for a CAWG identity claims aggregation credential is invalid. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_ICA_INVALID_DID_DOCUMENT: &str = "cawg.ica.invalid_did_document"; /// The CAWG identity claims aggregation signature does not match. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_ICA_SIGNATURE_MISMATCH: &str = "cawg.ica.signature_mismatch"; /// The CAWG identity claims aggregation timestamp was validated. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_ICA_TIME_STAMP_VALIDATED: &str = "cawg.ica.time_stamp.validated"; /// The CAWG identity claims aggregation timestamp is invalid. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_ICA_TIME_STAMP_INVALID: &str = "cawg.ica.time_stamp.invalid"; /// The valid-from time is missing from a CAWG identity claims aggregation credential. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_ICA_VALID_FROM_MISSING: &str = "cawg.ica.valid_from.missing"; /// The valid-from time in a CAWG identity claims aggregation credential is invalid. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_ICA_VALID_FROM_INVALID: &str = "cawg.ica.valid_from.invalid"; /// The valid-until time in a CAWG identity claims aggregation credential is invalid. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_ICA_VALID_UNTIL_INVALID: &str = "cawg.ica.valid_until.invalid"; /// A CAWG identity claims aggregation signer payload does not match the asset. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_ICA_SIGNER_PAYLOAD_MISMATCH: &str = "cawg.ica.signer_payload.mismatch"; /// A chain of trust from the CAWG identity assertion's X.509 signing /// certificate to a trust anchor configuration was verified. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_X509_CREDENTIAL_TRUSTED: &str = "cawg.x509.credential.trusted"; /// A chain of trust could not be verified from the CAWG identity /// assertion's X.509 signing certificate to any configured trust anchor, /// or a CA certificate in the chain was determined to be revoked. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_X509_CREDENTIAL_UNTRUSTED: &str = "cawg.x509.credential.untrusted"; /// The CAWG identity assertion's X.509 signing certificate (or its /// certificate chain) does not meet the certificate profile requirements /// defined in the C2PA technical specification -- for example, an invalid /// EKU, an unsupported algorithm, or a disallowed self-signed certificate. /// /// This is distinct from [`CAWG_X509_CREDENTIAL_UNTRUSTED`]: a credential /// can fail to meet the certificate profile even when the underlying /// cryptographic signature is otherwise valid and independent of whether /// a trust chain could be established. /// /// This code is not yet part of the ratified CAWG identity assertion /// specification, which currently has no code distinct from /// [`CAWG_X509_CREDENTIAL_UNTRUSTED`] for this condition (CAI-13385 tracks /// proposing this addition upstream). /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_X509_CREDENTIAL_INVALID: &str = "cawg.x509.credential.invalid"; /// The cryptographic signature over a CAWG identity assertion using an /// X.509 credential was validated against the signing certificate. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_X509_SIGNATURE_VALIDATED: &str = "cawg.x509.signature.validated"; /// The cryptographic signature over a CAWG identity assertion using an /// X.509 credential could not be validated against the signing /// certificate. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_X509_SIGNATURE_MISMATCH: &str = "cawg.x509.signature.mismatch"; /// The signature algorithm used to sign a CAWG identity assertion using /// an X.509 credential is not on the allowed or deprecated list defined /// in the C2PA technical specification. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_X509_ALGORITHM_UNSUPPORTED: &str = "cawg.x509.algorithm.unsupported"; /// The time of signing falls outside the validity period of the named /// actor's X.509 certificate or one of the CA certificates up to the /// trust anchor. /// /// Any corresponding URL should point to a CAWG identity assertion. pub const CAWG_X509_SIGNATURE_OUTSIDE_VALIDITY: &str = "cawg.x509.signature.outside_validity"; /// The signing credential is not valid for signing. /// /// Any corresponding URL should point to a C2PA claim signature box. pub const SIGNING_CREDENTIAL_INVALID: &str = "signingCredential.invalid"; /// The signing credential has been revoked by the issuer. /// /// Any corresponding URL should point to a C2PA claim signature box. pub const SIGNING_CREDENTIAL_REVOKED: &str = "signingCredential.ocsp.revoked"; /// The signing credential has expired. /// /// Any corresponding URL should point to a C2PA claim signature box. pub const SIGNING_CREDENTIAL_EXPIRED: &str = "signingCredential.expired"; /// The hash of the the referenced assertion in the manifest does not /// match the corresponding hash in the assertion's hashed URI in the claim. /// /// Any corresponding URL should point to a C2PA assertion. pub const ASSERTION_HASHEDURI_MISMATCH: &str = "assertion.hashedURI.mismatch"; /// An assertion listed in the ingredient's claim is missing from the /// ingredient's manifest. /// /// Any corresponding URL should point to a C2PA claim box. pub const ASSERTION_MISSING: &str = "assertion.missing"; /// An assertion was found in the ingredient's manifest that was not /// explicitly declared in the ingredient's claim. /// /// Any corresponding URL should point to a C2PA claim box or assertion. pub const ASSERTION_UNDECLARED: &str = "assertion.undeclared"; /// A non-embedded (remote) assertion was inaccessible at the time of /// validation. /// /// Any corresponding URL should point to a C2PA assertion. pub const ASSERTION_INACCESSIBLE: &str = "assertion.inaccessible"; /// An assertion was declared as redacted in the ingredient's claim /// but is still present in the ingredient's manifest. /// /// Any corresponding URL should point to a C2PA assertion. pub const ASSERTION_NOT_REDACTED: &str = "assertion.notRedacted"; /// An assertion was declared as redacted by its own claim. /// /// Any corresponding URL should point to a C2PA claim box. pub const ASSERTION_SELF_REDACTED: &str = "assertion.selfRedacted"; /// A required field is not present in an assertion. /// /// Any corresponding URL should point to a C2PA assertion. pub const ASSERTION_REQUIRED_MISSING: &str = "assertion.required.missing"; /// The JSON(-LD) of an assertion is not valid. /// /// Any corresponding URL should point to a C2PA assertion. pub const ASSERTION_JSON_INVALID: &str = "assertion.json.invalid"; /// The cbor of an assertion is not valid. /// /// Any corresponding URL should point to a C2PA assertion. pub const ASSERTION_CBOR_INVALID: &str = "assertion.cbor.invalid"; /// An action that requires an associated ingredient either does not have one /// or the one specified cannot be located /// /// Any corresponding URL should point to a C2PA assertion. pub const ACTION_ASSERTION_INGREDIENT_MISMATCH: &str = "assertion.action.ingredientMismatch"; /// An `action` assertion was redacted when the ingredient's /// claim was created. /// /// Any corresponding URL should point to a C2PA assertion. pub const ACTION_ASSERTION_REDACTED: &str = "assertion.action.redacted"; /// An `action` assertion missing required soft binding /// /// Any corresponding URL should point to a C2PA assertion. pub const ACTION_ASSERTION_SOFTBINDING_MISSING: &str = "assertion.action.softBindingMissing"; /// The hash of a byte range of the asset does not match the /// hash declared in the data hash assertion. /// /// Any corresponding URL should point to a C2PA assertion. pub const ASSERTION_DATAHASH_MISMATCH: &str = "assertion.dataHash.mismatch"; /// The hash of a box-based asset does not match the hash declared /// in the BMFF hash assertion. /// /// Any corresponding URL should point to a C2PA assertion. pub const ASSERTION_BMFFHASH_MISMATCH: &str = "assertion.bmffHash.mismatch"; /// The hash of a box-based asset does not match the hash declared /// in the General Boxes hash assertion. /// /// Any corresponding URL should point to a C2PA assertion. pub const ASSERTION_BOXHASH_MISMATCH: &str = "assertion.boxesHash.mismatch"; /// The hash of a box-based asset does not contain boxes in the expected order /// for the General Boxes hash assertion. /// /// Any corresponding URL should point to a C2PA assertion. pub const ASSERTION_BOXHASH_UNKNOWN_BOX: &str = "assertion.boxesHash.unknownBox"; /// A hard binding assertion is in a cloud data assertion. /// /// Any corresponding URL should point to a C2PA assertion. pub const ASSERTION_CLOUD_DATA_HARD_BINDING: &str = "assertion.cloud-data.hardBinding"; /// An update manifest contains a cloud data assertion referencing /// an actions assertion. /// /// Any corresponding URL should point to a C2PA assertion. pub const ASSERTION_CLOUD_DATA_ACTIONS: &str = "assertion.cloud-data.actions"; /// A cloud data assertion was incomplete or malformed. /// /// Any corresponding URL should point to a C2PA assertion. pub const ASSERTION_CLOUD_DATA_MALFORMED: &str = "assertion.cloud-data.malformed"; /// An external reference assertion was incomplete or malformed. /// /// Any corresponding URL should point to a C2PA assertion. pub const ASSERTION_EXTERNAL_REFERENCE_MALFORMED: &str = "assertion.external-reference.malformed"; /// The value of an `alg` header, or other header that specifies an /// algorithm used to compute the value of another field, is unknown /// or unsupported. /// /// Any corresponding URL should point to a C2PA claim box or C2PA assertion. pub const ALGORITHM_UNSUPPORTED: &str = "algorithm.unsupported"; /// A value to be used when there was an error not specifically listed here. /// /// Any corresponding URL should point to a C2PA claim box or C2PA assertion. pub const GENERAL_ERROR: &str = "general.error"; /// The claim signature referenced in the claim was created outside the validity /// period of the signing credential /// /// Any corresponding URL should point to a C2PA claim signature box. pub const CLAIM_SIGNATURE_OUTSIDE_VALIDITY: &str = "claimSignature.outsideValidity"; /// The manifest is a time-stamp manifest, but it contains a /// disallowed (non-ingredient) assertion. /// /// Any corresponding URL should point to a C2PA claim box. pub const MANIFEST_TIMESTAMP_INVALID: &str = "manifest.timestamp.invalid"; ///The manifest is an time-stamp manifest, but it contains either zero or /// multiple parentOf ingredients. /// /// Any corresponding URL should point to a C2PA claim box. pub const MANIFEST_TIMESTAMP_WRONG_PARENTS: &str = "manifest.timestamp.wrongParents"; /// The compressed manifest was not valid. /// /// Any corresponding URL should point to a C2PA claim box. pub const MANIFEST_COMPRESSED_INVALID: &str = "manifest.compressed.invalid"; /// The OCSP response contains an unknown status for the signing credential. /// /// Any corresponding URL should point to a C2PA claim signature box. pub const SIGNING_CREDENTIAL_OCSP_UNKNOWN: &str = "signingCredential.ocsp.unknown"; /// An assertion listed in the claim is not in the same C2PA Manifest as /// the claim. /// /// Any corresponding URL should point to a C2PA claim box. pub const ASSERTION_OUTSIDE_MANIFEST: &str = "assertion.outsideManifest"; /// An actions assertion is malformed. /// /// Any corresponding URL should point to a C2PA assertion box. pub const ASSERTION_ACTION_MALFORMED: &str = "assertion.action.malformed"; /// An actions assertion ingredient malformed. /// /// Any corresponding URL should point to a C2PA assertion box. pub const ASSERTION_ACTION_INGREDIENT_MISMATCH: &str = "assertion.action.ingredientMismatch"; /// An action that requires an associated redaction either does not have one /// or the one specified cannot be located /// /// Any corresponding URL should point to a C2PA assertion box. pub const ASSERTION_ACTION_REDACTION_MISMATCH: &str = "assertion.action.redactionMismatch"; /// An actions assertion was redacted when the claim was created. /// /// Any corresponding URL should point to a C2PA assertion. pub const ASSERTION_ACTION_REDACTED: &str = "assertion.action.redacted"; /// A data hash assertion is malformed. /// /// Any corresponding URL should point to a C2PA assertion box. pub const ASSERTION_DATAHASH_MALFORMED: &str = "assertion.dataHash.malformed"; /// A hard binding assertion was redacted when the claim was created. /// /// Any corresponding URL should point to a C2PA assertion box. #[deprecated( since = "0.91.0", note = "This status code is deprecated from C2PA spec version 2.3. Use `ASSERTION_HARDBINDING_REDACTED` instead. Will be removed in 0.92.0 (scheduled for mid-November 2026)." )] pub const ASSERTION_DATAHASH_REDACTED: &str = "assertion.dataHash.redacted"; /// A hard binding assertion was redacted when the claim was created. /// /// Any corresponding URL should point to a C2PA assertion box. Replaces ASSERTION_DATAHASH_REDACTED /// in 2.3 forward pub const ASSERTION_HARDBINDING_REDACTED: &str = "assertion.hardBinding.redacted"; /// A BMFF hash assertion is malformed. /// /// Any corresponding URL should point to a C2PA assertion box. pub const ASSERTION_BMFFHASH_MALFORMED: &str = "assertion.bmffHash.malformed"; /// A Box hash assertion is malformed. /// /// Any corresponding URL should point to a C2PA assertion box. pub const ASSERTION_BOXESHASH_MALFORMED: &str = "assertion.boxesHash.malformed"; /// A hash of an asset in the collection does not match hash declared in /// the collection data hash assertion. /// /// Any corresponding URL should point to a C2PA assertion box. pub const ASSERTION_COLLECTIONHASH_MISMATCH: &str = "assertion.collectionHash.mismatch"; /// An asset that was listed in the collection data hash assertion is /// missing from the collection. /// /// Any corresponding URL should point to a C2PA assertion box. pub const ASSERTION_COLLECTIONHASH_INCORRECT_FILE_COUNT: &str = "assertion.collectionHash.incorrectFileCount"; /// A URI of an asset in the collection data hash assertion contains /// the file part '..' or '.'. /// /// Any corresponding URL should point to a C2PA assertion box. pub const ASSERTION_COLLECTIONHASH_INVALID_URI: &str = "assertion.collectionHash.invalidURI"; /// The collection hash assertion was incomplete. /// /// Any corresponding URL should point to a C2PA assertion box. pub const ASSERTION_COLLECTIONHASH_MALFORMED: &str = "assertion.collectionHash.malformed"; /// The ingredient assertion was incomplete. /// /// Any corresponding URL should point to a C2PA assertion box. pub const ASSERTION_INGREDIENT_MALFORMED: &str = "assertion.ingredient.malformed"; /// The C2PA metadata assertion contains a field that is not /// allowed by this specification. /// /// Any corresponding URL should point to a C2PA assertion box. pub const ASSERTION_METADATA_DISALLOWED: &str = "assertion.metadata.disallowed"; /// The referenced ingredient C2PA Claim Signature was not found. /// /// Any corresponding URL should point to a C2PA assertion box. pub const INGREDIENT_MANIFEST_MISSING: &str = "ingredient.manifest.missing"; /// The hash of an embedded C2PA Manifest does not match the hash declared in /// the hashed_uri value of the activeManifest field in the ingredient /// assertion. /// /// Any corresponding URL should point to a C2PA assertion box. pub const INGREDIENT_MANIFEST_MISMATCH: &str = "ingredient.manifest.mismatch"; /// The referenced ingredient C2PA Claim Signature was not found. /// /// Any corresponding URL should point to a C2PA assertion box. pub const INGREDIENT_CLAIM_SIGNATURE_MISSING: &str = "ingredient.claimSignature.missing"; /// The hash of an embedded C2PA Manifest’s C2PA Claim Signature does not match /// the hash declared in the hashed_uri value of the claimSignature field in the /// ingredient assertion. /// /// Any corresponding URL should point to a C2PA assertion box. pub const INGREDIENT_CLAIM_SIGNATURE_MISMATCH: &str = "ingredient.claimSignature.mismatch"; /// The data pointed to by a hashed_uri cannot be located. /// /// Any corresponding URL should point to a C2PA assertion box. pub const HASHED_URI_MISSING: &str = "hashedURI.missing"; /// The hash of a given hashed_uri does not match the corresponding hash /// of the destination URI’s data /// /// Any corresponding URL should point to a C2PA assertion box. pub const HASHED_URI_MISMATCH: &str = "hashedURI.mismatch"; /// The timestamp assertion is malformed. /// /// Any corresponding URL should point to a C2PA assertion box. pub const ASSERTION_TIMESTAMP_MALFORMED: &str = "assertion.timestamp.malformed"; /// Returns `true` if the status code is a known C2PA success status code. /// /// Returns `false` if the status code is a known C2PA failure status /// code or is unknown. /// /// ## Examples /// /// ``` /// use c2pa::validation_results::validation_codes::*; /// /// assert!(is_success(CLAIM_SIGNATURE_VALIDATED)); /// assert!(!is_success(SIGNING_CREDENTIAL_REVOKED)); /// ``` pub fn is_success(status_code: &str) -> bool { matches!(log_kind(status_code), LogKind::Success) } /// Returns the [`LogKind`] for a given status code. // TODO: This needs to be expanded to include all status codes. pub fn log_kind(status_code: &str) -> LogKind { match status_code { CLAIM_SIGNATURE_VALIDATED | CLAIM_SIGNATURE_INSIDE_VALIDITY | SIGNING_CREDENTIAL_TRUSTED | SIGNING_CREDENTIAL_NOT_REVOKED | TIMESTAMP_TRUSTED | TIMESTAMP_VALIDATED | ASSERTION_HASHEDURI_MATCH | ASSERTION_DATAHASH_MATCH | ASSERTION_BMFFHASH_MATCH | ASSERTION_ACCESSIBLE | ASSERTION_BOXHASH_MATCH | ASSERTION_COLLECTIONHASH_MATCH | INGREDIENT_MANIFEST_VALIDATED | INGREDIENT_MANIFEST_MISSING | INGREDIENT_CLAIM_SIGNATURE_VALIDATED | CAWG_X509_CREDENTIAL_TRUSTED | CAWG_X509_SIGNATURE_VALIDATED => LogKind::Success, SIGNING_CREDENTIAL_OCSP_SKIPPED | SIGNING_CREDENTIAL_OCSP_INACCESSIBLE | SIGNING_CREDENTIAL_OCSP_UNKNOWN | TIMESTAMP_UNTRUSTED | TIMESTAMP_OUTSIDE_VALIDITY | TIMESTAMP_MISMATCH | TIMESTAMP_MALFORMED | MANIFEST_UNKNOWN_PROVENANCE | ALGORITHM_DEPRECATED | TIME_OF_SIGNING_INSIDE_VALIDITY | INGREDIENT_PROVENANCE_UNKNOWN | ASSERTION_DATAHASH_ADDITIONAL_EXCLUSIONS | ASSERTION_BMFFHASH_ADDITIONAL_EXCLUSIONS | ASSERTION_BOXHASH_ADDITIONAL_EXCLUSIONS | CAWG_ICA_UNTRUSTED_ISSUER => LogKind::Informational, _ => LogKind::Failure, } } } #[cfg(test)] pub mod tests { #![allow(clippy::unwrap_used)] use super::*; use crate::{ assertions, claim::Claim, jumbf::labels, log_item, validation_status::{ ASSERTION_ACTION_MALFORMED, ASSERTION_DATAHASH_MISMATCH, ASSERTION_HASHEDURI_MISMATCH, ASSERTION_INGREDIENT_MALFORMED, CAWG_X509_ALGORITHM_UNSUPPORTED, CAWG_X509_CREDENTIAL_INVALID, CAWG_X509_CREDENTIAL_UNTRUSTED, CAWG_X509_SIGNATURE_MISMATCH, CAWG_X509_SIGNATURE_OUTSIDE_VALIDITY, CLAIM_MALFORMED, CLAIM_SIGNATURE_INSIDE_VALIDITY, CLAIM_SIGNATURE_VALIDATED, SIGNING_CREDENTIAL_EXPIRED, SIGNING_CREDENTIAL_TRUSTED, SIGNING_CREDENTIAL_UNTRUSTED, }, HashedUri, Relationship, }; #[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_cawg_x509_failure() { // No CAWG X.509 identity assertion failure code -- whatever the // underlying problem -- should by itself render the enclosing // manifest invalid; each is scoped to that identity assertion. for code in [ CAWG_X509_ALGORITHM_UNSUPPORTED, CAWG_X509_CREDENTIAL_UNTRUSTED, CAWG_X509_CREDENTIAL_INVALID, CAWG_X509_SIGNATURE_MISMATCH, CAWG_X509_SIGNATURE_OUTSIDE_VALIDITY, ] { 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(code)); assert_eq!( validation_results.validation_state(), ValidationState::Valid, "expected Valid state for failure code {code}" ); } } #[test] fn not_trusted_state_with_cawg_identity_failure() { // No CAWG identity assertion failure code -- whatever the underlying // problem -- should by itself render the enclosing manifest invalid; // each is scoped to that identity assertion. for code in [ "cawg.identity.cbor.invalid", "cawg.identity.sig_type.unknown", "cawg.identity.pad.invalid", ] { 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(code)); assert_eq!( validation_results.validation_state(), ValidationState::Valid, "expected Valid state for failure code {code}" ); } } #[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 ); } #[test] fn failure_summary_active_manifest_failure() { let mut validation_results = ValidationResults::default(); validation_results.add_status(ValidationStatus::new_failure(ASSERTION_DATAHASH_MISMATCH)); assert_eq!( validation_results.validation_state(), ValidationState::Invalid ); assert_eq!( validation_results.failure_summary().to_string(), "failures:\n assertion.dataHash.mismatch" ); } #[test] fn failure_summary_ingredient_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(ASSERTION_DATAHASH_MISMATCH) .set_ingredient_uri("urn:uuid:1234"), ); assert_eq!( validation_results.validation_state(), ValidationState::Invalid ); assert_eq!( validation_results.failure_summary().to_string(), "ingredient [urn:uuid:1234] failures:\n assertion.dataHash.mismatch" ); } #[test] fn failure_summary_filters_untrusted_ingredient() { 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("urn:uuid:1234"), ); assert_eq!( validation_results.validation_state(), ValidationState::Valid ); assert_eq!(validation_results.failure_summary().to_string(), ""); } #[test] fn validation_results_display() { 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(validation_status::TIMESTAMP_MALFORMED) .set_kind(LogKind::Informational), ); validation_results.add_status( ValidationStatus::new_failure(CLAIM_MALFORMED) .set_url("self#jumbf=c2pa/urn:uuid:abc/c2pa.claim") .set_explanation("claim CBOR failed to decode".to_string()), ); validation_results.add_status( ValidationStatus::new_failure(ASSERTION_DATAHASH_MISMATCH) .set_ingredient_uri("urn:uuid:abcd"), ); assert_eq!( validation_results.validation_state(), ValidationState::Invalid ); assert_eq!( validation_results.to_string(), concat!( "spec validation version: 2.4.0\n", "state: Invalid\n", " success: claimSignature.validated, claimSignature.insideValidity\n", " informational:\n", " timeStamp.malformed\n", " failure:\n", " claim.malformed: claim CBOR failed to decode (self#jumbf=c2pa/urn:uuid:abc/c2pa.claim)\n", " ingredient [urn:uuid:abcd]:\n", " failure:\n", " assertion.dataHash.mismatch\n", ) ); } #[test] fn from_store_attested_ingredient_failure_after_serde_roundtrip() { let inner_label = "urn:uuid:inner-test"; let inner_manifest_uri = labels::to_manifest_uri(inner_label); let assertion_url = format!("{inner_manifest_uri}/c2pa.assertions/org.test.data"); let mut attested = ValidationResults::default(); attested.add_status( ValidationStatus::new_failure(ASSERTION_HASHEDURI_MISMATCH).set_url(&assertion_url), ); let ingredient = Ingredient { relationship: Relationship::ComponentOf, version: 3, active_manifest: Some(HashedUri::new( inner_manifest_uri.clone(), Some("sha256".into()), &[0u8; 32], )), validation_results: Some(attested), ..Default::default() }; let mut outer_claim = Claim::new("test-generator", None, 2); outer_claim.add_assertion(&ingredient).unwrap(); let outer_label = outer_claim.label().to_string(); // Use the ingredient assertion's real URI, exactly as `ingredient_checks` pushes it. let ingredient_uri = labels::to_assertion_uri( &outer_label, &outer_claim.ingredient_assertions()[0].label(), ); let mut store = Store::new(); store.insert_restored_claim(outer_label.clone(), outer_claim); let mut tracker = StatusTracker::default(); tracker.push_ingredient_uri(ingredient_uri); let _ = log_item!( assertion_url.clone(), "hash does not match assertion data", "verify_internal" ) .validation_status(ASSERTION_HASHEDURI_MISMATCH) .failure(&mut tracker, "hash mismatch"); tracker.pop_ingredient_uri(); let results = ValidationResults::from_store(&store, &tracker); let delta_failures: Vec<_> = results .ingredient_deltas .as_ref() .map(|deltas| { deltas .iter() .flat_map(|deltas| deltas.validation_deltas().failure().iter()) .collect() }) .unwrap_or_default(); // check that there are no failures since they were attested to assert!(delta_failures.is_empty()); } #[test] fn from_store_unreferenced_claim_cannot_suppress_referenced_failure() { // Active claim genuinely nests a victim ingredient. Live validation finds a // general tampering failure (dataHash mismatch) on the victim. A wholly // UNREFERENCED attacker claim forges an ingredient assertion carrying the same // (code,url) failure -> it must NOT suppress the genuine live finding. De-dup is // scoped to the ingredient that recorded a status (by ingredient_uri), so the // unrelated claim keys into a different URI and cannot cancel this finding. let victim_label = "urn:uuid:victim"; let victim_manifest_uri = labels::to_manifest_uri(victim_label); let victim_assertion_url = format!("{victim_manifest_uri}/c2pa.assertions/c2pa.hash.data"); let mut outer_claim = Claim::new("test-generator", None, 2); let outer_label = outer_claim.label().to_string(); let victim_ingredient = Ingredient { relationship: Relationship::ComponentOf, version: 3, active_manifest: Some(HashedUri::new( victim_manifest_uri, Some("sha256".into()), &[0u8; 32], )), validation_results: Some(ValidationResults::default()), ..Default::default() }; outer_claim.add_assertion(&victim_ingredient).unwrap(); // The ingredient assertion's real URI, exactly as `ingredient_checks` pushes it. let ingredient_uri = labels::to_assertion_uri( &outer_label, &outer_claim.ingredient_assertions()[0].label(), ); // Unreferenced attacker claim forging the matching failure. let mut attacker_claim = Claim::new("attacker-generator", None, 2); let attacker_label = attacker_claim.label().to_string(); let mut forged = ValidationResults::default(); forged.add_status( ValidationStatus::new_failure(ASSERTION_DATAHASH_MISMATCH) .set_url(&victim_assertion_url), ); let attacker_ingredient = Ingredient { relationship: Relationship::ComponentOf, version: 3, active_manifest: Some(HashedUri::new( labels::to_manifest_uri("urn:uuid:attacker-unrelated"), Some("sha256".into()), &[0u8; 32], )), validation_results: Some(forged), ..Default::default() }; attacker_claim.add_assertion(&attacker_ingredient).unwrap(); let mut store = Store::new(); // Insert the attacker claim first so the outer/victim-referencing claim is the // active (provenance) manifest, matching the real scenario. store.insert_restored_claim(attacker_label, attacker_claim); store.insert_restored_claim(outer_label.clone(), outer_claim); // The live re-check of the referenced victim genuinely finds a hash mismatch. let mut tracker = StatusTracker::default(); tracker.push_ingredient_uri(ingredient_uri); let _ = log_item!( victim_assertion_url.clone(), "hash mismatch", "verify_internal" ) .validation_status(ASSERTION_DATAHASH_MISMATCH) .failure(&mut tracker, "hash mismatch"); tracker.pop_ingredient_uri(); let results = ValidationResults::from_store(&store, &tracker); assert!( results .validation_errors() .unwrap_or_default() .iter() .any(|s| s.code() == ASSERTION_DATAHASH_MISMATCH), "unreferenced claim suppressed a genuine referenced-claim failure" ); } #[test] fn from_store_ingredient_cannot_suppress_active_manifest_failure() { // CAI-12751: an attacker-authored ingredient assertion must not be able to cancel a // genuine failure of the *active* manifest by attesting a status with the same // code + url. The active manifest's failure is logged against its bare claim label // (`urn:c2pa:...`) with no `ingredient_uri`, so it must survive de-duplication // against ingredient-attested statuses (whereas the sibling test above confirms a // genuinely ingredient-scoped failure is still deduped). let mut outer_claim = Claim::new("test-generator", None, 2); let outer_label = outer_claim.label().to_string(); // Attacker plants a validation status inside an ingredient that "attests" the exact // failure (code + url) the active manifest will produce live during validation. let mut attested = ValidationResults::default(); attested.add_status( ValidationStatus::new_failure(ASSERTION_ACTION_MALFORMED).set_url(&outer_label), ); let inner_manifest_uri = labels::to_manifest_uri("urn:uuid:inner-test"); let ingredient = Ingredient { relationship: Relationship::ComponentOf, version: 3, active_manifest: Some(HashedUri::new( inner_manifest_uri, Some("sha256".into()), &[0u8; 32], )), validation_results: Some(attested), ..Default::default() }; outer_claim.add_assertion(&ingredient).unwrap(); let mut store = Store::new(); store.insert_restored_claim(outer_label.clone(), outer_claim); let mut tracker = StatusTracker::default(); // Active-manifest signature checks pass, so the malformed-actions failure is the only // thing keeping this asset out of the `Valid` state. log_item!(outer_label.clone(), "claim signature valid", "verify") .validation_status(CLAIM_SIGNATURE_VALIDATED) .success(&mut tracker); log_item!( outer_label.clone(), "claim signature inside validity", "verify" ) .validation_status(CLAIM_SIGNATURE_INSIDE_VALIDITY) .success(&mut tracker); // A benign ingredient-scoped status ensures the de-duplication path actually runs (it // only runs when some status references an ingredient), so this test exercises the // retain filter rather than the early-out. let ingredient_uri = labels::to_assertion_uri(&outer_label, assertions::labels::INGREDIENT); tracker.push_ingredient_uri(ingredient_uri.clone()); log_item!( ingredient_uri.clone(), "ingredient signature valid", "verify" ) .validation_status(CLAIM_SIGNATURE_VALIDATED) .success(&mut tracker); tracker.pop_ingredient_uri(); // The genuine active-manifest failure, logged against the bare claim label with no // ingredient_uri — the exact status the attacker's attestation tries to cancel. let _ = log_item!( outer_label.clone(), "first action must be created or opened", "verify_actions" ) .validation_status(ASSERTION_ACTION_MALFORMED) .failure(&mut tracker, "malformed actions"); let results = ValidationResults::from_store(&store, &tracker); // The active-manifest failure must be retained (not suppressed by the ingredient)... let active_failures: Vec<&str> = results .active_manifest .as_ref() .map(|sc| sc.failure().iter().map(|s| s.code()).collect()) .unwrap_or_default(); assert!( active_failures.contains(&ASSERTION_ACTION_MALFORMED), "active-manifest failure was suppressed by ingredient attestation: {active_failures:?}" ); // ...so the overall state stays Invalid rather than being upgraded to Valid. assert_eq!(results.validation_state(), ValidationState::Invalid); } #[test] fn from_store_ingredient_cannot_suppress_active_manifest_ingredient_failure() { // `Store::ingredient_checks` logs some active-manifest failures // (e.g. `assertion.ingredient.malformed`) *while an ingredient URI is pushed*, so they // carry `ingredient_uri = Some` together with a URL pointing inside the active manifest. // Such a failure must not be cancellable by an ingredient that attests a matching // code + url, even though it was logged inside an ingredient scope. This is the corner // that a purely `ingredient_uri`-based guard would miss. let mut outer_claim = Claim::new("test-generator", None, 2); let outer_label = outer_claim.label().to_string(); // The active manifest's own finding is recorded against one of its ingredient // assertions — this is the URL `ingredient_checks` uses. let active_ingredient_uri = labels::to_assertion_uri(&outer_label, assertions::labels::INGREDIENT); // Attacker adds an ingredient whose attested status carries the exact code + url of that // active-manifest finding. let mut attested = ValidationResults::default(); attested.add_status( ValidationStatus::new_failure(ASSERTION_INGREDIENT_MALFORMED) .set_url(&active_ingredient_uri), ); let ingredient = Ingredient { relationship: Relationship::ComponentOf, version: 3, active_manifest: Some(HashedUri::new( labels::to_manifest_uri("urn:uuid:attacker-inner"), Some("sha256".into()), &[0u8; 32], )), validation_results: Some(attested), ..Default::default() }; outer_claim.add_assertion(&ingredient).unwrap(); let mut store = Store::new(); store.insert_restored_claim(outer_label.clone(), outer_claim); let mut tracker = StatusTracker::default(); // Active-manifest signature checks pass, so the ingredient-malformed finding is the only // thing keeping the asset out of `Valid`. log_item!(outer_label.clone(), "claim signature valid", "verify") .validation_status(CLAIM_SIGNATURE_VALIDATED) .success(&mut tracker); log_item!( outer_label.clone(), "claim signature inside validity", "verify" ) .validation_status(CLAIM_SIGNATURE_INSIDE_VALIDITY) .success(&mut tracker); // The genuine active-manifest finding, logged exactly the way `ingredient_checks` logs // it: inside an ingredient push, with a URL inside the active manifest. tracker.push_ingredient_uri(active_ingredient_uri.clone()); let _ = log_item!( active_ingredient_uri.clone(), "ingredient V3 must have validation results", "ingredient_checks" ) .validation_status(ASSERTION_INGREDIENT_MALFORMED) .failure(&mut tracker, "ingredient V3 missing validation status"); tracker.pop_ingredient_uri(); let results = ValidationResults::from_store(&store, &tracker); // The finding must survive de-duplication (it lands in ingredientDeltas because it is // scoped to an ingredient)... let failures = results.validation_errors().unwrap_or_default(); assert!( failures .iter() .any(|s| s.code() == ASSERTION_INGREDIENT_MALFORMED), "active-manifest ingredient failure was suppressed by ingredient attestation: {:?}", failures.iter().map(|s| s.code()).collect::>() ); // ...so the overall state stays Invalid rather than being upgraded to Valid. assert_eq!(results.validation_state(), ValidationState::Invalid); } #[test] fn from_store_suppresses_ingredient_cert_expiry_attested_inside_validity() { let mut outer_claim = Claim::new("test-generator", None, 2); let outer_label = outer_claim.label().to_string(); let inner_manifest_uri = labels::to_manifest_uri("urn:uuid:inner-test"); let inner_signature_uri = labels::to_signature_uri("urn:uuid:inner-test"); let mut attested = ValidationResults::default(); attested.add_status( ValidationStatus::new(CLAIM_SIGNATURE_VALIDATED) .set_kind(LogKind::Success) .set_url(&inner_signature_uri), ); attested.add_status( ValidationStatus::new(CLAIM_SIGNATURE_INSIDE_VALIDITY) .set_kind(LogKind::Success) .set_url(&inner_signature_uri), ); let ingredient = Ingredient { relationship: Relationship::ComponentOf, version: 3, active_manifest: Some(HashedUri::new( inner_manifest_uri, Some("sha256".into()), &[0u8; 32], )), validation_results: Some(attested), ..Default::default() }; outer_claim.add_assertion(&ingredient).unwrap(); // Use the ingredient assertion's real URI, exactly as `ingredient_checks` pushes it. let ingredient_uri = labels::to_assertion_uri( &outer_label, &outer_claim.ingredient_assertions()[0].label(), ); let mut store = Store::new(); store.insert_restored_claim(outer_label.clone(), outer_claim); let mut tracker = StatusTracker::default(); log_item!(outer_label.clone(), "claim signature valid", "verify") .validation_status(CLAIM_SIGNATURE_VALIDATED) .success(&mut tracker); log_item!( outer_label.clone(), "claim signature inside validity", "verify" ) .validation_status(CLAIM_SIGNATURE_INSIDE_VALIDITY) .success(&mut tracker); tracker.push_ingredient_uri(ingredient_uri); let _ = log_item!( inner_signature_uri.clone(), "certificate expired", "check_certificate_profile" ) .validation_status(SIGNING_CREDENTIAL_EXPIRED) .failure(&mut tracker, "certificate expired"); tracker.pop_ingredient_uri(); let results = ValidationResults::from_store(&store, &tracker); assert!( results .validation_errors() .unwrap_or_default() .iter() .all(|status| status.code() != SIGNING_CREDENTIAL_EXPIRED), "ingredient attestation should suppress nested expiry delta: {:?}", results.validation_errors() ); assert_eq!(results.validation_state(), ValidationState::Valid); } #[test] fn from_store_cannot_be_suppressed_by_unrelated_ingredient_attesting_foreign_manifest_label() { // Two independent claims in the store: `outer_claim` genuinely nests an ingredient whose // manifest ("urn:uuid:victim") has since had its certificate expire, and `attacker_claim` // is a wholly unrelated claim (not part of the active manifest's real ingredient chain) // that happens to carry an ingredient assertion whose own `active_manifest` differs, but // which forges a `claimSignature.insideValidity` attestation *for the victim's signature // URL*. Suppression must not be triggerable merely by an unrelated ingredient assertion // naming the same manifest label elsewhere in the store — it must only cancel a delta for // the ingredient that genuinely, cryptographically attests it. let victim_manifest_uri = labels::to_manifest_uri("urn:uuid:victim"); let victim_signature_uri = labels::to_signature_uri("urn:uuid:victim"); let mut outer_claim = Claim::new("test-generator", None, 2); let outer_label = outer_claim.label().to_string(); // The outer/active claim genuinely nests the victim ingredient, with no attestation of // its own -- so the *only* claimed insideValidity for the victim's signature URL is the // forged one added below via a completely separate claim. let victim_ingredient = Ingredient { relationship: Relationship::ComponentOf, version: 3, active_manifest: Some(HashedUri::new( victim_manifest_uri, Some("sha256".into()), &[0u8; 32], )), // v3 ingredients require `validation_results` to be present, but it carries no // `claimSignature.insideValidity` attestation of its own here. validation_results: Some(ValidationResults::default()), ..Default::default() }; outer_claim.add_assertion(&victim_ingredient).unwrap(); // The ingredient assertion's real URI, exactly as `ingredient_checks` pushes it. let ingredient_uri = labels::to_assertion_uri( &outer_label, &outer_claim.ingredient_assertions()[0].label(), ); // A wholly separate claim, unrelated to the outer/active manifest's real ingredient // chain, whose own ingredient assertion just happens to forge an attestation naming the // victim's signature URL. let mut attacker_claim = Claim::new("attacker-generator", None, 2); let attacker_label = attacker_claim.label().to_string(); let mut forged = ValidationResults::default(); forged.add_status( ValidationStatus::new(CLAIM_SIGNATURE_INSIDE_VALIDITY) .set_kind(LogKind::Success) .set_url(&victim_signature_uri), ); let attacker_ingredient = Ingredient { relationship: Relationship::ComponentOf, version: 3, active_manifest: Some(HashedUri::new( labels::to_manifest_uri("urn:uuid:attacker-unrelated"), Some("sha256".into()), &[0u8; 32], )), validation_results: Some(forged), ..Default::default() }; attacker_claim.add_assertion(&attacker_ingredient).unwrap(); let mut store = Store::new(); // Insert the attacker claim first so the outer/victim-referencing claim is the // active (provenance) manifest, matching the real scenario. store.insert_restored_claim(attacker_label, attacker_claim); store.insert_restored_claim(outer_label.clone(), outer_claim); let mut tracker = StatusTracker::default(); log_item!(outer_label.clone(), "claim signature valid", "verify") .validation_status(CLAIM_SIGNATURE_VALIDATED) .success(&mut tracker); log_item!( outer_label.clone(), "claim signature inside validity", "verify" ) .validation_status(CLAIM_SIGNATURE_INSIDE_VALIDITY) .success(&mut tracker); // The live re-check of the victim ingredient genuinely finds its certificate has expired. tracker.push_ingredient_uri(ingredient_uri); let _ = log_item!( victim_signature_uri.clone(), "certificate expired", "check_certificate_profile" ) .validation_status(SIGNING_CREDENTIAL_EXPIRED) .failure(&mut tracker, "certificate expired"); tracker.pop_ingredient_uri(); let results = ValidationResults::from_store(&store, &tracker); // The genuine, live-detected expiry for the victim manifest must survive: a forged // attestation from an unrelated ingredient elsewhere in the store must not be able to // suppress it merely by naming the same manifest/signature URL. assert!( results .validation_errors() .unwrap_or_default() .iter() .any(|status| status.code() == SIGNING_CREDENTIAL_EXPIRED), "an unrelated ingredient's forged attestation suppressed a genuine expiry finding: {:?}", results.validation_errors() ); } // Variant of the test above where the attacker's UNREFERENCED ingredient claims the // victim's own label as its `active_manifest` (own_label == victim) and forges an // `insideValidity` attestation for it. The old label-set expiry guard would have // admitted this (self-attestation for own_label) and suppressed the genuine expiry; // scoping the guard to the recording ingredient's URI must reject it. #[test] fn from_store_unreferenced_insidevalidity_cannot_suppress_expiry() { let victim_manifest_uri = labels::to_manifest_uri("urn:uuid:victim"); let victim_signature_uri = labels::to_signature_uri("urn:uuid:victim"); let mut outer_claim = Claim::new("test-generator", None, 2); let outer_label = outer_claim.label().to_string(); let victim_ingredient = Ingredient { relationship: Relationship::ComponentOf, version: 3, active_manifest: Some(HashedUri::new( victim_manifest_uri.clone(), Some("sha256".into()), &[0u8; 32], )), validation_results: Some(ValidationResults::default()), ..Default::default() }; outer_claim.add_assertion(&victim_ingredient).unwrap(); let ingredient_uri = labels::to_assertion_uri( &outer_label, &outer_claim.ingredient_assertions()[0].label(), ); // Unreferenced attacker claim whose ingredient claims the victim's own label and // forges an insideValidity attestation for the victim signature. let mut attacker_claim = Claim::new("attacker-generator", None, 2); let attacker_label = attacker_claim.label().to_string(); let mut forged = ValidationResults::default(); forged.add_status( ValidationStatus::new(CLAIM_SIGNATURE_INSIDE_VALIDITY) .set_kind(LogKind::Success) .set_url(&victim_signature_uri), ); let attacker_ingredient = Ingredient { relationship: Relationship::ComponentOf, version: 3, active_manifest: Some(HashedUri::new( victim_manifest_uri, Some("sha256".into()), &[0u8; 32], )), validation_results: Some(forged), ..Default::default() }; attacker_claim.add_assertion(&attacker_ingredient).unwrap(); let mut store = Store::new(); // Insert the attacker claim first so the outer/victim-referencing claim is the // active (provenance) manifest, matching the real scenario. store.insert_restored_claim(attacker_label, attacker_claim); store.insert_restored_claim(outer_label.clone(), outer_claim); let mut tracker = StatusTracker::default(); tracker.push_ingredient_uri(ingredient_uri); let _ = log_item!( victim_signature_uri.clone(), "certificate expired", "verify" ) .validation_status(SIGNING_CREDENTIAL_EXPIRED) .failure(&mut tracker, "certificate expired"); tracker.pop_ingredient_uri(); let results = ValidationResults::from_store(&store, &tracker); assert!( results .validation_errors() .unwrap_or_default() .iter() .any(|status| status.code() == SIGNING_CREDENTIAL_EXPIRED), "an unreferenced insideValidity attestation suppressed a genuine expiry finding" ); } // Deeply nested ingredients across a mix of ingredient versions: // A (active) --v3--> B --v2--> C --v1--> D // Each ingredient records its referenced manifest's (untrusted) signer status. The // live re-check reproduces all three, and they must de-duplicate across every level // and version (matched by content, since nested ingredients carry historical URIs), // leaving no ingredient-delta failures so the trusted active manifest is Trusted. #[test] fn from_store_deeply_nested_mixed_version_ingredients_dedup() { // Build bottom-up so each parent references its child's real generated label. let d_claim = Claim::new("gen-d", None, 1); let d_label = d_claim.label().to_string(); // C --v1 ingredient--> D (v1 carries `validation_status`, not `validation_results`) let mut c_claim = Claim::new("gen-c", None, 1); let c_label = c_claim.label().to_string(); c_claim .add_assertion(&Ingredient { relationship: Relationship::ComponentOf, version: 1, // v1/v2 ingredients require title/format and reference via `c2pa_manifest`, // recording history in `validation_status` (only v3 uses validation_results). title: Some("D".to_string()), format: Some("image/jpeg".to_string()), instance_id: Some("xmp:iid:level-d".to_string()), // v1 ingredients require instanceID c2pa_manifest: Some(HashedUri::new( labels::to_manifest_uri(&d_label), Some("sha256".into()), &[0u8; 32], )), validation_status: Some(vec![ValidationStatus::new_failure( SIGNING_CREDENTIAL_UNTRUSTED, ) .set_url(labels::to_signature_uri(&d_label))]), ..Default::default() }) .unwrap(); // B --v2 ingredient--> C let mut b_claim = Claim::new("gen-b", None, 2); let b_label = b_claim.label().to_string(); b_claim .add_assertion(&Ingredient { relationship: Relationship::ComponentOf, version: 2, title: Some("C".to_string()), format: Some("image/jpeg".to_string()), c2pa_manifest: Some(HashedUri::new( labels::to_manifest_uri(&c_label), Some("sha256".into()), &[0u8; 32], )), validation_status: Some(vec![ValidationStatus::new_failure( SIGNING_CREDENTIAL_UNTRUSTED, ) .set_url(labels::to_signature_uri(&c_label))]), ..Default::default() }) .unwrap(); // A (active) --v3 ingredient--> B let mut a_claim = Claim::new("gen-a", None, 3); let a_label = a_claim.label().to_string(); let mut b_results = ValidationResults::default(); b_results.add_status( ValidationStatus::new_failure(SIGNING_CREDENTIAL_UNTRUSTED) .set_url(labels::to_signature_uri(&b_label)), ); a_claim .add_assertion(&Ingredient { relationship: Relationship::ComponentOf, version: 3, active_manifest: Some(HashedUri::new( labels::to_manifest_uri(&b_label), Some("sha256".into()), &[0u8; 32], )), validation_results: Some(b_results), ..Default::default() }) .unwrap(); let mut store = Store::new(); store.insert_restored_claim(d_label.clone(), d_claim); store.insert_restored_claim(c_label.clone(), c_claim); store.insert_restored_claim(b_label.clone(), b_claim); store.insert_restored_claim(a_label.clone(), a_claim); // active, inserted last // The active manifest's own signature is fully trusted. let mut tracker = StatusTracker::default(); log_item!(a_label.clone(), "claim signature valid", "verify") .validation_status(CLAIM_SIGNATURE_VALIDATED) .success(&mut tracker); log_item!(a_label.clone(), "inside validity", "verify") .validation_status(CLAIM_SIGNATURE_INSIDE_VALIDITY) .success(&mut tracker); log_item!(a_label.clone(), "signing credential trusted", "verify") .validation_status(SIGNING_CREDENTIAL_TRUSTED) .success(&mut tracker); // Live re-check reproduces the untrusted finding at each nested level. The exact // ingredient_uri is irrelevant to de-dup (matching is by content); it only needs // to be ingredient-scoped and not the active manifest. for (parent, sig_label) in [ (&a_label, &b_label), (&b_label, &c_label), (&c_label, &d_label), ] { let ingredient_uri = labels::to_assertion_uri(parent, assertions::labels::INGREDIENT); tracker.push_ingredient_uri(ingredient_uri); let _ = log_item!(labels::to_signature_uri(sig_label), "untrusted", "verify") .validation_status(SIGNING_CREDENTIAL_UNTRUSTED) .failure(&mut tracker, "signing certificate untrusted"); tracker.pop_ingredient_uri(); } let results = ValidationResults::from_store(&store, &tracker); // Every nested untrusted finding was recorded by its ingredient, so all // de-duplicate: no ingredient-delta failures survive. let ingredient_failures: Vec<&str> = results .ingredient_deltas .as_ref() .map(|deltas| { deltas .iter() .flat_map(|idv| idv.validation_deltas().failure().iter()) .map(|s| s.code()) .collect() }) .unwrap_or_default(); assert!( ingredient_failures.is_empty(), "nested ingredient findings were not de-duplicated: {ingredient_failures:?}" ); assert_eq!(results.validation_state(), ValidationState::Trusted); } }