blob: 3a5d84288c041f95605c2a4e8a93af35675b51b0 [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 <functional>
#include <memory>
#include <string>
#include <utility>
#include "quiche/blind_sign_auth/proto/timestamp.pb.h"
#include "quiche/blind_sign_auth/proto/auth_and_sign.pb.h"
#include "quiche/blind_sign_auth/proto/get_initial_data.pb.h"
#include "quiche/blind_sign_auth/proto/key_services.pb.h"
#include "quiche/blind_sign_auth/proto/public_metadata.pb.h"
#include "quiche/blind_sign_auth/proto/spend_token_data.pb.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/escaping.h"
#include "absl/strings/string_view.h"
#include "quiche/blind_sign_auth/anonymous_tokens/cpp/testing/utils.h"
#include "quiche/blind_sign_auth/anonymous_tokens/proto/anonymous_tokens.pb.h"
#include "openssl/base.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::InvokeArgument;
using ::testing::StartsWith;
using ::testing::Unused;
class BlindSignAuthTest : public QuicheTest {
protected:
void SetUp() override {
// Create public key.
auto keypair = private_membership::anonymous_tokens::CreateTestKey();
if (!keypair.ok()) {
return;
}
keypair_ = *std::move(keypair);
keypair_.second.set_key_version(1);
keypair_.second.set_use_case("TEST_USE_CASE");
// Create fake 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);
// Create fake public key response.
privacy::ppn::GetInitialDataResponse fake_get_initial_data_response;
private_membership::anonymous_tokens::RSABlindSignaturePublicKey public_key;
ASSERT_TRUE(
public_key.ParseFromString(keypair_.second.SerializeAsString()));
*fake_get_initial_data_response.mutable_at_public_metadata_public_key() =
public_key;
// Create public metadata info.
std::string public_metadata_str = R"pb(
public_metadata {
exit_location { country: "US" }
service_type: "chromeipblinding"
expiration { seconds: 3600 }
}
validation_version: 1
)pb";
privacy::ppn::PublicMetadata::Location location;
location.set_country("US");
quiche::protobuf::Timestamp expiration;
expiration.set_seconds(3600);
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;
blind_sign_auth_ = std::make_unique<BlindSignAuth>(&mock_http_interface_);
}
void TearDown() override {
blind_sign_auth_.reset(nullptr);
keypair_.first.reset(nullptr);
keypair_.second.Clear();
}
public:
void CreateSignResponse(const std::string& body) {
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.public_metadata_info().SerializeAsString(),
public_metadata_info_.SerializeAsString());
EXPECT_EQ(request.key_version(), keypair_.second.key_version());
// 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));
absl::StatusOr<std::string> serialized_token =
private_membership::anonymous_tokens::TestSign(decoded_blinded_token,
keypair_.first.get());
QUICHE_EXPECT_OK(serialized_token);
response.add_blinded_token_signature(
absl::Base64Escape(*serialized_token));
}
sign_response_ = response;
}
void ValidateGetTokensOutput(const 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(),
keypair_.second.key_version());
EXPECT_NE(spend_token_data.use_case(),
private_membership::anonymous_tokens::AnonymousTokensUseCase::
ANONYMOUS_TOKENS_USE_CASE_UNDEFINED);
EXPECT_NE(spend_token_data.message_mask(), "");
}
}
MockBlindSignHttpInterface mock_http_interface_;
std::unique_ptr<BlindSignAuth> blind_sign_auth_;
std::pair<bssl::UniquePtr<RSA>,
private_membership::anonymous_tokens::RSABlindSignaturePublicKey>
keypair_;
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("/v1/getInitialData"), Eq(oauth_token_),
Eq(expected_get_initial_data_request_.SerializeAsString()),
_))
.Times(1)
.WillOnce(InvokeArgument<3>(fake_public_key_response));
EXPECT_CALL(mock_http_interface_, DoRequest(Eq("/v1/authWithHeaderCreds"),
Eq(oauth_token_), _, _))
.Times(1)
.WillOnce(Invoke(
[this](Unused, Unused, const std::string& body,
std::function<void(absl::StatusOr<BlindSignHttpResponse>)>
callback) {
CreateSignResponse(body);
BlindSignHttpResponse http_response(
200, sign_response_.SerializeAsString());
callback(http_response);
}));
}
int num_tokens = 1;
QuicheNotification done;
std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)> 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, callback);
done.WaitForNotification();
}
TEST_F(BlindSignAuthTest, TestGetTokensFailedNetworkError) {
EXPECT_CALL(mock_http_interface_,
DoRequest(Eq("/v1/getInitialData"), Eq(oauth_token_), _, _))
.Times(1)
.WillOnce(
InvokeArgument<3>(absl::InternalError("Failed to create socket")));
EXPECT_CALL(mock_http_interface_,
DoRequest(Eq("/v1/authWithHeaderCreds"), _, _, _))
.Times(0);
int num_tokens = 1;
QuicheNotification done;
std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)> 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, 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("/v1/getInitialData"), Eq(oauth_token_),
Eq(expected_get_initial_data_request_.SerializeAsString()), _))
.Times(1)
.WillOnce(InvokeArgument<3>(fake_public_key_response));
EXPECT_CALL(mock_http_interface_,
DoRequest(Eq("/v1/authWithHeaderCreds"), _, _, _))
.Times(0);
int num_tokens = 1;
QuicheNotification done;
std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)> 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, 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("/v1/getInitialData"), Eq(oauth_token_),
Eq(expected_get_initial_data_request_.SerializeAsString()),
_))
.Times(1)
.WillOnce(InvokeArgument<3>(fake_public_key_response));
EXPECT_CALL(mock_http_interface_, DoRequest(Eq("/v1/authWithHeaderCreds"),
Eq(oauth_token_), _, _))
.Times(1)
.WillOnce(Invoke(
[this](Unused, Unused, const std::string& body,
std::function<void(absl::StatusOr<BlindSignHttpResponse>)>
callback) {
CreateSignResponse(body);
// 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());
callback(http_response);
}));
}
int num_tokens = 1;
QuicheNotification done;
std::function<void(absl::StatusOr<absl::Span<BlindSignToken>>)> 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, callback);
done.WaitForNotification();
}
} // namespace
} // namespace test
} // namespace quiche