Enabling rolled out flags. Autogenerated by list_feature_flags.py. PiperOrigin-RevId: 518062860
diff --git a/build/source_list.bzl b/build/source_list.bzl index e5aba7e..3d8f341 100644 --- a/build/source_list.bzl +++ b/build/source_list.bzl
@@ -1545,6 +1545,55 @@ "quic/qbone/qbone_stream.cc", "quic/qbone/qbone_stream_test.cc", ] +blind_sign_auth_hdrs = [ + "blind_sign_auth/anonymous_tokens/cpp/client/anonymous_tokens_rsa_bssa_client.h", + "blind_sign_auth/anonymous_tokens/cpp/crypto/blinder.h", + "blind_sign_auth/anonymous_tokens/cpp/crypto/constants.h", + "blind_sign_auth/anonymous_tokens/cpp/crypto/crypto_utils.h", + "blind_sign_auth/anonymous_tokens/cpp/crypto/proto_utils.h", + "blind_sign_auth/anonymous_tokens/cpp/crypto/public_metadata_crypto_utils.h", + "blind_sign_auth/anonymous_tokens/cpp/crypto/rsa_blinder.h", + "blind_sign_auth/anonymous_tokens/cpp/crypto/status_utils.h", + "blind_sign_auth/anonymous_tokens/cpp/crypto/testing_utils.h", + "blind_sign_auth/blind_sign_auth.h", + "blind_sign_auth/blind_sign_auth_interface.h", + "blind_sign_auth/blind_sign_http_interface.h", + "blind_sign_auth/blind_sign_http_response.h", + "blind_sign_auth/cached_blind_sign_auth.h", + "blind_sign_auth/test_tools/mock_blind_sign_auth_interface.h", + "blind_sign_auth/test_tools/mock_blind_sign_http_interface.h", +] +blind_sign_auth_srcs = [ + "blind_sign_auth/anonymous_tokens/cpp/client/anonymous_tokens_rsa_bssa_client.cc", + "blind_sign_auth/anonymous_tokens/cpp/crypto/crypto_utils.cc", + "blind_sign_auth/anonymous_tokens/cpp/crypto/proto_utils.cc", + "blind_sign_auth/anonymous_tokens/cpp/crypto/public_metadata_crypto_utils.cc", + "blind_sign_auth/anonymous_tokens/cpp/crypto/rsa_blinder.cc", + "blind_sign_auth/anonymous_tokens/cpp/crypto/testing_utils.cc", + "blind_sign_auth/blind_sign_auth.cc", + "blind_sign_auth/cached_blind_sign_auth.cc", +] +blind_sign_auth_tests_hdrs = [ + +] +blind_sign_auth_tests_srcs = [ + "blind_sign_auth/anonymous_tokens/cpp/client/anonymous_tokens_rsa_bssa_client_test.cc", + "blind_sign_auth/anonymous_tokens/cpp/crypto/at_crypto_utils_test.cc", + "blind_sign_auth/anonymous_tokens/cpp/crypto/proto_utils_test.cc", + "blind_sign_auth/anonymous_tokens/cpp/crypto/public_metadata_crypto_utils_test.cc", + "blind_sign_auth/anonymous_tokens/cpp/crypto/rsa_blinder_test.cc", + "blind_sign_auth/blind_sign_auth_test.cc", + "blind_sign_auth/cached_blind_sign_auth_test.cc", +] +protobuf_blind_sign_auth = [ + "blind_sign_auth/anonymous_tokens/proto/anonymous_tokens.proto", + "blind_sign_auth/proto/attestation.proto", + "blind_sign_auth/proto/auth_and_sign.proto", + "blind_sign_auth/proto/get_initial_data.proto", + "blind_sign_auth/proto/key_services.proto", + "blind_sign_auth/proto/public_metadata.proto", + "blind_sign_auth/proto/spend_token_data.proto", +] libevent_hdrs = [ "quic/bindings/quic_libevent.h", ]
diff --git a/build/source_list.gni b/build/source_list.gni index cf1f119..5e70945 100644 --- a/build/source_list.gni +++ b/build/source_list.gni
@@ -1545,6 +1545,55 @@ "src/quiche/quic/qbone/qbone_stream.cc", "src/quiche/quic/qbone/qbone_stream_test.cc", ] +blind_sign_auth_hdrs = [ + "src/quiche/blind_sign_auth/anonymous_tokens/cpp/client/anonymous_tokens_rsa_bssa_client.h", + "src/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/blinder.h", + "src/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/constants.h", + "src/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/crypto_utils.h", + "src/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/proto_utils.h", + "src/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/public_metadata_crypto_utils.h", + "src/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/rsa_blinder.h", + "src/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/status_utils.h", + "src/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/testing_utils.h", + "src/quiche/blind_sign_auth/blind_sign_auth.h", + "src/quiche/blind_sign_auth/blind_sign_auth_interface.h", + "src/quiche/blind_sign_auth/blind_sign_http_interface.h", + "src/quiche/blind_sign_auth/blind_sign_http_response.h", + "src/quiche/blind_sign_auth/cached_blind_sign_auth.h", + "src/quiche/blind_sign_auth/test_tools/mock_blind_sign_auth_interface.h", + "src/quiche/blind_sign_auth/test_tools/mock_blind_sign_http_interface.h", +] +blind_sign_auth_srcs = [ + "src/quiche/blind_sign_auth/anonymous_tokens/cpp/client/anonymous_tokens_rsa_bssa_client.cc", + "src/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/crypto_utils.cc", + "src/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/proto_utils.cc", + "src/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/public_metadata_crypto_utils.cc", + "src/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/rsa_blinder.cc", + "src/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/testing_utils.cc", + "src/quiche/blind_sign_auth/blind_sign_auth.cc", + "src/quiche/blind_sign_auth/cached_blind_sign_auth.cc", +] +blind_sign_auth_tests_hdrs = [ + +] +blind_sign_auth_tests_srcs = [ + "src/quiche/blind_sign_auth/anonymous_tokens/cpp/client/anonymous_tokens_rsa_bssa_client_test.cc", + "src/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/at_crypto_utils_test.cc", + "src/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/proto_utils_test.cc", + "src/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/public_metadata_crypto_utils_test.cc", + "src/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/rsa_blinder_test.cc", + "src/quiche/blind_sign_auth/blind_sign_auth_test.cc", + "src/quiche/blind_sign_auth/cached_blind_sign_auth_test.cc", +] +protobuf_blind_sign_auth = [ + "src/quiche/blind_sign_auth/anonymous_tokens/proto/anonymous_tokens.proto", + "src/quiche/blind_sign_auth/proto/attestation.proto", + "src/quiche/blind_sign_auth/proto/auth_and_sign.proto", + "src/quiche/blind_sign_auth/proto/get_initial_data.proto", + "src/quiche/blind_sign_auth/proto/key_services.proto", + "src/quiche/blind_sign_auth/proto/public_metadata.proto", + "src/quiche/blind_sign_auth/proto/spend_token_data.proto", +] libevent_hdrs = [ "src/quiche/quic/bindings/quic_libevent.h", ]
diff --git a/build/source_list.json b/build/source_list.json index 563c80f..22a67dc 100644 --- a/build/source_list.json +++ b/build/source_list.json
@@ -1544,6 +1544,55 @@ "quiche/quic/qbone/qbone_stream.cc", "quiche/quic/qbone/qbone_stream_test.cc" ], + "blind_sign_auth_hdrs": [ + "quiche/blind_sign_auth/anonymous_tokens/cpp/client/anonymous_tokens_rsa_bssa_client.h", + "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/blinder.h", + "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/constants.h", + "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/crypto_utils.h", + "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/proto_utils.h", + "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/public_metadata_crypto_utils.h", + "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/rsa_blinder.h", + "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/status_utils.h", + "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/testing_utils.h", + "quiche/blind_sign_auth/blind_sign_auth.h", + "quiche/blind_sign_auth/blind_sign_auth_interface.h", + "quiche/blind_sign_auth/blind_sign_http_interface.h", + "quiche/blind_sign_auth/blind_sign_http_response.h", + "quiche/blind_sign_auth/cached_blind_sign_auth.h", + "quiche/blind_sign_auth/test_tools/mock_blind_sign_auth_interface.h", + "quiche/blind_sign_auth/test_tools/mock_blind_sign_http_interface.h" + ], + "blind_sign_auth_srcs": [ + "quiche/blind_sign_auth/anonymous_tokens/cpp/client/anonymous_tokens_rsa_bssa_client.cc", + "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/crypto_utils.cc", + "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/proto_utils.cc", + "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/public_metadata_crypto_utils.cc", + "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/rsa_blinder.cc", + "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/testing_utils.cc", + "quiche/blind_sign_auth/blind_sign_auth.cc", + "quiche/blind_sign_auth/cached_blind_sign_auth.cc" + ], + "blind_sign_auth_tests_hdrs": [ + + ], + "blind_sign_auth_tests_srcs": [ + "quiche/blind_sign_auth/anonymous_tokens/cpp/client/anonymous_tokens_rsa_bssa_client_test.cc", + "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/at_crypto_utils_test.cc", + "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/proto_utils_test.cc", + "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/public_metadata_crypto_utils_test.cc", + "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/rsa_blinder_test.cc", + "quiche/blind_sign_auth/blind_sign_auth_test.cc", + "quiche/blind_sign_auth/cached_blind_sign_auth_test.cc" + ], + "protobuf_blind_sign_auth": [ + "quiche/blind_sign_auth/anonymous_tokens/proto/anonymous_tokens.proto", + "quiche/blind_sign_auth/proto/attestation.proto", + "quiche/blind_sign_auth/proto/auth_and_sign.proto", + "quiche/blind_sign_auth/proto/get_initial_data.proto", + "quiche/blind_sign_auth/proto/key_services.proto", + "quiche/blind_sign_auth/proto/public_metadata.proto", + "quiche/blind_sign_auth/proto/spend_token_data.proto" + ], "libevent_hdrs": [ "quiche/quic/bindings/quic_libevent.h" ],
diff --git a/quiche/blind_sign_auth/anonymous_tokens/cpp/client/anonymous_tokens_rsa_bssa_client.cc b/quiche/blind_sign_auth/anonymous_tokens/cpp/client/anonymous_tokens_rsa_bssa_client.cc new file mode 100644 index 0000000..15584e1 --- /dev/null +++ b/quiche/blind_sign_auth/anonymous_tokens/cpp/client/anonymous_tokens_rsa_bssa_client.cc
@@ -0,0 +1,271 @@ +// Copyright 2023 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 "quiche/blind_sign_auth/anonymous_tokens/cpp/client/anonymous_tokens_rsa_bssa_client.h" + +#include <cstddef> +#include <memory> +#include <optional> +#include <string> +#include <utility> +#include <vector> + +#include "absl/container/flat_hash_set.h" +#include "absl/status/status.h" +#include "absl/time/time.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/constants.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/crypto_utils.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/proto_utils.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/status_utils.h" +#include "quiche/blind_sign_auth/anonymous_tokens/proto/anonymous_tokens.pb.h" +#include "openssl/rand.h" + +namespace private_membership { +namespace anonymous_tokens { + +namespace { + +absl::Status ValidityChecksForClientCreation( + const RSABlindSignaturePublicKey& public_key) { + // Basic validity checks. + if (!ParseUseCase(public_key.use_case()).ok()) { + return absl::InvalidArgumentError("Invalid use case for public key."); + } else if (public_key.key_version() <= 0) { + return absl::InvalidArgumentError( + "Key version cannot be zero or negative."); + } else if (public_key.key_size() < 256) { + return absl::InvalidArgumentError( + "Key modulus size cannot be less than 256 bytes."); + } else if (public_key.mask_gen_function() == AT_TEST_MGF || + public_key.mask_gen_function() == AT_MGF_UNDEFINED) { + return absl::InvalidArgumentError("Unknown or unacceptable mgf1 hash."); + } else if (public_key.sig_hash_type() == AT_TEST_HASH_TYPE || + public_key.sig_hash_type() == AT_HASH_TYPE_UNDEFINED) { + return absl::InvalidArgumentError( + "Unknown or unacceptable signature hash."); + } else if (public_key.salt_length() <= 0) { + return absl::InvalidArgumentError( + "Non-positive salt length is not allowed."); + } else if (public_key.mask_gen_function() == AT_TEST_MGF || + public_key.mask_gen_function() == AT_MGF_UNDEFINED) { + return absl::InvalidArgumentError("Message mask type must be defined."); + } else if (public_key.message_mask_size() <= 0) { + return absl::InvalidArgumentError("Message mask size must be positive."); + } + + RSAPublicKey rsa_public_key; + if (!rsa_public_key.ParseFromString(public_key.serialized_public_key())) { + return absl::InvalidArgumentError("Public key is malformed."); + } + if (rsa_public_key.n().size() != static_cast<size_t>(public_key.key_size())) { + return absl::InvalidArgumentError( + "Public key size does not match key size."); + } + return absl::OkStatus(); +} + +absl::Status CheckPublicKeyValidity( + const RSABlindSignaturePublicKey& public_key) { + absl::Time time_now = absl::Now(); + ANON_TOKENS_ASSIGN_OR_RETURN( + absl::Time start_time, + TimeFromProto(public_key.key_validity_start_time())); + if (start_time > time_now) { + return absl::FailedPreconditionError("Key is not valid yet."); + } + if (public_key.has_expiration_time()) { + ANON_TOKENS_ASSIGN_OR_RETURN(absl::Time expiration_time, + TimeFromProto(public_key.expiration_time())); + if (expiration_time <= time_now) { + return absl::FailedPreconditionError("Key is already expired."); + } + } + return absl::OkStatus(); +} + +absl::StatusOr<std::string> GenerateMask( + const RSABlindSignaturePublicKey& public_key) { + std::string mask; + if (public_key.message_mask_type() == AT_MESSAGE_MASK_CONCAT && + public_key.message_mask_size() >= kRsaMessageMaskSizeInBytes32) { + mask = std::string(public_key.message_mask_size(), '\0'); + RAND_bytes(reinterpret_cast<uint8_t*>(mask.data()), mask.size()); + } else { + return absl::InvalidArgumentError( + "Undefined or unsupported message mask type."); + } + return mask; +} + +} // namespace + +AnonymousTokensRsaBssaClient::AnonymousTokensRsaBssaClient( + const RSABlindSignaturePublicKey& public_key) + : public_key_(public_key) {} + +absl::StatusOr<std::unique_ptr<AnonymousTokensRsaBssaClient>> +AnonymousTokensRsaBssaClient::Create( + const RSABlindSignaturePublicKey& public_key) { + ANON_TOKENS_RETURN_IF_ERROR(ValidityChecksForClientCreation(public_key)); + return absl::WrapUnique(new AnonymousTokensRsaBssaClient(public_key)); +} + +// TODO(b/261866075): Offer an API to simply return bytes of blinded requests. +absl::StatusOr<AnonymousTokensSignRequest> +AnonymousTokensRsaBssaClient::CreateRequest( + const std::vector<PlaintextMessageWithPublicMetadata>& inputs) { + if (inputs.empty()) { + return absl::InvalidArgumentError("Cannot create an empty request."); + } else if (!blinding_info_map_.empty()) { + return absl::FailedPreconditionError( + "Blind signature request already created."); + } + + ANON_TOKENS_RETURN_IF_ERROR(CheckPublicKeyValidity(public_key_)); + + AnonymousTokensSignRequest request; + for (const PlaintextMessageWithPublicMetadata& input : inputs) { + if (input.plaintext_message().empty()) { + return absl::InvalidArgumentError( + "Cannot send an empty message to sign."); + } + + // Generate nonce and masked message. For more details, see + // https://datatracker.ietf.org/doc/draft-irtf-cfrg-rsa-blind-signatures/ + ANON_TOKENS_ASSIGN_OR_RETURN(std::string mask, GenerateMask(public_key_)); + std::string masked_message = + MaskMessageConcat(mask, input.plaintext_message()); + + // Generate RSA blinder. + ANON_TOKENS_ASSIGN_OR_RETURN(auto rsa_bssa_blinder, + RsaBlinder::New(public_key_)); + ANON_TOKENS_ASSIGN_OR_RETURN(const std::string blinded_message, + rsa_bssa_blinder->Blind(masked_message)); + + // Store randomness needed to unblind. + BlindingInfo blinding_info = { + input, + mask, + std::move(rsa_bssa_blinder), + }; + + // Create the blinded token. + AnonymousTokensSignRequest_BlindedToken* blinded_token = + request.add_blinded_tokens(); + blinded_token->set_use_case(public_key_.use_case()); + blinded_token->set_key_version(public_key_.key_version()); + blinded_token->set_serialized_token(blinded_message); + blinded_token->set_public_metadata(input.public_metadata()); + blinding_info_map_[blinded_message] = std::move(blinding_info); + } + + return request; +} + +absl::StatusOr<std::vector<RSABlindSignatureTokenWithInput>> +AnonymousTokensRsaBssaClient::ProcessResponse( + const AnonymousTokensSignResponse& response) { + if (blinding_info_map_.empty()) { + return absl::FailedPreconditionError( + "A valid Blind signature request was not created before calling " + "RetrieveAnonymousTokensFromSignResponse."); + } else if (response.anonymous_tokens().empty()) { + return absl::InvalidArgumentError("Cannot process an empty response."); + } else if (static_cast<size_t>(response.anonymous_tokens().size()) != + blinding_info_map_.size()) { + return absl::InvalidArgumentError( + "Response is missing some requested tokens."); + } + + // Vector to accumulate output tokens. + std::vector<RSABlindSignatureTokenWithInput> tokens; + + // Temporary set structure to check for duplicate responses. + absl::flat_hash_set<absl::string_view> blinded_messages; + + // Loop over all the anonymous tokens in the response. + for (const AnonymousTokensSignResponse_AnonymousToken& anonymous_token : + response.anonymous_tokens()) { + // Basic validity checks on the response. + if (anonymous_token.use_case() != public_key_.use_case()) { + return absl::InvalidArgumentError("Use case does not match public key."); + } else if (anonymous_token.key_version() != public_key_.key_version()) { + return absl::InvalidArgumentError( + "Key version does not match public key."); + } else if (anonymous_token.serialized_blinded_message().empty()) { + return absl::InvalidArgumentError( + "Blinded message that was sent in request cannot be empty in " + "response."); + } else if (anonymous_token.serialized_token().empty()) { + return absl::InvalidArgumentError( + "Blinded anonymous token (serialized_token) in response cannot be " + "empty."); + } + + // Check for duplicate in responses. + if (!blinded_messages.insert(anonymous_token.serialized_blinded_message()) + .second) { + return absl::InvalidArgumentError( + "Blinded message was repeated in the response."); + } + + // Retrieve blinding info associated with blind response. + auto it = + blinding_info_map_.find(anonymous_token.serialized_blinded_message()); + if (it == blinding_info_map_.end()) { + return absl::InvalidArgumentError( + "Response has some tokens for some blinded messages that were not " + "requested."); + } + const BlindingInfo& blinding_info = it->second; + + if (blinding_info.input.public_metadata() != + anonymous_token.public_metadata()) { + return absl::InvalidArgumentError( + "Response metadata does not match input."); + } + + // Unblind the blinded anonymous token to obtain the final anonymous token + // (signature). + ANON_TOKENS_ASSIGN_OR_RETURN( + const std::string final_anonymous_token, + blinding_info.rsa_blinder->Unblind(anonymous_token.serialized_token())); + + // Verify the signature for correctness. + ANON_TOKENS_RETURN_IF_ERROR(blinding_info.rsa_blinder->Verify( + final_anonymous_token, + MaskMessageConcat(blinding_info.mask, + blinding_info.input.plaintext_message()))); + + // Construct the final signature proto. + RSABlindSignatureTokenWithInput final_token_proto; + *final_token_proto.mutable_token()->mutable_token() = final_anonymous_token; + *final_token_proto.mutable_token()->mutable_message_mask() = + blinding_info.mask; + *final_token_proto.mutable_input() = blinding_info.input; + + tokens.push_back(final_token_proto); + } + + return tokens; +} + +absl::Status AnonymousTokensRsaBssaClient::Verify( + const RSABlindSignatureToken& /*token*/, absl::string_view /*message*/, + std::optional<absl::string_view> /*public_metadata*/) { + return absl::UnimplementedError("Verify not implemented yet."); +} + +} // namespace anonymous_tokens +} // namespace private_membership
diff --git a/quiche/blind_sign_auth/anonymous_tokens/cpp/client/anonymous_tokens_rsa_bssa_client.h b/quiche/blind_sign_auth/anonymous_tokens/cpp/client/anonymous_tokens_rsa_bssa_client.h new file mode 100644 index 0000000..3af0bf7 --- /dev/null +++ b/quiche/blind_sign_auth/anonymous_tokens/cpp/client/anonymous_tokens_rsa_bssa_client.h
@@ -0,0 +1,100 @@ +// Copyright 2023 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. + +#ifndef THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CLIENT_ANONYMOUS_TOKENS_RSA_BSSA_CLIENT_H_ +#define THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CLIENT_ANONYMOUS_TOKENS_RSA_BSSA_CLIENT_H_ + +#include <memory> +#include <string> +#include <vector> + +#include "absl/container/flat_hash_map.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "absl/types/optional.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/rsa_blinder.h" +#include "quiche/blind_sign_auth/anonymous_tokens/proto/anonymous_tokens.pb.h" +#include "openssl/rsa.h" +#include "quiche/common/platform/api/quiche_export.h" + +namespace private_membership { +namespace anonymous_tokens { + +// This class generates AnonymousTokens RSA BSSA +// (https://datatracker.ietf.org/doc/draft-irtf-cfrg-rsa-blind-signatures/) +// blind message signing request and processes the response. +// +// Each execution of the Anonymous Tokens RSA BSSA protocol requires a new +// instance of the AnonymousTokensRsaBssaClient. +// +// This class is not thread-safe. +class QUICHE_EXPORT AnonymousTokensRsaBssaClient { + public: + // AnonymousTokensRsaBssaClient is neither copyable nor copy assignable. + AnonymousTokensRsaBssaClient(const AnonymousTokensRsaBssaClient&) = delete; + AnonymousTokensRsaBssaClient& operator=(const AnonymousTokensRsaBssaClient&) = + delete; + + // Create client with the specified public key which can be used to send a + // sign request and process a response. + // + // This method is to be used to create a client as its constructor is private. + // It takes as input a public key and relevant parameters. + static absl::StatusOr<std::unique_ptr<AnonymousTokensRsaBssaClient>> Create( + const RSABlindSignaturePublicKey& public_key); + + // Class method that creates the signature requests by taking a vector where + // each element in the vector is the plaintext message along with its + // respective public metadata (if the metadata exists). + // + // The library will also fail if the key has expired. + // + // It only puts the blinded version of the messages in the request. + absl::StatusOr<AnonymousTokensSignRequest> CreateRequest( + const std::vector<PlaintextMessageWithPublicMetadata>& inputs); + + // Class method that processes the signature response from the server. + // + // It outputs a vector of a protos where each element contains an input + // plaintext message and associated public metadata (if it exists) along with + // its final (unblinded) anonymous token resulting from the blind signature + // protocol. + absl::StatusOr<std::vector<RSABlindSignatureTokenWithInput>> ProcessResponse( + const AnonymousTokensSignResponse& response); + + // Method to verify whether a blind token is valid or not. + // + // Returns OK on a valid token and non-OK otherwise. + absl::Status Verify( + const RSABlindSignatureToken& token, absl::string_view message, + absl::optional<absl::string_view> public_metadata = absl::nullopt); + + private: + struct BlindingInfo { + PlaintextMessageWithPublicMetadata input; + std::string mask; + std::unique_ptr<RsaBlinder> rsa_blinder; + }; + + explicit AnonymousTokensRsaBssaClient( + const RSABlindSignaturePublicKey& public_key); + + const RSABlindSignaturePublicKey public_key_; + absl::flat_hash_map<std::string, BlindingInfo> blinding_info_map_; +}; + +} // namespace anonymous_tokens +} // namespace private_membership + +#endif // THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CLIENT_ANONYMOUS_TOKENS_RSA_BSSA_CLIENT_H_
diff --git a/quiche/blind_sign_auth/anonymous_tokens/cpp/client/anonymous_tokens_rsa_bssa_client_test.cc b/quiche/blind_sign_auth/anonymous_tokens/cpp/client/anonymous_tokens_rsa_bssa_client_test.cc new file mode 100644 index 0000000..909096d --- /dev/null +++ b/quiche/blind_sign_auth/anonymous_tokens/cpp/client/anonymous_tokens_rsa_bssa_client_test.cc
@@ -0,0 +1,274 @@ +// Copyright 2023 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 "quiche/blind_sign_auth/anonymous_tokens/cpp/client/anonymous_tokens_rsa_bssa_client.h" + +#include <memory> +#include <string> +#include <utility> +#include <vector> + +#include "quiche/common/platform/api/quiche_test.h" +#include "quiche/common/test_tools/quiche_test_utils.h" +#include "absl/time/time.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/proto_utils.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/status_utils.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/testing_utils.h" +#include "quiche/blind_sign_auth/anonymous_tokens/proto/anonymous_tokens.pb.h" +#include "openssl/base.h" +#include "openssl/rsa.h" + +namespace private_membership { +namespace anonymous_tokens { +namespace { + +using quiche::test::StatusIs; + +absl::StatusOr<std::pair<bssl::UniquePtr<RSA>, RSABlindSignaturePublicKey>> +CreateClientTestKey(absl::string_view use_case = "TEST_USE_CASE", + int key_version = 1, + MessageMaskType mask_type = AT_MESSAGE_MASK_CONCAT, + int message_mask_size = 32) { + ANON_TOKENS_ASSIGN_OR_RETURN(auto key, CreateTestKey()); + key.second.set_use_case(use_case); + key.second.set_key_version(key_version); + key.second.set_message_mask_type(mask_type); + key.second.set_message_mask_size(message_mask_size); + absl::Time start_time = absl::Now() - absl::Minutes(100); + ANON_TOKENS_ASSIGN_OR_RETURN(*key.second.mutable_key_validity_start_time(), + TimeToProto(start_time)); + return key; +} + +TEST(CreateAnonymousTokensRsaBssaClientTest, Success) { + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(auto rsa_key, CreateClientTestKey()); + QUICHE_EXPECT_OK(AnonymousTokensRsaBssaClient::Create(rsa_key.second)); +} + +TEST(CreateAnonymousTokensRsaBssaClientTest, InvalidUseCase) { + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(auto rsa_key, + CreateClientTestKey("INVALID_USE_CASE")); + EXPECT_THAT(AnonymousTokensRsaBssaClient::Create(rsa_key.second), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(CreateAnonymousTokensRsaBssaClientTest, NotAUseCase) { + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(auto rsa_key, + CreateClientTestKey("NOT_A_USE_CASE")); + EXPECT_THAT(AnonymousTokensRsaBssaClient::Create(rsa_key.second), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(CreateAnonymousTokensRsaBssaClientTest, InvalidKeyVersion) { + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(auto rsa_key, + CreateClientTestKey("TEST_USE_CASE", 0)); + EXPECT_THAT(AnonymousTokensRsaBssaClient::Create(rsa_key.second), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(CreateAnonymousTokensRsaBssaClientTest, InvalidMessageMaskType) { + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + auto rsa_key, + CreateClientTestKey("TEST_USE_CASE", 0, AT_MESSAGE_MASK_TYPE_UNDEFINED)); + EXPECT_THAT(AnonymousTokensRsaBssaClient::Create(rsa_key.second), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(CreateAnonymousTokensRsaBssaClientTest, InvalidMessageMaskSize) { + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + auto rsa_key, + CreateClientTestKey("TEST_USE_CASE", 0, AT_MESSAGE_MASK_CONCAT, 0)); + EXPECT_THAT(AnonymousTokensRsaBssaClient::Create(rsa_key.second), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +class AnonymousTokensRsaBssaClientTest : public testing::Test { + protected: + void SetUp() override { + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(auto key, CreateClientTestKey()); + rsa_key_ = std::move(key.first); + public_key_ = std::move(key.second); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + client_, AnonymousTokensRsaBssaClient::Create(public_key_)); + } + + absl::StatusOr<AnonymousTokensSignResponse> CreateResponse( + const AnonymousTokensSignRequest& request) { + AnonymousTokensSignResponse response; + for (const auto& request_token : request.blinded_tokens()) { + auto* response_token = response.add_anonymous_tokens(); + response_token->set_use_case(request_token.use_case()); + response_token->set_key_version(request_token.key_version()); + response_token->set_public_metadata(request_token.public_metadata()); + response_token->set_serialized_blinded_message( + request_token.serialized_token()); + ANON_TOKENS_ASSIGN_OR_RETURN( + *response_token->mutable_serialized_token(), + TestSign(request_token.serialized_token(), rsa_key_.get())); + } + return response; + } + + std::vector<PlaintextMessageWithPublicMetadata> CreateInput( + const std::vector<std::string>& messages) { + std::vector<PlaintextMessageWithPublicMetadata> output; + output.reserve(messages.size()); + for (const std::string& message : messages) { + PlaintextMessageWithPublicMetadata proto; + proto.set_plaintext_message(message); + output.push_back(proto); + } + return output; + } + + bssl::UniquePtr<RSA> rsa_key_; + RSABlindSignaturePublicKey public_key_; + std::unique_ptr<AnonymousTokensRsaBssaClient> client_; +}; + +TEST_F(AnonymousTokensRsaBssaClientTest, SuccessOneMessage) { + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + AnonymousTokensSignRequest request, + client_->CreateRequest(CreateInput({"message"}))); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(AnonymousTokensSignResponse response, + CreateResponse(request)); + QUICHE_EXPECT_OK(client_->ProcessResponse(response)); + EXPECT_EQ(response.anonymous_tokens_size(), 1); +} + +TEST_F(AnonymousTokensRsaBssaClientTest, SuccessMultipleMessages) { + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + AnonymousTokensSignRequest request, + client_->CreateRequest(CreateInput( + {"message1", "msg2", "anotherMessage", "one_more_message"}))); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(AnonymousTokensSignResponse response, + CreateResponse(request)); + EXPECT_EQ(response.anonymous_tokens_size(), 4); + QUICHE_EXPECT_OK(client_->ProcessResponse(response)); +} + +TEST_F(AnonymousTokensRsaBssaClientTest, EnsureRandomTokens) { + std::string message = "test_same_message"; + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + AnonymousTokensSignRequest request, + client_->CreateRequest(CreateInput({message, message}))); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(AnonymousTokensSignResponse response, + CreateResponse(request)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + std::vector<RSABlindSignatureTokenWithInput> tokens, + client_->ProcessResponse(response)); + ASSERT_EQ(tokens.size(), 2); + for (const RSABlindSignatureTokenWithInput& token : tokens) { + EXPECT_EQ(token.input().plaintext_message(), message); + } + EXPECT_NE(tokens[0].token().message_mask(), tokens[1].token().message_mask()); + EXPECT_NE(tokens[0].token().token(), tokens[1].token().token()); +} + +TEST_F(AnonymousTokensRsaBssaClientTest, EmptyInput) { + EXPECT_THAT(client_->CreateRequest(CreateInput({})), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST_F(AnonymousTokensRsaBssaClientTest, NotYetValidKey) { + RSABlindSignaturePublicKey not_valid_key = public_key_; + absl::Time start_time = absl::Now() + absl::Minutes(100); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + *not_valid_key.mutable_key_validity_start_time(), + TimeToProto(start_time)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + std::unique_ptr<AnonymousTokensRsaBssaClient> client, + AnonymousTokensRsaBssaClient::Create(not_valid_key)); + EXPECT_THAT(client->CreateRequest(CreateInput({"message"})), + StatusIs(absl::StatusCode::kFailedPrecondition)); +} + +TEST_F(AnonymousTokensRsaBssaClientTest, ExpiredKey) { + RSABlindSignaturePublicKey expired_key = public_key_; + absl::Time end_time = absl::Now() - absl::Seconds(1); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(*expired_key.mutable_expiration_time(), + TimeToProto(end_time)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + std::unique_ptr<AnonymousTokensRsaBssaClient> client, + AnonymousTokensRsaBssaClient::Create(expired_key)); + EXPECT_THAT(client->CreateRequest(CreateInput({"message"})), + StatusIs(absl::StatusCode::kFailedPrecondition)); +} + +TEST_F(AnonymousTokensRsaBssaClientTest, CreateRequestTwice) { + QUICHE_EXPECT_OK(client_->CreateRequest(CreateInput({"once"}))); + EXPECT_THAT(client_->CreateRequest(CreateInput({"twice"})), + StatusIs(absl::StatusCode::kFailedPrecondition)); +} + +TEST_F(AnonymousTokensRsaBssaClientTest, ProcessResponseWithoutCreateRequest) { + AnonymousTokensSignResponse response; + EXPECT_THAT(client_->ProcessResponse(response), + StatusIs(absl::StatusCode::kFailedPrecondition)); +} + +TEST_F(AnonymousTokensRsaBssaClientTest, ProcessEmptyResponse) { + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + AnonymousTokensSignRequest request, + client_->CreateRequest(CreateInput({"message"}))); + AnonymousTokensSignResponse response; + EXPECT_THAT(client_->ProcessResponse(response), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST_F(AnonymousTokensRsaBssaClientTest, ProcessResponseWithBadUseCase) { + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + AnonymousTokensSignRequest request, + client_->CreateRequest(CreateInput({"message"}))); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(AnonymousTokensSignResponse response, + CreateResponse(request)); + response.mutable_anonymous_tokens(0)->set_use_case("TEST_USE_CASE_2"); + EXPECT_THAT(client_->ProcessResponse(response), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST_F(AnonymousTokensRsaBssaClientTest, ProcessResponseWithBadKeyVersion) { + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + AnonymousTokensSignRequest request, + client_->CreateRequest(CreateInput({"message"}))); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(AnonymousTokensSignResponse response, + CreateResponse(request)); + response.mutable_anonymous_tokens(0)->set_key_version(2); + EXPECT_THAT(client_->ProcessResponse(response), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST_F(AnonymousTokensRsaBssaClientTest, ProcessResponseFromDifferentClient) { + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + std::unique_ptr<AnonymousTokensRsaBssaClient> client2, + AnonymousTokensRsaBssaClient::Create(public_key_)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + AnonymousTokensSignRequest request1, + client_->CreateRequest(CreateInput({"message"}))); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + AnonymousTokensSignRequest request2, + client2->CreateRequest(CreateInput({"message"}))); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(AnonymousTokensSignResponse response1, + CreateResponse(request1)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(AnonymousTokensSignResponse response2, + CreateResponse(request2)); + EXPECT_THAT(client_->ProcessResponse(response2), + StatusIs(absl::StatusCode::kInvalidArgument)); + EXPECT_THAT(client2->ProcessResponse(response1), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +} // namespace +} // namespace anonymous_tokens +} // namespace private_membership
diff --git a/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/at_crypto_utils_test.cc b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/at_crypto_utils_test.cc new file mode 100644 index 0000000..da712dc --- /dev/null +++ b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/at_crypto_utils_test.cc
@@ -0,0 +1,148 @@ +// Copyright 2023 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 "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/crypto_utils.h" + +#include <string> +#include <vector> + +#include "quiche/common/platform/api/quiche_test.h" +#include "quiche/common/test_tools/quiche_test_utils.h" +#include "absl/strings/escaping.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/testing_utils.h" +#include "openssl/base.h" + +namespace private_membership { +namespace anonymous_tokens { +namespace { + +TEST(CryptoUtilsTest, BignumToStringAndBack) { + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(BnCtxPtr ctx, GetAndStartBigNumCtx()); + + // Create a new BIGNUM using the context and set it + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(bssl::UniquePtr<BIGNUM> bn_1, NewBigNum()); + ASSERT_EQ(BN_set_u64(bn_1.get(), 0x124435435), 1); + EXPECT_NE(bn_1, nullptr); + EXPECT_EQ(BN_is_zero(bn_1.get()), 0); + EXPECT_EQ(BN_is_one(bn_1.get()), 0); + + // Convert bn_1 to string from BIGNUM + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + const std::string converted_str, + BignumToString(*bn_1, BN_num_bytes(bn_1.get()))); + // Convert the string version of bn_1 back to BIGNUM + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(bssl::UniquePtr<BIGNUM> bn_2, + StringToBignum(converted_str)); + // Check whether the conversion back worked + EXPECT_EQ(BN_cmp(bn_1.get(), bn_2.get()), 0); +} + +TEST(CryptoUtilsTest, PowerOfTwoAndRsaSqrtTwo) { + // Compute 2^(10-1/2). + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(bssl::UniquePtr<BIGNUM> sqrt2, + GetRsaSqrtTwo(10)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(bssl::UniquePtr<BIGNUM> small_pow2, + ComputePowerOfTwo(9)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(bssl::UniquePtr<BIGNUM> large_pow2, + ComputePowerOfTwo(10)); + EXPECT_GT(BN_cmp(sqrt2.get(), small_pow2.get()), 0); + EXPECT_LT(BN_cmp(sqrt2.get(), large_pow2.get()), 0); +} + +TEST(CryptoUtilsTest, ComputeHashAcceptsNullStringView) { + absl::StatusOr<std::string> null_hash = + ComputeHash(absl::string_view(nullptr, 0), *EVP_sha512()); + absl::StatusOr<std::string> empty_hash = ComputeHash("", *EVP_sha512()); + std::string str; + absl::StatusOr<std::string> empty_str_hash = ComputeHash(str, *EVP_sha512()); + + QUICHE_EXPECT_OK(null_hash); + QUICHE_EXPECT_OK(empty_hash); + QUICHE_EXPECT_OK(empty_str_hash); + + EXPECT_EQ(*null_hash, *empty_hash); + EXPECT_EQ(*null_hash, *empty_str_hash); +} + +TEST(CryptoUtilsTest, ComputeCarmichaelLcm) { + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(BnCtxPtr ctx, GetAndStartBigNumCtx()); + + // Suppose that N = 1019 * 1187. + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(bssl::UniquePtr<BIGNUM> phi_p, NewBigNum()); + ASSERT_TRUE(BN_set_word(phi_p.get(), 1019 - 1)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(bssl::UniquePtr<BIGNUM> phi_q, NewBigNum()); + ASSERT_TRUE(BN_set_word(phi_q.get(), 1187 - 1)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(bssl::UniquePtr<BIGNUM> expected_lcm, + NewBigNum()); + ASSERT_TRUE(BN_set_word(expected_lcm.get(), (1019 - 1) * (1187 - 1) / 2)); + + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(bssl::UniquePtr<BIGNUM> lcm, + ComputeCarmichaelLcm(*phi_p, *phi_q, *ctx)); + EXPECT_EQ(BN_cmp(lcm.get(), expected_lcm.get()), 0); +} + +struct ComputeHashTestParam { + const EVP_MD* hasher; + absl::string_view input_hex; + absl::string_view expected_digest_hex; +}; + +using ComputeHashTest = testing::TestWithParam<ComputeHashTestParam>; + +// Returns the test parameters for ComputeHashTestParam from NIST's +// samples. +std::vector<ComputeHashTestParam> GetComputeHashTestParams() { + std::vector<ComputeHashTestParam> params; + params.push_back({ + EVP_sha256(), + "af397a8b8dd73ab702ce8e53aa9f", + "d189498a3463b18e846b8ab1b41583b0b7efc789dad8a7fb885bbf8fb5b45c5c", + }); + params.push_back({ + EVP_sha256(), + "59eb45bbbeb054b0b97334d53580ce03f699", + "32c38c54189f2357e96bd77eb00c2b9c341ebebacc2945f97804f59a93238288", + }); + params.push_back({ + EVP_sha512(), + "16b17074d3e3d97557f9ed77d920b4b1bff4e845b345a922", + "6884134582a760046433abcbd53db8ff1a89995862f305b887020f6da6c7b903a314721e" + "972bf438483f452a8b09596298a576c903c91df4a414c7bd20fd1d07", + }); + params.push_back({ + EVP_sha512(), + "7651ab491b8fa86f969d42977d09df5f8bee3e5899180b52c968b0db057a6f02a886ad61" + "7a84915a", + "f35e50e2e02b8781345f8ceb2198f068ba103476f715cfb487a452882c9f0de0c720b2a0" + "88a39d06a8a6b64ce4d6470dfeadc4f65ae06672c057e29f14c4daf9", + }); + return params; +} + +TEST_P(ComputeHashTest, ComputesHash) { + const ComputeHashTestParam& params = GetParam(); + ASSERT_NE(params.hasher, nullptr); + std::string data = absl::HexStringToBytes(params.input_hex); + std::string expected_digest = + absl::HexStringToBytes(params.expected_digest_hex); + QUICHE_EXPECT_OK_AND_ASSIGN(auto computed_hash, ComputeHash(data, *params.hasher)); + EXPECT_EQ(computed_hash, expected_digest); +} + +INSTANTIATE_TEST_SUITE_P(ComputeHashTests, ComputeHashTest, + testing::ValuesIn(GetComputeHashTestParams())); + +} // namespace +} // namespace anonymous_tokens +} // namespace private_membership
diff --git a/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/blinder.h b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/blinder.h new file mode 100644 index 0000000..fd29ad7 --- /dev/null +++ b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/blinder.h
@@ -0,0 +1,38 @@ +// Copyright 2023 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. + +#ifndef THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CRYPTO_BLINDER_H_ +#define THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CRYPTO_BLINDER_H_ + +#include <string> + +#include "absl/status/statusor.h" + +namespace private_membership { +namespace anonymous_tokens { + +class Blinder { + public: + enum class BlinderState { kCreated = 0, kBlinded, kUnblinded }; + virtual absl::StatusOr<std::string> Blind(absl::string_view message) = 0; + + virtual absl::StatusOr<std::string> Unblind( + absl::string_view blind_signature) = 0; + + virtual ~Blinder() = default; +}; + +} // namespace anonymous_tokens +} // namespace private_membership +#endif // THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CRYPTO_BLINDER_H_
diff --git a/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/constants.h b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/constants.h new file mode 100644 index 0000000..58ea190 --- /dev/null +++ b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/constants.h
@@ -0,0 +1,68 @@ +// Copyright 2023 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. + +#ifndef THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CRYPTO_CONSTANTS_H_ +#define THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CRYPTO_CONSTANTS_H_ + +#include <cstdint> + +#include "absl/strings/string_view.h" + +namespace private_membership { +namespace anonymous_tokens { + +// Returned integer on successful execution of BoringSSL methods +constexpr int kBsslSuccess = 1; + +// RSA modulus size, 4096 bits +// +// Recommended size. +constexpr int kRsaModulusSizeInBits4096 = 4096; + +// RSA modulus size, 512 bytes +constexpr int kRsaModulusSizeInBytes512 = 512; + +// RSA modulus size, 2048 bits +// +// Recommended size for RSA Blind Signatures without Public Metadata. +// +// https://www.ietf.org/archive/id/draft-ietf-privacypass-protocol-08.html#name-token-type-blind-rsa-2048-b. +constexpr int kRsaModulusSizeInBits2048 = 2048; + +// RSA modulus size, 256 bytes +constexpr int kRsaModulusSizeInBytes256 = 256; + +// Salt length, 48 bytes +// +// Recommended size. The convention is to use hLen, the length of the output of +// the hash function in bytes. A salt length of zero will result in a +// deterministic signature value. +// +// https://datatracker.ietf.org/doc/draft-irtf-cfrg-rsa-blind-signatures/ +constexpr int kSaltLengthInBytes48 = 48; + +// Length of message mask, 32 bytes. +// +// https://datatracker.ietf.org/doc/draft-irtf-cfrg-rsa-blind-signatures/ +constexpr int kRsaMessageMaskSizeInBytes32 = 32; + +// Info used in HKDF for Public Metadata Hash. +constexpr absl::string_view kHkdfPublicMetadataInfo = "PBRSA"; + +constexpr int kHkdfPublicMetadataInfoSizeInBytes = 5; + +} // namespace anonymous_tokens +} // namespace private_membership + +#endif // THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CRYPTO_CONSTANTS_H_
diff --git a/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/crypto_utils.cc b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/crypto_utils.cc new file mode 100644 index 0000000..fc0bd4e --- /dev/null +++ b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/crypto_utils.cc
@@ -0,0 +1,272 @@ +// Copyright 2023 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 "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/crypto_utils.h" + +#include <stddef.h> +#include <stdint.h> + +#include <iterator> +#include <string> +#include <utility> +#include <vector> + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/constants.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/status_utils.h" +#include "openssl/err.h" +#include "openssl/rsa.h" + +namespace private_membership { +namespace anonymous_tokens { + +namespace internal { + +BN_ULONG TOBN(BN_ULONG hi, BN_ULONG lo) { + return ((BN_ULONG)(hi) << 32 | (lo)); +} + +// Approximation of sqrt(2) taken from +// //depot/google3/third_party/openssl/boringssl/src/crypto/fipsmodule/rsa/rsa_impl.c;l=997 +const BN_ULONG kBoringSSLRSASqrtTwo[] = { + TOBN(0x4d7c60a5, 0xe633e3e1), TOBN(0x5fcf8f7b, 0xca3ea33b), + TOBN(0xc246785e, 0x92957023), TOBN(0xf9acce41, 0x797f2805), + TOBN(0xfdfe170f, 0xd3b1f780), TOBN(0xd24f4a76, 0x3facb882), + TOBN(0x18838a2e, 0xaff5f3b2), TOBN(0xc1fcbdde, 0xa2f7dc33), + TOBN(0xdea06241, 0xf7aa81c2), TOBN(0xf6a1be3f, 0xca221307), + TOBN(0x332a5e9f, 0x7bda1ebf), TOBN(0x0104dc01, 0xfe32352f), + TOBN(0xb8cf341b, 0x6f8236c7), TOBN(0x4264dabc, 0xd528b651), + TOBN(0xf4d3a02c, 0xebc93e0c), TOBN(0x81394ab6, 0xd8fd0efd), + TOBN(0xeaa4a089, 0x9040ca4a), TOBN(0xf52f120f, 0x836e582e), + TOBN(0xcb2a6343, 0x31f3c84d), TOBN(0xc6d5a8a3, 0x8bb7e9dc), + TOBN(0x460abc72, 0x2f7c4e33), TOBN(0xcab1bc91, 0x1688458a), + TOBN(0x53059c60, 0x11bc337b), TOBN(0xd2202e87, 0x42af1f4e), + TOBN(0x78048736, 0x3dfa2768), TOBN(0x0f74a85e, 0x439c7b4a), + TOBN(0xa8b1fe6f, 0xdc83db39), TOBN(0x4afc8304, 0x3ab8a2c3), + TOBN(0xed17ac85, 0x83339915), TOBN(0x1d6f60ba, 0x893ba84c), + TOBN(0x597d89b3, 0x754abe9f), TOBN(0xb504f333, 0xf9de6484), +}; +const int kBoringSSLRSASqrtTwoLen = 32; + +} // namespace internal + +absl::StatusOr<BnCtxPtr> GetAndStartBigNumCtx() { + // Create context to be used in intermediate computation. + BnCtxPtr bn_ctx = BnCtxPtr(BN_CTX_new()); + if (!bn_ctx.get()) { + return absl::InternalError("Error generating bignum context."); + } + BN_CTX_start(bn_ctx.get()); + + return bn_ctx; +} + +absl::StatusOr<bssl::UniquePtr<BIGNUM>> NewBigNum() { + bssl::UniquePtr<BIGNUM> bn(BN_new()); + if (!bn.get()) { + return absl::InternalError("Error generating bignum."); + } + return bn; +} + +absl::StatusOr<std::string> BignumToString(const BIGNUM& big_num, + const size_t output_len) { + std::vector<uint8_t> serialization(output_len); + if (BN_bn2bin_padded(serialization.data(), serialization.size(), &big_num) != + kBsslSuccess) { + return absl::InternalError( + absl::StrCat("Function BN_bn2bin_padded failed: ", GetSslErrors())); + } + return std::string(std::make_move_iterator(serialization.begin()), + std::make_move_iterator(serialization.end())); +} + +absl::StatusOr<bssl::UniquePtr<BIGNUM>> StringToBignum( + const absl::string_view input_str) { + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> output, NewBigNum()); + if (!BN_bin2bn(reinterpret_cast<const uint8_t*>(input_str.data()), + input_str.size(), output.get())) { + return absl::InternalError( + absl::StrCat("Function BN_bin2bn failed: ", GetSslErrors())); + } + if (!output.get()) { + return absl::InternalError("Function BN_bin2bn failed."); + } + return output; +} + +std::string GetSslErrors() { + std::string ret; + ERR_print_errors_cb( + [](const char* str, size_t len, void* ctx) -> int { + static_cast<std::string*>(ctx)->append(str, len); + return 1; + }, + &ret); + return ret; +} + +std::string MaskMessageConcat(absl::string_view mask, + absl::string_view message) { + return absl::StrCat(mask, message); +} + +absl::StatusOr<bssl::UniquePtr<BIGNUM>> GetRsaSqrtTwo(int x) { + // Compute hard-coded sqrt(2). + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> sqrt2, NewBigNum()); + for (int i = internal::kBoringSSLRSASqrtTwoLen - 1; i >= 0; --i) { + if (BN_add_word(sqrt2.get(), internal::kBoringSSLRSASqrtTwo[i]) != 1) { + return absl::InternalError(absl::StrCat( + "Cannot add word to compute RSA sqrt(2): ", GetSslErrors())); + } + if (i > 0) { + if (BN_lshift(sqrt2.get(), sqrt2.get(), 64) != 1) { + return absl::InternalError(absl::StrCat( + "Cannot shift to compute RSA sqrt(2): ", GetSslErrors())); + } + } + } + + // Check that hard-coded result is correct length. + int sqrt2_bits = 64 * internal::kBoringSSLRSASqrtTwoLen; + if (BN_num_bits(sqrt2.get()) != sqrt2_bits) { + return absl::InternalError("RSA sqrt(2) is not correct length."); + } + + // Either shift left or right depending on value x. + if (sqrt2_bits > x) { + if (BN_rshift(sqrt2.get(), sqrt2.get(), sqrt2_bits - x) != 1) { + return absl::InternalError( + absl::StrCat("Cannot rshift to compute 2^(x-1/2): ", GetSslErrors())); + } + } else { + // Round up and be pessimistic about minimium factors. + if (BN_add_word(sqrt2.get(), 1) != 1 || + BN_lshift(sqrt2.get(), sqrt2.get(), x - sqrt2_bits) != 1) { + return absl::InternalError(absl::StrCat( + "Cannot add/lshift to compute 2^(x-1/2): ", GetSslErrors())); + } + } + + // Check that 2^(x - 1/2) is correct length. + if (BN_num_bits(sqrt2.get()) != x) { + return absl::InternalError( + "2^(x-1/2) is not correct length after shifting."); + } + + return std::move(sqrt2); +} + +absl::StatusOr<bssl::UniquePtr<BIGNUM>> ComputePowerOfTwo(int x) { + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> ret, NewBigNum()); + if (BN_set_bit(ret.get(), x) != 1) { + return absl::InternalError( + absl::StrCat("Unable to set bit to compute 2^x: ", GetSslErrors())); + } + if (!BN_is_pow2(ret.get()) || !BN_is_bit_set(ret.get(), x)) { + return absl::InternalError(absl::StrCat("Unable to compute 2^", x, ".")); + } + return ret; +} + +absl::StatusOr<std::string> ComputeHash(absl::string_view input, + const EVP_MD& hasher) { + std::string digest; + digest.resize(EVP_MAX_MD_SIZE); + + uint32_t digest_length = 0; + if (EVP_Digest(input.data(), input.length(), + reinterpret_cast<uint8_t*>(&digest[0]), &digest_length, + &hasher, /*impl=*/nullptr) != 1) { + return absl::InternalError(absl::StrCat( + "Openssl internal error computing hash: ", GetSslErrors())); + } + digest.resize(digest_length); + return digest; +} + +absl::StatusOr<bssl::UniquePtr<RSA>> AnonymousTokensRSAPrivateKeyToRSA( + const RSAPrivateKey& private_key) { + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> n, + StringToBignum(private_key.n())); + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> e, + StringToBignum(private_key.e())); + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> d, + StringToBignum(private_key.d())); + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> p, + StringToBignum(private_key.p())); + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> q, + StringToBignum(private_key.q())); + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> dp, + StringToBignum(private_key.dp())); + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> dq, + StringToBignum(private_key.dq())); + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> crt, + StringToBignum(private_key.crt())); + + bssl::UniquePtr<RSA> rsa_private_key(RSA_new()); + // Populate private key. + if (!rsa_private_key.get()) { + return absl::InternalError( + absl::StrCat("RSA_new failed: ", GetSslErrors())); + } else if (RSA_set0_key(rsa_private_key.get(), n.get(), e.get(), d.get()) != + kBsslSuccess) { + return absl::InternalError( + absl::StrCat("RSA_set0_key failed: ", GetSslErrors())); + } else if (RSA_set0_factors(rsa_private_key.get(), p.get(), q.get()) != + kBsslSuccess) { + return absl::InternalError( + absl::StrCat("RSA_set0_factors failed: ", GetSslErrors())); + } else if (RSA_set0_crt_params(rsa_private_key.get(), dp.get(), dq.get(), + crt.get()) != kBsslSuccess) { + return absl::InternalError( + absl::StrCat("RSA_set0_crt_params failed: ", GetSslErrors())); + } else { + n.release(); + e.release(); + d.release(); + p.release(); + q.release(); + dp.release(); + dq.release(); + crt.release(); + } + return std::move(rsa_private_key); +} + +absl::StatusOr<bssl::UniquePtr<BIGNUM>> ComputeCarmichaelLcm( + const BIGNUM& phi_p, const BIGNUM& phi_q, BN_CTX& bn_ctx) { + // To compute lcm(phi(p), phi(q)), we first compute phi(n) = + // (p-1)(q-1). As n is assumed to be a safe RSA modulus (signing_key is + // assumed to be part of a strong rsa key pair), phi(n) = (p-1)(q-1) = + // (2 phi(p))(2 phi(q)) = 4 * phi(p) * phi(q) where phi(p) and phi(q) are also + // primes. So we get the lcm by outputting phi(n) >> 1 = 2 * phi(p) * phi(q). + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> phi_n, NewBigNum()); + if (BN_mul(phi_n.get(), &phi_p, &phi_q, &bn_ctx) != 1) { + return absl::InternalError( + absl::StrCat("Unable to compute phi(n): ", GetSslErrors())); + } + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> lcm, NewBigNum()); + if (BN_rshift1(lcm.get(), phi_n.get()) != 1) { + return absl::InternalError(absl::StrCat( + "Could not compute LCM(phi(p), phi(q)): ", GetSslErrors())); + } + return lcm; +} + +} // namespace anonymous_tokens +} // namespace private_membership
diff --git a/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/crypto_utils.h b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/crypto_utils.h new file mode 100644 index 0000000..ce96354 --- /dev/null +++ b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/crypto_utils.h
@@ -0,0 +1,102 @@ +// Copyright 2023 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. + +#ifndef THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CRYPTO_CRYPTO_UTILS_H_ +#define THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CRYPTO_CRYPTO_UTILS_H_ + +#include <stddef.h> + +#include <memory> +#include <string> + +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "quiche/blind_sign_auth/anonymous_tokens/proto/anonymous_tokens.pb.h" +#include "openssl/base.h" +#include "openssl/bn.h" +#include "openssl/evp.h" +// #include "quiche/common/platform/api/quiche_export.h" + +namespace private_membership { +namespace anonymous_tokens { + +// Deletes a BN_CTX. +class BnCtxDeleter { + public: + void operator()(BN_CTX* ctx) { BN_CTX_free(ctx); } +}; +typedef std::unique_ptr<BN_CTX, BnCtxDeleter> BnCtxPtr; + +// Deletes a BN_MONT_CTX. +class BnMontCtxDeleter { + public: + void operator()(BN_MONT_CTX* mont_ctx) { BN_MONT_CTX_free(mont_ctx); } +}; +typedef std::unique_ptr<BN_MONT_CTX, BnMontCtxDeleter> BnMontCtxPtr; + +// Deletes an EVP_MD_CTX. +class EvpMdCtxDeleter { + public: + void operator()(EVP_MD_CTX* ctx) { EVP_MD_CTX_destroy(ctx); } +}; +typedef std::unique_ptr<EVP_MD_CTX, EvpMdCtxDeleter> EvpMdCtxPtr; + +// Creates and starts a BIGNUM context. +absl::StatusOr<BnCtxPtr> QUICHE_EXPORT GetAndStartBigNumCtx(); + +// Creates a new BIGNUM. +absl::StatusOr<bssl::UniquePtr<BIGNUM>> QUICHE_EXPORT NewBigNum(); + +// Converts a BIGNUM to string. +absl::StatusOr<std::string> QUICHE_EXPORT BignumToString( + const BIGNUM& big_num, size_t output_len); + +// Converts a string to BIGNUM. +absl::StatusOr<bssl::UniquePtr<BIGNUM>> QUICHE_EXPORT StringToBignum( + absl::string_view input_str); + +// Retrieve error messages from OpenSSL. +std::string QUICHE_EXPORT GetSslErrors(); + +// Mask message using protocol at +// https://datatracker.ietf.org/doc/draft-irtf-cfrg-rsa-blind-signatures/ +std::string MaskMessageConcat(absl::string_view mask, + absl::string_view message); + +// Compute 2^(x - 1/2). +absl::StatusOr<bssl::UniquePtr<BIGNUM>> QUICHE_EXPORT GetRsaSqrtTwo( + int x); + +// Compute compute 2^x. +absl::StatusOr<bssl::UniquePtr<BIGNUM>> QUICHE_EXPORT ComputePowerOfTwo( + int x); + +// ComputeHash sub-routine used druing blindness and verification of RSA PSS +// AnonymousTokens. +absl::StatusOr<std::string> QUICHE_EXPORT ComputeHash( + absl::string_view input, const EVP_MD& hasher); + +// Computes the Carmichael LCM given phi(p) and phi(q) where N = pq is a safe +// RSA modulus. +absl::StatusOr<bssl::UniquePtr<BIGNUM>> QUICHE_EXPORT +ComputeCarmichaelLcm(const BIGNUM& phi_p, const BIGNUM& phi_q, BN_CTX& bn_ctx); + +// Converts AnonymousTokens::RSAPrivateKey To bssl::UniquePtr<RSA> +absl::StatusOr<bssl::UniquePtr<RSA>> QUICHE_EXPORT +AnonymousTokensRSAPrivateKeyToRSA(const RSAPrivateKey& private_key); + +} // namespace anonymous_tokens +} // namespace private_membership + +#endif // THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CRYPTO_CRYPTO_UTILS_H_
diff --git a/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/proto_utils.cc b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/proto_utils.cc new file mode 100644 index 0000000..3129725 --- /dev/null +++ b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/proto_utils.cc
@@ -0,0 +1,64 @@ +// Copyright 2023 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 "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/proto_utils.h" + +namespace private_membership { +namespace anonymous_tokens { + +absl::StatusOr<AnonymousTokensUseCase> ParseUseCase( + absl::string_view use_case) { + AnonymousTokensUseCase parsed_use_case; + if (!AnonymousTokensUseCase_Parse(std::string(use_case), &parsed_use_case) || + parsed_use_case == ANONYMOUS_TOKENS_USE_CASE_UNDEFINED) { + return absl::InvalidArgumentError( + "Invalid / undefined use case cannot be parsed."); + } + return parsed_use_case; +} + +absl::StatusOr<absl::Time> TimeFromProto( + const quiche::protobuf::Timestamp& proto) { + const auto sec = proto.seconds(); + const auto ns = proto.nanos(); + // sec must be [0001-01-01T00:00:00Z, 9999-12-31T23:59:59.999999999Z] + if (sec < -62135596800 || sec > 253402300799) { + return absl::InvalidArgumentError(absl::StrCat("seconds=", sec)); + } + if (ns < 0 || ns > 999999999) { + return absl::InvalidArgumentError(absl::StrCat("nanos=", ns)); + } + return absl::FromUnixSeconds(proto.seconds()) + + absl::Nanoseconds(proto.nanos()); +} + +absl::StatusOr<quiche::protobuf::Timestamp> TimeToProto(absl::Time time) { + quiche::protobuf::Timestamp proto; + const int64_t seconds = absl::ToUnixSeconds(time); + proto.set_seconds(seconds); + proto.set_nanos((time - absl::FromUnixSeconds(seconds)) / + absl::Nanoseconds(1)); + // seconds must be [0001-01-01T00:00:00Z, 9999-12-31T23:59:59.999999999Z] + if (seconds < -62135596800 || seconds > 253402300799) { + return absl::InvalidArgumentError(absl::StrCat("seconds=", seconds)); + } + const int64_t ns = proto.nanos(); + if (ns < 0 || ns > 999999999) { + return absl::InvalidArgumentError(absl::StrCat("nanos=", ns)); + } + return proto; +} + +} // namespace anonymous_tokens +} // namespace private_membership
diff --git a/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/proto_utils.h b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/proto_utils.h new file mode 100644 index 0000000..b16bf97 --- /dev/null +++ b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/proto_utils.h
@@ -0,0 +1,43 @@ +// Copyright 2023 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. + +#ifndef THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CRYPTO_PROTO_UTILS_H_ +#define THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CRYPTO_PROTO_UTILS_H_ + +#include "quiche/blind_sign_auth/proto/timestamp.pb.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "absl/time/time.h" +#include "quiche/blind_sign_auth/anonymous_tokens/proto/anonymous_tokens.pb.h" +#include "quiche/common/platform/api/quiche_export.h" + +namespace private_membership { +namespace anonymous_tokens { + +absl::StatusOr<AnonymousTokensUseCase> QUICHE_EXPORT ParseUseCase( + absl::string_view use_case); + +// Timestamp is defined here: +// https://developers.google.com/protocol-buffers/docs/reference/quiche.protobuf#timestamp +absl::StatusOr<absl::Time> QUICHE_EXPORT TimeFromProto( + const quiche::protobuf::Timestamp& proto); + +// Timestamp is defined here: +// https://developers.google.com/protocol-buffers/docs/reference/quiche.protobuf#timestamp +absl::StatusOr<quiche::protobuf::Timestamp> QUICHE_EXPORT TimeToProto(absl::Time time); + +} // namespace anonymous_tokens +} // namespace private_membership + +#endif // THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CRYPTO_PROTO_UTILS_H_
diff --git a/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/proto_utils_test.cc b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/proto_utils_test.cc new file mode 100644 index 0000000..a6ff0a2 --- /dev/null +++ b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/proto_utils_test.cc
@@ -0,0 +1,97 @@ +// Copyright 2023 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 "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/proto_utils.h" + +#include "quiche/blind_sign_auth/proto/timestamp.pb.h" +#include "quiche/common/platform/api/quiche_test.h" +#include "quiche/common/test_tools/quiche_test_utils.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/time/time.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/testing_utils.h" +#include "quiche/blind_sign_auth/anonymous_tokens/proto/anonymous_tokens.pb.h" + +namespace private_membership { +namespace anonymous_tokens { +namespace { + +using ::testing::EqualsProto; + +TEST(ProtoUtilsTest, EmptyUseCase) { + EXPECT_THAT(ParseUseCase("").status().code(), + absl::StatusCode::kInvalidArgument); +} + +TEST(ProtoUtilsTest, InvalidUseCase) { + EXPECT_THAT(ParseUseCase("NOT_A_USE_CASE").status().code(), + absl::StatusCode::kInvalidArgument); +} + +TEST(ProtoUtilsTest, UndefinedUseCase) { + EXPECT_THAT( + ParseUseCase("ANONYMOUS_TOKENS_USE_CASE_UNDEFINED").status().code(), + absl::StatusCode::kInvalidArgument); +} + +TEST(ProtoUtilsTest, ValidUseCase) { + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(AnonymousTokensUseCase use_case, + ParseUseCase("TEST_USE_CASE")); + EXPECT_EQ(use_case, AnonymousTokensUseCase::TEST_USE_CASE); +} + +TEST(ProtoUtilsTest, TimeFromProtoGood) { + quiche::protobuf::Timestamp timestamp; + timestamp.set_seconds(1234567890); + timestamp.set_nanos(12345); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(absl::Time time, TimeFromProto(timestamp)); + ASSERT_EQ(time, absl::FromUnixNanos(1234567890000012345)); +} + +TEST(ProtoUtilsTest, TimeFromProtoBad) { + quiche::protobuf::Timestamp proto; + proto.set_nanos(-1); + EXPECT_THAT(TimeFromProto(proto).status().code(), + absl::StatusCode::kInvalidArgument); + + proto.set_nanos(0); + proto.set_seconds(253402300800); + EXPECT_THAT(TimeFromProto(proto).status().code(), + absl::StatusCode::kInvalidArgument); +} + +TEST(ProtoUtilsTest, TimeToProtoGood) { + quiche::protobuf::Timestamp proto; + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + proto, TimeToProto(absl::FromUnixSeconds(1596762373))); + EXPECT_THAT(proto, EqualsProto(R"pb( + seconds: 1596762373 nanos: 0 + )pb")); + + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + proto, TimeToProto(absl::FromUnixMillis(1596762373123L))); + EXPECT_THAT(proto, EqualsProto(R"pb( + seconds: 1596762373 nanos: 123000000 + )pb")); +} + +TEST(ProtoUtilsTest, TimeToProtoBad) { + absl::StatusOr<quiche::protobuf::Timestamp> proto; + proto = TimeToProto(absl::FromUnixSeconds(253402300800)); + EXPECT_THAT(proto.status().code(), absl::StatusCode::kInvalidArgument); +} + +} // namespace +} // namespace anonymous_tokens +} // namespace private_membership
diff --git a/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/public_metadata_crypto_utils.cc b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/public_metadata_crypto_utils.cc new file mode 100644 index 0000000..ff31e23 --- /dev/null +++ b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/public_metadata_crypto_utils.cc
@@ -0,0 +1,160 @@ +// Copyright 2023 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 "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/public_metadata_crypto_utils.h" + +#include <cstdint> +#include <string> +#include <utility> +#include <vector> + +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/constants.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/crypto_utils.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/status_utils.h" +#include "quiche/blind_sign_auth/anonymous_tokens/proto/anonymous_tokens.pb.h" +#include "openssl/hkdf.h" +#include "openssl/rsa.h" + +namespace private_membership { +namespace anonymous_tokens { + +namespace public_metadata_crypto_utils_internal { + +absl::StatusOr<bssl::UniquePtr<BIGNUM>> PublicMetadataHashWithHKDF( + absl::string_view input, absl::string_view rsa_modulus_str, + size_t out_len_bytes) { + const EVP_MD* evp_md_sha_384 = EVP_sha384(); + // append 0x00 to input + std::vector<uint8_t> input_buffer(input.begin(), input.end()); + input_buffer.push_back(0x00); + std::string out_e; + // We set the out_e size beyond out_len_bytes so that out_e bytes are + // indifferentiable from truly random bytes even after truncations. + // + // Expanding to 16 more bytes is sufficient. + // https://cfrg.github.io/draft-irtf-cfrg-hash-to-curve/draft-irtf-cfrg-hash-to-curve.html#name-hashing-to-a-finite-field + const size_t hkdf_output_size = out_len_bytes + 16; + out_e.resize(hkdf_output_size); + // The modulus is used as salt to ensure different outputs for same metadata + // and different modulus. + if (HKDF(reinterpret_cast<uint8_t*>(out_e.data()), hkdf_output_size, + evp_md_sha_384, input_buffer.data(), input_buffer.size(), + reinterpret_cast<const uint8_t*>(rsa_modulus_str.data()), + rsa_modulus_str.size(), + reinterpret_cast<const uint8_t*>(kHkdfPublicMetadataInfo.data()), + kHkdfPublicMetadataInfoSizeInBytes) != kBsslSuccess) { + return absl::InternalError("HKDF failed in public_metadata_crypto_utils"); + } + // Truncate out_e to out_len_bytes + out_e.resize(out_len_bytes); + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> out, + StringToBignum(out_e)); + return std::move(out); +} + +} // namespace public_metadata_crypto_utils_internal + +absl::StatusOr<bssl::UniquePtr<BIGNUM>> PublicMetadataExponent( + const BIGNUM& n, absl::string_view public_metadata) { + // Check modulus length. + if (BN_num_bits(&n) % 2 == 1) { + return absl::InvalidArgumentError( + "Strong RSA modulus should be even length."); + } + int modulus_bytes = BN_num_bytes(&n); + // The integer modulus_bytes is expected to be a power of 2. + int prime_bytes = modulus_bytes / 2; + + ANON_TOKENS_ASSIGN_OR_RETURN(std::string rsa_modulus_str, + BignumToString(n, modulus_bytes)); + + // Get HKDF output of length prime_bytes. + ANON_TOKENS_ASSIGN_OR_RETURN( + bssl::UniquePtr<BIGNUM> exponent, + public_metadata_crypto_utils_internal::PublicMetadataHashWithHKDF( + public_metadata, rsa_modulus_str, prime_bytes)); + + // We need to generate random odd exponents < 2^(primes_bits - 2) where + // prime_bits = prime_bytes * 8. This will guarantee that the resulting + // exponent is coprime to phi(N) = 4p'q' as 2^(prime_bits - 2) < p', q' < + // 2^(prime_bits - 1). + // + // To do this, we can truncate the HKDF output (exponent) which is prime_bits + // long, to prime_bits - 2, by clearing its top two bits. We then set the + // least significant bit to 1. This way the final exponent will be less than + // 2^(primes_bits - 2) and will always be odd. + if (BN_clear_bit(exponent.get(), (prime_bytes * 8) - 1) != kBsslSuccess || + BN_clear_bit(exponent.get(), (prime_bytes * 8) - 2) != kBsslSuccess || + BN_set_bit(exponent.get(), 0) != kBsslSuccess) { + return absl::InvalidArgumentError(absl::StrCat( + "Could not clear the two most significant bits and set the least " + "significant bit to zero: ", + GetSslErrors())); + } + // Check that exponent is small enough to ensure it is coprime to phi(n). + if (BN_num_bits(exponent.get()) >= (8 * prime_bytes - 1)) { + return absl::InternalError("Generated exponent is too large."); + } + + return std::move(exponent); +} + +// TODO(b/259581423) Move ComputeFinalExponentUnderPublicMetadata to anonymous +// namespace once it is only used by RSAPublicKeyToRSAUnderPublicMetadata +absl::StatusOr<bssl::UniquePtr<BIGNUM>> ComputeFinalExponentUnderPublicMetadata( + const BIGNUM& n, const BIGNUM& e, absl::string_view public_metadata) { + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> md_exp, + PublicMetadataExponent(n, public_metadata)); + ANON_TOKENS_ASSIGN_OR_RETURN(BnCtxPtr bn_ctx, GetAndStartBigNumCtx()); + // new_e=e*md_exp + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> new_e, NewBigNum()); + if (BN_mul(new_e.get(), md_exp.get(), &e, bn_ctx.get()) != 1) { + return absl::InternalError( + absl::StrCat("Unable to multiply e with md_exp: ", GetSslErrors())); + } + return std::move(new_e); +} + +absl::StatusOr<bssl::UniquePtr<RSA>> RSAPublicKeyToRSAUnderPublicMetadata( + const RSAPublicKey& public_key, absl::string_view public_metadata) { + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> rsa_modulus, + StringToBignum(public_key.n())); + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> old_e, + StringToBignum(public_key.e())); + bssl::UniquePtr<BIGNUM> new_e; + if (!public_metadata.empty()) { + // Final exponent under Public metadata + ANON_TOKENS_ASSIGN_OR_RETURN( + new_e, ComputeFinalExponentUnderPublicMetadata( + *rsa_modulus.get(), *old_e.get(), public_metadata)); + } else { + new_e = std::move(old_e); + } + // Convert to OpenSSL RSA. + bssl::UniquePtr<RSA> rsa_public_key(RSA_new()); + if (!rsa_public_key.get()) { + return absl::InternalError( + absl::StrCat("RSA_new failed: ", GetSslErrors())); + } else if (RSA_set0_key(rsa_public_key.get(), rsa_modulus.get(), + new_e.get(), nullptr) != kBsslSuccess) { + return absl::InternalError( + absl::StrCat("RSA_set0_key failed: ", GetSslErrors())); + } + rsa_modulus.release(); + new_e.release(); + return std::move(rsa_public_key); +} + +} // namespace anonymous_tokens +} // namespace private_membership
diff --git a/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/public_metadata_crypto_utils.h b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/public_metadata_crypto_utils.h new file mode 100644 index 0000000..8f3575a --- /dev/null +++ b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/public_metadata_crypto_utils.h
@@ -0,0 +1,63 @@ +// Copyright 2023 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. + +#ifndef THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CRYPTO_PUBLIC_METADATA_CRYPTO_UTILS_H_ +#define THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CRYPTO_PUBLIC_METADATA_CRYPTO_UTILS_H_ + +#include <stddef.h> + +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "quiche/blind_sign_auth/anonymous_tokens/proto/anonymous_tokens.pb.h" +#include "openssl/base.h" +#include "quiche/common/platform/api/quiche_export.h" + +namespace private_membership { +namespace anonymous_tokens { + +// Internal functions only exposed for testing. +namespace public_metadata_crypto_utils_internal { + +absl::StatusOr<bssl::UniquePtr<BIGNUM>> QUICHE_EXPORT PublicMetadataHashWithHKDF( + absl::string_view input, absl::string_view rsa_modulus_str, + size_t out_len_bytes); + +} // namespace public_metadata_crypto_utils_internal + +// Compute exponent based only on the public metadata. Assumes that n is a safe +// modulus i.e. it produces a strong RSA key pair. If not, the exponent may be +// invalid. +absl::StatusOr<bssl::UniquePtr<BIGNUM>> QUICHE_EXPORT PublicMetadataExponent( + const BIGNUM& n, absl::string_view public_metadata); + +// Computes final exponent by multiplying the public exponent e with the +// exponent derived from public metadata. Assumes that n is a safe modulus i.e. +// it produces a strong RSA key pair. If not, the exponent may be invalid. +absl::StatusOr<bssl::UniquePtr<BIGNUM>> QUICHE_EXPORT ComputeFinalExponentUnderPublicMetadata( + const BIGNUM& n, const BIGNUM& e, absl::string_view public_metadata); + +// Converts AnonymousTokens RSAPublicKey to RSA under a fixed public_metadata. +// +// If the public_metadata is empty, this method doesn't modify the public +// exponent but instead simply outputs the RSA for the unmodified RSAPublicKey. +// +// TODO(b/271441409): Stop using RSA object from boringssl in +// AnonymousTokensService. Replace with a new internal struct. +absl::StatusOr<bssl::UniquePtr<RSA>> QUICHE_EXPORT RSAPublicKeyToRSAUnderPublicMetadata( + const RSAPublicKey& public_key, absl::string_view public_metadata); + +} // namespace anonymous_tokens +} // namespace private_membership + +#endif // THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CRYPTO_PUBLIC_METADATA_CRYPTO_UTILS_H_
diff --git a/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/public_metadata_crypto_utils_test.cc b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/public_metadata_crypto_utils_test.cc new file mode 100644 index 0000000..50ea2ab --- /dev/null +++ b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/public_metadata_crypto_utils_test.cc
@@ -0,0 +1,266 @@ +// Copyright 2023 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 "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/public_metadata_crypto_utils.h" + +#include <memory> +#include <string> +#include <utility> + +#include "quiche/common/platform/api/quiche_test.h" +#include "quiche/common/test_tools/quiche_test_utils.h" +#include "absl/strings/escaping.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/crypto_utils.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/testing_utils.h" +#include "quiche/blind_sign_auth/anonymous_tokens/proto/anonymous_tokens.pb.h" +#include "openssl/base.h" +#include "openssl/rsa.h" + +namespace private_membership { +namespace anonymous_tokens { +namespace { + +std::pair<RSAPublicKey, std::string> GetFixedTestPublicKeyAndPublicMetadata() { + RSAPublicKey public_key; + public_key.set_n(absl::HexStringToBytes( + "b2ae391467872a7506468a9ac4e980fa76164666955ef8999917295dbbd89dd7aa9c0e41" + "2dcda3dd1aa867e0c414d80afb9544a7c71c32d83e1b8417f293f325d2ffe2f9e296d28f" + "b89a443de5cc06ab3c516913fc18694539c370315d3e7f4ac5f87faaf3fee751c9f439ae" + "8d53eee249d8c49b33bd3bb7aa060eb462522da98a02f92eff110cc9408ca0ccc54abf2c" + "fcb68b77fb0ec7048d8b76416f61f2b182ea73169ed18f0d1d238dcaf6fc9de067d4831f" + "68f485483dd5c9ec17d9384825ba7284bc38bb1ea5e40d9207d9007e609a19e3fab695a1" + "8c30f1a7c4b03c77ef72211415a0bfeacd3298dccafa7e06e41dc2131f9076b92bb352c8" + "f7bccfe9")); + public_key.set_e(absl::HexStringToBytes("03")); + std::string public_metadata = absl::HexStringToBytes("6d65746164617461"); + return std::make_pair(std::move(public_key), std::move(public_metadata)); +} + +std::string GetFixedTestNewPublicKeyExponentUnderPublicMetadata() { + std::string new_e = absl::HexStringToBytes( + "0b2d80537b4c899c7107eef3b74ddc0dcd931aff9c583ce3cf3527d42483052b27d55dd4" + "d2f831a38430f13d81574c51aa97af6f5c3a6c03b269bc156d029273bd60e7af578fff15" + "c52cbb5c19288fd1ce59f6f756b2d93b6f2586210fb969efb5065700da5598bb8914d395" + "4d97a49c5ca05b2386bc3cf098281958cf372481"); + return new_e; +} + +using CreateTestKeyPairFunction = + absl::StatusOr<std::pair<RSAPublicKey, RSAPrivateKey>>(); + +class PublicMetadataCryptoUtilsTest + : public testing::TestWithParam<CreateTestKeyPairFunction*> { + protected: + void SetUp() override { + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(auto keys_pair, (*GetParam())()); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + private_key_, AnonymousTokensRSAPrivateKeyToRSA(keys_pair.second)); + public_key_ = std::move(keys_pair.first); + } + + bssl::UniquePtr<RSA> private_key_; + RSAPublicKey public_key_; +}; + +TEST_P(PublicMetadataCryptoUtilsTest, PublicExponentCoprime) { + std::string metadata = "md"; + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + bssl::UniquePtr<BIGNUM> exp, + PublicMetadataExponent(*RSA_get0_n(private_key_.get()), metadata)); + int rsa_mod_size_bits = BN_num_bits(RSA_get0_n(private_key_.get())); + // Check that exponent is odd. + EXPECT_EQ(BN_is_odd(exp.get()), 1); + // Check that exponent is small enough. + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(bssl::UniquePtr<BIGNUM> sqrt2, + GetRsaSqrtTwo(rsa_mod_size_bits / 2)); + EXPECT_LT(BN_cmp(exp.get(), sqrt2.get()), 0); + EXPECT_LT(BN_cmp(exp.get(), RSA_get0_p(private_key_.get())), 0); + EXPECT_LT(BN_cmp(exp.get(), RSA_get0_q(private_key_.get())), 0); +} + +TEST_P(PublicMetadataCryptoUtilsTest, PublicExponentHash) { + std::string metadata1 = "md1"; + std::string metadata2 = "md2"; + // Check that hash is deterministic. + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + bssl::UniquePtr<BIGNUM> exp1, + PublicMetadataExponent(*RSA_get0_n(private_key_.get()), metadata1)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + bssl::UniquePtr<BIGNUM> another_exp1, + PublicMetadataExponent(*RSA_get0_n(private_key_.get()), metadata1)); + EXPECT_EQ(BN_cmp(exp1.get(), another_exp1.get()), 0); + // Check that hashes are distinct for different metadata. + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + bssl::UniquePtr<BIGNUM> exp2, + PublicMetadataExponent(*RSA_get0_n(private_key_.get()), metadata2)); + EXPECT_NE(BN_cmp(exp1.get(), exp2.get()), 0); +} + +TEST_P(PublicMetadataCryptoUtilsTest, FinalExponentCoprime) { + std::string metadata = "md"; + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + bssl::UniquePtr<BIGNUM> final_exponent, + ComputeFinalExponentUnderPublicMetadata(*RSA_get0_n(private_key_.get()), + *RSA_get0_e(private_key_.get()), + metadata)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(BnCtxPtr ctx, GetAndStartBigNumCtx()); + + // Check that exponent is odd. + EXPECT_EQ(BN_is_odd(final_exponent.get()), 1); + // Check that exponent is co-prime to factors of the rsa modulus. + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(bssl::UniquePtr<BIGNUM> gcd_p_fe, + NewBigNum()); + ASSERT_EQ(BN_gcd(gcd_p_fe.get(), RSA_get0_p(private_key_.get()), + final_exponent.get(), ctx.get()), + 1); + EXPECT_EQ(BN_cmp(gcd_p_fe.get(), BN_value_one()), 0); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(bssl::UniquePtr<BIGNUM> gcd_q_fe, + NewBigNum()); + ASSERT_EQ(BN_gcd(gcd_q_fe.get(), RSA_get0_q(private_key_.get()), + final_exponent.get(), ctx.get()), + 1); + EXPECT_EQ(BN_cmp(gcd_q_fe.get(), BN_value_one()), 0); +} + +TEST_P(PublicMetadataCryptoUtilsTest, + DeterministicRSAPublicKeyToRSAUnderPublicMetadata) { + std::string metadata = "md"; + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + bssl::UniquePtr<RSA> rsa_public_key_1, + RSAPublicKeyToRSAUnderPublicMetadata(public_key_, metadata)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + bssl::UniquePtr<RSA> rsa_public_key_2, + RSAPublicKeyToRSAUnderPublicMetadata(public_key_, metadata)); + EXPECT_EQ(BN_cmp(RSA_get0_e(rsa_public_key_1.get()), + RSA_get0_e(rsa_public_key_2.get())), + 0); +} + +TEST_P(PublicMetadataCryptoUtilsTest, + DifferentPublicMetadataRSAPublicKeyToRSAUnderPublicMetadata) { + std::string metadata_1 = "md1"; + std::string metadata_2 = "md2"; + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + bssl::UniquePtr<RSA> rsa_public_key_1, + RSAPublicKeyToRSAUnderPublicMetadata(public_key_, metadata_1)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + bssl::UniquePtr<RSA> rsa_public_key_2, + RSAPublicKeyToRSAUnderPublicMetadata(public_key_, metadata_2)); + // Check that exponent is different in all keys + EXPECT_NE(BN_cmp(RSA_get0_e(rsa_public_key_1.get()), + RSA_get0_e(rsa_public_key_2.get())), + 0); + EXPECT_NE(BN_cmp(RSA_get0_e(rsa_public_key_1.get()), + RSA_get0_e(private_key_.get())), + 0); + EXPECT_NE(BN_cmp(RSA_get0_e(rsa_public_key_1.get()), + RSA_get0_e(private_key_.get())), + 0); +} + +TEST_P(PublicMetadataCryptoUtilsTest, + NoPublicMetadataRSAPublicKeyToRSAUnderPublicMetadata) { + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + bssl::UniquePtr<RSA> rsa_public_key, + RSAPublicKeyToRSAUnderPublicMetadata(public_key_, "")); + + // Check that exponent is same in output and input. + EXPECT_EQ( + BN_cmp(RSA_get0_e(rsa_public_key.get()), RSA_get0_e(private_key_.get())), + 0); + // Check that rsa_modulus is correct + EXPECT_EQ( + BN_cmp(RSA_get0_n(rsa_public_key.get()), RSA_get0_n(private_key_.get())), + 0); +} + +INSTANTIATE_TEST_SUITE_P( + PublicMetadataCryptoUtilsTest, PublicMetadataCryptoUtilsTest, + testing::Values(&GetStrongRsaKeys2048, &GetAnotherStrongRsaKeys2048, + &GetStrongRsaKeys3072, &GetStrongRsaKeys4096)); + +TEST(PublicMetadataCryptoUtilsInternalTest, PublicMetadataHashWithHKDF) { + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(BnCtxPtr ctx, GetAndStartBigNumCtx()); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(bssl::UniquePtr<BIGNUM> max_value, + NewBigNum()); + ASSERT_TRUE(BN_set_word(max_value.get(), 4294967296)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(auto key_pair, GetStrongRsaKeys2048()); + std::string input1 = "ro1"; + std::string input2 = "ro2"; + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + bssl::UniquePtr<BIGNUM> output1, + public_metadata_crypto_utils_internal::PublicMetadataHashWithHKDF( + input1, key_pair.first.n(), 1 + input1.size())); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + bssl::UniquePtr<BIGNUM> another_output1, + public_metadata_crypto_utils_internal::PublicMetadataHashWithHKDF( + input1, key_pair.first.n(), 1 + input1.size())); + EXPECT_EQ(BN_cmp(output1.get(), another_output1.get()), 0); + + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + bssl::UniquePtr<BIGNUM> output2, + public_metadata_crypto_utils_internal::PublicMetadataHashWithHKDF( + input2, key_pair.first.n(), 1 + input2.size())); + EXPECT_NE(BN_cmp(output1.get(), output2.get()), 0); + + EXPECT_LT(BN_cmp(output1.get(), max_value.get()), 0); + EXPECT_LT(BN_cmp(output2.get(), max_value.get()), 0); +} + +TEST(PublicMetadataCryptoUtilsTest, PublicExponentHashDifferentModulus) { + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(auto key_pair_1, GetStrongRsaKeys2048()); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(auto key_pair_2, + GetAnotherStrongRsaKeys2048()); + std::string metadata = "md"; + // Check that same metadata and different modulus result in different + // hashes. + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + auto rsa_private_key_1, + AnonymousTokensRSAPrivateKeyToRSA(key_pair_1.second)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + bssl::UniquePtr<BIGNUM> exp1, + PublicMetadataExponent(*RSA_get0_n(rsa_private_key_1.get()), metadata)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + auto rsa_private_key_2, + AnonymousTokensRSAPrivateKeyToRSA(key_pair_2.second)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + bssl::UniquePtr<BIGNUM> exp2, + PublicMetadataExponent(*RSA_get0_n(rsa_private_key_2.get()), metadata)); + EXPECT_NE(BN_cmp(exp1.get(), exp2.get()), 0); +} + +TEST(PublicMetadataCryptoUtilsTest, + FixedTestRSAPublicKeyToRSAUnderPublicMetadata) { + const auto public_key_and_metadata = GetFixedTestPublicKeyAndPublicMetadata(); + const std::string expected_new_e_str = + GetFixedTestNewPublicKeyExponentUnderPublicMetadata(); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + bssl::UniquePtr<BIGNUM> rsa_modulus, + StringToBignum(public_key_and_metadata.first.n())); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(bssl::UniquePtr<BIGNUM> expected_new_e, + StringToBignum(expected_new_e_str)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + bssl::UniquePtr<RSA> modified_rsa_public_key, + RSAPublicKeyToRSAUnderPublicMetadata(public_key_and_metadata.first, + public_key_and_metadata.second)); + EXPECT_EQ( + BN_cmp(RSA_get0_n(modified_rsa_public_key.get()), rsa_modulus.get()), 0); + EXPECT_EQ( + BN_cmp(RSA_get0_e(modified_rsa_public_key.get()), expected_new_e.get()), + 0); +} + +} // namespace +} // namespace anonymous_tokens +} // namespace private_membership
diff --git a/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/rsa_blinder.cc b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/rsa_blinder.cc new file mode 100644 index 0000000..abfc34f --- /dev/null +++ b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/rsa_blinder.cc
@@ -0,0 +1,342 @@ +// Copyright 2023 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 "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/rsa_blinder.h" + +#include <memory> +#include <string> +#include <utility> +#include <vector> + +#include "absl/status/status.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/constants.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/crypto_utils.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/public_metadata_crypto_utils.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/status_utils.h" +#include "quiche/blind_sign_auth/anonymous_tokens/proto/anonymous_tokens.pb.h" +#include "openssl/digest.h" +#include "openssl/rsa.h" + +namespace private_membership { +namespace anonymous_tokens { + +absl::StatusOr<std::unique_ptr<RsaBlinder>> RsaBlinder::New( + const RSABlindSignaturePublicKey& public_key, + absl::string_view public_metadata) { + RSAPublicKey rsa_public_key_proto; + if (!rsa_public_key_proto.ParseFromString( + public_key.serialized_public_key())) { + return absl::InvalidArgumentError("Public key is malformed."); + } + + // Convert to OpenSSL RSA. + // + // If public metadata is empty, RSAPublicKeyToRSAUnderPublicMetadata returns + // bssl::UniquePtr<RSA> valid for no public metadata. + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<RSA> rsa_public_key, + RSAPublicKeyToRSAUnderPublicMetadata( + rsa_public_key_proto, public_metadata)); + + // Owned by BoringSSL. + const EVP_MD* sig_hash; + if (public_key.sig_hash_type() == AT_HASH_TYPE_SHA256) { + sig_hash = EVP_sha256(); + } else if (public_key.sig_hash_type() == AT_HASH_TYPE_SHA384) { + sig_hash = EVP_sha384(); + } else { + return absl::InvalidArgumentError("Signature hash type is not safe."); + } + + // Owned by BoringSSL. + const EVP_MD* mgf1_hash; + if (public_key.mask_gen_function() == AT_MGF_SHA256) { + mgf1_hash = EVP_sha256(); + } else if (public_key.mask_gen_function() == AT_MGF_SHA384) { + mgf1_hash = EVP_sha384(); + } else { + return absl::InvalidArgumentError("Mask generation function is not safe."); + } + + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> r, NewBigNum()); + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> r_inv_mont, NewBigNum()); + + // Limit r between [2, n) so that an r of 1 never happens. An r of 1 doesn't + // blind. + if (BN_rand_range_ex(r.get(), 2, RSA_get0_n(rsa_public_key.get())) != + kBsslSuccess) { + return absl::InternalError( + "BN_rand_range_ex failed when called from RsaBlinder::New."); + } + + bssl::UniquePtr<BN_CTX> bn_ctx(BN_CTX_new()); + if (!bn_ctx) { + return absl::InternalError("BN_CTX_new failed."); + } + + bssl::UniquePtr<BN_MONT_CTX> bn_mont_ctx(BN_MONT_CTX_new_for_modulus( + RSA_get0_n(rsa_public_key.get()), bn_ctx.get())); + if (!bn_mont_ctx) { + return absl::InternalError("BN_MONT_CTX_new_for_modulus failed."); + } + + // We wish to compute r^-1 in the Montgomery domain, or r^-1 R mod n. This is + // can be done with BN_mod_inverse_blinded followed by BN_to_montgomery, but + // it is equivalent and slightly more efficient to first compute r R^-1 mod n + // with BN_from_montgomery, and then inverting that to give r^-1 R mod n. + int is_r_not_invertible = 0; + if (BN_from_montgomery(r_inv_mont.get(), r.get(), bn_mont_ctx.get(), + bn_ctx.get()) != kBsslSuccess || + BN_mod_inverse_blinded(r_inv_mont.get(), &is_r_not_invertible, + r_inv_mont.get(), bn_mont_ctx.get(), + bn_ctx.get()) != kBsslSuccess) { + return absl::InternalError( + absl::StrCat("BN_mod_inverse failed when called from RsaBlinder::New, " + "is_r_not_invertible = ", + is_r_not_invertible)); + } + + return absl::WrapUnique(new RsaBlinder( + std::move(r), std::move(r_inv_mont), std::move(rsa_public_key), + std::move(bn_mont_ctx), sig_hash, mgf1_hash, public_key.salt_length(), + public_metadata)); +} + +RsaBlinder::RsaBlinder(bssl::UniquePtr<BIGNUM> r, + bssl::UniquePtr<BIGNUM> r_inv_mont, + bssl::UniquePtr<RSA> public_key, + bssl::UniquePtr<BN_MONT_CTX> mont_n, + const EVP_MD* sig_hash, const EVP_MD* mgf1_hash, + int32_t salt_length, absl::string_view public_metadata) + : r_(std::move(r)), + r_inv_mont_(std::move(r_inv_mont)), + public_key_(std::move(public_key)), + mont_n_(std::move(mont_n)), + sig_hash_(sig_hash), + mgf1_hash_(mgf1_hash), + salt_length_(salt_length), + public_metadata_(public_metadata), + message_(""), + blinder_state_(RsaBlinder::BlinderState::kCreated) {} + +absl::StatusOr<std::string> RsaBlinder::Blind(const absl::string_view message) { + // Check that the blinder state was kCreated + if (blinder_state_ != RsaBlinder::BlinderState::kCreated) { + return absl::FailedPreconditionError( + "RsaBlinder is in wrong state to blind message."); + } + + if (message.empty()) { + return absl::InvalidArgumentError("Input message string is empty."); + } + ANON_TOKENS_ASSIGN_OR_RETURN(std::string digest_str, + ComputeHash(message, *sig_hash_)); + std::vector<uint8_t> digest(digest_str.begin(), digest_str.end()); + + // Construct the PSS padded message, using the same workflow as BoringSSL's + // RSA_sign_pss_mgf1 for processing the message (but not signing the message): + // google3/third_party/openssl/boringssl/src/crypto/fipsmodule/rsa/rsa.c?l=557 + if (digest.size() != EVP_MD_size(sig_hash_)) { + return absl::InternalError("Invalid input message length."); + } + + // Allocate for padded length + const int padded_len = RSA_size(public_key_.get()); + std::vector<uint8_t> padded(padded_len); + + // The |md| and |mgf1_md| arguments identify the hash used to calculate + // |digest| and the MGF1 hash, respectively. If |mgf1_md| is NULL, |md| is + // used. |salt_len| specifies the expected salt length in bytes. If |salt_len| + // is -1, then the salt length is the same as the hash length. If -2, then the + // salt length is maximal given the size of |rsa|. If unsure, use -1. + if (RSA_padding_add_PKCS1_PSS_mgf1( + /*rsa=*/public_key_.get(), /*EM=*/padded.data(), + /*mHash=*/digest.data(), /*Hash=*/sig_hash_, /*mgf1Hash=*/mgf1_hash_, + /*sLen=*/salt_length_) != kBsslSuccess) { + return absl::InternalError( + "RSA_padding_add_PKCS1_PSS_mgf1 failed when called from " + "RsaBlinder::Blind"); + } + + bssl::UniquePtr<BN_CTX> bn_ctx(BN_CTX_new()); + if (!bn_ctx) { + return absl::InternalError("BN_CTX_new failed."); + } + + std::string encoded_message(padded.begin(), padded.end()); + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> encoded_message_bn, + StringToBignum(encoded_message)); + + // Take `r^e mod n`. This is an equivalent operation to RSA_encrypt, without + // extra encode/decode trips. + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> rE, NewBigNum()); + if (BN_mod_exp_mont(rE.get(), r_.get(), RSA_get0_e(public_key_.get()), + RSA_get0_n(public_key_.get()), bn_ctx.get(), + mont_n_.get()) != kBsslSuccess) { + return absl::InternalError( + "BN_mod_exp_mont failed when called from RsaBlinder::Blind."); + } + + // Do `encoded_message*r^e mod n`. + // + // To avoid leaking side channels, we use Montgomery reduction. This would be + // FromMontgomery(ModMulMontgomery(ToMontgomery(m), ToMontgomery(r^e))). + // However, this is equivalent to ModMulMontgomery(m, ToMontgomery(r^e)). + // Each BN_mod_mul_montgomery removes a factor of R, so by having only one + // input in the Montgomery domain, we save a To/FromMontgomery pair. + // + // Internally, BN_mod_exp_mont actually computes r^e in the Montgomery domain + // and converts it out, but there is no public API for this, so we perform an + // extra conversion. + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> multiplication_res, + NewBigNum()); + if (BN_to_montgomery(multiplication_res.get(), rE.get(), mont_n_.get(), + bn_ctx.get()) != kBsslSuccess || + BN_mod_mul_montgomery(multiplication_res.get(), encoded_message_bn.get(), + multiplication_res.get(), mont_n_.get(), + bn_ctx.get()) != kBsslSuccess) { + return absl::InternalError( + "BN_mod_mul failed when called from RsaBlinder::Blind."); + } + + absl::StatusOr<std::string> blinded_msg = BignumToString( + *multiplication_res, BN_num_bytes(RSA_get0_n(public_key_.get()))); + + // Update RsaBlinder state to kBlinded + blinder_state_ = RsaBlinder::BlinderState::kBlinded; + + return blinded_msg; +} + +// Unblinds `blind_signature`. +absl::StatusOr<std::string> RsaBlinder::Unblind( + const absl::string_view blind_signature) { + if (blinder_state_ != RsaBlinder::BlinderState::kBlinded) { + return absl::FailedPreconditionError( + "RsaBlinder is in wrong state to unblind signature."); + } + const size_t mod_size = RSA_size(public_key_.get()); + // Parse the signed_blinded_data as BIGNUM. + if (blind_signature.size() != mod_size) { + return absl::InternalError(absl::StrCat( + "Expected blind signature size = ", mod_size, + " actual blind signature size = ", blind_signature.size(), " bytes.")); + } + + bssl::UniquePtr<BN_CTX> bn_ctx(BN_CTX_new()); + if (!bn_ctx) { + return absl::InternalError("BN_CTX_new failed."); + } + + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> signed_big_num, + StringToBignum(blind_signature)); + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> unblinded_sig_big, + NewBigNum()); + // Do `signed_message*r^-1 mod n`. + // + // To avoid leaking side channels, we use Montgomery reduction. This would be + // FromMontgomery(ModMulMontgomery(ToMontgomery(m), ToMontgomery(r^-1))). + // However, this is equivalent to ModMulMontgomery(m, ToMontgomery(r^-1)). + // Each BN_mod_mul_montgomery removes a factor of R, so by having only one + // input in the Montgomery domain, we save a To/FromMontgomery pair. + if (BN_mod_mul_montgomery(unblinded_sig_big.get(), signed_big_num.get(), + r_inv_mont_.get(), mont_n_.get(), + bn_ctx.get()) != kBsslSuccess) { + return absl::InternalError( + "BN_mod_mul failed when called from RsaBlinder::Unblind."); + } + absl::StatusOr<std::string> unblinded_signed_message = BignumToString( + *unblinded_sig_big, + /*output_len=*/BN_num_bytes(RSA_get0_n(public_key_.get()))); + blinder_state_ = RsaBlinder::BlinderState::kUnblinded; + return unblinded_signed_message; +} + +absl::Status RsaBlinder::Verify(absl::string_view signature, + absl::string_view message) { + if (message.empty()) { + return absl::InvalidArgumentError("Input message string is empty."); + } + ANON_TOKENS_ASSIGN_OR_RETURN(std::string message_digest, + ComputeHash(message, *sig_hash_)); + + const size_t kHashSize = EVP_MD_size(sig_hash_); + // Make sure the size of the digest is correct. + if (message_digest.size() != kHashSize) { + return absl::InvalidArgumentError( + absl::StrCat("Size of the digest doesn't match the one " + "of the hashing algorithm; expected ", + kHashSize, " got ", message_digest.size())); + } + const int kRsaModulusSize = RSA_size(public_key_.get()); + if (signature.size() != kRsaModulusSize) { + return absl::InvalidArgumentError( + "Signature size not equal to modulus size."); + } + + std::string recovered_message_digest(kRsaModulusSize, 0); + if (public_metadata_.empty()) { + int recovered_message_digest_size = RSA_public_decrypt( + /*flen=*/signature.size(), + /*from=*/reinterpret_cast<const uint8_t*>(signature.data()), + /*to=*/ + reinterpret_cast<uint8_t*>(recovered_message_digest.data()), + /*rsa=*/public_key_.get(), + /*padding=*/RSA_NO_PADDING); + if (recovered_message_digest_size != kRsaModulusSize) { + return absl::InvalidArgumentError( + absl::StrCat("Invalid signature size (likely an incorrect key is " + "used); expected ", + kRsaModulusSize, " got ", recovered_message_digest_size, + ": ", GetSslErrors())); + } + } else { + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> signature_bn, + StringToBignum(signature)); + if (BN_ucmp(signature_bn.get(), RSA_get0_n(public_key_.get())) >= 0) { + return absl::InvalidArgumentError("Data too large for modulus."); + } + ANON_TOKENS_ASSIGN_OR_RETURN(BnCtxPtr bn_ctx, GetAndStartBigNumCtx()); + bssl::UniquePtr<BN_MONT_CTX> bn_mont_ctx(BN_MONT_CTX_new_for_modulus( + RSA_get0_n(public_key_.get()), bn_ctx.get())); + if (!bn_mont_ctx) { + return absl::InternalError("BN_MONT_CTX_new_for_modulus failed."); + } + ANON_TOKENS_ASSIGN_OR_RETURN( + bssl::UniquePtr<BIGNUM> recovered_message_digest_bn, NewBigNum()); + if (BN_mod_exp_mont(recovered_message_digest_bn.get(), signature_bn.get(), + RSA_get0_e(public_key_.get()), + RSA_get0_n(public_key_.get()), bn_ctx.get(), + bn_mont_ctx.get()) != kBsslSuccess) { + return absl::InternalError("Exponentiation failed."); + } + ANON_TOKENS_ASSIGN_OR_RETURN( + recovered_message_digest, + BignumToString(*recovered_message_digest_bn, kRsaModulusSize)); + } + + if (RSA_verify_PKCS1_PSS_mgf1( + public_key_.get(), + reinterpret_cast<const uint8_t*>(&message_digest[0]), sig_hash_, + mgf1_hash_, + reinterpret_cast<const uint8_t*>(&recovered_message_digest[0]), + salt_length_) != kBsslSuccess) { + return absl::InvalidArgumentError( + absl::StrCat("PSS padding verification failed.", GetSslErrors())); + } + + return absl::OkStatus(); +} + +} // namespace anonymous_tokens +} // namespace private_membership
diff --git a/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/rsa_blinder.h b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/rsa_blinder.h new file mode 100644 index 0000000..e58226f --- /dev/null +++ b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/rsa_blinder.h
@@ -0,0 +1,78 @@ +// Copyright 2023 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. + +#ifndef THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CRYPTO_RSA_BLINDER_H_ +#define THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CRYPTO_RSA_BLINDER_H_ + +#include <stdint.h> + +#include <memory> +#include <string> + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/blinder.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/crypto_utils.h" +#include "quiche/blind_sign_auth/anonymous_tokens/proto/anonymous_tokens.pb.h" +#include "quiche/common/platform/api/quiche_export.h" + +namespace private_membership { +namespace anonymous_tokens { + +// RsaBlinder is able to blind a token, and unblind it after it has been signed. +class QUICHE_EXPORT RsaBlinder : public Blinder { + public: + static absl::StatusOr<std::unique_ptr<RsaBlinder>> New( + const RSABlindSignaturePublicKey& public_key, + absl::string_view public_metadata = ""); + + // Blind `message` using n and e derived from an RSA public key. + // `message` will first be encoded with the EMSA-PSS operation. + // This encoding operation matches that which is used by RsaVerifier. + absl::StatusOr<std::string> Blind(absl::string_view message) override; + + // Unblinds `blind_signature`. + absl::StatusOr<std::string> Unblind( + absl::string_view blind_signature) override; + + // Verifies a signature. + absl::Status Verify(absl::string_view signature, absl::string_view message); + + private: + // Use `New` to construct + RsaBlinder(bssl::UniquePtr<BIGNUM> r, bssl::UniquePtr<BIGNUM> r_inv_mont, + bssl::UniquePtr<RSA> public_key, + bssl::UniquePtr<BN_MONT_CTX> mont_n, const EVP_MD* sig_hash_, + const EVP_MD* mgf1_hash_, int32_t salt_length_, + absl::string_view public_metadata); + + const bssl::UniquePtr<BIGNUM> r_; + // r^-1 mod n in the Montgomery domain + const bssl::UniquePtr<BIGNUM> r_inv_mont_; + const bssl::UniquePtr<RSA> public_key_; + const bssl::UniquePtr<BN_MONT_CTX> mont_n_; + const EVP_MD* sig_hash_; // Owned by BoringSSL. + const EVP_MD* mgf1_hash_; // Owned by BoringSSL. + const int32_t salt_length_; + const absl::string_view public_metadata_; + + std::string message_; + BlinderState blinder_state_; +}; + +} // namespace anonymous_tokens +} // namespace private_membership + +#endif // THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CRYPTO_RSA_BLINDER_H_
diff --git a/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/rsa_blinder_test.cc b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/rsa_blinder_test.cc new file mode 100644 index 0000000..b9d9fad --- /dev/null +++ b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/rsa_blinder_test.cc
@@ -0,0 +1,329 @@ +// Copyright 2023 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 "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/rsa_blinder.h" + +#include <memory> +#include <string> +#include <utility> + +#include "quiche/common/platform/api/quiche_test.h" +#include "quiche/common/test_tools/quiche_test_utils.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/constants.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/crypto_utils.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/testing_utils.h" +#include "quiche/blind_sign_auth/anonymous_tokens/proto/anonymous_tokens.pb.h" +#include "openssl/base.h" +#include "openssl/rsa.h" + +namespace private_membership { +namespace anonymous_tokens { +namespace { + +using CreateTestKeyFunction = absl::StatusOr< + std::pair<bssl::UniquePtr<RSA>, RSABlindSignaturePublicKey>>(); + +absl::StatusOr<std::pair<bssl::UniquePtr<RSA>, RSABlindSignaturePublicKey>> +CreateStandardTestKey() { + return CreateTestKey(); +} + +absl::StatusOr<std::pair<bssl::UniquePtr<RSA>, RSABlindSignaturePublicKey>> +CreateShorterTestKey() { + return CreateTestKey(/*key_size=*/256); +} + +absl::StatusOr<std::pair<bssl::UniquePtr<RSA>, RSABlindSignaturePublicKey>> +CreateLongerTestKey() { + return CreateTestKey(/*key_size=*/544); +} + +absl::StatusOr<std::pair<bssl::UniquePtr<RSA>, RSABlindSignaturePublicKey>> +CreateSHA256TestKey() { + return CreateTestKey(/*key_size=*/512, AT_HASH_TYPE_SHA256, AT_MGF_SHA256); +} + +absl::StatusOr<std::pair<bssl::UniquePtr<RSA>, RSABlindSignaturePublicKey>> +CreateLongerSaltTestKey() { + return CreateTestKey(/*key_size=*/512, AT_HASH_TYPE_SHA384, AT_MGF_SHA384, + /*salt_length=*/64); +} + +class RsaBlinderTest : public testing::TestWithParam<CreateTestKeyFunction*> { + protected: + void SetUp() override { + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(auto test_key, (*GetParam())()); + rsa_key_ = std::move(test_key.first); + public_key_ = std::move(test_key.second); + } + + RSABlindSignaturePublicKey public_key_; + bssl::UniquePtr<RSA> rsa_key_; +}; + +TEST_P(RsaBlinderTest, BlindSignUnblindEnd2EndTest) { + const absl::string_view message = "Hello World!"; + + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(std::unique_ptr<RsaBlinder> blinder, + RsaBlinder::New(public_key_)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(std::string blinded_message, + blinder->Blind(message)); + EXPECT_NE(blinded_message, message); + + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(std::string blinded_signature, + TestSign(blinded_message, rsa_key_.get())); + EXPECT_NE(blinded_signature, blinded_message); + EXPECT_NE(blinded_signature, message); + + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(std::string signature, + blinder->Unblind(blinded_signature)); + EXPECT_NE(signature, blinded_signature); + EXPECT_NE(signature, blinded_message); + EXPECT_NE(signature, message); + + QUICHE_EXPECT_OK(blinder->Verify(signature, message)); +} + +TEST_P(RsaBlinderTest, DoubleBlindingFailure) { + const absl::string_view message = "Hello World2!"; + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(std::unique_ptr<RsaBlinder> blinder, + RsaBlinder::New(public_key_)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(const std::string blinded_message, + blinder->Blind(message)); + // Blind the blinded_message + absl::StatusOr<std::string> result = blinder->Blind(blinded_message); + EXPECT_EQ(result.status().code(), absl::StatusCode::kFailedPrecondition); + EXPECT_THAT(result.status().message(), testing::HasSubstr("wrong state")); + // Blind a new message + const absl::string_view new_message = "Hello World3!"; + result = blinder->Blind(new_message); + EXPECT_EQ(result.status().code(), absl::StatusCode::kFailedPrecondition); + EXPECT_THAT(result.status().message(), testing::HasSubstr("wrong state")); +} + +TEST_P(RsaBlinderTest, DoubleUnblindingFailure) { + const absl::string_view message = "Hello World2!"; + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(std::unique_ptr<RsaBlinder> blinder, + RsaBlinder::New(public_key_)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(const std::string blinded_message, + blinder->Blind(message)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(const std::string blinded_signature, + TestSign(blinded_message, rsa_key_.get())); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(std::string signature, + blinder->Unblind(blinded_signature)); + // Unblind the unblinded signature + absl::StatusOr<std::string> result = blinder->Unblind(signature); + EXPECT_EQ(result.status().code(), absl::StatusCode::kFailedPrecondition); + EXPECT_THAT(result.status().message(), testing::HasSubstr("wrong state")); + // Unblind the blinded_signature again + result = blinder->Unblind(signature); + EXPECT_EQ(result.status().code(), absl::StatusCode::kFailedPrecondition); + EXPECT_THAT(result.status().message(), testing::HasSubstr("wrong state")); +} + +TEST_P(RsaBlinderTest, InvalidSignature) { + const absl::string_view message = "Hello World2!"; + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(std::unique_ptr<RsaBlinder> blinder, + RsaBlinder::New(public_key_)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(const std::string blinded_message, + blinder->Blind(message)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(const std::string blinded_signature, + TestSign(blinded_message, rsa_key_.get())); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(std::string signature, + blinder->Unblind(blinded_signature)); + QUICHE_EXPECT_OK(blinder->Verify(signature, message)); + + // Invalidate the signature by replacing the last 10 characters by 10 '0's + for (int i = 0; i < 10; i++) { + signature.pop_back(); + } + for (int i = 0; i < 10; i++) { + signature.push_back('0'); + } + + absl::Status result = blinder->Verify(signature, message); + EXPECT_EQ(result.code(), absl::StatusCode::kInvalidArgument); + EXPECT_THAT(result.message(), testing::HasSubstr("verification failed")); +} + +TEST_P(RsaBlinderTest, InvalidVerificationKey) { + const absl::string_view message = "Hello World4!"; + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(std::unique_ptr<RsaBlinder> blinder, + RsaBlinder::New(public_key_)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(const std::string blinded_message, + blinder->Blind(message)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(const std::string blinded_signature, + TestSign(blinded_message, rsa_key_.get())); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(std::string signature, + blinder->Unblind(blinded_signature)); + + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(auto bad_key, CreateTestKey()); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(std::unique_ptr<RsaBlinder> bad_blinder, + RsaBlinder::New(bad_key.second)); + EXPECT_THAT(bad_blinder->Verify(signature, message).code(), + absl::StatusCode::kInvalidArgument); +} + +INSTANTIATE_TEST_SUITE_P(RsaBlinderTest, RsaBlinderTest, + testing::Values(&CreateStandardTestKey, + &CreateShorterTestKey, + &CreateLongerTestKey, + &CreateSHA256TestKey, + &CreateLongerSaltTestKey)); + +using CreateTestKeyPairFunction = + absl::StatusOr<std::pair<RSAPublicKey, RSAPrivateKey>>(); + +class RsaBlinderWithPublicMetadataTest + : public testing::TestWithParam<CreateTestKeyPairFunction*> { + protected: + void SetUp() override { + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(auto test_key, (*GetParam())()); + RSABlindSignaturePublicKey public_key; + public_key.set_sig_hash_type(HashType::AT_HASH_TYPE_SHA384); + public_key.set_mask_gen_function(AT_MGF_SHA384); + public_key.set_salt_length(kSaltLengthInBytes48); + public_key.set_serialized_public_key( + std::move(test_key.first).SerializeAsString()); + public_key_ = std::move(public_key); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + rsa_key_, AnonymousTokensRSAPrivateKeyToRSA(test_key.second)); + } + + RSABlindSignaturePublicKey public_key_; + bssl::UniquePtr<RSA> rsa_key_; +}; + +TEST_P(RsaBlinderWithPublicMetadataTest, + BlindSignUnblindWithPublicMetadataEnd2EndTest) { + const absl::string_view message = "Hello World!"; + const absl::string_view public_metadata = "pubmd!"; + + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + std::unique_ptr<RsaBlinder> blinder, + RsaBlinder::New(public_key_, public_metadata)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(std::string blinded_message, + blinder->Blind(message)); + EXPECT_NE(blinded_message, message); + + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + std::string blinded_signature, + TestSignWithPublicMetadata(blinded_message, public_metadata, *rsa_key_)); + EXPECT_NE(blinded_signature, blinded_message); + EXPECT_NE(blinded_signature, message); + + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(std::string signature, + blinder->Unblind(blinded_signature)); + EXPECT_NE(signature, blinded_signature); + EXPECT_NE(signature, blinded_message); + EXPECT_NE(signature, message); + + QUICHE_EXPECT_OK(blinder->Verify(signature, message)); +} + +TEST_P(RsaBlinderWithPublicMetadataTest, WrongPublicMetadata) { + const absl::string_view message = "Hello World!"; + const absl::string_view public_metadata = "pubmd!"; + const absl::string_view public_metadata_2 = "pubmd2"; + + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + std::unique_ptr<RsaBlinder> blinder, + RsaBlinder::New(public_key_, public_metadata)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(std::string blinded_message, + blinder->Blind(message)); + EXPECT_NE(blinded_message, message); + + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + std::string blinded_signature, + TestSignWithPublicMetadata(blinded_message, public_metadata_2, + *rsa_key_)); + EXPECT_NE(blinded_signature, blinded_message); + EXPECT_NE(blinded_signature, message); + + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(std::string signature, + blinder->Unblind(blinded_signature)); + EXPECT_NE(signature, blinded_signature); + EXPECT_NE(signature, blinded_message); + EXPECT_NE(signature, message); + EXPECT_THAT( + blinder->Verify(signature, message), + quiche::test::StatusIs(absl::StatusCode::kInvalidArgument, + ::testing::HasSubstr("verification failed"))); +} + +TEST_P(RsaBlinderWithPublicMetadataTest, NoPublicMetadataForSigning) { + const absl::string_view message = "Hello World!"; + const absl::string_view public_metadata = "pubmd!"; + + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + std::unique_ptr<RsaBlinder> blinder, + RsaBlinder::New(public_key_, public_metadata)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(std::string blinded_message, + blinder->Blind(message)); + EXPECT_NE(blinded_message, message); + + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(std::string blinded_signature, + TestSign(blinded_message, rsa_key_.get())); + EXPECT_NE(blinded_signature, blinded_message); + EXPECT_NE(blinded_signature, message); + + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(std::string signature, + blinder->Unblind(blinded_signature)); + EXPECT_NE(signature, blinded_signature); + EXPECT_NE(signature, blinded_message); + EXPECT_NE(signature, message); + EXPECT_THAT( + blinder->Verify(signature, message), + quiche::test::StatusIs(absl::StatusCode::kInvalidArgument, + ::testing::HasSubstr("verification failed"))); +} + +TEST_P(RsaBlinderWithPublicMetadataTest, NoPublicMetadataInBlinding) { + const absl::string_view message = "Hello World!"; + const absl::string_view public_metadata = "pubmd!"; + + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(std::unique_ptr<RsaBlinder> blinder, + RsaBlinder::New(public_key_)); + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(std::string blinded_message, + blinder->Blind(message)); + EXPECT_NE(blinded_message, message); + + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN( + std::string blinded_signature, + TestSignWithPublicMetadata(blinded_message, public_metadata, *rsa_key_)); + EXPECT_NE(blinded_signature, blinded_message); + EXPECT_NE(blinded_signature, message); + + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(std::string signature, + blinder->Unblind(blinded_signature)); + EXPECT_NE(signature, blinded_signature); + EXPECT_NE(signature, blinded_message); + EXPECT_NE(signature, message); + EXPECT_THAT( + blinder->Verify(signature, message), + quiche::test::StatusIs(absl::StatusCode::kInvalidArgument, + ::testing::HasSubstr("verification failed"))); +} + +INSTANTIATE_TEST_SUITE_P( + RsaBlinderWithPublicMetadataTest, RsaBlinderWithPublicMetadataTest, + testing::Values(&GetStrongRsaKeys2048, &GetAnotherStrongRsaKeys2048, + &GetStrongRsaKeys3072, &GetStrongRsaKeys4096)); + +} // namespace +} // namespace anonymous_tokens +} // namespace private_membership
diff --git a/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/status_utils.h b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/status_utils.h new file mode 100644 index 0000000..e698702 --- /dev/null +++ b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/status_utils.h
@@ -0,0 +1,49 @@ +// Copyright 2023 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. + +#ifndef THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CRYPTO_STATUS_UTILS_H_ +#define THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CRYPTO_STATUS_UTILS_H_ + +#include "absl/base/optimization.h" +#include "absl/status/status.h" + +namespace private_membership { +namespace anonymous_tokens { + +#define _ANON_TOKENS_STATUS_MACROS_CONCAT_NAME(x, y) \ + _ANON_TOKENS_STATUS_MACROS_CONCAT_IMPL(x, y) +#define _ANON_TOKENS_STATUS_MACROS_CONCAT_IMPL(x, y) x##y + +#define ANON_TOKENS_ASSIGN_OR_RETURN(lhs, rexpr) \ + _ANON_TOKENS_ASSIGN_OR_RETURN_IMPL( \ + _ANON_TOKENS_STATUS_MACROS_CONCAT_NAME(_status_or_val, __LINE__), lhs, \ + rexpr) + +#define _ANON_TOKENS_ASSIGN_OR_RETURN_IMPL(statusor, lhs, rexpr) \ + auto statusor = (rexpr); \ + if (ABSL_PREDICT_FALSE(!statusor.ok())) { \ + return statusor.status(); \ + } \ + lhs = std::move(statusor.value()) + +#define ANON_TOKENS_RETURN_IF_ERROR(expr) \ + do { \ + auto _status = (expr); \ + if (ABSL_PREDICT_FALSE(!_status.ok())) return _status; \ + } while (0) + +} // namespace anonymous_tokens +} // namespace private_membership + +#endif // THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CRYPTO_STATUS_UTILS_H_
diff --git a/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/testing_utils.cc b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/testing_utils.cc new file mode 100644 index 0000000..29212c5 --- /dev/null +++ b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/testing_utils.cc
@@ -0,0 +1,287 @@ +// Copyright 2023 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 "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/testing_utils.h" + +#include <stddef.h> +#include <stdint.h> + +#include <fstream> +#include <memory> +#include <string> +#include <utility> +#include <vector> + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/constants.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/crypto_utils.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/public_metadata_crypto_utils.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/status_utils.h" +#include "openssl/rsa.h" + +namespace private_membership { +namespace anonymous_tokens { + +namespace { + +absl::StatusOr<std::string> ReadFileToString(absl::string_view path) { + std::ifstream file((std::string(path))); + if (!file.is_open()) { + return absl::InternalError("Reading file failed."); + } + std::ostringstream ss; + ss << file.rdbuf(); + return ss.str(); +} + +absl::StatusOr<std::pair<RSAPublicKey, RSAPrivateKey>> ParseRsaKeysFromFile( + absl::string_view path) { + ANON_TOKENS_ASSIGN_OR_RETURN(std::string text_proto, ReadFileToString(path)); + RSAPrivateKey private_key; + if (!private_key.ParseFromString(text_proto)) { + return absl::InternalError("Parsing text proto failed."); + } + RSAPublicKey public_key; + public_key.set_n(private_key.n()); + public_key.set_e(private_key.e()); + return std::make_pair(std::move(public_key), std::move(private_key)); +} + +absl::StatusOr<bssl::UniquePtr<RSA>> GenerateRSAKey(int modulus_bit_size, + const BIGNUM& e) { + bssl::UniquePtr<RSA> rsa(RSA_new()); + if (!rsa.get()) { + return absl::InternalError( + absl::StrCat("RSA_new failed: ", GetSslErrors())); + } + if (RSA_generate_key_ex(rsa.get(), modulus_bit_size, &e, + /*cb=*/nullptr) != kBsslSuccess) { + return absl::InternalError( + absl::StrCat("Error generating private key: ", GetSslErrors())); + } + return rsa; +} + +} // namespace + +absl::StatusOr<std::pair<bssl::UniquePtr<RSA>, RSABlindSignaturePublicKey>> +CreateTestKey(int key_size, HashType sig_hash, MaskGenFunction mfg1_hash, + int salt_length, MessageMaskType message_mask_type, + int message_mask_size) { + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> rsa_f4, NewBigNum()); + BN_set_u64(rsa_f4.get(), RSA_F4); + + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<RSA> rsa_key, + GenerateRSAKey(key_size * 8, *rsa_f4)); + + RSAPublicKey rsa_public_key; + ANON_TOKENS_ASSIGN_OR_RETURN( + *rsa_public_key.mutable_n(), + BignumToString(*RSA_get0_n(rsa_key.get()), key_size)); + ANON_TOKENS_ASSIGN_OR_RETURN( + *rsa_public_key.mutable_e(), + BignumToString(*RSA_get0_e(rsa_key.get()), key_size)); + + RSABlindSignaturePublicKey public_key; + public_key.set_serialized_public_key(rsa_public_key.SerializeAsString()); + public_key.set_sig_hash_type(sig_hash); + public_key.set_mask_gen_function(mfg1_hash); + public_key.set_salt_length(salt_length); + public_key.set_key_size(key_size); + public_key.set_message_mask_type(message_mask_type); + public_key.set_message_mask_size(message_mask_size); + + return std::make_pair(std::move(rsa_key), std::move(public_key)); +} + +absl::StatusOr<std::string> TestSign(const absl::string_view blinded_data, + RSA* rsa_key) { + if (blinded_data.empty()) { + return absl::InvalidArgumentError("blinded_data string is empty."); + } + const size_t mod_size = RSA_size(rsa_key); + if (blinded_data.size() != mod_size) { + return absl::InternalError(absl::StrCat( + "Expected blind data size = ", mod_size, + " actual blind data size = ", blinded_data.size(), " bytes.")); + } + // Compute a raw RSA signature. + std::string signature(mod_size, 0); + size_t out_len; + if (RSA_sign_raw(/*rsa=*/rsa_key, /*out_len=*/&out_len, + /*out=*/reinterpret_cast<uint8_t*>(&signature[0]), + /*max_out=*/mod_size, + /*in=*/reinterpret_cast<const uint8_t*>(&blinded_data[0]), + /*in_len=*/mod_size, + /*padding=*/RSA_NO_PADDING) != kBsslSuccess) { + return absl::InternalError( + "RSA_sign_raw failed when called from RsaBlindSigner::Sign"); + } + if (out_len != mod_size && out_len == signature.size()) { + return absl::InternalError(absl::StrCat( + "Expected value of out_len = ", mod_size, + " bytes, actual value of out_len and signature.size() = ", out_len, + " and ", signature.size(), " bytes.")); + } + return signature; +} + +absl::StatusOr<std::string> TestSignWithPublicMetadata( + const absl::string_view blinded_data, absl::string_view public_metadata, + const RSA& rsa_key) { + if (public_metadata.empty()) { + return absl::InvalidArgumentError("Public Metadata is empty."); + } else if (blinded_data.empty()) { + return absl::InvalidArgumentError("blinded_data string is empty."); + } else if (blinded_data.size() != RSA_size(&rsa_key)) { + return absl::InternalError(absl::StrCat( + "Expected blind data size = ", RSA_size(&rsa_key), + " actual blind data size = ", blinded_data.size(), " bytes.")); + } + ANON_TOKENS_ASSIGN_OR_RETURN( + bssl::UniquePtr<BIGNUM> new_e, + ComputeFinalExponentUnderPublicMetadata( + *RSA_get0_n(&rsa_key), *RSA_get0_e(&rsa_key), public_metadata)); + // Compute phi(p) = p-1 + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> phi_p, NewBigNum()); + if (BN_sub(phi_p.get(), RSA_get0_p(&rsa_key), BN_value_one()) != 1) { + return absl::InternalError( + absl::StrCat("Unable to compute phi(p): ", GetSslErrors())); + } + // Compute phi(q) = q-1 + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> phi_q, NewBigNum()); + if (BN_sub(phi_q.get(), RSA_get0_q(&rsa_key), BN_value_one()) != 1) { + return absl::InternalError( + absl::StrCat("Unable to compute phi(q): ", GetSslErrors())); + } + // Compute phi(n) = phi(p)*phi(q) + ANON_TOKENS_ASSIGN_OR_RETURN(auto ctx, GetAndStartBigNumCtx()); + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> phi_n, NewBigNum()); + if (BN_mul(phi_n.get(), phi_p.get(), phi_q.get(), ctx.get()) != 1) { + return absl::InternalError( + absl::StrCat("Unable to compute phi(n): ", GetSslErrors())); + } + // Compute lcm(phi(p), phi(q)). + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> lcm, NewBigNum()); + if (BN_rshift1(lcm.get(), phi_n.get()) != 1) { + return absl::InternalError(absl::StrCat( + "Could not compute LCM(phi(p), phi(q)): ", GetSslErrors())); + } + // Compute the new private exponent new_d + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> new_d, NewBigNum()); + if (!BN_mod_inverse(new_d.get(), new_e.get(), lcm.get(), ctx.get())) { + return absl::InternalError( + absl::StrCat("Could not compute private exponent d: ", GetSslErrors())); + } + + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> input_bn, + StringToBignum(blinded_data)); + if (BN_ucmp(input_bn.get(), RSA_get0_n(&rsa_key)) >= 0) { + return absl::InvalidArgumentError( + "RsaSign input size too large for modulus size"); + } + + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> result, NewBigNum()); + if (!BN_mod_exp(result.get(), input_bn.get(), new_d.get(), + RSA_get0_n(&rsa_key), ctx.get())) { + return absl::InternalError( + "BN_mod_exp failed in TestSignWithPublicMetadata"); + } + + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> vrfy, NewBigNum()); + if (vrfy == nullptr || + !BN_mod_exp(vrfy.get(), result.get(), new_e.get(), RSA_get0_n(&rsa_key), + ctx.get()) || + BN_cmp(vrfy.get(), input_bn.get()) != 0) { + return absl::InternalError("Signature verification failed in RsaSign"); + } + + return BignumToString(*result, BN_num_bytes(RSA_get0_n(&rsa_key))); +} + +absl::StatusOr<std::string> EncodeMessageForTests(absl::string_view message, + RSAPublicKey public_key, + const EVP_MD* sig_hasher, + const EVP_MD* mgf1_hasher, + int32_t salt_length) { + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> rsa_modulus, + StringToBignum(public_key.n())); + ANON_TOKENS_ASSIGN_OR_RETURN(bssl::UniquePtr<BIGNUM> e, + StringToBignum(public_key.e())); + // Convert to OpenSSL RSA. + bssl::UniquePtr<RSA> rsa_public_key(RSA_new()); + if (!rsa_public_key.get()) { + return absl::InternalError( + absl::StrCat("RSA_new failed: ", GetSslErrors())); + } else if (RSA_set0_key(rsa_public_key.get(), rsa_modulus.release(), + e.release(), nullptr) != kBsslSuccess) { + return absl::InternalError( + absl::StrCat("RSA_set0_key failed: ", GetSslErrors())); + } + + const int padded_len = RSA_size(rsa_public_key.get()); + std::vector<uint8_t> padded(padded_len); + ANON_TOKENS_ASSIGN_OR_RETURN(std::string digest, + ComputeHash(message, *sig_hasher)); + if (RSA_padding_add_PKCS1_PSS_mgf1( + /*rsa=*/rsa_public_key.get(), /*EM=*/padded.data(), + /*mHash=*/reinterpret_cast<uint8_t*>(&digest[0]), /*Hash=*/sig_hasher, + /*mgf1Hash=*/mgf1_hasher, + /*sLen=*/salt_length) != kBsslSuccess) { + return absl::InternalError( + "RSA_padding_add_PKCS1_PSS_mgf1 failed when called from " + "testing_utils"); + } + std::string encoded_message(padded.begin(), padded.end()); + return encoded_message; +} + +absl::StatusOr<std::pair<RSAPublicKey, RSAPrivateKey>> GetStrongRsaKeys2048() { + ANON_TOKENS_ASSIGN_OR_RETURN( + auto key_pair, + ParseRsaKeysFromFile("quiche/blind_sign_auth/anonymous_tokens/testing/data/" + "strong_rsa_modulus2048_example.binarypb")); + return std::make_pair(std::move(key_pair.first), std::move(key_pair.second)); +} + +absl::StatusOr<std::pair<RSAPublicKey, RSAPrivateKey>> +GetAnotherStrongRsaKeys2048() { + ANON_TOKENS_ASSIGN_OR_RETURN( + auto key_pair, + ParseRsaKeysFromFile("quiche/blind_sign_auth/anonymous_tokens/testing/data/" + "strong_rsa_modulus2048_example_2.binarypb")); + return std::make_pair(std::move(key_pair.first), std::move(key_pair.second)); +} + +absl::StatusOr<std::pair<RSAPublicKey, RSAPrivateKey>> GetStrongRsaKeys3072() { + ANON_TOKENS_ASSIGN_OR_RETURN( + auto key_pair, + ParseRsaKeysFromFile("quiche/blind_sign_auth/anonymous_tokens/testing/data/" + "strong_rsa_modulus3072_example.binarypb")); + return std::make_pair(std::move(key_pair.first), std::move(key_pair.second)); +} + +absl::StatusOr<std::pair<RSAPublicKey, RSAPrivateKey>> GetStrongRsaKeys4096() { + ANON_TOKENS_ASSIGN_OR_RETURN( + auto key_pair, + ParseRsaKeysFromFile("quiche/blind_sign_auth/anonymous_tokens/testing/data/" + "strong_rsa_modulus4096_example.binarypb")); + return std::make_pair(std::move(key_pair.first), std::move(key_pair.second)); +} + +} // namespace anonymous_tokens +} // namespace private_membership
diff --git a/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/testing_utils.h b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/testing_utils.h new file mode 100644 index 0000000..933de96 --- /dev/null +++ b/quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/testing_utils.h
@@ -0,0 +1,90 @@ +// Copyright 2023 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. + +#ifndef THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CRYPTO_TESTING_UTILS_H_ +#define THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CRYPTO_TESTING_UTILS_H_ + +#include <stdint.h> + +#include <string> +#include <utility> + +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/constants.h" +#include "quiche/blind_sign_auth/anonymous_tokens/proto/anonymous_tokens.pb.h" +#include "openssl/base.h" +#include "quiche/common/platform/api/quiche_export.h" + +namespace private_membership { +namespace anonymous_tokens { + +absl::StatusOr<std::pair<bssl::UniquePtr<RSA>, + RSABlindSignaturePublicKey>> QUICHE_EXPORT +CreateTestKey(int key_size = 512, HashType sig_hash = AT_HASH_TYPE_SHA384, + MaskGenFunction mfg1_hash = AT_MGF_SHA384, int salt_length = 48, + MessageMaskType message_mask_type = AT_MESSAGE_MASK_CONCAT, + int message_mask_size = kRsaMessageMaskSizeInBytes32); + +absl::StatusOr<std::string> EncodeMessageForTests(absl::string_view message, + RSAPublicKey public_key, + const EVP_MD* sig_hasher, + const EVP_MD* mgf1_hasher, + int32_t salt_length); + +// TestSign can be removed once rsa_blind_signer is moved to +// anonympous_tokens/public/cpp/crypto +absl::StatusOr<std::string> QUICHE_EXPORT TestSign( + absl::string_view blinded_data, RSA* rsa_key); + +// TestSignWithPublicMetadata can be removed once rsa_blind_signer is moved to +// anonympous_tokens/public/cpp/crypto +absl::StatusOr<std::string> QUICHE_EXPORT TestSignWithPublicMetadata( + absl::string_view blinded_data, absl::string_view public_metadata, + const RSA& rsa_key); + +// Method returns fixed 2048-bit strong RSA modulus for testing. +absl::StatusOr<std::pair<RSAPublicKey, RSAPrivateKey>> QUICHE_EXPORT +GetStrongRsaKeys2048(); + +// Method returns another fixed 2048-bit strong RSA modulus for testing. +absl::StatusOr<std::pair<RSAPublicKey, RSAPrivateKey>> QUICHE_EXPORT +GetAnotherStrongRsaKeys2048(); + +// Method returns fixed 3072-bit strong RSA modulus for testing. +absl::StatusOr<std::pair<RSAPublicKey, RSAPrivateKey>> QUICHE_EXPORT +GetStrongRsaKeys3072(); + +// Method returns fixed 4096-bit strong RSA modulus for testing. +absl::StatusOr<std::pair<RSAPublicKey, RSAPrivateKey>> QUICHE_EXPORT +GetStrongRsaKeys4096(); + +#define ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN(lhs, rexpr) \ + ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN_IMPL_( \ + ANON_TOKENS_STATUS_TESTING_IMPL_CONCAT_(_status_or_value, __LINE__), \ + lhs, rexpr) + +#define ANON_TOKENS_QUICHE_EXPECT_OK_AND_ASSIGN_IMPL_(statusor, lhs, rexpr) \ + auto statusor = (rexpr); \ + ASSERT_THAT(statusor.ok(), ::testing::Eq(true)); \ + lhs = std::move(statusor).value() + +#define ANON_TOKENS_STATUS_TESTING_IMPL_CONCAT_INNER_(x, y) x##y +#define ANON_TOKENS_STATUS_TESTING_IMPL_CONCAT_(x, y) \ + ANON_TOKENS_STATUS_TESTING_IMPL_CONCAT_INNER_(x, y) + +} // namespace anonymous_tokens +} // namespace private_membership + +#endif // THIRD_PARTY_ANONYMOUS_TOKENS_CPP_CRYPTO_TESTING_UTILS_H_
diff --git a/quiche/blind_sign_auth/anonymous_tokens/proto/anonymous_tokens.proto b/quiche/blind_sign_auth/anonymous_tokens/proto/anonymous_tokens.proto new file mode 100644 index 0000000..df77b6d --- /dev/null +++ b/quiche/blind_sign_auth/anonymous_tokens/proto/anonymous_tokens.proto
@@ -0,0 +1,323 @@ +// Copyright 2023 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. + +syntax = "proto3"; + +package private_membership.anonymous_tokens; + +import "quiche/blind_sign_auth/proto/timestamp.proto"; + +// Different use cases for the Anonymous Tokens service. +// Next ID: 9 +enum AnonymousTokensUseCase { + // Test use cases here. + ANONYMOUS_TOKENS_USE_CASE_UNDEFINED = 0; + TEST_USE_CASE = 1; + TEST_USE_CASE_2 = 2; + TEST_USE_CASE_3 = 4; + TEST_USE_CASE_4 = 5; + TEST_USE_CASE_5 = 6; + + PROVABLY_PRIVATE_NETWORK = 3; + CHROME_IP_BLINDING = 7; + NOCTOGRAM_PPISSUER = 8; +} + +// An enum describing different types of available hash functions. +enum HashType { + AT_HASH_TYPE_UNDEFINED = 0; + AT_TEST_HASH_TYPE = 1; + AT_HASH_TYPE_SHA256 = 2; + AT_HASH_TYPE_SHA384 = 3; + // Add more hash types if necessary. +} + +// An enum describing different types of hash functions that can be used by the +// mask generation function. +enum MaskGenFunction { + AT_MGF_UNDEFINED = 0; + AT_TEST_MGF = 1; + AT_MGF_SHA256 = 2; + AT_MGF_SHA384 = 3; + // Add more hash types if necessary. +} + +// An enum describing different types of message masking. +enum MessageMaskType { + AT_MESSAGE_MASK_TYPE_UNDEFINED = 0; + AT_MESSAGE_MASK_XOR = 1; + AT_MESSAGE_MASK_CONCAT = 2; +} + +// Proto representation for RSA private key. +message RSAPrivateKey { + // Modulus. + bytes n = 1; + // Public exponent. + bytes e = 2; + // Private exponent. + bytes d = 3; + // The prime factor p of n. + bytes p = 4; + // The prime factor q of n. + bytes q = 5; + // d mod (p - 1). + bytes dp = 6; + // d mod (q - 1). + bytes dq = 7; + // Chinese Remainder Theorem coefficient q^(-1) mod p. + bytes crt = 8; +} + +// Proto representation for RSA public key. +message RSAPublicKey { + // Modulus. + bytes n = 1; + // Public exponent. + bytes e = 2; +} + +// Next ID: 13 +message RSABlindSignaturePublicKey { + // Use case associated with this public key. + bytes use_case = 9; + + // Version number of public key. + int64 key_version = 1; + + // Serialization of the public key. + bytes serialized_public_key = 2; + + // Timestamp of expiration. + // + // Note that we will not return keys whose expiration times are in the past. + quiche.protobuf.Timestamp expiration_time = 3; + + // Key becomes valid at key_validity_start_time. + quiche.protobuf.Timestamp key_validity_start_time = 8; + + // Hash function used in computing hash of the signing message + // (see https://tools.ietf.org/html/rfc8017#section-9.1.1) + HashType sig_hash_type = 4; + + // Hash function used in MGF1 (a mask generation function based on a + // hash function) (see https://tools.ietf.org/html/rfc8017#appendix-B.2.1). + MaskGenFunction mask_gen_function = 5; + + // Length in bytes of the salt (see + // https://tools.ietf.org/html/rfc8017#section-9.1.1) + int64 salt_length = 6; + + // Key size: bytes of RSA key. + int64 key_size = 7; + + // Type of masking of message (see https://eprint.iacr.org/2022/895.pdf). + MessageMaskType message_mask_type = 10; + + // Length of message mask in bytes. + int64 message_mask_size = 11; + + // Conveys whether public metadata support is enabled and RSA blind signatures + // with public metadata protocol should be used. If false, standard RSA blind + // signatures are used and all public metadata inputs are ignored. + bool public_metadata_support = 12; +} + +message AnonymousTokensPublicKeysGetRequest { + // Use case associated with this request. + // + // Returns an error if the token type does not support public key verification + // for the requested use_case. + bytes use_case = 1; + + // Key version associated with this request. + // + // Returns an error if the token type does not support public key verification + // for the requested use_case and key_version combination. + // + // If unset, all valid possibilities for the key are returned. + int64 key_version = 2; + + // Public key that becomes valid at or before this requested time and not + // after. More explicitly, we need the requested key to be valid at the + // requested key_validity_start_time. + // + // If unset it will be set to current time. + quiche.protobuf.Timestamp key_validity_start_time = 3 + ; + + // Public key that is definitely not valid after this particular time. If + // unset / null, only keys that are indefinitely valid are returned. + // + // Note: It is possible that the key becomes invalid before this time. But the + // key should not be valid after this time. + quiche.protobuf.Timestamp key_validity_end_time = 4 + ; +} + +message AnonymousTokensPublicKeysGetResponse { + // List of currently valid RSA public keys. + repeated RSABlindSignaturePublicKey rsa_public_keys = 1; +} + +message AnonymousTokensSignRequest { + // Next ID: 5 + message BlindedToken { + // Use case associated with this request. + bytes use_case = 1; + + // Version of key used to sign and generate the token. + int64 key_version = 2; + + // Public metadata to be tied to the ciphertext. + bytes public_metadata = 4; + + // Serialization of the token. + bytes serialized_token = 3; + } + + // Token(s) that have been blinded by the user, not yet signed + repeated BlindedToken blinded_tokens = 1; +} + +message AnonymousTokensSignResponse { + // Next ID: 6 + message AnonymousToken { + // Use case associated with this anonymous token. + bytes use_case = 1; + + // Version of key used to sign and generate the token. + int64 key_version = 2; + + // Public metadata tied to the ciphertext. + bytes public_metadata = 4; + + // The serialized_token in BlindedToken in the AnonymousTokensSignRequest. + bytes serialized_blinded_message = 5; + + // Serialization of the signed token. + bytes serialized_token = 3; + } + + // Returned anonymous token(s) + repeated AnonymousToken anonymous_tokens = 1; +} + +message AnonymousTokensRedemptionRequest { + // Next ID: 7 + message AnonymousTokenToRedeem { + // Use case associated with this anonymous token that needs to be redeemed. + bytes use_case = 1; + + // Version of key associated with this anonymous token that needs to be + // redeemed. + int64 key_version = 2; + + // Public metadata to be used for verifying the ciphertext. + bytes public_metadata = 4; + + // Serialization of the unblinded anonymous token that needs to be redeemed. + bytes serialized_unblinded_token = 3; + + // Plaintext input message to verify the signature for. + bytes plaintext_message = 5; + + // Nonce used to mask plaintext message before cryptographic verification. + bytes message_mask = 6; + } + + // One or more anonymous tokens to redeem. + repeated AnonymousTokenToRedeem anonymous_tokens_to_redeem = 1; +} + +message AnonymousTokensRedemptionResponse { + // Next ID: 9 + message AnonymousTokenRedemptionResult { + // Use case associated with this redeemed anonymous token. + bytes use_case = 3; + + // Version of key associated with this redeemed anonymous token. + int64 key_version = 4; + + // Public metadata used for verifying the ciphertext. + bytes public_metadata = 5; + + // Serialization of this redeemed unblinded anonymous token. + bytes serialized_unblinded_token = 6; + + // Unblinded input message that the signature was verified against. + bytes plaintext_message = 7; + + // Nonce used to mask plaintext message before cryptographic verification. + bytes message_mask = 8; + + // Returns true if and only if the anonymous token was redeemed + // successfully i.e. token was cryptographically verified, all relevant + // state in the server was updated successfully and the token was not + // redeemed already. + // + bool verified = 1; + + // Returns true if and only if the anonymous token has already been + // redeemed. + bool double_spent = 2; + } + + // Redemption response for requested anonymous tokens. + repeated AnonymousTokenRedemptionResult anonymous_token_redemption_results = + 1; +} + +// Plaintext message with public metadata. +message PlaintextMessageWithPublicMetadata { + // Message to be signed. + bytes plaintext_message = 1; + + // Public metadata to be tied to the signature. + bytes public_metadata = 2; +} + +// Proto representing a token created during the blind signing protocol. +message RSABlindSignatureToken { + // Resulting token from the blind signing protocol. + bytes token = 1; + + // Nonce used to mask messages. + bytes message_mask = 2; +} + +// Proto representing a token along with the input. +message RSABlindSignatureTokenWithInput { + // Input consisting of plaintext message and public metadata. + PlaintextMessageWithPublicMetadata input = 1; + + // Resulting token after blind signing protocol. + RSABlindSignatureToken token = 2; +} + +// Proto representing redemption result along with the token and the token +// input. +message RSABlindSignatureRedemptionResult { + // Proto representing a token along with the input. + RSABlindSignatureTokenWithInput token_with_input = 1; + + // This is set to true if and only if the anonymous token was redeemed + // successfully i.e. token was cryptographically verified, all relevant + // state in the redemption server was updated successfully and the token was + // not redeemed already. + bool redeemed = 2; + + // True if and only if the token was redeemed before. + bool double_spent = 3; +}
diff --git a/quiche/blind_sign_auth/anonymous_tokens/testing/data/strong_rsa_modulus2048_example.binarypb b/quiche/blind_sign_auth/anonymous_tokens/testing/data/strong_rsa_modulus2048_example.binarypb new file mode 100644 index 0000000..c54070b --- /dev/null +++ b/quiche/blind_sign_auth/anonymous_tokens/testing/data/strong_rsa_modulus2048_example.binarypb Binary files differ
diff --git a/quiche/blind_sign_auth/anonymous_tokens/testing/data/strong_rsa_modulus2048_example_2.binarypb b/quiche/blind_sign_auth/anonymous_tokens/testing/data/strong_rsa_modulus2048_example_2.binarypb new file mode 100644 index 0000000..50faa96 --- /dev/null +++ b/quiche/blind_sign_auth/anonymous_tokens/testing/data/strong_rsa_modulus2048_example_2.binarypb Binary files differ
diff --git a/quiche/blind_sign_auth/anonymous_tokens/testing/data/strong_rsa_modulus3072_example.binarypb b/quiche/blind_sign_auth/anonymous_tokens/testing/data/strong_rsa_modulus3072_example.binarypb new file mode 100644 index 0000000..3e4bae9 --- /dev/null +++ b/quiche/blind_sign_auth/anonymous_tokens/testing/data/strong_rsa_modulus3072_example.binarypb Binary files differ
diff --git a/quiche/blind_sign_auth/anonymous_tokens/testing/data/strong_rsa_modulus4096_example.binarypb b/quiche/blind_sign_auth/anonymous_tokens/testing/data/strong_rsa_modulus4096_example.binarypb new file mode 100644 index 0000000..f322a9b --- /dev/null +++ b/quiche/blind_sign_auth/anonymous_tokens/testing/data/strong_rsa_modulus4096_example.binarypb Binary files differ
diff --git a/quiche/blind_sign_auth/blind_sign_auth.cc b/quiche/blind_sign_auth/blind_sign_auth.cc new file mode 100644 index 0000000..f09e6df --- /dev/null +++ b/quiche/blind_sign_auth/blind_sign_auth.cc
@@ -0,0 +1,261 @@ +// 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 <algorithm> +#include <cstddef> +#include <functional> +#include <string> +#include <vector> + +#include "privacy/net/common/cpp/public_metadata/fingerprint.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/str_cat.h" +#include "absl/types/span.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/crypto/proto_utils.h" +#include "quiche/blind_sign_auth/anonymous_tokens/proto/anonymous_tokens.pb.h" +#include "quiche/blind_sign_auth/blind_sign_http_response.h" +#include "quiche/common/platform/api/quiche_logging.h" +#include "quiche/common/quiche_random.h" + +namespace quiche { + +void BlindSignAuth::GetTokens( + absl::string_view oauth_token, int num_tokens, + std::function<void(absl::StatusOr<absl::Span<const std::string>>)> + callback) { + // Create GetInitialData RPC. + privacy::ppn::GetInitialDataRequest request; + request.set_use_attestation(false); + request.set_service_type("chromeipblinding"); + request.set_location_granularity( + privacy::ppn::GetInitialDataRequest_LocationGranularity_UNKNOWN); + + // Call GetInitialData on the HttpFetcher. + std::string path_and_query = "/v1/getInitialData"; + std::string body = request.SerializeAsString(); + http_fetcher_->DoRequest( + path_and_query, oauth_token.data(), body, + [this, callback, oauth_token, + num_tokens](absl::StatusOr<BlindSignHttpResponse> response) { + GetInitialDataCallback(response, oauth_token, num_tokens, callback); + }); +} + +void BlindSignAuth::GetInitialDataCallback( + absl::StatusOr<BlindSignHttpResponse> response, + absl::string_view oauth_token, int num_tokens, + std::function<void(absl::StatusOr<absl::Span<std::string>>)> callback) { + if (!response.ok()) { + QUICHE_LOG(WARNING) << "GetInitialDataRequest failed: " + << response.status(); + callback(response.status()); + return; + } + int status_code = response.value().status_code(); + if (response.value().status_code() != 200) { + QUICHE_LOG(WARNING) << "GetInitialDataRequest failed with code: " + << status_code; + callback(response.status()); + return; + } + // Parse GetInitialDataResponse. + privacy::ppn::GetInitialDataResponse initial_data_response; + if (!initial_data_response.ParseFromString(response.value().body())) { + QUICHE_LOG(WARNING) << "Failed to parse GetInitialDataResponse"; + callback(absl::InternalError("Failed to parse GetInitialDataResponse")); + return; + } + + // Create RSA BSSA client. + auto bssa_client = + private_membership::anonymous_tokens::AnonymousTokensRsaBssaClient:: + Create(initial_data_response.at_public_metadata_public_key()); + if (!bssa_client.ok()) { + QUICHE_LOG(WARNING) << "Failed to create AT BSSA client: " + << bssa_client.status(); + callback(bssa_client.status()); + return; + } + + // Create plaintext tokens. + // Client blinds plaintext tokens (random 32-byte strings) in CreateRequest. + std::vector< + private_membership::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:". + private_membership::anonymous_tokens::PlaintextMessageWithPublicMetadata + plaintext_message; + std::string rand_bytes(32, '\0'); + random->RandBytes(rand_bytes.data(), rand_bytes.size()); + plaintext_message.set_plaintext_message(absl::StrCat("blind:", rand_bytes)); + uint64_t fingerprint = 0; + absl::Status fingerprint_status = privacy::ppn::FingerprintPublicMetadata( + initial_data_response.public_metadata_info().public_metadata(), + &fingerprint); + if (!fingerprint_status.ok()) { + QUICHE_LOG(WARNING) << "Failed to fingerprint public metadata: " + << fingerprint_status; + callback(fingerprint_status); + return; + } + plaintext_message.set_public_metadata(absl::StrCat(fingerprint)); + plaintext_tokens.push_back(plaintext_message); + } + + absl::StatusOr< + private_membership::anonymous_tokens::AnonymousTokensSignRequest> + 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(); + callback(at_sign_request.status()); + return; + } + + // Create AuthAndSign RPC. + 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_public_metadata_info() = + initial_data_response.public_metadata_info(); + for (int i = 0; i < at_sign_request->blinded_tokens_size(); i++) { + sign_request.add_blinded_token(absl::Base64Escape( + at_sign_request->blinded_tokens().at(i).serialized_token())); + } + + privacy::ppn::PublicMetadataInfo public_metadata_info = + initial_data_response.public_metadata_info(); + http_fetcher_->DoRequest( + "/v1/authWithHeaderCreds", oauth_token.data(), + sign_request.SerializeAsString(), + [this, at_sign_request, public_metadata_info, + bssa_client_ = bssa_client.value().get(), + callback](absl::StatusOr<BlindSignHttpResponse> response) { + AuthAndSignCallback(response, public_metadata_info, *at_sign_request, + bssa_client_, callback); + }); +} + +void BlindSignAuth::AuthAndSignCallback( + absl::StatusOr<BlindSignHttpResponse> response, + privacy::ppn::PublicMetadataInfo public_metadata_info, + private_membership::anonymous_tokens::AnonymousTokensSignRequest + at_sign_request, + private_membership::anonymous_tokens::AnonymousTokensRsaBssaClient* + bssa_client, + std::function<void(absl::StatusOr<absl::Span<std::string>>)> callback) { + // Validate response. + if (!response.ok()) { + QUICHE_LOG(WARNING) << "AuthAndSign failed: " << response.status(); + callback(response.status()); + return; + } + int status_code = response.value().status_code(); + if (response.value().status_code() != 200) { + QUICHE_LOG(WARNING) << "AuthAndSign failed with code: " << status_code; + callback(response.status()); + return; + } + + // Decode AuthAndSignResponse. + privacy::ppn::AuthAndSignResponse sign_response; + if (!sign_response.ParseFromString(response.value().body())) { + QUICHE_LOG(WARNING) << "Failed to parse AuthAndSignResponse"; + callback(absl::InternalError("Failed to parse AuthAndSignResponse")); + return; + } + + // Create vector of unblinded anonymous tokens. + private_membership::anonymous_tokens::AnonymousTokensSignResponse + at_sign_response; + + if (sign_response.blinded_token_signature_size() != + at_sign_request.blinded_tokens_size()) { + QUICHE_LOG(WARNING) + << "Response signature size does not equal request tokens size"; + callback(absl::InternalError( + "Response signature size does not equal request tokens size")); + return; + } + // This depends on the signing server returning the signatures in the order + // that the tokens were sent. Phosphor does guarantee this. + for (int i = 0; i < sign_response.blinded_token_signature_size(); i++) { + std::string blinded_token; + if (!absl::Base64Unescape(sign_response.blinded_token_signature(i), + &blinded_token)) { + QUICHE_LOG(WARNING) << "Failed to unescape blinded token signature"; + callback( + absl::InternalError("Failed to unescape blinded token signature")); + return; + } + private_membership::anonymous_tokens::AnonymousTokensSignResponse:: + AnonymousToken anon_token_proto; + *anon_token_proto.mutable_use_case() = + at_sign_request.blinded_tokens(i).use_case(); + anon_token_proto.set_key_version( + at_sign_request.blinded_tokens(i).key_version()); + *anon_token_proto.mutable_public_metadata() = + at_sign_request.blinded_tokens(i).public_metadata(); + *anon_token_proto.mutable_serialized_blinded_message() = + at_sign_request.blinded_tokens(i).serialized_token(); + *anon_token_proto.mutable_serialized_token() = blinded_token; + at_sign_response.add_anonymous_tokens()->Swap(&anon_token_proto); + } + + auto signed_tokens = bssa_client->ProcessResponse(at_sign_response); + if (!signed_tokens.ok()) { + QUICHE_LOG(WARNING) << "AuthAndSign ProcessResponse failed: " + << signed_tokens.status(); + callback(signed_tokens.status()); + return; + } + if (signed_tokens->size() != + static_cast<size_t>(at_sign_response.anonymous_tokens_size())) { + QUICHE_LOG(WARNING) + << "ProcessResponse did not output the right number of signed tokens"; + callback(absl::InternalError( + "ProcessResponse did not output the right number of signed tokens")); + return; + } + + // Output SpendTokenData with data for the redeemer to make a SpendToken RPC. + std::vector<std::string> tokens_vec; + for (size_t i = 0; i < signed_tokens->size(); i++) { + privacy::ppn::SpendTokenData spend_token_data; + *spend_token_data.mutable_public_metadata() = public_metadata_info; + *spend_token_data.mutable_unblinded_token() = + signed_tokens->at(i).input().plaintext_message(); + *spend_token_data.mutable_unblinded_token_signature() = + signed_tokens->at(i).token().token(); + spend_token_data.set_signing_key_version( + at_sign_response.anonymous_tokens(i).key_version()); + auto use_case = private_membership::anonymous_tokens::ParseUseCase( + at_sign_response.anonymous_tokens(i).use_case()); + if (!use_case.ok()) { + QUICHE_LOG(WARNING) << "Failed to parse use case: " << use_case.status(); + callback(use_case.status()); + return; + } + spend_token_data.set_use_case(*use_case); + tokens_vec.push_back(spend_token_data.SerializeAsString()); + } + + callback(absl::Span<std::string>(tokens_vec)); +} + +} // namespace quiche
diff --git a/quiche/blind_sign_auth/blind_sign_auth.h b/quiche/blind_sign_auth/blind_sign_auth.h new file mode 100644 index 0000000..5a85dea --- /dev/null +++ b/quiche/blind_sign_auth/blind_sign_auth.h
@@ -0,0 +1,62 @@ +// 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. + +#ifndef QUICHE_BLIND_SIGN_AUTH_BLIND_SIGN_AUTH_H_ +#define QUICHE_BLIND_SIGN_AUTH_BLIND_SIGN_AUTH_H_ + +#include <functional> +#include <memory> +#include <string> +#include <vector> + +#include "quiche/blind_sign_auth/proto/public_metadata.pb.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "quiche/blind_sign_auth/anonymous_tokens/cpp/client/anonymous_tokens_rsa_bssa_client.h" +#include "quiche/blind_sign_auth/anonymous_tokens/proto/anonymous_tokens.pb.h" +#include "quiche/blind_sign_auth/blind_sign_auth_interface.h" +#include "quiche/blind_sign_auth/blind_sign_http_interface.h" +#include "quiche/blind_sign_auth/blind_sign_http_response.h" +#include "quiche/common/platform/api/quiche_export.h" + +namespace quiche { + +// BlindSignAuth provides signed, unblinded tokens to callers. +class QUICHE_EXPORT BlindSignAuth : public BlindSignAuthInterface { + public: + explicit BlindSignAuth(BlindSignHttpInterface* http_fetcher) + : http_fetcher_(http_fetcher) {} + + // Returns signed unblinded tokens in a callback. Tokens are single-use. + // GetTokens starts asynchronous HTTP POST requests to a signer hostname + // specified by the caller, with path and query params given in the request. + // The GetTokens callback will run on the same thread as the + // BlindSignHttpInterface callbacks. + // Callers can make multiple concurrent requests to GetTokens. + void GetTokens( + absl::string_view oauth_token, int num_tokens, + std::function<void(absl::StatusOr<absl::Span<const std::string>>)> + callback) override; + + private: + void GetInitialDataCallback( + absl::StatusOr<BlindSignHttpResponse> response, + absl::string_view oauth_token, int num_tokens, + std::function<void(absl::StatusOr<absl::Span<std::string>>)> callback); + void AuthAndSignCallback( + absl::StatusOr<BlindSignHttpResponse> response, + privacy::ppn::PublicMetadataInfo public_metadata_info, + private_membership::anonymous_tokens::AnonymousTokensSignRequest + at_sign_request, + private_membership::anonymous_tokens::AnonymousTokensRsaBssaClient* + bssa_client, + std::function<void(absl::StatusOr<absl::Span<std::string>>)> callback); + + BlindSignHttpInterface* http_fetcher_ = nullptr; +}; + +} // namespace quiche + +#endif // QUICHE_BLIND_SIGN_AUTH_BLIND_SIGN_AUTH_H_
diff --git a/quiche/blind_sign_auth/blind_sign_auth_interface.h b/quiche/blind_sign_auth/blind_sign_auth_interface.h new file mode 100644 index 0000000..f7e3905 --- /dev/null +++ b/quiche/blind_sign_auth/blind_sign_auth_interface.h
@@ -0,0 +1,32 @@ +// 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. + +#ifndef QUICHE_BLIND_SIGN_AUTH_BLIND_SIGN_AUTH_INTERFACE_H_ +#define QUICHE_BLIND_SIGN_AUTH_BLIND_SIGN_AUTH_INTERFACE_H_ + +#include <functional> +#include <string> + +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "quiche/common/platform/api/quiche_export.h" + +namespace quiche { + +// BlindSignAuth provides signed, unblinded tokens to callers. +class QUICHE_EXPORT BlindSignAuthInterface { + public: + virtual ~BlindSignAuthInterface() = default; + + // Returns signed unblinded tokens in a callback. Tokens are single-use. + virtual void GetTokens( + absl::string_view oauth_token, int num_tokens, + std::function<void(absl::StatusOr<absl::Span<const std::string>>)> + callback) = 0; +}; + +} // namespace quiche + +#endif // QUICHE_BLIND_SIGN_AUTH_BLIND_SIGN_AUTH_INTERFACE_H_
diff --git a/quiche/blind_sign_auth/blind_sign_auth_test.cc b/quiche/blind_sign_auth/blind_sign_auth_test.cc new file mode 100644 index 0000000..39bf880 --- /dev/null +++ b/quiche/blind_sign_auth/blind_sign_auth_test.cc
@@ -0,0 +1,307 @@ +// 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 <vector> + +#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/crypto/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::EqualsProto; +using ::testing::InSequence; +using ::testing::Invoke; +using ::testing::InvokeArgument; +using ::testing::StartsWith; +using ::testing::Unused; +using ::testing::proto::WhenDeserializedAs; + +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("CHROME_IP_BLINDING"); + + // 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::PublicMetadataInfo public_metadata_info; + ASSERT_TRUE(proto2::TextFormat::ParseFromString(public_metadata_str, + &public_metadata_info)); + *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_THAT(request.public_metadata_info(), EqualsProto(R"pb( + public_metadata { + exit_location { country: "US" } + service_type: "chromeipblinding" + expiration { seconds: 3600 } + } + validation_version: 1 + )pb")); + 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<const std::string>& tokens) { + for (const auto& token : tokens) { + privacy::ppn::SpendTokenData spend_token_data; + ASSERT_TRUE(spend_token_data.ParseFromString(token)); + // Validate token structure. + EXPECT_THAT(spend_token_data.public_metadata(), EqualsProto(R"pb( + public_metadata { + exit_location { country: "US" } + service_type: "chromeipblinding" + expiration { seconds: 3600 } + } + validation_version: 1 + )pb")); + 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_THAT(spend_token_data.use_case(), + private_membership::anonymous_tokens::AnonymousTokensUseCase:: + CHROME_IP_BLINDING); + } + } + + MockBlindSignHttpInterface mock_http_interface_; + std::unique_ptr<BlindSignAuth> blind_sign_auth_; + std::pair<bssl::UniquePtr<RSA>, + private_membership::anonymous_tokens::RSABlindSignaturePublicKey> + keypair_; + privacy::ppn::AuthAndSignResponse sign_response_; + privacy::ppn::GetInitialDataResponse fake_get_initial_data_response_; + std::string oauth_token_ = "oauth_token"; + absl::string_view expected_get_initial_data_request_ = R"pb( + use_attestation: false + service_type: "chromeipblinding" + location_granularity: 0 + )pb"; +}; + +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_), + WhenDeserializedAs<privacy::ppn::GetInitialDataRequest>( + EqualsProto(expected_get_initial_data_request_)), + _)) + .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<const std::string>>)> callback = + [this, &done, + num_tokens](absl::StatusOr<absl::Span<const std::string>> 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<const std::string>>)> callback = + [&done](absl::StatusOr<absl::Span<const std::string>> 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_), + WhenDeserializedAs<privacy::ppn::GetInitialDataRequest>( + EqualsProto(expected_get_initial_data_request_)), + _)) + .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<const std::string>>)> callback = + [&done](absl::StatusOr<absl::Span<const std::string>> 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_), + WhenDeserializedAs<privacy::ppn::GetInitialDataRequest>( + EqualsProto(expected_get_initial_data_request_)), + _)) + .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<const std::string>>)> callback = + [&done](absl::StatusOr<absl::Span<const std::string>> 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
diff --git a/quiche/blind_sign_auth/blind_sign_http_interface.h b/quiche/blind_sign_auth/blind_sign_http_interface.h new file mode 100644 index 0000000..d8111b4 --- /dev/null +++ b/quiche/blind_sign_auth/blind_sign_http_interface.h
@@ -0,0 +1,42 @@ +// 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. + +#ifndef QUICHE_BLIND_SIGN_AUTH_BLIND_SIGN_HTTP_INTERFACE_H_ +#define QUICHE_BLIND_SIGN_AUTH_BLIND_SIGN_HTTP_INTERFACE_H_ + +#include <functional> +#include <map> +#include <string> +#include <vector> + +#include "absl/status/statusor.h" +#include "quiche/blind_sign_auth/blind_sign_http_response.h" +#include "quiche/common/platform/api/quiche_export.h" + +namespace quiche { + +// Interface for async HTTP POST requests in BlindSignAuth. +// Implementers must send a request to a signer hostname, using the request's +// arguments, and call the provided callback when a request is complete. +class QUICHE_EXPORT BlindSignHttpInterface { + public: + virtual ~BlindSignHttpInterface() = default; + // Non-HTTP errors (like failing to create a socket) must return an + // absl::Status. + // HTTP errors must set status_code and body in BlindSignHttpResponse. + // DoRequest must be a HTTP POST request. + // Requests do not need cookies and must follow redirects. + // The implementer must set Content-Type and Accept headers to + // "application/x-protobuf". + // DoRequest is async. When the request completes, the implementer must call + // the provided callback. + virtual void DoRequest( + const std::string& path_and_query, + const std::string& authorization_header, const std::string& body, + std::function<void(absl::StatusOr<BlindSignHttpResponse>)> callback) = 0; +}; + +} // namespace quiche + +#endif // QUICHE_BLIND_SIGN_AUTH_BLIND_SIGN_HTTP_INTERFACE_H_
diff --git a/quiche/blind_sign_auth/blind_sign_http_response.h b/quiche/blind_sign_auth/blind_sign_http_response.h new file mode 100644 index 0000000..89d9072 --- /dev/null +++ b/quiche/blind_sign_auth/blind_sign_http_response.h
@@ -0,0 +1,33 @@ +// 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. + +#ifndef QUICHE_BLIND_SIGN_AUTH_BLIND_SIGN_HTTP_RESPONSE_H_ +#define QUICHE_BLIND_SIGN_AUTH_BLIND_SIGN_HTTP_RESPONSE_H_ + +#include <map> +#include <string> +#include <utility> +#include <vector> + +#include "quiche/common/platform/api/quiche_export.h" + +namespace quiche { + +// Contains a response to a HTTP POST request issued by BlindSignAuth. +class QUICHE_EXPORT BlindSignHttpResponse { + public: + BlindSignHttpResponse(int status_code, std::string body) + : status_code_(status_code), body_(std::move(body)) {} + + int status_code() const { return status_code_; } + const std::string& body() const { return body_; } + + private: + int status_code_; + std::string body_; +}; + +} // namespace quiche + +#endif // QUICHE_BLIND_SIGN_AUTH_BLIND_SIGN_HTTP_RESPONSE_H_
diff --git a/quiche/blind_sign_auth/cached_blind_sign_auth.cc b/quiche/blind_sign_auth/cached_blind_sign_auth.cc new file mode 100644 index 0000000..34e5e73 --- /dev/null +++ b/quiche/blind_sign_auth/cached_blind_sign_auth.cc
@@ -0,0 +1,115 @@ +// 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/cached_blind_sign_auth.h" + +#include <utility> +#include <vector> + +#include "absl/status/status.h" +#include "absl/strings/str_format.h" +#include "absl/types/span.h" +#include "quiche/common/platform/api/quiche_logging.h" +#include "quiche/common/platform/api/quiche_mutex.h" + +namespace quiche { + +void CachedBlindSignAuth::GetTokens( + absl::string_view oauth_token, int num_tokens, + std::function<void(absl::StatusOr<absl::Span<const std::string>>)> + callback) { + if (num_tokens > max_tokens_per_request_) { + callback(absl::InvalidArgumentError( + absl::StrFormat("Number of tokens requested exceeds maximum: %d", + kBlindSignAuthRequestMaxTokens))); + return; + } + if (num_tokens < 0) { + callback(absl::InvalidArgumentError(absl::StrFormat( + "Negative number of tokens requested: %d", num_tokens))); + return; + } + + std::vector<std::string> output_tokens; + { + QuicheWriterMutexLock lock(&mutex_); + + // Try to fill the request from cache. + if (static_cast<size_t>(num_tokens) <= cached_tokens_.size()) { + output_tokens = CreateOutputTokens(num_tokens); + } + } + if (!output_tokens.empty() || num_tokens == 0) { + callback(output_tokens); + return; + } + + // Make a GetTokensRequest if the cache can't handle the request size. + std::function<void(absl::StatusOr<absl::Span<const std::string>>)> + caching_callback = + [this, num_tokens, + callback](absl::StatusOr<absl::Span<const std::string>> tokens) { + HandleGetTokensResponse(tokens, num_tokens, callback); + }; + blind_sign_auth_->GetTokens(oauth_token, kBlindSignAuthRequestMaxTokens, + caching_callback); +} + +void CachedBlindSignAuth::HandleGetTokensResponse( + absl::StatusOr<absl::Span<const std::string>> tokens, int num_tokens, + std::function<void(absl::StatusOr<absl::Span<const std::string>>)> + callback) { + if (!tokens.ok()) { + QUICHE_LOG(WARNING) << "BlindSignAuth::GetTokens failed: " + << tokens.status(); + callback(tokens); + return; + } + if (tokens->size() < static_cast<size_t>(num_tokens) || + tokens->size() > kBlindSignAuthRequestMaxTokens) { + QUICHE_LOG(WARNING) << "Expected " << num_tokens << " tokens, got " + << tokens->size(); + } + + std::vector<std::string> output_tokens; + size_t cache_size; + { + QuicheWriterMutexLock lock(&mutex_); + + // Add returned tokens to cache. + for (const std::string& token : *tokens) { + cached_tokens_.push_back(token); + } + + // Return tokens or a ResourceExhaustedError. + cache_size = cached_tokens_.size(); + if (cache_size >= static_cast<size_t>(num_tokens)) { + output_tokens = CreateOutputTokens(num_tokens); + } + } + + if (!output_tokens.empty()) { + callback(output_tokens); + return; + } + callback(absl::ResourceExhaustedError(absl::StrFormat( + "Requested %d tokens, cache only has %d after GetTokensRequest", + num_tokens, cache_size))); +} + +std::vector<std::string> CachedBlindSignAuth::CreateOutputTokens( + int num_tokens) { + std::vector<std::string> output_tokens; + if (cached_tokens_.size() < static_cast<size_t>(num_tokens)) { + QUICHE_LOG(FATAL) << "Check failed, not enough tokens in cache: " + << cached_tokens_.size() << " < " << num_tokens; + } + for (int i = 0; i < num_tokens; i++) { + output_tokens.push_back(std::move(cached_tokens_.front())); + cached_tokens_.pop_front(); + } + return output_tokens; +} + +} // namespace quiche
diff --git a/quiche/blind_sign_auth/cached_blind_sign_auth.h b/quiche/blind_sign_auth/cached_blind_sign_auth.h new file mode 100644 index 0000000..ee405a1 --- /dev/null +++ b/quiche/blind_sign_auth/cached_blind_sign_auth.h
@@ -0,0 +1,65 @@ +// 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. + +#ifndef QUICHE_BLIND_SIGN_AUTH_CACHED_BLIND_SIGN_AUTH_H_ +#define QUICHE_BLIND_SIGN_AUTH_CACHED_BLIND_SIGN_AUTH_H_ + +#include <cstddef> +#include <functional> +#include <string> +#include <vector> + +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "quiche/blind_sign_auth/blind_sign_auth_interface.h" +#include "quiche/common/platform/api/quiche_export.h" +#include "quiche/common/platform/api/quiche_mutex.h" +#include "quiche/common/quiche_circular_deque.h" + +namespace quiche { + +inline constexpr int kBlindSignAuthRequestMaxTokens = 1024; + +// CachedBlindSignAuth caches signed tokens generated by BlindSignAuth. +// This class does not guarantee that tokens returned are fresh. +// Tokens may be stale if the backend has rotated its signing key since tokens +// were generated. +// This class is thread-safe. +class QUICHE_EXPORT CachedBlindSignAuth : public BlindSignAuthInterface { + public: + CachedBlindSignAuth( + BlindSignAuthInterface* blind_sign_auth, + int max_tokens_per_request = kBlindSignAuthRequestMaxTokens) + : blind_sign_auth_(blind_sign_auth), + max_tokens_per_request_(max_tokens_per_request) {} + + // Returns signed unblinded tokens in a callback. Tokens are single-use. + // + // The GetTokens callback may be called synchronously on the calling thread, + // or asynchronously on BlindSignAuth's BlindSignHttpInterface thread. + // The GetTokens callback must not acquire any locks that the calling thread + // owns, otherwise the callback will deadlock. + void GetTokens( + absl::string_view oauth_token, int num_tokens, + std::function<void(absl::StatusOr<absl::Span<const std::string>>)> + callback) override; + + private: + void HandleGetTokensResponse( + absl::StatusOr<absl::Span<const std::string>> tokens, int num_tokens, + std::function<void(absl::StatusOr<absl::Span<const std::string>>)> + callback); + std::vector<std::string> CreateOutputTokens(int num_tokens) + QUICHE_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + + BlindSignAuthInterface* blind_sign_auth_; + int max_tokens_per_request_; + QuicheMutex mutex_; + QuicheCircularDeque<std::string> cached_tokens_ QUICHE_GUARDED_BY(mutex_); +}; + +} // namespace quiche + +#endif // QUICHE_BLIND_SIGN_AUTH_CACHED_BLIND_SIGN_AUTH_H_
diff --git a/quiche/blind_sign_auth/cached_blind_sign_auth_test.cc b/quiche/blind_sign_auth/cached_blind_sign_auth_test.cc new file mode 100644 index 0000000..dfad523 --- /dev/null +++ b/quiche/blind_sign_auth/cached_blind_sign_auth_test.cc
@@ -0,0 +1,337 @@ +// 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/cached_blind_sign_auth.h" + +#include <functional> +#include <memory> +#include <string> +#include <vector> + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/types/span.h" +#include "quiche/blind_sign_auth/test_tools/mock_blind_sign_auth_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::Invoke; +using ::testing::InvokeArgument; +using ::testing::Unused; + +class CachedBlindSignAuthTest : public QuicheTest { + protected: + void SetUp() override { + cached_blind_sign_auth_ = + std::make_unique<CachedBlindSignAuth>(&mock_blind_sign_auth_interface_); + } + + void TearDown() override { + fake_tokens_.clear(); + cached_blind_sign_auth_.reset(); + } + + public: + std::vector<std::string> MakeFakeTokens(int num_tokens) { + std::vector<std::string> fake_tokens; + for (int i = 0; i < kBlindSignAuthRequestMaxTokens; i++) { + fake_tokens.push_back(absl::StrCat("token:", i)); + } + return fake_tokens; + } + MockBlindSignAuthInterface mock_blind_sign_auth_interface_; + std::unique_ptr<CachedBlindSignAuth> cached_blind_sign_auth_; + std::string oauth_token_ = "oauth_token"; + std::vector<std::string> fake_tokens_; +}; + +TEST_F(CachedBlindSignAuthTest, TestGetTokensOneCallSuccessful) { + EXPECT_CALL(mock_blind_sign_auth_interface_, + GetTokens(oauth_token_, kBlindSignAuthRequestMaxTokens, _)) + .Times(1) + .WillOnce(Invoke( + [this](Unused, int num_tokens, + std::function<void(absl::StatusOr<absl::Span<std::string>>)> + callback) { + fake_tokens_ = MakeFakeTokens(num_tokens); + callback(absl::MakeSpan(fake_tokens_)); + })); + + int num_tokens = 5; + QuicheNotification done; + std::function<void(absl::StatusOr<absl::Span<const std::string>>)> callback = + [num_tokens, + &done](absl::StatusOr<absl::Span<const std::string>> tokens) { + QUICHE_EXPECT_OK(tokens); + EXPECT_EQ(num_tokens, tokens->size()); + for (int i = 0; i < num_tokens; i++) { + EXPECT_EQ(tokens->at(i), absl::StrCat("token:", i)); + } + done.Notify(); + }; + + cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens, callback); + done.WaitForNotification(); +} + +TEST_F(CachedBlindSignAuthTest, TestGetTokensMultipleRemoteCallsSuccessful) { + EXPECT_CALL(mock_blind_sign_auth_interface_, + GetTokens(oauth_token_, kBlindSignAuthRequestMaxTokens, _)) + .Times(2) + .WillRepeatedly(Invoke( + [this](Unused, int num_tokens, + std::function<void(absl::StatusOr<absl::Span<std::string>>)> + callback) { + fake_tokens_ = MakeFakeTokens(num_tokens); + callback(absl::MakeSpan(fake_tokens_)); + })); + + int num_tokens = kBlindSignAuthRequestMaxTokens - 1; + QuicheNotification first; + std::function<void(absl::StatusOr<absl::Span<const std::string>>)> + first_callback = + [num_tokens, + &first](absl::StatusOr<absl::Span<const std::string>> tokens) { + QUICHE_EXPECT_OK(tokens); + EXPECT_EQ(num_tokens, tokens->size()); + for (int i = 0; i < num_tokens; i++) { + EXPECT_EQ(tokens->at(i), absl::StrCat("token:", i)); + } + first.Notify(); + }; + + cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens, first_callback); + first.WaitForNotification(); + + QuicheNotification second; + std::function<void(absl::StatusOr<absl::Span<const std::string>>)> + second_callback = + [num_tokens, + &second](absl::StatusOr<absl::Span<const std::string>> tokens) { + QUICHE_EXPECT_OK(tokens); + EXPECT_EQ(num_tokens, tokens->size()); + EXPECT_EQ( + tokens->at(0), + absl::StrCat("token:", kBlindSignAuthRequestMaxTokens - 1)); + for (int i = 1; i < num_tokens; i++) { + EXPECT_EQ(tokens->at(i), absl::StrCat("token:", i - 1)); + } + second.Notify(); + }; + + cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens, second_callback); + second.WaitForNotification(); +} + +TEST_F(CachedBlindSignAuthTest, TestGetTokensSecondRequestFilledFromCache) { + EXPECT_CALL(mock_blind_sign_auth_interface_, + GetTokens(oauth_token_, kBlindSignAuthRequestMaxTokens, _)) + .Times(1) + .WillOnce(Invoke( + [this](Unused, int num_tokens, + std::function<void(absl::StatusOr<absl::Span<std::string>>)> + callback) { + fake_tokens_ = MakeFakeTokens(num_tokens); + callback(absl::MakeSpan(fake_tokens_)); + })); + + int num_tokens = kBlindSignAuthRequestMaxTokens / 2; + QuicheNotification first; + std::function<void(absl::StatusOr<absl::Span<const std::string>>)> + first_callback = + [num_tokens, + &first](absl::StatusOr<absl::Span<const std::string>> tokens) { + QUICHE_EXPECT_OK(tokens); + EXPECT_EQ(num_tokens, tokens->size()); + for (int i = 0; i < num_tokens; i++) { + EXPECT_EQ(tokens->at(i), absl::StrCat("token:", i)); + } + first.Notify(); + }; + + cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens, first_callback); + first.WaitForNotification(); + + QuicheNotification second; + std::function<void(absl::StatusOr<absl::Span<const std::string>>)> + second_callback = + [num_tokens, + &second](absl::StatusOr<absl::Span<const std::string>> tokens) { + QUICHE_EXPECT_OK(tokens); + EXPECT_EQ(num_tokens, tokens->size()); + for (int i = 0; i < num_tokens; i++) { + EXPECT_EQ(tokens->at(i), absl::StrCat("token:", i + num_tokens)); + } + second.Notify(); + }; + + cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens, second_callback); + second.WaitForNotification(); +} + +TEST_F(CachedBlindSignAuthTest, TestGetTokensThirdRequestRefillsCache) { + EXPECT_CALL(mock_blind_sign_auth_interface_, + GetTokens(oauth_token_, kBlindSignAuthRequestMaxTokens, _)) + .Times(2) + .WillRepeatedly(Invoke( + [this](Unused, int num_tokens, + std::function<void(absl::StatusOr<absl::Span<std::string>>)> + callback) { + fake_tokens_ = MakeFakeTokens(num_tokens); + callback(absl::MakeSpan(fake_tokens_)); + })); + + int num_tokens = kBlindSignAuthRequestMaxTokens / 2; + QuicheNotification first; + std::function<void(absl::StatusOr<absl::Span<const std::string>>)> + first_callback = + [num_tokens, + &first](absl::StatusOr<absl::Span<const std::string>> tokens) { + QUICHE_EXPECT_OK(tokens); + EXPECT_EQ(num_tokens, tokens->size()); + for (int i = 0; i < num_tokens; i++) { + EXPECT_EQ(tokens->at(i), absl::StrCat("token:", i)); + } + first.Notify(); + }; + + cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens, first_callback); + first.WaitForNotification(); + + QuicheNotification second; + std::function<void(absl::StatusOr<absl::Span<const std::string>>)> + second_callback = + [num_tokens, + &second](absl::StatusOr<absl::Span<const std::string>> tokens) { + QUICHE_EXPECT_OK(tokens); + EXPECT_EQ(num_tokens, tokens->size()); + for (int i = 0; i < num_tokens; i++) { + EXPECT_EQ(tokens->at(i), absl::StrCat("token:", i + num_tokens)); + } + second.Notify(); + }; + + cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens, second_callback); + second.WaitForNotification(); + + QuicheNotification third; + int third_request_tokens = 10; + std::function<void(absl::StatusOr<absl::Span<const std::string>>)> + third_callback = + [third_request_tokens, + &third](absl::StatusOr<absl::Span<const std::string>> tokens) { + QUICHE_EXPECT_OK(tokens); + EXPECT_EQ(third_request_tokens, tokens->size()); + for (int i = 0; i < third_request_tokens; i++) { + EXPECT_EQ(tokens->at(i), absl::StrCat("token:", i)); + } + third.Notify(); + }; + + cached_blind_sign_auth_->GetTokens(oauth_token_, third_request_tokens, + third_callback); + third.WaitForNotification(); +} + +TEST_F(CachedBlindSignAuthTest, TestGetTokensRequestTooLarge) { + EXPECT_CALL(mock_blind_sign_auth_interface_, + GetTokens(oauth_token_, kBlindSignAuthRequestMaxTokens, _)) + .Times(0); + + int num_tokens = kBlindSignAuthRequestMaxTokens + 1; + std::function<void(absl::StatusOr<absl::Span<const std::string>>)> callback = + [](absl::StatusOr<absl::Span<const std::string>> tokens) { + EXPECT_THAT(tokens.status().code(), absl::StatusCode::kInvalidArgument); + EXPECT_THAT( + tokens.status().message(), + absl::StrFormat("Number of tokens requested exceeds maximum: %d", + kBlindSignAuthRequestMaxTokens)); + }; + + cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens, callback); +} + +TEST_F(CachedBlindSignAuthTest, TestGetTokensRequestNegative) { + EXPECT_CALL(mock_blind_sign_auth_interface_, + GetTokens(oauth_token_, kBlindSignAuthRequestMaxTokens, _)) + .Times(0); + + int num_tokens = -1; + std::function<void(absl::StatusOr<absl::Span<const std::string>>)> callback = + [num_tokens](absl::StatusOr<absl::Span<const std::string>> tokens) { + EXPECT_THAT(tokens.status().code(), absl::StatusCode::kInvalidArgument); + EXPECT_THAT(tokens.status().message(), + absl::StrFormat("Negative number of tokens requested: %d", + num_tokens)); + }; + + cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens, callback); +} + +TEST_F(CachedBlindSignAuthTest, TestHandleGetTokensResponseErrorHandling) { + EXPECT_CALL(mock_blind_sign_auth_interface_, + GetTokens(oauth_token_, kBlindSignAuthRequestMaxTokens, _)) + .Times(2) + .WillOnce(InvokeArgument<2>(absl::InternalError("AuthAndSign failed"))) + .WillOnce(Invoke( + [this](Unused, int num_tokens, + std::function<void(absl::StatusOr<absl::Span<std::string>>)> + callback) { + fake_tokens_ = MakeFakeTokens(num_tokens); + fake_tokens_.pop_back(); + callback(absl::MakeSpan(fake_tokens_)); + })); + + int num_tokens = kBlindSignAuthRequestMaxTokens; + QuicheNotification first; + std::function<void(absl::StatusOr<absl::Span<const std::string>>)> + first_callback = + [&first](absl::StatusOr<absl::Span<const std::string>> tokens) { + EXPECT_THAT(tokens.status().code(), absl::StatusCode::kInternal); + EXPECT_THAT(tokens.status().message(), "AuthAndSign failed"); + first.Notify(); + }; + + cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens, first_callback); + first.WaitForNotification(); + + QuicheNotification second; + std::function<void(absl::StatusOr<absl::Span<const std::string>>)> + second_callback = + [&second](absl::StatusOr<absl::Span<const std::string>> tokens) { + EXPECT_THAT(tokens.status().code(), + absl::StatusCode::kResourceExhausted); + second.Notify(); + }; + + cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens, second_callback); + second.WaitForNotification(); +} + +TEST_F(CachedBlindSignAuthTest, TestGetTokensZeroTokensRequested) { + EXPECT_CALL(mock_blind_sign_auth_interface_, + GetTokens(oauth_token_, kBlindSignAuthRequestMaxTokens, _)) + .Times(0); + + int num_tokens = 0; + std::function<void(absl::StatusOr<absl::Span<const std::string>>)> callback = + [](absl::StatusOr<absl::Span<const std::string>> tokens) { + QUICHE_EXPECT_OK(tokens); + EXPECT_EQ(tokens->size(), 0); + }; + + cached_blind_sign_auth_->GetTokens(oauth_token_, num_tokens, callback); +} + +} // namespace +} // namespace test +} // namespace quiche
diff --git a/quiche/blind_sign_auth/proto/attestation.proto b/quiche/blind_sign_auth/proto/attestation.proto new file mode 100644 index 0000000..1743234 --- /dev/null +++ b/quiche/blind_sign_auth/proto/attestation.proto
@@ -0,0 +1,105 @@ +syntax = "proto3"; + +package privacy.ppn; + +import "quiche/blind_sign_auth/proto/any.proto"; +import "storage/datapol/annotations/proto/semantic_annotations.proto"; + +option go_api_flag = "OPEN_TO_OPAQUE_HYBRID"; +option java_api_version = 2; +option java_multiple_files = true; +option java_outer_classname = "AttestationProto"; +option java_package = "com.google.android.libraries.privacy.ppn.proto"; +option cc_api_version = 2; +option (datapol.file_vetting_status) = "latest"; + +message NonceRequest {} + +message NonceResponse { + // A nonce with the following format: + // ECDSA( + // SHA256( + // <random bytes of length [64, 128]>.<expiry time in ms>)). + bytes nonce = 1 ; + + // Nonce signature. + bytes sig = 2; + + // Algorithm used to sign the nonce. Should be "es256". + bytes alg = 3; +} + +message ValidateDeviceRequest { + // Attestation data that is returned by the client. + oneof attestation_data { + AndroidAttestationData android_attestation_data = 1 [deprecated = true]; + IosAttestationData ios_attestation_data = 2 [deprecated = true]; + } + AttestationData attestation = 3; + + string package_name = 4; + + // If attestation is AndroidAttestationData device models should be listed in: + // https://storage.googleapis.com/play_public/supported_devices.html + repeated string allowed_models = 5; +} + +message ValidateDeviceResponse { + // True iff all checks passed + // (integrity token, nonce, hardware properties are legitimate). + // Hardware properties check will be performed by the calling service + // as attestation only checks to see if the device's hardware properties + // are genuine. + bool device_verified = 1; + + // Detailed information on what specifically passed and what did not. + VerdictBreakdown breakdown = 2; + + // If verified, contains the device model. + string verified_device_type = 3; +} + +message VerdictBreakdown { + enum Verdict { + VERDICT_UNKNOWN = 0; + VERDICT_PASS = 1; + VERDICT_FAIL = 2; + } + + // Integrity verdict as determined by either Play Server or AppAttest. + Verdict integrity_verdict = 1; + + // Whether nonce check passed. + Verdict nonce_verdict = 2; + + // Whether or not the device properties sent by the client are + // legitimate. + Verdict device_properties_verdict = 3; +} + +message PrepareAttestationData { + bytes attestation_nonce = 2 [ + + json_name = "attestation_nonce" + ]; +} + +message AndroidAttestationData { + // Play IntegrityToken returned by Play Integrity API is detailed in + // https://developer.android.com/google/play/integrity/verdict. + string attestation_token = 1 ; + + // X509 Certificate chain generated by Android Keystore used for + // Hardware-Backed Key Attestation. + repeated bytes hardware_backed_certs = 2; +} + +message IosAttestationData { + // AppAttest attestation token. + // Encoded in CBOR format. + bytes attestation_token = 1 ; +} + +message AttestationData { + quiche.protobuf.Any attestation_data = 1; +}
diff --git a/quiche/blind_sign_auth/proto/auth_and_sign.proto b/quiche/blind_sign_auth/proto/auth_and_sign.proto new file mode 100644 index 0000000..64d396d --- /dev/null +++ b/quiche/blind_sign_auth/proto/auth_and_sign.proto
@@ -0,0 +1,77 @@ +syntax = "proto3"; + +package privacy.ppn; + +import "quiche/blind_sign_auth/proto/attestation.proto"; +import "quiche/blind_sign_auth/proto/key_services.proto"; +import "quiche/blind_sign_auth/proto/public_metadata.proto"; +import "storage/datapol/annotations/proto/semantic_annotations.proto"; + +option cc_api_version = 2; +option (datapol.file_vetting_status) = "latest"; + +// Client is requesting to auth using the provided auth token. +// Next ID: 9 +message AuthAndSignRequest { + reserved 3; + + // A 'bearer' oauth token to be validated. + // https://datatracker.ietf.org/doc/html/rfc6750#section-6.1.1 + string oauth_token = 1 ; + + // A string uniquely identifying the strategy this client should be + // authenticated with. + string service_type = 2 ; + + // A set of blinded tokens to be signed by zinc. b64 encoded. + repeated string blinded_token = 4 + ; + + // A sha256 of the public key PEM used in generated `blinded_token`. This + // Ensures the signer signs with the matching key. Only required if key_type + // is ZINC_KEY_TYPE. + string public_key_hash = 5 ; + + oneof attestation_data { + AndroidAttestationData android_attestation_data = 6 [deprecated = true]; + IosAttestationData ios_attestation_data = 7 [deprecated = true]; + } + privacy.ppn.AttestationData attestation = 8; + + privacy.ppn.KeyType key_type = 10 ; + + privacy.ppn.PublicMetadataInfo public_metadata_info = 11 + ; + + // Indicates which key to use for signing. Only set if key type is + // PUBLIC_METADATA. + int64 key_version = 12 ; +} + +message AuthAndSignResponse { + reserved 1, 2, 3; + + // A set of signatures corresponding by index to `blinded_token` in the + // request. b64 encoded. + repeated string blinded_token_signature = 4 [ + + json_name = "blinded_token_signature" + ]; + + // The marconi server hostname bridge-proxy used to set up tunnel. + string copper_controller_hostname = 5 [ + + json_name = "copper_controller_hostname" + ]; + + // The base64 encoding of override_region token and signature for white listed + // users in the format of "${Region}.${timestamp}.${signature}". + string region_token_and_signature = 6 [ + + json_name = "region_token_and_signature" + ]; + + // The APN type bridge-proxy use to deside which APN to use for connecting. + string apn_type = 7 + [ json_name = "apn_type"]; +}
diff --git a/quiche/blind_sign_auth/proto/get_initial_data.proto b/quiche/blind_sign_auth/proto/get_initial_data.proto new file mode 100644 index 0000000..f05b637 --- /dev/null +++ b/quiche/blind_sign_auth/proto/get_initial_data.proto
@@ -0,0 +1,46 @@ +syntax = "proto3"; + +package privacy.ppn; + +import "quiche/blind_sign_auth/proto/attestation.proto"; +import "quiche/blind_sign_auth/proto/public_metadata.proto"; +import "storage/datapol/annotations/proto/semantic_annotations.proto"; +import "quiche/blind_sign_auth/anonymous_tokens/proto/anonymous_tokens.proto"; + +option java_multiple_files = true; +option cc_api_version = 2; +option (datapol.file_vetting_status) = "latest"; + +// Request data needed to prepare for AuthAndSign. +message GetInitialDataRequest { + // Whether the client wants to use attestation as part of authentication. + bool use_attestation = 1 ; + + // A string uniquely identifying the strategy this client should be + // authenticated with. + string service_type = 2 ; + + enum LocationGranularity { + UNKNOWN = 0; + COUNTRY = 1; + // Geographic area with population greater than 1 million. + CITY_GEOS = 2; + } + // The user selected granularity of exit IP location. + LocationGranularity location_granularity = 3 + ; +} + +// Contains data needed to perform blind signing and prepare for calling +// AuthAndSign. +message GetInitialDataResponse { + private_membership.anonymous_tokens.RSABlindSignaturePublicKey + at_public_metadata_public_key = 1; + + // Metadata to associate with the token. + privacy.ppn.PublicMetadataInfo public_metadata_info = 2; + + // Data needed to set up attestation, included if use_attestation is true or + // if the service_type input requires it. + privacy.ppn.PrepareAttestationData attestation = 3; +} \ No newline at end of file
diff --git a/quiche/blind_sign_auth/proto/key_services.proto b/quiche/blind_sign_auth/proto/key_services.proto new file mode 100644 index 0000000..271b033 --- /dev/null +++ b/quiche/blind_sign_auth/proto/key_services.proto
@@ -0,0 +1,16 @@ +syntax = "proto3"; + +package privacy.ppn; + +import "storage/datapol/annotations/proto/semantic_annotations.proto"; + +option java_multiple_files = true; +option (datapol.file_vetting_status) = "latest"; + +// Indicates client's desired or capable key support. +enum KeyType { + UNKNOWN_KEY_TYPE = 0; + ZINC_KEY_TYPE = 1; + AT_PUBLIC_METADATA_KEY_TYPE = 2; + AT_PUBLIC_METADATA_VERIFIED_KEY_TYPE = 3; +}
diff --git a/quiche/blind_sign_auth/proto/public_metadata.proto b/quiche/blind_sign_auth/proto/public_metadata.proto new file mode 100644 index 0000000..c542e0f --- /dev/null +++ b/quiche/blind_sign_auth/proto/public_metadata.proto
@@ -0,0 +1,43 @@ +syntax = "proto3"; + +package privacy.ppn; + +import "quiche/blind_sign_auth/proto/timestamp.proto"; +import "storage/datapol/annotations/proto/semantic_annotations.proto"; + +option java_multiple_files = true; +option cc_api_version = 2; +option (datapol.file_vetting_status) = "latest"; + +// Contains fields which will be cryptographically linked to a blinded token and +// visible to client, signer, and verifier. Clients should validate/set fields +// contained within such that the values are reasonable for the security and +// privacy constraints of the application. +message PublicMetadata { + // Contains desired exit IP address's declared location. + message Location { + // TODO(b/268354975): fix copybara regex to strip this line automatically + + // All caps ISO 3166-1 alpha-2. + string country = 1; + + // City region geo id if requested by the client. + string city_geo_id = 2; + } + Location exit_location = 1; + + // Indicates which service this token is associated with. + string service_type = 2; + + // When the token and metadata expire. + quiche.protobuf.Timestamp expiration = 3; +} + +// Contains PublicMetadata and associated information. Only the public_metadata +// is cryptographically associated with the token. +message PublicMetadataInfo { + PublicMetadata public_metadata = 1; + + // Earliest validation version that this public metadata conforms to. + int32 validation_version = 2; +}
diff --git a/quiche/blind_sign_auth/proto/spend_token_data.proto b/quiche/blind_sign_auth/proto/spend_token_data.proto new file mode 100644 index 0000000..ba2af00 --- /dev/null +++ b/quiche/blind_sign_auth/proto/spend_token_data.proto
@@ -0,0 +1,26 @@ +syntax = "proto3"; + +package privacy.ppn; + +import "quiche/blind_sign_auth/proto/public_metadata.proto"; +import "quiche/blind_sign_auth/anonymous_tokens/proto/anonymous_tokens.proto"; + +option cc_api_version = 2; + +message SpendTokenData { + // Public metadata associated with the token being spent. + // See go/ppn-token-spend and go/ppn-phosphor-at-service for details. + PublicMetadataInfo public_metadata = 1; + // The unblinded token to be spent which was blind-signed by Phosphor. + bytes unblinded_token = 2; + // The signature for the token to be spent, obtained from Phosphor and + // unblinded. + bytes unblinded_token_signature = 3; + // The version number of the signing key that was used during blind-signing. + int64 signing_key_version = 4; + // A use case identifying the caller. Should be a fixed, hardcoded value to + // prevent cross-spending tokens. + private_membership.anonymous_tokens.AnonymousTokensUseCase use_case = 5; + // Nonce used to mask plaintext message before cryptographic verification. + bytes message_mask = 6; +}
diff --git a/quiche/blind_sign_auth/test_tools/mock_blind_sign_auth_interface.h b/quiche/blind_sign_auth/test_tools/mock_blind_sign_auth_interface.h new file mode 100644 index 0000000..dcb4876 --- /dev/null +++ b/quiche/blind_sign_auth/test_tools/mock_blind_sign_auth_interface.h
@@ -0,0 +1,33 @@ +// 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. + +#ifndef QUICHE_BLIND_SIGN_AUTH_TEST_TOOLS_MOCK_BLIND_SIGN_AUTH_INTERFACE_H_ +#define QUICHE_BLIND_SIGN_AUTH_TEST_TOOLS_MOCK_BLIND_SIGN_AUTH_INTERFACE_H_ + +#include <functional> +#include <string> + +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "quiche/blind_sign_auth/blind_sign_auth_interface.h" +#include "quiche/common/platform/api/quiche_export.h" +#include "quiche/common/platform/api/quiche_test.h" + +namespace quiche::test { + +class QUICHE_NO_EXPORT MockBlindSignAuthInterface + : public BlindSignAuthInterface { + public: + MOCK_METHOD( + void, GetTokens, + (absl::string_view oauth_token, int num_tokens, + std::function<void(absl::StatusOr<absl::Span<const std::string>>)> + callback), + (override)); +}; + +} // namespace quiche::test + +#endif // QUICHE_BLIND_SIGN_AUTH_TEST_TOOLS_MOCK_BLIND_SIGN_AUTH_INTERFACE_H_
diff --git a/quiche/blind_sign_auth/test_tools/mock_blind_sign_http_interface.h b/quiche/blind_sign_auth/test_tools/mock_blind_sign_http_interface.h new file mode 100644 index 0000000..15e970b --- /dev/null +++ b/quiche/blind_sign_auth/test_tools/mock_blind_sign_http_interface.h
@@ -0,0 +1,32 @@ +// 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. + +#ifndef QUICHE_BLIND_SIGN_AUTH_TEST_TOOLS_MOCK_BLIND_SIGN_HTTP_INTERFACE_H_ +#define QUICHE_BLIND_SIGN_AUTH_TEST_TOOLS_MOCK_BLIND_SIGN_HTTP_INTERFACE_H_ + +#include <functional> +#include <string> + +#include "absl/status/statusor.h" +#include "quiche/blind_sign_auth/blind_sign_http_interface.h" +#include "quiche/blind_sign_auth/blind_sign_http_response.h" +#include "quiche/common/platform/api/quiche_export.h" +#include "quiche/common/platform/api/quiche_test.h" + +namespace quiche::test { + +class QUICHE_NO_EXPORT MockBlindSignHttpInterface + : public BlindSignHttpInterface { + public: + MOCK_METHOD( + void, DoRequest, + (const std::string& path_and_query, + const std::string& authorization_header, const std::string& body, + std::function<void(absl::StatusOr<BlindSignHttpResponse>)> callback), + (override)); +}; + +} // namespace quiche::test + +#endif // QUICHE_BLIND_SIGN_AUTH_TEST_TOOLS_MOCK_BLIND_SIGN_HTTP_INTERFACE_H_