blob: 9478d116961e3e90007d26b36188ba4ebfb23d7d [file]
// 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/validator.h"
#include <algorithm>
#include <cstdint>
#include <iterator>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <variant>
#include <vector>
#include "absl/base/nullability.h"
#include "absl/container/flat_hash_map.h"
#include "absl/functional/function_ref.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/strings/substitute.h"
#include "bindings/bmff_hash_hard_binding_validator.h"
#include "bindings/boxes_hash_hard_binding_validator.h"
#include "bindings/collection_data_hash_hard_binding_validator.h"
#include "bindings/data_hash_hard_binding_validator.h"
#include "constants/status_codes.h"
#include "formats/asset_box.h"
#include "formats/bmff/assessor.h"
#include "formats/bmff/box_header.h"
#include "formats/byte_range.h"
#include "formats/format.h"
#include "formats/registry.h"
#include "jumbf/uri.h"
#include "proto/assertion.pb.h"
#include "proto/bmff_based_hash_assertion.pb.h"
#include "proto/hashed_uri.pb.h"
#include "proto/manifest.pb.h"
#include "proto/multi_asset_hash_assertion.pb.h"
#include "proto/validation_result.pb.h"
#include "proto/validation_status.pb.h"
#include "riegeli/bytes/reader.h"
#include "utils/dual_status_tracker.h"
#include "utils/status_tracker.h"
#include "utils/two_stage_status_tracker.h"
#include "utils/uri.h"
#include "validator/result.h"
#include "validator/validation_result_internal.h"
namespace credentio {
namespace {
struct OffsetLocator {
int64_t offset;
int64_t length;
};
struct BmffLocator {
std::string path;
};
absl::StatusOr<std::variant<OffsetLocator, BmffLocator>> GetLocator(
const MultiAssetPartLocation& part) {
if (!part.has_byte_offset() && !part.has_length() && !part.has_bmff_box()) {
return absl::InvalidArgumentError("no location specified");
}
if (part.has_bmff_box() && (part.has_byte_offset() || part.has_length())) {
return absl::InvalidArgumentError(
"bmff box cannot be used with byte range locator");
}
if (part.has_bmff_box()) {
return BmffLocator{.path = std::string(part.bmff_box())};
}
if (!part.has_byte_offset() || !part.has_length()) {
return absl::InvalidArgumentError(
"byte range locator must have both offset and length");
}
return OffsetLocator{.offset = part.byte_offset(), .length = part.length()};
}
std::unique_ptr<PartialValidationResultProto> CopyManifestLabels(
const PartialValidationResultProto& source) {
auto target = std::make_unique<PartialValidationResultProto>();
target->set_hard_binding_uri(source.hard_binding_uri());
target->set_multi_asset_hash_uri(source.multi_asset_hash_uri());
target->mutable_active_manifest()->set_label(
source.active_manifest().label());
for (const Manifest& manifest : source.ingredient_manifests()) {
target->add_ingredient_manifests()->set_label(manifest.label());
}
return target;
}
absl::StatusOr<std::unique_ptr<ValidationResultProto>>
CompleteValidationResults(
std::unique_ptr<PartialValidationResultProto> target,
std::unique_ptr<PartialValidationResultProto> source) {
target->mutable_active_manifest()->mutable_validation()->MergeFrom(
std::move(*source->mutable_active_manifest()->mutable_validation()));
absl::flat_hash_map<absl::string_view, ValidationStatusSet> source_results;
for (const Manifest& source_manifest : source->ingredient_manifests()) {
source_results[source_manifest.label()] =
std::move(source_manifest.validation());
}
for (Manifest& target_manifest : *target->mutable_ingredient_manifests()) {
if (source_results.contains(target_manifest.label())) {
target_manifest.mutable_validation()->MergeFrom(
std::move(source_results[target_manifest.label()]));
}
}
return MakeFullValidationResult(std::move(target));
}
// Validates the parts of a multi-asset hash assertion. This ensures that the
// parts do not overlap and that the total size of the required parts does not
// exceed the size of the asset. Only problems accessing the files return as
// non-OK status, other errors are recorded in the status tracker.
absl::Status ValidateParts(const MultiAssetHashAssertion& assertion,
absl::string_view assertion_uri, int64_t asset_size,
StatusTracker& tracker) {
int64_t next_expected_offset = 0;
int64_t minimum_required_size = 0;
for (const auto& part : assertion.parts()) {
absl::StatusOr<std::variant<OffsetLocator, BmffLocator>> locator =
GetLocator(part.location());
if (!locator.ok()) {
tracker.RecordFailure(
FailureStatusCode::kAssertionMultiAssetHashMalformed,
{.url = assertion_uri});
return absl::OkStatus();
}
if (std::holds_alternative<BmffLocator>(*locator)) {
// Contains a BMFF locator, cannot ensure bytes included based on offsets.
return absl::OkStatus();
}
OffsetLocator location = std::get<OffsetLocator>(*locator);
if (location.offset != next_expected_offset) {
tracker.RecordFailure(
FailureStatusCode::kAssertionMultiAssetHashMalformed,
{.url = assertion_uri});
return absl::OkStatus();
}
next_expected_offset = location.offset + location.length;
if (!part.is_optional()) {
minimum_required_size += location.length;
}
}
if (minimum_required_size > asset_size) {
tracker.RecordFailure(
FailureStatusCode::kAssertionMultiAssetHashMissingPart,
{.url = assertion_uri});
}
return absl::OkStatus();
}
} // namespace
absl::StatusOr<ContentBindingValidator::Structure>
ContentBindingValidator::ExtractAssetStructure(riegeli::Reader& contents,
const Format* format,
int64_t end_offset) const {
ContentBindingValidator::Structure structure{
.asset = ByteRange{.offset = contents.pos(),
.length = end_offset - contents.pos()}};
absl::StatusOr<const Format*> inferred_format;
if (format == nullptr) {
inferred_format = format_registry_->GetFormat(contents);
if (!inferred_format.ok()) {
if (structure.asset.offset == 0) {
// Invalid format on the first asset therefore unsupported format.
return inferred_format.status();
}
return structure;
}
format = *inferred_format;
}
absl::StatusOr<std::vector<AssetBox>> boxes =
format->extractor()->ExtractBoxes(contents, {.requires_c2pa = false});
if (boxes.ok() && !boxes->empty()) {
ABSL_ASSIGN_OR_RETURN(
int64_t end_of_asset_offset,
PopulateStructureFromBoxes(*std::move(boxes), structure));
// Only assets which contain boxes can support multiple assets (by finding
// the c2pa.after box). Navigate to the start of the next potential asset
// and continue to extract the structure.
if (!contents.Seek(end_of_asset_offset) ||
contents.pos() != end_of_asset_offset) {
return contents.StatusOrAnnotate(
absl::InternalError("failed to seek to end of asset"));
}
return structure;
}
// The determined format does not support multiple assets at this point,
// attempt to find the manifest store location and treat the remainder of
// the file as the last asset.
absl::StatusOr<std::optional<ByteRange>> manifest_store_location =
format->extractor()->ExtractManifestStoreLocation(
contents, {.requires_c2pa = false, .end_offset = end_offset});
if (manifest_store_location.ok() && manifest_store_location->has_value()) {
structure.manifest_store_location = *std::move(manifest_store_location);
}
return structure;
}
absl::StatusOr<int64_t> ContentBindingValidator::PopulateStructureFromBoxes(
std::vector<AssetBox> boxes,
ContentBindingValidator::Structure& structure) const {
if (boxes.empty()) {
return absl::InvalidArgumentError("no boxes found");
}
structure.boxes = std::move(boxes);
const AssetBox* last_box = nullptr;
for (const AssetBox& box : structure.boxes) {
last_box = &box;
if (box.identifier == "C2PA") {
structure.manifest_store_location = ByteRange{
.offset = box.byte_range.offset, .length = box.byte_range.length};
}
}
if (last_box != nullptr && last_box->identifier == "c2pa.after") {
structure.asset.length =
last_box->byte_range.offset - structure.asset.offset;
}
return structure.asset.offset + structure.asset.length;
}
absl::StatusOr<std::vector<ContentBindingValidator::Structure>>
ContentBindingValidator::GetStructure(riegeli::Reader& contents,
const Format& format) const {
std::vector<ContentBindingValidator::Structure> structures;
if (!contents.SupportsSize() || !contents.Size().has_value()) {
return absl::InvalidArgumentError("asset does not support size");
}
if (contents.Size() == 0) {
return absl::InvalidArgumentError("asset has no contents");
}
if (!contents.Seek(0) || contents.pos() != 0) {
return contents.StatusOrAnnotate(
absl::InternalError("failed to seek to start of asset"));
}
uint64_t end_offset = *contents.Size();
bool at_end_of_file = false;
while (!at_end_of_file) {
// Format is always inferred for non-first parts.
const Format* part_format = structures.empty() ? &format : nullptr;
ABSL_ASSIGN_OR_RETURN(
ContentBindingValidator::Structure structure,
ExtractAssetStructure(contents, part_format, end_offset));
at_end_of_file =
(structure.asset.offset + structure.asset.length) == end_offset;
structures.push_back(std::move(structure));
}
return structures;
}
absl::StatusOr<std::unique_ptr<ValidationResultProto>>
ContentBindingValidator::Validate(riegeli::Reader& contents,
const Format& format,
std::unique_ptr<PartialValidationResultProto>
partial_validation_result) const {
std::unique_ptr<PartialValidationResultProto> result =
CopyManifestLabels(*partial_validation_result);
ABSL_ASSIGN_OR_RETURN(
std::unique_ptr<DualStatusTracker> tracker,
DualStatusTracker::FromPartialValidationResult(&*result));
bool assertion_in_ingredient_manifest = tracker->WritesToIngredientManifest();
TwoStageStatusTracker staging_tracker(*tracker);
ABSL_ASSIGN_OR_RETURN(
std::vector<ContentBindingValidator::Structure> asset_structure,
GetStructure(contents, format));
auto assertion_fetcher = [&](absl::string_view uri) {
return GetAssertion(partial_validation_result.get(), uri);
};
// Getting the structure causes the Tell to be at the end of the file, reset.
if (!contents.Seek(0) || contents.pos() != 0) {
return contents.StatusOrAnnotate(
absl::InternalError("failed to seek to start of asset"));
}
ABSL_RETURN_IF_ERROR(ValidateAssertion(
contents, assertion_fetcher,
partial_validation_result->hard_binding_uri(), asset_structure,
/*requires_c2pa=*/true, staging_tracker,
assertion_in_ingredient_manifest));
if (!staging_tracker.HasFailuresInStage1() ||
partial_validation_result->multi_asset_hash_uri().empty()) {
// Success or no multi-asset hard binding, finished.
staging_tracker.WriteStage1();
return CompleteValidationResults(std::move(partial_validation_result),
std::move(result));
}
staging_tracker.MoveStage1ToStage2();
// Validate the multi-asset hard binding assertion.
if (!contents.Seek(0) || contents.pos() != 0) {
return contents.StatusOrAnnotate(
absl::InternalError("failed to seek to start of asset"));
}
ABSL_RETURN_IF_ERROR(ValidateAssertion(
contents, assertion_fetcher,
partial_validation_result->multi_asset_hash_uri(), asset_structure,
/*requires_c2pa=*/true, staging_tracker,
assertion_in_ingredient_manifest));
if (staging_tracker.HasFailuresInStage1()) {
// Failed, add all the staged codes to the result.
staging_tracker.WriteStage2();
}
staging_tracker.WriteStage1();
return CompleteValidationResults(std::move(partial_validation_result),
std::move(result));
}
absl::Status ContentBindingValidator::ValidateAssertion(
riegeli::Reader& contents, AssertionFetcherRef assertion_fetcher,
absl::string_view assertion_uri,
std::vector<ContentBindingValidator::Structure> asset_structure,
bool requires_c2pa, TwoStageStatusTracker& tracker,
bool assertion_in_ingredient_manifest, int64_t end_offset) const {
const Assertion* absl_nullable assertion = assertion_fetcher(assertion_uri);
if (assertion == nullptr) {
return absl::InvalidArgumentError(
absl::StrCat("missing assertion: ", assertion_uri));
}
switch (assertion->assertion_case()) {
case Assertion::kBoxesHash:
if (asset_structure.empty()) {
return absl::InvalidArgumentError(
"asset structure cannot be determined");
}
return ValidateBoxesHash(contents, assertion_uri, assertion->boxes_hash(),
asset_structure, requires_c2pa, tracker,
end_offset);
case Assertion::kDataHash:
return ValidateDataHash(
contents, assertion_uri, assertion->data_hash(),
asset_structure.empty()
? std::nullopt
: asset_structure.front().manifest_store_location,
requires_c2pa, tracker, assertion_in_ingredient_manifest, end_offset);
case Assertion::kCollectionDataHash:
return ValidateCollectionDataHash(contents, assertion_uri,
assertion->collection_data_hash(),
requires_c2pa, tracker);
case Assertion::kBmffBasedHash:
return ValidateBmffHash(contents, assertion_uri,
assertion->bmff_based_hash(), requires_c2pa,
tracker);
case Assertion::kMultiAssetHash:
return ValidateMultiAssetHash(
contents, assertion_uri, assertion->multi_asset_hash(),
asset_structure, requires_c2pa, assertion_fetcher, tracker,
assertion_in_ingredient_manifest);
default:
return absl::InvalidArgumentError(
absl::StrCat("assertion is not a hard binding assertion: ",
assertion->assertion_case()));
}
}
absl::Status ContentBindingValidator::ValidateDataHash(
riegeli::Reader& contents, absl::string_view assertion_uri,
const DataHashAssertion& assertion,
std::optional<ByteRange> manifest_store_location, bool requires_c2pa,
StatusTracker& tracker, bool assertion_in_ingredient_manifest,
int64_t end_offset) const {
DataHashHardBindingValidator().Validate(
contents, manifest_store_location, assertion, assertion_uri, tracker,
assertion_in_ingredient_manifest, contents.pos(), end_offset);
return absl::OkStatus();
}
absl::Status ContentBindingValidator::ValidateCollectionDataHash(
riegeli::Reader& contents, absl::string_view assertion_uri,
const CollectionDataHashAssertion& assertion, bool requires_c2pa,
StatusTracker& tracker) const {
CollectionDataHashHardBindingValidator().Validate(contents, assertion,
assertion_uri, tracker);
return absl::OkStatus();
}
absl::Status ContentBindingValidator::ValidateBoxesHash(
riegeli::Reader& contents, absl::string_view assertion_uri,
const BoxesHashAssertion& assertion,
std::vector<ContentBindingValidator::Structure> remaining_structure,
bool requires_c2pa, StatusTracker& tracker, int64_t end_offset) const {
if (assertion.boxes().empty()) {
// No boxes in assertion.
tracker.RecordFailure(FailureStatusCode::kAssertionBoxesHashMalformed,
{.url = assertion_uri});
return absl::OkStatus();
}
for (const auto& box : assertion.boxes()) {
if (box.names().empty()) {
// Boxes Assertion has no names.
tracker.RecordFailure(FailureStatusCode::kAssertionBoxesHashMalformed,
{.url = assertion_uri});
return absl::OkStatus();
}
}
bool assertion_has_c2pa_after =
*assertion.boxes().rbegin()->names().rbegin() == "c2pa.after";
if (remaining_structure.empty()) {
// No Asset Data.
tracker.RecordFailure(FailureStatusCode::kAssertionBoxesHashMismatch,
{.url = assertion_uri});
return absl::OkStatus();
}
std::vector<AssetBox> asset_boxes = remaining_structure.front().boxes;
if (asset_boxes.empty()) {
// No boxes in asset.
tracker.RecordFailure(FailureStatusCode::kAssertionBoxesHashMismatch,
{.url = assertion_uri});
return absl::OkStatus();
}
bool extraction_has_c2pa_after =
asset_boxes.back().identifier == "c2pa.after";
if (!assertion_has_c2pa_after && extraction_has_c2pa_after) {
int64_t part_idx = 1;
while (part_idx < remaining_structure.size() &&
!remaining_structure[part_idx].boxes.empty()) {
// Remove the c2pa.after box.
asset_boxes.pop_back();
// Add the boxes from the next asset.
Structure& next_asset = remaining_structure[part_idx];
asset_boxes.reserve(asset_boxes.size() + next_asset.boxes.size());
asset_boxes.insert(asset_boxes.end(), next_asset.boxes.begin(),
next_asset.boxes.end());
// Increment the index to the next asset.
++part_idx;
}
}
BoxesHashHardBindingValidator().Validate(contents, asset_boxes, assertion,
assertion_uri, tracker);
return absl::OkStatus();
}
absl::Status ContentBindingValidator::ValidateBmffHash(
riegeli::Reader& contents, absl::string_view assertion_uri,
const BmffBasedHashAssertion& assertion, bool requires_c2pa,
StatusTracker& tracker) const {
BmffHashHardBindingValidator().Validate(contents, assertion, assertion_uri,
tracker);
return absl::OkStatus();
}
absl::Status ContentBindingValidator::ValidateMultiAssetHash(
riegeli::Reader& contents, absl::string_view assertion_uri,
const MultiAssetHashAssertion& assertion,
std::vector<ContentBindingValidator::Structure> asset_structure,
bool requires_c2pa, AssertionFetcherRef assertion_fetcher,
TwoStageStatusTracker& tracker,
bool assertion_in_ingredient_manifest) const {
bool recorded_multi_asset_failure = false;
int64_t max_offset_validated = 0;
if (!contents.SupportsSize() || !contents.Size().has_value()) {
return absl::InvalidArgumentError(
"multi-asset hash requires an asset with a known size");
}
uint64_t asset_size = *contents.Size();
// multi-asset hash should drop all `c2pa.after` boxes
for (auto& structure : asset_structure) {
if (!structure.boxes.empty()) {
if (structure.boxes.back().identifier == "c2pa.after") {
structure.boxes.pop_back();
}
}
}
ABSL_RETURN_IF_ERROR(
ValidateParts(assertion, assertion_uri, asset_size, tracker));
if (tracker.HasFailuresInStage1()) {
// No need to validate if we already have failures.
return absl::OkStatus();
}
TwoStageStatusTracker multi_asset_tracker(
*static_cast<StatusTracker*>(&tracker));
for (int part_index = 0; part_index < assertion.parts_size(); ++part_index) {
const auto& part = assertion.parts(part_index);
ABSL_ASSIGN_OR_RETURN(auto locator, GetLocator(part.location()));
std::optional<int64_t> offset = std::nullopt;
std::optional<int64_t> length = std::nullopt;
if (std::holds_alternative<OffsetLocator>(locator)) {
OffsetLocator location = std::get<OffsetLocator>(locator);
offset = location.offset;
length = location.length;
} else {
BmffLocator location = std::get<BmffLocator>(locator);
if (!contents.Seek(0) || contents.pos() != 0) {
return contents.StatusOrAnnotate(
absl::InternalError("failed to seek to start of asset"));
}
ABSL_ASSIGN_OR_RETURN(bool is_supported,
BmffAssessor().IsSupported(contents));
if (!is_supported) {
return absl::InvalidArgumentError(
"Uses a BMFF locator, but file is not a BMFF file");
}
ABSL_RETURN_IF_ERROR(IterateOverBmffBoxes(
contents, [&location, &offset, &length](const BmffBoxHeader& header) {
if (header.xpath == location.path) {
offset = header.start;
length = header.box_size;
return false; // Terminate loop
}
return true; // Continue
}));
if (!offset.has_value()) {
// Did not find the location
multi_asset_tracker.RecordFailure(
FailureStatusCode::kAssertionMultiAssetHashMalformed,
{.url = assertion_uri});
recorded_multi_asset_failure = true;
}
}
if (!offset.has_value() || !length.has_value()) {
multi_asset_tracker.RecordFailure(
FailureStatusCode::kAssertionMultiAssetHashMissingPart,
{.url = assertion_uri,
.explanation =
absl::Substitute("missing location for part $0 of $1",
part_index + 1, assertion.parts_size())});
recorded_multi_asset_failure = true;
if (part.is_optional()) {
multi_asset_tracker.MoveStage1ToStage2();
} else {
multi_asset_tracker.WriteStage1();
}
requires_c2pa = false;
continue;
}
if (!contents.Seek(*offset) || contents.pos() != *offset ||
(*offset + *length) > asset_size) {
multi_asset_tracker.RecordFailure(
FailureStatusCode::kAssertionMultiAssetHashMissingPart,
{.url = assertion_uri,
.explanation =
absl::Substitute("missing data for part $0 of $1",
part_index + 1, assertion.parts_size())});
if (part.is_optional()) {
multi_asset_tracker.MoveStage1ToStage2();
} else {
multi_asset_tracker.WriteStage1();
}
requires_c2pa = false;
continue;
}
ABSL_ASSIGN_OR_RETURN(absl::string_view manifest_label,
GetManifestLabelFromAbsoluteUri(assertion_uri));
ABSL_ASSIGN_OR_RETURN(std::string part_hash_assertion_path,
jumbf::UriResolver::GetAbsolutePathFromUri(
part.hash_assertion().url(),
absl::StrCat("/c2pa/", manifest_label)));
std::string part_hash_assertion_uri =
absl::StrCat("self#jumbf=", part_hash_assertion_path);
std::vector<ContentBindingValidator::Structure> part_structure =
(asset_structure.size() - 1) < part_index
? std::vector<ContentBindingValidator::Structure>{}
: std::vector<ContentBindingValidator::Structure>(
asset_structure.begin() + part_index, asset_structure.end());
ABSL_RETURN_IF_ERROR(
ValidateAssertion(contents, assertion_fetcher, part_hash_assertion_uri,
part_structure, requires_c2pa, multi_asset_tracker,
assertion_in_ingredient_manifest, *offset + *length));
if (part.is_optional()) {
if (multi_asset_tracker.HasFailuresInStage1()) {
// Optional, but failed, move to stage 2.
multi_asset_tracker.MoveStage1ToStage2();
} else {
// Optional, but succeeded, write.
multi_asset_tracker.WriteStage1();
max_offset_validated =
std::max(max_offset_validated, *offset + *length);
}
} else {
if (multi_asset_tracker.HasFailuresInStage1()) {
// Encountered a failure, but not optional, write all status codes.
multi_asset_tracker.WriteStage2();
}
multi_asset_tracker.WriteStage1();
max_offset_validated = std::max(max_offset_validated, *offset + *length);
}
// We only need to ensure C2PA exists on the first part.
requires_c2pa = false;
}
if (!recorded_multi_asset_failure && max_offset_validated != asset_size) {
// Less than the whole file was validated, if the whole file was read it's
// a mismatch, otherwise it's malformed.
multi_asset_tracker.RecordFailure(
contents.pos() == asset_size
? FailureStatusCode::kAssertionMultiAssetHashMismatch
: FailureStatusCode::kAssertionMultiAssetHashMalformed,
{.url = assertion_uri});
recorded_multi_asset_failure = true;
}
// Write all status codes to the tracker.
multi_asset_tracker.WriteStage1();
if (tracker.HasFailuresInStage1()) {
// Failures were added during validation, write all stage two codes.
multi_asset_tracker.WriteStage2();
if (!recorded_multi_asset_failure) {
tracker.RecordFailure(FailureStatusCode::kAssertionMultiAssetHashMismatch,
{.url = assertion_uri});
}
} else {
tracker.RecordSuccess(SuccessStatusCode::kAssertionMultiAssetHashMatch,
{.url = assertion_uri});
}
return absl::OkStatus();
}
} // namespace credentio