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 <rich@kde.org>
This commit is contained in:
Peter Hartmann 2013-04-30 14:48:22 +02:00 committed by The Qt Project
parent 2116f9904a
commit 3be197881f
12 changed files with 200 additions and 9 deletions

View File

@ -163,12 +163,18 @@ QT_BEGIN_NAMESPACE
possibility that an attacker could inject plaintext into the SSL session. possibility that an attacker could inject plaintext into the SSL session.
\value SslOptionDisableSessionSharing Disables SSL session sharing via \value SslOptionDisableSessionSharing Disables SSL session sharing via
the session ID handshake attribute. 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 By default, SslOptionDisableEmptyFragments is turned on since this causes
problems with a large number of servers. SslOptionDisableLegacyRenegotiation problems with a large number of servers. SslOptionDisableLegacyRenegotiation
is also turned on, since it introduces a security risk. is also turned on, since it introduces a security risk.
SslOptionDisableCompression is turned on to prevent the attack publicised by 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 Note: Availability of above options depends on the version of the SSL
backend in use. backend in use.

View File

@ -96,7 +96,8 @@ namespace QSsl {
SslOptionDisableCompression = 0x04, SslOptionDisableCompression = 0x04,
SslOptionDisableServerNameIndication = 0x08, SslOptionDisableServerNameIndication = 0x08,
SslOptionDisableLegacyRenegotiation = 0x10, SslOptionDisableLegacyRenegotiation = 0x10,
SslOptionDisableSessionSharing = 0x20 SslOptionDisableSessionSharing = 0x20,
SslOptionDisableSessionPersistence = 0x40
}; };
Q_DECLARE_FLAGS(SslOptions, SslOption) Q_DECLARE_FLAGS(SslOptions, SslOption)
} }

View File

@ -49,7 +49,8 @@ QT_BEGIN_NAMESPACE
const QSsl::SslOptions QSslConfigurationPrivate::defaultSslOptions = QSsl::SslOptionDisableEmptyFragments const QSsl::SslOptions QSslConfigurationPrivate::defaultSslOptions = QSsl::SslOptionDisableEmptyFragments
|QSsl::SslOptionDisableLegacyRenegotiation |QSsl::SslOptionDisableLegacyRenegotiation
|QSsl::SslOptionDisableCompression; |QSsl::SslOptionDisableCompression
|QSsl::SslOptionDisableSessionPersistence;
/*! /*!
\class QSslConfiguration \class QSslConfiguration
@ -182,7 +183,9 @@ bool QSslConfiguration::operator==(const QSslConfiguration &other) const
d->peerVerifyMode == other.d->peerVerifyMode && d->peerVerifyMode == other.d->peerVerifyMode &&
d->peerVerifyDepth == other.d->peerVerifyDepth && d->peerVerifyDepth == other.d->peerVerifyDepth &&
d->allowRootCertOnDemandLoading == other.d->allowRootCertOnDemandLoading && 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->privateKey.isNull() &&
d->peerCertificate.isNull() && d->peerCertificate.isNull() &&
d->peerCertificateChain.count() == 0 && 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; 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 Returns the default SSL configuration to be used in new SSL
connections. connections.

View File

@ -124,6 +124,10 @@ public:
void setSslOption(QSsl::SslOption option, bool on); void setSslOption(QSsl::SslOption option, bool on);
bool testSslOption(QSsl::SslOption option) const; bool testSslOption(QSsl::SslOption option) const;
QByteArray session() const;
void setSession(const QByteArray &session);
int sessionTicketLifeTimeHint() const;
static QSslConfiguration defaultConfiguration(); static QSslConfiguration defaultConfiguration();
static void setDefaultConfiguration(const QSslConfiguration &configuration); static void setDefaultConfiguration(const QSslConfiguration &configuration);

View File

@ -85,7 +85,8 @@ public:
peerVerifyDepth(0), peerVerifyDepth(0),
allowRootCertOnDemandLoading(true), allowRootCertOnDemandLoading(true),
peerSessionShared(false), peerSessionShared(false),
sslOptions(QSslConfigurationPrivate::defaultSslOptions) sslOptions(QSslConfigurationPrivate::defaultSslOptions),
sslSessionTicketLifeTimeHint(-1)
{ } { }
QSslCertificate peerCertificate; QSslCertificate peerCertificate;
@ -110,6 +111,9 @@ public:
Q_AUTOTEST_EXPORT static const QSsl::SslOptions defaultSslOptions; Q_AUTOTEST_EXPORT static const QSsl::SslOptions defaultSslOptions;
QByteArray sslSession;
int sslSessionTicketLifeTimeHint;
// in qsslsocket.cpp: // in qsslsocket.cpp:
static QSslConfiguration defaultConfiguration(); static QSslConfiguration defaultConfiguration();
static void setDefaultConfiguration(const QSslConfiguration &configuration); static void setDefaultConfiguration(const QSslConfiguration &configuration);

View File

@ -57,7 +57,8 @@ extern QString getErrorsFromOpenSsl();
QSslContext::QSslContext() QSslContext::QSslContext()
: ctx(0), : ctx(0),
pkey(0), pkey(0),
session(0) session(0),
m_sessionTicketLifeTimeHint(-1)
{ {
} }
@ -258,6 +259,10 @@ init_context:
if (sslContext->sslConfiguration.peerVerifyDepth() != 0) if (sslContext->sslConfiguration.peerVerifyDepth() != 0)
q_SSL_CTX_set_verify_depth(sslContext->ctx, sslContext->sslConfiguration.peerVerifyDepth()); 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; return sslContext;
} }
@ -267,6 +272,12 @@ SSL* QSslContext::createSsl()
SSL* ssl = q_SSL_new(ctx); SSL* ssl = q_SSL_new(ctx);
q_SSL_clear(ssl); 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) { if (session) {
// Try to resume the last session we cached // Try to resume the last session we cached
if (!q_SSL_set_session(ssl, session)) { 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 // cache the session the caller gave us and increase reference count
session = q_SSL_get1_session(ssl); 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 QSslError::SslError QSslContext::error() const

View File

@ -69,6 +69,9 @@ public:
SSL* createSsl(); SSL* createSsl();
bool cacheSession(SSL*); // should be called when handshake completed bool cacheSession(SSL*); // should be called when handshake completed
QByteArray sessionASN1() const;
void setSessionASN1(const QByteArray &sessionASN1);
int sessionTicketLifeTimeHint() const;
protected: protected:
QSslContext(); QSslContext();
@ -76,6 +79,8 @@ private:
SSL_CTX* ctx; SSL_CTX* ctx;
EVP_PKEY *pkey; EVP_PKEY *pkey;
SSL_SESSION *session; SSL_SESSION *session;
QByteArray m_sessionASN1;
int m_sessionTicketLifeTimeHint;
QSslError::SslError errorCode; QSslError::SslError errorCode;
QString errorStr; QString errorStr;
QSslConfiguration sslConfiguration; QSslConfiguration sslConfiguration;

View File

@ -903,6 +903,8 @@ void QSslSocket::setSslConfiguration(const QSslConfiguration &configuration)
d->configuration.peerVerifyMode = configuration.peerVerifyMode(); d->configuration.peerVerifyMode = configuration.peerVerifyMode();
d->configuration.protocol = configuration.protocol(); d->configuration.protocol = configuration.protocol();
d->configuration.sslOptions = configuration.d->sslOptions; 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 // if the CA certificates were set explicitly (either via
// QSslConfiguration::setCaCertificates() or QSslSocket::setCaCertificates(), // QSslConfiguration::setCaCertificates() or QSslSocket::setCaCertificates(),

View File

@ -1450,8 +1450,16 @@ void QSslSocketBackendPrivate::continueHandshake()
// Cache this SSL session inside the QSslContext // Cache this SSL session inside the QSslContext
if (!(configuration.sslOptions & QSsl::SslOptionDisableSessionSharing)) { if (!(configuration.sslOptions & QSsl::SslOptionDisableSessionSharing)) {
if (!sslContextPointer->cacheSession(ssl)) if (!sslContextPointer->cacheSession(ssl)) {
sslContextPointer.clear(); // we could not cache the session 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; connectionEncrypted = true;

View File

@ -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) 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(long, SSLeay, void, DUMMYARG, return 0, return)
DEFINEFUNC(const char *, SSLeay_version, int a, a, 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) \ #define RESOLVEFUNC(func) \
if (!(_q_##func = _q_PTR_##func(libs.first->resolve(#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(SSL_CTX_load_verify_locations)
RESOLVEFUNC(SSLeay) RESOLVEFUNC(SSLeay)
RESOLVEFUNC(SSLeay_version) RESOLVEFUNC(SSLeay_version)
RESOLVEFUNC(i2d_SSL_SESSION)
RESOLVEFUNC(d2i_SSL_SESSION)
symbolsResolved = true; symbolsResolved = true;
delete libs.first; delete libs.first;

View File

@ -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); int q_SSL_CTX_load_verify_locations(SSL_CTX *ctx, const char *CAfile, const char *CApath);
long q_SSLeay(); long q_SSLeay();
const char *q_SSLeay_version(int type); 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 // Helper function
class QDateTime; class QDateTime;

View File

@ -367,6 +367,8 @@ private Q_SLOTS:
#ifdef QT_BUILD_INTERNAL #ifdef QT_BUILD_INTERNAL
void sslSessionSharing_data(); void sslSessionSharing_data();
void sslSessionSharing(); void sslSessionSharing();
void sslSessionSharingFromPersistentSession_data();
void sslSessionSharingFromPersistentSession();
#endif #endif
#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_BUILD_INTERNAL
#endif // QT_NO_SSL #endif // QT_NO_SSL