blob: 9ec8efe23a1cde8057644ab9fffe61fb6b6d5f90 [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/merkle_validator.h"
#include <cstdint>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "absl/container/flat_hash_map.h"
#include "absl/status/status.h"
#include "absl/status/status_macros.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_format.h"
#include "absl/strings/string_view.h"
#include "bindings/input_hasher.h"
#include "bindings/merkle.h"
#include "cbor/cbor.h"
#include "cbor/parse.h"
#include "constants/labels.h"
#include "constants/status_codes.h"
#include "crypto/hash.h"
#include "formats/bmff/box_header.h"
#include "proto/bmff_based_hash_assertion.cbor.h"
#include "proto/bmff_based_hash_assertion.pb.h"
#include "riegeli/bytes/reader.h"
#include "utils/riegeli.h"
#include "utils/status_tracker.h"
namespace credentio {
namespace {
std::string GetMerkleKey(int64_t unique_id, int64_t local_id) {
return absl::StrFormat("%d-%d", unique_id, local_id);
}
absl::StatusOr<std::string> ReadPurpose(riegeli::Reader& contents) {
std::string purpose;
if (!ReadNullTerminatedString(contents, 20, purpose)) {
return contents.StatusOrAnnotate(absl::DataLossError("kUnexpectedEof"));
}
return purpose;
}
absl::StatusOr<BmffMerkleMap> ReadBmffMerkleMap(riegeli::Reader& contents,
int64_t length) {
constexpr int64_t kMaxMerkleMapSize = 10 * 1024 * 1024; // 10 MiB
if (length < 0 || length > kMaxMerkleMapSize) {
return absl::InvalidArgumentError(
absl::StrFormat("invalid length for merkle map: %d", length));
}
std::string aux_box_raw;
if (!contents.Read(length, aux_box_raw)) {
return absl::InternalError("failed to read auxiliary merkle map");
}
BmffMerkleMap aux_box;
ABSL_ASSIGN_OR_RETURN(auto aux_box_cbor, cbor::Parse(aux_box_raw));
ABSL_ASSIGN_OR_RETURN(auto aux_box_cbor_map, aux_box_cbor->AsMap());
ABSL_RETURN_IF_ERROR(cbor::ToProto(aux_box_cbor_map, &aux_box));
return aux_box;
}
absl::StatusOr<std::string> ComputeLeafHash(const HasherFactory& factory,
riegeli::Reader& contents,
int64_t offset, int64_t length) {
ABSL_ASSIGN_OR_RETURN(auto hasher, factory.Create());
ABSL_ASSIGN_OR_RETURN(auto input_hasher,
InputHasher::Create(std::move(hasher)));
ABSL_RETURN_IF_ERROR(input_hasher->Update(contents, offset, length));
return input_hasher->Digest();
}
absl::StatusOr<std::string> JoinHashes(const HasherFactory& factory,
absl::string_view hash1,
absl::string_view hash2) {
ABSL_ASSIGN_OR_RETURN(auto hasher, factory.Create());
hasher->Update(hash1);
hasher->Update(hash2);
return hasher->Digest();
}
struct LeafData {
std::vector<BmffBoxHeader> mdat_atoms;
absl::flat_hash_map<std::string, std::vector<BmffMerkleMap>> auxiliary_data;
};
absl::StatusOr<LeafData> ExtractLeafData(riegeli::Reader& contents) {
LeafData result;
if (!contents.Seek(0) || contents.pos() != 0) {
return absl::InternalError("failed to seek to start of file");
}
auto status = IterateOverBmffBoxes(
contents,
[&result,
&contents](const BmffBoxHeader& header) -> absl::StatusOr<bool> {
// Non-fragmented assets have 1 merkle tree per `mdat`
// Fragmented assets could have merkle tree per track
// https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_general_20
if (header.type == "mdat") {
// Non-fragmented assets require auxiliary data after the last mdat.
// https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_non_fragmented_asset_that_can_be_validated_piecewise
if (!result.auxiliary_data.empty()) {
return absl::InvalidArgumentError(
"encountered auxiliary atom before the first mdat atom");
}
result.mdat_atoms.push_back(header);
}
if (header.type == "uuid" &&
header.user_type == credentio::kC2paBmffBoxUuid) {
// Read next string to determine the purpose of the uuid atom.
ABSL_ASSIGN_OR_RETURN(auto purpose, ReadPurpose(contents));
if (purpose == "merkle") {
// Leverage the offset found within the manifest to find first aux
// 4 bytes are the version and flag data
// 1 byte is the null terminator of the purpose string
const uint64_t metadata_header_size = purpose.size() + 4 + 1;
if (header.box_size < header.header_size ||
header.box_size - header.header_size < metadata_header_size) {
return absl::InvalidArgumentError(
"box size too small for metadata headers");
}
int64_t remaining_data =
header.box_size - header.header_size - metadata_header_size;
ABSL_ASSIGN_OR_RETURN(auto aux_box,
ReadBmffMerkleMap(contents, remaining_data));
result
.auxiliary_data[GetMerkleKey(aux_box.unique_id(),
aux_box.local_id())]
.push_back(aux_box);
}
}
return true;
});
ABSL_RETURN_IF_ERROR(status);
return result;
}
} // namespace
absl::Status MerkleValidator::ValidateMerkleMap(
const BmffMerkle& merkle, const BmffBoxHeader& mdat_atom,
std::vector<BmffMerkleMap> auxiliary_merkle_maps,
absl::string_view fallback_algo, StatusTracker& tracker) const {
if (merkle.has_init_hash()) {
tracker.RecordFailure(
FailureStatusCode::kGoogleInternalError,
{.url = assertion_uri_,
.explanation = "fragmented merkle validations are not supported yet"});
return absl::UnimplementedError(
"fragmented merkle validations are not supported yet");
}
// Derive the full row sizes and number of rows away and validates the counts.
absl::StatusOr<DerivedTreeData> derived_data = DeriveMerkleTreeData(
merkle.count(), merkle.hashes_size(), auxiliary_merkle_maps.size());
if (!derived_data.ok()) {
tracker.RecordFailure(FailureStatusCode::kAssertionBmffHashMalformed,
{.url = assertion_uri_,
.explanation = derived_data.status().message()});
return derived_data.status();
}
if (mdat_atom.box_size < mdat_atom.header_size) {
tracker.RecordFailure(
FailureStatusCode::kAssertionBmffHashMalformed,
{.url = assertion_uri_,
.explanation = "mdat atom box_size is smaller than header_size"});
return absl::InvalidArgumentError(
"mdat atom box_size is smaller than header_size");
}
// Calculate and validate the lengths of each leaf.
absl::StatusOr<std::vector<int64_t>> leaf_sizes = DeriveMerkleBlockSizes(
merkle, mdat_atom.box_size - mdat_atom.header_size);
if (!leaf_sizes.ok()) {
tracker.RecordFailure(
FailureStatusCode::kAssertionBmffHashMalformed,
{.url = assertion_uri_, .explanation = leaf_sizes.status().message()});
return leaf_sizes.status();
}
// Based on the above calculations, we ensure the assertion is not malformed,
// now compute and compare the leaf hashes.
int64_t offset = mdat_atom.start + mdat_atom.header_size;
for (int64_t i = 0; i < merkle.count(); ++i) {
absl::string_view algo = merkle.has_alg() ? merkle.alg() : fallback_algo;
absl::StatusOr<std::unique_ptr<HasherFactory>> factory =
factory_provider_->Create(algo);
if (!factory.ok()) {
tracker.RecordFailure(FailureStatusCode::kAssertionBmffHashMalformed,
{.url = assertion_uri_,
.explanation = absl::StrFormat(
"unsupported hash algorithm: %s", algo)});
return absl::InvalidArgumentError(
absl::StrFormat("unsupported hash algorithm: %s", algo));
}
absl::StatusOr<std::string> leaf_hash =
ComputeLeafHash(**factory, contents_, offset, leaf_sizes.value()[i]);
offset += leaf_sizes.value()[i];
if (!leaf_hash.ok()) {
tracker.RecordFailure(
FailureStatusCode::kAssertionBmffHashMalformed,
{.url = assertion_uri_, .explanation = leaf_hash.status().message()});
return leaf_hash.status();
}
std::string computed_hash = std::move(leaf_hash.value());
if (!auxiliary_merkle_maps.empty()) {
int64_t running_row_index = i;
BmffMerkleMap auxiliary = auxiliary_merkle_maps[i];
for (const auto& hash : auxiliary.hashes()) {
if (running_row_index % 2 == 1) {
ABSL_ASSIGN_OR_RETURN(computed_hash,
JoinHashes(**factory, hash, computed_hash));
} else {
ABSL_ASSIGN_OR_RETURN(computed_hash,
JoinHashes(**factory, computed_hash, hash));
}
running_row_index >>= 1;
}
}
int64_t hashes_index = i >> derived_data->delta_rows;
if (merkle.hashes(hashes_index) != computed_hash) {
tracker.RecordFailure(
FailureStatusCode::kAssertionBmffHashMismatch,
{.url = assertion_uri_,
.explanation = absl::StrFormat(
"merkle map hash mismatch at index %d: Expected: %s, Actual: %s",
hashes_index, merkle.hashes(hashes_index), computed_hash)});
return absl::InternalError("merkle map hash mismatch");
}
}
return absl::OkStatus();
}
absl::Status MerkleValidator::Validate(StatusTracker& tracker) const {
absl::StatusOr<LeafData> atoms = ExtractLeafData(contents_);
if (!atoms.ok()) {
tracker.RecordFailure(
FailureStatusCode::kGoogleInternalError,
{.url = assertion_uri_, .explanation = atoms.status().message()});
return absl::InternalError("failed to extract leaf data");
}
if (atoms->mdat_atoms.size() != assertion_.merkles_size()) {
// This should only apply to non-fragmented assets.
tracker.RecordFailure(
FailureStatusCode::kAssertionBmffHashMalformed,
{.url = assertion_uri_,
.explanation =
"number of mdat atoms does not match the number of merkle maps"});
return absl::InternalError(
"number of mdat atoms does not match the number of merkle maps");
}
for (int64_t i = 0; i < atoms->mdat_atoms.size(); ++i) {
BmffMerkle merkle = assertion_.merkles(i);
const auto& aux_maps = atoms->auxiliary_data[GetMerkleKey(
merkle.unique_id(), merkle.local_id())];
ABSL_RETURN_IF_ERROR(ValidateMerkleMap(
merkle, atoms->mdat_atoms[i], aux_maps, assertion_.alg(), tracker));
}
return absl::OkStatus();
}
} // namespace credentio