Add Privacy Pass token generation to BlindSignAuth and test clients. PiperOrigin-RevId: 572689274
diff --git a/quiche/blind_sign_auth/blind_sign_auth.cc b/quiche/blind_sign_auth/blind_sign_auth.cc index 0c8ea53..fbfd429 100644 --- a/quiche/blind_sign_auth/blind_sign_auth.cc +++ b/quiche/blind_sign_auth/blind_sign_auth.cc
@@ -4,7 +4,10 @@ #include "quiche/blind_sign_auth/blind_sign_auth.h" +#include <algorithm> #include <cstddef> +#include <cstdint> +#include <cstring> #include <memory> #include <string> #include <utility> @@ -15,7 +18,12 @@ #include "absl/status/statusor.h" #include "absl/strings/escaping.h" #include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" #include "absl/types/span.h" +#include "anonymous_tokens/cpp/client/anonymous_tokens_rsa_bssa_client.h" +#include "anonymous_tokens/cpp/crypto/crypto_utils.h" +#include "anonymous_tokens/cpp/privacy_pass/rsa_bssa_public_metadata_client.h" +#include "anonymous_tokens/cpp/privacy_pass/token_encodings.h" #include "anonymous_tokens/cpp/shared/proto_utils.h" #include "quiche/blind_sign_auth/blind_sign_auth_protos.h" #include "quiche/blind_sign_auth/blind_sign_http_response.h" @@ -31,16 +39,13 @@ return value == 0 ? "" : absl::StrCat(value); } +constexpr absl::string_view kIssuerHostname = + "https://ipprotection-ppissuer.googleapis.com"; + } // namespace void BlindSignAuth::GetTokens(std::string oauth_token, int num_tokens, SignedTokenCallback callback) { - // Check whether Privacy Pass crypto is enabled. - if (auth_options_.enable_privacy_pass()) { - std::move(callback)( - absl::UnimplementedError("Privacy Pass is not supported.")); - return; - } // Create GetInitialData RPC. privacy::ppn::GetInitialDataRequest request; request.set_use_attestation(false); @@ -66,7 +71,7 @@ std::move(callback)(response.status()); return; } - absl::StatusCode code = HttpCodeToStatusCode(response.value().status_code()); + absl::StatusCode code = HttpCodeToStatusCode(response->status_code()); if (code != absl::StatusCode::kOk) { std::string message = absl::StrCat("GetInitialDataRequest failed with code: ", code); @@ -76,7 +81,7 @@ } // Parse GetInitialDataResponse. privacy::ppn::GetInitialDataResponse initial_data_response; - if (!initial_data_response.ParseFromString(response.value().body())) { + if (!initial_data_response.ParseFromString(response->body())) { QUICHE_LOG(WARNING) << "Failed to parse GetInitialDataResponse"; std::move(callback)( absl::InternalError("Failed to parse GetInitialDataResponse")); @@ -93,7 +98,148 @@ return; } - // Create RSA BSSA client. + // Create token signing requests. + bool use_privacy_pass_client = + initial_data_response.has_privacy_pass_data() && + auth_options_.enable_privacy_pass(); + + if (use_privacy_pass_client) { + QUICHE_DVLOG(1) << "Using Privacy Pass client"; + GeneratePrivacyPassTokens( + initial_data_response, *public_metadata_expiry_time, + std::move(oauth_token), num_tokens, std::move(callback)); + } else { + QUICHE_DVLOG(1) << "Using public metadata client"; + GenerateRsaBssaTokens(initial_data_response, *public_metadata_expiry_time, + std::move(oauth_token), num_tokens, + std::move(callback)); + } +} + +void BlindSignAuth::GeneratePrivacyPassTokens( + privacy::ppn::GetInitialDataResponse initial_data_response, + absl::Time public_metadata_expiry_time, std::string oauth_token, + int num_tokens, SignedTokenCallback callback) { + // Set up values used in the token generation loop. + anonymous_tokens::RSAPublicKey public_key_proto; + if (!public_key_proto.ParseFromString( + initial_data_response.at_public_metadata_public_key() + .serialized_public_key())) { + std::move(callback)( + absl::InvalidArgumentError("Failed to parse Privacy Pass public key")); + return; + } + absl::StatusOr<bssl::UniquePtr<RSA>> bssl_rsa_key = + anonymous_tokens::CreatePublicKeyRSA( + public_key_proto.n(), public_key_proto.e()); + if (!bssl_rsa_key.ok()) { + std::move(callback)(bssl_rsa_key.status()); + return; + } + absl::StatusOr<anonymous_tokens::Extensions> extensions = + anonymous_tokens::DecodeExtensions( + initial_data_response.privacy_pass_data() + .public_metadata_extensions()); + if (!extensions.ok()) { + QUICHE_LOG(WARNING) << "Failed to decode extensions: " + << extensions.status(); + std::move(callback)(extensions.status()); + return; + } + + // Create token challenge. + anonymous_tokens::TokenChallenge challenge; + challenge.issuer_name = kIssuerHostname; + absl::StatusOr<std::string> token_challenge = + anonymous_tokens::MarshalTokenChallenge(challenge); + if (!token_challenge.ok()) { + QUICHE_LOG(WARNING) << "Failed to marshal token challenge: " + << token_challenge.status(); + std::move(callback)(token_challenge.status()); + return; + } + + QuicheRandom* random = QuicheRandom::GetInstance(); + // Create vector of Privacy Pass clients, one for each token. + std::vector<anonymous_tokens::ExtendedTokenRequest> + extended_token_requests; + std::vector<std::unique_ptr<anonymous_tokens:: + PrivacyPassRsaBssaPublicMetadataClient>> + privacy_pass_clients; + std::vector<std::string> privacy_pass_blinded_tokens; + + for (int i = 0; i < num_tokens; i++) { + // Create client. + auto client = anonymous_tokens:: + PrivacyPassRsaBssaPublicMetadataClient::Create(*bssl_rsa_key.value()); + if (!client.ok()) { + QUICHE_LOG(WARNING) << "Failed to create Privacy Pass client: " + << client.status(); + std::move(callback)(client.status()); + return; + } + + // Create nonce. + std::string nonce_rand(32, '\0'); + random->RandBytes(nonce_rand.data(), nonce_rand.size()); + + // Create token request. + absl::StatusOr<anonymous_tokens::ExtendedTokenRequest> + extended_token_request = client.value()->CreateTokenRequest( + *token_challenge, nonce_rand, + initial_data_response.privacy_pass_data().token_key_id(), + *extensions); + if (!extended_token_request.ok()) { + QUICHE_LOG(WARNING) << "Failed to create ExtendedTokenRequest: " + << extended_token_request.status(); + std::move(callback)(extended_token_request.status()); + return; + } + privacy_pass_clients.push_back(*std::move(client)); + extended_token_requests.push_back(*extended_token_request); + privacy_pass_blinded_tokens.push_back(absl::Base64Escape( + extended_token_request->request.blinded_token_request)); + } + + privacy::ppn::AuthAndSignRequest sign_request; + sign_request.set_oauth_token(oauth_token); + sign_request.set_service_type("chromeipblinding"); + sign_request.set_key_type(privacy::ppn::AT_PUBLIC_METADATA_KEY_TYPE); + sign_request.set_key_version( + initial_data_response.at_public_metadata_public_key().key_version()); + sign_request.mutable_blinded_token()->Assign( + privacy_pass_blinded_tokens.begin(), privacy_pass_blinded_tokens.end()); + sign_request.mutable_public_metadata_extensions()->assign( + initial_data_response.privacy_pass_data().public_metadata_extensions()); + // TODO(b/295924807): deprecate this option after AT server defaults to it + sign_request.set_do_not_use_rsa_public_exponent(true); + + absl::StatusOr<anonymous_tokens::AnonymousTokensUseCase> + use_case = anonymous_tokens::ParseUseCase( + initial_data_response.at_public_metadata_public_key().use_case()); + if (!use_case.ok()) { + QUICHE_LOG(WARNING) << "Failed to parse use case: " << use_case.status(); + std::move(callback)(use_case.status()); + return; + } + + BlindSignHttpCallback auth_and_sign_callback = + absl::bind_front(&BlindSignAuth::PrivacyPassAuthAndSignCallback, this, + std::move(initial_data_response.privacy_pass_data() + .public_metadata_extensions()), + public_metadata_expiry_time, *use_case, + std::move(privacy_pass_clients), std::move(callback)); + // TODO(b/304811277): remove other usages of string.data() + http_fetcher_->DoRequest(BlindSignHttpRequestType::kAuthAndSign, oauth_token, + sign_request.SerializeAsString(), + std::move(auth_and_sign_callback)); +} + +void BlindSignAuth::GenerateRsaBssaTokens( + privacy::ppn::GetInitialDataResponse initial_data_response, + absl::Time public_metadata_expiry_time, std::string oauth_token, + int num_tokens, SignedTokenCallback callback) { + // Create public metadata client. auto bssa_client = anonymous_tokens::AnonymousTokensRsaBssaClient:: Create(initial_data_response.at_public_metadata_public_key()); @@ -106,10 +252,10 @@ // Create plaintext tokens. // Client blinds plaintext tokens (random 32-byte strings) in CreateRequest. + QuicheRandom* random = QuicheRandom::GetInstance(); std::vector< anonymous_tokens::PlaintextMessageWithPublicMetadata> plaintext_tokens; - QuicheRandom* random = QuicheRandom::GetInstance(); for (int i = 0; i < num_tokens; i++) { // Create random 32-byte string prefixed with "blind:". anonymous_tokens::PlaintextMessageWithPublicMetadata @@ -134,10 +280,9 @@ plaintext_message.set_public_metadata(key); plaintext_tokens.push_back(plaintext_message); } - absl::StatusOr< anonymous_tokens::AnonymousTokensSignRequest> - at_sign_request = bssa_client.value()->CreateRequest(plaintext_tokens); + at_sign_request = *bssa_client.value()->CreateRequest(plaintext_tokens); if (!at_sign_request.ok()) { QUICHE_LOG(WARNING) << "Failed to create AT Sign Request: " << at_sign_request.status(); @@ -165,8 +310,8 @@ initial_data_response.public_metadata_info(); BlindSignHttpCallback auth_and_sign_callback = absl::bind_front( &BlindSignAuth::AuthAndSignCallback, this, public_metadata_info, - public_metadata_expiry_time.value(), *at_sign_request, - *std::move(bssa_client), std::move(callback)); + public_metadata_expiry_time, *at_sign_request, *std::move(bssa_client), + std::move(callback)); http_fetcher_->DoRequest(BlindSignHttpRequestType::kAuthAndSign, oauth_token.data(), sign_request.SerializeAsString(), std::move(auth_and_sign_callback)); @@ -188,23 +333,21 @@ std::move(callback)(response.status()); return; } - absl::StatusCode code = HttpCodeToStatusCode(response.value().status_code()); + absl::StatusCode code = HttpCodeToStatusCode(response->status_code()); if (code != absl::StatusCode::kOk) { std::string message = absl::StrCat("AuthAndSign failed with code: ", code); QUICHE_LOG(WARNING) << message; std::move(callback)(absl::Status(code, message)); return; } - // Decode AuthAndSignResponse. privacy::ppn::AuthAndSignResponse sign_response; - if (!sign_response.ParseFromString(response.value().body())) { + if (!sign_response.ParseFromString(response->body())) { QUICHE_LOG(WARNING) << "Failed to parse AuthAndSignResponse"; std::move(callback)( absl::InternalError("Failed to parse AuthAndSignResponse")); return; } - // Create vector of unblinded anonymous tokens. anonymous_tokens::AnonymousTokensSignResponse at_sign_response; @@ -288,6 +431,88 @@ std::move(callback)(absl::Span<BlindSignToken>(tokens_vec)); } +void BlindSignAuth::PrivacyPassAuthAndSignCallback( + std::string encoded_extensions, absl::Time public_key_expiry_time, + anonymous_tokens::AnonymousTokensUseCase use_case, + std::vector<std::unique_ptr<anonymous_tokens:: + PrivacyPassRsaBssaPublicMetadataClient>> + privacy_pass_clients, + SignedTokenCallback callback, + absl::StatusOr<BlindSignHttpResponse> response) { + // Validate response. + if (!response.ok()) { + QUICHE_LOG(WARNING) << "AuthAndSign failed: " << response.status(); + std::move(callback)(response.status()); + return; + } + absl::StatusCode code = HttpCodeToStatusCode(response->status_code()); + if (code != absl::StatusCode::kOk) { + std::string message = absl::StrCat("AuthAndSign failed with code: ", code); + QUICHE_LOG(WARNING) << message; + std::move(callback)(absl::Status(code, message)); + return; + } + + // Decode AuthAndSignResponse. + privacy::ppn::AuthAndSignResponse sign_response; + if (!sign_response.ParseFromString(response->body())) { + QUICHE_LOG(WARNING) << "Failed to parse AuthAndSignResponse"; + std::move(callback)( + absl::InternalError("Failed to parse AuthAndSignResponse")); + return; + } + if (static_cast<size_t>(sign_response.blinded_token_signature_size()) != + privacy_pass_clients.size()) { + QUICHE_LOG(WARNING) << "Number of signatures does not equal number of " + "Privacy Pass tokens sent"; + std::move(callback)( + absl::InternalError("Number of signatures does not equal number of " + "Privacy Pass tokens sent")); + return; + } + + // Create tokens using blinded signatures. + std::vector<BlindSignToken> tokens_vec; + for (int i = 0; i < sign_response.blinded_token_signature_size(); i++) { + std::string unescaped_blinded_sig; + if (!absl::Base64Unescape(sign_response.blinded_token_signature()[i], + &unescaped_blinded_sig)) { + QUICHE_LOG(WARNING) << "Failed to unescape blinded signature"; + std::move(callback)( + absl::InternalError("Failed to unescape blinded signature")); + return; + } + + absl::StatusOr<anonymous_tokens::Token> token = + privacy_pass_clients[i]->FinalizeToken(unescaped_blinded_sig); + if (!token.ok()) { + QUICHE_LOG(WARNING) << "Failed to finalize token: " << token.status(); + std::move(callback)(token.status()); + return; + } + + absl::StatusOr<std::string> marshaled_token = + anonymous_tokens::MarshalToken(*token); + if (!marshaled_token.ok()) { + QUICHE_LOG(WARNING) << "Failed to marshal token: " + << marshaled_token.status(); + std::move(callback)(marshaled_token.status()); + return; + } + + privacy::ppn::PrivacyPassTokenData privacy_pass_token_data; + privacy_pass_token_data.mutable_token()->assign( + absl::Base64Escape(*marshaled_token)); + privacy_pass_token_data.mutable_encoded_extensions()->assign( + absl::Base64Escape(encoded_extensions)); + privacy_pass_token_data.set_use_case_override(use_case); + tokens_vec.push_back(BlindSignToken{ + privacy_pass_token_data.SerializeAsString(), public_key_expiry_time}); + } + + std::move(callback)(absl::Span<BlindSignToken>(tokens_vec)); +} + absl::Status BlindSignAuth::FingerprintPublicMetadata( const privacy::ppn::PublicMetadata& metadata, uint64_t* fingerprint) { const EVP_MD* hasher = EVP_sha256();
diff --git a/quiche/blind_sign_auth/blind_sign_auth.h b/quiche/blind_sign_auth/blind_sign_auth.h index d678afd..43fd604 100644 --- a/quiche/blind_sign_auth/blind_sign_auth.h +++ b/quiche/blind_sign_auth/blind_sign_auth.h
@@ -12,6 +12,7 @@ #include "absl/status/statusor.h" #include "absl/time/time.h" #include "anonymous_tokens/cpp/client/anonymous_tokens_rsa_bssa_client.h" +#include "anonymous_tokens/cpp/privacy_pass/rsa_bssa_public_metadata_client.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" @@ -41,6 +42,14 @@ void GetInitialDataCallback(std::string oauth_token, int num_tokens, SignedTokenCallback callback, absl::StatusOr<BlindSignHttpResponse> response); + void GeneratePrivacyPassTokens( + privacy::ppn::GetInitialDataResponse initial_data_response, + absl::Time public_metadata_expiry_time, std::string oauth_token, + int num_tokens, SignedTokenCallback callback); + void GenerateRsaBssaTokens( + privacy::ppn::GetInitialDataResponse initial_data_response, + absl::Time public_metadata_expiry_time, std::string oauth_token, + int num_tokens, SignedTokenCallback callback); void AuthAndSignCallback( privacy::ppn::PublicMetadataInfo public_metadata_info, absl::Time public_key_expiry_time, @@ -51,6 +60,14 @@ bssa_client, SignedTokenCallback callback, absl::StatusOr<BlindSignHttpResponse> response); + void PrivacyPassAuthAndSignCallback( + std::string encoded_extensions, absl::Time public_key_expiry_time, + anonymous_tokens::AnonymousTokensUseCase use_case, + std::vector<std::unique_ptr<anonymous_tokens:: + PrivacyPassRsaBssaPublicMetadataClient>> + privacy_pass_clients, + SignedTokenCallback callback, + absl::StatusOr<BlindSignHttpResponse> response); absl::Status FingerprintPublicMetadata( const privacy::ppn::PublicMetadata& metadata, uint64_t* fingerprint); absl::StatusCode HttpCodeToStatusCode(int http_code);
diff --git a/quiche/blind_sign_auth/blind_sign_auth_test.cc b/quiche/blind_sign_auth/blind_sign_auth_test.cc index c2e33cd..9f61ee4 100644 --- a/quiche/blind_sign_auth/blind_sign_auth_test.cc +++ b/quiche/blind_sign_auth/blind_sign_auth_test.cc
@@ -4,6 +4,7 @@ #include "quiche/blind_sign_auth/blind_sign_auth.h" +#include <cstdint> #include <memory> #include <string> #include <utility> @@ -13,9 +14,12 @@ #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/proto_utils.h" #include "anonymous_tokens/cpp/testing/utils.h" #include "openssl/base.h" +#include "openssl/digest.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" @@ -38,28 +42,48 @@ class BlindSignAuthTest : public QuicheTest { protected: void SetUp() override { - // Create public key. - auto keypair = 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 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)); - // Create fake GetInitialDataRequest. + 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); - // Create fake public key response. + // Create fake GetInitialDataResponse. privacy::ppn::GetInitialDataResponse fake_get_initial_data_response; - 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; + public_key_proto_; // Create public metadata info. privacy::ppn::PublicMetadata::Location location; @@ -76,6 +100,57 @@ 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.country_code = "US"; + 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); + + 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); @@ -86,12 +161,10 @@ void TearDown() override { blind_sign_auth_.reset(nullptr); - keypair_.first.reset(nullptr); - keypair_.second.Clear(); } public: - void CreateSignResponse(const std::string& body) { + void CreateSignResponse(const std::string& body, bool use_privacy_pass) { privacy::ppn::AuthAndSignRequest request; ASSERT_TRUE(request.ParseFromString(body)); @@ -102,27 +175,45 @@ // 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()); + 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)); - absl::StatusOr<std::string> serialized_token = - anonymous_tokens::TestSign(decoded_blinded_token, - keypair_.first.get()); - QUICHE_EXPECT_OK(serialized_token); - response.add_blinded_token_signature( - absl::Base64Escape(*serialized_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(const absl::Span<BlindSignToken>& tokens) { + 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)); @@ -133,7 +224,7 @@ 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()); + public_key_proto_.key_version()); EXPECT_NE(spend_token_data.use_case(), anonymous_tokens::AnonymousTokensUseCase:: ANONYMOUS_TOKENS_USE_CASE_UNDEFINED); @@ -141,11 +232,28 @@ } } + 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::Base64Unescape(privacy_pass_token_data.token(), + &decoded_token)); + std::string decoded_extensions; + ASSERT_TRUE(absl::Base64Unescape( + privacy_pass_token_data.encoded_extensions(), &decoded_extensions)); + } + } + MockBlindSignHttpInterface mock_http_interface_; std::unique_ptr<BlindSignAuth> blind_sign_auth_; - std::pair<bssl::UniquePtr<RSA>, - anonymous_tokens::RSABlindSignaturePublicKey> - keypair_; + 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_; @@ -176,7 +284,7 @@ .Times(1) .WillOnce(Invoke([this](Unused, Unused, const std::string& body, BlindSignHttpCallback callback) { - CreateSignResponse(body); + CreateSignResponse(body, false); BlindSignHttpResponse http_response( 200, sign_response_.SerializeAsString()); std::move(callback)(http_response); @@ -275,7 +383,7 @@ .Times(1) .WillOnce(Invoke([this](Unused, Unused, const std::string& body, BlindSignHttpCallback callback) { - CreateSignResponse(body); + CreateSignResponse(body, false); // Add an invalid signature that can't be Base64 decoded. sign_response_.add_blinded_token_signature("invalid_signature%"); BlindSignHttpResponse http_response( @@ -295,17 +403,87 @@ done.WaitForNotification(); } -TEST_F(BlindSignAuthTest, TestGetTokensFailedPrivacyPass) { +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, 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::kUnimplemented); + EXPECT_THAT(tokens.status().code(), absl::StatusCode::kInvalidArgument); done.Notify(); }; blind_sign_auth_->GetTokens(oauth_token_, num_tokens, std::move(callback));
diff --git a/quiche/blind_sign_auth/proto/spend_token_data.proto b/quiche/blind_sign_auth/proto/spend_token_data.proto index 90b490d..694f189 100644 --- a/quiche/blind_sign_auth/proto/spend_token_data.proto +++ b/quiche/blind_sign_auth/proto/spend_token_data.proto
@@ -39,3 +39,13 @@ // or public_metadata. bytes binary_public_metadata = 7; } + +message PrivacyPassTokenData { + // The base64 encoded Privacy Pass token. + string token = 1; + // The base64 encoded, Privacy Pass compliant binary public metadata. + string encoded_extensions = 2; + // Populate only for testing, this will affect which public key is used. + anonymous_tokens.AnonymousTokensUseCase use_case_override = + 3; +}