blob: 2f64d6469cb700f6113019cdaf0e1a3841c6f112 [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/collection_data_hash_hard_binding_validator.h"
#include <cstdint>
#include <map>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "absl/base/nullability.h"
#include "absl/log/die_if_null.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "absl/status/status_macros.h"
#include "absl/status/statusor.h"
#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "bindings/binding_hasher.h"
#include "bindings/input_hasher.h"
#include "constants/status_codes.h"
#include "crypto/algorithms.h"
#include "formats/asset_byte_info.h"
#include "formats/zip/constants.h"
#include "formats/zip/reader.h"
#include "proto/assertion.pb.h"
#include "proto/collection_data_hash_assertion.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 CollectionDataHashBindingHasherTracker : public BindingHasherTracker {
public:
explicit CollectionDataHashBindingHasherTracker(
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::kAssertionCollectionHashMismatch,
{.url = hard_binding_uri_});
}
void RecordMalformed() override {
tracker_.RecordFailure(FailureStatusCode::kAssertionCollectionHashMalformed,
{.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()});
}
void RecordInvalidUri() {
tracker_.RecordFailure(
FailureStatusCode::kAssertionCollectionHashInvalidUri,
{.url = hard_binding_uri_});
}
void RecordIncorrectFileCount() {
tracker_.RecordFailure(
FailureStatusCode::kAssertionCollectionHashIncorrectFileCount,
{.url = hard_binding_uri_});
}
private:
StatusTracker& tracker_;
absl::string_view hard_binding_uri_;
};
bool HasRelativePathComponents(absl::string_view uri_str) {
return uri_str == "." || uri_str == ".." || absl::StartsWith(uri_str, "./") ||
absl::StartsWith(uri_str, "../") ||
absl::StrContains(uri_str, "/./") ||
absl::StrContains(uri_str, "/../") || absl::EndsWith(uri_str, "/.") ||
absl::EndsWith(uri_str, "/..");
}
} // namespace
absl::StatusOr<std::unique_ptr<ValidationResultProto>>
CollectionDataHashHardBindingValidator::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_collection_data_hash()) {
return absl::InvalidArgumentError(
absl::StrCat("assertion is not a collection data hash assertion: ",
partial_validation_result->hard_binding_uri()));
}
ABSL_ASSIGN_OR_RETURN(auto tracker,
DualStatusTracker::FromPartialValidationResult(
partial_validation_result.get()));
Validate(contents, assertion->collection_data_hash(),
partial_validation_result->hard_binding_uri(), *tracker);
return MakeFullValidationResult(std::move(partial_validation_result));
}
void CollectionDataHashHardBindingValidator::Validate(
riegeli::Reader& contents, const CollectionDataHashAssertion& assertion,
absl::string_view hard_binding_uri, StatusTracker& tracker) const {
if (!contents.SupportsSize() || !contents.Size().has_value()) {
tracker.RecordFailure(
FailureStatusCode::kGeneralError,
{.url = hard_binding_uri,
.explanation = "Failed to get the size of the file"});
return;
}
uint64_t file_size = *contents.Size();
CollectionDataHashBindingHasherTracker hasher_tracker(&tracker,
hard_binding_uri);
if (assertion.uris().empty()) {
hasher_tracker.RecordMalformed();
return;
}
auto alg = ParseHashAlgorithm(assertion.alg());
if (!alg.ok()) {
hasher_tracker.RecordAlgorithmUnsupported();
return;
}
auto zip_reader = ZipReader::Create(&contents);
if (!zip_reader.ok()) {
LOG(ERROR) << "Failed to open the file: " << zip_reader.status();
hasher_tracker.RecordGeneralError(
absl::InternalError("Failed to open the file"));
return;
}
std::vector<ZipReader::FileEntry> file_entries;
while ((*zip_reader)->HasNext()) {
auto entry = (*zip_reader)->Next();
if (!entry.ok()) {
hasher_tracker.RecordGeneralError(
absl::InternalError("Failed to read ZIP entry"));
return;
}
file_entries.push_back(*entry);
}
std::map<std::string, const UriHashedData*> expected_uris;
for (const auto& uri_data : assertion.uris()) {
if (uri_data.uri().empty() || uri_data.hash().empty()) {
hasher_tracker.RecordMalformed();
return;
}
// Validate that the URI has no relative path components (i.e. no "." or
// "..")
if (HasRelativePathComponents(uri_data.uri())) {
hasher_tracker.RecordInvalidUri();
return;
}
expected_uris[std::string(uri_data.uri())] = &uri_data;
}
// Validate URI hashes
const ZipReader::FileEntry* manifest_file_entry = nullptr;
for (const auto& entry : file_entries) {
if (entry.file_name == kZipManifestFileName) {
manifest_file_entry = &entry;
continue;
}
auto it = expected_uris.find(entry.file_name);
if (it == expected_uris.end()) {
hasher_tracker.RecordIncorrectFileCount();
return;
}
const auto& expected = *it->second;
auto file_hasher = InputHasher::Create(*alg);
if (!file_hasher.ok()) {
hasher_tracker.RecordGeneralError(file_hasher.status());
return;
}
uint32_t entry_start = entry.local_file_header_offset;
uint32_t entry_end = entry.file_range.offset + entry.file_range.length;
if (entry.has_data_descriptor) {
if (!contents.Seek(entry_end)) {
hasher_tracker.RecordGeneralError(
absl::InternalError("Failed to seek to data descriptor"));
return;
}
std::string sig;
if (!contents.Read(4, sig)) {
hasher_tracker.RecordGeneralError(
absl::InternalError("Failed to read data descriptor signature"));
return;
}
if (sig == kZipDataDescriptorSignature) {
entry_end += kDataDescriptorWithSignatureSize;
} else {
entry_end += kDataDescriptorSize;
}
}
auto update_status =
(*file_hasher)->Update(contents, entry_start, entry_end - entry_start);
if (!update_status.ok()) {
hasher_tracker.RecordGeneralError(update_status);
return;
}
std::string file_hash = (*file_hasher)->Digest();
if (expected.hash() != file_hash) {
hasher_tracker.RecordMismatch();
return;
}
expected_uris.erase(it);
}
if (!expected_uris.empty()) {
hasher_tracker.RecordIncorrectFileCount();
return;
}
// Validate ZIP central directory hash
if (manifest_file_entry == nullptr) {
hasher_tracker.RecordGeneralError(
absl::InternalError("Manifest file entry not found"));
return;
}
auto cd_hasher = InputHasher::Create(*alg);
if (!cd_hasher.ok()) {
hasher_tracker.RecordGeneralError(cd_hasher.status());
return;
}
uint32_t cd_offset = (*zip_reader)->central_directory_offset();
uint32_t manifest_crc32_offset =
manifest_file_entry->central_directory_header_offset + 16;
auto update_status =
(*cd_hasher)
->Update(contents, cd_offset, manifest_crc32_offset - cd_offset);
if (!update_status.ok()) {
hasher_tracker.RecordGeneralError(update_status);
return;
}
update_status = (*cd_hasher)
->Update(contents, manifest_crc32_offset + 4,
file_size - (manifest_crc32_offset + 4));
if (!update_status.ok()) {
hasher_tracker.RecordGeneralError(update_status);
return;
}
std::string cd_hash = (*cd_hasher)->Digest();
if (assertion.zip_central_directory_hash() != cd_hash) {
hasher_tracker.RecordMismatch();
return;
}
tracker.RecordSuccess(SuccessStatusCode::kAssertionCollectionHashMatch,
{.url = hard_binding_uri});
}
} // namespace credentio