| // Copyright 2026 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| // |
| |
| #include "assertion/validator.h" |
| |
| #include <algorithm> |
| #include <cstddef> |
| #include <cstdint> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/base/no_destructor.h" |
| #include "absl/container/flat_hash_map.h" |
| #include "absl/container/flat_hash_set.h" |
| #include "absl/log/log.h" |
| #include "absl/status/statusor.h" |
| #include "absl/strings/match.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/string_view.h" |
| #include "assertion/actions_assertion_validator.h" |
| #include "assertion/assertion_parser.h" |
| #include "assertion/hashed_uri_validator.h" |
| #include "assertion/references_validator.h" |
| #include "constants/ingredient_relationships.h" |
| #include "constants/labels.h" |
| #include "constants/status_codes.h" |
| #include "google/protobuf/repeated_ptr_field.h" |
| #include "jumbf/box.h" |
| #include "jumbf/uri.h" |
| #include "proto/actions_assertion.pb.h" |
| #include "proto/assertion.pb.h" |
| #include "proto/generator_info.pb.h" |
| #include "proto/hashed_uri.pb.h" |
| #include "proto/ingredient_assertion.pb.h" |
| #include "proto/manifest.pb.h" |
| #include "validator/tracker.h" |
| |
| namespace credentio { |
| namespace { |
| |
| using ::jumbf::BinaryDataBox; |
| using ::jumbf::CborBox; |
| using ::jumbf::EmbeddedFileDescriptionBox; |
| using ::jumbf::JsonBox; |
| using ::jumbf::SuperBox; |
| |
| constexpr absl::string_view kUnknown = "c2pa.unknown"; |
| |
| enum ManifestType { |
| kStandardManifest, |
| kUpdateManifest, |
| }; |
| |
| absl::string_view Basename(absl::string_view path) { |
| const size_t pos = path.find_last_of('/'); |
| if (pos == absl::string_view::npos) { |
| return path; |
| } |
| return path.substr(pos + 1); |
| } |
| |
| template <typename ActionsT> |
| bool ValidateActions(const ActionsT& actions, absl::string_view assertion_url, |
| ValidationTracker& validation_tracker) { |
| for (const auto& action : actions.actions()) { |
| if (action.action() == kUnknown) { |
| validation_tracker.RecordFailure( |
| FailureStatusCode::kGoogleAssertionActionUnknownAction, |
| {.url = assertion_url}); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| bool HasSuccesses(const ValidationStatusSet& validation) { |
| return !validation.successes().empty(); |
| } |
| |
| bool HasFailures(const ValidationStatusSet& validation) { |
| return !validation.failures().empty(); |
| } |
| |
| bool IsHardBindingSuccessCode(absl::string_view code) { |
| static const absl::NoDestructor<absl::flat_hash_set<std::string>> |
| kHardBindingSuccessCodes({ |
| absl::StrCat(SuccessStatusCode::kAssertionBmffHashMatch), |
| absl::StrCat(SuccessStatusCode::kAssertionBoxesHashMatch), |
| absl::StrCat(SuccessStatusCode::kAssertionCollectionHashMatch), |
| absl::StrCat(SuccessStatusCode::kAssertionDataHashMatch), |
| }); |
| return kHardBindingSuccessCodes->contains(code); |
| } |
| |
| bool HasHardBindingSuccess(const ValidationStatusSet& validation) { |
| return std::any_of(validation.successes().begin(), |
| validation.successes().end(), [](const auto& status) { |
| return IsHardBindingSuccessCode(status.code()); |
| }); |
| } |
| |
| bool ValidateIngredientAssertionValidationResults( |
| const IngredientAssertionV3& ingredient, absl::string_view assertion_url, |
| ManifestType manifest_type, ValidationTracker& validation_tracker) { |
| if (!ingredient.has_active_manifest()) { |
| // No validation results expected. |
| return true; |
| } |
| const ValidationStatusSet& ingredient_validation = |
| ingredient.validation_results().active_manifest(); |
| if (!HasFailures(ingredient_validation)) { |
| switch (manifest_type) { |
| case kStandardManifest: |
| if (!HasHardBindingSuccess(ingredient_validation)) { |
| validation_tracker.RecordFailure( |
| FailureStatusCode::kAssertionIngredientMalformed, |
| {.url = assertion_url, |
| .explanation = "no hard binding check recorded"}); |
| return false; |
| } |
| break; |
| case kUpdateManifest: |
| if (!HasSuccesses(ingredient_validation)) { |
| validation_tracker.RecordFailure( |
| FailureStatusCode::kAssertionIngredientMalformed, |
| {.url = assertion_url, |
| .explanation = "no validation successes or failures recorded"}); |
| return false; |
| } |
| break; |
| } |
| } |
| return true; |
| } |
| |
| bool IsValidIngredientRelationship(absl::string_view relationship) { |
| return relationship == kIngredientRelationshipParentOf || |
| relationship == kIngredientRelationshipComponentOf || |
| relationship == kIngredientRelationshipInputTo; |
| } |
| |
| bool ValidateIngredientAssertion(const IngredientAssertionV3& ingredient, |
| absl::string_view assertion_url, |
| ManifestType manifest_type, |
| ValidationTracker& validation_tracker) { |
| if (ingredient.has_active_manifest() && |
| !ingredient.digital_source_type().empty()) { |
| validation_tracker.RecordFailure( |
| FailureStatusCode::kAssertionIngredientMalformed, |
| {.url = assertion_url, |
| .explanation = "Ingredient assertion must not contain both " |
| "activeManifest and digitalSourceType"}); |
| return false; |
| } |
| // Note that while |
| // https://spec.c2pa.org/specifications/specifications/2.1/specs/C2PA_Specification.html#_performing_explicit_validation |
| // only calls for relationship validation on v3 ingredient assertions, this |
| // was subsequently corrected for C2PA 2.2 in |
| // https://github.com/c2pa-org/specs-core/pull/1513. |
| if (ingredient.relationship().empty()) { |
| validation_tracker.RecordFailure( |
| FailureStatusCode::kAssertionIngredientMalformed, |
| {.url = assertion_url, |
| .explanation = "Missing ingredient relationship"}); |
| return false; |
| } |
| if (!IsValidIngredientRelationship(ingredient.relationship())) { |
| validation_tracker.RecordFailure( |
| FailureStatusCode::kAssertionIngredientMalformed, |
| {.url = assertion_url, |
| .explanation = "Unrecognized ingredient relationship"}); |
| return false; |
| } |
| return ValidateIngredientAssertionValidationResults( |
| ingredient, assertion_url, manifest_type, validation_tracker); |
| } |
| |
| bool ActionAllowedInUpdateManifest(absl::string_view action) { |
| return action == "c2pa.edited.metadata" || action == "c2pa.opened" || |
| action == "c2pa.published" || action == "c2pa.redacted"; |
| } |
| |
| struct AssertionCounter { |
| uint32_t hard_bindings = 0; |
| uint32_t ingredients = 0; |
| uint32_t thumbnails = 0; |
| std::string action_disallowed_in_update; |
| absl::flat_hash_map<std::string, uint32_t> ingredient_relationships; |
| bool has_soft_binding = false; |
| bool has_watermarked_bound_action = false; |
| |
| void RecordAction(absl::string_view action) { |
| if (action_disallowed_in_update.empty() && |
| !ActionAllowedInUpdateManifest(action)) { |
| action_disallowed_in_update = action; |
| } |
| if (action == "c2pa.watermarked.bound" || action == "c2pa.watermarked") { |
| has_watermarked_bound_action = true; |
| } |
| } |
| |
| void Add(const Assertion& assertion) { |
| if (assertion.has_soft_binding()) { |
| has_soft_binding = true; |
| } |
| if (assertion.has_data_hash() || assertion.has_bmff_based_hash() || |
| assertion.has_boxes_hash() || assertion.has_collection_data_hash()) { |
| if (!absl::StrContains(assertion.label(), ".part")) { |
| // Only count the full asset hard binding. |
| ++hard_bindings; |
| } |
| } |
| for (const auto& action : assertion.actions().actions()) { |
| RecordAction(action.action()); |
| } |
| for (const auto& action : assertion.actions_v1().actions()) { |
| RecordAction(action.action()); |
| } |
| if (assertion.has_ingredient_v3()) { |
| ++ingredients; |
| ++ingredient_relationships[assertion.ingredient_v3().relationship()]; |
| } |
| if (assertion.has_claim_thumbnail() || |
| assertion.has_ingredient_thumbnail()) { |
| ++thumbnails; |
| } |
| } |
| |
| // See: |
| // https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_validate_the_assertions |
| void ValidateStandardManifestCounts(absl::string_view claim_box_url, |
| ValidationTracker& validation_tracker) { |
| if (hard_bindings == 0) { |
| validation_tracker.RecordFailure( |
| FailureStatusCode::kClaimHardBindingsMissing, |
| {.url = claim_box_url, |
| .explanation = "Standard manifest must contain one hard binding."}); |
| } else if (hard_bindings > 1) { |
| validation_tracker.RecordFailure( |
| FailureStatusCode::kAssertionMultipleHardBindings, |
| {.url = kAssertionStoreLabel, |
| .explanation = "Standard manifest must not contain more than one " |
| "hard binding."}); |
| } |
| if (ingredient_relationships[kIngredientRelationshipParentOf] > 1) { |
| validation_tracker.RecordFailure( |
| FailureStatusCode::kManifestMultipleParents, |
| {.url = claim_box_url, |
| .explanation = "Standard manifest must contain at most one " |
| "ingredient with parentOf relationship."}); |
| } |
| } |
| |
| // See: |
| // https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_validate_the_assertions |
| void ValidateUpdateManifestCounts(absl::string_view claim_box_url, |
| ValidationTracker& validation_tracker) { |
| if (hard_bindings != 0) { |
| validation_tracker.RecordFailure( |
| FailureStatusCode::kManifestUpdateInvalid, |
| {.url = claim_box_url, |
| .explanation = "Update manifest must not contain hard bindings."}); |
| } |
| if (!action_disallowed_in_update.empty()) { |
| validation_tracker.RecordFailure( |
| FailureStatusCode::kManifestUpdateInvalid, |
| {.url = claim_box_url, |
| .explanation = |
| absl::StrCat("Update manifest contains disallowed action: ", |
| action_disallowed_in_update)}); |
| } |
| if (ingredients != 1) { |
| validation_tracker.RecordFailure( |
| FailureStatusCode::kManifestUpdateWrongParents, |
| {.url = claim_box_url, |
| .explanation = absl::StrCat( |
| "Update manifest must contain exactly one ingredient; found ", |
| ingredients)}); |
| } else if (ingredient_relationships[kIngredientRelationshipParentOf] != 1) { |
| validation_tracker.RecordFailure( |
| FailureStatusCode::kManifestUpdateWrongParents, |
| {.url = claim_box_url, |
| .explanation = |
| "Update manifest ingredient relationship must be parentOf."}); |
| } |
| } |
| |
| void ValidateSoftBinding(absl::string_view claim_box_url, |
| ValidationTracker& validation_tracker) { |
| if (has_watermarked_bound_action && !has_soft_binding) { |
| validation_tracker.RecordFailure( |
| FailureStatusCode::kAssertionActionSoftBindingMissing, |
| {.url = claim_box_url, |
| .explanation = "c2pa.watermarked.bound action requires a " |
| "c2pa.soft-binding assertion"}); |
| } |
| } |
| }; |
| |
| void ValidateCborAssertion( |
| absl::string_view label, absl::string_view payload, |
| ManifestType manifest_type, const AssertionParser& assertion_parser, |
| const ReferencesValidator& references_validator, |
| google::protobuf::RepeatedPtrField<Assertion>* assertions_out, |
| ValidationTracker& validation_tracker, |
| AssertionCounter& assertion_counter) { |
| std::optional<Assertion> assertion_proto = |
| assertion_parser.ParseCbor(label, payload, validation_tracker); |
| if (!assertion_proto.has_value()) { |
| return; |
| } |
| assertion_counter.Add(*assertion_proto); |
| if (!references_validator.Validate(*assertion_proto)) { |
| return; |
| } |
| const std::string assertion_url = absl::StrCat( |
| kAssertionStoreLabel, kManifestLabelDelimiter, assertion_proto->label()); |
| switch (assertion_proto->assertion_case()) { |
| case Assertion::kActionsV1: |
| if (!ValidateActions(assertion_proto->actions_v1(), assertion_url, |
| validation_tracker)) { |
| return; |
| } |
| break; |
| case Assertion::kActions: |
| if (!ValidateActions(assertion_proto->actions(), assertion_url, |
| validation_tracker)) { |
| return; |
| } |
| break; |
| case Assertion::kIngredientV3: |
| if (!ValidateIngredientAssertion(assertion_proto->ingredient_v3(), |
| assertion_url, manifest_type, |
| validation_tracker)) { |
| return; |
| } |
| break; |
| default: |
| break; |
| } |
| assertions_out->Add(*std::move(assertion_proto)); |
| } |
| |
| void ValidateAssertionContents( |
| const SuperBox* assertion_box, ManifestType manifest_type, |
| const AssertionParser& assertion_parser, |
| const ReferencesValidator& references_validator, |
| google::protobuf::RepeatedPtrField<Assertion>* assertions_out, |
| ValidationTracker& validation_tracker, |
| AssertionCounter& assertion_counter) { |
| // CBOR assertion. |
| if (assertion_box->contents.size() == 1 && |
| assertion_box->contents[0].Holds<CborBox>()) { |
| ValidateCborAssertion(assertion_box->description.label.value_or(""), |
| assertion_box->contents[0].Get<CborBox>().payload, |
| manifest_type, assertion_parser, references_validator, |
| assertions_out, validation_tracker, |
| assertion_counter); |
| return; |
| } |
| // Thumbnail assertion (EmbeddedFileDescriptionBox + BinaryDataBox). |
| if (assertion_box->contents.size() == 2 && |
| assertion_box->contents[0].Holds<EmbeddedFileDescriptionBox>() && |
| assertion_box->contents[1].Holds<BinaryDataBox>()) { |
| std::optional<Assertion> assertion_proto = assertion_parser.ParseThumbnail( |
| assertion_box->description.label.value_or(""), |
| assertion_box->contents[0].Get<EmbeddedFileDescriptionBox>().media_type, |
| assertion_box->contents[1].Get<BinaryDataBox>().payload, |
| assertion_box->contents[0].Get<EmbeddedFileDescriptionBox>().file_name, |
| validation_tracker); |
| if (assertion_proto.has_value()) { |
| assertions_out->Add(*std::move(assertion_proto)); |
| } |
| return; |
| } |
| // JSON-LD assertion (e.g., c2pa.metadata). |
| if (assertion_box->contents.size() == 1 && |
| assertion_box->contents[0].Holds<JsonBox>()) { |
| std::optional<Assertion> assertion_proto = assertion_parser.ParseMetadata( |
| assertion_box->description.label.value_or(""), |
| assertion_box->contents[0].Get<JsonBox>().payload, validation_tracker); |
| if (assertion_proto.has_value()) { |
| assertions_out->Add(*std::move(assertion_proto)); |
| } |
| return; |
| } |
| |
| // Ignore other types of assertions. |
| } |
| |
| bool ContainsAllZeroBytes(absl::string_view data) { |
| return std::all_of(data.begin(), data.end(), [](char c) { return c == 0; }); |
| } |
| |
| bool ContainsAllZeroBytes(const jumbf::ContentBox& content) { |
| if (content.Holds<CborBox>()) { |
| return ContainsAllZeroBytes(content.Get<CborBox>().payload); |
| } |
| if (content.Holds<JsonBox>()) { |
| return ContainsAllZeroBytes(content.Get<JsonBox>().payload); |
| } |
| if (content.Holds<BinaryDataBox>()) { |
| return ContainsAllZeroBytes(content.Get<BinaryDataBox>().payload); |
| } |
| if (content.Holds<EmbeddedFileDescriptionBox>()) { |
| // We don't (yet) require redaction of the file description. |
| return true; |
| } |
| return false; |
| } |
| |
| bool ContainsAllZeroBytes(const jumbf::SuperBox& box) { |
| return std::all_of( |
| box.contents.begin(), box.contents.end(), |
| [](const auto& content) { return ContainsAllZeroBytes(content); }); |
| } |
| |
| void ValidateRedactionsAllowed(const Claim& claim, |
| ValidationTracker& validation_tracker) { |
| // Note that the selfRedacted check is done in ManifestGraph. |
| const AssertionTypeMatcher actions_v1_matcher(kActionsAssertionV1Label); |
| const AssertionTypeMatcher actions_v2_matcher(kActionsAssertionV2Label); |
| for (const auto& redacted_assertion : claim.redacted_assertions()) { |
| absl::string_view label = Basename(redacted_assertion); |
| if (actions_v1_matcher.Matches(label) || |
| actions_v2_matcher.Matches(label)) { |
| validation_tracker.RecordFailure( |
| FailureStatusCode::kAssertionActionRedacted, |
| {.url = absl::StrCat(kAssertionStoreLabel, kManifestLabelDelimiter, |
| label)}); |
| } |
| } |
| } |
| |
| // Returns true if `path` is a child of `parent`. |
| bool IsChildPath(absl::string_view path, absl::string_view parent) { |
| if (!absl::StartsWith(path, parent)) return false; |
| if (path.size() <= parent.size()) return false; |
| if (path[parent.size()] != '/') return false; |
| return path.find('/', parent.size() + 1) == absl::string_view::npos; |
| } |
| |
| // Returns true if the validation succeeded. |
| bool ValidateRedactedAssertion(absl::string_view path, |
| const jumbf::UriResolver& uri_resolver, |
| ValidationTracker& validation_tracker) { |
| // https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#_claim_signature_hash_validation_method |
| // "if the referenced assertion is present and its data consists of anything |
| // other than zero or more 0x00 bytes, the claim shall be rejected with a |
| // failure code of assertion.notRedacted." |
| |
| auto box = uri_resolver.ResolvePath(path); |
| if (!box.ok()) { |
| return true; |
| } |
| if (!ContainsAllZeroBytes(**box)) { |
| validation_tracker.RecordFailure(FailureStatusCode::kAssertionNotRedacted, |
| {.url = path}); |
| return false; |
| } |
| return true; |
| } |
| |
| std::optional<HashedUri> ConvertToHashedUri( |
| const InternalOrExternalHashedUri& uri, absl::string_view default_algorithm, |
| absl::string_view claim_uri, ValidationTracker& validation_tracker) { |
| if (!uri.has_url()) { |
| validation_tracker.RecordFailure( |
| FailureStatusCode::kHashedUriMissing, |
| {.url = claim_uri, .explanation = "Icon URI is missing"}); |
| return std::nullopt; |
| } |
| if (!absl::StartsWith(uri.url(), "self#jumbf=")) { |
| validation_tracker.RecordFailure( |
| FailureStatusCode::kGoogleHashedUriUnsupported, |
| {.url = claim_uri, |
| .explanation = "Icon URI is pointing to an external resource"}); |
| return std::nullopt; |
| } |
| HashedUri hashed_uri; |
| hashed_uri.set_url(uri.url()); |
| hashed_uri.set_hash(uri.hash()); |
| hashed_uri.set_algorithm(uri.has_algorithm() ? uri.algorithm() |
| : default_algorithm); |
| return hashed_uri; |
| } |
| |
| void ValidateAssertion( |
| const HashedUri& hashed_uri, absl::string_view manifest_path, |
| ManifestType manifest_type, |
| const absl::flat_hash_set<std::string>& redacted_assertion_paths, |
| const HashedUriValidator& assertion_uri_validator, |
| const ReferencesValidator& references_validator, |
| absl::string_view assertion_store_path, const Claim& claim, |
| const jumbf::UriResolver& uri_resolver, |
| const AssertionParser& assertion_parser, |
| google::protobuf::RepeatedPtrField<Assertion>* assertions_out, |
| absl::flat_hash_set<std::string>& assertion_labels, |
| ValidationTracker& validation_tracker, |
| AssertionCounter& assertion_counter) { |
| auto assertion_path = jumbf::UriResolver::GetAbsolutePathFromUri( |
| hashed_uri.url(), manifest_path); |
| if (!assertion_path.ok()) { |
| validation_tracker.RecordFailure( |
| FailureStatusCode::kAssertionMissing, |
| {.explanation = "malformed assertion URI"}); |
| return; |
| } |
| if (redacted_assertion_paths.contains(*assertion_path)) { |
| if (ValidateRedactedAssertion(*assertion_path, uri_resolver, |
| validation_tracker)) { |
| Assertion* redacted_assertion = assertions_out->Add(); |
| redacted_assertion->set_label(Basename(*assertion_path)); |
| redacted_assertion->mutable_redacted_by_generator(); |
| } |
| // Don't treat the redacted assertion as undeclared. |
| assertion_labels.insert(std::string(Basename(*assertion_path))); |
| return; |
| } |
| if (!assertion_uri_validator |
| .Validate(hashed_uri, absl::StrCat("self#jumbf=", *assertion_path), |
| validation_tracker) |
| .has_value()) { |
| return; // Failure has been recorded by Validate(). |
| } |
| validation_tracker.RecordSuccess( |
| SuccessStatusCode::kAssertionHashedUriMatch, |
| {.url = absl::StrCat("self#jumbf=", *assertion_path)}); |
| if (!absl::StartsWith(*assertion_path, manifest_path)) { |
| validation_tracker.RecordFailure( |
| FailureStatusCode::kAssertionOutsideManifest, |
| {.url = claim.label(), |
| .explanation = |
| absl::StrCat("Violated claim assertion: ", *assertion_path)}); |
| return; |
| } |
| if (!IsChildPath(*assertion_path, assertion_store_path)) { |
| validation_tracker.RecordFailure( |
| FailureStatusCode::kAssertionMissing, |
| {.url = claim.label(), |
| .explanation = |
| absl::StrCat("Violated claim assertion: ", *assertion_path)}); |
| return; |
| } |
| auto assertion_box = uri_resolver.ResolvePath(*assertion_path); |
| if (!assertion_box.ok()) { |
| // Can't happen -- assertion_uri_validator.Validate would have failed. |
| DLOG(FATAL) << "unexpected missing assertion: " << assertion_box.status(); |
| validation_tracker.RecordFailure( |
| FailureStatusCode::kGoogleInternalError, |
| {.url = claim.label(), .explanation = "unexpected missing assertion"}); |
| return; |
| } |
| ValidateAssertionContents(*assertion_box, manifest_type, assertion_parser, |
| references_validator, assertions_out, |
| validation_tracker, assertion_counter); |
| assertion_labels.insert(std::string(Basename(*assertion_path))); |
| } |
| |
| void ValidateClaimGeneratorIcon(const Claim& claim, |
| absl::string_view manifest_path, |
| const HashedUriValidator& hashed_uri_validator, |
| ValidationTracker& validation_tracker) { |
| if (!claim.claim_generator_info().has_icon()) { |
| return; |
| } |
| std::optional<HashedUri> icon_hashed_uri = ConvertToHashedUri( |
| claim.claim_generator_info().icon(), claim.default_algorithm(), |
| claim.label(), validation_tracker); |
| if (icon_hashed_uri.has_value()) { |
| auto icon_path = jumbf::UriResolver::GetAbsolutePathFromUri( |
| icon_hashed_uri->url(), manifest_path); |
| if (!icon_path.ok()) { |
| // The only way this can happen is if the icon URI doesn't start with |
| // "self#jumbf=". This would have already been caught by |
| // ConvertToHashedUri. |
| validation_tracker.RecordFailure( |
| FailureStatusCode::kHashedUriMissing, |
| {.url = claim.label(), .explanation = "Icon URI is malformed"}); |
| } else if (hashed_uri_validator |
| .Validate(*icon_hashed_uri, claim.label(), |
| validation_tracker) |
| .has_value()) { |
| // No success code for icon URI validation. See |
| // https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#_validation_of_references |
| } |
| } |
| } |
| |
| void ValidateUndeclaredAssertions( |
| absl::string_view assertion_store_path, |
| const absl::flat_hash_set<std::string>& assertion_labels, |
| const jumbf::UriResolver& uri_resolver, |
| ValidationTracker& validation_tracker) { |
| auto assertion_store_box = uri_resolver.ResolvePath(assertion_store_path); |
| if (!assertion_store_box.ok()) { |
| return; |
| } |
| for (const auto& content : (*assertion_store_box)->contents) { |
| if (!content.Holds<SuperBox>()) { |
| validation_tracker.RecordFailure( |
| FailureStatusCode::kAssertionUndeclared, |
| {.explanation = |
| "Assertion store contains a box that is not a superbox."}); |
| continue; |
| } |
| const SuperBox& assertion_box = content.Get<SuperBox>(); |
| auto assertion_label = assertion_box.description.label.value_or(""); |
| if (!assertion_labels.contains(assertion_label)) { |
| validation_tracker.RecordFailure( |
| FailureStatusCode::kAssertionUndeclared, |
| {.url = absl::StrCat(kAssertionStoreLabel, kManifestLabelDelimiter, |
| assertion_label)}); |
| continue; |
| } |
| } |
| } |
| |
| } // namespace |
| |
| void AssertionValidator::ValidateClaimAssertions( |
| const jumbf::UriResolver& uri_resolver, |
| const absl::flat_hash_set<std::string>& redacted_assertion_paths, |
| Manifest& manifest, ValidationTracker& validation_tracker) const { |
| std::string manifest_path = absl::StrCat("/c2pa/", manifest.label()); |
| const Claim& claim = manifest.claim(); |
| AssertionCounter assertion_counter; |
| auto assertion_store_path = |
| jumbf::UriResolver::GetAbsolutePath(kAssertionStoreLabel, manifest_path); |
| // Validates claim assertion URIs. |
| HashedUriValidator assertion_uri_validator(std::string(manifest_path), |
| uri_resolver); |
| assertion_uri_validator.SetErrorCodes(HashedUriValidator::Codes{ |
| .missing = FailureStatusCode::kAssertionMissing, |
| .mismatch = FailureStatusCode::kAssertionHashedUriMismatch}); |
| assertion_uri_validator.SetDefaultAlgorithm(claim.default_algorithm()); |
| HashedUriValidator hashed_uri_validator(std::string(manifest_path), |
| uri_resolver); |
| hashed_uri_validator.SetDefaultAlgorithm(claim.default_algorithm()); |
| |
| ValidateClaimGeneratorIcon(claim, manifest_path, hashed_uri_validator, |
| validation_tracker); |
| |
| ManifestType manifest_type = manifest.is_update_manifest() |
| ? ManifestType::kUpdateManifest |
| : ManifestType::kStandardManifest; |
| ReferencesValidator references_validator( |
| &hashed_uri_validator, manifest.label(), &validation_tracker); |
| absl::flat_hash_set<std::string> assertion_labels; |
| assertion_labels.reserve(claim.created_assertions_size() + |
| claim.gathered_assertions_size()); |
| for (const auto& assertions : |
| {claim.created_assertions(), claim.gathered_assertions()}) { |
| for (const HashedUri& hashed_uri : assertions) { |
| ValidateAssertion( |
| hashed_uri, manifest_path, manifest_type, redacted_assertion_paths, |
| assertion_uri_validator, references_validator, assertion_store_path, |
| claim, uri_resolver, assertion_parser_, manifest.mutable_assertions(), |
| assertion_labels, validation_tracker, assertion_counter); |
| } |
| } |
| |
| // Validate that this claim's redactions are allowed. |
| ValidateRedactionsAllowed(claim, validation_tracker); |
| |
| switch (manifest_type) { |
| case ManifestType::kUpdateManifest: |
| assertion_counter.ValidateUpdateManifestCounts(claim.label(), |
| validation_tracker); |
| break; |
| case ManifestType::kStandardManifest: |
| assertion_counter.ValidateStandardManifestCounts(claim.label(), |
| validation_tracker); |
| break; |
| } |
| |
| assertion_counter.ValidateSoftBinding(claim.label(), validation_tracker); |
| |
| // Validates any undeclared assertions. |
| ValidateUndeclaredAssertions(assertion_store_path, assertion_labels, |
| uri_resolver, validation_tracker); |
| |
| if (!options_.skip_actions_assertion_validation_for_test) { |
| ActionsAssertionValidator actions_validator; |
| actions_validator.Validate(manifest, validation_tracker); |
| } |
| } |
| |
| } // namespace credentio |