persisting and re-using SSL sessions in Qt apps
Many Qt applications use the network in one way or another; typical examples are a Facebook or a Twitter app, which load data from a Web service at start up, and then display the data in e.g. a QML list view. This post explains how you can cut off some of the loading time at start up and display your content faster by saving one network round trip (~ 200 milliseconds on Wifi).
Most Web services offer their API via an SSL endpoint like https://graph.facebook.com or https://api.twitter.com. This means that connecting to the server usually involves one DNS round trip for the DNS lookup, one TCP round trip for the TCP handshake and 2 TCP round trips for the SSL handshake, and one TCP round trip for the HTTP request and reply. This makes 5 network round trips in total; assuming 200 milliseconds for a round trip on a Wifi device, this makes it a lower boundary of 1 second before a HTTPS request has finished.
Qt 5.2 comes with a new feature to persist and re-use SSL sessions via TLS session tickets, so that 1 round trip of the SSL handshake can be saved, given the server supports this feature (and most servers like Facebook, Twitter, Google etc. do) and the session is still fresh (life time hint examples: graph.facebook.com: almost one day, api.twitter.com: 4 hours). Note that this comes in addition to re-using SSL sessions from servers that have been connected to earlier, which is already part of Qt 5.1.
API
A few methods were added in Qt 5.2 to allow an app developer to persist and re-use an SSL session:
QSsl::SslOptionDisableSessionPersistence
QByteArray QSslConfiguration::sessionTicket() const
void QSslConfiguration::setSessionTicket(
const QByteArray &sessionTicket)
int QSslConfiguration::sessionTicketLifeTimeHint() const
Note that TLS session tickets need to be enabled explicitly by setting the SslOptionDisableSessionPersistence option to "false". The session() method lets you retrieve the session sent by the server (if there is one), while the setSesion() method lets you resume one that you have persisted earlier. Usually sessions come with a life time hint suggesting when the session should expire, which can be queried through sessionTicketLifeTimeHint(). Important: When persisting SSL sessions to a form of persistent storage, make sure that nobody else has read or write access to that location.
Example
step 0: Code skeleton
The code example below shows a program that issues a HTTPS request to a server, and does not re-use SSL sessions yet.
#include <QtCore> #include <QtNetwork> class SslSessionPersistence : public QObject { Q_OBJECT public: SslSessionPersistence(QObject *parent = 0); public slots: void replyFinished(QNetworkReply *reply); void sendRequest(const QUrl &url); private: QNetworkAccessManager m_manager; QByteArray m_storedSession; QElapsedTimer m_timer; }; SslSessionPersistence::SslSessionPersistence(QObject *parent)
: QObject(parent) { connect(&m_manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(replyFinished(QNetworkReply*))); const QUrl url("https://api.twitter.com"); QMetaObject::invokeMethod(this,
"sendRequest", Qt::QueuedConnection,
Q_ARG(QUrl, url)); } void SslSessionPersistence::sendRequest(const QUrl &url) { QNetworkRequest request(url); m_timer.start(); m_manager.get(request); } void SslSessionPersistence::replyFinished(QNetworkReply *reply) { qDebug() << "reply finished and took"
<< m_timer.elapsed() << "ms."; QCoreApplication::quit(); } int main(int argc, char **argv) { QCoreApplication app(argc, argv); SslSessionPersistence sslSessionPersistence; return app.exec(); } #include "main.moc" step 1: persisting the session ticket and re-using it The snippet below adds code to persist the session to disk if the server sent one, and re-use it upon sending a new request. Also note that all access to the session file is restricted to the owner of the file for security reasons. Newly added lines are marked with green background. #include <QtCore> #include <QtNetwork> class SslSessionPersistence : public QObject { Q_OBJECT public: SslSessionPersistence(QObject *parent = 0); public slots: void replyFinished(QNetworkReply *reply); void sendRequest(const QUrl &url); private: QNetworkAccessManager m_manager; QByteArray m_storedSession; QElapsedTimer m_timer; }; SslSessionPersistence::SslSessionPersistence(QObject *parent)
: QObject(parent) { connect(&m_manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(replyFinished(QNetworkReply*))); const QUrl url("https://api.twitter.com"); QMetaObject::invokeMethod(this,
"sendRequest", Qt::QueuedConnection,
Q_ARG(QUrl, url)); } void SslSessionPersistence::sendRequest(const QUrl &url) { QNetworkRequest request(url); QSslConfiguration sslConfiguration = request.sslConfiguration(); // storing the session needs to be enabled explicitly sslConfiguration.setSslOption(
QSsl::SslOptionDisableSessionPersistence, false); // check whether we have a fresh session on disk QString fileName =
QString::fromLatin1("ssl-session-").append(url.host()); QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { qWarning("could not open ssl session file for reading," " falling back to full handshake"); } else { m_storedSession = file.readAll(); file.close(); qDebug("found fresh SSL session in store,"
"trying to re-use it..."); sslConfiguration.setSessionTicket(m_storedSession); } request.setSslConfiguration(sslConfiguration); m_timer.start(); m_manager.get(request); } void SslSessionPersistence::replyFinished(QNetworkReply *reply) { qDebug() << "reply finished and took"
<< m_timer.elapsed() << "ms."; const QSslConfiguration sslConfiguration
= reply->sslConfiguration(); QByteArray usedSession = reply->sslConfiguration()
.sessionTicket(); if (usedSession.size() > 0) { if (usedSession == m_storedSession) { qDebug("SSL session was re-used, nothing to do."); } else { qDebug("server sent a new SSL session," " updating SSL session file..."); // store the session to disk QString fileName =
QString::fromLatin1("ssl-session-").append( reply->url().host()); QFile file(fileName); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qWarning("could not open ssl session"
"file for writing"); } else { QFile::Permissions permissions =
(QFile::ReadOwner | QFile::WriteOwner); if (!file.setPermissions(permissions)) { qWarning("could not restrict file permissions," " not saving session"); } else { if (!file.write(usedSession)) qWarning("could not write session"
"ticket to file"); file.close(); } } } } QCoreApplication::quit(); } int main(int argc, char **argv) { QCoreApplication app(argc, argv); SslSessionPersistence sslSessionPersistence; return app.exec(); } #include "main.moc" step 2: respecting the life time hint The last step now is to respect the life time hint sent by the server, and discard the session once it has expired. The example below is assuming a maximum life time of 24 hours in case the server did not send a life time hint. Again, additions are marked with green background. #include <QtCore> #include <QtNetwork> class SslSessionPersistence : public QObject { Q_OBJECT public: SslSessionPersistence(QObject *parent = 0); public slots: void replyFinished(QNetworkReply *reply); void sendRequest(const QUrl &url); private: QNetworkAccessManager m_manager; QByteArray m_storedSession; QElapsedTimer m_timer; }; SslSessionPersistence::SslSessionPersistence(QObject *parent)
: QObject(parent) { connect(&m_manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(replyFinished(QNetworkReply*))); const QUrl url("https://api.twitter.com"); QMetaObject::invokeMethod(this,
"sendRequest", Qt::QueuedConnection,
Q_ARG(QUrl, url)); } void SslSessionPersistence::sendRequest(const QUrl &url) { QNetworkRequest request(url); QSslConfiguration sslConfiguration = request.sslConfiguration(); // storing the session needs to be enabled explicitly sslConfiguration.setSslOption(
QSsl::SslOptionDisableSessionPersistence, false); // check whether we have a fresh session on disk QString fileName =
QString::fromLatin1("ssl-session-").append(url.host()); QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { qWarning("could not open ssl session file for reading," " falling back to full handshake"); } else { QString expirationDateString =
QString::fromUtf8(file.readLine()); QDateTime expirationDate =
QDateTime::fromString(expirationDateString); m_storedSession = file.readAll(); file.close(); if (expirationDate < QDateTime::currentDateTime()) { qWarning("SSL session has expired, deleting session file" " and falling back to full handshake"); if (!file.remove()) qWarning("could not remove session file"); } else { qDebug("found fresh SSL session in store,
trying to re-use it..."); sslConfiguration.setSessionTicket(m_storedSession); } } request.setSslConfiguration(sslConfiguration); m_timer.start(); m_manager.get(request); } void SslSessionPersistence::replyFinished(QNetworkReply *reply) { qDebug() << "reply finished and took"
<< m_timer.elapsed() << "ms."; const QSslConfiguration sslConfiguration =
reply->sslConfiguration(); QByteArray usedSession = reply->sslConfiguration()
.sessionTicket(); if (usedSession.size() > 0) { if (usedSession == m_storedSession) { qDebug("SSL session was re-used, nothing to do."); } else { qDebug("server sent a new SSL session," " updating SSL session file..."); int lifeTimeHint =
sslConfiguration.sessionTicketLifeTimeHint(); if (lifeTimeHint <= 0) { qDebug("no life time hint given," " falling back to life time hint of 1 day."); lifeTimeHint = 24 * 60 * 60; } // calculate the expiration date and store it with the session QDateTime sessionExpirationDate =
QDateTime::currentDateTime().addSecs(lifeTimeHint); // store the session to disk QString fileName =
QString::fromLatin1("ssl-session-").append( reply->url().host()); QFile file(fileName); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qWarning("could not open ssl session"
"file for writing"); } else { QFile::Permissions permissions =
(QFile::ReadOwner | QFile::WriteOwner); if (!file.setPermissions(permissions)) { qWarning("could not restrict file permissions," " not saving session"); } else { if (!file.write(
sessionExpirationDate.toString().toUtf8() .append('\n'))) qWarning("could not write session"
"expiration date"); if (!file.write(usedSession)) qWarning("could not write session"
"ticket to file"); file.close(); } } } } QCoreApplication::quit(); } int main(int argc, char **argv) { QCoreApplication app(argc, argv); SslSessionPersistence sslSessionPersistence; return app.exec(); } #include "main.moc" Summary The above code snippet can be used to persist and re-use SSL sessions in your Qt app, making it display Web content faster.