blob: 5245668c433fb5a8b533a1314440a977c3580178 [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/boxes_hash_validator.h"
#include <string>
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "absl/strings/str_format.h"
#include "absl/strings/string_view.h"
#include "constants/status_codes.h"
#include "gmock/gmock.h"
#include "google/protobuf/util/message_differencer.h"
#include "gtest/gtest.h"
#include "proto/boxes_hash_assertion.pb.h"
#include "proto/validation_status.pb.h"
#include "testing/proto_test_utils.h"
#include "testing/test_status_tracker.h"
namespace credentio {
namespace {
using ::credentio_testing::ParseTextProtoOrDie;
struct ExpectedValues {
absl::Status status;
ValidationStatusSet validation;
};
// Matcher for ValidationStatusSet that ignores the `url` field.
MATCHER_P(EqualsValidationStatusSetIgnoringUrl, expected, "") {
google::protobuf::util::MessageDifferencer differencer;
differencer.IgnoreField(
ValidationStatus::descriptor()->FindFieldByName("url"));
return differencer.Compare(arg, expected);
}
struct BoxesHashValidatorTestParams {
std::string name;
BoxesHashAssertion assertion;
ExpectedValues expected;
};
ExpectedValues CreateStatusSet(bool has_additional_exclusions = false) {
ExpectedValues expected;
expected.status = absl::OkStatus();
if (has_additional_exclusions) {
expected.validation.add_informationals()->set_code(absl::StrFormat(
"%v", InformationalStatusCode::
kAssertionBoxesHashAdditionalExclusionsPresent));
}
return expected;
}
ExpectedValues CreateStatusSet(FailureStatusCode failure_code,
absl::string_view explanation,
bool has_additional_exclusions = false) {
ExpectedValues expected;
ValidationStatus* failure = expected.validation.add_failures();
failure->set_code(absl::StrFormat("%v", failure_code));
failure->set_explanation(explanation);
expected.status = absl::InvalidArgumentError(explanation);
if (has_additional_exclusions) {
expected.validation.add_informationals()->set_code(absl::StrFormat(
"%v", InformationalStatusCode::
kAssertionBoxesHashAdditionalExclusionsPresent));
}
return expected;
}
using BoxesHashValidatorTest =
testing::TestWithParam<BoxesHashValidatorTestParams>;
TEST_P(BoxesHashValidatorTest, Validate) {
TestStatusTracker tracker;
BoxesHashValidator validator(GetParam().assertion, "test_uri", "", tracker);
EXPECT_EQ(validator.Validate(), GetParam().expected.status);
EXPECT_THAT(tracker.GetStatusSet(), EqualsValidationStatusSetIgnoringUrl(
GetParam().expected.validation));
}
INSTANTIATE_TEST_SUITE_P(
BoxesHashValidatorTest, BoxesHashValidatorTest,
testing::ValuesIn<BoxesHashValidatorTestParams>({
{
.name = "empty_assertion",
.assertion = BoxesHashAssertion(),
.expected =
CreateStatusSet(FailureStatusCode::kAssertionBoxesHashMalformed,
"box-map.boxes is empty"),
},
{
.name = "empty_names",
.assertion = ParseTextProtoOrDie<BoxesHashAssertion>(R"pb(
boxes { hash: "hash" }
)pb"),
.expected =
CreateStatusSet(FailureStatusCode::kAssertionBoxesHashMalformed,
"box-map.names is empty"),
},
{
.name = "empty_hash",
.assertion = ParseTextProtoOrDie<BoxesHashAssertion>(R"pb(
boxes { names: "box1" }
)pb"),
.expected =
CreateStatusSet(FailureStatusCode::kAssertionBoxesHashMismatch,
"box-map.hash is empty"),
},
{
.name = "no_algo",
.assertion = ParseTextProtoOrDie<BoxesHashAssertion>(R"pb(
boxes { names: "box1" hash: "hash" }
)pb"),
.expected =
CreateStatusSet(FailureStatusCode::kAlgorithmUnsupported,
"Algorithm not supported: "),
},
{
.name = "bad_algo_box_map",
.assertion = ParseTextProtoOrDie<BoxesHashAssertion>(R"pb(
boxes { names: "box1" hash: "hash" alg: "bad_algo" }
)pb"),
.expected =
CreateStatusSet(FailureStatusCode::kAlgorithmUnsupported,
"Algorithm not supported: bad_algo"),
},
{
.name = "bad_algo_assertion",
.assertion = ParseTextProtoOrDie<BoxesHashAssertion>(R"pb(
boxes { names: "box1" hash: "hash" }
alg: "bad_algo"
)pb"),
.expected =
CreateStatusSet(FailureStatusCode::kAlgorithmUnsupported,
"Algorithm not supported: bad_algo"),
},
{
.name = "c2pa_not_only_box",
.assertion = ParseTextProtoOrDie<BoxesHashAssertion>(R"pb(
boxes { names: "box1" names: "C2PA" hash: "hash" }
alg: "sha256"
)pb"),
.expected = CreateStatusSet(
FailureStatusCode::kAssertionBoxesHashMalformed,
"C2PA box must be the only name in the names array"),
},
{
.name = "c2pa_not_zero_byte_hash",
.assertion = ParseTextProtoOrDie<BoxesHashAssertion>(R"pb(
boxes { names: "C2PA" hash: "hash" }
alg: "sha256"
)pb"),
.expected =
CreateStatusSet(FailureStatusCode::kAssertionBoxesHashMismatch,
"C2PA box hash is not a single 0 byte"),
},
{
.name = "only_c2pa_excluded",
.assertion = []() -> BoxesHashAssertion {
auto assertion = ParseTextProtoOrDie<BoxesHashAssertion>(R"pb(
boxes { names: "box1" hash: "hash" }
boxes { names: "C2PA" hash: "" }
alg: "sha256"
)pb");
assertion.mutable_boxes(1)->set_hash(std::string("\0", 1));
return assertion;
}(),
.expected = CreateStatusSet(),
},
{
.name = "box_1_excluded",
.assertion = []() -> BoxesHashAssertion {
auto assertion = ParseTextProtoOrDie<BoxesHashAssertion>(R"pb(
boxes { names: "box1" hash: "hash", excluded: true }
boxes { names: "C2PA" hash: "" }
alg: "sha256"
)pb");
assertion.mutable_boxes(1)->set_hash(std::string("\0", 1));
return assertion;
}(),
.expected = CreateStatusSet(/*has_additional_exclusions=*/true),
},
{
.name = "single_box_exclusion_ranges_out_of_order",
.assertion = []() -> BoxesHashAssertion {
auto assertion = ParseTextProtoOrDie<BoxesHashAssertion>(R"pb(
boxes {
names: "box1"
hash: "hash",
exclusions { start: 100 length: 100 }
exclusions { start: 10 length: 50 }
}
boxes { names: "C2PA" hash: "" }
alg: "sha256"
)pb");
assertion.mutable_boxes(1)->set_hash(std::string("\0", 1));
return assertion;
}(),
.expected = CreateStatusSet(
FailureStatusCode::kAssertionBoxesHashMalformed,
"box-map.start is overlapping with the last box-map.start"),
},
{
.name = "single_box_exclusion_ranges_overlapping",
.assertion = []() -> BoxesHashAssertion {
auto assertion = ParseTextProtoOrDie<BoxesHashAssertion>(R"pb(
boxes {
names: "box1"
hash: "hash",
exclusions { start: 100 length: 100 }
exclusions { start: 150 length: 100 }
}
boxes { names: "C2PA" hash: "" }
alg: "sha256"
)pb");
assertion.mutable_boxes(1)->set_hash(std::string("\0", 1));
return assertion;
}(),
.expected = CreateStatusSet(
FailureStatusCode::kAssertionBoxesHashMalformed,
"box-map.start is overlapping with the last box-map.start"),
},
{
.name = "two_boxes_exclusion_ranges_missing_box_index",
.assertion = []() -> BoxesHashAssertion {
auto assertion = ParseTextProtoOrDie<BoxesHashAssertion>(R"pb(
boxes {
names: "box1"
names: "box2"
hash: "hash",
exclusions { start: 100 length: 100 }
}
boxes { names: "C2PA" hash: "" }
alg: "sha256"
)pb");
assertion.mutable_boxes(1)->set_hash(std::string("\0", 1));
return assertion;
}(),
.expected = CreateStatusSet(
FailureStatusCode::kAssertionBoxesHashMalformed,
"box-map.boxIndex is required when there is more than one box"),
},
{
.name = "two_boxes_exclusion_ranges_out_of_range",
.assertion = []() -> BoxesHashAssertion {
auto assertion = ParseTextProtoOrDie<BoxesHashAssertion>(R"pb(
boxes {
names: "box1"
names: "box2"
hash: "hash",
exclusions { start: 100 length: 100 box_index: 2 }
}
boxes { names: "C2PA" hash: "" }
alg: "sha256"
)pb");
assertion.mutable_boxes(1)->set_hash(std::string("\0", 1));
return assertion;
}(),
.expected =
CreateStatusSet(FailureStatusCode::kAssertionBoxesHashMalformed,
"box-map.boxIndex is out of range"),
},
{
.name = "two_boxes_exclusion_ranges_negative_box_index",
.assertion = []() -> BoxesHashAssertion {
auto assertion = ParseTextProtoOrDie<BoxesHashAssertion>(R"pb(
boxes {
names: "box1"
names: "box2"
hash: "hash",
exclusions { start: 100 length: 100 box_index: -1 }
}
boxes { names: "C2PA" hash: "" }
alg: "sha256"
)pb");
assertion.mutable_boxes(1)->set_hash(std::string("\0", 1));
return assertion;
}(),
.expected =
CreateStatusSet(FailureStatusCode::kAssertionBoxesHashMalformed,
"box-map.boxIndex is out of range"),
},
{
.name = "two_boxes_exclusion_ranges_box_index_not_in_order",
.assertion = []() -> BoxesHashAssertion {
auto assertion = ParseTextProtoOrDie<BoxesHashAssertion>(R"pb(
boxes {
names: "box1"
names: "box2"
hash: "hash",
exclusions { start: 100 length: 100 box_index: 1 }
exclusions { start: 100 length: 100 box_index: 0 }
}
boxes { names: "C2PA" hash: "" }
alg: "sha256"
)pb");
assertion.mutable_boxes(1)->set_hash(std::string("\0", 1));
return assertion;
}(),
.expected =
CreateStatusSet(FailureStatusCode::kAssertionBoxesHashMalformed,
"box-map.boxIndex is not in order"),
},
{
.name = "two_boxes_exclusion_ranges_box_index_in_order",
.assertion = []() -> BoxesHashAssertion {
auto assertion = ParseTextProtoOrDie<BoxesHashAssertion>(R"pb(
boxes {
names: "box1"
names: "box2"
hash: "hash"
exclusions { start: 100 length: 100 box_index: 0 }
exclusions { start: 100 length: 100 box_index: 1 }
}
boxes { names: "C2PA" hash: "" }
alg: "sha256"
)pb");
assertion.mutable_boxes(1)->set_hash(std::string("\0", 1));
return assertion;
}(),
.expected = CreateStatusSet(/*has_additional_exclusions=*/true),
},
{
.name = "one_box_exclusion_ranges_valid",
.assertion = []() -> BoxesHashAssertion {
auto assertion = ParseTextProtoOrDie<BoxesHashAssertion>(R"pb(
boxes {
names: "box1"
hash: "hash",
exclusions { start: 10 length: 10 }
exclusions { start: 30 length: 10 }
}
boxes { names: "C2PA" hash: "" }
alg: "sha256"
)pb");
assertion.mutable_boxes(1)->set_hash(std::string("\0", 1));
return assertion;
}(),
.expected = CreateStatusSet(/*has_additional_exclusions=*/true),
},
{
.name = "negative_exclusion_start",
.assertion = []() -> BoxesHashAssertion {
auto assertion = ParseTextProtoOrDie<BoxesHashAssertion>(R"pb(
boxes {
names: "box1"
hash: "hash",
exclusions { start: -10 length: 10 }
}
boxes { names: "C2PA" hash: "" }
alg: "sha256"
)pb");
assertion.mutable_boxes(1)->set_hash(std::string("\0", 1));
return assertion;
}(),
.expected = CreateStatusSet(
FailureStatusCode::kAssertionBoxesHashMalformed,
"box-map.start and box-map.length must be non-negative"),
},
{
.name = "negative_exclusion_length",
.assertion = []() -> BoxesHashAssertion {
auto assertion = ParseTextProtoOrDie<BoxesHashAssertion>(R"pb(
boxes {
names: "box1"
hash: "hash",
exclusions { start: 10 length: -10 }
}
boxes { names: "C2PA" hash: "" }
alg: "sha256"
)pb");
assertion.mutable_boxes(1)->set_hash(std::string("\0", 1));
return assertion;
}(),
.expected = CreateStatusSet(
FailureStatusCode::kAssertionBoxesHashMalformed,
"box-map.start and box-map.length must be non-negative"),
},
{
.name = "exclusion_range_overflow",
.assertion = []() -> BoxesHashAssertion {
auto assertion = ParseTextProtoOrDie<BoxesHashAssertion>(R"pb(
boxes {
names: "box1"
hash: "hash",
exclusions { start: 9223372036854775800 length: 100 }
}
boxes { names: "C2PA" hash: "" }
alg: "sha256"
)pb");
assertion.mutable_boxes(1)->set_hash(std::string("\0", 1));
return assertion;
}(),
.expected =
CreateStatusSet(FailureStatusCode::kAssertionBoxesHashMalformed,
"box-map.start + box-map.length overflows"),
},
}),
[](const testing::TestParamInfo<BoxesHashValidatorTestParams>& info) {
return info.param.name;
});
} // namespace
} // namespace credentio