Implement HTTP Signature Authentication

This CL implements HTTP Signature Authentication as defined in [draft-ietf-httpbis-unprompted-auth-05](https://www.ietf.org/archive/id/draft-ietf-httpbis-unprompted-auth-05.html).

For now this is all inside the MASQUE code to allow easily testing interop at the IETF hackathon without the testing requirements that come with the main HTTP/3 code.

PiperOrigin-RevId: 597238379
diff --git a/quiche/quic/masque/masque_client_bin.cc b/quiche/quic/masque/masque_client_bin.cc
index 5ee715f..d1a8608 100644
--- a/quiche/quic/masque/masque_client_bin.cc
+++ b/quiche/quic/masque/masque_client_bin.cc
@@ -7,11 +7,16 @@
 // HTTP/3 requests to web servers tunnelled over that MASQUE connection.
 // e.g.: masque_client $PROXY_HOST:$PROXY_PORT $URL1 $URL2
 
+#include <cstdint>
 #include <memory>
 #include <string>
+#include <vector>
 
+#include "absl/strings/escaping.h"
 #include "absl/strings/str_cat.h"
+#include "absl/strings/str_split.h"
 #include "absl/strings/string_view.h"
+#include "openssl/curve25519.h"
 #include "quiche/quic/core/io/quic_default_event_loop.h"
 #include "quiche/quic/core/io/quic_event_loop.h"
 #include "quiche/quic/core/quic_default_clock.h"
@@ -47,6 +52,12 @@
     "For example: \"name1:value1;name2:value2\".");
 
 DEFINE_QUICHE_COMMAND_LINE_FLAG(
+    std::string, signature_auth, "",
+    "Enables HTTP Signature Authentication. Pass in the string \"new\" to "
+    "generate new keys. Otherwise, pass in the key ID in ASCII followed by a "
+    "colon and the 32-byte private key as hex. For example: \"kid:0123...f\".");
+
+DEFINE_QUICHE_COMMAND_LINE_FLAG(
     bool, bring_up_tun, false,
     "If set to true, no URLs need to be specified and instead a TUN device "
     "is brought up with the assigned IP from the MASQUE CONNECT-IP server.");
@@ -232,6 +243,76 @@
   // {?target_host,target_port}.
   std::vector<std::string> urls =
       quiche::QuicheParseCommandLineFlags(usage, argc, argv);
+
+  std::string signature_auth_param =
+      quiche::GetQuicheCommandLineFlag(FLAGS_signature_auth);
+  std::string signature_auth_key_id;
+  std::string signature_auth_private_key;
+  std::string signature_auth_public_key;
+  if (!signature_auth_param.empty()) {
+    static constexpr size_t kEd25519Rfc8032PrivateKeySize = 32;
+    uint8_t public_key[ED25519_PUBLIC_KEY_LEN];
+    uint8_t private_key[ED25519_PRIVATE_KEY_LEN];
+    const bool is_new_key_pair = signature_auth_param == "new";
+    if (is_new_key_pair) {
+      ED25519_keypair(public_key, private_key);
+      QUIC_LOG(INFO) << "Generated new Signature Authentication key pair";
+    } else {
+      std::vector<absl::string_view> signature_auth_param_split =
+          absl::StrSplit(signature_auth_param, absl::MaxSplits(':', 1));
+      std::string private_key_seed;
+      if (signature_auth_param_split.size() != 2) {
+        QUIC_LOG(ERROR)
+            << "Signature authentication parameter is missing a colon";
+        return 1;
+      }
+      signature_auth_key_id = signature_auth_param_split[0];
+      if (signature_auth_key_id.empty()) {
+        QUIC_LOG(ERROR) << "Signature authentication key ID cannot be empty";
+        return 1;
+      }
+      private_key_seed = absl::HexStringToBytes(signature_auth_param_split[1]);
+      if (private_key_seed.size() != kEd25519Rfc8032PrivateKeySize) {
+        QUIC_LOG(ERROR)
+            << "Invalid signature authentication private key length "
+            << private_key_seed.size();
+        return 1;
+      }
+      ED25519_keypair_from_seed(
+          public_key, private_key,
+          reinterpret_cast<uint8_t*>(private_key_seed.data()));
+      QUIC_LOG(INFO) << "Loaded Signature Authentication key pair";
+    }
+    // Note that Ed25519 private keys are 32 bytes long per RFC 8032. However,
+    // to reduce CPU costs, BoringSSL represents private keys in memory as the
+    // concatenation of the 32-byte private key and the corresponding 32-byte
+    // public key - which makes for a total of 64 bytes. The private key log
+    // below relies on this BoringSSL implementation detail to extract the
+    // RFC 8032 private key because BoringSSL does not provide a supported way
+    // to access it. This is required to allow us to print the private key in a
+    // format that can be passed back in to BoringSSL from the command-line. See
+    // curve25519.h for details. The rest of our signature authentication code
+    // uses the BoringSSL representation without relying on this implementation
+    // detail.
+    static_assert(kEd25519Rfc8032PrivateKeySize <=
+                  static_cast<size_t>(ED25519_PRIVATE_KEY_LEN));
+    QUIC_LOG(INFO) << "Private key: "
+                   << absl::BytesToHexString(absl::string_view(
+                          reinterpret_cast<char*>(private_key),
+                          kEd25519Rfc8032PrivateKeySize));
+    QUIC_LOG(INFO) << "Public key: "
+                   << absl::BytesToHexString(
+                          absl::string_view(reinterpret_cast<char*>(public_key),
+                                            ED25519_PUBLIC_KEY_LEN));
+    if (is_new_key_pair) {
+      return 0;
+    }
+    signature_auth_private_key = std::string(
+        reinterpret_cast<char*>(private_key), ED25519_PRIVATE_KEY_LEN);
+    signature_auth_public_key = std::string(reinterpret_cast<char*>(public_key),
+                                            ED25519_PUBLIC_KEY_LEN);
+  }
+
   bool bring_up_tun = quiche::GetQuicheCommandLineFlag(FLAGS_bring_up_tun);
   bool bring_up_tap = quiche::GetQuicheCommandLineFlag(FLAGS_bring_up_tap);
   if (urls.empty() && !bring_up_tun && !bring_up_tap) {
@@ -314,6 +395,11 @@
 
   masque_client->masque_client_session()->set_additional_headers(
       quiche::GetQuicheCommandLineFlag(FLAGS_proxy_headers));
+  if (!signature_auth_param.empty()) {
+    masque_client->masque_client_session()->EnableSignatureAuth(
+        signature_auth_key_id, signature_auth_private_key,
+        signature_auth_public_key);
+  }
 
   if (bring_up_tun) {
     QUIC_LOG(INFO) << "Bringing up tun";
diff --git a/quiche/quic/masque/masque_client_session.cc b/quiche/quic/masque/masque_client_session.cc
index 14cf58a..258394d 100644
--- a/quiche/quic/masque/masque_client_session.cc
+++ b/quiche/quic/masque/masque_client_session.cc
@@ -4,16 +4,20 @@
 
 #include "quiche/quic/masque/masque_client_session.h"
 
+#include <cstdint>
 #include <cstring>
+#include <optional>
 #include <string>
 #include <vector>
 
 #include "absl/algorithm/container.h"
 #include "absl/container/flat_hash_map.h"
 #include "absl/container/flat_hash_set.h"
+#include "absl/strings/escaping.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/str_split.h"
 #include "absl/strings/string_view.h"
+#include "openssl/curve25519.h"
 #include "quiche/quic/core/http/spdy_utils.h"
 #include "quiche/quic/core/quic_data_reader.h"
 #include "quiche/quic/core/quic_data_writer.h"
@@ -149,7 +153,7 @@
   headers[":authority"] = authority;
   headers[":path"] = canonicalized_path;
   headers["connect-udp-version"] = "12";
-  AddAdditionalHeaders(headers);
+  AddAdditionalHeaders(headers, url);
   size_t bytes_sent =
       stream->SendRequest(std::move(headers), /*body=*/"", /*fin=*/false);
   if (bytes_sent == 0) {
@@ -196,7 +200,7 @@
   headers[":authority"] = authority;
   headers[":path"] = path;
   headers["connect-ip-version"] = "3";
-  AddAdditionalHeaders(headers);
+  AddAdditionalHeaders(headers, url);
   size_t bytes_sent =
       stream->SendRequest(std::move(headers), /*body=*/"", /*fin=*/false);
   if (bytes_sent == 0) {
@@ -245,7 +249,7 @@
   headers[":scheme"] = scheme;
   headers[":authority"] = authority;
   headers[":path"] = path;
-  AddAdditionalHeaders(headers);
+  AddAdditionalHeaders(headers, url);
   size_t bytes_sent =
       stream->SendRequest(std::move(headers), /*body=*/"", /*fin=*/false);
   if (bytes_sent == 0) {
@@ -685,8 +689,71 @@
   fake_addresses_.erase(fake_address.ToPackedString());
 }
 
-void MasqueClientSession::AddAdditionalHeaders(
-    spdy::Http2HeaderBlock& headers) const {
+void MasqueClientSession::EnableSignatureAuth(absl::string_view key_id,
+                                              absl::string_view private_key,
+                                              absl::string_view public_key) {
+  QUICHE_CHECK(!key_id.empty());
+  QUICHE_CHECK_EQ(private_key.size(),
+                  static_cast<size_t>(ED25519_PRIVATE_KEY_LEN));
+  QUICHE_CHECK_EQ(public_key.size(),
+                  static_cast<size_t>(ED25519_PUBLIC_KEY_LEN));
+  signature_auth_key_id_ = key_id;
+  signature_auth_private_key_ = private_key;
+  signature_auth_public_key_ = public_key;
+}
+
+std::optional<std::string> MasqueClientSession::ComputeSignatureAuthHeader(
+    const QuicUrl& url) {
+  if (signature_auth_private_key_.empty()) {
+    return std::nullopt;
+  }
+  std::string scheme = url.scheme();
+  std::string host = url.host();
+  uint16_t port = url.port();
+  std::string realm = "";
+  std::string key_exporter_output;
+  std::string key_exporter_context = ComputeSignatureAuthContext(
+      kEd25519SignatureScheme, signature_auth_key_id_,
+      signature_auth_public_key_, scheme, host, port, realm);
+  if (!GetMutableCryptoStream()->ExportKeyingMaterial(
+          kSignatureAuthLabel, key_exporter_context, kSignatureAuthExporterSize,
+          &key_exporter_output)) {
+    QUIC_LOG(FATAL) << "Signature auth TLS exporter failed";
+    return std::nullopt;
+  }
+  QUICHE_CHECK_EQ(key_exporter_output.size(), kSignatureAuthExporterSize);
+  std::string signature_input =
+      key_exporter_output.substr(0, kSignatureAuthSignatureInputSize);
+  std::string verification = key_exporter_output.substr(
+      kSignatureAuthSignatureInputSize, kSignatureAuthVerificationSize);
+  std::string data_covered_by_signature =
+      SignatureAuthDataCoveredBySignature(signature_input);
+  uint8_t signature[ED25519_SIGNATURE_LEN];
+  if (ED25519_sign(
+          signature,
+          reinterpret_cast<const uint8_t*>(data_covered_by_signature.data()),
+          data_covered_by_signature.size(),
+          reinterpret_cast<const uint8_t*>(
+              signature_auth_private_key_.data())) != 1) {
+    QUIC_LOG(FATAL) << "Signature auth signature failed";
+    return std::nullopt;
+  }
+  return absl::StrCat(
+      "Signature k=", absl::WebSafeBase64Escape(signature_auth_key_id_),
+      ", a=", absl::WebSafeBase64Escape(signature_auth_public_key_), ", p=",
+      absl::WebSafeBase64Escape(absl::string_view(
+          reinterpret_cast<const char*>(signature), sizeof(signature))),
+      ", s=", kEd25519SignatureScheme,
+      ", v=", absl::WebSafeBase64Escape(verification));
+}
+
+void MasqueClientSession::AddAdditionalHeaders(spdy::Http2HeaderBlock& headers,
+                                               const QuicUrl& url) {
+  std::optional<std::string> signature_auth_header =
+      ComputeSignatureAuthHeader(url);
+  if (signature_auth_header.has_value()) {
+    headers["authorization"] = *signature_auth_header;
+  }
   if (additional_headers_.empty()) {
     return;
   }
diff --git a/quiche/quic/masque/masque_client_session.h b/quiche/quic/masque/masque_client_session.h
index 8b8331e..eeb317a 100644
--- a/quiche/quic/masque/masque_client_session.h
+++ b/quiche/quic/masque/masque_client_session.h
@@ -13,6 +13,7 @@
 #include "quiche/quic/masque/masque_utils.h"
 #include "quiche/quic/platform/api/quic_export.h"
 #include "quiche/quic/platform/api/quic_socket_address.h"
+#include "quiche/quic/tools/quic_url.h"
 
 namespace quic {
 
@@ -151,6 +152,13 @@
     additional_headers_ = additional_headers;
   }
 
+  // Set the signature auth key ID and private key. key_id MUST be non-empty,
+  // private_key MUST be ED25519_PRIVATE_KEY_LEN bytes long and public_key MUST
+  // be ED25519_PUBLIC_KEY_LEN bytes long.
+  void EnableSignatureAuth(absl::string_view key_id,
+                           absl::string_view private_key,
+                           absl::string_view public_key);
+
  private:
   // State that the MasqueClientSession keeps for each CONNECT-UDP request.
   class QUIC_NO_EXPORT ConnectUdpClientState
@@ -290,11 +298,16 @@
   const ConnectEthernetClientState* GetOrCreateConnectEthernetClientState(
       EncapsulatedEthernetSession* encapsulated_ethernet_session);
 
-  void AddAdditionalHeaders(spdy::Http2HeaderBlock& headers) const;
+  std::optional<std::string> ComputeSignatureAuthHeader(const QuicUrl& url);
+  void AddAdditionalHeaders(spdy::Http2HeaderBlock& headers,
+                            const QuicUrl& url);
 
   MasqueMode masque_mode_;
   std::string uri_template_;
   std::string additional_headers_;
+  std::string signature_auth_key_id_;
+  std::string signature_auth_private_key_;
+  std::string signature_auth_public_key_;
   std::list<ConnectUdpClientState> connect_udp_client_states_;
   std::list<ConnectIpClientState> connect_ip_client_states_;
   std::list<ConnectEthernetClientState> connect_ethernet_client_states_;
diff --git a/quiche/quic/masque/masque_server_backend.cc b/quiche/quic/masque/masque_server_backend.cc
index a40ecc0..8fbeec7 100644
--- a/quiche/quic/masque/masque_server_backend.cc
+++ b/quiche/quic/masque/masque_server_backend.cc
@@ -5,7 +5,9 @@
 #include "quiche/quic/masque/masque_server_backend.h"
 
 #include "absl/strings/str_cat.h"
+#include "absl/strings/str_split.h"
 #include "absl/strings/string_view.h"
+#include "openssl/curve25519.h"
 
 namespace quic {
 
@@ -151,4 +153,44 @@
   return address;
 }
 
+void MasqueServerBackend::SetSignatureAuth(absl::string_view signature_auth) {
+  signature_auth_credentials_.clear();
+  if (signature_auth.empty()) {
+    return;
+  }
+  for (absl::string_view sp : absl::StrSplit(signature_auth, ';')) {
+    quiche::QuicheTextUtils::RemoveLeadingAndTrailingWhitespace(&sp);
+    if (sp.empty()) {
+      continue;
+    }
+    std::vector<absl::string_view> kv =
+        absl::StrSplit(sp, absl::MaxSplits(':', 1));
+    quiche::QuicheTextUtils::RemoveLeadingAndTrailingWhitespace(&kv[0]);
+    quiche::QuicheTextUtils::RemoveLeadingAndTrailingWhitespace(&kv[1]);
+    SignatureAuthCredential credential;
+    credential.key_id = std::string(kv[0]);
+    std::string public_key = absl::HexStringToBytes(kv[1]);
+    if (public_key.size() != sizeof(credential.public_key)) {
+      QUIC_LOG(FATAL) << "Invalid signature auth public key length "
+                      << public_key.size();
+    }
+    memcpy(credential.public_key, public_key.data(),
+           sizeof(credential.public_key));
+    signature_auth_credentials_.push_back(credential);
+  }
+}
+
+bool MasqueServerBackend::GetSignatureAuthKeyForId(
+    absl::string_view key_id,
+    uint8_t out_public_key[ED25519_PUBLIC_KEY_LEN]) const {
+  for (const auto& credential : signature_auth_credentials_) {
+    if (credential.key_id == key_id) {
+      memcpy(out_public_key, credential.public_key,
+             sizeof(credential.public_key));
+      return true;
+    }
+  }
+  return false;
+}
+
 }  // namespace quic
diff --git a/quiche/quic/masque/masque_server_backend.h b/quiche/quic/masque/masque_server_backend.h
index 50c5a02..2285291 100644
--- a/quiche/quic/masque/masque_server_backend.h
+++ b/quiche/quic/masque/masque_server_backend.h
@@ -5,10 +5,22 @@
 #ifndef QUICHE_QUIC_MASQUE_MASQUE_SERVER_BACKEND_H_
 #define QUICHE_QUIC_MASQUE_MASQUE_SERVER_BACKEND_H_
 
+#include <cstdint>
+#include <list>
+#include <memory>
+#include <string>
+#include <vector>
+
 #include "absl/container/flat_hash_map.h"
+#include "absl/strings/string_view.h"
+#include "openssl/curve25519.h"
+#include "quiche/quic/core/quic_connection_id.h"
 #include "quiche/quic/masque/masque_utils.h"
 #include "quiche/quic/platform/api/quic_export.h"
+#include "quiche/quic/platform/api/quic_ip_address.h"
+#include "quiche/quic/tools/quic_backend_response.h"
 #include "quiche/quic/tools/quic_memory_cache_backend.h"
+#include "quiche/quic/tools/quic_simple_server_backend.h"
 #include "quiche/spdy/core/http2_header_block.h"
 
 namespace quic {
@@ -56,6 +68,21 @@
   // Provides a unique client IP address for each CONNECT-IP client.
   QuicIpAddress GetNextClientIpAddress();
 
+  // Pass in a list of key identifiers and hex-encoded public keys, separated
+  // with colons and semicolons. For example: "kid1:0123...f;kid2:0123...f".
+  void SetSignatureAuth(absl::string_view signature_auth);
+
+  // Returns whether any signature auth credentials are configured.
+  bool IsSignatureAuthEnabled() const {
+    return !signature_auth_credentials_.empty();
+  }
+
+  // If the key ID is known, copies the corresponding public key to
+  // out_public_key and returns true. Otherwise returns false.
+  bool GetSignatureAuthKeyForId(
+      absl::string_view key_id,
+      uint8_t out_public_key[ED25519_PUBLIC_KEY_LEN]) const;
+
  private:
   // Handle MASQUE request.
   bool MaybeHandleMasqueRequest(
@@ -73,6 +100,11 @@
                       QuicConnectionIdHash>
       backend_client_states_;
   uint8_t connect_ip_next_client_ip_[4];
+  struct QUIC_NO_EXPORT SignatureAuthCredential {
+    std::string key_id;
+    uint8_t public_key[ED25519_PUBLIC_KEY_LEN];
+  };
+  std::list<SignatureAuthCredential> signature_auth_credentials_;
 };
 
 }  // namespace quic
diff --git a/quiche/quic/masque/masque_server_bin.cc b/quiche/quic/masque/masque_server_bin.cc
index 763ec55..45b53f9 100644
--- a/quiche/quic/masque/masque_server_bin.cc
+++ b/quiche/quic/masque/masque_server_bin.cc
@@ -35,6 +35,13 @@
     std::string, masque_mode, "",
     "Allows setting MASQUE mode, currently only valid value is \"open\".");
 
+DEFINE_QUICHE_COMMAND_LINE_FLAG(
+    std::string, signature_auth, "",
+    "Require HTTP Signature Authentication. Pass in a list of key identifiers "
+    "and hex-encoded public keys. "
+    "Separated with colons and semicolons. "
+    "For example: \"kid1:0123...f;kid2:0123...f\".");
+
 int main(int argc, char* argv[]) {
   const char* usage = "Usage: masque_server [options]";
   std::vector<std::string> non_option_args =
@@ -56,6 +63,9 @@
       masque_mode, quiche::GetQuicheCommandLineFlag(FLAGS_server_authority),
       quiche::GetQuicheCommandLineFlag(FLAGS_cache_dir));
 
+  backend->SetSignatureAuth(
+      quiche::GetQuicheCommandLineFlag(FLAGS_signature_auth));
+
   auto server =
       std::make_unique<quic::MasqueServer>(masque_mode, backend.get());
 
diff --git a/quiche/quic/masque/masque_server_session.cc b/quiche/quic/masque/masque_server_session.cc
index 4e7fcdf..43efd6e 100644
--- a/quiche/quic/masque/masque_server_session.cc
+++ b/quiche/quic/masque/masque_server_session.cc
@@ -14,22 +14,29 @@
 #include <cstdint>
 #include <limits>
 #include <optional>
+#include <string>
+#include <vector>
 
 
 #include "absl/cleanup/cleanup.h"
+#include "absl/strings/escaping.h"
+#include "absl/strings/numbers.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/str_split.h"
 #include "absl/strings/string_view.h"
+#include "openssl/curve25519.h"
 #include "quiche/quic/core/http/spdy_utils.h"
 #include "quiche/quic/core/io/quic_event_loop.h"
 #include "quiche/quic/core/quic_data_reader.h"
 #include "quiche/quic/core/quic_udp_socket.h"
 #include "quiche/quic/masque/masque_utils.h"
 #include "quiche/quic/platform/api/quic_ip_address.h"
+#include "quiche/quic/tools/quic_backend_response.h"
 #include "quiche/quic/tools/quic_url.h"
 #include "quiche/common/capsule.h"
 #include "quiche/common/platform/api/quiche_url_utils.h"
 #include "quiche/common/quiche_ip_address.h"
+#include "quiche/common/quiche_text_utils.h"
 
 namespace quic {
 
@@ -153,6 +160,198 @@
   QuicSimpleServerSession::OnStreamClosed(stream_id);
 }
 
+std::unique_ptr<QuicBackendResponse>
+MasqueServerSession::MaybeCheckSignatureAuth(
+    const spdy::Http2HeaderBlock& request_headers, absl::string_view authority,
+    absl::string_view scheme,
+    QuicSimpleServerBackend::RequestHandler* request_handler) {
+  // TODO(dschinazi) Add command-line flag that makes this implementation
+  // probe-resistant by returning the usual failure instead of 401.
+  constexpr absl::string_view kSignatureAuthStatus = "401";
+  if (!masque_server_backend_->IsSignatureAuthEnabled()) {
+    return nullptr;
+  }
+  auto authorization_pair = request_headers.find("authorization");
+  if (authorization_pair == request_headers.end()) {
+    return CreateBackendErrorResponse(kSignatureAuthStatus,
+                                      "Missing authorization header");
+  }
+  absl::string_view credentials = authorization_pair->second;
+  quiche::QuicheTextUtils::RemoveLeadingAndTrailingWhitespace(&credentials);
+  std::vector<absl::string_view> v =
+      absl::StrSplit(credentials, absl::MaxSplits(' ', 1));
+  if (v.size() != 2) {
+    return CreateBackendErrorResponse(kSignatureAuthStatus,
+                                      "Authorization header missing space");
+  }
+  absl::string_view auth_scheme = v[0];
+  if (auth_scheme != "Signature") {
+    return CreateBackendErrorResponse(kSignatureAuthStatus,
+                                      "Unexpected auth scheme");
+  }
+  absl::string_view auth_parameters = v[1];
+  std::vector<absl::string_view> auth_parameters_split =
+      absl::StrSplit(auth_parameters, ',');
+  std::optional<std::string> key_id;
+  std::optional<std::string> header_public_key;
+  std::optional<std::string> proof;
+  std::optional<uint16_t> signature_scheme;
+  std::optional<std::string> verification;
+  for (absl::string_view auth_parameter : auth_parameters_split) {
+    std::vector<absl::string_view> auth_parameter_split =
+        absl::StrSplit(auth_parameter, absl::MaxSplits('=', 1));
+    if (auth_parameter_split.size() != 2) {
+      continue;
+    }
+    absl::string_view param_name = auth_parameter_split[0];
+    quiche::QuicheTextUtils::RemoveLeadingAndTrailingWhitespace(&param_name);
+    if (param_name.size() != 1) {
+      // All currently known authentication parameters are one character long.
+      continue;
+    }
+    absl::string_view param_value = auth_parameter_split[1];
+    quiche::QuicheTextUtils::RemoveLeadingAndTrailingWhitespace(&param_value);
+    std::string decoded_param;
+    switch (param_name[0]) {
+      case 'k': {
+        if (key_id.has_value()) {
+          return CreateBackendErrorResponse(kSignatureAuthStatus,
+                                            "Duplicate k");
+        }
+        if (!absl::WebSafeBase64Unescape(param_value, &decoded_param)) {
+          return CreateBackendErrorResponse(kSignatureAuthStatus,
+                                            "Failed to base64 decode k");
+        }
+        key_id = decoded_param;
+      } break;
+      case 'a': {
+        if (header_public_key.has_value()) {
+          return CreateBackendErrorResponse(kSignatureAuthStatus,
+                                            "Duplicate a");
+        }
+        if (!absl::WebSafeBase64Unescape(param_value, &decoded_param)) {
+          return CreateBackendErrorResponse(kSignatureAuthStatus,
+                                            "Failed to base64 decode a");
+        }
+        header_public_key = decoded_param;
+      } break;
+      case 'p': {
+        if (proof.has_value()) {
+          return CreateBackendErrorResponse(kSignatureAuthStatus,
+                                            "Duplicate p");
+        }
+        if (!absl::WebSafeBase64Unescape(param_value, &decoded_param)) {
+          return CreateBackendErrorResponse(kSignatureAuthStatus,
+                                            "Failed to base64 decode p");
+        }
+        proof = decoded_param;
+      } break;
+      case 's': {
+        if (signature_scheme.has_value()) {
+          return CreateBackendErrorResponse(kSignatureAuthStatus,
+                                            "Duplicate s");
+        }
+        int signature_scheme_int = 0;
+        if (!absl::SimpleAtoi(param_value, &signature_scheme_int) ||
+            signature_scheme_int < 0 ||
+            signature_scheme_int > std::numeric_limits<uint16_t>::max()) {
+          return CreateBackendErrorResponse(kSignatureAuthStatus,
+                                            "Failed to parse s");
+        }
+        signature_scheme = static_cast<uint16_t>(signature_scheme_int);
+      } break;
+      case 'v': {
+        if (verification.has_value()) {
+          return CreateBackendErrorResponse(kSignatureAuthStatus,
+                                            "Duplicate v");
+        }
+        if (!absl::WebSafeBase64Unescape(param_value, &decoded_param)) {
+          return CreateBackendErrorResponse(kSignatureAuthStatus,
+                                            "Failed to base64 decode v");
+        }
+        verification = decoded_param;
+      } break;
+    }
+  }
+  if (!key_id.has_value()) {
+    return CreateBackendErrorResponse(kSignatureAuthStatus,
+                                      "Missing k auth parameter");
+  }
+  if (!header_public_key.has_value()) {
+    return CreateBackendErrorResponse(kSignatureAuthStatus,
+                                      "Missing a auth parameter");
+  }
+  if (!proof.has_value()) {
+    return CreateBackendErrorResponse(kSignatureAuthStatus,
+                                      "Missing p auth parameter");
+  }
+  if (!signature_scheme.has_value()) {
+    return CreateBackendErrorResponse(kSignatureAuthStatus,
+                                      "Missing s auth parameter");
+  }
+  if (!verification.has_value()) {
+    return CreateBackendErrorResponse(kSignatureAuthStatus,
+                                      "Missing v auth parameter");
+  }
+  uint8_t config_public_key[ED25519_PUBLIC_KEY_LEN];
+  if (!masque_server_backend_->GetSignatureAuthKeyForId(*key_id,
+                                                        config_public_key)) {
+    return CreateBackendErrorResponse(kSignatureAuthStatus,
+                                      "Unexpected key id");
+  }
+  if (*header_public_key !=
+      std::string(reinterpret_cast<const char*>(config_public_key),
+                  sizeof(config_public_key))) {
+    return CreateBackendErrorResponse(kSignatureAuthStatus,
+                                      "Unexpected public key in header");
+  }
+  std::string realm = "";
+  QuicUrl url(authority, scheme);
+  std::optional<std::string> key_exporter_context = ComputeSignatureAuthContext(
+      kEd25519SignatureScheme, *key_id, *header_public_key, scheme, url.host(),
+      url.port(), realm);
+  if (!key_exporter_context.has_value()) {
+    return CreateBackendErrorResponse(
+        "500", "Failed to generate key exporter context");
+  }
+  std::string key_exporter_output;
+  if (!GetMutableCryptoStream()->ExportKeyingMaterial(
+          kSignatureAuthLabel, *key_exporter_context,
+          kSignatureAuthExporterSize, &key_exporter_output)) {
+    return CreateBackendErrorResponse("500", "Key exporter failed");
+  }
+  QUICHE_CHECK_EQ(key_exporter_output.size(), kSignatureAuthExporterSize);
+  std::string signature_input =
+      key_exporter_output.substr(0, kSignatureAuthSignatureInputSize);
+  std::string expected_verification = key_exporter_output.substr(
+      kSignatureAuthSignatureInputSize, kSignatureAuthVerificationSize);
+  if (verification != expected_verification) {
+    return CreateBackendErrorResponse(kSignatureAuthStatus,
+                                      "Unexpected verification");
+  }
+  std::string data_covered_by_signature =
+      SignatureAuthDataCoveredBySignature(signature_input);
+  if (*signature_scheme != kEd25519SignatureScheme) {
+    return CreateBackendErrorResponse(kSignatureAuthStatus,
+                                      "Unexpected signature scheme");
+  }
+  if (proof->size() != ED25519_SIGNATURE_LEN) {
+    return CreateBackendErrorResponse(kSignatureAuthStatus,
+                                      "Unexpected proof length");
+  }
+  if (ED25519_verify(
+          reinterpret_cast<const uint8_t*>(data_covered_by_signature.data()),
+          data_covered_by_signature.size(),
+          reinterpret_cast<const uint8_t*>(proof->data()),
+          config_public_key) != 1) {
+    return CreateBackendErrorResponse(kSignatureAuthStatus,
+                                      "Signature failed to validate");
+  }
+  QUIC_LOG(INFO) << "Successfully validated signature auth for stream ID "
+                 << request_handler->stream_id();
+  return nullptr;
+}
+
 std::unique_ptr<QuicBackendResponse> MasqueServerSession::HandleMasqueRequest(
     const spdy::Http2HeaderBlock& request_headers,
     QuicSimpleServerBackend::RequestHandler* request_handler) {
@@ -203,6 +402,13 @@
                      << "\"";
     return CreateBackendErrorResponse("400", "Bad protocol");
   }
+
+  auto signature_auth_reply = MaybeCheckSignatureAuth(
+      request_headers, authority, scheme, request_handler);
+  if (signature_auth_reply) {
+    return signature_auth_reply;
+  }
+
   if (protocol == "connect-ip") {
     QuicSpdyStream* stream = static_cast<QuicSpdyStream*>(
         GetActiveStream(request_handler->stream_id()));
diff --git a/quiche/quic/masque/masque_server_session.h b/quiche/quic/masque/masque_server_session.h
index 0226f9a..bd87f50 100644
--- a/quiche/quic/masque/masque_server_session.h
+++ b/quiche/quic/masque/masque_server_session.h
@@ -61,6 +61,10 @@
                                   QuicSocketEventMask events);
   bool HandleConnectEthernetSocketEvent(QuicUdpSocketFd fd,
                                         QuicSocketEventMask events);
+  std::unique_ptr<QuicBackendResponse> MaybeCheckSignatureAuth(
+      const spdy::Http2HeaderBlock& request_headers,
+      absl::string_view authority, absl::string_view scheme,
+      QuicSimpleServerBackend::RequestHandler* request_handler);
 
   // State that the MasqueServerSession keeps for each CONNECT-UDP request.
   class QUIC_NO_EXPORT ConnectUdpServerState
diff --git a/quiche/quic/masque/masque_utils.cc b/quiche/quic/masque/masque_utils.cc
index fc35005..322920b 100644
--- a/quiche/quic/masque/masque_utils.cc
+++ b/quiche/quic/masque/masque_utils.cc
@@ -4,6 +4,11 @@
 
 #include "quiche/quic/masque/masque_utils.h"
 
+#include <string>
+
+#include "absl/strings/str_cat.h"
+#include "absl/strings/string_view.h"
+
 #if defined(__linux__)
 #include <fcntl.h>
 #include <linux/if.h>
@@ -204,4 +209,38 @@
 }
 #endif  // defined(__linux__)
 
+std::string ComputeSignatureAuthContext(uint16_t signature_scheme,
+                                        absl::string_view key_id,
+                                        absl::string_view public_key,
+                                        absl::string_view scheme,
+                                        absl::string_view host, uint16_t port,
+                                        absl::string_view realm) {
+  std::string key_exporter_output;
+  std::string key_exporter_context;
+  key_exporter_context.resize(
+      sizeof(signature_scheme) + QuicDataWriter::GetVarInt62Len(key_id.size()) +
+      key_id.size() + QuicDataWriter::GetVarInt62Len(public_key.size()) +
+      public_key.size() + QuicDataWriter::GetVarInt62Len(scheme.size()) +
+      scheme.size() + QuicDataWriter::GetVarInt62Len(host.size()) +
+      host.size() + sizeof(port) +
+      QuicDataWriter::GetVarInt62Len(realm.size()) + realm.size());
+  QuicDataWriter writer(key_exporter_context.size(),
+                        key_exporter_context.data());
+  if (!writer.WriteUInt16(signature_scheme) ||
+      !writer.WriteStringPieceVarInt62(key_id) ||
+      !writer.WriteStringPieceVarInt62(public_key) ||
+      !writer.WriteStringPieceVarInt62(scheme) ||
+      !writer.WriteStringPieceVarInt62(host) || !writer.WriteUInt16(port) ||
+      !writer.WriteStringPieceVarInt62(realm) || writer.remaining() != 0) {
+    QUIC_LOG(FATAL) << "ComputeSignatureAuthContext failed";
+  }
+  return key_exporter_output;
+}
+
+std::string SignatureAuthDataCoveredBySignature(
+    absl::string_view signature_input) {
+  return absl::StrCat(std::string(64, 0x20), "HTTP Signature Authentication",
+                      "\0", signature_input);
+}
+
 }  // namespace quic
diff --git a/quiche/quic/masque/masque_utils.h b/quiche/quic/masque/masque_utils.h
index 68cce05..c55da9e 100644
--- a/quiche/quic/masque/masque_utils.h
+++ b/quiche/quic/masque/masque_utils.h
@@ -5,9 +5,18 @@
 #ifndef QUICHE_QUIC_MASQUE_MASQUE_UTILS_H_
 #define QUICHE_QUIC_MASQUE_MASQUE_UTILS_H_
 
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+#include <ostream>
+#include <string>
+
+#include "absl/strings/string_view.h"
 #include "quiche/quic/core/quic_config.h"
 #include "quiche/quic/core/quic_types.h"
 #include "quiche/quic/core/quic_versions.h"
+#include "quiche/quic/platform/api/quic_export.h"
+#include "quiche/quic/platform/api/quic_ip_address.h"
 
 namespace quic {
 
@@ -53,6 +62,25 @@
 // Create a TAP interface. Requires root.
 int CreateTapInterface();
 
+inline constexpr size_t kSignatureAuthSignatureInputSize = 32;
+inline constexpr size_t kSignatureAuthVerificationSize = 16;
+inline constexpr size_t kSignatureAuthExporterSize =
+    kSignatureAuthSignatureInputSize + kSignatureAuthVerificationSize;
+inline constexpr uint16_t kEd25519SignatureScheme = 0x0807;
+inline constexpr absl::string_view kSignatureAuthLabel =
+    "EXPORTER-HTTP-Signature-Authentication";
+
+// Returns the signature auth TLS key exporter context.
+QUIC_NO_EXPORT std::string ComputeSignatureAuthContext(
+    uint16_t signature_scheme, absl::string_view key_id,
+    absl::string_view public_key, absl::string_view scheme,
+    absl::string_view host, uint16_t port, absl::string_view realm);
+
+// Returns the data covered by signature auth signatures, computed by
+// concatenating a fixed prefix from the specification and the signature input.
+QUIC_NO_EXPORT std::string SignatureAuthDataCoveredBySignature(
+    absl::string_view signature_input);
+
 }  // namespace quic
 
 #endif  // QUICHE_QUIC_MASQUE_MASQUE_UTILS_H_