From b5652df775efbd1c52eecee5f08e40e600e5d70b Mon Sep 17 00:00:00 2001 From: Peter Hartmann Date: Fri, 21 Dec 2012 14:02:38 +0100 Subject: SSL: Implement session sharing and use it from QNetworkAccessManager This improves performance since a network round trip can be avoided. Change-Id: I1aaff7e48ef9638cb137de0f43942c3a4dd2884a Initial-patch-by: Markus Goetz Reviewed-by: Richard J. Moore --- src/network/access/qhttpnetworkconnection.cpp | 12 + src/network/access/qhttpnetworkconnection_p.h | 9 + .../access/qhttpnetworkconnectionchannel.cpp | 19 +- src/network/ssl/qsslconfiguration.cpp | 6 + src/network/ssl/qsslconfiguration.h | 2 + src/network/ssl/qsslconfiguration_p.h | 5 + src/network/ssl/qsslcontext.cpp | 306 +++++++++++++++++++++ src/network/ssl/qsslcontext_p.h | 88 ++++++ src/network/ssl/qsslsocket.cpp | 17 ++ src/network/ssl/qsslsocket_openssl.cpp | 243 +++------------- src/network/ssl/qsslsocket_openssl_p.h | 5 +- src/network/ssl/qsslsocket_openssl_symbols.cpp | 8 + src/network/ssl/qsslsocket_openssl_symbols_p.h | 4 + src/network/ssl/qsslsocket_p.h | 11 +- src/network/ssl/ssl.pri | 6 +- .../access/qnetworkreply/tst_qnetworkreply.cpp | 77 ++++++ 16 files changed, 604 insertions(+), 214 deletions(-) create mode 100644 src/network/ssl/qsslcontext.cpp create mode 100644 src/network/ssl/qsslcontext_p.h diff --git a/src/network/access/qhttpnetworkconnection.cpp b/src/network/access/qhttpnetworkconnection.cpp index eec5cfa96d..509f8b3251 100644 --- a/src/network/access/qhttpnetworkconnection.cpp +++ b/src/network/access/qhttpnetworkconnection.cpp @@ -1224,6 +1224,18 @@ void QHttpNetworkConnection::setSslConfiguration(const QSslConfiguration &config d->channels[i].setSslConfiguration(config); } +QSharedPointer QHttpNetworkConnection::sslContext() +{ + Q_D(QHttpNetworkConnection); + return d->sslContext; +} + +void QHttpNetworkConnection::setSslContext(QSharedPointer context) +{ + Q_D(QHttpNetworkConnection); + d->sslContext = context; +} + void QHttpNetworkConnection::ignoreSslErrors(int channel) { Q_D(QHttpNetworkConnection); diff --git a/src/network/access/qhttpnetworkconnection_p.h b/src/network/access/qhttpnetworkconnection_p.h index 57d40bfcf2..956499ddab 100644 --- a/src/network/access/qhttpnetworkconnection_p.h +++ b/src/network/access/qhttpnetworkconnection_p.h @@ -62,6 +62,7 @@ #include #include #include +#include #include #include @@ -72,6 +73,8 @@ #ifndef QT_NO_HTTP #ifndef QT_NO_SSL +# include +# include # include # include #else @@ -124,6 +127,8 @@ public: void setSslConfiguration(const QSslConfiguration &config); void ignoreSslErrors(int channel = -1); void ignoreSslErrors(const QList &errors, int channel = -1); + QSharedPointer sslContext(); + void setSslContext(QSharedPointer context); #endif private: @@ -234,6 +239,10 @@ public: QList highPriorityQueue; QList lowPriorityQueue; +#ifndef QT_NO_SSL + QSharedPointer sslContext; +#endif + #ifndef QT_NO_BEARERMANAGEMENT QSharedPointer networkSession; #endif diff --git a/src/network/access/qhttpnetworkconnectionchannel.cpp b/src/network/access/qhttpnetworkconnectionchannel.cpp index 38723a7032..4dfed762f5 100644 --- a/src/network/access/qhttpnetworkconnectionchannel.cpp +++ b/src/network/access/qhttpnetworkconnectionchannel.cpp @@ -622,6 +622,13 @@ bool QHttpNetworkConnectionChannel::ensureConnection() if (ssl) { #ifndef QT_NO_SSL QSslSocket *sslSocket = qobject_cast(socket); + + // check whether we can re-use an existing SSL session + // (meaning another socket in this connection has already + // performed a full handshake) + if (!connection->sslContext().isNull()) + QSslSocketPrivate::checkSettingSslContext(sslSocket, connection->sslContext()); + sslSocket->connectToHostEncrypted(connectHost, connectPort, QIODevice::ReadWrite, networkLayerPreference); if (ignoreAllSslErrors) sslSocket->ignoreSslErrors(); @@ -1065,7 +1072,17 @@ void QHttpNetworkConnectionChannel::_q_connected() // ### FIXME: if the server closes the connection unexpectedly, we shouldn't send the same broken request again! //channels[i].reconnectAttempts = 2; - if (!pendingEncrypt) { + if (pendingEncrypt) { +#ifndef QT_NO_SSL + if (connection->sslContext().isNull()) { + // this socket is making the 1st handshake for this connection, + // we need to set the SSL context so new sockets can reuse it + QSharedPointer socketSslContext = QSslSocketPrivate::sslContext(static_cast(socket)); + if (!socketSslContext.isNull()) + connection->setSslContext(socketSslContext); + } +#endif + } else { state = QHttpNetworkConnectionChannel::IdleState; if (!reply) connection->d_func()->dequeueRequest(socket); diff --git a/src/network/ssl/qsslconfiguration.cpp b/src/network/ssl/qsslconfiguration.cpp index 0ae67b3c1f..145cd7be5d 100644 --- a/src/network/ssl/qsslconfiguration.cpp +++ b/src/network/ssl/qsslconfiguration.cpp @@ -584,4 +584,10 @@ void QSslConfiguration::setDefaultConfiguration(const QSslConfiguration &configu QSslConfigurationPrivate::setDefaultConfiguration(configuration); } +/*! \internal +*/ +bool QSslConfigurationPrivate::peerSessionWasShared(const QSslConfiguration &configuration) { + return configuration.d->peerSessionShared; + } + QT_END_NAMESPACE diff --git a/src/network/ssl/qsslconfiguration.h b/src/network/ssl/qsslconfiguration.h index 5bde5cb446..b1d24514f5 100644 --- a/src/network/ssl/qsslconfiguration.h +++ b/src/network/ssl/qsslconfiguration.h @@ -127,6 +127,8 @@ public: private: friend class QSslSocket; friend class QSslConfigurationPrivate; + friend class QSslSocketBackendPrivate; + friend class QSslContext; QSslConfiguration(QSslConfigurationPrivate *dd); QSharedDataPointer d; }; diff --git a/src/network/ssl/qsslconfiguration_p.h b/src/network/ssl/qsslconfiguration_p.h index 3e6e43361d..3acbdd5bef 100644 --- a/src/network/ssl/qsslconfiguration_p.h +++ b/src/network/ssl/qsslconfiguration_p.h @@ -84,11 +84,13 @@ public: peerVerifyMode(QSslSocket::AutoVerifyPeer), peerVerifyDepth(0), allowRootCertOnDemandLoading(true), + peerSessionShared(false), sslOptions(QSslConfigurationPrivate::defaultSslOptions) { } QSslCertificate peerCertificate; QList peerCertificateChain; + QSslCertificate localCertificate; QSslKey privateKey; @@ -100,6 +102,9 @@ public: QSslSocket::PeerVerifyMode peerVerifyMode; int peerVerifyDepth; bool allowRootCertOnDemandLoading; + bool peerSessionShared; + + Q_AUTOTEST_EXPORT static bool peerSessionWasShared(const QSslConfiguration &configuration); QSsl::SslOptions sslOptions; diff --git a/src/network/ssl/qsslcontext.cpp b/src/network/ssl/qsslcontext.cpp new file mode 100644 index 0000000000..4e0143253d --- /dev/null +++ b/src/network/ssl/qsslcontext.cpp @@ -0,0 +1,306 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtNetwork module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + + +#include +#include + +#include "private/qsslcontext_p.h" +#include "private/qsslsocket_p.h" +#include "private/qsslsocket_openssl_p.h" +#include "private/qsslsocket_openssl_symbols_p.h" + +QT_BEGIN_NAMESPACE + +// defined in qsslsocket_openssl.cpp: +extern int q_X509Callback(int ok, X509_STORE_CTX *ctx); +extern QString getErrorsFromOpenSsl(); + +QSslContext::QSslContext() + : ctx(0), + pkey(0), + session(0) +{ +} + +QSslContext::~QSslContext() +{ + if (ctx) + // This will decrement the reference count by 1 and free the context eventually when possible + q_SSL_CTX_free(ctx); + + if (pkey) + q_EVP_PKEY_free(pkey); + + if (session) + q_SSL_SESSION_free(session); +} + +QSslContext* QSslContext::fromConfiguration(QSslSocket::SslMode mode, const QSslConfiguration &configuration, bool allowRootCertOnDemandLoading) +{ + QSslContext *sslContext = new QSslContext(); + sslContext->sslConfiguration = configuration; + sslContext->errorCode = QSslError::NoError; + + bool client = (mode == QSslSocket::SslClientMode); + + bool reinitialized = false; +init_context: + switch (sslContext->sslConfiguration.protocol()) { + case QSsl::SslV2: +#ifndef OPENSSL_NO_SSL2 + sslContext->ctx = q_SSL_CTX_new(client ? q_SSLv2_client_method() : q_SSLv2_server_method()); +#else + sslContext->ctx = 0; // SSL 2 not supported by the system, but chosen deliberately -> error +#endif + break; + case QSsl::SslV3: + sslContext->ctx = q_SSL_CTX_new(client ? q_SSLv3_client_method() : q_SSLv3_server_method()); + break; + case QSsl::SecureProtocols: // SslV2 will be disabled below + case QSsl::TlsV1SslV3: // SslV2 will be disabled below + case QSsl::AnyProtocol: + default: + sslContext->ctx = q_SSL_CTX_new(client ? q_SSLv23_client_method() : q_SSLv23_server_method()); + break; + case QSsl::TlsV1_0: + sslContext->ctx = q_SSL_CTX_new(client ? q_TLSv1_client_method() : q_TLSv1_server_method()); + break; + case QSsl::TlsV1_1: +#if OPENSSL_VERSION_NUMBER >= 0x10001000L + sslContext->ctx = q_SSL_CTX_new(client ? q_TLSv1_1_client_method() : q_TLSv1_1_server_method()); +#else + sslContext->ctx = 0; // TLS 1.1 not supported by the system, but chosen deliberately -> error +#endif + break; + case QSsl::TlsV1_2: +#if OPENSSL_VERSION_NUMBER >= 0x10001000L + sslContext->ctx = q_SSL_CTX_new(client ? q_TLSv1_2_client_method() : q_TLSv1_2_server_method()); +#else + sslContext->ctx = 0; // TLS 1.2 not supported by the system, but chosen deliberately -> error +#endif + break; + } + if (!sslContext->ctx) { + // After stopping Flash 10 the SSL library looses its ciphers. Try re-adding them + // by re-initializing the library. + if (!reinitialized) { + reinitialized = true; + if (q_SSL_library_init() == 1) + goto init_context; + } + + sslContext->errorStr = QSslSocket::tr("Error creating SSL context (%1)").arg(QSslSocketBackendPrivate::getErrorsFromOpenSsl()); + sslContext->errorCode = QSslError::UnspecifiedError; + return sslContext; + } + + // Enable bug workarounds. + long options = QSslSocketBackendPrivate::setupOpenSslOptions(configuration.protocol(), configuration.d->sslOptions); + q_SSL_CTX_set_options(sslContext->ctx, options); + +#if OPENSSL_VERSION_NUMBER >= 0x10000000L + // Tell OpenSSL to release memory early + // http://www.openssl.org/docs/ssl/SSL_CTX_set_mode.html + if (q_SSLeay() >= 0x10000000L) + q_SSL_CTX_set_mode(sslContext->ctx, SSL_MODE_RELEASE_BUFFERS); +#endif + + // Initialize ciphers + QByteArray cipherString; + int first = true; + QList ciphers = sslContext->sslConfiguration.ciphers(); + if (ciphers.isEmpty()) + ciphers = QSslSocketPrivate::defaultCiphers(); + foreach (const QSslCipher &cipher, ciphers) { + if (first) + first = false; + else + cipherString.append(':'); + cipherString.append(cipher.name().toLatin1()); + } + + if (!q_SSL_CTX_set_cipher_list(sslContext->ctx, cipherString.data())) { + sslContext->errorStr = QSslSocket::tr("Invalid or empty cipher list (%1)").arg(QSslSocketBackendPrivate::getErrorsFromOpenSsl()); + sslContext->errorCode = QSslError::UnspecifiedError; + return sslContext; + } + + // Add all our CAs to this store. + QList expiredCerts; + foreach (const QSslCertificate &caCertificate, sslContext->sslConfiguration.caCertificates()) { + // add expired certs later, so that the + // valid ones are used before the expired ones + if (caCertificate.expiryDate() < QDateTime::currentDateTime()) { + expiredCerts.append(caCertificate); + } else { + q_X509_STORE_add_cert(sslContext->ctx->cert_store, (X509 *)caCertificate.handle()); + } + } + + bool addExpiredCerts = true; +#if defined(Q_OS_MAC) && (MAC_OS_X_VERSION_MAX_ALLOWED == MAC_OS_X_VERSION_10_5) + //On Leopard SSL does not work if we add the expired certificates. + if (QSysInfo::MacintoshVersion == QSysInfo::MV_10_5) + addExpiredCerts = false; +#endif + // now add the expired certs + if (addExpiredCerts) { + foreach (const QSslCertificate &caCertificate, expiredCerts) { + q_X509_STORE_add_cert(sslContext->ctx->cert_store, reinterpret_cast(caCertificate.handle())); + } + } + + if (QSslSocketPrivate::s_loadRootCertsOnDemand && allowRootCertOnDemandLoading) { + // tell OpenSSL the directories where to look up the root certs on demand + QList unixDirs = QSslSocketPrivate::unixRootCertDirectories(); + for (int a = 0; a < unixDirs.count(); ++a) + q_SSL_CTX_load_verify_locations(sslContext->ctx, 0, unixDirs.at(a).constData()); + } + + // Register a custom callback to get all verification errors. + X509_STORE_set_verify_cb_func(sslContext->ctx->cert_store, q_X509Callback); + + if (!sslContext->sslConfiguration.localCertificate().isNull()) { + // Require a private key as well. + if (sslContext->sslConfiguration.privateKey().isNull()) { + sslContext->errorStr = QSslSocket::tr("Cannot provide a certificate with no key, %1").arg(QSslSocketBackendPrivate::getErrorsFromOpenSsl()); + sslContext->errorCode = QSslError::UnspecifiedError; + return sslContext; + } + + // Load certificate + if (!q_SSL_CTX_use_certificate(sslContext->ctx, (X509 *)sslContext->sslConfiguration.localCertificate().handle())) { + sslContext->errorStr = QSslSocket::tr("Error loading local certificate, %1").arg(QSslSocketBackendPrivate::getErrorsFromOpenSsl()); + sslContext->errorCode = QSslError::UnspecifiedError; + return sslContext; + } + + if (configuration.d->privateKey.algorithm() == QSsl::Opaque) { + sslContext->pkey = reinterpret_cast(configuration.d->privateKey.handle()); + } else { + // Load private key + sslContext->pkey = q_EVP_PKEY_new(); + // before we were using EVP_PKEY_assign_R* functions and did not use EVP_PKEY_free. + // this lead to a memory leak. Now we use the *_set1_* functions which do not + // take ownership of the RSA/DSA key instance because the QSslKey already has ownership. + if (configuration.d->privateKey.algorithm() == QSsl::Rsa) + q_EVP_PKEY_set1_RSA(sslContext->pkey, reinterpret_cast(configuration.d->privateKey.handle())); + else + q_EVP_PKEY_set1_DSA(sslContext->pkey, reinterpret_cast(configuration.d->privateKey.handle())); + } + + if (!q_SSL_CTX_use_PrivateKey(sslContext->ctx, sslContext->pkey)) { + sslContext->errorStr = QSslSocket::tr("Error loading private key, %1").arg(QSslSocketBackendPrivate::getErrorsFromOpenSsl()); + sslContext->errorCode = QSslError::UnspecifiedError; + return sslContext; + } + if (configuration.d->privateKey.algorithm() == QSsl::Opaque) + sslContext->pkey = 0; // Don't free the private key, it belongs to QSslKey + + // Check if the certificate matches the private key. + if (!q_SSL_CTX_check_private_key(sslContext->ctx)) { + sslContext->errorStr = QSslSocket::tr("Private key does not certify public key, %1").arg(QSslSocketBackendPrivate::getErrorsFromOpenSsl()); + sslContext->errorCode = QSslError::UnspecifiedError; + return sslContext; + } + } + + // Initialize peer verification. + if (sslContext->sslConfiguration.peerVerifyMode() == QSslSocket::VerifyNone) { + q_SSL_CTX_set_verify(sslContext->ctx, SSL_VERIFY_NONE, 0); + } else { + q_SSL_CTX_set_verify(sslContext->ctx, SSL_VERIFY_PEER, q_X509Callback); + } + + // Set verification depth. + if (sslContext->sslConfiguration.peerVerifyDepth() != 0) + q_SSL_CTX_set_verify_depth(sslContext->ctx, sslContext->sslConfiguration.peerVerifyDepth()); + + return sslContext; +} + +// Needs to be deleted by caller +SSL* QSslContext::createSsl() +{ + SSL* ssl = q_SSL_new(ctx); + q_SSL_clear(ssl); + + if (session) { + // Try to resume the last session we cached + if (!q_SSL_set_session(ssl, session)) { + qWarning("could not set SSL session"); + q_SSL_SESSION_free(session); + session = 0; + } + } + return ssl; +} + +// We cache exactly one session here +bool QSslContext::cacheSession(SSL* ssl) +{ + // dont cache the same session again + if (session && session == q_SSL_get_session(ssl)) + return true; + + // decrease refcount of currently stored session + // (this might happen if there are several concurrent handshakes in flight) + if (session) + q_SSL_SESSION_free(session); + + // cache the session the caller gave us and increase reference count + session = q_SSL_get1_session(ssl); + return (session != NULL); + +} + +QSslError::SslError QSslContext::error() const +{ + return errorCode; +} + +QString QSslContext::errorString() const +{ + return errorStr; +} + +QT_END_NAMESPACE diff --git a/src/network/ssl/qsslcontext_p.h b/src/network/ssl/qsslcontext_p.h new file mode 100644 index 0000000000..c8578d349e --- /dev/null +++ b/src/network/ssl/qsslcontext_p.h @@ -0,0 +1,88 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtNetwork module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + + +#ifndef QSSLCONTEXT_H +#define QSSLCONTEXT_H + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +#ifndef QT_NO_SSL + +class QSslContextPrivate; + +class QSslContext +{ +public: + + ~QSslContext(); + + static QSslContext* fromConfiguration(QSslSocket::SslMode mode, const QSslConfiguration &configuration, + bool allowRootCertOnDemandLoading); + + QSslError::SslError error() const; + QString errorString() const; + + SSL* createSsl(); + bool cacheSession(SSL*); // should be called when handshake completed + +protected: + QSslContext(); + +private: + SSL_CTX* ctx; + EVP_PKEY *pkey; + SSL_SESSION *session; + QSslError::SslError errorCode; + QString errorStr; + QSslConfiguration sslConfiguration; +}; + +#endif // QT_NO_SSL + +QT_END_NAMESPACE + +#endif // QSSLCONTEXT_H diff --git a/src/network/ssl/qsslsocket.cpp b/src/network/ssl/qsslsocket.cpp index cfc3c19bba..712ba7aa79 100644 --- a/src/network/ssl/qsslsocket.cpp +++ b/src/network/ssl/qsslsocket.cpp @@ -2406,6 +2406,23 @@ QList QSslSocketPrivate::unixRootCertDirectories() << "/opt/openssl/certs/"; // HP-UX } +/*! + \internal +*/ +void QSslSocketPrivate::checkSettingSslContext(QSslSocket* socket, QSharedPointer sslContext) +{ + if (socket->d_func()->sslContextPointer.isNull()) + socket->d_func()->sslContextPointer = sslContext; +} + +/*! + \internal +*/ +QSharedPointer QSslSocketPrivate::sslContext(QSslSocket *socket) +{ + return (socket) ? socket->d_func()->sslContextPointer : QSharedPointer(); +} + QT_END_NAMESPACE #include "moc_qsslsocket.cpp" diff --git a/src/network/ssl/qsslsocket_openssl.cpp b/src/network/ssl/qsslsocket_openssl.cpp index fd5e12fec3..894703acb7 100644 --- a/src/network/ssl/qsslsocket_openssl.cpp +++ b/src/network/ssl/qsslsocket_openssl.cpp @@ -141,6 +141,19 @@ private: }; Q_GLOBAL_STATIC(QOpenSslLocks, openssl_locks) +QString QSslSocketBackendPrivate::getErrorsFromOpenSsl() +{ + QString errorString; + unsigned long errNum; + while ((errNum = q_ERR_get_error())) { + if (! errorString.isEmpty()) + errorString.append(QLatin1String(", ")); + const char *error = q_ERR_error_string(errNum, NULL); + errorString.append(QString::fromLatin1(error)); // error is ascii according to man ERR_error_string + } + return errorString; +} + extern "C" { static void locking_function(int mode, int lockNumber, const char *, int) { @@ -160,8 +173,6 @@ static unsigned long id_function() QSslSocketBackendPrivate::QSslSocketBackendPrivate() : ssl(0), - ctx(0), - pkey(0), readBio(0), writeBio(0), session(0) @@ -225,7 +236,8 @@ struct QSslErrorList QList > errors; }; Q_GLOBAL_STATIC(QSslErrorList, _q_sslErrorList) -static int q_X509Callback(int ok, X509_STORE_CTX *ctx) + +int q_X509Callback(int ok, X509_STORE_CTX *ctx) { if (!ok) { // Store the error and at which depth the error was detected. @@ -296,191 +308,21 @@ bool QSslSocketBackendPrivate::initSslContext() { Q_Q(QSslSocket); - // Create and initialize SSL context. Accept SSLv2, SSLv3 and TLSv1_0. - bool client = (mode == QSslSocket::SslClientMode); - - bool reinitialized = false; - -init_context: - switch (configuration.protocol) { - case QSsl::SslV2: -#ifndef OPENSSL_NO_SSL2 - ctx = q_SSL_CTX_new(client ? q_SSLv2_client_method() : q_SSLv2_server_method()); -#else - ctx = 0; // SSL 2 not supported by the system, but chosen deliberately -> error -#endif - break; - case QSsl::SslV3: - ctx = q_SSL_CTX_new(client ? q_SSLv3_client_method() : q_SSLv3_server_method()); - break; - case QSsl::SecureProtocols: // SslV2 will be disabled below - case QSsl::TlsV1SslV3: // SslV2 will be disabled below - case QSsl::AnyProtocol: - default: - ctx = q_SSL_CTX_new(client ? q_SSLv23_client_method() : q_SSLv23_server_method()); - break; - case QSsl::TlsV1_0: - ctx = q_SSL_CTX_new(client ? q_TLSv1_client_method() : q_TLSv1_server_method()); - break; - case QSsl::TlsV1_1: -#if OPENSSL_VERSION_NUMBER >= 0x10001000L - ctx = q_SSL_CTX_new(client ? q_TLSv1_1_client_method() : q_TLSv1_1_server_method()); -#else - ctx = 0; // TLS 1.1 not supported by the system, but chosen deliberately -> error -#endif - break; - case QSsl::TlsV1_2: -#if OPENSSL_VERSION_NUMBER >= 0x10001000L - ctx = q_SSL_CTX_new(client ? q_TLSv1_2_client_method() : q_TLSv1_2_server_method()); -#else - ctx = 0; // TLS 1.2 not supported by the system, but chosen deliberately -> error -#endif - break; - } - if (!ctx) { - // After stopping Flash 10 the SSL library looses its ciphers. Try re-adding them - // by re-initializing the library. - if (!reinitialized) { - reinitialized = true; - if (q_SSL_library_init() == 1) - goto init_context; - } - - q->setErrorString(QSslSocket::tr("Error creating SSL context (%1)").arg(getErrorsFromOpenSsl())); - q->setSocketError(QAbstractSocket::SslInternalError); - emit q->error(QAbstractSocket::SslInternalError); - return false; - } - - // Enable bug workarounds. - long options = setupOpenSslOptions(configuration.protocol, configuration.sslOptions); - q_SSL_CTX_set_options(ctx, options); - -#if OPENSSL_VERSION_NUMBER >= 0x10000000L - // Tell OpenSSL to release memory early - // http://www.openssl.org/docs/ssl/SSL_CTX_set_mode.html - if (q_SSLeay() >= 0x10000000L) - q_SSL_CTX_set_mode(ctx, SSL_MODE_RELEASE_BUFFERS); -#endif - - // Initialize ciphers - QByteArray cipherString; - int first = true; - QList ciphers = configuration.ciphers; - if (ciphers.isEmpty()) - ciphers = defaultCiphers(); - foreach (const QSslCipher &cipher, ciphers) { - if (first) - first = false; - else - cipherString.append(':'); - cipherString.append(cipher.name().toLatin1()); - } + // If no external context was set (e.g. bei QHttpNetworkConnection) we will create a default context + if (!sslContextPointer) + sslContextPointer = QSharedPointer( + QSslContext::fromConfiguration(mode, QSslConfiguration(&configuration), allowRootCertOnDemandLoading)); - if (!q_SSL_CTX_set_cipher_list(ctx, cipherString.data())) { - q->setErrorString(QSslSocket::tr("Invalid or empty cipher list (%1)").arg(getErrorsFromOpenSsl())); + if (sslContextPointer->error() != QSslError::NoError) { + q->setErrorString(sslContextPointer->errorString()); q->setSocketError(QAbstractSocket::SslInvalidUserDataError); emit q->error(QAbstractSocket::SslInvalidUserDataError); + sslContextPointer.clear(); // deletes the QSslContext return false; } - // Add all our CAs to this store. - QList expiredCerts; - foreach (const QSslCertificate &caCertificate, q->caCertificates()) { - // add expired certs later, so that the - // valid ones are used before the expired ones - if (caCertificate.expiryDate() < QDateTime::currentDateTime()) { - expiredCerts.append(caCertificate); - } else { - q_X509_STORE_add_cert(ctx->cert_store, reinterpret_cast(caCertificate.handle())); - } - } - - bool addExpiredCerts = true; -#if defined(Q_OS_MAC) && (MAC_OS_X_VERSION_MAX_ALLOWED == MAC_OS_X_VERSION_10_5) - //On Leopard SSL does not work if we add the expired certificates. - if (QSysInfo::MacintoshVersion == QSysInfo::MV_10_5) - addExpiredCerts = false; -#endif - // now add the expired certs - if (addExpiredCerts) { - foreach (const QSslCertificate &caCertificate, expiredCerts) { - q_X509_STORE_add_cert(ctx->cert_store, reinterpret_cast(caCertificate.handle())); - } - } - - if (s_loadRootCertsOnDemand && allowRootCertOnDemandLoading) { - // tell OpenSSL the directories where to look up the root certs on demand - QList unixDirs = unixRootCertDirectories(); - for (int a = 0; a < unixDirs.count(); ++a) - q_SSL_CTX_load_verify_locations(ctx, 0, unixDirs.at(a).constData()); - } - - // Register a custom callback to get all verification errors. - X509_STORE_set_verify_cb_func(ctx->cert_store, q_X509Callback); - - if (!configuration.localCertificate.isNull()) { - // Require a private key as well. - if (configuration.privateKey.isNull()) { - q->setErrorString(QSslSocket::tr("Cannot provide a certificate with no key, %1").arg(getErrorsFromOpenSsl())); - q->setSocketError(QAbstractSocket::SslInvalidUserDataError); - emit q->error(QAbstractSocket::SslInvalidUserDataError); - return false; - } - - // Load certificate - if (!q_SSL_CTX_use_certificate(ctx, reinterpret_cast(configuration.localCertificate.handle()))) { - q->setErrorString(QSslSocket::tr("Error loading local certificate, %1").arg(getErrorsFromOpenSsl())); - q->setSocketError(QAbstractSocket::SslInternalError); - emit q->error(QAbstractSocket::SslInternalError); - return false; - } - - if (configuration.privateKey.algorithm() == QSsl::Opaque) { - pkey = reinterpret_cast(configuration.privateKey.handle()); - } else { - // Load private key - pkey = q_EVP_PKEY_new(); - // before we were using EVP_PKEY_assign_R* functions and did not use EVP_PKEY_free. - // this lead to a memory leak. Now we use the *_set1_* functions which do not - // take ownership of the RSA/DSA key instance because the QSslKey already has ownership. - if (configuration.privateKey.algorithm() == QSsl::Rsa) - q_EVP_PKEY_set1_RSA(pkey, reinterpret_cast(configuration.privateKey.handle())); - else - q_EVP_PKEY_set1_DSA(pkey, reinterpret_cast(configuration.privateKey.handle())); - } - - if (!q_SSL_CTX_use_PrivateKey(ctx, pkey)) { - q->setErrorString(QSslSocket::tr("Error loading private key, %1").arg(getErrorsFromOpenSsl())); - q->setSocketError(QAbstractSocket::SslInternalError); - emit q->error(QAbstractSocket::SslInternalError); - return false; - } - if (configuration.privateKey.algorithm() == QSsl::Opaque) - pkey = 0; // Don't free the private key, it belongs to QSslKey - - // Check if the certificate matches the private key. - if (!q_SSL_CTX_check_private_key(ctx)) { - q->setErrorString(QSslSocket::tr("Private key does not certify public key, %1").arg(getErrorsFromOpenSsl())); - q->setSocketError(QAbstractSocket::SslInvalidUserDataError); - emit q->error(QAbstractSocket::SslInvalidUserDataError); - return false; - } - } - - // Initialize peer verification. - if (configuration.peerVerifyMode == QSslSocket::VerifyNone) { - q_SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, 0); - } else { - q_SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, q_X509Callback); - } - - // Set verification depth. - if (configuration.peerVerifyDepth != 0) - q_SSL_CTX_set_verify_depth(ctx, configuration.peerVerifyDepth); - // Create and initialize SSL session - if (!(ssl = q_SSL_new(ctx))) { + if (!(ssl = sslContextPointer->createSsl())) { // ### Bad error code q->setErrorString(QSslSocket::tr("Error creating SSL session, %1").arg(getErrorsFromOpenSsl())); q->setSocketError(QAbstractSocket::SslInternalError); @@ -495,7 +337,7 @@ init_context: configuration.protocol == QSsl::TlsV1_2 || configuration.protocol == QSsl::SecureProtocols || configuration.protocol == QSsl::AnyProtocol) && - client && q_SSLeay() >= 0x00090806fL) { + mode == QSslSocket::SslClientMode && q_SSLeay() >= 0x00090806fL) { // Set server hostname on TLS extension. RFC4366 section 3.1 requires it in ACE format. QString tlsHostName = verificationPeerName.isEmpty() ? q->peerName() : verificationPeerName; if (tlsHostName.isEmpty()) @@ -512,7 +354,6 @@ init_context: #endif // Clear the session. - q_SSL_clear(ssl); errorList.clear(); // Initialize memory BIOs for encryption and decryption. @@ -542,14 +383,7 @@ void QSslSocketBackendPrivate::destroySslContext() q_SSL_free(ssl); ssl = 0; } - if (ctx) { - q_SSL_CTX_free(ctx); - ctx = 0; - } - if (pkey) { - q_EVP_PKEY_free(pkey); - pkey = 0; - } + sslContextPointer.clear(); } /*! @@ -1087,7 +921,7 @@ void QSslSocketBackendPrivate::transmit() break; } } while (ssl && readBytes > 0); - } while (ssl && ctx && transmitting); + } while (ssl && transmitting); } static QSslError _q_OpenSSL_to_QSslError(int errorCode, const QSslCertificate &cert) @@ -1293,7 +1127,6 @@ bool QSslSocketBackendPrivate::startHandshake() } } #endif - if (!checkSslErrors()) return false; } else { @@ -1517,7 +1350,7 @@ void QSslSocketBackendPrivate::disconnected() QSslCipher QSslSocketBackendPrivate::sessionCipher() const { - if (!ssl || !ctx) + if (!ssl) return QSslCipher(); #if OPENSSL_VERSION_NUMBER >= 0x10000000L // FIXME This is fairly evil, but needed to keep source level compatibility @@ -1537,6 +1370,15 @@ void QSslSocketBackendPrivate::continueHandshake() if (readBufferMaxSize) plainSocket->setReadBufferSize(readBufferMaxSize); + if (q_SSL_ctrl((ssl), SSL_CTRL_GET_SESSION_REUSED, 0, NULL)) + configuration.peerSessionShared = true; + + // Cache this SSL session inside the QSslContext + if (!(configuration.sslOptions & QSsl::SslOptionDisableSessionTickets)) { + if (!sslContextPointer->cacheSession(ssl)) + sslContextPointer.clear(); // we could not cache the session + } + connectionEncrypted = true; emit q->encrypted(); if (autoStartHandshake && pendingClose) { @@ -1556,19 +1398,6 @@ QList QSslSocketBackendPrivate::STACKOFX509_to_QSslCertificates return certificates; } -QString QSslSocketBackendPrivate::getErrorsFromOpenSsl() -{ - QString errorString; - unsigned long errNum; - while((errNum = q_ERR_get_error())) { - if (! errorString.isEmpty()) - errorString.append(QLatin1String(", ")); - const char *error = q_ERR_error_string(errNum, NULL); - errorString.append(QString::fromLatin1(error)); // error is ascii according to man ERR_error_string - } - return errorString; -} - bool QSslSocketBackendPrivate::isMatchingHostname(const QSslCertificate &cert, const QString &peerName) { QStringList commonNameList = cert.subjectInfo(QSslCertificate::CommonName); diff --git a/src/network/ssl/qsslsocket_openssl_p.h b/src/network/ssl/qsslsocket_openssl_p.h index 9acdcbd1c5..7d5fa0bd6d 100644 --- a/src/network/ssl/qsslsocket_openssl_p.h +++ b/src/network/ssl/qsslsocket_openssl_p.h @@ -61,7 +61,10 @@ #if defined(OCSP_RESPONSE) #undef OCSP_RESPONSE #endif +#if defined(X509_NAME) +#undef X509_NAME #endif +#endif // Q_OS_WIN #include #include @@ -101,8 +104,6 @@ public: bool initSslContext(); void destroySslContext(); SSL *ssl; - SSL_CTX *ctx; - EVP_PKEY *pkey; BIO *readBio; BIO *writeBio; SSL_SESSION *session; diff --git a/src/network/ssl/qsslsocket_openssl_symbols.cpp b/src/network/ssl/qsslsocket_openssl_symbols.cpp index 5829170320..82f475078c 100644 --- a/src/network/ssl/qsslsocket_openssl_symbols.cpp +++ b/src/network/ssl/qsslsocket_openssl_symbols.cpp @@ -231,6 +231,10 @@ DEFINEFUNC3(void, SSL_set_bio, SSL *a, a, BIO *b, b, BIO *c, c, return, DUMMYARG DEFINEFUNC(void, SSL_set_accept_state, SSL *a, a, return, DUMMYARG) DEFINEFUNC(void, SSL_set_connect_state, SSL *a, a, return, DUMMYARG) DEFINEFUNC(int, SSL_shutdown, SSL *a, a, return -1, return) +DEFINEFUNC2(int, SSL_set_session, SSL* to, to, SSL_SESSION *session, session, return -1, return) +DEFINEFUNC(void, SSL_SESSION_free, SSL_SESSION *ses, ses, return, DUMMYARG) +DEFINEFUNC(SSL_SESSION*, SSL_get1_session, const SSL *ssl, ssl, return 0, return) +DEFINEFUNC(SSL_SESSION*, SSL_get_session, const SSL *ssl, ssl, return 0, return) #if OPENSSL_VERSION_NUMBER >= 0x10000000L #ifndef OPENSSL_NO_SSL2 DEFINEFUNC(const SSL_METHOD *, SSLv2_client_method, DUMMYARG, DUMMYARG, return 0, return) @@ -685,6 +689,10 @@ bool q_resolveOpenSslSymbols() RESOLVEFUNC(SSL_set_bio) RESOLVEFUNC(SSL_set_connect_state) RESOLVEFUNC(SSL_shutdown) + RESOLVEFUNC(SSL_set_session) + RESOLVEFUNC(SSL_SESSION_free) + RESOLVEFUNC(SSL_get1_session) + RESOLVEFUNC(SSL_get_session) RESOLVEFUNC(SSL_write) #ifndef OPENSSL_NO_SSL2 RESOLVEFUNC(SSLv2_client_method) diff --git a/src/network/ssl/qsslsocket_openssl_symbols_p.h b/src/network/ssl/qsslsocket_openssl_symbols_p.h index 782725407e..b7daedcbb6 100644 --- a/src/network/ssl/qsslsocket_openssl_symbols_p.h +++ b/src/network/ssl/qsslsocket_openssl_symbols_p.h @@ -333,6 +333,10 @@ void q_SSL_set_bio(SSL *a, BIO *b, BIO *c); void q_SSL_set_accept_state(SSL *a); void q_SSL_set_connect_state(SSL *a); int q_SSL_shutdown(SSL *a); +int q_SSL_set_session(SSL *to, SSL_SESSION *session); +void q_SSL_SESSION_free(SSL_SESSION *ses); +SSL_SESSION *q_SSL_get1_session(const SSL *ssl); +SSL_SESSION *q_SSL_get_session(const SSL *ssl); #if OPENSSL_VERSION_NUMBER >= 0x10000000L const SSL_METHOD *q_SSLv2_client_method(); const SSL_METHOD *q_SSLv3_client_method(); diff --git a/src/network/ssl/qsslsocket_p.h b/src/network/ssl/qsslsocket_p.h index 851dec5840..72117353ac 100644 --- a/src/network/ssl/qsslsocket_p.h +++ b/src/network/ssl/qsslsocket_p.h @@ -59,6 +59,7 @@ #include #include "qsslkey.h" #include "qsslconfiguration_p.h" +#include #include @@ -114,6 +115,7 @@ public: QSslConfigurationPrivate configuration; QList sslErrors; + QSharedPointer sslContextPointer; // if set, this hostname is used for certificate validation instead of the hostname // that was used for connecting to. @@ -121,6 +123,8 @@ public: bool allowRootCertOnDemandLoading; + static bool s_loadRootCertsOnDemand; + static bool supportsSsl(); static long sslLibraryVersionNumber(); static QString sslLibraryVersionString(); @@ -155,6 +159,9 @@ public: void createPlainSocket(QIODevice::OpenMode openMode); static void pauseSocketNotifiers(QSslSocket*); static void resumeSocketNotifiers(QSslSocket*); + // ### The 2 methods below should be made member methods once the QSslContext class is made public + static void checkSettingSslContext(QSslSocket*, QSharedPointer); + static QSharedPointer sslContext(QSslSocket *socket); bool isPaused() const; void _q_connectedSlot(); void _q_hostFoundSlot(); @@ -170,6 +177,8 @@ public: virtual void _q_caRootLoaded(QSslCertificate,QSslCertificate) = 0; #endif + static QList unixRootCertDirectories(); // used also by QSslContext + virtual qint64 peek(char *data, qint64 maxSize); virtual QByteArray peek(qint64 maxSize); @@ -192,8 +201,6 @@ private: static bool s_loadedCiphersAndCerts; protected: bool verifyErrorsHaveBeenIgnored(); - static bool s_loadRootCertsOnDemand; - static QList unixRootCertDirectories(); bool paused; }; diff --git a/src/network/ssl/ssl.pri b/src/network/ssl/ssl.pri index 8ebb4b29af..f5afb43759 100644 --- a/src/network/ssl/ssl.pri +++ b/src/network/ssl/ssl.pri @@ -14,7 +14,8 @@ contains(QT_CONFIG, openssl) | contains(QT_CONFIG, openssl-linked) { ssl/qsslsocket_openssl_symbols_p.h \ ssl/qsslsocket_p.h \ ssl/qsslcertificateextension.h \ - ssl/qsslcertificateextension_p.h + ssl/qsslcertificateextension_p.h \ + ssl/qsslcontext_p.h SOURCES += ssl/qssl.cpp \ ssl/qsslcertificate.cpp \ ssl/qsslconfiguration.cpp \ @@ -24,7 +25,8 @@ contains(QT_CONFIG, openssl) | contains(QT_CONFIG, openssl-linked) { ssl/qsslsocket.cpp \ ssl/qsslsocket_openssl.cpp \ ssl/qsslsocket_openssl_symbols.cpp \ - ssl/qsslcertificateextension.cpp + ssl/qsslcertificateextension.cpp \ + ssl/qsslcontext.cpp # Add optional SSL libs # Static linking of OpenSSL with msvc: diff --git a/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp b/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp index 624ce5e800..075735295c 100644 --- a/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp +++ b/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp @@ -71,6 +71,9 @@ #ifndef QT_NO_SSL #include #include +#ifdef QT_BUILD_INTERNAL +#include +#endif #endif #ifndef QT_NO_BEARERMANAGEMENT #include @@ -173,6 +176,9 @@ public Q_SLOTS: void sslErrors(QNetworkReply*,const QList &); void storeSslConfiguration(); void ignoreSslErrorListSlot(QNetworkReply *reply, const QList &); +#ifdef QT_BUILD_INTERNAL + void sslSessionSharingHelperSlot(); +#endif #endif protected Q_SLOTS: @@ -357,6 +363,10 @@ private Q_SLOTS: void ignoreSslErrorsListWithSlot(); void sslConfiguration_data(); void sslConfiguration(); +#ifdef QT_BUILD_INTERNAL + void sslSessionSharing_data(); + void sslSessionSharing(); +#endif #endif void getAndThenDeleteObject_data(); @@ -5871,6 +5881,73 @@ void tst_QNetworkReply::sslConfiguration() QCOMPARE(reply->error(), expectedError); } +#ifdef QT_BUILD_INTERNAL + +void tst_QNetworkReply::sslSessionSharing_data() +{ + QTest::addColumn("sessionSharingEnabled"); + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void tst_QNetworkReply::sslSessionSharing() +{ + QString urlString("https://" + QtNetworkSettings::serverName() + "/qtest/mediumfile"); + QList replies; + + // warm up SSL session cache + QNetworkRequest warmupRequest(urlString); + QFETCH(bool, sessionSharingEnabled); + warmupRequest.setAttribute(QNetworkRequest::User, sessionSharingEnabled); // so we can read it from the slot + if (! sessionSharingEnabled) { + QSslConfiguration configuration(QSslConfiguration::defaultConfiguration()); + configuration.setSslOption(QSsl::SslOptionDisableSessionTickets, true); + warmupRequest.setSslConfiguration(configuration); + } + QNetworkReply *reply = manager.get(warmupRequest); + reply->ignoreSslErrors(); + connect(reply, SIGNAL(finished()), &QTestEventLoop::instance(), SLOT(exitLoop())); + QTestEventLoop::instance().enterLoop(20); + QVERIFY(!QTestEventLoop::instance().timeout()); + QCOMPARE(reply->error(), QNetworkReply::NoError); + reply->deleteLater(); + + // now send several requests at the same time, so we open more sockets and reuse the SSL session + for (int a = 0; a < 6; a++) { + QNetworkRequest request(warmupRequest); + replies.append(QNetworkReplyPtr(manager.get(request))); + connect(replies.at(a), SIGNAL(finished()), this, SLOT(sslSessionSharingHelperSlot())); + } + QTestEventLoop::instance().enterLoop(20); + QVERIFY(!QTestEventLoop::instance().timeout()); +} + +void tst_QNetworkReply::sslSessionSharingHelperSlot() +{ + static int count = 0; + + // check that SSL session sharing was used in at least one of the replies + static bool sslSessionSharingWasUsed = false; + QNetworkReply *reply = qobject_cast(sender()); + bool sslSessionSharingWasUsedInReply = QSslConfigurationPrivate::peerSessionWasShared(reply->sslConfiguration()); + if (sslSessionSharingWasUsedInReply) + sslSessionSharingWasUsed = true; + + QString urlQueryString = reply->url().query(); + QCOMPARE(reply->error(), QNetworkReply::NoError); + + count++; + + if (count == 6) { // all replies have finished + QTestEventLoop::instance().exitLoop(); + bool sessionSharingWasEnabled = reply->request().attribute(QNetworkRequest::User).toBool(); + QCOMPARE(sslSessionSharingWasUsed, sessionSharingWasEnabled); + count = 0; // reset for next row + sslSessionSharingWasUsed = false; // reset for next row + } +} + +#endif // QT_BUILD_INTERNAL #endif // QT_NO_SSL void tst_QNetworkReply::getAndThenDeleteObject_data() -- cgit v1.2.3