| // 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 |