Skip to content

Improving SSL Security with Certificate Change Notification

Sunday, 22 May 2011  |  rich

Improving the security of SSL is a hot topic these days, and trust in certifcate authorities is rightly at an all time low. One way of improving the situation that doesn't rely on believing that a 3rd party will actually do their job properly is to notify users when the certificate for a site changes. There are already extensions for some browsers that offer this facility, so I sat down today to write a proof of concept that looked at how this implemented in Qt.

What I've implemented is a class that can simply be plugged into QSslSocket based code to give the user a warning if the certificate for a site has changed since their last visit. The monitor class can keep track of many sockets and the cache is persistent. The idea is that the application tells the certificate monitor about the SSL socket, so that it becomes monitored by calling the addSocket() method, this method simply adds the socket to a QSignalMapper:

void SslCertificateMonitor::addSocket(QSslSocket *socket)
{
    d->mapper->setMapping(socket, socket);
    connect(socket, SIGNAL(encrypted()), d->mapper, SLOT(map()));
}

As you can see, we're connecting to the encrypted() signal of the socket in order to ensure that we get informed as soon as the SSL handshake is complete, and before any sensitive data (like cookies) are transmitted to the client. Unfortunately, this is also the cause of one of the problems of the implementation with the current Qt APIs as I'll describe later.

The core of the implementation is the socketReady() method which is called whenever the encrypted() signal for a socket we're monitoring is emitted. We'll take a look at the method a chunk at a time:

void SslCertificateMonitor::socketReady(QObject *sockobj)
{
    QSslSocket *sock = qobject_cast<QSslSocket *>(sockobj);
    if (!sock)
        return;

    QString peerName = sock->peerName();
    if (peerName == d->lastPeerAccepted)
        return; // Fast path for most recently accepted certificate

Since we're using QSignalMapper to let us know which socket we're dealing with in any given call, we first cast the QObject pointer we're given down to a QSslSocket. This means we can now call all the SSL functions. First we extract the name of the 'peer' (this is a fancy name for the site we're connecting to). The next two lines are actually part of an optimisation - since we'll often get repeated calls for the same site, I've added a simple single-entry cache that lets us quickly handle two requests that follow one after the other. A more complete implementation of this concept would probably use a slightly larger cache. If we've just accepted the site, then we know that there's no need to go through the full set of checks again, so we simply approve it immediately.

The next step is to check that the cache itself exists, and if not then create it. In this example, the cache is simply a directory that contains a file named after the host being visited. In the file, we store a cryptographic hash of the certificate that was used.

    if (!hasCertificateCache()) {
        bool ok = createCertificateCache();
        if (!ok)
            return;
    }

Finally, we get to the real meat of the class - the algorithm that determines if we should warn the user. This checks if we have a cached value, and if so checks for differences. If the certificate has changed then the signal certificateWarning() is emitted.

    QSslCertificate certificate = sock->peerCertificate();

    // Have we been here before?
    if (hasCachedCertificate(peerName)) {
        if (isMatchingCertificate(peerName, certificate)) {
            d->lastPeerAccepted = peerName;
            return; // All is well
        }

        // Cert has changed
        d->acceptCurrent = false;
        QString message = tr("The certificate for %1 has changed since you previously visited, " \
                             "it could be that someone is trying to intercept your communication.");
        message = message.arg(peerName);
        emit certificateWarning(peerName, message);
    }
    else {
        // The certificate is new. We don't show anything to user because then
        // we're simply training them to click through our warning message without
        // thinking.
        d->acceptCurrent = true;
    }

People using the class need to connect to the certificateWarning() signal, and if they wish the connection to be approved should call the acceptCertificate() method. This operates in a similar way to the QSslCertificate ignoreSslErrors() method, ie. expecting the client to use a nested event loop (such as a modal dialog) to interact with the user. If the acceptCertificate() method is called then the value of acceptCurrent will be set true. The value of this variable governs what we do next:

    // If the user has chosen to accept the certificate or the certficate is new
    // then we store the updated entry.
    if (d->acceptCurrent) {
        d->lastPeerAccepted = peerName;
        addCertificate(peerName, certificate);
    }
    else {
        // Certficate has been considered dangerous by the user
        sock->abort();
    }

The code above is the core of the entire implementation, but we should take a look at the internals of two of the functions it uses: the method for writing an entry into the cache, and the method for checking for changes. Both of the methods are very simple, adding a certificate to the cache is simply a matter of writing the certificate digest to a file:

void SslCertificateMonitor::addCertificate(const QString &peerName, const QSslCertificate &cert)
{
    QString cacheEntry = d->cacheDir + QLatin1Char('/') + peerName;
    QFile f( cacheEntry );
    if (!f.open(QIODevice::WriteOnly))
        return;

    f.write(cert.digest());
    f.close();
}

Checking if the certificate has changed is just as easy - we just compute the digest of the certificate the site has provided and compare it with the value we recorded previously:

bool SslCertificateMonitor::isMatchingCertificate(const QString &peerName, const QSslCertificate &cert)
{
    QString cacheEntry = d->cacheDir + QLatin1Char('/') + peerName;
    QFile f( cacheEntry );
    if (!f.open(QIODevice::ReadOnly))
        return false;

    QByteArray oldDigest = f.readAll();
    f.close();

    if (oldDigest != cert.digest())
        return false;

    return true;
}

In order to test the code worked, I wrote a simple app that will connect via SSL and dump the response. The code that integrates the monitor is simple:

MonitorTest::MonitorTest()
    : QWidget()
{
    monitor = new SslCertificateMonitor(this);
    connect(monitor, SIGNAL(certificateWarning(const QString &, const QString &)),
            SLOT(certificateWarning(const QString &, const QString &)));

This creates the monitor and connects the warning signal to the slot that will inform the user about the issue. When we actually create a socket, we have to call:

    monitor->addSocket(sock);

This tells the monitor to watch the certificates for our socket. Finally, we have to let the user choose what to do when a changed certificate is spotted. In the example, this is as simple as displaying a dialog:

void MonitorTest::certificateWarning(const QString &host, const QString &message)
{
    QMessageBox::StandardButton result =
        QMessageBox::warning( this,
                              tr("Certificate for %1 has changed").arg(host),
                              message + QLatin1String("\n\nAre you sure you wish to continue?"),
                              QMessageBox::Yes | QMessageBox::No );

    if (result == QMessageBox::Yes)
        monitor->acceptCertificate();
}

This means that when a certificate is found to have changed, the user sees a message like the one in the screenshot below:

As you can see, this proves that the basic concept works ok. Unfortunately there are some major API limitations that mean this code isn't as useful in practice as it could be:

  • It cannot be extended to QNetworkAccessManager right now as that neither gives us access to the underlying QSslSocket nor provides a function for encrypted() that is analogous to the sslError() forwarding function.
  • Since we need to tap the encrypted() signal, it's not safe for the application to send data in response to this signal (which is the way it is normally used). We really need to have a signal that works immediately prior to the one the application uses, or get the application to use a signal sent by this class.

In addition there are some easily fixable issues, for example it doesn't track the date or anything else from the certificate. This could easily be added though eg. by using the modification time on the cache entry. This is a good time however to be finding that there are API issues since there are plans for a reworking of the QSslSocket APIs for Qt5. Hopefully experiments like this will let us significantly improve things for the future.

The code as usual is in my qt-examples gitorious repository.