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:
parent
33c9a1e0bc
commit
b36b7abb40
@ -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
|
||||
{
|
||||
|
@ -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);
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user