blob: 42479c52f6fda2a63a03aec96c754a24bedfd08f [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/network/trust_tokens/trust_token_key_commitment_parser.h"
#include "base/base64.h"
#include "base/json/json_reader.h"
#include "base/no_destructor.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind_test_util.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "mojo/public/cpp/bindings/struct_ptr.h"
#include "services/network/public/mojom/trust_tokens.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using ::testing::ElementsAre;
using ::testing::Truly;
using ::testing::UnorderedElementsAre;
namespace network {
namespace {
// For convenience, define a matcher for checking mojo struct pointer equality
// in order to simplify validating results returning containers of points.
template <typename T>
decltype(auto) EqualsMojo(const mojo::StructPtr<T>& value) {
return Truly([&](const mojo::StructPtr<T>& candidate) {
return mojo::Equals(candidate, value);
});
}
} // namespace
TEST(TrustTokenKeyCommitmentParser, RejectsEmpty) {
// If the input isn't valid JSON, we should
// reject it. In particular, we should reject
// empty input.
EXPECT_FALSE(TrustTokenKeyCommitmentParser().Parse(""));
}
TEST(TrustTokenKeyCommitmentParser, RejectsNonemptyMalformed) {
// If the input isn't valid JSON, we should
// reject it.
const char input[] = "certainly not valid JSON";
// Sanity check that the input is not valid JSON.
ASSERT_FALSE(base::JSONReader::Read(input));
EXPECT_FALSE(TrustTokenKeyCommitmentParser().Parse(input));
}
TEST(TrustTokenKeyCommitmentParser, RejectsNonDictionaryInput) {
// The outermost value should be a dictionary.
// Valid JSON, but not a dictionary.
const char input[] = "5";
// Sanity check that the input is actually valid JSON.
ASSERT_TRUE(base::JSONReader::Read(input));
EXPECT_FALSE(TrustTokenKeyCommitmentParser().Parse(input));
}
TEST(TrustTokenKeyCommitmentParser, AcceptsMinimal) {
std::string input = R"( { "batchsize": 5, "srrkey": "aaaa" } )";
// Sanity check that the input is actually valid JSON.
ASSERT_TRUE(base::JSONReader::Read(input));
auto expectation = mojom::TrustTokenKeyCommitmentResult::New();
expectation->batch_size = 5;
base::Base64Decode("aaaa",
&expectation->signed_redemption_record_verification_key);
EXPECT_THAT(TrustTokenKeyCommitmentParser().Parse(input),
EqualsMojo(expectation));
}
TEST(TrustTokenKeyCommitmentParser, RejectsMissingSrrkey) {
std::string input = R"( {"batchsize": 5} )";
// Sanity check that the input is actually valid JSON.
ASSERT_TRUE(base::JSONReader::Read(input));
mojom::TrustTokenKeyCommitmentResultPtr result =
TrustTokenKeyCommitmentParser().Parse(input);
EXPECT_FALSE(result);
}
TEST(TrustTokenKeyCommitmentParser, RejectsTypeUnsafeSrrkey) {
std::string input = R"( { "batchsize": 5, "srrkey": 5 } )";
// Sanity check that the input is actually valid JSON.
ASSERT_TRUE(base::JSONReader::Read(input));
mojom::TrustTokenKeyCommitmentResultPtr result =
TrustTokenKeyCommitmentParser().Parse(input);
EXPECT_FALSE(result);
}
TEST(TrustTokenKeyCommitmentParser, RejectsNonBase64Srrkey) {
std::string input = R"( { "batchsize": 5,
"srrkey": "spaces aren't valid base64" } )";
// Sanity check that the input is actually valid JSON.
ASSERT_TRUE(base::JSONReader::Read(input));
mojom::TrustTokenKeyCommitmentResultPtr result =
TrustTokenKeyCommitmentParser().Parse(input);
EXPECT_FALSE(result);
}
TEST(TrustTokenKeyCommitmentParser, RejectsKeyWithTypeUnsafeKeyLabel) {
base::test::TaskEnvironment env(
base::test::TaskEnvironment::TimeSource::MOCK_TIME);
base::Time one_minute_from_now =
base::Time::Now() + base::TimeDelta::FromMinutes(1);
int64_t one_minute_from_now_in_micros =
(one_minute_from_now - base::Time::UnixEpoch()).InMicroseconds();
// (The expiry will likely exceed the JSON spec's maximum integer value, so
// it's encoded as a string.)
const std::string input = base::StringPrintf(
R"({
"batchsize": 5, "srrkey": "aaaa",
"this label is not an integer": {
"Y": "akey",
"expiry": "%s"
}
})",
base::NumberToString(one_minute_from_now_in_micros).c_str());
// Sanity check that the input is actually valid JSON.
ASSERT_TRUE(base::JSONReader::Read(input));
// Key labels must be integers in the representable range of uint32_t, so this
// result shouldn't parse.
EXPECT_FALSE(TrustTokenKeyCommitmentParser().Parse(input));
}
TEST(TrustTokenKeyCommitmentParser, RejectsKeyWithKeyLabelTooSmall) {
base::test::TaskEnvironment env(
base::test::TaskEnvironment::TimeSource::MOCK_TIME);
base::Time one_minute_from_now =
base::Time::Now() + base::TimeDelta::FromMinutes(1);
int64_t one_minute_from_now_in_micros =
(one_minute_from_now - base::Time::UnixEpoch()).InMicroseconds();
const std::string input = base::StringPrintf(
R"({
"batchsize": 5, "srrkey": "aaaa",
"-1": {
"Y": "akey",
"expiry": "%s"
}
})",
base::NumberToString(one_minute_from_now_in_micros).c_str());
// Sanity check that the input is actually valid JSON.
ASSERT_TRUE(base::JSONReader::Read(input));
// Key labels must be integers in the representable range of uint32_t, so this
// result shouldn't parse.
EXPECT_FALSE(TrustTokenKeyCommitmentParser().Parse(input));
}
TEST(TrustTokenKeyCommitmentParser, RejectsKeyWithKeyLabelTooLarge) {
base::test::TaskEnvironment env(
base::test::TaskEnvironment::TimeSource::MOCK_TIME);
base::Time one_minute_from_now =
base::Time::Now() + base::TimeDelta::FromMinutes(1);
int64_t one_minute_from_now_in_micros =
(one_minute_from_now - base::Time::UnixEpoch()).InMicroseconds();
const std::string input = base::StringPrintf(
R"({
"batchsize": 5, "srrkey": "aaaa",
"1000000000000": {
"Y": "akey",
"expiry": "%s"
}
})",
base::NumberToString(one_minute_from_now_in_micros).c_str());
// Sanity check that the input is actually valid JSON.
ASSERT_TRUE(base::JSONReader::Read(input));
// Key labels must be integers in the representable range of uint32_t, so this
// result shouldn't parse.
EXPECT_FALSE(TrustTokenKeyCommitmentParser().Parse(input));
}
TEST(TrustTokenKeyCommitmentParser, RejectsOtherwiseValidButNonBase64Key) {
base::test::TaskEnvironment env(
base::test::TaskEnvironment::TimeSource::MOCK_TIME);
base::Time one_minute_from_now =
base::Time::Now() + base::TimeDelta::FromMinutes(1);
int64_t one_minute_from_now_in_micros =
(one_minute_from_now - base::Time::UnixEpoch()).InMicroseconds();
const std::string input = base::StringPrintf(
R"({
"batchsize": 5, "srrkey": "aaaa",
"1": {
"Y": "this key isn't valid base64, so it should be rejected",
"expiry": "%s"
}
})",
base::NumberToString(one_minute_from_now_in_micros).c_str());
// Sanity check that the input is actually valid JSON,
// and that the given time is valid.
ASSERT_TRUE(base::JSONReader::Read(input));
EXPECT_FALSE(TrustTokenKeyCommitmentParser().Parse(input));
}
TEST(TrustTokenKeyCommitmentParser, AcceptsKeyWithExpiryAndBody) {
base::test::TaskEnvironment env(
base::test::TaskEnvironment::TimeSource::MOCK_TIME);
base::Time one_minute_from_now =
base::Time::Now() + base::TimeDelta::FromMinutes(1);
int64_t one_minute_from_now_in_micros =
(one_minute_from_now - base::Time::UnixEpoch()).InMicroseconds();
const std::string input = base::StringPrintf(
R"({
"batchsize": 5, "srrkey": "aaaa",
"1": { "Y": "akey", "expiry": "%s" }
})",
base::NumberToString(one_minute_from_now_in_micros).c_str());
// Sanity check that the input is actually valid JSON,
// and that the given time is valid.
ASSERT_TRUE(base::JSONReader::Read(input));
auto my_key = mojom::TrustTokenVerificationKey::New();
ASSERT_TRUE(base::Base64Decode("akey", &my_key->body));
my_key->expiry = one_minute_from_now;
auto result = TrustTokenKeyCommitmentParser().Parse(input);
ASSERT_TRUE(result);
EXPECT_THAT(result->keys, ElementsAre(EqualsMojo(my_key)));
}
TEST(TrustTokenKeyCommitmentParser, AcceptsMultipleKeys) {
base::test::TaskEnvironment env(
base::test::TaskEnvironment::TimeSource::MOCK_TIME);
base::Time one_minute_from_now =
base::Time::Now() + base::TimeDelta::FromMinutes(1);
int64_t one_minute_from_now_in_micros =
(one_minute_from_now - base::Time::UnixEpoch()).InMicroseconds();
base::Time two_minutes_from_now =
base::Time::Now() + base::TimeDelta::FromMinutes(2);
int64_t two_minutes_from_now_in_micros =
(two_minutes_from_now - base::Time::UnixEpoch()).InMicroseconds();
const std::string input = base::StringPrintf(
R"({
"batchsize": 5, "srrkey": "aaaa",
"1": { "Y": "akey", "expiry": "%s" },
"2": { "Y": "aaaa", "expiry": "%s" }
})",
base::NumberToString(one_minute_from_now_in_micros).c_str(),
base::NumberToString(two_minutes_from_now_in_micros).c_str());
// Sanity check that the input is actually valid JSON,
// and that the given time is valid.
ASSERT_TRUE(base::JSONReader::Read(input));
auto a_key = mojom::TrustTokenVerificationKey::New();
ASSERT_TRUE(base::Base64Decode("akey", &a_key->body));
a_key->expiry = one_minute_from_now;
auto another_key = mojom::TrustTokenVerificationKey::New();
ASSERT_TRUE(base::Base64Decode("aaaa", &another_key->body));
another_key->expiry = two_minutes_from_now;
auto result = TrustTokenKeyCommitmentParser().Parse(input);
ASSERT_TRUE(result);
EXPECT_THAT(result->keys,
UnorderedElementsAre(EqualsMojo(a_key), EqualsMojo(another_key)));
}
TEST(TrustTokenKeyCommitmentParser, RejectsKeyWithNoExpiry) {
// If a key has a missing "expiry" field, we should reject the entire
// record.
const std::string input = R"( { "batchsize": 5, "srrkey": "aaaa",
"1": { "Y": "akey" } })";
// Sanity check that the input is actually valid JSON.
ASSERT_TRUE(base::JSONReader::Read(input));
// Since the key doesn't have an expiry, reject it.
EXPECT_FALSE(TrustTokenKeyCommitmentParser().Parse(input));
}
TEST(TrustTokenKeyCommitmentParser, RejectsKeyWithMalformedExpiry) {
// If a key has a malformed "expiry" field, we should reject the entire
// record.
const std::string input =
R"(
{
"batchsize": 5, "srrkey": "aaaa",
"1": {
"Y": "akey",
"expiry": "absolutely not a valid timestamp"
}
})";
// Sanity check that the input is actually valid JSON.
ASSERT_TRUE(base::JSONReader::Read(input));
// Since the key doesn't have an expiry, reject it.
EXPECT_FALSE(TrustTokenKeyCommitmentParser().Parse(input));
}
TEST(TrustTokenKeyCommitmentParser, IgnoreKeyWithExpiryInThePast) {
base::test::TaskEnvironment env(
base::test::TaskEnvironment::TimeSource::MOCK_TIME);
// Ensure that "one minute ago" yields a nonnegative number of microseconds
// past the Unix epoch.
env.AdvanceClock(std::max<base::TimeDelta>(
base::TimeDelta(), base::Time::UnixEpoch() +
base::TimeDelta::FromMinutes(1) -
base::Time::Now()));
base::Time one_minute_before_now =
base::Time::Now() - base::TimeDelta::FromMinutes(1);
int64_t one_minute_before_now_in_micros =
(one_minute_before_now - base::Time::UnixEpoch()).InMicroseconds();
// If the time has passed a key's "expiry" field, we should reject the entire
// record.
const std::string input = base::StringPrintf(
R"( { "batchsize": 5, "srrkey": "aaaa",
"1": { "Y": "akey", "expiry": "%s" } })",
base::NumberToString(one_minute_before_now_in_micros).c_str());
// Sanity check that the input is actually valid JSON.
ASSERT_TRUE(base::JSONReader::Read(input));
auto expectation = mojom::TrustTokenKeyCommitmentResult::New();
base::Base64Decode("aaaa",
&expectation->signed_redemption_record_verification_key);
expectation->batch_size = 5;
EXPECT_TRUE(
mojo::Equals(TrustTokenKeyCommitmentParser().Parse(input), expectation));
}
TEST(TrustTokenKeyCommitmentParser, RejectsKeyWithNoBody) {
base::test::TaskEnvironment env(
base::test::TaskEnvironment::TimeSource::MOCK_TIME);
base::Time one_minute_from_now =
base::Time::Now() + base::TimeDelta::FromMinutes(1);
int64_t one_minute_from_now_in_micros =
(one_minute_from_now - base::Time::UnixEpoch()).InMicroseconds();
// If a key has an expiry but is missing its body,
// we should reject the entire result.
const std::string input = base::StringPrintf(
R"( { "batchsize": 5, "srrkey": "aaaa",
"1": { "expiry": "%s" } } )",
base::NumberToString(one_minute_from_now_in_micros).c_str());
// Sanity check that the input is actually valid JSON,
// and that the date is valid.
ASSERT_TRUE(base::JSONReader::Read(input));
// Since the key doesn't have a body, reject it.
EXPECT_FALSE(TrustTokenKeyCommitmentParser().Parse(input));
}
TEST(TrustTokenKeyCommitmentParser, RejectsEmptyKey) {
// If a key has neither an expiry or a body,
// we should reject the entire result.
const std::string input = R"( { "batchsize": 5,
"srrkey": "aaaa", "1": { } })";
// Sanity check that the input is actually valid JSON,
// and that the date is valid.
ASSERT_TRUE(base::JSONReader::Read(input));
// Since the key doesn't have an expiry or a body, reject it.
EXPECT_FALSE(TrustTokenKeyCommitmentParser().Parse(input));
}
TEST(TrustTokenKeyCommitmentParser, ParsesBatchSize) {
std::string input =
R"({
"batchsize": 5,
"srrkey": "aaaa"
})";
mojom::TrustTokenKeyCommitmentResultPtr result =
TrustTokenKeyCommitmentParser().Parse(input);
ASSERT_TRUE(result);
ASSERT_TRUE(result->batch_size);
EXPECT_EQ(result->batch_size, 5);
}
TEST(TrustTokenKeyCommitmentParser, RejectsMissingBatchSize) {
std::string input =
R"({
"srrkey": "aaaa",
})";
mojom::TrustTokenKeyCommitmentResultPtr result =
TrustTokenKeyCommitmentParser().Parse(input);
EXPECT_FALSE(result);
}
TEST(TrustTokenKeyCommitmentParser, RejectsNonpositiveBatchSize) {
std::string input =
R"({
"srrkey": "aaaa",
"batchsize": "0",
})";
mojom::TrustTokenKeyCommitmentResultPtr result =
TrustTokenKeyCommitmentParser().Parse(input);
EXPECT_FALSE(result);
}
TEST(TrustTokenKeyCommitmentParser, RejectsTypeUnsafeBatchSize) {
std::string input =
R"({
"srrkey": "aaaa",
"batchsize": "not a number"
})";
mojom::TrustTokenKeyCommitmentResultPtr result =
TrustTokenKeyCommitmentParser().Parse(input);
EXPECT_FALSE(result);
}
TEST(TrustTokenKeyCommitmentParserMultipleIssuers, InvalidJson) {
std::string input = "";
ASSERT_FALSE(
base::JSONReader::Read(input)); // Make sure it's really not valid JSON.
auto result = TrustTokenKeyCommitmentParser().ParseMultipleIssuers(input);
EXPECT_FALSE(result);
}
TEST(TrustTokenKeyCommitmentParserMultipleIssuers, NotADictionary) {
std::string input = "3";
auto result = TrustTokenKeyCommitmentParser().ParseMultipleIssuers(input);
EXPECT_FALSE(result);
}
TEST(TrustTokenKeyCommitmentParserMultipleIssuers, Empty) {
std::string input = "{}";
// Make sure the input is actually valid JSON.
ASSERT_TRUE(base::JSONReader::Read(input));
auto result = TrustTokenKeyCommitmentParser().ParseMultipleIssuers(input);
ASSERT_TRUE(result);
EXPECT_TRUE(result->empty());
}
TEST(TrustTokenKeyCommitmentParserMultipleIssuers, UnsuitableKey) {
// Test that a key with an unsuitable Trust Tokens origin gets skipped.
std::string input =
R"( { "http://insecure.example/":
{ "batchsize": 5, "srrkey": "aaaa" } } )";
// Make sure the input is actually valid JSON.
ASSERT_TRUE(base::JSONReader::Read(input));
auto result = TrustTokenKeyCommitmentParser().ParseMultipleIssuers(input);
ASSERT_TRUE(result);
EXPECT_TRUE(result->empty());
}
TEST(TrustTokenKeyCommitmentParserMultipleIssuers, SuitableKeyInvalidValue) {
// Test that a key-value pair with a malformed value gets skipped.
std::string input =
R"( { "https://insecure.example/":
"not a valid encoding of a key commitment result" } )";
// Make sure the input is actually valid JSON.
ASSERT_TRUE(base::JSONReader::Read(input));
auto result = TrustTokenKeyCommitmentParser().ParseMultipleIssuers(input);
ASSERT_TRUE(result);
EXPECT_TRUE(result->empty());
}
TEST(TrustTokenKeyCommitmentParserMultipleIssuers, SingleIssuer) {
std::string input =
R"( { "https://issuer.example/": {
"batchsize": 5, "srrkey": "aaaa" } } )";
// Make sure the input is actually valid JSON.
ASSERT_TRUE(base::JSONReader::Read(input));
TrustTokenKeyCommitmentParser parser;
auto result = parser.ParseMultipleIssuers(input);
ASSERT_TRUE(result);
auto issuer =
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.example"));
ASSERT_TRUE(result->count(issuer));
EXPECT_TRUE(mojo::Equals(result->at(issuer), parser.Parse(R"({ "batchsize": 5,
"srrkey": "aaaa" })")));
}
TEST(TrustTokenKeyCommitmentParserMultipleIssuers, DuplicateIssuer) {
std::string input =
R"( { "https://issuer.example/": { "batchsize": 5, "srrkey": "aaaa" },
"https://other.example/": { "batchsize": 5, "srrkey": "aaab" },
"https://issuer.example/this-is-really-the-same-issuer-as-the-first-entry":
{ "batchsize": 5, "srrkey": "aaac" }
} )";
// Make sure the input is actually valid JSON.
ASSERT_TRUE(base::JSONReader::Read(input));
TrustTokenKeyCommitmentParser parser;
auto result = parser.ParseMultipleIssuers(input);
ASSERT_TRUE(result);
// The result should have been deduplicated. Moreover, the entry with the
// largest issuer JSON string lexicographically should win.
ASSERT_EQ(result->size(), 2u);
auto issuer =
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.example"));
ASSERT_TRUE(result->count(issuer));
EXPECT_TRUE(
mojo::Equals(result->at(issuer),
parser.Parse(R"({ "batchsize": 5, "srrkey": "aaac" })")));
}
TEST(TrustTokenKeyCommitmentParserMultipleIssuers, DuplicateIssuerFirstWins) {
// This is the same as DuplicateIssuer, except this time the first entry
// position-wise is the longest lexicographically, so deduplication should
// favor it.
std::string input =
R"( {
"https://issuer.example/longer": { "batchsize": 5, "srrkey": "aaaa" },
"https://other.example/": { "batchsize": 5, "srrkey": "aaab" },
"https://issuer.example/":
{ "batchsize": 5, "srrkey": "aaac" }
} )";
// Make sure the input is actually valid JSON.
ASSERT_TRUE(base::JSONReader::Read(input));
TrustTokenKeyCommitmentParser parser;
auto result = parser.ParseMultipleIssuers(input);
ASSERT_TRUE(result);
// The result should have been deduplicated. Moreover, the entry with the
// largest issuer JSON string lexicographically should win.
ASSERT_EQ(result->size(), 2u);
auto issuer =
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.example"));
ASSERT_TRUE(result->count(issuer));
EXPECT_TRUE(
mojo::Equals(result->at(issuer),
parser.Parse(R"({ "batchsize": 5, "srrkey": "aaaa" })")));
}
TEST(TrustTokenKeyCommitmentParserMultipleIssuers,
MixOfSuitableAndUnsuitableIssuers) {
std::string input = R"( {
"https://issuer.example/": { "batchsize": 5, "srrkey": "aaaa" },
"http://insecure.example": { "batchsize": 5, "srrkey": "bbbb" } } )";
// Make sure the input is actually valid JSON.
ASSERT_TRUE(base::JSONReader::Read(input));
TrustTokenKeyCommitmentParser parser;
auto result = parser.ParseMultipleIssuers(input);
ASSERT_TRUE(result);
EXPECT_EQ(result->size(), 1u);
auto issuer =
*SuitableTrustTokenOrigin::Create(GURL("https://issuer.example"));
ASSERT_TRUE(result->count(issuer));
EXPECT_TRUE(
mojo::Equals(result->at(issuer),
parser.Parse(R"({ "batchsize": 5, "srrkey": "aaaa" })")));
}
} // namespace network