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; +}