From 3be197881f100d1c3c8f3ce00501d7a32eb51119 Mon Sep 17 00:00:00 2001 From: Peter Hartmann Date: Tue, 30 Apr 2013 14:48:22 +0200 Subject: 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 Change-Id: I10255932dcd528ee1231538cb72b52b97f9f4a3c Reviewed-by: Richard J. Moore --- src/network/ssl/qssl.cpp | 8 +++- src/network/ssl/qssl.h | 3 +- src/network/ssl/qsslconfiguration.cpp | 65 ++++++++++++++++++++++++-- src/network/ssl/qsslconfiguration.h | 4 ++ src/network/ssl/qsslconfiguration_p.h | 6 ++- src/network/ssl/qsslcontext.cpp | 41 +++++++++++++++- src/network/ssl/qsslcontext_p.h | 5 ++ src/network/ssl/qsslsocket.cpp | 2 + src/network/ssl/qsslsocket_openssl.cpp | 10 +++- src/network/ssl/qsslsocket_openssl_symbols.cpp | 4 ++ src/network/ssl/qsslsocket_openssl_symbols_p.h | 2 + 11 files changed, 141 insertions(+), 9 deletions(-) (limited to 'src/network') 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); } /*! @@ -593,6 +598,60 @@ bool QSslConfiguration::testSslOption(QSsl::SslOption option) const return d->sslOptions & option; } +/*! + \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(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(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; -- cgit v1.2.3