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
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
{

View File

@ -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<QSslCipher> q_getDefaultDtlsCiphers();
@ -168,8 +172,8 @@ SSL* QSslContext::createSsl()
if (!session && !sessionASN1().isEmpty()
&& !sslConfiguration.testSslOption(QSsl::SslOptionDisableSessionPersistence)) {
const unsigned char *data = reinterpret_cast<const unsigned char *>(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);

View File

@ -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"

View File

@ -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;

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."
}
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 // !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()
{
Q_Q(QSslSocket);

View File

@ -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

View File

@ -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)

View File

@ -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)