Implement/fix session resumption with TLS 1.3

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 <marten.nordheim@qt.io>
Reviewed-by: Timur Pocheptsov <timur.pocheptsov@qt.io>
This commit is contained in:
Timur Pocheptsov 2020-01-27 14:11:08 +01:00
parent 33c9a1e0bc
commit b36b7abb40
8 changed files with 130 additions and 7 deletions

View File

@ -782,7 +782,7 @@ bool QSslConfiguration::testSslOption(QSsl::SslOption option) const
knowledge of the session allows for eavesdropping on data knowledge of the session allows for eavesdropping on data
encrypted with the session parameters. encrypted with the session parameters.
\sa setSessionTicket(), QSsl::SslOptionDisableSessionPersistence, setSslOption() \sa setSessionTicket(), QSsl::SslOptionDisableSessionPersistence, setSslOption(), QSslSocket::newSessionTicketReceived()
*/ */
QByteArray QSslConfiguration::sessionTicket() const 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 for this to work, and \a sessionTicket must be in ASN.1 format
as returned by sessionTicket(). as returned by sessionTicket().
\sa sessionTicket(), QSsl::SslOptionDisableSessionPersistence, setSslOption() \sa sessionTicket(), QSsl::SslOptionDisableSessionPersistence, setSslOption(), QSslSocket::newSessionTicketReceived()
*/ */
void QSslConfiguration::setSessionTicket(const QByteArray &sessionTicket) void QSslConfiguration::setSessionTicket(const QByteArray &sessionTicket)
{ {
@ -815,7 +815,7 @@ void QSslConfiguration::setSessionTicket(const QByteArray &sessionTicket)
QSsl::SslOptionDisableSessionPersistence was not turned off, QSsl::SslOptionDisableSessionPersistence was not turned off,
this function returns -1. this function returns -1.
\sa sessionTicket(), QSsl::SslOptionDisableSessionPersistence, setSslOption() \sa sessionTicket(), QSsl::SslOptionDisableSessionPersistence, setSslOption(), QSslSocket::newSessionTicketReceived()
*/ */
int QSslConfiguration::sessionTicketLifeTimeHint() const int QSslConfiguration::sessionTicketLifeTimeHint() const
{ {

View File

@ -70,6 +70,10 @@ extern "C" int q_verify_cookie_callback(SSL *ssl, const unsigned char *cookie,
} }
#endif // dtls #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 // Defined in qsslsocket.cpp
QList<QSslCipher> q_getDefaultDtlsCiphers(); QList<QSslCipher> q_getDefaultDtlsCiphers();
@ -168,8 +172,8 @@ SSL* QSslContext::createSsl()
if (!session && !sessionASN1().isEmpty() if (!session && !sessionASN1().isEmpty()
&& !sslConfiguration.testSslOption(QSsl::SslOptionDisableSessionPersistence)) { && !sslConfiguration.testSslOption(QSsl::SslOptionDisableSessionPersistence)) {
const unsigned char *data = reinterpret_cast<const unsigned char *>(m_sessionASN1.constData()); const unsigned char *data = reinterpret_cast<const unsigned char *>(m_sessionASN1.constData());
session = q_d2i_SSL_SESSION( session = q_d2i_SSL_SESSION(nullptr, &data, m_sessionASN1.size());
nullptr, &data, m_sessionASN1.size()); // refcount is 1 already, set by function above // 'session' has refcount 1 already, set by the function above
} }
if (session) { 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) { if (sslContext->sslConfiguration.peerVerifyMode() == QSslSocket::VerifyNone) {
q_SSL_CTX_set_verify(sslContext->ctx, SSL_VERIFY_NONE, nullptr); q_SSL_CTX_set_verify(sslContext->ctx, SSL_VERIFY_NONE, nullptr);
} else { } else {
@ -596,7 +601,17 @@ init_context:
q_X509Callback); 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) #if QT_CONFIG(dtls)
// DTLS cookies:
if (mode == QSslSocket::SslServerMode && isDtls && configuration.dtlsCookieVerificationEnabled()) { 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_generate_cb(sslContext->ctx, dtlscallbacks::q_generate_cookie_callback);
q_SSL_CTX_set_cookie_verify_cb(sslContext->ctx, dtlscallbacks::q_verify_cookie_callback); q_SSL_CTX_set_cookie_verify_cb(sslContext->ctx, dtlscallbacks::q_verify_cookie_callback);

View File

@ -322,6 +322,22 @@
\sa QSslPreSharedKeyAuthenticator \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 "qssl_p.h"
#include "qsslsocket.h" #include "qsslsocket.h"
#include "qsslcipher.h" #include "qsslcipher.h"

View File

@ -217,6 +217,7 @@ Q_SIGNALS:
void modeChanged(QSslSocket::SslMode newMode); void modeChanged(QSslSocket::SslMode newMode);
void encryptedBytesWritten(qint64 totalBytes); void encryptedBytesWritten(qint64 totalBytes);
void preSharedKeyAuthenticationRequired(QSslPreSharedKeyAuthenticator *authenticator); void preSharedKeyAuthenticationRequired(QSslPreSharedKeyAuthenticator *authenticator);
void newSessionTicketReceived();
protected: protected:
qint64 readData(char *data, qint64 maxlen) override; qint64 readData(char *data, qint64 maxlen) override;

View File

@ -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." 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<QSslSocketBackendPrivate *>(q_SSL_get_ex_data(ssl,
QSslSocketBackendPrivate::s_indexForSSLExtraData));
return socketPrivate->handleNewSessionTicket(ssl);
}
#endif // TLS1_3_VERSION #endif // TLS1_3_VERSION
#endif // !OPENSSL_NO_PSK #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<unsigned char *>(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() bool QSslSocketBackendPrivate::checkSslErrors()
{ {
Q_Q(QSslSocket); Q_Q(QSslSocket);

View File

@ -146,6 +146,7 @@ public:
void continueHandshake() override; void continueHandshake() override;
bool checkSslErrors(); bool checkSslErrors();
void storePeerCertificates(); 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 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); unsigned int tlsPskServerCallback(const char *identity, unsigned char *psk, unsigned int max_psk_len);
#ifdef Q_OS_WIN #ifdef Q_OS_WIN

View File

@ -159,6 +159,8 @@ DEFINEFUNC2(unsigned long, SSL_CTX_set_options, SSL_CTX *ctx, ctx, unsigned long
#ifdef TLS1_3_VERSION #ifdef TLS1_3_VERSION
DEFINEFUNC2(int, SSL_CTX_set_ciphersuites, SSL_CTX *ctx, ctx, const char *str, str, return 0, return) 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_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 #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_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) 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 #ifdef TLS1_3_VERSION
RESOLVEFUNC(SSL_CTX_set_ciphersuites) RESOLVEFUNC(SSL_CTX_set_ciphersuites)
RESOLVEFUNC(SSL_set_psk_use_session_callback) 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 #endif // TLS 1.3 or OpenSSL > 1.1.1
RESOLVEFUNC(SSL_get_client_random) RESOLVEFUNC(SSL_get_client_random)

View File

@ -224,7 +224,6 @@ QT_BEGIN_NAMESPACE
// To reduce the amount of the change, I'm directly copying and pasting the // 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, // content of the header here. Later, can be better sorted/split into groups,
// depending on the functionality. // depending on the functionality.
//#include "qsslsocket_openssl11_symbols_p.h"
const unsigned char * q_ASN1_STRING_get0_data(const ASN1_STRING *x); 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 #ifdef TLS1_3_VERSION
int q_SSL_CTX_set_ciphersuites(SSL_CTX *ctx, const char *str); 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 #endif
#if QT_CONFIG(dtls) #if QT_CONFIG(dtls)