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