blob: 22359ea6745bf87c9f0e0a20aa49e173ae8a8b34 [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_hard_binding_validator.h"
#include <cstdint>
#include <memory>
#include <utility>
#include <vector>
#include "absl/base/nullability.h"
#include "absl/container/flat_hash_map.h"
#include "absl/log/die_if_null.h"
#include "absl/status/status.h"
#include "absl/status/status_macros.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "absl/types/span.h"
#include "bindings/binding_hasher.h"
#include "bindings/boxes_hash_validator.h"
#include "constants/status_codes.h"
#include "formats/asset_box.h"
#include "formats/asset_byte_info.h"
#include "formats/byte_range.h"
#include "proto/assertion.pb.h"
#include "proto/boxes_hash_assertion.pb.h"
#include "proto/manifest.pb.h"
#include "proto/validation_result.pb.h"
#include "riegeli/bytes/reader.h"
#include "utils/dual_status_tracker.h"
#include "utils/status_tracker.h"
#include "validator/result.h"
#include "validator/validation_result_internal.h"
namespace credentio {
namespace {
class BoxesHashBindingHasherTracker : public BindingHasherTracker {
public:
explicit BoxesHashBindingHasherTracker(StatusTracker* tracker,
absl::string_view hard_binding_uri)
: tracker_(*ABSL_DIE_IF_NULL(tracker)),
hard_binding_uri_(hard_binding_uri) {}
void RecordMismatch() override {
tracker_.RecordFailure(FailureStatusCode::kAssertionBoxesHashMismatch,
{.url = hard_binding_uri_});
}
void RecordMalformed() override {
tracker_.RecordFailure(FailureStatusCode::kAssertionBoxesHashMalformed,
{.url = hard_binding_uri_});
}
void RecordAlgorithmUnsupported() override {
tracker_.RecordFailure(FailureStatusCode::kAlgorithmUnsupported,
{.url = hard_binding_uri_});
}
void RecordGeneralError(absl::Status error) override {
tracker_.RecordFailure(
FailureStatusCode::kGeneralError,
{.url = hard_binding_uri_, .explanation = error.message()});
}
private:
StatusTracker& tracker_;
absl::string_view hard_binding_uri_;
};
absl::Status AddInclusions(const AssetBox& asset_box,
absl::Span<const BoxExclusion> exclusions,
absl::string_view hard_binding_uri,
StatusTracker& tracker,
std::vector<BindingHasher::Chunk>& inclusions) {
uint64_t last_handled_offset = asset_box.byte_range.offset;
for (const auto& exclusion : exclusions) {
uint64_t exclusion_asset_offset =
asset_box.byte_range.offset + exclusion.start();
if (exclusion.start() >= asset_box.byte_range.length ||
exclusion.length() >
(asset_box.byte_range.length - exclusion.start())) {
// Exclusion starts after the box or extends beyond the end of the box.
tracker.RecordFailure(
FailureStatusCode::kAssertionBoxesHashMalformed,
{.url = hard_binding_uri,
.explanation = "Exclusion starts after the box or extends beyond "
"the end of the box"});
return absl::InvalidArgumentError(
"Exclusion starts after the box or extends beyond the end of the "
"box");
}
if (last_handled_offset < exclusion_asset_offset) {
inclusions.push_back({
.op = BindingHasher::Chunk::Op::kInclusion,
.offset = last_handled_offset,
.length = static_cast<int64_t>(exclusion_asset_offset) -
static_cast<int64_t>(last_handled_offset),
});
}
last_handled_offset = exclusion_asset_offset + exclusion.length();
}
if (last_handled_offset <
(asset_box.byte_range.offset + asset_box.byte_range.length)) {
inclusions.push_back({
.op = BindingHasher::Chunk::Op::kInclusion,
.offset = last_handled_offset,
.length = static_cast<int64_t>(asset_box.byte_range.offset +
asset_box.byte_range.length) -
static_cast<int64_t>(last_handled_offset),
});
}
return absl::OkStatus();
}
} // namespace
absl::StatusOr<std::unique_ptr<ValidationResultProto>>
BoxesHashHardBindingValidator::Validate(
riegeli::Reader& contents, const AssetByteInfo& asset_byte_info,
std::unique_ptr<PartialValidationResultProto> partial_validation_result)
const {
const Assertion* absl_nullable assertion =
GetAssertion(partial_validation_result.get(),
partial_validation_result->hard_binding_uri());
if (assertion == nullptr) {
return absl::InvalidArgumentError(absl::StrCat(
"missing assertion: ", partial_validation_result->hard_binding_uri()));
}
if (!assertion->has_boxes_hash()) {
return absl::InvalidArgumentError(
absl::StrCat("assertion is not a boxes hash assertion: ",
partial_validation_result->hard_binding_uri()));
}
ABSL_ASSIGN_OR_RETURN(auto tracker,
DualStatusTracker::FromPartialValidationResult(
partial_validation_result.get()));
Validate(contents, asset_byte_info.boxes, assertion->boxes_hash(),
partial_validation_result->hard_binding_uri(), *tracker);
return MakeFullValidationResult(std::move(partial_validation_result));
}
void BoxesHashHardBindingValidator::Validate(
riegeli::Reader& contents, std::vector<AssetBox> contents_boxes,
const BoxesHashAssertion& assertion, absl::string_view hard_binding_uri,
StatusTracker& tracker) const {
if (!BoxesHashValidator(assertion, hard_binding_uri,
/*claims_algo=*/"", tracker)
.Validate()
.ok()) {
return;
}
// Needed to determine "unknown box" vs "out of order box"
absl::flat_hash_map<absl::string_view, uint64_t> box_counts;
for (const auto& box : contents_boxes) {
box_counts[box.identifier] += 1;
}
int asset_box_index = 0;
for (const auto& assertion_box_map : assertion.boxes()) {
auto algorithm(assertion_box_map.alg());
if (algorithm.empty()) {
algorithm = assertion.alg();
}
std::vector<BindingHasher::Chunk> inclusions;
for (int box_name_idx = 0; box_name_idx < assertion_box_map.names_size();
++box_name_idx) {
const absl::string_view assertion_box_name =
assertion_box_map.names(box_name_idx);
if (asset_box_index >= contents_boxes.size()) {
// Additional boxes in the assertion that are not in the asset.
tracker.RecordFailure(FailureStatusCode::kAssertionBoxesHashMismatch,
{.url = hard_binding_uri});
return;
}
const auto& current_asset_box = contents_boxes[asset_box_index];
if (current_asset_box.identifier != assertion_box_name) {
tracker.RecordFailure(
box_counts[assertion_box_name] > 0
? FailureStatusCode::kAssertionBoxesHashMismatch
: FailureStatusCode::kAssertionBoxesHashUnknownBox,
{.url = hard_binding_uri});
return;
}
--box_counts[assertion_box_name];
if (assertion_box_name != "C2PA") {
std::vector<BoxExclusion> exclusions;
if (!assertion_box_map.excluded()) {
for (const auto& exclusion : assertion_box_map.exclusions()) {
if (exclusion.has_box_index() &&
exclusion.box_index() > box_name_idx) {
// Exclusion is for a box further down the line.
break;
}
if (!exclusion.has_box_index() ||
exclusion.box_index() == box_name_idx) {
exclusions.push_back(exclusion);
}
}
}
if (!AddInclusions(current_asset_box, exclusions, hard_binding_uri,
tracker, inclusions)
.ok()) {
return;
}
}
++asset_box_index;
}
BoxesHashBindingHasherTracker boxes_hash_tracker(&tracker,
hard_binding_uri);
if (!inclusions.empty()) {
auto hasher = BindingHasher::Create(algorithm, std::move(inclusions),
boxes_hash_tracker);
if (hasher == nullptr) {
return;
}
auto digest = hasher->Digest(contents, boxes_hash_tracker);
if (!digest.has_value()) {
tracker.RecordFailure(
FailureStatusCode::kGoogleInternalError,
{.url = hard_binding_uri, .explanation = "Digest failed"});
return;
}
// If the box_set is excluded, we don't care if the hash matches, just
// that it can be hashed.
bool required =
!(assertion_box_map.has_excluded() && assertion_box_map.excluded());
if (required && *digest != assertion_box_map.hash()) {
tracker.RecordFailure(FailureStatusCode::kAssertionBoxesHashMismatch,
{.url = hard_binding_uri});
return;
}
}
}
if (asset_box_index < contents_boxes.size()) {
// Additional boxes in the asset that are not in the assertion.
tracker.RecordFailure(FailureStatusCode::kAssertionBoxesHashUnknownBox,
{.url = hard_binding_uri});
return;
}
tracker.RecordSuccess(SuccessStatusCode::kAssertionBoxesHashMatch,
{.url = hard_binding_uri});
}
} // namespace credentio