c2pa-rs

A fork of https://github.com/contentauth/c2pa-rs/
git clone git://archive.git.mtrnord.blog/mtrnords-photography-manager/c2pa-rs.git
Log | Files | Refs | README

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 }