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:
parent
2116f9904a
commit
3be197881f
@ -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.
|
||||
|
@ -96,7 +96,8 @@ namespace QSsl {
|
||||
SslOptionDisableCompression = 0x04,
|
||||
SslOptionDisableServerNameIndication = 0x08,
|
||||
SslOptionDisableLegacyRenegotiation = 0x10,
|
||||
SslOptionDisableSessionSharing = 0x20
|
||||
SslOptionDisableSessionSharing = 0x20,
|
||||
SslOptionDisableSessionPersistence = 0x40
|
||||
};
|
||||
Q_DECLARE_FLAGS(SslOptions, SslOption)
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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(),
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user