From 054e85bfdae2af37f0c2ee17d131e0b3cb2d2f25 Mon Sep 17 00:00:00 2001 From: Peter Hartmann Date: Tue, 30 Apr 2013 14:48:22 +0200 Subject: [BB10-internal] QSslConfiguration: add API to persist and resume SSL sessions Session tickets can be cached on the client side for hours (e.g. graph.facebook.com: ~ 24 hours, api.twitter.com: 4 hours), because the server does not need to maintain state. We need public API for it so an application can cache the session (e.g. to disk) and resume a session already with the 1st handshake, saving one network round trip. Task-number: QTBUG-20668 (backport of commit 3be197881f100d1c3c8f3ce00501d7a32eb51119) Change-Id: I4c7f3a749edf0012b52deeb495706e550d24c42d Signed-off-by: Peter Hartmann --- src/network/access/qhttpnetworkrequest.cpp | 15 +++++- src/network/access/qhttpnetworkrequest_p.h | 4 ++ src/network/access/qhttpthreaddelegate.cpp | 3 ++ src/network/access/qnetworkaccesshttpbackend.cpp | 27 +++++++++- src/network/ssl/qsslconfiguration.cpp | 10 +++- src/network/ssl/qsslconfiguration.h | 4 ++ src/network/ssl/qsslconfiguration_p.h | 8 ++- src/network/ssl/qsslcontext.cpp | 42 ++++++++++++++- src/network/ssl/qsslcontext_p.h | 5 ++ src/network/ssl/qsslsocket.cpp | 3 ++ src/network/ssl/qsslsocket_openssl.cpp | 10 +++- src/network/ssl/qsslsocket_openssl_symbols.cpp | 4 ++ src/network/ssl/qsslsocket_openssl_symbols_p.h | 2 + tests/auto/qnetworkreply/tst_qnetworkreply.cpp | 66 ++++++++++++++++++++++++ 14 files changed, 195 insertions(+), 8 deletions(-) diff --git a/src/network/access/qhttpnetworkrequest.cpp b/src/network/access/qhttpnetworkrequest.cpp index 3fc6229e29..d437f6363a 100644 --- a/src/network/access/qhttpnetworkrequest.cpp +++ b/src/network/access/qhttpnetworkrequest.cpp @@ -50,6 +50,7 @@ QHttpNetworkRequestPrivate::QHttpNetworkRequestPrivate(QHttpNetworkRequest::Oper QHttpNetworkRequest::Priority pri, const QUrl &newUrl) : QHttpNetworkHeaderPrivate(newUrl), operation(op), priority(pri), uploadByteDevice(0), autoDecompress(false), pipeliningAllowed(false), withCredentials(true) + , m_cacheSslSession(false) { } @@ -64,6 +65,7 @@ QHttpNetworkRequestPrivate::QHttpNetworkRequestPrivate(const QHttpNetworkRequest customVerb = other.customVerb; withCredentials = other.withCredentials; ssl = other.ssl; + m_cacheSslSession = other.m_cacheSslSession; } QHttpNetworkRequestPrivate::~QHttpNetworkRequestPrivate() @@ -75,7 +77,8 @@ bool QHttpNetworkRequestPrivate::operator==(const QHttpNetworkRequestPrivate &ot return QHttpNetworkHeaderPrivate::operator==(other) && (operation == other.operation) && (ssl == other.ssl) - && (uploadByteDevice == other.uploadByteDevice); + && (uploadByteDevice == other.uploadByteDevice) + && (m_cacheSslSession == other.m_cacheSslSession); } QByteArray QHttpNetworkRequestPrivate::methodName() const @@ -311,6 +314,16 @@ QNonContiguousByteDevice* QHttpNetworkRequest::uploadByteDevice() const return d->uploadByteDevice; } +bool QHttpNetworkRequest::cacheSslSession() +{ + return d->m_cacheSslSession; +} + +void QHttpNetworkRequest::setCacheSslSession(bool cacheSession) +{ + d->m_cacheSslSession = cacheSession; +} + int QHttpNetworkRequest::majorVersion() const { return 1; diff --git a/src/network/access/qhttpnetworkrequest_p.h b/src/network/access/qhttpnetworkrequest_p.h index e6216db2e3..09dd76e1fa 100644 --- a/src/network/access/qhttpnetworkrequest_p.h +++ b/src/network/access/qhttpnetworkrequest_p.h @@ -122,6 +122,9 @@ public: void setUploadByteDevice(QNonContiguousByteDevice *bd); QNonContiguousByteDevice* uploadByteDevice() const; + bool cacheSslSession(); + void setCacheSslSession(bool cacheSession); + private: QSharedDataPointer d; friend class QHttpNetworkRequestPrivate; @@ -150,6 +153,7 @@ public: bool pipeliningAllowed; bool withCredentials; bool ssl; + bool m_cacheSslSession; }; diff --git a/src/network/access/qhttpthreaddelegate.cpp b/src/network/access/qhttpthreaddelegate.cpp index 38d9bc1177..7ace6279d7 100644 --- a/src/network/access/qhttpthreaddelegate.cpp +++ b/src/network/access/qhttpthreaddelegate.cpp @@ -279,6 +279,9 @@ void QHttpThreadDelegate::startRequest() #endif #ifndef QT_NO_OPENSSL // Set the QSslConfiguration from this QNetworkRequest. + if (httpRequest.cacheSslSession()) + incomingSslConfiguration.d->cacheSslSession = true; + if (ssl && incomingSslConfiguration != QSslConfiguration::defaultConfiguration()) { httpConnection->setSslConfiguration(incomingSslConfiguration); } diff --git a/src/network/access/qnetworkaccesshttpbackend.cpp b/src/network/access/qnetworkaccesshttpbackend.cpp index f3ae1bcb7f..68200b1f04 100644 --- a/src/network/access/qnetworkaccesshttpbackend.cpp +++ b/src/network/access/qnetworkaccesshttpbackend.cpp @@ -518,6 +518,9 @@ void QNetworkAccessHttpBackend::postRequest() QNetworkRequest::Automatic).toInt()) == QNetworkRequest::Manual) httpRequest.setWithCredentials(false); + if (request().attribute(static_cast( + static_cast(QNetworkRequest::User)-1)).toBool() == true) + httpRequest.setCacheSslSession(true); // Create the HTTP thread delegate QHttpThreadDelegate *delegate = new QHttpThreadDelegate; @@ -544,8 +547,15 @@ void QNetworkAccessHttpBackend::postRequest() #endif delegate->ssl = ssl; #ifndef QT_NO_OPENSSL - if (ssl) + if (ssl) { delegate->incomingSslConfiguration = request().sslConfiguration(); + QNetworkRequest::Attribute sslSessionAttribute = + static_cast( + static_cast(QNetworkRequest::User)-3); + QByteArray sslSession = request().attribute(sslSessionAttribute).toByteArray(); + if (!sslSession.isEmpty()) + delegate->incomingSslConfiguration.d->sslSession = sslSession; + } #endif // Do we use synchronous HTTP? @@ -913,6 +923,21 @@ void QNetworkAccessHttpBackend::replySslConfigurationChanged(const QSslConfigura *pendingSslConfiguration = c; else if (!c.isNull()) pendingSslConfiguration = new QSslConfiguration(c); + + if (c.d->sslSession.size() > 0) { + QNetworkRequest::Attribute sslSessionAttribute = + static_cast( + static_cast(QNetworkRequest::User)-3); + QNetworkRequest::Attribute sslSessionTicketLifeTimeHintAttribute = + static_cast( + static_cast(QNetworkRequest::User)-2); + // only set the attribute once; this method is called several times + if (attribute(sslSessionAttribute).toByteArray().isEmpty()) { + setAttribute(sslSessionAttribute, c.d->sslSession); + setAttribute(sslSessionTicketLifeTimeHintAttribute, + c.d->sslSessionTicketLifeTimeHint); + } + } } #endif diff --git a/src/network/ssl/qsslconfiguration.cpp b/src/network/ssl/qsslconfiguration.cpp index 54e6751664..d1d4b87231 100644 --- a/src/network/ssl/qsslconfiguration.cpp +++ b/src/network/ssl/qsslconfiguration.cpp @@ -169,7 +169,10 @@ bool QSslConfiguration::operator==(const QSslConfiguration &other) const d->peerVerifyMode == other.d->peerVerifyMode && d->peerVerifyDepth == other.d->peerVerifyDepth && d->allowRootCertOnDemandLoading == other.d->allowRootCertOnDemandLoading && - d->sslOptions == other.d->sslOptions; + d->sslOptions == other.d->sslOptions && + d->sslSession == other.d->sslSession && + d->cacheSslSession == other.d->cacheSslSession && + d->sslSessionTicketLifeTimeHint == other.d->sslSessionTicketLifeTimeHint; } /*! @@ -205,7 +208,10 @@ bool QSslConfiguration::isNull() const d->peerCertificateChain.count() == 0 && d->sslOptions == ( QSsl::SslOptionDisableEmptyFragments |QSsl::SslOptionDisableLegacyRenegotiation - |QSsl::SslOptionDisableCompression)); + |QSsl::SslOptionDisableCompression) && + d->sslSession.isNull() && + d->cacheSslSession == false && + d->sslSessionTicketLifeTimeHint == -1); } /*! diff --git a/src/network/ssl/qsslconfiguration.h b/src/network/ssl/qsslconfiguration.h index e27b99fc0e..dc37ba2af0 100644 --- a/src/network/ssl/qsslconfiguration.h +++ b/src/network/ssl/qsslconfiguration.h @@ -130,6 +130,10 @@ private: friend class QSslConfigurationPrivate; friend class QSslSocketBackendPrivate; friend class QSslContext; + // hack to set the SSL session from a QNAM attribute: + friend class QHttpThreadDelegate; + friend class QNetworkAccessHttpBackend; + QSslConfiguration(QSslConfigurationPrivate *dd); QSharedDataPointer d; }; diff --git a/src/network/ssl/qsslconfiguration_p.h b/src/network/ssl/qsslconfiguration_p.h index cf871c8c71..40f4729a78 100644 --- a/src/network/ssl/qsslconfiguration_p.h +++ b/src/network/ssl/qsslconfiguration_p.h @@ -87,7 +87,9 @@ public: peerSessionShared(false), sslOptions(QSsl::SslOptionDisableEmptyFragments |QSsl::SslOptionDisableLegacyRenegotiation - |QSsl::SslOptionDisableCompression) + |QSsl::SslOptionDisableCompression), + cacheSslSession(false), + sslSessionTicketLifeTimeHint(-1) { } QSslCertificate peerCertificate; @@ -110,6 +112,10 @@ public: QSsl::SslOptions sslOptions; + bool cacheSslSession; + QByteArray sslSession; + int sslSessionTicketLifeTimeHint; + // in qsslsocket.cpp: static QSslConfiguration defaultConfiguration(); static void setDefaultConfiguration(const QSslConfiguration &configuration); diff --git a/src/network/ssl/qsslcontext.cpp b/src/network/ssl/qsslcontext.cpp index e0a9cbc12c..c73fb939cb 100644 --- a/src/network/ssl/qsslcontext.cpp +++ b/src/network/ssl/qsslcontext.cpp @@ -58,7 +58,8 @@ extern QString getErrorsFromOpenSsl(); QSslContext::QSslContext() : ctx(0), pkey(0), - session(0) + session(0), + m_sessionTicketLifeTimeHint(-1) { } @@ -259,6 +260,10 @@ init_context: if (sslContext->sslConfiguration.peerVerifyDepth() != 0) q_SSL_CTX_set_verify_depth(sslContext->ctx, sslContext->sslConfiguration.peerVerifyDepth()); + // set persisted session if the user set it + if (!configuration.d->sslSession.isEmpty()) + sslContext->setSessionASN1(configuration.d->sslSession); + return sslContext; } @@ -268,6 +273,12 @@ SSL* QSslContext::createSsl() SSL* ssl = q_SSL_new(ctx); q_SSL_clear(ssl); + if (!session && !sessionASN1().isEmpty() + && !sslConfiguration.testSslOption(QSsl::SslOptionDisableSessionTickets)) { + const unsigned char *data = reinterpret_cast(m_sessionASN1.constData()); + session = q_d2i_SSL_SESSION(0, &data, m_sessionASN1.size()); // refcount is 1 already, set by function above + } + if (session) { // Try to resume the last session we cached if (!q_SSL_set_session(ssl, session)) { @@ -293,8 +304,35 @@ bool QSslContext::cacheSession(SSL* ssl) // cache the session the caller gave us and increase reference count session = q_SSL_get1_session(ssl); - return (session != NULL); + if (session && !sslConfiguration.testSslOption(QSsl::SslOptionDisableSessionTickets) + && sslConfiguration.d->cacheSslSession == true) { + int sessionSize = q_i2d_SSL_SESSION(session, 0); + if (sessionSize > 0) { + m_sessionASN1.resize(sessionSize); + unsigned char *data = reinterpret_cast(m_sessionASN1.data()); + if (!q_i2d_SSL_SESSION(session, &data)) + qWarning("could not store persistent version of SSL session"); + m_sessionTicketLifeTimeHint = session->tlsext_tick_lifetime_hint; + } + } + + return (session != 0); +} + +QByteArray QSslContext::sessionASN1() const +{ + return m_sessionASN1; +} + +void QSslContext::setSessionASN1(const QByteArray &session) +{ + m_sessionASN1 = session; +} + +int QSslContext::sessionTicketLifeTimeHint() const +{ + return m_sessionTicketLifeTimeHint; } QSslError::SslError QSslContext::error() const diff --git a/src/network/ssl/qsslcontext_p.h b/src/network/ssl/qsslcontext_p.h index 7f2fc4bc9c..b54a833cb6 100644 --- a/src/network/ssl/qsslcontext_p.h +++ b/src/network/ssl/qsslcontext_p.h @@ -69,6 +69,9 @@ public: SSL* createSsl(); bool cacheSession(SSL*); // should be called when handshake completed + QByteArray sessionASN1() const; + void setSessionASN1(const QByteArray &sessionASN1); + int sessionTicketLifeTimeHint() const; protected: QSslContext(); @@ -76,6 +79,8 @@ private: SSL_CTX* ctx; EVP_PKEY *pkey; SSL_SESSION *session; + QByteArray m_sessionASN1; + int m_sessionTicketLifeTimeHint; QSslError::SslError errorCode; QString errorStr; QSslConfiguration sslConfiguration; diff --git a/src/network/ssl/qsslsocket.cpp b/src/network/ssl/qsslsocket.cpp index be46ca5ffb..fc75fa52e1 100644 --- a/src/network/ssl/qsslsocket.cpp +++ b/src/network/ssl/qsslsocket.cpp @@ -897,6 +897,9 @@ void QSslSocket::setSslConfiguration(const QSslConfiguration &configuration) d->configuration.peerVerifyMode = configuration.peerVerifyMode(); d->configuration.protocol = configuration.protocol(); d->configuration.sslOptions = configuration.d->sslOptions; + d->configuration.cacheSslSession = configuration.d->cacheSslSession; + d->configuration.sslSession = configuration.d->sslSession; + d->configuration.sslSessionTicketLifeTimeHint = configuration.d->sslSessionTicketLifeTimeHint; // if the CA certificates were set explicitly (either via // QSslConfiguration::setCaCertificates() or QSslSocket::setCaCertificates(), diff --git a/src/network/ssl/qsslsocket_openssl.cpp b/src/network/ssl/qsslsocket_openssl.cpp index 073ad27a4b..d3b8ba979e 100644 --- a/src/network/ssl/qsslsocket_openssl.cpp +++ b/src/network/ssl/qsslsocket_openssl.cpp @@ -1271,8 +1271,16 @@ bool QSslSocketBackendPrivate::startHandshake() // Cache this SSL session inside the QSslContext if (!(configuration.sslOptions & QSsl::SslOptionDisableSessionTickets)) { - if (!sslContextPointer->cacheSession(ssl)) + if (!sslContextPointer->cacheSession(ssl)) { sslContextPointer.clear(); // we could not cache the session + } else { + // Cache the session for permanent usage as well + if (!sslContextPointer->sessionASN1().isEmpty()) { + configuration.sslSession = sslContextPointer->sessionASN1(); + configuration.sslSessionTicketLifeTimeHint = + sslContextPointer->sessionTicketLifeTimeHint(); + } + } } connectionEncrypted = true; diff --git a/src/network/ssl/qsslsocket_openssl_symbols.cpp b/src/network/ssl/qsslsocket_openssl_symbols.cpp index e7c9077e85..1c32dbed90 100644 --- a/src/network/ssl/qsslsocket_openssl_symbols.cpp +++ b/src/network/ssl/qsslsocket_openssl_symbols.cpp @@ -286,6 +286,8 @@ DEFINEFUNC(void, OPENSSL_add_all_algorithms_noconf, void, DUMMYARG, return, DUMM DEFINEFUNC(void, OPENSSL_add_all_algorithms_conf, void, DUMMYARG, return, DUMMYARG) DEFINEFUNC3(int, SSL_CTX_load_verify_locations, SSL_CTX *ctx, ctx, const char *CAfile, CAfile, const char *CApath, CApath, return 0, return) DEFINEFUNC(long, SSLeay, void, DUMMYARG, return 0, return) +DEFINEFUNC2(int, i2d_SSL_SESSION, SSL_SESSION *in, in, unsigned char **pp, pp, return 0, return) +DEFINEFUNC3(SSL_SESSION *, d2i_SSL_SESSION, SSL_SESSION **a, a, const unsigned char **pp, pp, long length, length, return 0, return) #ifdef Q_OS_SYMBIAN #define RESOLVEFUNC(func, ordinal, lib) \ @@ -872,6 +874,8 @@ bool q_resolveOpenSslSymbols() RESOLVEFUNC(OPENSSL_add_all_algorithms_conf) RESOLVEFUNC(SSL_CTX_load_verify_locations) RESOLVEFUNC(SSLeay) + RESOLVEFUNC(i2d_SSL_SESSION) + RESOLVEFUNC(d2i_SSL_SESSION) #endif // Q_OS_SYMBIAN symbolsResolved = true; delete libs.first; diff --git a/src/network/ssl/qsslsocket_openssl_symbols_p.h b/src/network/ssl/qsslsocket_openssl_symbols_p.h index c740fa445c..761b8ba04f 100644 --- a/src/network/ssl/qsslsocket_openssl_symbols_p.h +++ b/src/network/ssl/qsslsocket_openssl_symbols_p.h @@ -429,6 +429,8 @@ void q_OPENSSL_add_all_algorithms_noconf(); void q_OPENSSL_add_all_algorithms_conf(); int q_SSL_CTX_load_verify_locations(SSL_CTX *ctx, const char *CAfile, const char *CApath); long q_SSLeay(); +int q_i2d_SSL_SESSION(SSL_SESSION *in, unsigned char **pp); +SSL_SESSION *q_d2i_SSL_SESSION(SSL_SESSION **a, const unsigned char **pp, long length); // Helper function class QDateTime; diff --git a/tests/auto/qnetworkreply/tst_qnetworkreply.cpp b/tests/auto/qnetworkreply/tst_qnetworkreply.cpp index c0975817ac..0f60e31086 100644 --- a/tests/auto/qnetworkreply/tst_qnetworkreply.cpp +++ b/tests/auto/qnetworkreply/tst_qnetworkreply.cpp @@ -349,6 +349,8 @@ private Q_SLOTS: #ifdef QT_BUILD_INTERNAL void sslSessionSharing_data(); void sslSessionSharing(); + void sslSessionSharingFromPersistentSession_data(); + void sslSessionSharingFromPersistentSession(); #endif #endif @@ -5742,6 +5744,70 @@ void tst_QNetworkReply::sslSessionSharingHelperSlot() } } +void tst_QNetworkReply::sslSessionSharingFromPersistentSession_data() +{ + QTest::addColumn("sessionPersistenceEnabled"); + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void tst_QNetworkReply::sslSessionSharingFromPersistentSession() +{ + QNetworkRequest::Attribute sslSessionEnablePersistenceAttribute = + static_cast( + static_cast(QNetworkRequest::User)-1); + QNetworkRequest::Attribute sslSessionTicketLifeTimeHintAttribute = + static_cast( + static_cast(QNetworkRequest::User)-2); + QNetworkRequest::Attribute sslSessionAttribute = + static_cast( + static_cast(QNetworkRequest::User)-3); + + QString urlString("https://" + QtNetworkSettings::serverName()); + + // warm up SSL session cache to get a working session + QNetworkRequest warmupRequest(urlString); + QFETCH(bool, sessionPersistenceEnabled); + if (sessionPersistenceEnabled) + warmupRequest.setAttribute(sslSessionEnablePersistenceAttribute, true); + + QNetworkReply *warmupReply = manager.get(warmupRequest); + warmupReply->ignoreSslErrors(); + connect(warmupReply, SIGNAL(finished()), &QTestEventLoop::instance(), SLOT(exitLoop())); + QTestEventLoop::instance().enterLoop(20); + QVERIFY(!QTestEventLoop::instance().timeout()); + QCOMPARE(warmupReply->error(), QNetworkReply::NoError); + QByteArray sslSession = warmupReply->attribute(sslSessionAttribute).toByteArray(); + QCOMPARE(!sslSession.isEmpty(), sessionPersistenceEnabled); + + QVariant sessionTicketLifeTimeHint = warmupReply->attribute( + sslSessionTicketLifeTimeHintAttribute); + // the value will always be 0: If enabled the server sends a value of 0, + // but in that case the attribute is defined at least + QCOMPARE(sessionPersistenceEnabled, !sessionTicketLifeTimeHint.isNull()); + QCOMPARE(sessionTicketLifeTimeHint.toInt(), 0); + + warmupReply->deleteLater(); + + // now send another request with a new QNAM and the persisted session, + // to verify it can be resumed without any internal state + QNetworkRequest request(warmupRequest); + if (sessionPersistenceEnabled) + request.setAttribute(sslSessionAttribute, sslSession); + + QNetworkAccessManager newManager; + QNetworkReply *reply = newManager.get(request); + reply->ignoreSslErrors(); + connect(reply, SIGNAL(finished()), &QTestEventLoop::instance(), SLOT(exitLoop())); + QTestEventLoop::instance().enterLoop(20); + QVERIFY(!QTestEventLoop::instance().timeout()); + QCOMPARE(reply->error(), QNetworkReply::NoError); + + bool sslSessionSharingWasUsedInReply = QSslConfigurationPrivate::peerSessionWasShared( + reply->sslConfiguration()); + QCOMPARE(sessionPersistenceEnabled, sslSessionSharingWasUsedInReply); +} + #endif // QT_BUILD_INTERNAL #endif // QT_NO_OPENSSL -- cgit v1.2.1