JAN
9
2011

Getting the details of an SSL connection

SSL is a pretty complex topic, and whilst Qt makes the APIs pretty easy to use, there's a lot going on underneath and it can be hard to debug. In order to make life a bit easier, I've written a simple tool that will connect to an SSL service then dump every piece of information Qt makes available concerning it. Of course the aim here is both that the tool be useful in itself, and also that it provide a good illustration of how to use the APIs concerned.

The first step, naturally enough, is to connect to the service using SSL - as you might expect, this uses the QSslSocket class. In order to be informed if we manage to connect successfully, or if we hit a problem we connect to some useful signals/

  • encrypted() - This is emitted if we manage to successfuly connect to the service.
  • sslErrors(const QList<QSslError> &) - This is emitted if there are problems establishing a secure connection.
  • error(QAbstractSocket::SocketError) - This is emitted if the connection fails. It is declared in QAbstractSocket. Note that ssl errors can get reported here too.

The connectToHost() method is the one that triggers the actual connection, and by default performs the SSL handshake etc. for us:

void Connector::connectToHost()
{
    qDebug() << "Connecting...";

    d->sock = new QSslSocket(this);
    connect( d->sock, SIGNAL(encrypted()), this, SLOT(ready()) );
    connect( d->sock, SIGNAL(error(QAbstractSocket::SocketError)),
             this, SLOT(socketError(QAbstractSocket::SocketError)) );
    connect( d->sock, SIGNAL(sslErrors(const QList<QSslError> &)),
             this, SLOT(sslError(const QList<QSslError> &)) );
    d->sock->connectToHostEncrypted( d->host, d->port );
}

If we get an error during the connection, then one of the two error handling slots we've connected gets called. If it's a socket error then we have a problem such as a refused connection we can't do much, however if it's an error caused by one of the SSL checks then we handle it differently. Normally, we should act as Qt does by default and treat SSL errors as hard errors - if we don't then SSL is unable to protect us against attacks. For the purposes of this example however, we want to be more lenient since we're specifically writing a way to figure out what's going on. As a result, our SSL error handling looks like this:

void Connector::sslError( const QList<QSslError> &errors )
{
    foreach( const QSslError &error, errors ) {
        qDebug() << "SSL Error: " << error.errorString();
    }

    // This is only used because we are interested in dumping all the info
    // do NOT use if you want real security (it introduces man-in-the-middle
    // attacks).
    d->sock->ignoreSslErrors();

If we manage to establish the connection ok, then we'll call a couple of functions that tell us the details. The first gives us the SSL certificate used by the remote server we've connected to, the second tells us the cipher we're using. We'll pass these to a couple of functions that print out the details, then terminate the application:

void Connector::ready()
{
    qDebug() << " === Peer Certificate ===";

    QSslCertificate cert = d->sock->peerCertificate();
    dumpCertificate( cert );

    QSslCipher cipher = d->sock->sessionCipher();
    dumpCipher( cipher );

    qDebug() << "Done";

    qApp->quit();
}

First, we'll look at how we dump the information about the cipher we're using. QSslCipher provides accessors that let us see the details - it's worth remembering that users will not be able to understand this information, so any interface for general use should be designed with rather more care.

void Connector::dumpCipher( const QSslCipher &cipher )
{
    qDebug() << "\n== Cipher ==";

    qDebug() << "Authentication:\t\t" << cipher.authenticationMethod();
    qDebug() << "Encryption:\t\t" << cipher.encryptionMethod();
    qDebug() << "Key Exchange:\t\t" << cipher.keyExchangeMethod();
    qDebug() << "Cipher Name:\t\t" << cipher.name();
    qDebug() << "Protocol:\t\t" <<  cipher.protocolString();
    qDebug() << "Supported Bits:\t\t" << cipher.supportedBits();
    qDebug() << "Used Bits:\t\t" << cipher.usedBits();
}

Now let's take a look at how we dump the certificate information. A certificate contains a lot of information (not all of which is accessible using Qt's API). In order to make the output managable, I've grouped it into three sections - information about the subject, information about the issuer, and finally information about the certificate itself.

The first thing we do is dump the certificate itself in a format known as PEM which a base64 string. Then we move onto the subject. We have quite a bit of information about the subject of a certificate, but two parts are particularly important for SSL - the common name and the subject alternative names. These two fields are the ones that are matched against the name of the host we're connecting to in order to determine if the certificate is valid for this host. After the information about the subject, we dump similar information about the issuer. Finally we display the information about the certificate itself which here is primarily just the dates for which it is valid.

	
void Connector::dumpCertificate( const QSslCertificate &cert )
{
    qDebug() << cert.toPem();

    qDebug() << "== Subject Info ==\b";
    qDebug() << "CommonName:\t\t" << cert.subjectInfo( QSslCertificate::CommonName );
    qDebug() << "Organization:\t\t" << cert.subjectInfo( QSslCertificate::Organization );
    qDebug() << "LocalityName:\t\t" << cert.subjectInfo( QSslCertificate::LocalityName );
    qDebug() << "OrganizationalUnitName:\t" << cert.subjectInfo( QSslCertificate::OrganizationalUnitName );
    qDebug() << "StateOrProvinceName:\t" << cert.subjectInfo( QSslCertificate::StateOrProvinceName );

    QMultiMap<QSsl::AlternateNameEntryType, QString> altNames = cert.alternateSubjectNames();
    if ( !altNames.isEmpty() ) {
        qDebug() << "Alternate Subject Names (DNS):";
        foreach (const QString &altName, altNames.values(QSsl::DnsEntry)) {
            qDebug() << altName;
        }

        qDebug() << "Alternate Subject Names (Email):";
        foreach (const QString &altName, altNames.values(QSsl::EmailEntry)) {
            qDebug() << altName;
        }
    }

    qDebug() << "\n== Issuer Info ==";
    qDebug() << "CommonName:\t\t" << cert.issuerInfo( QSslCertificate::CommonName );
    qDebug() << "Organization:\t\t" << cert.issuerInfo( QSslCertificate::Organization );
    qDebug() << "LocalityName:\t\t" << cert.issuerInfo( QSslCertificate::LocalityName );
    qDebug() << "OrganizationalUnitName:\t" << cert.issuerInfo( QSslCertificate::OrganizationalUnitName );
    qDebug() << "StateOrProvinceName:\t" << cert.issuerInfo( QSslCertificate::StateOrProvinceName );

    qDebug() << "\n== Certificate ==";
    //qDebug() << "Serial Number:\t\t" << cert.serialNumber(); // This seems buggy
    qDebug() << "Effective Date:\t\t" << cert.effectiveDate().toString();
    qDebug() << "Expiry Date:\t\t" << cert.expiryDate().toString();
    qDebug() << "Valid:\t\t\t" << (cert.isValid() ? "Yes" : "No");
}

The end result of all this is a small command line tool that will connect to the host specified and dump all this information. It should be useful for debugging any issues that may be encountered when connecting to an SSL site using Qt. There are a number of possibile enhancements that could be made (eg. dumping the whole certificate chain) but even now, the output of this tool should make tracking down SSL issues in Qt applications a lot simpler. The code, as ever, is in my qt-examples gitorious repository. An example of the output generated is here.