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_