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(¶m_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(¶m_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_