blob: cd4fcb685e2800942709987d9539fccd568126f5 [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/data_hash_hard_binding_validator.h"
#include <cstddef>
#include <memory>
#include <optional>
#include <string>
#include <type_traits>
#include <utility>
#include "absl/status/status.h"
#include "absl/status/status_matchers.h"
#include "absl/status/statusor.h"
#include "absl/strings/escaping.h"
#include "absl/strings/string_view.h"
#include "formats/asset_byte_info.h"
#include "gmock/gmock.h"
#include "google/protobuf/util/message_differencer.h"
#include "gtest/gtest.h"
#include "proto/assertion.pb.h"
#include "proto/bmff_based_hash_assertion.pb.h"
#include "proto/boxes_hash_assertion.pb.h"
#include "proto/data_hash_assertion.pb.h"
#include "proto/ingredient_assertion.pb.h"
#include "proto/manifest.pb.h"
#include "proto/validation_result.pb.h"
#include "proto/validation_status.pb.h"
#include "riegeli/base/object.h"
#include "riegeli/base/types.h"
#include "riegeli/bytes/reader.h"
#include "riegeli/bytes/string_reader.h"
#include "testing/proto_test_utils.h"
#include "utils/dual_status_tracker.h"
#include "utils/status_tracker.h"
namespace credentio {
namespace {
using ::absl_testing::IsOkAndHolds;
using ::absl_testing::StatusIs;
using ::credentio_testing::EqualsProto;
using ::credentio_testing::ParseTextProtoOrDie;
using ::testing::HasSubstr;
using ::testing::Pointee;
using ::testing::TestWithParam;
// Matches a `ValidationResultProto` against `expected` (a proto or a text
// proto), ignoring the assertions of all manifests.
MATCHER_P(EqualsProtoIgnoringAssertions, expected, "") {
using Proto = std::decay_t<decltype(arg)>;
Proto expected_proto;
if constexpr (std::is_convertible_v<decltype(expected), absl::string_view>) {
expected_proto = ParseTextProtoOrDie(expected);
} else {
expected_proto = expected;
}
google::protobuf::util::MessageDifferencer differencer;
differencer.IgnoreField(
Manifest::descriptor()->FindFieldByName("assertions"));
std::string diff;
differencer.ReportDifferencesToString(&diff);
if (!differencer.Compare(expected_proto, arg)) {
*result_listener << "\nDifference:\n" << diff;
return false;
}
return true;
}
// A custom reader that supports size but fails on read.
class FailingReader : public riegeli::Reader {
public:
explicit FailingReader(riegeli::Closed) : Reader(riegeli::kClosed) {}
FailingReader() = default;
bool PullSlow(size_t min_length, size_t recommended_length) override {
return Fail(absl::InternalError("Simulated read error"));
}
bool SupportsSize() override { return true; }
std::optional<riegeli::Position> SizeImpl() override { return 10; }
};
struct DataHashValidatorTestCase {
std::string test_name;
std::string contents;
DataHashAssertion data_hash_assertion;
std::optional<std::string> hash;
AssetByteInfo asset_byte_info;
ValidationStatusSet expected_validation_result;
};
using DataHashValidatorTest = TestWithParam<DataHashValidatorTestCase>;
// ------------------------------------------------
TEST(DataHashHardBindingValidatorTest, MissingAssertion) {
auto input = riegeli::StringReader<std::string>("");
auto partial_result = std::make_unique<
PartialValidationResultProto>(ParseTextProtoOrDie(R"pb(
active_manifest: {
label: "urn:uuid:1234"
assertions: {
label: "another_data_hash_assertion"
data_hash: { alg: "sha256" hash: "\x11\x22\x33" pad: "\x00\x00" }
}
}
hard_binding_uri: "self#jumbf=/c2pa/urn:uuid:1234/c2pa.assertions/data_hash_assertion"
)pb"));
EXPECT_THAT(DataHashHardBindingValidator().Validate(
input, AssetByteInfo(), std::move(partial_result)),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("missing assertion")));
}
TEST(DataHashHardBindingValidatorTest, NotDataHashAssertion) {
auto input = riegeli::StringReader<std::string>("");
auto partial_result = std::make_unique<
PartialValidationResultProto>(ParseTextProtoOrDie(R"pb(
active_manifest: {
label: "urn:uuid:1234"
assertions: {
label: "bmff_based_hash_assertion"
bmff_based_hash: {
alg: "some alg",
name: "Example `c2pa.hash.bmff.v2` assertion"
hash: "\x11\x22\x33'"
}
}
}
hard_binding_uri: "self#jumbf=/c2pa/urn:uuid:1234/c2pa.assertions/bmff_based_hash_assertion"
)pb"));
EXPECT_THAT(DataHashHardBindingValidator().Validate(
input, AssetByteInfo(), std::move(partial_result)),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("not a data hash assertion")));
}
TEST(DataHashHardBindingValidatorTest, InvalidAlgorithm) {
auto input = riegeli::StringReader<std::string>("");
auto partial_validation_result = std::make_unique<
PartialValidationResultProto>(ParseTextProtoOrDie(R"pb(
active_manifest {
label: "manifest_label"
assertions {
label: "data_hash_assertion"
data_hash { hash: "\x11\x22\x33" alg: "some algo" }
}
}
hard_binding_uri: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
)pb"));
EXPECT_THAT(
DataHashHardBindingValidator().Validate(
input, AssetByteInfo(), std::move(partial_validation_result)),
IsOkAndHolds(Pointee(EqualsProtoIgnoringAssertions(R"pb(
active_manifest {
label: "manifest_label"
validation {
failures {
code: "algorithm.unsupported"
url: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
}
}
}
)pb"))));
}
TEST(DataHashHardBindingValidatorTest, ClosedReaderRecordsGeneralError) {
auto input = riegeli::StringReader<std::string>("");
EXPECT_TRUE(input.Close());
auto partial_validation_result = std::make_unique<
PartialValidationResultProto>(ParseTextProtoOrDie(R"pb(
active_manifest {
label: "manifest_label"
assertions {
label: "data_hash_assertion"
data_hash { hash: "123" alg: "sha256" }
}
}
hard_binding_uri: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
)pb"));
EXPECT_THAT(
DataHashHardBindingValidator().Validate(
input, AssetByteInfo(), std::move(partial_validation_result)),
IsOkAndHolds(Pointee(EqualsProtoIgnoringAssertions(R"pb(
active_manifest {
label: "manifest_label"
validation {
failures {
code: "com.google.internal.error"
url: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
explanation: "Asset size cannot be determined"
}
}
}
)pb"))));
}
TEST(DataHashHardBindingValidatorTest, FailingReaderRecordsGeneralError) {
FailingReader input;
auto partial_validation_result = std::make_unique<
PartialValidationResultProto>(ParseTextProtoOrDie(R"pb(
active_manifest {
label: "manifest_label"
assertions {
label: "data_hash_assertion"
data_hash { hash: "123" alg: "sha256" }
}
}
hard_binding_uri: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
)pb"));
EXPECT_THAT(
DataHashHardBindingValidator().Validate(
input, AssetByteInfo(), std::move(partial_validation_result)),
IsOkAndHolds(Pointee(EqualsProtoIgnoringAssertions(R"pb(
active_manifest {
label: "manifest_label"
validation {
failures {
code: "general.error"
url: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
explanation: "Simulated read error; at byte 0"
}
}
}
)pb"))));
}
TEST(DataHashHardBindingValidatorTest, NoManifestStoreLocation) {
auto input =
riegeli::StringReader<std::string>("adding_some_data_to_the_input");
auto partial_validation_result = std::make_unique<
PartialValidationResultProto>(ParseTextProtoOrDie(R"pb(
active_manifest {
label: "manifest_label"
assertions {
label: "data_hash_assertion"
data_hash {
hash: "\x11\x22\x33"
alg: "sha256"
exclusions: { start: 1, length: 4 },
}
}
}
hard_binding_uri: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
)pb"));
absl::StatusOr<std::unique_ptr<StatusTracker>> tracker =
DualStatusTracker::FromPartialValidationResult(
partial_validation_result.get());
ABSL_ASSERT_OK(tracker);
DataHashHardBindingValidator().Validate(
input, std::nullopt,
partial_validation_result->active_manifest().assertions(0).data_hash(),
partial_validation_result->hard_binding_uri(), **tracker,
/*assertion_in_ingredient_manifest=*/false);
EXPECT_THAT(
partial_validation_result->active_manifest().validation(),
EqualsProto(R"pb(
failures {
code: "assertion.dataHash.mismatch"
url: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
}
)pb"));
}
TEST(DataHashHardBindingValidatorTest, TestTruncatedWindow) {
auto input = riegeli::StringReader<std::string>("---ab___cd----------");
auto partial_validation_result = std::make_unique<
PartialValidationResultProto>(ParseTextProtoOrDie(R"pb(
active_manifest {
label: "manifest_label"
assertions {
label: "data_hash_assertion"
data_hash {
hash: "\x11\x22\x33"
alg: "sha256"
exclusions: { start: 2, length: 3 },
}
}
}
hard_binding_uri: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
)pb"));
absl::Base64Unescape("iNQmb9TmM40TuEX88olXnSCciXgjuSF9o+Fhk28DFYk=",
partial_validation_result->mutable_active_manifest()
->mutable_assertions(0)
->mutable_data_hash()
->mutable_hash());
absl::Base64Unescape("0000",
partial_validation_result->mutable_active_manifest()
->mutable_assertions(0)
->mutable_data_hash()
->mutable_pad());
absl::StatusOr<std::unique_ptr<StatusTracker>> tracker =
DualStatusTracker::FromPartialValidationResult(
partial_validation_result.get());
ABSL_ASSERT_OK(tracker);
DataHashHardBindingValidator().Validate(
input, std::nullopt,
partial_validation_result->active_manifest().assertions(0).data_hash(),
partial_validation_result->hard_binding_uri(), **tracker,
/*assertion_in_ingredient_manifest=*/false, /*start_offset=*/3,
/*end_offset=*/10);
EXPECT_THAT(
partial_validation_result->active_manifest().validation(),
EqualsProto(R"pb(
successes {
code: "assertion.dataHash.match"
url: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
}
)pb"));
}
TEST(DataHashHardBindingValidatorTest, NegativeOffsetAdjustment) {
auto input =
riegeli::StringReader<std::string>("012345678901234567890123456789");
auto partial_validation_result = std::make_unique<
PartialValidationResultProto>(ParseTextProtoOrDie(R"pb(
active_manifest {
label: "manifest_label"
assertions {
label: "data_hash_assertion"
data_hash {
hash: "\x11\x22\x33"
alg: "sha256"
exclusions: { start: 10,
length: 10 } # This will set offset_adjustment
exclusions: { start: 1, length: 1 } # This should trigger the failure
}
}
}
hard_binding_uri: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
)pb"));
absl::StatusOr<std::unique_ptr<StatusTracker>> tracker =
DualStatusTracker::FromPartialValidationResult(
partial_validation_result.get());
ABSL_ASSERT_OK(tracker);
DataHashHardBindingValidator().Validate(
input, /*manifest_store_location=*/{{.offset = 20, .length = 5}},
partial_validation_result->active_manifest().assertions(0).data_hash(),
partial_validation_result->hard_binding_uri(), **tracker,
/*assertion_in_ingredient_manifest=*/true, /*start_offset=*/10,
/*end_offset=*/30);
EXPECT_THAT(
partial_validation_result->active_manifest().validation(),
EqualsProto(R"pb(
failures {
code: "assertion.dataHash.malformed"
url: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
}
)pb"));
}
TEST_P(DataHashValidatorTest, ValidateBindingHashData) {
const DataHashValidatorTestCase& test_case = GetParam();
auto input = riegeli::StringReader<>(test_case.contents);
auto partial_validation_result = std::make_unique<
PartialValidationResultProto>(ParseTextProtoOrDie(R"pb(
active_manifest {
label: "manifest_label"
assertions { label: "data_hash_assertion" }
}
hard_binding_uri: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
)pb"));
partial_validation_result->mutable_active_manifest()
->mutable_assertions(0)
->mutable_data_hash()
->MergeFrom(test_case.data_hash_assertion);
if (test_case.hash.has_value()) {
absl::Base64Unescape(*test_case.hash,
partial_validation_result->mutable_active_manifest()
->mutable_assertions(0)
->mutable_data_hash()
->mutable_hash());
absl::Base64Unescape("0000",
partial_validation_result->mutable_active_manifest()
->mutable_assertions(0)
->mutable_data_hash()
->mutable_pad());
}
AssetByteInfo asset_byte_info = test_case.asset_byte_info;
ValidationResultProto expected_validation_result = ParseTextProtoOrDie(R"pb(
active_manifest {
label: "manifest_label"
validation {}
}
)pb");
*expected_validation_result.mutable_active_manifest()->mutable_validation() =
test_case.expected_validation_result;
EXPECT_THAT(DataHashHardBindingValidator().Validate(
input, asset_byte_info, std::move(partial_validation_result)),
IsOkAndHolds(Pointee(
EqualsProtoIgnoringAssertions(expected_validation_result))));
}
INSTANTIATE_TEST_SUITE_P(
DataHashHardBindingDataHashValidatorTest, DataHashValidatorTest,
testing::ValuesIn<DataHashValidatorTestCase>({
{
.test_name = "Sha256",
.contents = "abcd",
.data_hash_assertion = ParseTextProtoOrDie(R"pb(
hash: "\x11\x22\x33"
alg: "sha256"
)pb"),
.hash = "iNQmb9TmM40TuEX88olXnSCciXgjuSF9o+Fhk28DFYk=",
.asset_byte_info = AssetByteInfo(),
.expected_validation_result = ParseTextProtoOrDie(R"pb(
successes {
code: "assertion.dataHash.match"
url: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
}
)pb"),
},
{
.test_name = "Sha384",
.contents = "abcd",
.data_hash_assertion = ParseTextProtoOrDie(R"pb(
hash: "\x11\x22\x33"
alg: "sha384"
)pb"),
.hash = "EWWzQG/"
"wtSo9JHIfeFRiyiJ2yfRUoRbCsrogFxp5BepaAmaC62WcTV8RXDY6o8eb",
.asset_byte_info = AssetByteInfo(),
.expected_validation_result = ParseTextProtoOrDie(R"pb(
successes {
code: "assertion.dataHash.match"
url: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
}
)pb"),
},
{
.test_name = "Sha512",
.contents = "abcd",
.data_hash_assertion = ParseTextProtoOrDie(R"pb(
hash: "\x11\x22\x33"
alg: "sha512"
)pb"),
.hash = "2AIvIGCtbv0perc9zFNVybIUBUsNF3ahNqZp0mp9OxT3OqDQ6/"
"8Z7jMzaPAWS2QZqW2knj5IF1Pn6Wtxa9zLbw==",
.asset_byte_info = AssetByteInfo(),
.expected_validation_result = ParseTextProtoOrDie(R"pb(
successes {
code: "assertion.dataHash.match"
url: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
}
)pb"),
},
{
.test_name = "NoExclusionRange",
.contents = "",
.data_hash_assertion = ParseTextProtoOrDie(R"pb(
hash: "\x11\x22\x33"
alg: "sha256"
pad: "\x00\x00"
)pb"),
.hash = std::nullopt,
.asset_byte_info = AssetByteInfo(),
.expected_validation_result = ParseTextProtoOrDie(R"pb(
failures {
code: "assertion.dataHash.mismatch"
url: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
}
)pb"),
},
{
.test_name = "ExcludesManifestStore",
.contents = "a___bcd",
.data_hash_assertion = ParseTextProtoOrDie(R"pb(
hash: "\x11\x22\x33",
alg: "sha256",
exclusions: { start: 1, length: 3 },
)pb"),
.hash = "iNQmb9TmM40TuEX88olXnSCciXgjuSF9o+Fhk28DFYk=",
.asset_byte_info = {.manifest_store_location = {.offset = 1,
.length = 3}},
.expected_validation_result = ParseTextProtoOrDie(R"pb(
successes {
code: "assertion.dataHash.match"
url: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
}
)pb"),
},
{
.test_name = "ExcludesManifestStoreAndAdditionalExclusions",
.contents = "a-b______c-d",
.data_hash_assertion = ParseTextProtoOrDie(R"pb(
hash: "\x11\x22\x33",
alg: "sha256",
pad: "\x00\x00",
exclusions: { start: 1, length: 1 }
exclusions: { start: 3, length: 6 }
exclusions: { start: 10, length: 1 }
)pb"),
.hash = "iNQmb9TmM40TuEX88olXnSCciXgjuSF9o+Fhk28DFYk=",
.asset_byte_info = {.manifest_store_location = {.offset = 3,
.length = 6}},
.expected_validation_result = ParseTextProtoOrDie(R"pb(
successes {
code: "assertion.dataHash.match"
url: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
}
informationals {
code: "assertion.dataHash.additionalExclusionsPresent"
url: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
}
)pb"),
},
{
.test_name = "NegativeStartOffset",
.contents = "ab______cd",
.data_hash_assertion = ParseTextProtoOrDie(R"pb(
hash: "\x11\x22\x33",
alg: "sha256",
pad: "\x00\x00",
exclusions: { start: -1, length: 10 }
)pb"),
.hash = std::nullopt,
.asset_byte_info = {.manifest_store_location = {.offset = 2,
.length = 6}},
.expected_validation_result = ParseTextProtoOrDie(R"pb(
failures {
code: "assertion.dataHash.malformed"
url: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
}
)pb"),
},
{
.test_name = "NegativeLength",
.contents = "ab______cd",
.data_hash_assertion = ParseTextProtoOrDie(R"pb(
hash: "\x11\x22\x33",
alg: "sha256",
pad: "\x00\x00",
exclusions: { start: 1, length: -10 }
)pb"),
.hash = std::nullopt,
.asset_byte_info = {.manifest_store_location = {.offset = 2,
.length = 6}},
.expected_validation_result = ParseTextProtoOrDie(R"pb(
failures {
code: "assertion.dataHash.malformed"
url: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
}
)pb"),
},
{
.test_name = "EndsPastEndOfAsset",
.contents = "ab______cd",
.data_hash_assertion = ParseTextProtoOrDie(R"pb(
hash: "\x11\x22\x33",
alg: "sha256",
pad: "\x00\x00",
exclusions: { start: 2, length: 6 }
exclusions: { start: 9, length: 5 }
)pb"),
.hash = std::nullopt,
.asset_byte_info = {.manifest_store_location = {.offset = 2,
.length = 6}},
.expected_validation_result = ParseTextProtoOrDie(R"pb(
failures {
code: "assertion.dataHash.mismatch"
url: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
}
)pb"),
},
{
.test_name = "OverlappingExclusions",
.contents = "ab______cd",
.data_hash_assertion = ParseTextProtoOrDie(R"pb(
hash: "\x11\x22\x33",
alg: "sha256",
pad: "\x00\x00",
exclusions: { start: 1, length: 4 }
exclusions: { start: 2, length: 6 }
)pb"),
.hash = std::nullopt,
.asset_byte_info = {.manifest_store_location = {.offset = 2,
.length = 6}},
.expected_validation_result = ParseTextProtoOrDie(R"pb(
failures {
code: "assertion.dataHash.malformed"
url: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
}
)pb"),
},
{
.test_name = "BackToBackExclusions",
.contents = "ab--______cd",
.data_hash_assertion = ParseTextProtoOrDie(R"pb(
hash: "\x11\x22\x33",
alg: "sha256",
pad: "\x00\x00",
exclusions: { start: 2, length: 1 }
exclusions: { start: 3, length: 1 }
exclusions: { start: 4, length: 6 }
)pb"),
.hash = "iNQmb9TmM40TuEX88olXnSCciXgjuSF9o+Fhk28DFYk=",
.asset_byte_info = {.manifest_store_location = {.offset = 4,
.length = 6}},
.expected_validation_result = ParseTextProtoOrDie(R"pb(
successes {
code: "assertion.dataHash.match"
url: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
}
informationals {
code: "assertion.dataHash.additionalExclusionsPresent"
url: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
}
)pb"),
},
{
.test_name = "ExclusionsNotInOrder",
.contents = "ab-c______d",
.data_hash_assertion = ParseTextProtoOrDie(R"pb(
hash: "\x11\x22\x33",
alg: "sha256",
pad: "\x00\x00",
exclusions: { start: 3, length: 1 }
exclusions: { start: 2, length: 1 }
exclusions: { start: 4, length: 6 }
)pb"),
.hash = "iNQmb9TmM40TuEX88olXnSCciXgjuSF9o+Fhk28DFYk=",
.asset_byte_info = {.manifest_store_location = {.offset = 4,
.length = 6}},
.expected_validation_result = ParseTextProtoOrDie(R"pb(
failures {
code: "assertion.dataHash.malformed"
url: "self#jumbf=/c2pa/manifest_label/c2pa.assertions/data_hash_assertion"
}
)pb"),
},
}),
[](const testing::TestParamInfo<DataHashValidatorTest::ParamType>& info) {
return info.param.test_name;
});
} // namespace
} // namespace credentio