diff options
-rw-r--r-- | src/network/ssl/qssl.cpp | 8 | ||||
-rw-r--r-- | src/network/ssl/qssl.h | 3 | ||||
-rw-r--r-- | src/network/ssl/qsslconfiguration.cpp | 65 | ||||
-rw-r--r-- | src/network/ssl/qsslconfiguration.h | 4 | ||||
-rw-r--r-- | src/network/ssl/qsslconfiguration_p.h | 6 | ||||
-rw-r--r-- | src/network/ssl/qsslcontext.cpp | 41 | ||||
-rw-r--r-- | src/network/ssl/qsslcontext_p.h | 5 | ||||
-rw-r--r-- | src/network/ssl/qsslsocket.cpp | 2 | ||||
-rw-r--r-- | src/network/ssl/qsslsocket_openssl.cpp | 10 | ||||
-rw-r--r-- | src/network/ssl/qsslsocket_openssl_symbols.cpp | 4 | ||||
-rw-r--r-- | src/network/ssl/qsslsocket_openssl_symbols_p.h | 2 | ||||
-rw-r--r-- | tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp | 59 |
12 files changed, 200 insertions, 9 deletions
diff --git a/src/network/ssl/qssl.cpp b/src/network/ssl/qssl.cpp index d5f5de10a2..ec771e1f49 100644 --- a/src/network/ssl/qssl.cpp +++ b/src/network/ssl/qssl.cpp @@ -163,12 +163,18 @@ QT_BEGIN_NAMESPACE possibility that an attacker could inject plaintext into the SSL session. \value SslOptionDisableSessionSharing Disables SSL session sharing via the session ID handshake attribute. + \value SslOptionDisableSessionPersistence Disables storing the SSL session + in ASN.1 format as returned by QSslConfiguration::session(). Enabling + this feature adds memory overhead of approximately 1K per used session + ticket. By default, SslOptionDisableEmptyFragments is turned on since this causes problems with a large number of servers. SslOptionDisableLegacyRenegotiation is also turned on, since it introduces a security risk. SslOptionDisableCompression is turned on to prevent the attack publicised by - CRIME. The other options are turned off. + CRIME. + SslOptionDisableSessionPersistence is turned on to optimize memory usage. + The other options are turned off. Note: Availability of above options depends on the version of the SSL backend in use. diff --git a/src/network/ssl/qssl.h b/src/network/ssl/qssl.h index 2429f3d580..21d03cb703 100644 --- a/src/network/ssl/qssl.h +++ b/src/network/ssl/qssl.h @@ -96,7 +96,8 @@ namespace QSsl { SslOptionDisableCompression = 0x04, SslOptionDisableServerNameIndication = 0x08, SslOptionDisableLegacyRenegotiation = 0x10, - SslOptionDisableSessionSharing = 0x20 + SslOptionDisableSessionSharing = 0x20, + SslOptionDisableSessionPersistence = 0x40 }; Q_DECLARE_FLAGS(SslOptions, SslOption) } diff --git a/src/network/ssl/qsslconfiguration.cpp b/src/network/ssl/qsslconfiguration.cpp index afbd4fac77..6cc06dfbd2 100644 --- a/src/network/ssl/qsslconfiguration.cpp +++ b/src/network/ssl/qsslconfiguration.cpp @@ -49,7 +49,8 @@ QT_BEGIN_NAMESPACE const QSsl::SslOptions QSslConfigurationPrivate::defaultSslOptions = QSsl::SslOptionDisableEmptyFragments |QSsl::SslOptionDisableLegacyRenegotiation - |QSsl::SslOptionDisableCompression; + |QSsl::SslOptionDisableCompression + |QSsl::SslOptionDisableSessionPersistence; /*! \class QSslConfiguration @@ -182,7 +183,9 @@ 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->sslSessionTicketLifeTimeHint == other.d->sslSessionTicketLifeTimeHint; } /*! @@ -216,7 +219,9 @@ bool QSslConfiguration::isNull() const d->privateKey.isNull() && d->peerCertificate.isNull() && d->peerCertificateChain.count() == 0 && - d->sslOptions == QSslConfigurationPrivate::defaultSslOptions); + d->sslOptions == QSslConfigurationPrivate::defaultSslOptions && + d->sslSession.isNull() && + d->sslSessionTicketLifeTimeHint == -1); } /*! @@ -594,6 +599,60 @@ bool QSslConfiguration::testSslOption(QSsl::SslOption option) const } /*! + \since 5.2 + + If QSsl::SslOptionDisableSessionPersistence was turned off, this + function returns the session used in the SSL handshake in ASN.1 + format, suitable to e.g. be persisted to disk. If no session was + used or QSsl::SslOptionDisableSessionPersistence was not turned off, + this function returns an empty QByteArray. + + \b{Note:} When persisting the session to disk or similar, be + careful not to expose the session to a potential attacker, as + knowledge of the session allows for eavesdropping on data + encrypted with the session parameters. + + \sa setSession(), QSsl::SslOptionDisableSessionPersistence, setSslOption() + */ +QByteArray QSslConfiguration::session() const +{ + return d->sslSession; +} + +/*! + \since 5.2 + + Sets the session to be used in an SSL handshake. + QSsl::SslOptionDisableSessionPersistence must be turned off + for this to work, and \a session must be in ASN.1 format + as returned by session(). + + \sa session(), QSsl::SslOptionDisableSessionPersistence, setSslOption() + */ +void QSslConfiguration::setSession(const QByteArray &session) +{ + d->sslSession = session; +} + +/*! + \since 5.2 + + If QSsl::SslOptionDisableSessionPersistence was turned off, this + function returns the session ticket life time hint sent by the + server (which might be 0). + If the server did not send a session ticket (e.g. when + resuming a session or when the server does not support it) or + QSsl::SslOptionDisableSessionPersistence was not turned off, + this function returns -1. + + \sa session(), QSsl::SslOptionDisableSessionPersistence, setSslOption() + */ +int QSslConfiguration::sessionTicketLifeTimeHint() const +{ + return d->sslSessionTicketLifeTimeHint; +} + +/*! Returns the default SSL configuration to be used in new SSL connections. diff --git a/src/network/ssl/qsslconfiguration.h b/src/network/ssl/qsslconfiguration.h index 0000382ed5..949ce70d4c 100644 --- a/src/network/ssl/qsslconfiguration.h +++ b/src/network/ssl/qsslconfiguration.h @@ -124,6 +124,10 @@ public: void setSslOption(QSsl::SslOption option, bool on); bool testSslOption(QSsl::SslOption option) const; + QByteArray session() const; + void setSession(const QByteArray &session); + int sessionTicketLifeTimeHint() const; + static QSslConfiguration defaultConfiguration(); static void setDefaultConfiguration(const QSslConfiguration &configuration); diff --git a/src/network/ssl/qsslconfiguration_p.h b/src/network/ssl/qsslconfiguration_p.h index 54b7264d3d..71ee8d2bfe 100644 --- a/src/network/ssl/qsslconfiguration_p.h +++ b/src/network/ssl/qsslconfiguration_p.h @@ -85,7 +85,8 @@ public: peerVerifyDepth(0), allowRootCertOnDemandLoading(true), peerSessionShared(false), - sslOptions(QSslConfigurationPrivate::defaultSslOptions) + sslOptions(QSslConfigurationPrivate::defaultSslOptions), + sslSessionTicketLifeTimeHint(-1) { } QSslCertificate peerCertificate; @@ -110,6 +111,9 @@ public: Q_AUTOTEST_EXPORT static const QSsl::SslOptions defaultSslOptions; + 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 22ad42116b..6d281c390d 100644 --- a/src/network/ssl/qsslcontext.cpp +++ b/src/network/ssl/qsslcontext.cpp @@ -57,7 +57,8 @@ extern QString getErrorsFromOpenSsl(); QSslContext::QSslContext() : ctx(0), pkey(0), - session(0) + session(0), + m_sessionTicketLifeTimeHint(-1) { } @@ -258,6 +259,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.session().isEmpty()) + sslContext->setSessionASN1(configuration.session()); + return sslContext; } @@ -267,6 +272,12 @@ SSL* QSslContext::createSsl() SSL* ssl = q_SSL_new(ctx); q_SSL_clear(ssl); + 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(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)) { @@ -292,8 +303,34 @@ 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::SslOptionDisableSessionPersistence)) { + int sessionSize = q_i2d_SSL_SESSION(session, 0); + if (sessionSize > 0) { + m_sessionASN1.resize(sessionSize); + unsigned char *data = reinterpret_cast<unsigned char *>(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 c8578d349e..2b596798a6 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 f2310356df..0e7ac39d14 100644 --- a/src/network/ssl/qsslsocket.cpp +++ b/src/network/ssl/qsslsocket.cpp @@ -903,6 +903,8 @@ void QSslSocket::setSslConfiguration(const QSslConfiguration &configuration) d->configuration.peerVerifyMode = configuration.peerVerifyMode(); d->configuration.protocol = configuration.protocol(); d->configuration.sslOptions = configuration.d->sslOptions; + d->configuration.sslSession = configuration.session(); + d->configuration.sslSessionTicketLifeTimeHint = configuration.sessionTicketLifeTimeHint(); // 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 24428a0321..6415c69936 100644 --- a/src/network/ssl/qsslsocket_openssl.cpp +++ b/src/network/ssl/qsslsocket_openssl.cpp @@ -1450,8 +1450,16 @@ void QSslSocketBackendPrivate::continueHandshake() // Cache this SSL session inside the QSslContext if (!(configuration.sslOptions & QSsl::SslOptionDisableSessionSharing)) { - 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 (!(configuration.sslOptions & QSsl::SslOptionDisableSessionPersistence)) { + 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 fed99752b0..3ab038a3f7 100644 --- a/src/network/ssl/qsslsocket_openssl_symbols.cpp +++ b/src/network/ssl/qsslsocket_openssl_symbols.cpp @@ -328,6 +328,8 @@ DEFINEFUNC(void, OPENSSL_add_all_algorithms_conf, void, DUMMYARG, return, DUMMYA 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) DEFINEFUNC(const char *, SSLeay_version, int a, a, 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) #define RESOLVEFUNC(func) \ if (!(_q_##func = _q_PTR_##func(libs.first->resolve(#func))) \ @@ -797,6 +799,8 @@ bool q_resolveOpenSslSymbols() RESOLVEFUNC(SSL_CTX_load_verify_locations) RESOLVEFUNC(SSLeay) RESOLVEFUNC(SSLeay_version) + RESOLVEFUNC(i2d_SSL_SESSION) + RESOLVEFUNC(d2i_SSL_SESSION) 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 62648e3e37..44c8f298c3 100644 --- a/src/network/ssl/qsslsocket_openssl_symbols_p.h +++ b/src/network/ssl/qsslsocket_openssl_symbols_p.h @@ -465,6 +465,8 @@ 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(); const char *q_SSLeay_version(int type); +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/network/access/qnetworkreply/tst_qnetworkreply.cpp b/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp index 97f9667140..81c3e48d61 100644 --- a/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp +++ b/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp @@ -367,6 +367,8 @@ private Q_SLOTS: #ifdef QT_BUILD_INTERNAL void sslSessionSharing_data(); void sslSessionSharing(); + void sslSessionSharingFromPersistentSession_data(); + void sslSessionSharingFromPersistentSession(); #endif #endif @@ -5966,6 +5968,63 @@ void tst_QNetworkReply::sslSessionSharingHelperSlot() } } +void tst_QNetworkReply::sslSessionSharingFromPersistentSession_data() +{ + QTest::addColumn<bool>("sessionPersistenceEnabled"); + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void tst_QNetworkReply::sslSessionSharingFromPersistentSession() +{ + QString urlString("https://" + QtNetworkSettings::serverName()); + + // warm up SSL session cache to get a working session + QNetworkRequest warmupRequest(urlString); + QFETCH(bool, sessionPersistenceEnabled); + if (sessionPersistenceEnabled) { + QSslConfiguration warmupConfiguration(QSslConfiguration::defaultConfiguration()); + warmupConfiguration.setSslOption(QSsl::SslOptionDisableSessionPersistence, false); + warmupRequest.setSslConfiguration(warmupConfiguration); + } + 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->sslConfiguration().session(); + QCOMPARE(!sslSession.isEmpty(), sessionPersistenceEnabled); + + // test server sends a life time hint of 0, which is not common + // practice; however it is good enough because the default is -1 + int expectedSessionTicketLifeTimeHint = sessionPersistenceEnabled ? 0 : -1; + QCOMPARE(warmupReply->sslConfiguration().sessionTicketLifeTimeHint(), + expectedSessionTicketLifeTimeHint); + + 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) { + QSslConfiguration configuration = request.sslConfiguration(); + configuration.setSession(sslSession); + request.setSslConfiguration(configuration); + } + 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_SSL |