From eec67f04490a477d69891c8b05a94bea05e5efbf Mon Sep 17 00:00:00 2001 From: Christos Tsantilas Date: Fri, 12 Jun 2020 23:32:16 +0000 Subject: [PATCH] SslBump: Support parsing GREASEd (and future) TLS handshakes (#663) A peeking or staring Squid aborted TLS connections containing a GREASE version in the supported_versions handshake extension (e.g., 0x3a3a). Here is a sample cache.log error (debug_options ALL,1 83,2): ``` parseTlsHandshake: ... check failed: vMajor == 3 exception location: Handshake.cc(119) ParseProtocolVersion ``` The same problem would apply to some other "unsupported" (and currently non-existent) TLS versions (e.g., 0x0400). The growing popularity of GREASE values exposed the bug (just like RFC 8710 was designed to do). Squid now ignores all unsupported-by-Squid TLS versions in handshake extensions. Squid still requires a supported TLS version for framing-related handshake fields -- no changes there. It is difficult to define "supported" in this context precisely because a peeking Squid only observes the TLS handshake. Our handshake parser may report the following versions: SSL v2.0, SSL v3.0, and TLS v1.x (with x >= 0). This logic allows us to safely parse the handshake (no framing errors) and continue to make version-based decisions like disabling OpenSSL TLS v1.3 support when the agents are negotiating TLS v1.3+ (master cd29a42). Also, access logs benefit from this "support" going beyond TLS v1.2 even though Squid cannot bump most TLS v1.3+ handshakes. Squid was and still is compliant with the "MUST NOT treat GREASE values differently from any unknown value" requirement of RFC 8710. Also added source code comments to mark other places where unsupported (for some definition of "support") TLS values, including GREASE values may appear. Folks studying debugging logs (where GREASE values may appear) will probably either recognize their easy-to-remember 0x?A?A pattern or not really know/care about RFC 8710 and its special values. This is a Measurement Factory project. --- src/security/Handshake.cc | 65 +++++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/src/security/Handshake.cc b/src/security/Handshake.cc index 3db9cfec73..f045b82c32 100644 --- a/src/security/Handshake.cc +++ b/src/security/Handshake.cc @@ -9,6 +9,7 @@ /* DEBUG: section 83 SSL-Bump Server/Peer negotiation */ #include "squid.h" +#include "sbuf/Stream.h" #include "security/Handshake.h" #if USE_OPENSSL #include "ssl/support.h" @@ -104,25 +105,52 @@ public: typedef std::unordered_set Extensions; static Extensions SupportedExtensions(); -} // namespace Security - /// parse TLS ProtocolVersion (uint16) and convert it to AnyP::ProtocolVersion +/// \retval PROTO_NONE for unsupported values (in relaxed mode) static AnyP::ProtocolVersion -ParseProtocolVersion(Parser::BinaryTokenizer &tk, const char *contextLabel = ".version") +ParseProtocolVersionBase(Parser::BinaryTokenizer &tk, const char *contextLabel, const bool beStrict) { Parser::BinaryTokenizerContext context(tk, contextLabel); uint8_t vMajor = tk.uint8(".major"); uint8_t vMinor = tk.uint8(".minor"); + if (vMajor == 0 && vMinor == 2) return AnyP::ProtocolVersion(AnyP::PROTO_SSL, 2, 0); - Must(vMajor == 3); - if (vMinor == 0) - return AnyP::ProtocolVersion(AnyP::PROTO_SSL, 3, 0); + if (vMajor == 3) { + if (vMinor == 0) + return AnyP::ProtocolVersion(AnyP::PROTO_SSL, 3, 0); + return AnyP::ProtocolVersion(AnyP::PROTO_TLS, 1, (vMinor - 1)); + } + + /* handle unsupported versions */ + + const uint16_t vRaw = (vMajor << 8) | vMinor; + debugs(83, 7, "unsupported: " << asHex(vRaw)); + if (beStrict) + throw TextException(ToSBuf("unsupported TLS version: ", asHex(vRaw)), Here()); + // else hide unsupported version details from the caller behind PROTO_NONE + return AnyP::ProtocolVersion(); +} + +/// parse a framing-related TLS ProtocolVersion +/// \returns a supported SSL or TLS Anyp::ProtocolVersion, never PROTO_NONE +static AnyP::ProtocolVersion +ParseProtocolVersion(Parser::BinaryTokenizer &tk) +{ + return ParseProtocolVersionBase(tk, ".version", true); +} - return AnyP::ProtocolVersion(AnyP::PROTO_TLS, 1, (vMinor - 1)); +/// parse a framing-unrelated TLS ProtocolVersion +/// \retval PROTO_NONE for unsupported values +static AnyP::ProtocolVersion +ParseOptionalProtocolVersion(Parser::BinaryTokenizer &tk, const char *contextLabel) +{ + return ParseProtocolVersionBase(tk, contextLabel, false); } +} // namespace Security + Security::TLSPlaintext::TLSPlaintext(Parser::BinaryTokenizer &tk) { Parser::BinaryTokenizerContext context(tk, "TLSPlaintext"); @@ -431,6 +459,8 @@ Security::HandshakeParser::parseExtensions(const SBuf &raw) break; case 16: { // Application-Layer Protocol Negotiation Extension, RFC 7301 Parser::BinaryTokenizer tkAPN(extension.data); + // Store the entire protocol list, including unsupported-by-Squid + // values (if any). We have to use all when peeking at the server. details->tlsAppLayerProtoNeg = tkAPN.pstring16("APN"); break; } @@ -441,8 +471,9 @@ Security::HandshakeParser::parseExtensions(const SBuf &raw) case 43: // supported_versions extension; RFC 8446 parseSupportedVersionsExtension(extension.data); break; - case 13172: // Next Protocol Negotiation Extension (expired draft?) default: + // other extensions, including those that Squid does not support, do + // not require special handling here, but see unsupportedExtensions break; } } @@ -455,7 +486,7 @@ Security::HandshakeParser::parseCiphers(const SBuf &raw) Parser::BinaryTokenizer tk(raw); while (!tk.atEnd()) { const uint16_t cipher = tk.uint16("cipher"); - details->ciphers.insert(cipher); + details->ciphers.insert(cipher); // including Squid-unsupported ones } } @@ -473,7 +504,7 @@ Security::HandshakeParser::parseV23Ciphers(const SBuf &raw) const uint8_t prefix = tk.uint8("prefix"); const uint16_t cipher = tk.uint16("cipher"); if (prefix == 0) - details->ciphers.insert(cipher); + details->ciphers.insert(cipher); // including Squid-unsupported ones } } @@ -486,6 +517,7 @@ Security::HandshakeParser::parseServerHelloHandshakeMessage(const SBuf &raw) details->tlsSupportedVersion = ParseProtocolVersion(tk); tk.skip(HelloRandomSize, ".random"); details->sessionId = tk.pstring8(".session_id"); + // cipherSuite may be unsupported by a peeking Squid details->ciphers.insert(tk.uint16(".cipher_suite")); details->compressionSupported = tk.uint8(".compression_method") != 0; // not null if (!tk.atEnd()) // extensions present @@ -554,12 +586,15 @@ Security::HandshakeParser::parseSupportedVersionsExtension(const SBuf &extension Parser::BinaryTokenizer tkList(extensionData); Parser::BinaryTokenizer tkVersions(tkList.pstring8("SupportedVersions")); while (!tkVersions.atEnd()) { - const auto version = ParseProtocolVersion(tkVersions, "supported_version"); + const auto version = ParseOptionalProtocolVersion(tkVersions, "supported_version"); + // ignore values unsupported by Squid,represented by a falsy version + if (!version) + continue; if (!supportedVersionMax || TlsVersionEarlierThan(supportedVersionMax, version)) supportedVersionMax = version; } - // ignore empty supported_versions + // ignore empty and ignored-values-only supported_versions if (!supportedVersionMax) return; @@ -569,7 +604,11 @@ Security::HandshakeParser::parseSupportedVersionsExtension(const SBuf &extension } else { assert(messageSource == fromServer); Parser::BinaryTokenizer tkVersion(extensionData); - const auto version = ParseProtocolVersion(tkVersion, "selected_version"); + const auto version = ParseOptionalProtocolVersion(tkVersion, "selected_version"); + // Ignore values unsupported by Squid. There should not be any until we + // start seeing TLS v2+, but they do not affect TLS framing anyway. + if (!version) + return; // RFC 8446 Section 4.2.1: // A server which negotiates a version of TLS prior to TLS 1.3 [...] // MUST NOT send the "supported_versions" extension. -- 2.47.2