blob: 80c4bd6a75a6695e012ee7964436676d9bc220a8 [file] [edit]
// 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