validation_status.rs (19435B)
1 // Copyright 2022 Adobe. All rights reserved. 2 // This file is licensed to you under the Apache License, 3 // Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 4 // or the MIT license (http://opensource.org/licenses/MIT), 5 // at your option. 6 7 // Unless required by applicable law or agreed to in writing, 8 // this software is distributed on an "AS IS" BASIS, WITHOUT 9 // WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or 10 // implied. See the LICENSE-MIT and LICENSE-APACHE files for the 11 // specific language governing permissions and limitations under 12 // each license. 13 14 //! Implements validation status for specific parts of a manifest. 15 //! 16 //! See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_existing_manifests>. 17 18 #![deny(missing_docs)] 19 20 #[cfg(feature = "json_schema")] 21 use schemars::JsonSchema; 22 use serde::{Deserialize, Serialize}; 23 use tracing::debug; 24 25 use crate::{ 26 assertion::AssertionBase, 27 assertions::Ingredient, 28 error::Error, 29 jumbf, 30 status_tracker::{LogItem, StatusTracker}, 31 store::Store, 32 }; 33 34 /// A `ValidationStatus` struct describes the validation status of a 35 /// specific part of a manifest. 36 /// 37 /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_existing_manifests>. 38 #[derive(Clone, Debug, Deserialize, Serialize)] 39 #[cfg_attr(feature = "json_schema", derive(JsonSchema))] 40 pub struct ValidationStatus { 41 code: String, 42 43 #[serde(skip_serializing_if = "Option::is_none")] 44 url: Option<String>, 45 46 #[serde(skip_serializing_if = "Option::is_none")] 47 explanation: Option<String>, 48 } 49 50 impl ValidationStatus { 51 pub(crate) fn new<S: Into<String>>(code: S) -> Self { 52 Self { 53 code: code.into(), 54 url: None, 55 explanation: None, 56 } 57 } 58 59 /// Returns the validation status code. 60 /// 61 /// Validation status codes are the labels from the "Value" 62 /// column in <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_existing_manifests>. 63 /// 64 /// These are also defined as constants in the 65 /// [`validation_status`](crate::validation_status) mod. 66 pub fn code(&self) -> &str { 67 &self.code 68 } 69 70 /// Returns the internal JUMBF reference to the entity that was validated. 71 pub fn url(&self) -> Option<&str> { 72 self.url.as_deref() 73 } 74 75 /// Returns a human-readable description of the validation that was performed. 76 pub fn explanation(&self) -> Option<&str> { 77 self.explanation.as_deref() 78 } 79 80 /// Sets the internal JUMBF reference to the entity was validated. 81 pub(crate) fn set_url(mut self, url: String) -> Self { 82 self.url = Some(url); 83 self 84 } 85 86 /// Sets the human-readable description of the validation that was performed. 87 pub(crate) fn set_explanation(mut self, explanation: String) -> Self { 88 self.explanation = Some(explanation); 89 self 90 } 91 92 /// Returns `true` if this has a successful validation code. 93 pub fn passed(&self) -> bool { 94 is_success(&self.code) 95 } 96 97 // Maps errors into validation_status codes. 98 fn code_from_error_str(error: &str) -> &str { 99 match error { 100 e if e.starts_with("ClaimMissing") => CLAIM_MISSING, 101 e if e.starts_with("AssertionMissing") => ASSERTION_MISSING, 102 e if e.starts_with("AssertionDecoding") => ASSERTION_REQUIRED_MISSING, 103 e if e.starts_with("HashMismatch") => ASSERTION_DATAHASH_MATCH, 104 e if e.starts_with("RemoteManifestFetch") => MANIFEST_INACCESSIBLE, 105 e if e.starts_with("PrereleaseError") => STATUS_PRERELEASE, 106 _ => GENERAL_ERROR, 107 } 108 } 109 110 // Maps errors into validation_status codes. 111 const fn code_from_error(error: &Error) -> &str { 112 match error { 113 Error::ClaimMissing { .. } => CLAIM_MISSING, 114 Error::AssertionMissing { .. } => ASSERTION_MISSING, 115 Error::AssertionDecoding(_code) => ASSERTION_REQUIRED_MISSING, /* todo detect json/cbor errors */ 116 Error::HashMismatch(_) => ASSERTION_DATAHASH_MATCH, 117 Error::RemoteManifestFetch(_) => MANIFEST_INACCESSIBLE, 118 Error::PrereleaseError => STATUS_PRERELEASE, 119 _ => GENERAL_ERROR, 120 } 121 } 122 123 /// Creates a ValidationStatus from an error code. 124 pub(crate) fn from_error(error: &Error) -> Self { 125 // We need to create error codes here for client processing. 126 let code = Self::code_from_error(error); 127 debug!("ValidationStatus {} from error {:#?}", code, error); 128 Self::new(code.to_string()).set_explanation(error.to_string()) 129 } 130 131 /// Creates a ValidationStatus from a validation_log item. 132 pub(crate) fn from_validation_item(item: &LogItem) -> Option<Self> { 133 match item.validation_status.as_ref() { 134 Some(status) => Some( 135 Self::new(status.to_string()) 136 .set_url(item.label.to_string()) 137 .set_explanation(item.description.to_string()), 138 ), 139 // If we don't have a validation_status, then make one from the err_val 140 // using the description plus error text explanation. 141 None => item.error_str().as_ref().map(|e| { 142 let code = Self::code_from_error_str(e); 143 Self::new(code.to_string()) 144 .set_url(item.label.to_string()) 145 .set_explanation(format!("{}: {}", item.description, e)) 146 }), 147 } 148 } 149 } 150 151 impl PartialEq for ValidationStatus { 152 fn eq(&self, other: &Self) -> bool { 153 self.code == other.code && self.url == other.url 154 } 155 } 156 157 // TODO: Does this still need to be public? (I do see one reference in the JS SDK.) 158 159 /// Given a `Store` and a `StatusTracker`, return `ValidationStatus` items for each 160 /// item in the tracker which reflect errors in the active manifest or which would not 161 /// be reported as a validation error for any ingredient. 162 pub fn status_for_store( 163 store: &Store, 164 validation_log: &impl StatusTracker, 165 ) -> Vec<ValidationStatus> { 166 let statuses: Vec<ValidationStatus> = validation_log 167 .get_log() 168 .iter() 169 .filter_map(ValidationStatus::from_validation_item) 170 .filter(|s| !is_success(&s.code)) 171 .collect(); 172 173 // Filter out any status that is already captured in an ingredient assertion. 174 if let Some(claim) = store.provenance_claim() { 175 let active_manifest = Some(claim.label().to_string()); 176 177 // This closure returns true if the URI references the store's active manifest. 178 let is_active_manifest = |uri: Option<&str>| { 179 uri.filter(|uri| jumbf::labels::manifest_label_from_uri(uri) == active_manifest) 180 .is_some() 181 }; 182 183 // We only need to do the more detailed filtering if there are any status 184 // reports that reference ingredients. 185 if statuses 186 .iter() 187 .any(|s| !is_active_manifest(s.url.as_deref())) 188 { 189 // Collect all the ValidationStatus records from all the ingredients in the store. 190 let ingredient_statuses: Vec<ValidationStatus> = claim 191 .ingredient_assertions() 192 .iter() 193 .filter_map(|a| Ingredient::from_assertion(a).ok()) 194 .filter_map(|i| i.validation_status) 195 .flat_map(|x| x.into_iter()) 196 .collect(); 197 198 // Filter to only contain the active statuses and nested statuses not found in active. 199 return statuses 200 .iter() 201 .filter(|s| { 202 is_active_manifest(s.url.as_deref()) 203 || !ingredient_statuses.iter().any(|i| s == &i) 204 }) 205 .map(|s| s.to_owned()) 206 .collect(); 207 } 208 } 209 210 statuses 211 } 212 213 // -- success codes -- 214 215 /// The claim signature referenced in the ingredient's claim validated. 216 /// 217 /// `ValidationStatus.url()` will point to a C2PA claim signature box. 218 pub const CLAIM_SIGNATURE_VALIDATED: &str = "claimSignature.validated"; 219 220 /// The signing credential is listed on the validator's trust list. 221 /// 222 /// `ValidationStatus.url()` will point to a C2PA claim signature box. 223 pub const SIGNING_CREDENTIAL_TRUSTED: &str = "signingCredential.trusted"; 224 225 /// The time-stamp credential is listed on the validator's trust list. 226 /// 227 /// `ValidationStatus.url()` will point to a C2PA claim signature box. 228 pub const TIMESTAMP_TRUSTED: &str = "timeStamp.trusted"; 229 230 /// The hash of the the referenced assertion in the ingredient's manifest 231 /// matches the corresponding hash in the assertion's hashed URI in the claim. 232 /// 233 /// `ValidationStatus.url()` will point to a C2PA assertion. 234 pub const ASSERTION_HASHEDURI_MATCH: &str = "assertion.hashedURI.match"; 235 236 /// Hash of a byte range of the asset matches the hash declared in the 237 /// data hash assertion. 238 /// 239 /// `ValidationStatus.url()` will point to a C2PA assertion. 240 pub const ASSERTION_DATAHASH_MATCH: &str = "assertion.dataHash.match"; 241 242 /// Hash of a box-based asset matches the hash declared in the BMFF 243 /// hash assertion. 244 /// 245 /// `ValidationStatus.url()` will point to a C2PA assertion. 246 pub const ASSERTION_BMFFHASH_MATCH: &str = "assertion.bmffHash.match"; 247 248 /// Hash of a box-based asset matches the hash declared in the General Box 249 /// Hash assertion. 250 /// 251 /// `ValidationStatus.url()` will point to a C2PA assertion. 252 pub const ASSERTION_BOXHASH_MATCH: &str = "assertion.boxesHash.match"; 253 254 /// A non-embedded (remote) assertion was accessible at the time of 255 /// validation. 256 /// 257 /// `ValidationStatus.url()` will point to a C2PA assertion. 258 pub const ASSERTION_ACCESSIBLE: &str = "assertion.accessible"; 259 260 // -- failure codes -- 261 262 /// The referenced claim in the ingredient's manifest cannot be found. 263 /// 264 /// `ValidationStatus.url()` will point to a C2PA claim box. 265 pub const CLAIM_MISSING: &str = "claim.missing"; 266 267 /// More than one claim box is present in the manifest. 268 /// 269 /// `ValidationStatus.url()` will point to a C2PA claim box. 270 pub const CLAIM_MULTIPLE: &str = "claim.multiple"; 271 272 /// No hard bindings are present in the claim. 273 /// 274 /// `ValidationStatus.url()` will point to a C2PA claim box. 275 pub const HARD_BINDINGS_MISSING: &str = "claim.hardBindings.missing"; 276 277 /// A required field is not present in the claim. 278 /// 279 /// `ValidationStatus.url()` will point to a C2PA claim box. 280 pub const CLAIM_REQUIRED_MISSING: &str = "claim.required.missing"; 281 282 /// The cbor of the claim is not valid. 283 /// 284 /// `ValidationStatus.url()` will point to a C2PA claim box. 285 pub const CLAIM_CBOR_INVALID: &str = "claim.cbor.invalid"; 286 287 /// The hash of the the referenced ingredient claim in the manifest 288 /// does not match the corresponding hash in the ingredient's hashed 289 /// URI in the claim. 290 /// 291 /// `ValidationStatus.url()` will point to a C2PA assertion. 292 pub const INGREDIENT_HASHEDURI_MISMATCH: &str = "ingredient.hashedURI.mismatch"; 293 294 /// The claim signature referenced in the ingredient's claim 295 /// cannot be found in its manifest. 296 /// 297 /// `ValidationStatus.url()` will point to a C2PA claim signature box. 298 pub const CLAIM_SIGNATURE_MISSING: &str = "claimSignature.missing"; 299 300 /// The claim signature referenced in the ingredient's claim 301 /// failed to validate. 302 /// 303 /// `ValidationStatus.url()` will point to a C2PA claim signature box. 304 pub const CLAIM_SIGNATURE_MISMATCH: &str = "claimSignature.mismatch"; 305 306 /// If a manifest was documented to exist in a remote location, 307 /// but is not present there, or the location is not currently available 308 /// (such as in an offline scenario), 309 /// the `manifest.inaccessible` error code shall be used to report the situation. 310 /// 311 /// `ValidationStatus.url()` URI reference to the C2PA Manifest that could not be accessed. 312 pub const MANIFEST_INACCESSIBLE: &str = "manifest.inaccessible"; 313 314 /// The manifest has more than one ingredient whose `relationship` 315 /// is `parentOf`. 316 /// 317 /// `ValidationStatus.url()` will point to a C2PA claim box. 318 pub const MANIFEST_MULTIPLE_PARENTS: &str = "manifest.multipleParents"; 319 320 /// The manifest is an update manifest, but it contains hard binding 321 /// or actions assertions. 322 /// 323 /// `ValidationStatus.url()` will point to a C2PA claim box. 324 pub const MANIFEST_UPDATE_INVALID: &str = "manifest.update.invalid"; 325 326 /// The manifest is an update manifest, but it contains either zero 327 /// or multiple `parentOf` ingredients. 328 /// 329 /// `ValidationStatus.url()` will point to a C2PA claim box. 330 pub const MANIFEST_UPDATE_WRONG_PARENTS: &str = "manifest.update.wrongParents"; 331 332 /// The signing credential is not listed on the validator's trust list. 333 /// 334 /// `ValidationStatus.url()` will point to a C2PA claim signature box. 335 pub const SIGNING_CREDENTIAL_UNTRUSTED: &str = "signingCredential.untrusted"; 336 337 /// The signing credential is not valid for signing. 338 /// 339 /// `ValidationStatus.url()` will point to a C2PA claim signature box. 340 pub const SIGNING_CREDENTIAL_INVALID: &str = "signingCredential.invalid"; 341 342 /// The signing credential has been revoked by the issuer. 343 /// 344 /// `ValidationStatus.url()` will point to a C2PA claim signature box. 345 pub const SIGNING_CREDENTIAL_REVOKED: &str = "signingCredential.revoked"; 346 347 /// The signing credential has expired. 348 /// 349 /// `ValidationStatus.url()` will point to a C2PA claim signature box. 350 pub const SIGNING_CREDENTIAL_EXPIRED: &str = "signingCredential.expired"; 351 352 /// The time-stamp does not correspond to the contents of the claim. 353 /// 354 /// `ValidationStatus.url()` will point to a C2PA claim signature box. 355 pub const TIMESTAMP_MISMATCH: &str = "timeStamp.mismatch"; 356 357 /// The time-stamp credential is not listed on the validator's trust list. 358 /// 359 /// `ValidationStatus.url()` will point to a C2PA claim signature box. 360 pub const TIMESTAMP_UNTRUSTED: &str = "timeStamp.untrusted"; 361 362 /// The signed time-stamp attribute in the signature falls outside the 363 /// validity window of the signing certificate or the TSA's certificate. 364 /// 365 /// `ValidationStatus.url()` will point to a C2PA claim signature box. 366 pub const TIMESTAMP_OUTSIDE_VALIDITY: &str = "timeStamp.outsideValidity"; 367 368 /// The hash of the the referenced assertion in the manifest does not 369 /// match the corresponding hash in the assertion's hashed URI in the claim. 370 /// 371 /// `ValidationStatus.url()` will point to a C2PA assertion. 372 pub const ASSERTION_HASHEDURI_MISMATCH: &str = "assertion.hashedURI.mismatch"; 373 374 /// An assertion listed in the ingredient's claim is missing from the 375 /// ingredient's manifest. 376 /// 377 /// `ValidationStatus.url()` will point to a C2PA claim box. 378 pub const ASSERTION_MISSING: &str = "assertion.missing"; 379 380 /// An assertion was found in the ingredient's manifest that was not 381 /// explicitly declared in the ingredient's claim. 382 /// 383 /// `ValidationStatus.url()` will point to a C2PA claim box or assertion. 384 pub const ASSERTION_UNDECLARED: &str = "assertion.undeclared"; 385 386 /// A non-embedded (remote) assertion was inaccessible at the time of validation. 387 /// 388 /// `ValidationStatus.url()` will point to a C2PA assertion. 389 pub const ASSERTION_INACCESSIBLE: &str = "assertion.inaccessible"; 390 391 /// An assertion was declared as redacted in the ingredient's claim 392 /// but is still present in the ingredient's manifest. 393 /// 394 /// `ValidationStatus.url()` will point to a C2PA assertion. 395 pub const ASSERTION_NOT_REDACTED: &str = "assertion.notRedacted"; 396 397 /// An assertion was declared as redacted by its own claim. 398 /// 399 /// `ValidationStatus.url()` will point to a C2PA claim box. 400 pub const ASSERTION_SELF_REDACTED: &str = "assertion.selfRedacted"; 401 402 /// A required field is not present in an assertion. 403 /// 404 /// `ValidationStatus.url()` will point to a C2PA assertion. 405 pub const ASSERTION_REQUIRED_MISSING: &str = "assertion.required.missing"; 406 407 /// The JSON(-LD) of an assertion is not valid. 408 /// 409 /// `ValidationStatus.url()` will point to a C2PA assertion. 410 pub const ASSERTION_JSON_INVALID: &str = "assertion.json.invalid"; 411 412 /// The cbor of an assertion is not valid. 413 /// 414 /// `ValidationStatus.url()` will point to a C2PA assertion. 415 pub const ASSERTION_CBOR_INVALID: &str = "assertion.cbor.invalid"; 416 417 /// An action that requires an associated ingredient either does not have one 418 /// or the one specified cannot be located 419 /// 420 /// `ValidationStatus.url()` will point to a C2PA assertion. 421 pub const ACTION_ASSERTION_INGREDIENT_MISMATCH: &str = "assertion.action.ingredientMismatch"; 422 423 /// An `action` assertion was redacted when the ingredient's 424 /// claim was created. 425 /// 426 /// `ValidationStatus.url()` will point to a C2PA assertion. 427 pub const ACTION_ASSERTION_REDACTED: &str = "assertion.action.redacted"; 428 429 /// The hash of a byte range of the asset does not match the 430 /// hash declared in the data hash assertion. 431 /// 432 /// `ValidationStatus.url()` will point to a C2PA assertion. 433 pub const ASSERTION_DATAHASH_MISMATCH: &str = "assertion.dataHash.mismatch"; 434 435 /// The hash of a box-based asset does not match the hash declared 436 /// in the BMFF hash assertion. 437 /// 438 /// `ValidationStatus.url()` will point to a C2PA assertion. 439 pub const ASSERTION_BMFFHASH_MISMATCH: &str = "assertion.bmffHash.mismatch"; 440 441 /// The hash of a box-based asset does not match the hash declared 442 /// in the General Boxes hash assertion. 443 /// 444 /// `ValidationStatus.url()` will point to a C2PA assertion. 445 pub const ASSERTION_BOXHASH_MISMATCH: &str = "assertion.boxesHash.mismatch"; 446 447 /// The hash of a box-based asset does not contain boxes in the expected order for 448 /// the General Boxes hash assertion. 449 /// 450 /// `ValidationStatus.url()` will point to a C2PA assertion. 451 pub const ASSERTION_BOXHASH_UNKNOWN: &str = "assertion.boxesHash."; 452 453 /// A hard binding assertion is in a cloud data assertion. 454 /// 455 /// `ValidationStatus.url()` will point to a C2PA assertion. 456 pub const ASSERTION_CLOUD_DATA_HARD_BINDING: &str = "assertion.cloud-data.hardBinding"; 457 458 /// An update manifest contains a cloud data assertion referencing 459 /// an actions assertion. 460 /// 461 /// `ValidationStatus.url()` will point to a C2PA assertion. 462 pub const ASSERTION_CLOUD_DATA_ACTIONS: &str = "assertion.cloud-data.actions"; 463 464 /// The value of an `alg` header, or other header that specifies an 465 /// algorithm used to compute the value of another field, is unknown 466 /// or unsupported. 467 /// 468 /// `ValidationStatus.url()` will point to a C2PA claim box or C2PA assertion. 469 pub const ALGORITHM_UNSUPPORTED: &str = "algorithm.unsupported"; 470 471 /// A value to be used when there was an error not specifically listed here. 472 /// 473 /// `ValidationStatus.url()` will point to a C2PA claim box or C2PA assertion. 474 pub const GENERAL_ERROR: &str = "general.error"; 475 476 // -- unofficial status codes -- 477 478 pub(crate) const STATUS_PRERELEASE: &str = "com.adobe.prerelease"; 479 480 /// Returns `true` if the status code is a known C2PA success status code. 481 /// 482 /// Returns `false` if the status code is a known C2PA failure status 483 /// code or is unknown. 484 /// 485 /// # Examples 486 /// 487 /// ``` 488 /// use c2pa::validation_status::*; 489 /// 490 /// assert!(is_success(CLAIM_SIGNATURE_VALIDATED)); 491 /// assert!(!is_success(SIGNING_CREDENTIAL_REVOKED)); 492 /// ``` 493 pub fn is_success(status_code: &str) -> bool { 494 matches!( 495 status_code, 496 CLAIM_SIGNATURE_VALIDATED 497 | SIGNING_CREDENTIAL_TRUSTED 498 | TIMESTAMP_TRUSTED 499 | ASSERTION_HASHEDURI_MATCH 500 | ASSERTION_DATAHASH_MATCH 501 | ASSERTION_BMFFHASH_MATCH 502 | ASSERTION_ACCESSIBLE 503 | ASSERTION_BOXHASH_MATCH 504 ) 505 }