unsigned int major; ///< major version number
unsigned int minor; ///< minor version number
+ /// whether the version is "known" (e.g., has been parsed or explicitly set)
+ explicit operator bool() const { return protocol != PROTO_NONE; }
+
bool operator==(const ProtocolVersion& that) const {
if (this->protocol != that.protocol)
return false;
bodyParser(nullptr),
#if USE_OPENSSL
sslBumpMode(Ssl::bumpEnd),
+ tlsParser(Security::HandshakeParser::fromClient),
#endif
needProxyProtocolHeader_(false),
#if USE_OPENSSL
} // namespace Security
-/// Convenience helper: We parse ProtocolVersion but store "int".
+/// parse TLS ProtocolVersion (uint16) and convert it to AnyP::ProtocolVersion
static AnyP::ProtocolVersion
-ParseProtocolVersion(Parser::BinaryTokenizer &tk)
+ParseProtocolVersion(Parser::BinaryTokenizer &tk, const char *contextLabel = ".version")
{
- Parser::BinaryTokenizerContext context(tk, ".version");
+ Parser::BinaryTokenizerContext context(tk, contextLabel);
uint8_t vMajor = tk.uint8(".major");
uint8_t vMinor = tk.uint8(".minor");
if (vMajor == 0 && vMinor == 2)
/* Security::HandshakeParser */
-Security::HandshakeParser::HandshakeParser():
+Security::HandshakeParser::HandshakeParser(const MessageSource source):
details(new TlsDetails),
state(atHelloNone),
resumingSession(false),
+ messageSource(source),
currentContentType(0),
done(nullptr),
expectingModernRecords(false)
{
Must(currentContentType == ContentType::ctChangeCipherSpec);
// We are currently ignoring Change Cipher Spec Protocol messages.
- skipMessage("ChangeCipherCpec msg [fragment]");
+ skipMessage("ChangeCipherSpec msg [fragment]");
+
+ // In TLS v1.2 and earlier, ChangeCipherSpec is sent after Hello (when
+ // tlsSupportedVersion is already known) and indicates session resumption.
+ // In later TLS versions, ChangeCipherSpec may be sent before and after
+ // Hello, but it is unused for session resumption and should be ignored.
+ if (!details->tlsSupportedVersion || Tls1p3orLater(details->tlsSupportedVersion))
+ return;
- // Everything after the ChangeCipherCpec message may be encrypted.
- // Continuing parsing is pointless. Stop here.
resumingSession = true;
- done = "ChangeCipherCpec";
+
+ // Everything after the ChangeCipherSpec message may be encrypted. Stop.
+ done = "ChangeCipherSpec in v1.2-";
}
void
switch (message.msg_type) {
case HandshakeType::hskClientHello:
Must(state < atHelloReceived);
+ Must(messageSource == fromClient);
Security::HandshakeParser::parseClientHelloHandshakeMessage(message.msg_body);
state = atHelloReceived;
done = "ClientHello";
return;
case HandshakeType::hskServerHello:
Must(state < atHelloReceived);
+ Must(messageSource == fromServer);
parseServerHelloHandshakeMessage(message.msg_body);
state = atHelloReceived;
+ // for TLSv1.3 and later, anything after the server Hello is encrypted
+ if (Tls1p3orLater(details->tlsSupportedVersion))
+ done = "ServerHello in v1.3+";
return;
case HandshakeType::hskCertificate:
Must(state < atCertificatesReceived);
case 35: // SessionTicket TLS Extension; RFC 5077
details->tlsTicketsExtension = true;
details->hasTlsTicket = !extension.data.isEmpty();
+ break;
+ case 43: // supported_versions extension; RFC 8446
+ parseSupportedVersionsExtension(extension.data);
+ break;
case 13172: // Next Protocol Negotiation Extension (expired draft?)
default:
break;
return SBuf(); // SNI extension lacks host_name
}
+/// RFC 8446 Section 4.2.1: SupportedVersions extension
+void
+Security::HandshakeParser::parseSupportedVersionsExtension(const SBuf &extensionData) const
+{
+ // Upon detecting a quoted RFC MUST violation, this parser immediately
+ // returns, ignoring the entire extension and resulting in Squid relying on
+ // the legacy_version field value or another (valid) supported_versions
+ // extension. The alternative would be to reject the whole handshake as
+ // invalid. Deployment experience will show which alternative is the best.
+
+ // Please note that several of these MUSTs also imply certain likely
+ // handling of a hypothetical next TLS version (e.g., v1.4).
+
+ // RFC 8446 Section 4.1.2:
+ // In TLS 1.3, the client indicates its version preferences in the
+ // "supported_versions" extension (Section 4.2.1) and the legacy_version
+ // field MUST be set to 0x0303, which is the version number for TLS 1.2.
+ //
+ // RFC 8446 Section 4.2.1:
+ // A server which negotiates TLS 1.3 MUST respond by sending a
+ // "supported_versions" extension containing the selected version value
+ // (0x0304). It MUST set the ServerHello.legacy_version field to 0x0303
+ // (TLS 1.2).
+ //
+ // Ignore supported_versions senders violating legacy_version MUSTs above:
+ if (details->tlsSupportedVersion != AnyP::ProtocolVersion(AnyP::PROTO_TLS, 1, 2))
+ return;
+
+ AnyP::ProtocolVersion supportedVersionMax;
+ if (messageSource == fromClient) {
+ Parser::BinaryTokenizer tkList(extensionData);
+ Parser::BinaryTokenizer tkVersions(tkList.pstring8("SupportedVersions"));
+ while (!tkVersions.atEnd()) {
+ const auto version = ParseProtocolVersion(tkVersions, "supported_version");
+ if (!supportedVersionMax || TlsVersionEarlierThan(supportedVersionMax, version))
+ supportedVersionMax = version;
+ }
+
+ // ignore empty supported_versions
+ if (!supportedVersionMax)
+ return;
+
+ // supportedVersionMax here may be "earlier" than tlsSupportedVersion: A
+ // TLS v1.3 client may try to negotiate a _legacy_ version X with a TLS
+ // v1.3 server by sending supported_versions containing just X.
+ } else {
+ assert(messageSource == fromServer);
+ Parser::BinaryTokenizer tkVersion(extensionData, "selected_version");
+ const auto version = ParseProtocolVersion(tkVersion);
+ // 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.
+ if (Tls1p2orEarlier(version))
+ return;
+ supportedVersionMax = version;
+ }
+
+ // We overwrite Hello-derived legacy_version because the following MUSTs
+ // indicate that it is ignored in the presence of valid supported_versions
+ // as far as the negotiated version is concerned. For simplicity sake, we
+ // may also overwrite previous valid supported_versions extensions (if any).
+ //
+ // RFC 8446 Section 4.2.1:
+ // If this extension is present in the ClientHello, servers MUST NOT use the
+ // ClientHello.legacy_version value for version negotiation and MUST use
+ // only the "supported_versions" extension to determine client preferences.
+ // Servers MUST only select a version of TLS present in that extension
+ debugs(83, 7, "found " << supportedVersionMax);
+ assert(supportedVersionMax);
+ details->tlsSupportedVersion = supportedVersionMax;
+}
+
void
Security::HandshakeParser::skipMessage(const char *description)
{
#if defined(TLSEXT_TYPE_next_proto_neg) // 13172
extensions.insert(TLSEXT_TYPE_next_proto_neg);
#endif
+#if defined(TLSEXT_TYPE_supported_versions) // 43
+ extensions.insert(TLSEXT_TYPE_supported_versions);
+#endif
/*
* OpenSSL does not support these last extensions by default, but those
std::ostream & print(std::ostream &os) const;
AnyP::ProtocolVersion tlsVersion; ///< The TLS hello message version
- AnyP::ProtocolVersion tlsSupportedVersion; ///< The requested/used TLS version
+
+ /// For most compliant TLS v1.3+ agents, this is supported_versions maximum.
+ /// For others agents, this is the legacy_version field.
+ AnyP::ProtocolVersion tlsSupportedVersion;
+
bool compressionSupported; ///< The requested/used compressed method
SBuf serverName; ///< The SNI hostname, if any
bool doHeartBeats;
/// The parsing states
typedef enum {atHelloNone = 0, atHelloStarted, atHelloReceived, atCertificatesReceived, atHelloDoneReceived, atNstReceived, atCcsReceived, atFinishReceived} ParserState;
- HandshakeParser();
+ /// the originator of the TLS handshake being parsed
+ typedef enum { fromClient = 0, fromServer } MessageSource;
+
+ explicit HandshakeParser(MessageSource);
/// Parses the initial sequence of raw bytes sent by the TLS/SSL agent.
/// Returns true upon successful completion (e.g., got HelloDone).
/// Throws on errors.
bool parseHello(const SBuf &data);
- TlsDetails::Pointer details; ///< TLS handshake meta info or nil.
+ TlsDetails::Pointer details; ///< TLS handshake meta info. Never nil.
Security::CertList serverCertificates; ///< parsed certificates chain
bool resumingSession; ///< True if this is a resuming session
+ /// whether we are parsing Server or Client TLS handshake messages
+ MessageSource messageSource;
+
private:
bool isSslv2Record(const SBuf &raw) const;
void parseRecord();
bool parseCompressionMethods(const SBuf &raw);
void parseExtensions(const SBuf &raw);
SBuf parseSniExtension(const SBuf &extensionData) const;
+ void parseSupportedVersionsExtension(const SBuf &extensionData) const;
void parseCiphers(const SBuf &raw);
void parseV23Ciphers(const SBuf &raw);
YesNoNone expectingModernRecords;
};
+/// whether the given protocol belongs to the TLS/SSL group of protocols
+inline bool
+TlsFamilyProtocol(const AnyP::ProtocolVersion &version)
+{
+ return (version.protocol == AnyP::PROTO_TLS || version.protocol == AnyP::PROTO_SSL);
+}
+
+/// whether TLS/SSL protocol `a` precedes TLS/SSL protocol `b`
+inline bool
+TlsVersionEarlierThan(const AnyP::ProtocolVersion &a, const AnyP::ProtocolVersion &b)
+{
+ Must(TlsFamilyProtocol(a));
+ Must(TlsFamilyProtocol(b));
+
+ if (a.protocol == b.protocol)
+ return a < b;
+
+ return a.protocol == AnyP::PROTO_SSL; // implies that b is TLS
+}
+
+/// whether the given TLS/SSL protocol is TLS v1.2 or earlier, including SSL
+inline bool
+Tls1p2orEarlier(const AnyP::ProtocolVersion &p)
+{
+ return TlsVersionEarlierThan(p, AnyP::ProtocolVersion(AnyP::PROTO_TLS, 1, 3));
+}
+
+/// whether the given TLS/SSL protocol is TLS v1.3 or later
+inline bool
+Tls1p3orLater(const AnyP::ProtocolVersion &p)
+{
+ return !Tls1p2orEarlier(p);
+}
+
}
#endif // SQUID_SECURITY_HANDSHAKE_H
const char *
Security::NegotiationHistory::printTlsVersion(AnyP::ProtocolVersion const &v) const
{
- if (v.protocol != AnyP::PROTO_SSL && v.protocol != AnyP::PROTO_TLS)
+ if (!TlsFamilyProtocol(v))
return nullptr;
static char buf[512];
BIO *b = SSL_get_rbio(session.get());
Ssl::ServerBio *srvBio = static_cast<Ssl::ServerBio *>(BIO_get_data(b));
- // In Peek mode, the ClientHello message sent to the server. If the
- // server resuming a previous (spliced) SSL session with the client,
- // then probably we are here because local SSL object does not know
- // anything about the session being resumed.
- //
- if (srvBio->bumpMode() == Ssl::bumpPeek && (resumingSession = srvBio->resumingSession())) {
- // we currently splice all resumed sessions unconditionally
- // if (const bool spliceResumed = true) {
- bypassCertValidator();
- checkForPeekAndSpliceMatched(Ssl::bumpSplice);
- return;
- // } // else fall through to find a matching ssl_bump action (with limited info)
+ if (srvBio->bumpMode() == Ssl::bumpPeek) {
+ auto bypassValidator = false;
+ if (srvBio->encryptedCertificates()) {
+ // it is pointless to peek at encrypted certificates
+ //
+ // we currently splice all sessions with encrypted certificates
+ // if (const auto spliceEncryptedCertificates = true) {
+ bypassValidator = true;
+ // } // else fall through to find a matching ssl_bump action (with limited info)
+ } else if (srvBio->resumingSession()) {
+ // In peek mode, the ClientHello message is forwarded to the server.
+ // If the server is resuming a previous (spliced) SSL session with
+ // the client, then probably we are here because our local SSL
+ // object does not know anything about the session being resumed.
+ //
+ // we currently splice all resumed sessions
+ // if (const auto spliceResumed = true) {
+ bypassValidator = true;
+ // } // else fall through to find a matching ssl_bump action (with limited info)
+ }
+
+ if (bypassValidator) {
+ bypassCertValidator();
+ checkForPeekAndSpliceMatched(Ssl::bumpSplice);
+ return;
+ }
}
// If we are in peek-and-splice mode and still we did not write to
Security::PeerConnector(aServerConn, aCallback, alp, timeout),
clientConn(aClientConn),
splice(false),
- resumingSession(false),
serverCertificateHandled(false)
{
request = aRequest;
Comm::ConnectionPointer clientConn; ///< TCP connection to the client
AsyncCall::Pointer closeHandler; ///< we call this when the connection closed
bool splice; ///< whether we are going to splice or not
- bool resumingSession; ///< whether it is an SSL resuming session connection
bool serverCertificateHandled; ///< whether handleServerCertificate() succeeded
};
parsedHandshake(false),
parseError(false),
bumpMode_(bumpNone),
- rbufConsumePos(0)
+ rbufConsumePos(0),
+ parser_(Security::HandshakeParser::fromServer)
{
}
return parser_.resumingSession;
}
+bool
+Ssl::ServerBio::encryptedCertificates() const
+{
+ return parser_.details->tlsSupportedVersion &&
+ Security::Tls1p3orLater(parser_.details->tlsSupportedVersion);
+}
+
/// initializes BIO table after allocation
static int
squid_bio_create(BIO *bi)
SSL_set_options(ssl, SSL_OP_NO_COMPRESSION);
#endif
+#if defined(SSL_OP_NO_TLSv1_3)
+ // avoid "inappropriate fallback" OpenSSL error messages
+ if (details->tlsSupportedVersion && Security::Tls1p2orEarlier(details->tlsSupportedVersion))
+ SSL_set_options(ssl, SSL_OP_NO_TLSv1_3);
+#endif
+
#if defined(TLSEXT_STATUSTYPE_ocsp)
if (details->tlsStatusRequest)
SSL_set_tlsext_status_type(ssl, TLSEXT_STATUSTYPE_ocsp);
bool resumingSession();
+ /// whether the server encrypts its certificate (e.g., TLS v1.3)
+ /// \retval false the server uses plain certs or its intent is unknown
+ bool encryptedCertificates() const;
+
/// The write hold state
bool holdWrite() const {return holdWrite_;}
/// Enables or disables the write hold state
std::ostream &Security::operator <<(std::ostream &os, const Security::EncryptorAnswer &) STUB_RETVAL(os)
#include "security/Handshake.h"
-Security::HandshakeParser::HandshakeParser() STUB
+Security::HandshakeParser::HandshakeParser(MessageSource) STUB
bool Security::HandshakeParser::parseHello(const SBuf &) STUB_RETVAL(false)
#include "security/KeyData.h"