blob: 27883cebe66ceffaa10cb8c8b9af869b2ffeee26 [file] [log] [blame]
// Copyright (c) 2023 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 "quiche/blind_sign_auth/blind_sign_auth.h"
#include <cstdint>
#include <memory>
#include <string>
#include <utility>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/escaping.h"
#include "absl/strings/string_view.h"
#include "absl/time/time.h"
#include "anonymous_tokens/cpp/crypto/crypto_utils.h"
#include "anonymous_tokens/cpp/privacy_pass/token_encodings.h"
#include "anonymous_tokens/cpp/testing/utils.h"
#include "openssl/base.h"
#include "openssl/digest.h"
#include "quiche/blind_sign_auth/blind_sign_auth_interface.h"
#include "quiche/blind_sign_auth/blind_sign_auth_protos.h"
#include "quiche/blind_sign_auth/blind_sign_http_interface.h"
#include "quiche/blind_sign_auth/blind_sign_http_response.h"
#include "quiche/blind_sign_auth/test_tools/mock_blind_sign_http_interface.h"
#include "quiche/common/platform/api/quiche_mutex.h"
#include "quiche/common/platform/api/quiche_test.h"
#include "quiche/common/test_tools/quiche_test_utils.h"
namespace quiche {
namespace test {
namespace {
using ::testing::_;
using ::testing::Eq;
using ::testing::InSequence;
using ::testing::Invoke;
using ::testing::StartsWith;
using ::testing::Unused;
class BlindSignAuthTest : public QuicheTest {
protected:
void SetUp() override {
// Create keypair and populate protos.
auto [test_rsa_public_key, test_rsa_private_key] =
anonymous_tokens::GetStrongTestRsaKeyPair2048();
ANON_TOKENS_ASSERT_OK_AND_ASSIGN(
rsa_public_key_,
anonymous_tokens::CreatePublicKeyRSA(
test_rsa_public_key.n, test_rsa_public_key.e));
ANON_TOKENS_ASSERT_OK_AND_ASSIGN(
rsa_private_key_,
anonymous_tokens::CreatePrivateKeyRSA(
test_rsa_private_key.n, test_rsa_private_key.e,
test_rsa_private_key.d, test_rsa_private_key.p,
test_rsa_private_key.q, test_rsa_private_key.dp,
test_rsa_private_key.dq, test_rsa_private_key.crt));
anonymous_tokens::RSAPublicKey public_key;
public_key.set_n(test_rsa_public_key.n);
public_key.set_e(test_rsa_public_key.e);
public_key_proto_.set_key_version(1);
public_key_proto_.set_use_case("TEST_USE_CASE");
public_key_proto_.set_serialized_public_key(public_key.SerializeAsString());
public_key_proto_.set_sig_hash_type(
anonymous_tokens::AT_HASH_TYPE_SHA384);
public_key_proto_.set_mask_gen_function(
anonymous_tokens::AT_MGF_SHA384);
public_key_proto_.set_salt_length(48);
public_key_proto_.set_key_size(256);
public_key_proto_.set_message_mask_type(
anonymous_tokens::AT_MESSAGE_MASK_CONCAT);
public_key_proto_.set_message_mask_size(32);
// Create expected GetInitialDataRequest.
expected_get_initial_data_request_.set_use_attestation(false);
expected_get_initial_data_request_.set_service_type("chromeipblinding");
expected_get_initial_data_request_.set_location_granularity(
privacy::ppn::GetInitialDataRequest_LocationGranularity_CITY_GEOS);
expected_get_initial_data_request_.set_validation_version(2);
expected_get_initial_data_request_.set_proxy_layer(privacy::ppn::PROXY_A);
// Create fake GetInitialDataResponse.
privacy::ppn::GetInitialDataResponse fake_get_initial_data_response;
*fake_get_initial_data_response.mutable_at_public_metadata_public_key() =
public_key_proto_;
// Create public metadata info.
privacy::ppn::PublicMetadata::Location location;
location.set_country("US");
anonymous_tokens::Timestamp expiration;
expiration.set_seconds(absl::ToUnixSeconds(absl::Now() + absl::Hours(1)));
privacy::ppn::PublicMetadata public_metadata;
*public_metadata.mutable_exit_location() = location;
public_metadata.set_service_type("chromeipblinding");
*public_metadata.mutable_expiration() = expiration;
public_metadata_info_.set_validation_version(1);
*public_metadata_info_.mutable_public_metadata() = public_metadata;
*fake_get_initial_data_response.mutable_public_metadata_info() =
public_metadata_info_;
fake_get_initial_data_response_ = fake_get_initial_data_response;
// Create PrivacyPassData.
privacy::ppn::GetInitialDataResponse::PrivacyPassData privacy_pass_data;
// token_key_id is derived from public key.
ANON_TOKENS_ASSERT_OK_AND_ASSIGN(
std::string public_key_der,
anonymous_tokens::RsaSsaPssPublicKeyToDerEncoding(
rsa_public_key_.get()));
const EVP_MD* sha256 = EVP_sha256();
ANON_TOKENS_ASSERT_OK_AND_ASSIGN(
token_key_id_, anonymous_tokens::ComputeHash(
public_key_der, *sha256));
// Create and serialize fake extensions.
anonymous_tokens::ExpirationTimestamp
expiration_timestamp;
int64_t one_hour_away = absl::ToUnixSeconds(absl::Now() + absl::Hours(1));
expiration_timestamp.timestamp = one_hour_away - (one_hour_away % 900);
expiration_timestamp.timestamp_precision = 900;
absl::StatusOr<anonymous_tokens::Extension>
expiration_extension = expiration_timestamp.AsExtension();
QUICHE_EXPECT_OK(expiration_extension);
extensions_.extensions.push_back(*expiration_extension);
anonymous_tokens::GeoHint geo_hint;
geo_hint.geo_hint = "US,US-AL,ALABASTER";
absl::StatusOr<anonymous_tokens::Extension>
geo_hint_extension = geo_hint.AsExtension();
QUICHE_EXPECT_OK(geo_hint_extension);
extensions_.extensions.push_back(*geo_hint_extension);
anonymous_tokens::ServiceType service_type;
service_type.service_type_id =
anonymous_tokens::ServiceType::kChromeIpBlinding;
absl::StatusOr<anonymous_tokens::Extension>
service_type_extension = service_type.AsExtension();
QUICHE_EXPECT_OK(service_type_extension);
extensions_.extensions.push_back(*service_type_extension);
anonymous_tokens::DebugMode debug_mode;
debug_mode.mode = anonymous_tokens::DebugMode::kDebug;
absl::StatusOr<anonymous_tokens::Extension>
debug_mode_extension = debug_mode.AsExtension();
QUICHE_EXPECT_OK(debug_mode_extension);
extensions_.extensions.push_back(*debug_mode_extension);
anonymous_tokens::ProxyLayer proxy_layer;
proxy_layer.layer =
anonymous_tokens::ProxyLayer::kProxyA;
absl::StatusOr<anonymous_tokens::Extension>
proxy_layer_extension = proxy_layer.AsExtension();
QUICHE_EXPECT_OK(proxy_layer_extension);
extensions_.extensions.push_back(*proxy_layer_extension);
absl::StatusOr<std::string> serialized_extensions =
anonymous_tokens::EncodeExtensions(extensions_);
QUICHE_EXPECT_OK(serialized_extensions);
privacy_pass_data.set_token_key_id(token_key_id_);
privacy_pass_data.set_public_metadata_extensions(*serialized_extensions);
*fake_get_initial_data_response.mutable_public_metadata_info() =
public_metadata_info_;
*fake_get_initial_data_response.mutable_privacy_pass_data() =
privacy_pass_data;
fake_get_initial_data_response_ = fake_get_initial_data_response;
// Create BlindSignAuthOptions.
privacy::ppn::BlindSignAuthOptions options;
options.set_enable_privacy_pass(false);
blind_sign_auth_ =
std::make_unique<BlindSignAuth>(&mock_http_interface_, options);
}
void TearDown() override {
blind_sign_auth_.reset(nullptr);
}
public:
void CreateSignResponse(const std::string& body, bool use_privacy_pass) {
privacy::ppn::AuthAndSignRequest request;
ASSERT_TRUE(request.ParseFromString(body));
// Validate AuthAndSignRequest.
EXPECT_EQ(request.oauth_token(), oauth_token_);
EXPECT_EQ(request.service_type(), "chromeipblinding");
// Phosphor does not need the public key hash if the KeyType is
// privacy::ppn::AT_PUBLIC_METADATA_KEY_TYPE.
EXPECT_EQ(request.key_type(), privacy::ppn::AT_PUBLIC_METADATA_KEY_TYPE);
EXPECT_EQ(request.public_key_hash(), "");
EXPECT_EQ(request.key_version(), public_key_proto_.key_version());
EXPECT_EQ(request.do_not_use_rsa_public_exponent(), true);
EXPECT_NE(request.blinded_token().size(), 0);
if (use_privacy_pass) {
EXPECT_EQ(request.public_metadata_extensions(),
fake_get_initial_data_response_.privacy_pass_data()
.public_metadata_extensions());
} else {
EXPECT_EQ(request.public_metadata_info().SerializeAsString(),
public_metadata_info_.SerializeAsString());
}
// Construct AuthAndSignResponse.
privacy::ppn::AuthAndSignResponse response;
for (const auto& request_token : request.blinded_token()) {
std::string decoded_blinded_token;
ASSERT_TRUE(absl::Base64Unescape(request_token, &decoded_blinded_token));
if (use_privacy_pass) {
absl::StatusOr<std::string> signature =
anonymous_tokens::TestSignWithPublicMetadata(
decoded_blinded_token, request.public_metadata_extensions(),
*rsa_private_key_, false);
QUICHE_EXPECT_OK(signature);
response.add_blinded_token_signature(absl::Base64Escape(*signature));
} else {
absl::StatusOr<std::string> serialized_token =
anonymous_tokens::TestSign(
decoded_blinded_token, rsa_private_key_.get());
// TestSignWithPublicMetadata for privacy pass
QUICHE_EXPECT_OK(serialized_token);
response.add_blinded_token_signature(
absl::Base64Escape(*serialized_token));
}
}
sign_response_ = response;
}
void ValidateGetTokensOutput(absl::Span<BlindSignToken> tokens) {
for (const auto& token : tokens) {
privacy::ppn::SpendTokenData spend_token_data;
ASSERT_TRUE(spend_token_data.ParseFromString(token.token));
// Validate token structure.
EXPECT_EQ(spend_token_data.public_metadata().SerializeAsString(),
public_metadata_info_.public_metadata().SerializeAsString());
EXPECT_THAT(spend_token_data.unblinded_token(), StartsWith("blind:"));
EXPECT_GE(spend_token_data.unblinded_token_signature().size(),
spend_token_data.unblinded_token().size());
EXPECT_EQ(spend_token_data.signing_key_version(),
public_key_proto_.key_version());
EXPECT_NE(spend_token_data.use_case(),
anonymous_tokens::AnonymousTokensUseCase::
ANONYMOUS_TOKENS_USE_CASE_UNDEFINED);
EXPECT_NE(spend_token_data.message_mask(), "");
}
}
void ValidatePrivacyPassTokensOutput(absl::Span<BlindSignToken> tokens) {
for (const auto& token : tokens) {
privacy::ppn::PrivacyPassTokenData privacy_pass_token_data;
ASSERT_TRUE(privacy_pass_token_data.ParseFromString(token.token));
// Validate token structure.
std::string decoded_token;
ASSERT_TRUE(absl::WebSafeBase64Unescape(privacy_pass_token_data.token(),
&decoded_token));
std::string decoded_extensions;
ASSERT_TRUE(absl::WebSafeBase64Unescape(
privacy_pass_token_data.encoded_extensions(), &decoded_extensions));
}
}
MockBlindSignHttpInterface mock_http_interface_;
std::unique_ptr<BlindSignAuth> blind_sign_auth_;
anonymous_tokens::RSABlindSignaturePublicKey
public_key_proto_;
bssl::UniquePtr<RSA> rsa_public_key_;
bssl::UniquePtr<RSA> rsa_private_key_;
std::string token_key_id_;
anonymous_tokens::Extensions extensions_;
privacy::ppn::PublicMetadataInfo public_metadata_info_;
privacy::ppn::AuthAndSignResponse sign_response_;
privacy::ppn::GetInitialDataResponse fake_get_initial_data_response_;
std::string oauth_token_ = "oauth_token";
privacy::ppn::GetInitialDataRequest expected_get_initial_data_request_;
};
TEST_F(BlindSignAuthTest, TestGetTokensSuccessful) {
BlindSignHttpResponse fake_public_key_response(
200, fake_get_initial_data_response_.SerializeAsString());
{
InSequence seq;
EXPECT_CALL(
mock_http_interface_,
DoRequest(
Eq(BlindSignHttpRequestType::kGetInitialData), Eq(oauth_token_),
Eq(expected_get_initial_data_request_.SerializeAsString()), _))
.Times(1)
.WillOnce([=](auto&&, auto&&, auto&&, auto get_initial_data_cb) {
std::move(get_initial_data_cb)(fake_public_key_response);
});
EXPECT_CALL(mock_http_interface_,
DoRequest(Eq(BlindSignHttpRequestType::kAuthAndSign),
Eq(oauth_token_), _, _))
.Times(1)
.WillOnce(Invoke([this](Unused, Unused, const std::string& body,
BlindSignHttpCallback callback) {
CreateSignResponse(body, false);
BlindSignHttpResponse http_response(
200, sign_response_.SerializeAsString());
std::move(callback)(http_response);
}));
}
int num_tokens = 1;
QuicheNotification done;
SignedTokenCallback callback =
[this, &done,
num_tokens](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
QUICHE_EXPECT_OK(tokens);
EXPECT_EQ(tokens->size(), num_tokens);
ValidateGetTokensOutput(*tokens);
done.Notify();
};
blind_sign_auth_->GetTokens(oauth_token_, num_tokens, ProxyLayer::kProxyA,
std::move(callback));
done.WaitForNotification();
}
TEST_F(BlindSignAuthTest, TestGetTokensFailedNetworkError) {
EXPECT_CALL(mock_http_interface_,
DoRequest(Eq(BlindSignHttpRequestType::kGetInitialData),
Eq(oauth_token_), _, _))
.Times(1)
.WillOnce([=](auto&&, auto&&, auto&&, auto get_initial_data_cb) {
std::move(get_initial_data_cb)(
absl::InternalError("Failed to create socket"));
});
EXPECT_CALL(mock_http_interface_,
DoRequest(Eq(BlindSignHttpRequestType::kAuthAndSign), _, _, _))
.Times(0);
int num_tokens = 1;
QuicheNotification done;
SignedTokenCallback callback =
[&done](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
EXPECT_THAT(tokens.status().code(), absl::StatusCode::kInternal);
done.Notify();
};
blind_sign_auth_->GetTokens(oauth_token_, num_tokens, ProxyLayer::kProxyA,
std::move(callback));
done.WaitForNotification();
}
TEST_F(BlindSignAuthTest, TestGetTokensFailedBadGetInitialDataResponse) {
*fake_get_initial_data_response_.mutable_at_public_metadata_public_key()
->mutable_use_case() = "SPAM";
BlindSignHttpResponse fake_public_key_response(
200, fake_get_initial_data_response_.SerializeAsString());
EXPECT_CALL(
mock_http_interface_,
DoRequest(Eq(BlindSignHttpRequestType::kGetInitialData), Eq(oauth_token_),
Eq(expected_get_initial_data_request_.SerializeAsString()), _))
.Times(1)
.WillOnce([=](auto&&, auto&&, auto&&, auto get_initial_data_cb) {
std::move(get_initial_data_cb)(fake_public_key_response);
});
EXPECT_CALL(mock_http_interface_,
DoRequest(Eq(BlindSignHttpRequestType::kAuthAndSign), _, _, _))
.Times(0);
int num_tokens = 1;
QuicheNotification done;
SignedTokenCallback callback =
[&done](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
EXPECT_THAT(tokens.status().code(), absl::StatusCode::kInvalidArgument);
done.Notify();
};
blind_sign_auth_->GetTokens(oauth_token_, num_tokens, ProxyLayer::kProxyA,
std::move(callback));
done.WaitForNotification();
}
TEST_F(BlindSignAuthTest, TestGetTokensFailedBadAuthAndSignResponse) {
BlindSignHttpResponse fake_public_key_response(
200, fake_get_initial_data_response_.SerializeAsString());
{
InSequence seq;
EXPECT_CALL(
mock_http_interface_,
DoRequest(
Eq(BlindSignHttpRequestType::kGetInitialData), Eq(oauth_token_),
Eq(expected_get_initial_data_request_.SerializeAsString()), _))
.Times(1)
.WillOnce([=](auto&&, auto&&, auto&&, auto get_initial_data_cb) {
std::move(get_initial_data_cb)(fake_public_key_response);
});
EXPECT_CALL(mock_http_interface_,
DoRequest(Eq(BlindSignHttpRequestType::kAuthAndSign),
Eq(oauth_token_), _, _))
.Times(1)
.WillOnce(Invoke([this](Unused, Unused, const std::string& body,
BlindSignHttpCallback callback) {
CreateSignResponse(body, false);
// Add an invalid signature that can't be Base64 decoded.
sign_response_.add_blinded_token_signature("invalid_signature%");
BlindSignHttpResponse http_response(
200, sign_response_.SerializeAsString());
std::move(callback)(http_response);
}));
}
int num_tokens = 1;
QuicheNotification done;
SignedTokenCallback callback =
[&done](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
EXPECT_THAT(tokens.status().code(), absl::StatusCode::kInternal);
done.Notify();
};
blind_sign_auth_->GetTokens(oauth_token_, num_tokens, ProxyLayer::kProxyA,
std::move(callback));
done.WaitForNotification();
}
TEST_F(BlindSignAuthTest, TestPrivacyPassGetTokensSucceeds) {
privacy::ppn::BlindSignAuthOptions options;
options.set_enable_privacy_pass(true);
blind_sign_auth_ =
std::make_unique<BlindSignAuth>(&mock_http_interface_, options);
public_key_proto_.set_message_mask_type(
anonymous_tokens::AT_MESSAGE_MASK_NO_MASK);
public_key_proto_.set_message_mask_size(0);
*fake_get_initial_data_response_.mutable_at_public_metadata_public_key() =
public_key_proto_;
BlindSignHttpResponse fake_public_key_response(
200, fake_get_initial_data_response_.SerializeAsString());
{
InSequence seq;
EXPECT_CALL(
mock_http_interface_,
DoRequest(
Eq(BlindSignHttpRequestType::kGetInitialData), Eq(oauth_token_),
Eq(expected_get_initial_data_request_.SerializeAsString()), _))
.Times(1)
.WillOnce([=](auto&&, auto&&, auto&&, auto get_initial_data_cb) {
std::move(get_initial_data_cb)(fake_public_key_response);
});
EXPECT_CALL(mock_http_interface_,
DoRequest(Eq(BlindSignHttpRequestType::kAuthAndSign),
Eq(oauth_token_), _, _))
.Times(1)
.WillOnce(Invoke([this](Unused, Unused, const std::string& body,
BlindSignHttpCallback callback) {
CreateSignResponse(body, /*use_privacy_pass=*/true);
BlindSignHttpResponse http_response(
200, sign_response_.SerializeAsString());
std::move(callback)(http_response);
}));
}
int num_tokens = 1;
QuicheNotification done;
SignedTokenCallback callback =
[this, &done](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
QUICHE_EXPECT_OK(tokens);
ValidatePrivacyPassTokensOutput(*tokens);
done.Notify();
};
blind_sign_auth_->GetTokens(oauth_token_, num_tokens, ProxyLayer::kProxyA,
std::move(callback));
done.WaitForNotification();
}
TEST_F(BlindSignAuthTest, TestPrivacyPassGetTokensFailsWithBadExtensions) {
privacy::ppn::BlindSignAuthOptions options;
options.set_enable_privacy_pass(true);
blind_sign_auth_ =
std::make_unique<BlindSignAuth>(&mock_http_interface_, options);
public_key_proto_.set_message_mask_type(
anonymous_tokens::AT_MESSAGE_MASK_NO_MASK);
public_key_proto_.set_message_mask_size(0);
*fake_get_initial_data_response_.mutable_at_public_metadata_public_key() =
public_key_proto_;
fake_get_initial_data_response_.mutable_privacy_pass_data()
->set_public_metadata_extensions("spam");
BlindSignHttpResponse fake_public_key_response(
200, fake_get_initial_data_response_.SerializeAsString());
EXPECT_CALL(
mock_http_interface_,
DoRequest(Eq(BlindSignHttpRequestType::kGetInitialData), Eq(oauth_token_),
Eq(expected_get_initial_data_request_.SerializeAsString()), _))
.Times(1)
.WillOnce([=](auto&&, auto&&, auto&&, auto get_initial_data_cb) {
std::move(get_initial_data_cb)(fake_public_key_response);
});
int num_tokens = 1;
QuicheNotification done;
SignedTokenCallback callback =
[&done](absl::StatusOr<absl::Span<BlindSignToken>> tokens) {
EXPECT_THAT(tokens.status().code(), absl::StatusCode::kInvalidArgument);
done.Notify();
};
blind_sign_auth_->GetTokens(oauth_token_, num_tokens, ProxyLayer::kProxyA,
std::move(callback));
done.WaitForNotification();
}
} // namespace
} // namespace test
} // namespace quiche