From b36b7abb40f04f265c0453a2f4beb466ed462976 Mon Sep 17 00:00:00 2001 From: Timur Pocheptsov Date: Mon, 27 Jan 2020 14:11:08 +0100 Subject: Implement/fix session resumption with TLS 1.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The session we cache at the end of a handshake is non-resumable in TLS 1.3, since NewSessionTicket message appears quite some time after the handshake was complete. OpenSSL has a callback where we can finally obtain a resumable session and inform an application about session ticket updated by emitting a signal. Truism: OpenSSL-only. [ChangeLog][QtNetwork] A new signal introduced to report when a valid session ticket received (TLS 1.3) Fixes: QTBUG-81591 Change-Id: I4d22fad5cc082e431577e20ddbda2835e864b511 Reviewed-by: MÃ¥rten Nordheim Reviewed-by: Timur Pocheptsov --- src/network/ssl/qsslconfiguration.cpp | 6 +-- src/network/ssl/qsslcontext_openssl.cpp | 21 ++++++-- src/network/ssl/qsslsocket.cpp | 16 ++++++ src/network/ssl/qsslsocket.h | 1 + src/network/ssl/qsslsocket_openssl.cpp | 70 ++++++++++++++++++++++++++ src/network/ssl/qsslsocket_openssl_p.h | 1 + src/network/ssl/qsslsocket_openssl_symbols.cpp | 4 ++ src/network/ssl/qsslsocket_openssl_symbols_p.h | 18 ++++++- 8 files changed, 130 insertions(+), 7 deletions(-) diff --git a/src/network/ssl/qsslconfiguration.cpp b/src/network/ssl/qsslconfiguration.cpp index d0674042b8..f5ce02807f 100644 --- a/src/network/ssl/qsslconfiguration.cpp +++ b/src/network/ssl/qsslconfiguration.cpp @@ -782,7 +782,7 @@ bool QSslConfiguration::testSslOption(QSsl::SslOption option) const knowledge of the session allows for eavesdropping on data encrypted with the session parameters. - \sa setSessionTicket(), QSsl::SslOptionDisableSessionPersistence, setSslOption() + \sa setSessionTicket(), QSsl::SslOptionDisableSessionPersistence, setSslOption(), QSslSocket::newSessionTicketReceived() */ QByteArray QSslConfiguration::sessionTicket() const { @@ -797,7 +797,7 @@ QByteArray QSslConfiguration::sessionTicket() const for this to work, and \a sessionTicket must be in ASN.1 format as returned by sessionTicket(). - \sa sessionTicket(), QSsl::SslOptionDisableSessionPersistence, setSslOption() + \sa sessionTicket(), QSsl::SslOptionDisableSessionPersistence, setSslOption(), QSslSocket::newSessionTicketReceived() */ void QSslConfiguration::setSessionTicket(const QByteArray &sessionTicket) { @@ -815,7 +815,7 @@ void QSslConfiguration::setSessionTicket(const QByteArray &sessionTicket) QSsl::SslOptionDisableSessionPersistence was not turned off, this function returns -1. - \sa sessionTicket(), QSsl::SslOptionDisableSessionPersistence, setSslOption() + \sa sessionTicket(), QSsl::SslOptionDisableSessionPersistence, setSslOption(), QSslSocket::newSessionTicketReceived() */ int QSslConfiguration::sessionTicketLifeTimeHint() const { diff --git a/src/network/ssl/qsslcontext_openssl.cpp b/src/network/ssl/qsslcontext_openssl.cpp index 562aa4f518..0aa8a4f438 100644 --- a/src/network/ssl/qsslcontext_openssl.cpp +++ b/src/network/ssl/qsslcontext_openssl.cpp @@ -70,6 +70,10 @@ extern "C" int q_verify_cookie_callback(SSL *ssl, const unsigned char *cookie, } #endif // dtls +#ifdef TLS1_3_VERSION +extern "C" int q_ssl_sess_set_new_cb(SSL *context, SSL_SESSION *session); +#endif // TLS1_3_VERSION + // Defined in qsslsocket.cpp QList q_getDefaultDtlsCiphers(); @@ -168,8 +172,8 @@ SSL* QSslContext::createSsl() if (!session && !sessionASN1().isEmpty() && !sslConfiguration.testSslOption(QSsl::SslOptionDisableSessionPersistence)) { const unsigned char *data = reinterpret_cast(m_sessionASN1.constData()); - session = q_d2i_SSL_SESSION( - nullptr, &data, m_sessionASN1.size()); // refcount is 1 already, set by function above + session = q_d2i_SSL_SESSION(nullptr, &data, m_sessionASN1.size()); + // 'session' has refcount 1 already, set by the function above } if (session) { @@ -585,7 +589,8 @@ init_context: } } - // Initialize peer verification. + // Initialize peer verification, different callbacks, TLS/DTLS verification first + // (note, all these set_some_callback do not have return value): if (sslContext->sslConfiguration.peerVerifyMode() == QSslSocket::VerifyNone) { q_SSL_CTX_set_verify(sslContext->ctx, SSL_VERIFY_NONE, nullptr); } else { @@ -596,7 +601,17 @@ init_context: q_X509Callback); } +#ifdef TLS1_3_VERSION + // NewSessionTicket callback: + if (mode == QSslSocket::SslClientMode && !isDtls) { + q_SSL_CTX_sess_set_new_cb(sslContext->ctx, q_ssl_sess_set_new_cb); + q_SSL_CTX_set_session_cache_mode(sslContext->ctx, SSL_SESS_CACHE_CLIENT); + } + +#endif // TLS1_3_VERSION + #if QT_CONFIG(dtls) + // DTLS cookies: if (mode == QSslSocket::SslServerMode && isDtls && configuration.dtlsCookieVerificationEnabled()) { q_SSL_CTX_set_cookie_generate_cb(sslContext->ctx, dtlscallbacks::q_generate_cookie_callback); q_SSL_CTX_set_cookie_verify_cb(sslContext->ctx, dtlscallbacks::q_verify_cookie_callback); diff --git a/src/network/ssl/qsslsocket.cpp b/src/network/ssl/qsslsocket.cpp index 19be48a656..8fa9f914a2 100644 --- a/src/network/ssl/qsslsocket.cpp +++ b/src/network/ssl/qsslsocket.cpp @@ -322,6 +322,22 @@ \sa QSslPreSharedKeyAuthenticator */ +/*! + \fn void QSslSocket::newSessionTicketReceived() + \since 5.15 + + If TLS 1.3 protocol was negotiated during a handshake, QSslSocket + emits this signal after receiving NewSessionTicket message. Session + and session ticket's lifetime hint are updated in the socket's + configuration. The session can be used for session resumption (and + a shortened handshake) in future TLS connections. + + \note This functionality enabled only with OpenSSL backend and requires + OpenSSL v 1.1.1 or above. + + \sa QSslSocket::sslConfiguration(), QSslConfiguration::sessionTicket(), QSslConfiguration::sessionTicketLifeTimeHint() +*/ + #include "qssl_p.h" #include "qsslsocket.h" #include "qsslcipher.h" diff --git a/src/network/ssl/qsslsocket.h b/src/network/ssl/qsslsocket.h index 2c3b876c49..298e7aa6c8 100644 --- a/src/network/ssl/qsslsocket.h +++ b/src/network/ssl/qsslsocket.h @@ -217,6 +217,7 @@ Q_SIGNALS: void modeChanged(QSslSocket::SslMode newMode); void encryptedBytesWritten(qint64 totalBytes); void preSharedKeyAuthenticationRequired(QSslPreSharedKeyAuthenticator *authenticator); + void newSessionTicketReceived(); protected: qint64 readData(char *data, qint64 maxlen) override; diff --git a/src/network/ssl/qsslsocket_openssl.cpp b/src/network/ssl/qsslsocket_openssl.cpp index 41f933b299..4be27affca 100644 --- a/src/network/ssl/qsslsocket_openssl.cpp +++ b/src/network/ssl/qsslsocket_openssl.cpp @@ -183,6 +183,22 @@ static int q_ssl_psk_use_session_callback(SSL *ssl, const EVP_MD *md, const unsi return 1; // need to return 1 or else "the connection setup fails." } + +int q_ssl_sess_set_new_cb(SSL *ssl, SSL_SESSION *session) +{ + if (!ssl) { + qCWarning(lcSsl, "Invalid SSL (nullptr)"); + return 0; + } + if (!session) { + qCWarning(lcSsl, "Invalid SSL_SESSION (nullptr)"); + return 0; + } + + auto socketPrivate = static_cast(q_SSL_get_ex_data(ssl, + QSslSocketBackendPrivate::s_indexForSSLExtraData)); + return socketPrivate->handleNewSessionTicket(ssl); +} #endif // TLS1_3_VERSION #endif // !OPENSSL_NO_PSK @@ -1392,6 +1408,60 @@ void QSslSocketBackendPrivate::storePeerCertificates() } } +int QSslSocketBackendPrivate::handleNewSessionTicket(SSL *connection) +{ + // If we return 1, this means we own the session, but we don't. + // 0 would tell OpenSSL to deref (but they still have it in the + // internal cache). + Q_Q(QSslSocket); + + Q_ASSERT(connection); + + if (q->sslConfiguration().testSslOption(QSsl::SslOptionDisableSessionPersistence)) { + // We silently ignore, do nothing, remove from cache. + return 0; + } + + SSL_SESSION *currentSession = q_SSL_get_session(connection); + if (!currentSession) { + qCWarning(lcSsl, + "New session ticket callback, the session is invalid (nullptr)"); + return 0; + } + + if (q_SSL_version(connection) < 0x304) { + // We only rely on this mechanics with TLS >= 1.3 + return 0; + } + +#ifdef TLS1_3_VERSION + if (!q_SSL_SESSION_is_resumable(currentSession)) { + qCDebug(lcSsl, "New session ticket, but the session is non-resumable"); + return 0; + } +#endif // TLS1_3_VERSION + + const int sessionSize = q_i2d_SSL_SESSION(currentSession, nullptr); + if (sessionSize <= 0) { + qCWarning(lcSsl, "could not store persistent version of SSL session"); + return 0; + } + + // We have somewhat perverse naming, it's not a ticket, it's a session. + QByteArray sessionTicket(sessionSize, 0); + auto data = reinterpret_cast(sessionTicket.data()); + if (!q_i2d_SSL_SESSION(currentSession, &data)) { + qCWarning(lcSsl, "could not store persistent version of SSL session"); + return 0; + } + + configuration.sslSession = sessionTicket; + configuration.sslSessionTicketLifeTimeHint = int(q_SSL_SESSION_get_ticket_lifetime_hint(currentSession)); + + emit q->newSessionTicketReceived(); + return 0; +} + bool QSslSocketBackendPrivate::checkSslErrors() { Q_Q(QSslSocket); diff --git a/src/network/ssl/qsslsocket_openssl_p.h b/src/network/ssl/qsslsocket_openssl_p.h index 0370a7d2ac..47ccf06e71 100644 --- a/src/network/ssl/qsslsocket_openssl_p.h +++ b/src/network/ssl/qsslsocket_openssl_p.h @@ -146,6 +146,7 @@ public: void continueHandshake() override; bool checkSslErrors(); void storePeerCertificates(); + int handleNewSessionTicket(SSL *context); unsigned int tlsPskClientCallback(const char *hint, char *identity, unsigned int max_identity_len, unsigned char *psk, unsigned int max_psk_len); unsigned int tlsPskServerCallback(const char *identity, unsigned char *psk, unsigned int max_psk_len); #ifdef Q_OS_WIN diff --git a/src/network/ssl/qsslsocket_openssl_symbols.cpp b/src/network/ssl/qsslsocket_openssl_symbols.cpp index 3504924888..71a268ae6e 100644 --- a/src/network/ssl/qsslsocket_openssl_symbols.cpp +++ b/src/network/ssl/qsslsocket_openssl_symbols.cpp @@ -159,6 +159,8 @@ DEFINEFUNC2(unsigned long, SSL_CTX_set_options, SSL_CTX *ctx, ctx, unsigned long #ifdef TLS1_3_VERSION DEFINEFUNC2(int, SSL_CTX_set_ciphersuites, SSL_CTX *ctx, ctx, const char *str, str, return 0, return) DEFINEFUNC2(void, SSL_set_psk_use_session_callback, SSL *ssl, ssl, q_SSL_psk_use_session_cb_func_t callback, callback, return, DUMMYARG) +DEFINEFUNC2(void, SSL_CTX_sess_set_new_cb, SSL_CTX *ctx, ctx, NewSessionCallback cb, cb, return, return) +DEFINEFUNC(int, SSL_SESSION_is_resumable, const SSL_SESSION *s, s, return 0, return) #endif DEFINEFUNC3(size_t, SSL_get_client_random, SSL *a, a, unsigned char *out, out, size_t outlen, outlen, return 0, return) DEFINEFUNC3(size_t, SSL_SESSION_get_master_key, const SSL_SESSION *ses, ses, unsigned char *out, out, size_t outlen, outlen, return 0, return) @@ -843,6 +845,8 @@ bool q_resolveOpenSslSymbols() #ifdef TLS1_3_VERSION RESOLVEFUNC(SSL_CTX_set_ciphersuites) RESOLVEFUNC(SSL_set_psk_use_session_callback) + RESOLVEFUNC(SSL_CTX_sess_set_new_cb) + RESOLVEFUNC(SSL_SESSION_is_resumable) #endif // TLS 1.3 or OpenSSL > 1.1.1 RESOLVEFUNC(SSL_get_client_random) diff --git a/src/network/ssl/qsslsocket_openssl_symbols_p.h b/src/network/ssl/qsslsocket_openssl_symbols_p.h index baf1a43113..f35e0ba22b 100644 --- a/src/network/ssl/qsslsocket_openssl_symbols_p.h +++ b/src/network/ssl/qsslsocket_openssl_symbols_p.h @@ -224,7 +224,6 @@ QT_BEGIN_NAMESPACE // To reduce the amount of the change, I'm directly copying and pasting the // content of the header here. Later, can be better sorted/split into groups, // depending on the functionality. -//#include "qsslsocket_openssl11_symbols_p.h" const unsigned char * q_ASN1_STRING_get0_data(const ASN1_STRING *x); @@ -287,6 +286,23 @@ unsigned long q_SSL_set_options(SSL *s, unsigned long op); #ifdef TLS1_3_VERSION int q_SSL_CTX_set_ciphersuites(SSL_CTX *ctx, const char *str); + +// The functions below do not really have to be ifdefed like this, but for now +// they only used in TLS 1.3 handshake (and probably future versions). +// Plus, 'is resumalbe' is OpenSSL 1.1.1-only (and again we need it for +// TLS 1.3-specific session management). + +extern "C" +{ +using NewSessionCallback = int (*)(SSL *, SSL_SESSION *); +} + +void q_SSL_CTX_sess_set_new_cb(SSL_CTX *ctx, NewSessionCallback cb); +int q_SSL_SESSION_is_resumable(const SSL_SESSION *s); + +#define q_SSL_CTX_set_session_cache_mode(ctx,m) \ + q_SSL_CTX_ctrl(ctx,SSL_CTRL_SET_SESS_CACHE_MODE,m,NULL) + #endif #if QT_CONFIG(dtls) -- cgit v1.2.3