blob: 667872b27f6b059a28647b8ededa1d6d596f5386 [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 "assertion/hashed_uri_validator.h"
#include <memory>
#include <optional>
#include "absl/log/check.h"
#include "absl/status/statusor.h"
#include "absl/strings/escaping.h"
#include "absl/strings/string_view.h"
#include "constants/status_codes.h"
#include "crypto/algorithms.h"
#include "crypto/hash.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "jumbf/box.h"
#include "jumbf/uri.h"
#include "proto/hashed_uri.pb.h"
#include "testing/jumbf_utils.h"
#include "testing/proto_test_utils.h"
#include "testing/test_validation_tracker.h"
namespace credentio {
namespace {
using ::credentio_testing::ParseTextProtoOrDie;
using ::testing::Eq;
using ::testing::IsEmpty;
class FakeHashChecker : public HashChecker {
public:
explicit FakeHashChecker(absl::string_view hash_value)
: hash_value_(hash_value) {}
void Update(absl::string_view data) override {}
bool Check(absl::string_view hash) override { return hash == hash_value_; }
private:
absl::string_view hash_value_;
};
class FakeHashCheckerFactory : public HashCheckerFactory {
public:
explicit FakeHashCheckerFactory(absl::string_view hash_value)
: hash_value_(hash_value) {}
absl::StatusOr<std::unique_ptr<HashChecker>> Create(
HashAlgorithm algorithm) const override {
return std::make_unique<FakeHashChecker>(hash_value_);
}
private:
absl::string_view hash_value_;
};
class HashedUriValidatorTest : public ::testing::Test {
protected:
HashedUriValidatorTest()
: box_a_({
.description = {.label = "box_a"},
.raw_bytes = "box a contents",
}),
box_a_hash_("1CCMpryihdV55fyQNDUQgDLS5aXhkyGwEH6cVcNtmCE="),
manifest_store_(CreateManifestStore(
{CreateStandardManifest("urn:c2pa:manifest0", {}),
CreateStandardManifest("urn:c2pa:manifest1", {box_a_})})),
uri_resolver_(
jumbf::UriResolver::WithSingleRootChild(&manifest_store_)) {}
jumbf::SuperBox box_a_;
absl::string_view box_a_hash_;
jumbf::SuperBox manifest_store_;
jumbf::UriResolver uri_resolver_;
TestValidationTracker t_;
};
TEST_F(HashedUriValidatorTest, UriDoesNotResolve) {
HashedUriValidator validator("/c2pa/urn:c2pa:manifest0", uri_resolver_);
EXPECT_THAT(validator.Validate(ParseTextProtoOrDie<HashedUri>(R"pb(
url: "no_such_box"
)pb"),
"", t_.tracker()),
Eq(std::nullopt));
EXPECT_THAT(t_.GetFailures(),
ContainsFailure(FailureStatusCode::kHashedUriMissing));
}
TEST_F(HashedUriValidatorTest, UriDoesNotResolveCustomCode) {
HashedUriValidator validator("/c2pa/urn:c2pa:manifest0", uri_resolver_);
validator.SetErrorCodes(
{.missing = FailureStatusCode::kIngredientManifestMissing,
.mismatch = FailureStatusCode::kIngredientManifestMismatch});
EXPECT_THAT(validator.Validate(ParseTextProtoOrDie<HashedUri>(R"pb(
url: "no_such_box"
)pb"),
"", t_.tracker()),
Eq(std::nullopt));
EXPECT_THAT(t_.GetFailures(),
ContainsFailure(FailureStatusCode::kIngredientManifestMissing));
}
TEST_F(HashedUriValidatorTest, InvalidBoxHeader) {
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest("urn:c2pa:manifest0", {}),
CreateStandardManifest("urn:c2pa:manifest1",
{{
.description = {.label = "box_a"},
.raw_bytes = "x", // Invalid header
}})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
HashedUriValidator validator("/c2pa/urn:c2pa:manifest0", uri_resolver);
EXPECT_THAT(
validator.Validate(ParseTextProtoOrDie<HashedUri>(R"pb(
url: "self#jumbf=/c2pa/urn:c2pa:manifest1/box_a"
algorithm: "sha256"
hash: "abc"
)pb"),
"", t_.tracker()),
Eq(std::nullopt));
EXPECT_THAT(t_.GetFailures(),
ContainsFailure(FailureStatusCode::kHashedUriMismatch));
}
TEST_F(HashedUriValidatorTest, HashMismatch) {
HashedUriValidator validator("/c2pa/urn:c2pa:manifest0", uri_resolver_);
EXPECT_THAT(validator.Validate(ParseTextProtoOrDie<HashedUri>(R"pb(
url: "self#jumbf=/c2pa/urn:c2pa:manifest1"
algorithm: "sha256"
hash: "abc"
)pb"),
"", t_.tracker()),
Eq(std::nullopt));
EXPECT_THAT(t_.GetFailures(),
ContainsFailure(FailureStatusCode::kHashedUriMismatch));
}
TEST_F(HashedUriValidatorTest, HashMismatchCustomCode) {
HashedUriValidator validator("/c2pa/urn:c2pa:manifest0", uri_resolver_);
validator.SetErrorCodes(
{.missing = FailureStatusCode::kIngredientManifestMissing,
.mismatch = FailureStatusCode::kIngredientManifestMismatch});
EXPECT_THAT(validator.Validate(ParseTextProtoOrDie<HashedUri>(R"pb(
url: "self#jumbf=/c2pa/urn:c2pa:manifest1"
algorithm: "sha256"
hash: "abc"
)pb"),
"", t_.tracker()),
Eq(std::nullopt));
EXPECT_THAT(t_.GetFailures(),
ContainsFailure(FailureStatusCode::kIngredientManifestMismatch));
}
TEST_F(HashedUriValidatorTest, UnsupportedAlgorithm) {
HashedUriValidator validator("/c2pa/urn:c2pa:manifest0", uri_resolver_);
EXPECT_THAT(validator.Validate(ParseTextProtoOrDie<HashedUri>(R"pb(
url: "self#jumbf=/c2pa/urn:c2pa:manifest1"
algorithm: "rutabaga256"
hash: "abc"
)pb"),
"", t_.tracker()),
Eq(std::nullopt));
EXPECT_THAT(t_.GetFailures(),
ContainsFailure(FailureStatusCode::kAlgorithmUnsupported));
}
TEST_F(HashedUriValidatorTest, Success) {
HashedUriValidator validator("/c2pa/urn:c2pa:manifest0", uri_resolver_);
HashedUri hashed_uri;
hashed_uri.set_url("self#jumbf=/c2pa/urn:c2pa:manifest1/box_a");
hashed_uri.set_algorithm("sha256");
ASSERT_TRUE(
absl::Base64Unescape("1CCMpryihdV55fyQNDUQgDLS5aXhkyGwEH6cVcNtmCE=",
hashed_uri.mutable_hash()));
EXPECT_THAT(validator.Validate(hashed_uri, "", t_.tracker()),
Eq("/c2pa/urn:c2pa:manifest1/box_a"));
EXPECT_THAT(t_.GetFailures(), IsEmpty());
}
TEST_F(HashedUriValidatorTest, RelativeUri) {
HashedUriValidator validator("/c2pa/urn:c2pa:manifest1", uri_resolver_);
HashedUri hashed_uri;
hashed_uri.set_url("self#jumbf=box_a");
hashed_uri.set_algorithm("sha256");
ASSERT_TRUE(
absl::Base64Unescape("1CCMpryihdV55fyQNDUQgDLS5aXhkyGwEH6cVcNtmCE=",
hashed_uri.mutable_hash()));
EXPECT_THAT(validator.Validate(hashed_uri, "", t_.tracker()),
Eq("/c2pa/urn:c2pa:manifest1/box_a"));
EXPECT_THAT(t_.GetFailures(), IsEmpty());
}
TEST_F(HashedUriValidatorTest, DefaultAlgorithmIgnoredWithExplicitAlgorithm) {
HashedUriValidator validator("/c2pa/urn:c2pa:manifest1", uri_resolver_);
validator.SetDefaultAlgorithm("sha512");
HashedUri hashed_uri;
hashed_uri.set_url("self#jumbf=box_a");
hashed_uri.set_algorithm("sha256");
ASSERT_TRUE(
absl::Base64Unescape("1CCMpryihdV55fyQNDUQgDLS5aXhkyGwEH6cVcNtmCE=",
hashed_uri.mutable_hash()));
EXPECT_THAT(validator.Validate(hashed_uri, "", t_.tracker()),
Eq("/c2pa/urn:c2pa:manifest1/box_a"));
EXPECT_THAT(t_.GetFailures(), IsEmpty());
}
TEST_F(HashedUriValidatorTest, DefaultAlgorithmUsed) {
HashedUriValidator validator("/c2pa/urn:c2pa:manifest1", uri_resolver_);
validator.SetDefaultAlgorithm("sha512");
HashedUri hashed_uri;
hashed_uri.set_url("self#jumbf=box_a");
ASSERT_TRUE(absl::Base64Unescape(
R"(69KGliqftQfRtFL+ouQG63R8RZef70mzgHC1GmUahLbyrFOzXliLyZkzDNwX2WfUGLYP1b4KCc5JRfl4OvtNSQ==)",
hashed_uri.mutable_hash()));
EXPECT_THAT(validator.Validate(hashed_uri, "", t_.tracker()),
Eq("/c2pa/urn:c2pa:manifest1/box_a"));
EXPECT_THAT(t_.GetFailures(), IsEmpty());
}
TEST_F(HashedUriValidatorTest, InjectedHashCheckerFactory) {
FakeHashCheckerFactory hash_checker_factory("fake_hash_value");
HashedUriValidator validator("/c2pa/urn:c2pa:manifest1", uri_resolver_,
&hash_checker_factory);
auto hashed_uri = ParseTextProtoOrDie<HashedUri>(R"pb(
url: "self#jumbf=box_a"
algorithm: "sha256"
hash: "fake_hash_value"
)pb");
EXPECT_THAT(validator.Validate(hashed_uri, "", t_.tracker()),
Eq("/c2pa/urn:c2pa:manifest1/box_a"));
EXPECT_THAT(t_.GetFailures(), IsEmpty());
}
} // namespace
} // namespace credentio