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