Add new PRIVACYPASS test tool

PiperOrigin-RevId: 863986949
diff --git a/build/source_list.bzl b/build/source_list.bzl
index c974bdd..29754c2 100644
--- a/build/source_list.bzl
+++ b/build/source_list.bzl
@@ -1034,6 +1034,7 @@
     "quic/masque/masque_server_backend.h",
     "quic/masque/masque_server_session.h",
     "quic/masque/masque_utils.h",
+    "quic/masque/private_tokens.h",
 ]
 masque_support_srcs = [
     "quic/masque/masque_client.cc",
@@ -1049,6 +1050,7 @@
     "quic/masque/masque_server_backend.cc",
     "quic/masque/masque_server_session.cc",
     "quic/masque/masque_utils.cc",
+    "quic/masque/private_tokens.cc",
 ]
 io_tool_support_hdrs = [
     "common/platform/api/quiche_event_loop.h",
@@ -1449,6 +1451,7 @@
     "quic/masque/masque_server_bin.cc",
     "quic/masque/masque_tcp_client_bin.cc",
     "quic/masque/masque_tcp_server_bin.cc",
+    "quic/masque/private_tokens_bin.cc",
     "quic/moqt/tools/chat_client_bin.cc",
     "quic/moqt/tools/moqt_ingestion_server_bin.cc",
     "quic/moqt/tools/moqt_relay_bin.cc",
diff --git a/build/source_list.gni b/build/source_list.gni
index 9e1a1cf..55bc643 100644
--- a/build/source_list.gni
+++ b/build/source_list.gni
@@ -1034,6 +1034,7 @@
     "src/quiche/quic/masque/masque_server_backend.h",
     "src/quiche/quic/masque/masque_server_session.h",
     "src/quiche/quic/masque/masque_utils.h",
+    "src/quiche/quic/masque/private_tokens.h",
 ]
 masque_support_srcs = [
     "src/quiche/quic/masque/masque_client.cc",
@@ -1049,6 +1050,7 @@
     "src/quiche/quic/masque/masque_server_backend.cc",
     "src/quiche/quic/masque/masque_server_session.cc",
     "src/quiche/quic/masque/masque_utils.cc",
+    "src/quiche/quic/masque/private_tokens.cc",
 ]
 io_tool_support_hdrs = [
     "src/quiche/common/platform/api/quiche_event_loop.h",
@@ -1452,6 +1454,7 @@
     "src/quiche/quic/masque/masque_server_bin.cc",
     "src/quiche/quic/masque/masque_tcp_client_bin.cc",
     "src/quiche/quic/masque/masque_tcp_server_bin.cc",
+    "src/quiche/quic/masque/private_tokens_bin.cc",
     "src/quiche/quic/moqt/tools/chat_client_bin.cc",
     "src/quiche/quic/moqt/tools/moqt_ingestion_server_bin.cc",
     "src/quiche/quic/moqt/tools/moqt_relay_bin.cc",
diff --git a/build/source_list.json b/build/source_list.json
index e4cb214..4d6d296 100644
--- a/build/source_list.json
+++ b/build/source_list.json
@@ -1032,7 +1032,8 @@
     "quiche/quic/masque/masque_server.h",
     "quiche/quic/masque/masque_server_backend.h",
     "quiche/quic/masque/masque_server_session.h",
-    "quiche/quic/masque/masque_utils.h"
+    "quiche/quic/masque/masque_utils.h",
+    "quiche/quic/masque/private_tokens.h"
   ],
   "masque_support_srcs": [
     "quiche/quic/masque/masque_client.cc",
@@ -1047,7 +1048,8 @@
     "quiche/quic/masque/masque_server.cc",
     "quiche/quic/masque/masque_server_backend.cc",
     "quiche/quic/masque/masque_server_session.cc",
-    "quiche/quic/masque/masque_utils.cc"
+    "quiche/quic/masque/masque_utils.cc",
+    "quiche/quic/masque/private_tokens.cc"
   ],
   "io_tool_support_hdrs": [
     "quiche/common/platform/api/quiche_event_loop.h",
@@ -1451,6 +1453,7 @@
     "quiche/quic/masque/masque_server_bin.cc",
     "quiche/quic/masque/masque_tcp_client_bin.cc",
     "quiche/quic/masque/masque_tcp_server_bin.cc",
+    "quiche/quic/masque/private_tokens_bin.cc",
     "quiche/quic/moqt/tools/chat_client_bin.cc",
     "quiche/quic/moqt/tools/moqt_ingestion_server_bin.cc",
     "quiche/quic/moqt/tools/moqt_relay_bin.cc",
diff --git a/quiche/quic/masque/masque_ohttp_client_bin.cc b/quiche/quic/masque/masque_ohttp_client_bin.cc
index 5f50cbe..1fcf9f3 100644
--- a/quiche/quic/masque/masque_ohttp_client_bin.cc
+++ b/quiche/quic/masque/masque_ohttp_client_bin.cc
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include <stdbool.h>
-
+#include <cstddef>
+#include <cstdint>
 #include <optional>
 #include <string>
 #include <utility>
diff --git a/quiche/quic/masque/private_tokens.cc b/quiche/quic/masque/private_tokens.cc
new file mode 100644
index 0000000..c34a3fc
--- /dev/null
+++ b/quiche/quic/masque/private_tokens.cc
@@ -0,0 +1,194 @@
+// Copyright 2026 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/quic/masque/private_tokens.h"
+
+#include <memory>
+#include <string>
+
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/escaping.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/str_replace.h"
+#include "absl/strings/string_view.h"
+#include "anonymous_tokens/cpp/crypto/crypto_utils.h"
+#include "anonymous_tokens/cpp/crypto/rsa_blinder.h"
+#include "anonymous_tokens/cpp/privacy_pass/rsa_bssa_client.h"
+#include "anonymous_tokens/cpp/privacy_pass/token_encodings.h"
+#include "openssl/base.h"
+#include "openssl/bio.h"
+#include "openssl/bn.h"
+#include "openssl/digest.h"
+#include "openssl/pem.h"
+#include "openssl/rand.h"
+#include "openssl/rsa.h"
+#include "openssl/sha.h"
+#include "openssl/sha2.h"
+#include "quiche/common/quiche_status_utils.h"
+
+namespace quic {
+
+namespace AT = ::anonymous_tokens;
+
+namespace {
+
+absl::StatusOr<std::unique_ptr<AT::RsaBlinder>> CreateBlinder(
+    const RSA* public_key) {
+  const BIGNUM* rsa_modulus_bignum = RSA_get0_n(public_key);
+  if (rsa_modulus_bignum == nullptr) {
+    return absl::InternalError("Failed to get RSA modulus");
+  }
+  QUICHE_ASSIGN_OR_RETURN(std::string rsa_modulus,
+                          AT::BignumToString(*rsa_modulus_bignum,
+                                             BN_num_bytes(rsa_modulus_bignum)));
+  const BIGNUM* rsa_public_exponent_bignum = RSA_get0_e(public_key);
+  if (rsa_public_exponent_bignum == nullptr) {
+    return absl::InternalError("Failed to get RSA public exponent");
+  }
+  QUICHE_ASSIGN_OR_RETURN(
+      std::string rsa_public_exponent,
+      AT::BignumToString(*rsa_public_exponent_bignum,
+                         BN_num_bytes(rsa_public_exponent_bignum)));
+
+  return AT::RsaBlinder::New(rsa_modulus, rsa_public_exponent, EVP_sha384(),
+                             EVP_sha384(), SHA384_DIGEST_LENGTH,
+                             /*use_rsa_public_exponent=*/false);
+}
+
+absl::StatusOr<std::string> BlindSign(RSA* private_key,
+                                      absl::string_view blinded_message) {
+  if (blinded_message.size() != RSA_size(private_key)) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("Blind message size ", blinded_message.size(),
+                     " does not match RSA size ", RSA_size(private_key)));
+  }
+  std::string signature(blinded_message.size(), 0);
+  // Compute a raw RSA signature.
+  size_t out_len;
+  if (RSA_sign_raw(
+          /*rsa=*/private_key, /*out_len=*/&out_len,
+          /*out=*/reinterpret_cast<uint8_t*>(signature.data()),
+          /*max_out=*/signature.size(),
+          /*in=*/reinterpret_cast<const uint8_t*>(blinded_message.data()),
+          /*in_len=*/blinded_message.size(),
+          /*padding=*/RSA_NO_PADDING) != 1) {
+    return absl::InternalError(
+        "RSA_sign_raw failed when called from RsaBlindSigner::Sign");
+  }
+  if (out_len != signature.size()) {
+    return absl::InternalError(absl::StrCat("RSA_sign_raw set out_len to ",
+                                            out_len, " instead of ",
+                                            signature.size()));
+  }
+  return signature;
+}
+
+}  // namespace
+
+std::string Base64UrlEncodeWithPadding(absl::string_view input) {
+  // Private tokens require padded base64url so we need to use the non-URL-safe
+  // base64 encoding to get the padding then replace '+' and '/'.
+  std::string base64_encoded = absl::Base64Escape(input);
+  absl::StrReplaceAll({{"+", "-"}, {"/", "_"}}, &base64_encoded);
+  return base64_encoded;
+}
+
+absl::StatusOr<bssl::UniquePtr<RSA>> ParseRsaPrivateKey(
+    absl::string_view file_path) {
+  BIO* bio = BIO_new_file(file_path.data(), "r");
+  if (!bio) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("Failed to open file \"", file_path, "\""));
+  }
+  RSA* rsa_key = PEM_read_bio_RSAPrivateKey(bio, nullptr, nullptr, nullptr);
+  BIO_free(bio);
+  if (!rsa_key) {
+    return absl::InvalidArgumentError(absl::StrCat(
+        "Failed to read RSA private key from file \"", file_path, "\""));
+  }
+  return bssl::UniquePtr<RSA>(rsa_key);
+}
+
+absl::StatusOr<bssl::UniquePtr<RSA>> ParseRsaPublicKey(
+    absl::string_view file_path) {
+  BIO* bio = BIO_new_file(file_path.data(), "r");
+  if (!bio) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("Failed to open file \"", file_path, "\""));
+  }
+  RSA* rsa_key = PEM_read_bio_RSA_PUBKEY(bio, nullptr, nullptr, nullptr);
+  BIO_free(bio);
+  if (!rsa_key) {
+    return absl::InvalidArgumentError(absl::StrCat(
+        "Failed to read RSA public key from file \"", file_path, "\""));
+  }
+  return bssl::UniquePtr<RSA>(rsa_key);
+}
+
+absl::StatusOr<std::string> EncodePrivacyPassPublicKey(const RSA* public_key) {
+  QUICHE_ASSIGN_OR_RETURN(std::string der_encoding,
+                          AT::RsaSsaPssPublicKeyToDerEncoding(public_key));
+  return Base64UrlEncodeWithPadding(der_encoding);
+}
+
+absl::StatusOr<std::string> CreateTokenLocally(RSA* private_key,
+                                               const RSA* public_key) {
+  QUICHE_ASSIGN_OR_RETURN(std::string public_key_der,
+                          AT::RsaSsaPssPublicKeyToDerEncoding(public_key));
+  static constexpr size_t kNonceSize = 32;
+  static constexpr uint16_t kPrivacyPassBlindRsa2048TokenType = 0x0002;
+  static constexpr absl::string_view kChallenge = "";
+  AT::Token token;
+  token.token_type = kPrivacyPassBlindRsa2048TokenType;
+  token.token_key_id = std::string(SHA256_DIGEST_LENGTH, '\0');
+  if (SHA256(reinterpret_cast<const uint8_t*>(public_key_der.data()),
+             public_key_der.size(),
+             reinterpret_cast<uint8_t*>(token.token_key_id.data())) ==
+      nullptr) {
+    return absl::InternalError("Failed to compute token_key_id");
+  }
+  token.context = std::string(SHA256_DIGEST_LENGTH, '\0');
+  if (SHA256(reinterpret_cast<const uint8_t*>(kChallenge.data()),
+             kChallenge.size(),
+             reinterpret_cast<uint8_t*>(token.context.data())) == nullptr) {
+    return absl::InternalError("Failed to compute context/challenge_digest");
+  }
+  token.nonce = std::string(kNonceSize, '\0');
+  RAND_bytes(reinterpret_cast<uint8_t*>(token.nonce.data()),
+             token.nonce.size());
+  QUICHE_ASSIGN_OR_RETURN(std::string token_input,
+                          AT::AuthenticatorInput(token));
+  QUICHE_ASSIGN_OR_RETURN(std::unique_ptr<AT::RsaBlinder> blinder,
+                          CreateBlinder(public_key));
+  QUICHE_ASSIGN_OR_RETURN(std::string blinded_message,
+                          blinder->Blind(token_input));
+  QUICHE_ASSIGN_OR_RETURN(std::string blinded_signature,
+                          BlindSign(private_key, blinded_message));
+  QUICHE_ASSIGN_OR_RETURN(token.authenticator,
+                          blinder->Unblind(blinded_signature));
+  QUICHE_RETURN_IF_ERROR(blinder->Verify(token.authenticator, token_input));
+  QUICHE_ASSIGN_OR_RETURN(std::string token_bytes, AT::MarshalToken(token));
+  return Base64UrlEncodeWithPadding(token_bytes);
+}
+
+absl::Status ValidateToken(absl::string_view base64_public_key,
+                           absl::string_view base64_token) {
+  std::string der_public_key;
+  if (!absl::WebSafeBase64Unescape(base64_public_key, &der_public_key)) {
+    return absl::InvalidArgumentError("Failed to decode the base64 public key");
+  }
+  QUICHE_ASSIGN_OR_RETURN(
+      bssl::UniquePtr<RSA> public_key,
+      AT::RsaSsaPssPublicKeyFromDerEncoding(der_public_key));
+  std::string binary_token;
+  if (!absl::WebSafeBase64Unescape(base64_token, &binary_token)) {
+    return absl::InvalidArgumentError("Failed to decode the base64 token");
+  }
+
+  QUICHE_ASSIGN_OR_RETURN(AT::Token token, AT::UnmarshalToken(binary_token));
+  return AT::PrivacyPassRsaBssaClient::Verify(token, *public_key);
+}
+
+}  // namespace quic
diff --git a/quiche/quic/masque/private_tokens.h b/quiche/quic/masque/private_tokens.h
new file mode 100644
index 0000000..0e7de59
--- /dev/null
+++ b/quiche/quic/masque/private_tokens.h
@@ -0,0 +1,45 @@
+// Copyright 2026 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_QUIC_MASQUE_PRIVATE_TOKENS_H_
+#define QUICHE_QUIC_MASQUE_PRIVATE_TOKENS_H_
+
+#include <string>
+
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/string_view.h"
+#include "openssl/base.h"
+#include "openssl/rsa.h"
+
+namespace quic {
+
+// PRIVACYPASS requires base64url but with padding.
+std::string Base64UrlEncodeWithPadding(absl::string_view input);
+
+// Parse an RSA private key from the given file path in PEM format.
+absl::StatusOr<bssl::UniquePtr<RSA>> ParseRsaPrivateKey(
+    absl::string_view file_path);
+
+// Parse an RSA public key from the given file path in PEM format.
+absl::StatusOr<bssl::UniquePtr<RSA>> ParseRsaPublicKey(
+    absl::string_view file_path);
+
+// Encodes the key into a entry in the base64 token-key object from the
+// PRIVACYPASS RFC. https://www.rfc-editor.org/rfc/rfc9578.html#section-4
+absl::StatusOr<std::string> EncodePrivacyPassPublicKey(const RSA* public_key);
+
+// Performs both the client and issuer sides of the blind signature protocol
+// locally.
+absl::StatusOr<std::string> CreateTokenLocally(RSA* private_key,
+                                               const RSA* public_key);
+
+// Checks that a token is valid for the given public key. Takes the token and
+// public key as base64 encoded strings in the format from RFC 9578.
+absl::Status ValidateToken(absl::string_view base64_public_key,
+                           absl::string_view base64_token);
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_MASQUE_PRIVATE_TOKENS_H_
diff --git a/quiche/quic/masque/private_tokens_bin.cc b/quiche/quic/masque/private_tokens_bin.cc
new file mode 100644
index 0000000..02317d5
--- /dev/null
+++ b/quiche/quic/masque/private_tokens_bin.cc
@@ -0,0 +1,73 @@
+// Copyright 2026 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 <string>
+#include <vector>
+
+#include "absl/status/status.h"
+#include "absl/strings/str_cat.h"
+#include "openssl/base.h"
+#include "openssl/rsa.h"
+#include "quiche/quic/masque/private_tokens.h"
+#include "quiche/common/platform/api/quiche_command_line_flags.h"
+#include "quiche/common/platform/api/quiche_logging.h"
+#include "quiche/common/quiche_status_utils.h"
+
+DEFINE_QUICHE_COMMAND_LINE_FLAG(std::string, private_key_file, "",
+                                "Path to the PEM-encoded RSA private key.");
+
+DEFINE_QUICHE_COMMAND_LINE_FLAG(std::string, public_key_file, "",
+                                "Path to the PEM-encoded RSA public key.");
+
+namespace quic {
+namespace {
+
+absl::Status RunPrivateTokens(int argc, char* argv[]) {
+  const char* usage =
+      "Usage: private_tokens --private_key_file=<private-key-file> "
+      "--public_key_file=<public-key-file>";
+  std::vector<std::string> params =
+      quiche::QuicheParseCommandLineFlags(usage, argc, argv);
+
+  QUICHE_ASSIGN_OR_RETURN(bssl::UniquePtr<RSA> private_key,
+                          ParseRsaPrivateKey(quiche::GetQuicheCommandLineFlag(
+                              FLAGS_private_key_file)));
+  QUICHE_ASSIGN_OR_RETURN(bssl::UniquePtr<RSA> public_key,
+                          ParseRsaPublicKey(quiche::GetQuicheCommandLineFlag(
+                              FLAGS_public_key_file)));
+  QUICHE_ASSIGN_OR_RETURN(std::string encoded_public_key,
+                          EncodePrivacyPassPublicKey(public_key.get()));
+
+  std::string issuer_config = absl::StrCat(
+      "{\n  \"issuer-request-uri\": \"https://issuer.example.net/request\",\n",
+      "  \"token-keys\": [\n    {\n      \"token-type\": 2,\n",
+      "      \"token-key\": \"", encoded_public_key, "\",\n    }\n  ]\n}");
+
+  QUICHE_LOG(INFO) << "The issuer config could look like:\n" << issuer_config;
+
+  QUICHE_ASSIGN_OR_RETURN(
+      std::string token,
+      CreateTokenLocally(private_key.get(), public_key.get()));
+
+  std::string auth_header =
+      absl::StrCat("Authorization: PrivateToken token=\"", token, "\"");
+
+  QUICHE_LOG(INFO) << "The auth header would look like:\n" << auth_header;
+
+  QUICHE_RETURN_IF_ERROR(ValidateToken(encoded_public_key, token));
+  QUICHE_LOG(INFO) << "Token validation succeeded";
+  return absl::OkStatus();
+}
+
+}  // namespace
+}  // namespace quic
+
+int main(int argc, char* argv[]) {
+  absl::Status status = quic::RunPrivateTokens(argc, argv);
+  if (!status.ok()) {
+    QUICHE_LOG(ERROR) << status.message();
+    return 1;
+  }
+  return 0;
+}