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_