| // 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 "bindings/boxes_hash_validator.h" | |
| #include <cstdint> | |
| #include <limits> | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_macros.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/string_view.h" | |
| #include "absl/types/span.h" | |
| #include "constants/status_codes.h" | |
| #include "crypto/algorithms.h" | |
| #include "proto/boxes_hash_assertion.pb.h" | |
| namespace credentio { | |
| namespace { | |
| constexpr absl::string_view kSingleZeroByte("\0", 1); | |
| absl::string_view DetermineAlgorithm( | |
| absl::Span<const absl::string_view> algos) { | |
| absl::string_view algo_to_use = ""; | |
| for (const auto& alg : algos) { | |
| if (!alg.empty()) { | |
| algo_to_use = alg; | |
| break; | |
| } | |
| } | |
| return algo_to_use; | |
| } | |
| } // namespace | |
| absl::Status BoxesHashValidator::Validate() { | |
| if (assertion_.boxes().empty()) { | |
| return LogFailure(FailureStatusCode::kAssertionBoxesHashMalformed, | |
| "box-map.boxes is empty"); | |
| } | |
| for (const auto& box_hash_map : assertion_.boxes()) { | |
| ABSL_RETURN_IF_ERROR(ValidateBoxHashMap(box_hash_map)); | |
| } | |
| return absl::OkStatus(); | |
| } | |
| absl::Status BoxesHashValidator::LogFailure(FailureStatusCode failure_code, | |
| absl::string_view explanation) { | |
| tracker_.RecordFailure( | |
| failure_code, {.url = hard_binding_uri_, .explanation = explanation}); | |
| return absl::InvalidArgumentError(explanation); | |
| } | |
| void BoxesHashValidator::LogAdditionalExclusionsPresent() { | |
| if (!found_additional_exclusions_) { | |
| found_additional_exclusions_ = true; | |
| tracker_.RecordInformational( | |
| InformationalStatusCode::kAssertionBoxesHashAdditionalExclusionsPresent, | |
| {.url = hard_binding_uri_}); | |
| } | |
| } | |
| absl::Status BoxesHashValidator::ValidateBoxHashMap( | |
| const BoxHash& box_hash_map) { | |
| if (box_hash_map.names().empty()) { | |
| return LogFailure(FailureStatusCode::kAssertionBoxesHashMalformed, | |
| "box-map.names is empty"); | |
| } | |
| if (box_hash_map.hash().empty()) { | |
| return LogFailure(FailureStatusCode::kAssertionBoxesHashMismatch, | |
| "box-map.hash is empty"); | |
| } | |
| absl::string_view algo = | |
| DetermineAlgorithm({box_hash_map.alg(), assertion_.alg(), claims_algo_}); | |
| if (!ParseHashAlgorithm(algo).ok()) { | |
| return LogFailure(FailureStatusCode::kAlgorithmUnsupported, | |
| absl::StrCat("Algorithm not supported: ", algo)); | |
| } | |
| bool contains_c2pa_box = false; | |
| for (const auto& name : box_hash_map.names()) { | |
| if (name == "C2PA") { | |
| contains_c2pa_box = true; | |
| if (box_hash_map.names_size() != 1) { | |
| return LogFailure(FailureStatusCode::kAssertionBoxesHashMalformed, | |
| "C2PA box must be the only name in the names array"); | |
| } | |
| if (box_hash_map.hash() != kSingleZeroByte) { | |
| // Is this the right error code? It's not really a mismatch. | |
| return LogFailure(FailureStatusCode::kAssertionBoxesHashMismatch, | |
| "C2PA box hash is not a single 0 byte"); | |
| } | |
| } | |
| } | |
| if (box_hash_map.excluded()) { | |
| return ValidateExclusion(box_hash_map, contains_c2pa_box); | |
| } else if (!box_hash_map.exclusions().empty()) { | |
| return ValidateExclusionRanges(box_hash_map, contains_c2pa_box); | |
| } | |
| return absl::OkStatus(); | |
| } | |
| absl::Status BoxesHashValidator::ValidateExclusion(const BoxHash& box_hash_map, | |
| bool contains_c2pa_box) { | |
| if (!contains_c2pa_box) { | |
| LogAdditionalExclusionsPresent(); | |
| } | |
| return absl::OkStatus(); | |
| } | |
| absl::Status BoxesHashValidator::ValidateExclusionRanges( | |
| const BoxHash& box_hash_map, bool contains_c2pa_box) { | |
| int64_t last_box_index = -1; | |
| int64_t last_start = -1; | |
| int64_t last_end_offset = -1; | |
| for (const auto& exclusion : box_hash_map.exclusions()) { | |
| if (exclusion.start() < 0 || exclusion.length() < 0) { | |
| return LogFailure( | |
| FailureStatusCode::kAssertionBoxesHashMalformed, | |
| "box-map.start and box-map.length must be non-negative"); | |
| } | |
| if (std::numeric_limits<int64_t>::max() - exclusion.start() < | |
| exclusion.length()) { | |
| return LogFailure(FailureStatusCode::kAssertionBoxesHashMalformed, | |
| "box-map.start + box-map.length overflows"); | |
| } | |
| if (!exclusion.has_box_index() && box_hash_map.names_size() > 1) { | |
| return LogFailure(FailureStatusCode::kAssertionBoxesHashMalformed, | |
| "box-map.boxIndex is required when there is more " | |
| "than one box"); | |
| } else if (exclusion.has_box_index() && | |
| (exclusion.box_index() < 0 || | |
| exclusion.box_index() >= box_hash_map.names_size())) { | |
| return LogFailure(FailureStatusCode::kAssertionBoxesHashMalformed, | |
| "box-map.boxIndex is out of range"); | |
| } | |
| int64_t new_box_index = | |
| exclusion.has_box_index() ? exclusion.box_index() : 0; | |
| if (last_box_index == -1 || last_box_index < new_box_index) { | |
| // First exclusion of this box, so just set the last box index. | |
| last_box_index = new_box_index; | |
| last_start = exclusion.start(); | |
| last_end_offset = last_start + exclusion.length(); | |
| continue; | |
| } else if (new_box_index < last_box_index) { | |
| // Next box index should be greater or equal to the last one. | |
| return LogFailure(FailureStatusCode::kAssertionBoxesHashMalformed, | |
| "box-map.boxIndex is not in order"); | |
| } else if (new_box_index == last_box_index) { | |
| if (last_start >= exclusion.start() || | |
| last_end_offset > exclusion.start()) { | |
| return LogFailure(FailureStatusCode::kAssertionBoxesHashMalformed, | |
| "box-map.start is overlapping with the last " | |
| "box-map.start"); | |
| } | |
| last_start = exclusion.start(); | |
| last_end_offset = last_start + exclusion.length(); | |
| } | |
| } | |
| if (!contains_c2pa_box) { | |
| LogAdditionalExclusionsPresent(); | |
| } | |
| return absl::OkStatus(); | |
| } | |
| } // namespace credentio |