| // 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 "validator/graph.h" |
| |
| #include <cstddef> |
| #include <memory> |
| #include <optional> |
| #include <stack> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/base/nullability.h" |
| #include "absl/container/flat_hash_set.h" |
| #include "absl/log/check.h" |
| #include "absl/status/status.h" |
| #include "absl/status/status_macros.h" |
| #include "absl/status/statusor.h" |
| #include "absl/strings/match.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/str_join.h" |
| #include "absl/strings/string_view.h" |
| #include "assertion/hashed_uri_validator.h" |
| #include "constants/ingredient_relationships.h" |
| #include "constants/labels.h" |
| #include "constants/status_codes.h" |
| #include "jumbf/box.h" |
| #include "proto/assertion.pb.h" |
| #include "proto/bmff_based_hash_assertion.pb.h" |
| #include "proto/boxes_hash_assertion.pb.h" |
| #include "proto/collection_data_hash_assertion.pb.h" |
| #include "proto/data_hash_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 "proto/validation_result.pb.h" |
| #include "proto/validation_status.pb.h" |
| #include "uuid/uuid.h" |
| #include "validator/graph_internals.h" |
| #include "validator/tracker.h" |
| |
| namespace credentio { |
| namespace { |
| |
| template <typename T> |
| const Manifest* GetManifest(const T* result, absl::string_view label) { |
| if (result->active_manifest().label() == label) { |
| return &result->active_manifest(); |
| } |
| for (const auto& ingredient_manifest : result->ingredient_manifests()) { |
| if (ingredient_manifest.label() == label) { |
| return &ingredient_manifest; |
| } |
| } |
| return nullptr; |
| } |
| |
| bool IsIngredientAssertion(const Assertion& assertion) { |
| return assertion.has_ingredient_v3(); |
| } |
| |
| // Returns true if the given label is a hard binding assertion label. |
| // Does not match multi-asset hash assertions or the "part hash" assertions they |
| // reference. |
| |
| // Returns true if the given label is for a hard binding assertion, a |
| // multi-asset hash assertion, or a "part hash" assertion. |
| |
| std::optional<HashedUri> GetIngredientManifestHashedUri( |
| const Assertion& assertion) { |
| if (assertion.ingredient_v3().has_active_manifest()) { |
| return assertion.ingredient_v3().active_manifest(); |
| } |
| return std::nullopt; |
| } |
| |
| absl::string_view GetIngredientRelationship(const Assertion& assertion) { |
| return assertion.ingredient_v3().relationship(); |
| } |
| |
| std::string GetAssertionUri(absl::string_view manifest_label, |
| const Assertion& assertion) { |
| return absl::StrJoin({absl::StrCat(kManifestStoreUrlPrefix, manifest_label), |
| kAssertionStoreLabel, assertion.label()}, |
| kManifestLabelDelimiter); |
| } |
| |
| // No failures are recorded in the tracker for this method as these failures |
| // are recorded during the ValidateNode call. |
| std::optional<std::string> GetHardBindingUri( |
| const PartialValidationResultProto& result, |
| absl::string_view manifest_label, ValidationTracker& tracker) { |
| if (manifest_label.empty()) { |
| return std::nullopt; |
| } |
| auto* manifest_with_content_bindings = GetManifest(&result, manifest_label); |
| if (manifest_with_content_bindings == nullptr) { |
| return std::nullopt; |
| } |
| |
| std::optional<std::string> uri; |
| for (const auto& assertion : manifest_with_content_bindings->assertions()) { |
| if ((assertion.has_bmff_based_hash() || assertion.has_boxes_hash() || |
| assertion.has_data_hash() || assertion.has_collection_data_hash()) && |
| !absl::StrContains(assertion.label(), ".part")) { |
| uri = GetAssertionUri(manifest_label, assertion); |
| break; |
| } |
| } |
| return uri; |
| } |
| |
| // No failures are recorded in the tracker for this method as these failures |
| // are recorded during the ValidateNode call. |
| std::optional<std::string> GetMultiAssetHashUri( |
| const PartialValidationResultProto& result, |
| absl::string_view manifest_label, ValidationTracker& tracker) { |
| if (manifest_label.empty()) { |
| return std::nullopt; |
| } |
| auto* manifest_with_content_bindings = GetManifest(&result, manifest_label); |
| if (manifest_with_content_bindings == nullptr) { |
| return std::nullopt; |
| } |
| |
| std::optional<std::string> uri; |
| for (const auto& assertion : manifest_with_content_bindings->assertions()) { |
| if (assertion.has_multi_asset_hash()) { |
| uri = GetAssertionUri(manifest_label, assertion); |
| break; |
| } |
| } |
| return uri; |
| } |
| |
| void SetHardBindingUri(absl::string_view manifest_with_content_bindings_label, |
| PartialValidationResultProto& result) { |
| ValidationTracker tracker(result.mutable_active_manifest()); |
| |
| if (result.active_manifest().label().empty()) { |
| return; |
| } |
| if (auto hard_binding_uri = GetHardBindingUri( |
| result, manifest_with_content_bindings_label, tracker); |
| hard_binding_uri.has_value()) { |
| result.set_hard_binding_uri(*hard_binding_uri); |
| } |
| if (auto multi_asset_hash_uri = GetMultiAssetHashUri( |
| result, manifest_with_content_bindings_label, tracker); |
| multi_asset_hash_uri.has_value()) { |
| result.set_multi_asset_hash_uri(*multi_asset_hash_uri); |
| } |
| } |
| |
| // Returns the containing manifest path from the given assertion path, or |
| // nullopt if the assertion path is invalid. |
| std::optional<absl::string_view> ManifestPathFromAssertionPath( |
| absl::string_view path) { |
| // Absolute assertion path should look like `/c2pa/<manifest |
| // label>/c2pa.assertions/...`. Find the position of the third slash to |
| // extract the manifest path. |
| if (path.empty() || path[0] != '/') { |
| return std::nullopt; |
| } |
| size_t second_slash = path.find('/', 1); |
| if (second_slash == std::string::npos) { |
| return std::nullopt; |
| } |
| size_t third_slash = path.find('/', second_slash + 1); |
| if (third_slash == std::string::npos) { |
| return std::nullopt; |
| } |
| return path.substr(0, third_slash); |
| } |
| |
| absl::string_view ManifestLabel(const jumbf::SuperBox& node) { |
| return node.description.label.value_or(""); |
| } |
| |
| std::string ManifestPath(const jumbf::SuperBox& node) { |
| return absl::StrCat("/c2pa/", ManifestLabel(node)); |
| } |
| |
| // Applies checks based on the manifest type, returning true if processing |
| // should continue. |
| bool CheckManifestType(Manifest& manifest, credentio::Uuid type_uuid, |
| std::string* manifest_with_content_bindings_label, |
| ValidationTracker& tracker) { |
| if (type_uuid == kStandardManifestUuid) { |
| if (manifest_with_content_bindings_label->empty()) { |
| // This is either the active manifest or the first standard manifest |
| // found by following the chain of parent ingredients from the active |
| // manifest. |
| *manifest_with_content_bindings_label = manifest.label(); |
| } |
| return true; |
| } |
| if (type_uuid == kUpdateManifestUuid) { |
| // Update manifest checks are performed in the assertion validator. |
| manifest.set_is_update_manifest(true); |
| return true; |
| } |
| if (type_uuid == kCompressedManifestUuid) { |
| tracker.RecordFailure( |
| FailureStatusCode::kGoogleUnsupportedManifestType, |
| {.explanation = "Compressed manifests not supported"}); |
| return false; |
| } |
| if (type_uuid == kTimestampManifestUuid) { |
| // No need to support time-stamp manifests (deprecated in C2PA 2.2). |
| tracker.RecordFailure(FailureStatusCode::kGoogleUnsupportedManifestType, |
| {.explanation = "Timestamp manifests not supported"}); |
| return false; |
| } |
| tracker.RecordFailure( |
| FailureStatusCode::kGoogleUnsupportedManifestType, |
| {.explanation = |
| absl::StrCat("Unrecognized manifest type: ", type_uuid.ToString())}); |
| return false; |
| } |
| |
| // Returns true if the manifest label uses a legacy (pre-C2PA-2.1) format. |
| bool IsLegacyManifestLabel(absl::string_view manifest_label) { |
| return !absl::StartsWith(manifest_label, "urn:c2pa"); |
| } |
| |
| } // namespace |
| |
| absl::StatusOr<std::unique_ptr<PartialValidationResultProto>> |
| ManifestGraph::Validate() { |
| ABSL_RETURN_IF_ERROR(ValidateGraph()); |
| ABSL_ASSIGN_OR_RETURN(auto result, ValidatePostProcess()); |
| // At this point, the set of redacted assertions and the set of ingredient |
| // manifests should be fully populated. We can perform the second pass of the |
| // ingredient validation algorithm in |
| // https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#_performing_explicit_validation. |
| return ValidateWithRedactions(std::move(result)); |
| } |
| |
| absl::StatusOr<std::unique_ptr<PartialValidationResultProto>> |
| ManifestGraph::ValidatePostProcess() { |
| auto result = std::make_unique<PartialValidationResultProto>(); |
| for (const auto& ingredient_manifest_path : ingredient_manifest_paths_) { |
| ABSL_ASSIGN_OR_RETURN(auto manifest_state, |
| GetValidatedManifestState(ingredient_manifest_path)); |
| absl::flat_hash_set<std::pair<absl::string_view, absl::string_view>> |
| existing_failures; |
| existing_failures.reserve( |
| manifest_state->manifest.validation().failures_size()); |
| for (const auto& failure : |
| manifest_state->manifest.validation().failures()) { |
| existing_failures.insert(std::make_pair(failure.code(), failure.url())); |
| } |
| for (const IngredientAssertion& assertion : |
| manifest_state->referencing_ingredient_assertions) { |
| PropagateFailuresFromIngredientAssertion( |
| assertion.assertion->ingredient_v3(), |
| *manifest_state->manifest.mutable_validation(), existing_failures); |
| } |
| *result->add_ingredient_manifests() = std::move(manifest_state->manifest); |
| } |
| ABSL_ASSIGN_OR_RETURN( |
| auto active_manifest_state, |
| GetValidatedManifestState(ManifestPath(active_manifest_))); |
| *result->mutable_active_manifest() = |
| std::move(active_manifest_state->manifest); |
| SetHardBindingUri(manifest_with_content_bindings_label_, *result); |
| return result; |
| } |
| |
| std::optional<std::string> ManifestGraph::ProcessIngredientAssertion( |
| const Assertion& assertion, absl::string_view manifest_path, |
| absl::string_view default_algorithm, ValidationTracker& tracker) { |
| const std::string assertion_url = absl::StrCat( |
| kAssertionStoreLabel, kManifestLabelDelimiter, assertion.label()); |
| std::optional<HashedUri> manifest_hashed_uri = |
| GetIngredientManifestHashedUri(assertion); |
| if (!manifest_hashed_uri.has_value()) { |
| // Record informational code if the ingredient assertion does not have |
| // an `active_manifest` field, unless relationship is `inputTo` (Step |
| // 7-a-iii in the algorithm). |
| if (GetIngredientRelationship(assertion) != |
| kIngredientRelationshipInputTo) { |
| tracker.RecordInformational( |
| InformationalStatusCode::kIngredientUnknownProvenance, |
| {.url = assertion_url}); |
| } |
| return std::nullopt; |
| } |
| auto absolute_path = uri_resolver_.GetAbsolutePathFromUri( |
| manifest_hashed_uri->url(), manifest_path); |
| if (!absolute_path.ok()) { |
| tracker.RecordFailure(FailureStatusCode::kIngredientManifestMissing, |
| {.url = assertion_url}); |
| return std::nullopt; |
| } |
| if (assertion.ingredient_v3().has_claim_signature()) { |
| // Validate the claim signature hash. |
| const auto& claim_signature = assertion.ingredient_v3().claim_signature(); |
| HashedUriValidator claim_signature_hash_validator( |
| std::string(manifest_path), uri_resolver_, &hash_checker_factory_); |
| claim_signature_hash_validator.SetErrorCodes(HashedUriValidator::Codes{ |
| .missing = FailureStatusCode::kIngredientClaimSignatureMissing, |
| .mismatch = FailureStatusCode::kIngredientClaimSignatureMismatch}); |
| claim_signature_hash_validator.SetDefaultAlgorithm( |
| std::string(default_algorithm)); |
| auto path = claim_signature_hash_validator.Validate(claim_signature, |
| assertion_url, tracker); |
| if (!path.has_value()) { |
| return std::nullopt; // Validation failure status has been recorded. |
| } |
| tracker.RecordSuccess(SuccessStatusCode::kIngredientClaimSignatureValidated, |
| {.url = assertion_url}); |
| } else { |
| // Validate the manifest hash. |
| HashedUriValidator manifest_hash_validator( |
| std::string(manifest_path), uri_resolver_, &hash_checker_factory_); |
| manifest_hash_validator.SetErrorCodes(HashedUriValidator::Codes{ |
| .missing = FailureStatusCode::kIngredientManifestMissing, |
| .mismatch = FailureStatusCode::kIngredientManifestMismatch}); |
| manifest_hash_validator.SetDefaultAlgorithm(std::string(default_algorithm)); |
| auto path = manifest_hash_validator.Validate(*manifest_hashed_uri, |
| assertion_url, tracker); |
| if (!path.has_value()) { |
| return std::nullopt; // Validation failure status has been recorded. |
| } |
| tracker.RecordSuccess(SuccessStatusCode::kIngredientManifestValidated, |
| {.url = assertion_url}); |
| } |
| return *absolute_path; |
| } |
| |
| ManifestGraph::ManifestState& ManifestGraph::GetOrCreateManifestState( |
| absl::string_view path) { |
| auto [it, inserted] = manifests_.insert({std::string(path), nullptr}); |
| if (inserted) { |
| it->second = std::make_unique<ManifestState>(); |
| } |
| return *it->second; |
| } |
| |
| absl::StatusOr<ManifestGraph::ManifestState* absl_nonnull> |
| ManifestGraph::GetValidatedManifestState(absl::string_view path) { |
| auto it = manifests_.find(path); |
| if (it == manifests_.end()) { |
| return absl::InternalError( |
| absl::StrCat("manifest state not found: ", path)); |
| } |
| if (it->second->validation_state != InternalValidationState::kValidated) { |
| return absl::InternalError( |
| absl::StrCat("manifest is not yet validated: ", path)); |
| } |
| return it->second.get(); |
| } |
| |
| absl::Status ManifestGraph::ValidateGraph() { |
| std::stack<const jumbf::SuperBox*> to_visit; |
| to_visit.push(&active_manifest_); |
| bool is_active_manifest = true; |
| while (!to_visit.empty()) { |
| const jumbf::SuperBox* node = to_visit.top(); |
| to_visit.pop(); |
| ABSL_RETURN_IF_ERROR(ValidateNode(*node, |
| /*is_active_manifest=*/is_active_manifest, |
| to_visit)); |
| is_active_manifest = false; |
| } |
| return absl::OkStatus(); |
| } |
| |
| // Validates the claim and assertions for the given "node" that represents a |
| // manifest in the manifest graph. Calling this function for a node is similar |
| // to jumping to Step 3 in the ingredient validation algorithm in |
| // https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#_performing_explicit_validation |
| // for a target manifest represented by this node in the manifest graph. |
| // |
| // The method: |
| // * Locates and validates the claim (Step 4 in the algorithm). |
| // * Updates the set of redacted assertions (Step 5 in the algorithm). |
| // * Validates all assertions in the claim (Step 6 in the algorithm). |
| // * Finally, the method pushes all eligible ingredient manifests onto the |
| // `to_visit` stack (Step 7 in the algorithm). |
| absl::Status ManifestGraph::ValidateNode( |
| const jumbf::SuperBox& node, bool is_active_manifest, |
| std::stack<const jumbf::SuperBox*>& to_visit) { |
| absl::string_view manifest_label = ManifestLabel(node); |
| std::string manifest_path = ManifestPath(node); |
| ManifestState& manifest_state = GetOrCreateManifestState(manifest_path); |
| if (manifest_state.validation_state == InternalValidationState::kValidated) { |
| return absl::OkStatus(); |
| } |
| if (!is_active_manifest) ingredient_manifest_paths_.push_back(manifest_path); |
| InternalValidationStateLock validation_state_lock(&manifest_state); |
| |
| Manifest& manifest = manifest_state.manifest; |
| manifest.set_label(manifest_label); |
| ValidationTracker tracker(&manifest); |
| { |
| if (IsLegacyManifestLabel(manifest_label)) { |
| tracker.RecordFailure( |
| FailureStatusCode::kGoogleUnsupportedSpecVersion, |
| {.explanation = "deprecated manifest label format"}); |
| return absl::OkStatus(); |
| } |
| if (!CheckManifestType(manifest, node.description.type_uuid, |
| &manifest_with_content_bindings_label_, tracker)) { |
| return absl::OkStatus(); |
| } |
| |
| auto validated_claim = claim_validator_.Validate(node, tracker); |
| if (!validated_claim.has_value()) { |
| return absl::OkStatus(); |
| } |
| *manifest.mutable_claim() = *std::move(validated_claim); |
| // Update the set of redacted assertions. |
| for (const auto& assertion : manifest.claim().redacted_assertions()) { |
| ProcessRedaction(assertion, manifest_path, tracker); |
| } |
| // |
| // Validate assertions and populate `manifest.assertions`. |
| assertion_validator_.ValidateClaimAssertions( |
| uri_resolver_, manifest_state.redacted_assertions, manifest, tracker); |
| // Find ingredient manifests from the validated ingredient assertions and |
| // add them to the `ingredients` field of the result. |
| std::vector<std::string> ingredients; |
| for (const auto& assertion : manifest.assertions()) { |
| if (IsIngredientAssertion(assertion)) { |
| auto ingredient_manifest_path = ProcessIngredientAssertion( |
| assertion, manifest_path, |
| /*default_algorithm=*/ |
| manifest.claim().default_algorithm(), tracker); |
| if (ingredient_manifest_path.has_value()) { |
| auto ingredient_manifest_box = |
| uri_resolver_.ResolvePath(*ingredient_manifest_path); |
| if (!ingredient_manifest_box.ok() || |
| *ingredient_manifest_box == nullptr) { |
| tracker.RecordFailure( |
| FailureStatusCode::kIngredientManifestMissing, |
| {.url = |
| absl::StrCat(kAssertionStoreLabel, kManifestLabelDelimiter, |
| assertion.label()), |
| .explanation = absl::StrCat( |
| "Failed to resolve the ingredient manifest URI: ", |
| *ingredient_manifest_path)}); |
| } else { |
| ManifestState& ingredient_manifest_state = |
| GetOrCreateManifestState(*ingredient_manifest_path); |
| ingredient_manifest_state.referencing_ingredient_assertions |
| .push_back(IngredientAssertion{.assertion = &assertion, |
| .assertion_uri = GetAssertionUri( |
| manifest_label, assertion)}); |
| // Update the set of ingredients that will be recursed into. |
| if (ingredient_manifest_state.validation_state != |
| InternalValidationState::kValidated) { |
| ingredients.push_back(*ingredient_manifest_path); |
| } |
| } |
| } |
| } |
| } |
| // Push ingredient manifests onto the stack for depth-first traversal. |
| for (; !ingredients.empty(); ingredients.pop_back()) { |
| const std::string& ingredient_manifest = ingredients.back(); |
| auto ingredient_manifest_box = |
| uri_resolver_.ResolvePath(ingredient_manifest); |
| if (!ingredient_manifest_box.ok() || |
| *ingredient_manifest_box == nullptr) { |
| // This should never happen because we have already validated the |
| // ingredient manifest URI. |
| continue; |
| } |
| to_visit.push(*ingredient_manifest_box); |
| } |
| } |
| if (manifest.validation().failures_size() == 0 && |
| manifest.validation().successes_size() == 0) { |
| tracker.RecordFailure(FailureStatusCode::kGoogleInternalError, |
| {.explanation = "No failure or success " |
| "status codes were recorded."}); |
| } |
| return absl::OkStatus(); |
| } |
| |
| absl::StatusOr<std::unique_ptr<PartialValidationResultProto>> |
| ManifestGraph::ValidateWithRedactions( |
| std::unique_ptr<PartialValidationResultProto> result) { |
| return result; |
| } |
| |
| void ManifestGraph::ProcessRedaction(absl::string_view redacted_assertion_uri, |
| absl::string_view manifest_path, |
| ValidationTracker& tracker) { |
| // Resolve the redacted assertion URI to an absolute path before adding it |
| // to the set. |
| absl::StatusOr<std::string> absolute_path = |
| uri_resolver_.GetAbsolutePathFromUri(redacted_assertion_uri, |
| manifest_path); |
| if (!absolute_path.ok()) { |
| // Malformed URI. |
| tracker.RecordFailure(FailureStatusCode::kAssertionMissing, {}); |
| return; |
| } |
| // Check if the redacted assertion URI points into the manifest whose claim |
| // redacted it. |
| if (absl::StartsWith(*absolute_path, manifest_path)) { |
| tracker.RecordFailure(FailureStatusCode::kAssertionSelfRedacted, {}); |
| return; |
| } |
| std::optional<absl::string_view> redacted_assertion_manifest_path = |
| ManifestPathFromAssertionPath(*absolute_path); |
| if (!redacted_assertion_manifest_path.has_value()) { |
| // Malformed path. |
| tracker.RecordFailure(FailureStatusCode::kAssertionMissing, {}); |
| return; |
| } |
| GetOrCreateManifestState(*redacted_assertion_manifest_path) |
| .redacted_assertions.insert(*absolute_path); |
| } |
| |
| ManifestGraph::InternalValidationStateLock::InternalValidationStateLock( |
| ManifestState* absl_nonnull manifest_state) |
| : manifest_state_(*manifest_state) { |
| DCHECK(manifest_state_.validation_state == |
| InternalValidationState::kUnvalidated); |
| manifest_state_.validation_state = InternalValidationState::kBeingValidated; |
| } |
| |
| ManifestGraph::InternalValidationStateLock::~InternalValidationStateLock() { |
| manifest_state_.validation_state = InternalValidationState::kValidated; |
| } |
| } // namespace credentio |