[prev in list] [next in list] [prev in thread] [next in thread] 

List:       kde-core-devel
Subject:    Re: Using system SSL certificates...
From:       Thiago Macieira <thiago () kde ! org>
Date:       2010-01-29 21:21:17
Message-ID: 201001292221.17673.thiago () kde ! org
[Download RAW message or body]

[Attachment #2 (multipart/mixed)]


Em Sexta-feira 29. Janeiro 2010, ās 21.53.55, Andreas Hartmetz escreveu:
> On Friday 29 January 2010 14:30:37 Thiago Macieira wrote:
> > Em Sexta-feira 29 Janeiro 2010, ās 14:04:06, Pierre Schmitz escreveu:
> > > > I've already made a script to do that. Actually, a Qt program.
> > > > 
> > > > I'll probably update Qt's certificate list with the Firefox ones for
> > > > the next  Qt version.
> > > > 
> > > > So all KDE has to do is stop overriding Qt's default certificate
> > > > bundle.
> > > 
> > > I would appreciate if KDE and Qt would use the system wide cert bundle
> > > (optionally configurable at build time).
> > 
> > The only thing that's holding me back in updating the Qt certificates is
> > to decide whether keeping expired certificates is a good thing.
> > 
> > There are 81 certificates in Qt's bundle. One of them is repeated, so 80
> > are unique.
> > 
> > However, from those 80, 8 have expired already.
> > 
> > Of the 72 non-expired, unique certificates in Qt, 48 are *not* in the
> > Firefox certificate store. But when the remainder of the Firefox ones are
> > added, the total increases to 120.
> 
> I'd really want *only* the certificates from Firefox and no expired
> certificates. Expired certificates generate an SSL error when connecting,
> just like a missing certificate. So the only change for client code is a
> different SSL error.
> Can you do that - i.e. just sync with Firefox?
> Or introduce a policy to remove expired certificates after n years and
> otherwise sync with Firefox... As I mentioned, the type of SSL error won't
> matter very much.
> If you can't do that, would you mind posting the script to download the
> certificates? :)

Attaching the program. I'm going to add it to Qt's util/ dir.

As you can see, it downloads the certificates from the XML file in 
http://www.mozilla.org/projects/security/certs/included/. That's the only 
resource I found in Mozilla. If there are more certificates, it would be nice 
to know about them.

The Qt non-Firefox certificates contain the likes of VeriSign, Thawte and 
Equifax. The question is: why are those well-known certificates in Qt but not 
in Firefox?

-- 
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
  Senior Product Manager - Nokia, Qt Development Frameworks
      PGP/GPG: 0x6EF45358; fingerprint:
      E067 918B B660 DBD1 105C  966C 33F5 F005 6EF4 5358

["main.cpp" (text/x-c++src)]

#include <QtCore/QCoreApplication>
#include <QtCore/QDate>
#include <QtCore/QDebug>
#include <QtCore/QDir>
#include <QtCore/QFile>
#include <QtCore/QProcessEnvironment>
#include <QtCore/QMap>
#include <QtCore/QSet>
#include <QtCore/QUrl>
#include <QtCore/QXmlStreamReader>
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkRequest>
#include <QtNetwork/QNetworkReply>
#include <QtNetwork/QSslCertificate>
#include <QtNetwork/QSslConfiguration>
#include <QtNetwork/QSslKey>

static const char mozillaCertUrl[] = \
"http://www.mozilla.org/projects/security/certs/included/"; static const char \
additionalCerts[] = "qrc:/extra/additional.xml";

struct Certificate
{
    enum TrustFlag { Web = 1, Email = 2 };
    Q_DECLARE_FLAGS(Trust, TrustFlag);

    Certificate() : errorString(QLatin1String("Timed out")), trust(0), version(0) {}

    QSslCertificate cert;
    QUrl url;
    QString name;
    QByteArray sha1;
    QDate effectiveDate;
    QDate expiryDate;
    QString errorString;
    Trust trust;
    int version;
};
Q_DECLARE_OPERATORS_FOR_FLAGS(Certificate::Trust)

class SslBundleApp: public QObject
{
    Q_OBJECT
    typedef QHash<QByteArray, Certificate> Certificates;
    QString bundleFile;
    QNetworkAccessManager manager;
    QSet<QNetworkReply *> pendingDownloads;
    Certificates certificates;
public:
    SslBundleApp();

    bool findQtDir(const QString &path);
    Certificate readCertificate(QXmlStreamReader &xml);
    void getCertificate(const QNetworkRequest &req);
    void parseCertificate(QNetworkReply *reply);
    void addQtDefaultCaCertificates();
    void writeAllCertificates();

protected:
    void timerEvent(QTimerEvent *);

public slots:
    void downloadFinished(QNetworkReply *reply);
    void masterXmlDownloaded(QNetworkReply *reply);
    void certDownloaded(QNetworkReply *reply);
};

static QByteArray toHexDots(const QByteArray &input)
{
    if (input.isEmpty())
        return QByteArray();

    QByteArray result;
    result.resize(input.size() * 3);
    char *dst = result.data();
    const char *src = input.constData();

    for (int i = 0; i < input.length(); ++i, ++src) {
        static const char hexdigits[] = "0123456789abcdef";
        register uchar c = *src;
        *dst++ = hexdigits[c >> 4];
        *dst++ = hexdigits[c & 0xF];
        *dst++ = ':';
    }
    result.chop(1);
    return result;
}

static inline bool isHexDigit(register const QChar &ch)
{
    register ushort u = ch.unicode();
    return (u >= '0' && u <= '9') ||
           ((u|0x20) >= 'a' && (u|0x20) <= 'f');
}

static inline uchar hexNibble(register const QChar &ch)
{
    register ushort u = ch.unicode();
    return u <= '9' ? u - '0' : (u|0x20) - 'a' + 0xa;
}

static QString decodeEscapedString(const QString &input)
{
    QByteArray output;
    bool simple = true;
    const QChar *in = input.constData();
    const QChar *end = in + input.length();
    for (; in < end; ++in) {
        if (in[0].unicode() > 0x7f)
            return input;    // it's not escaped!

        if (in[0].unicode() != '\\' || in + 3 >= end ||
            in[1].unicode() != 'x' || !isHexDigit(in[2]) || !isHexDigit(in[3])) {
            if (!simple)
                output.append(in->unicode());
        } else {
            if (simple) {
                // initialise the output up to here
                const QChar *begin = input.constData();
                int pos = in - begin;
                output.reserve(input.length());
                output.resize(pos);

                while (--pos >= 0) {
                    output[pos] = begin[pos].unicode();
                }
                simple = false;
            }
            in += 2;
            output.append((hexNibble(in[0]) << 4) | hexNibble(in[1]));
            ++in;
        }
    }
    return output.isEmpty() ? input : QString::fromUtf8(output);
}

bool SslBundleApp::findQtDir(const QString &path)
{
    if (path.isEmpty())
        return false;

    QDir dir(path);
    if (!dir.exists())
        return false;

    QString subPath = QLatin1String("src/network/ssl/qt-ca-bundle.crt");
    if (!dir.exists(subPath))
        return false;

    bundleFile = dir.absoluteFilePath(subPath);
    return true;
}

SslBundleApp::SslBundleApp()
{
    // find where the Qt source code is
    QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
    if (!findQtDir(env.value(QLatin1String("QTSRCDIR")))) {
        if (!findQtDir(env.value(QLatin1String("QTDIR")))) {
            static const char qt_source_tree[] = QT_SOURCE_TREE;
            if (!findQtDir(QFile::decodeName(qt_source_tree))) {
                QString self = QCoreApplication::applicationDirPath() + \
QLatin1String("../..");  if (!findQtDir(self)) {
                    qWarning() << "Cannot find qt-ca-bundle.crt file. Cannot \
continue.";  ::exit(1);
                }
            }
        }
    }

    addQtDefaultCaCertificates();

    connect(&manager, SIGNAL(finished(QNetworkReply*)), \
SLOT(downloadFinished(QNetworkReply*)));  pendingDownloads << \
                manager.get(QNetworkRequest(QUrl::fromEncoded(mozillaCertUrl)))
                     << \
manager.get(QNetworkRequest(QUrl::fromEncoded(additionalCerts))); }

void SslBundleApp::getCertificate(const QNetworkRequest &req)
{
    QNetworkReply *reply = manager.get(req);
    QSslConfiguration sslconf = reply->sslConfiguration();
    sslconf.setProtocol(QSsl::TlsV1);
    sslconf.setPeerVerifyDepth(0);
    sslconf.setPeerVerifyMode(QSslSocket::VerifyNone);
    reply->setSslConfiguration(sslconf);
    reply->ignoreSslErrors();
    pendingDownloads << reply;
}

Certificate SslBundleApp::readCertificate(QXmlStreamReader &xml)
{
    Certificate cert;
    cert.name = xml.attributes().value(QLatin1String("name")).toString();
    QStringRef name = xml.name();
    while (xml.readNextStartElement()) {
        name = xml.name();
        //qDebug() << xml.name();
        if (name == QLatin1String("data")) {
            QXmlStreamAttributes attr = xml.attributes();
            cert.url = \
                QUrl::fromEncoded(attr.value(QLatin1String("url")).toString().toUtf8());
                
            cert.sha1 = \
                QByteArray::fromHex(attr.value(QLatin1String("sha1")).toString().toLatin1());
                
            cert.version = attr.value(QLatin1String("version")).toString().toInt();
            cert.effectiveDate = \
                QDate::fromString(attr.value(QLatin1String("from")).toString(), \
                Qt::ISODate);
            cert.expiryDate = \
QDate::fromString(attr.value(QLatin1String("to")).toString(), Qt::ISODate);  \
xml.skipCurrentElement();  } else if (name == QLatin1String("trust")) {
            while (xml.readNextStartElement()) {
                if (xml.name() == QLatin1String("flag")) {
                    QStringRef type = xml.attributes().value(QLatin1String("type"));
                    if (type == QLatin1String("web"))
                        cert.trust |= Certificate::Web;
                    else if (type == QLatin1String("email"))
                        cert.trust |= Certificate::Email;
                }
                xml.skipCurrentElement();
            }
        } else {
            xml.skipCurrentElement();
        }
    }
    return cert;
}

void SslBundleApp::masterXmlDownloaded(QNetworkReply *reply)
{
    int newDownloads = 0;

    // parse the XML reply and download all certificates
    QXmlStreamReader xml(reply);
    xml.readNextStartElement();
    if (xml.name() != QLatin1String("certificates"))
        return;  // malformed file
    while (xml.readNextStartElement()) {
        QStringRef name = xml.name();
        //qDebug() << xml.name();
        if (name == QLatin1String("authority")) {
            while (xml.readNextStartElement()) {
                name = xml.name();
                //qDebug() << xml.name();
                if (name == QLatin1String("certificate")) {
                    Certificate cert = readCertificate(xml);
                    Certificates::Iterator it = certificates.find(cert.sha1);
                    if (cert.trust & Certificate::Web && it == certificates.end()) {
                        qDebug() << "Downloading certificate" << cert.name << \
toHexDots(cert.sha1) << "from" << cert.url;  QNetworkRequest req(cert.url);
                        req.setAttribute(QNetworkRequest::User, cert.sha1);
                        getCertificate(req);

                        ++newDownloads;
                        certificates.insert(cert.sha1, cert);
                    } else if (it != certificates.end()) {
                        //qDebug() << "Certificate" << cert.name << "already present \
in Qt's bundle, not downloading again";

                        if (it->url.scheme() == QLatin1String("qrc"))
                            it->url = cert.url;
                        if (it->name.isEmpty())
                            it->name = cert.name;
                    }
                } else {
                    xml.skipCurrentElement();
                }
            }
        } else {
            xml.skipCurrentElement();
        }
    }

    qDebug() << "Downloading" << newDownloads << "certificates from" << reply->url();
    startTimer(45000);
}

void SslBundleApp::parseCertificate(QNetworkReply *reply)
{
    QByteArray sha1 = \
reply->request().attribute(QNetworkRequest::User).toByteArray();  if (sha1.isEmpty())
        return;   // something else got done?

    Certificates::Iterator cit = certificates.find(sha1);
    if (cit == certificates.end())
        return;   // also something wrong

    // was this redirection?
    QVariant redirectionTarget = \
reply->attribute(QNetworkRequest::RedirectionTargetAttribute);  if \
                (redirectionTarget.isValid()) {
        qDebug() << "Downloading certificate" << cit->name << "from" << \
redirectionTarget.toUrl() << "(redirected)";  QNetworkRequest \
req(redirectionTarget.toUrl());  req.setAttribute(QNetworkRequest::User, sha1);
        getCertificate(req);
        return;
    }

    if (reply->error()) {
        bool retried = \
reply->request().attribute(QNetworkRequest::Attribute(QNetworkRequest::User+1)).toBool();
  if (!retried) {
            // try again
            qDebug() << "Downloading certificate" << cit->name << "from" << \
reply->url() << "(retry)";  QNetworkRequest req(reply->url());
            req.setAttribute(QNetworkRequest::User, sha1);
            req.setAttribute(QNetworkRequest::Attribute(QNetworkRequest::User+1), \
true);  getCertificate(req);
        } else {
            cit->errorString = QString::fromLatin1("Failed to download certificate \
                from %1: %2")
                               .arg(QString::fromLatin1(reply->url().toEncoded()), \
reply->errorString());  }
        return;
    }

    qDebug() << "Downloaded certificate" << cit->name << "from" << reply->url();

    // parse the certificate
    // try DER first
    QByteArray data = reply->readAll();
    QSslCertificate cert = QSslCertificate(data, QSsl::Der);
    if (!cert.isValid()) {
        // try PEM
        cert = QSslCertificate(data, QSsl::Pem);
        if (!cert.isValid()) {
            cit->errorString = QString::fromLatin1("Failed to parse certificate from \
                %1")
                               .arg(QString::fromLatin1(reply->url().toEncoded()));
            return;
        }
    }

    // verify that the certificate data matches
    if (cert.version().toInt() != cit->version) {
        cit->errorString = QString::fromLatin1("Downloaded certificate has version \
                %1, but %2 was expected")
                           .arg(QLatin1String(cert.version())).arg(cit->version);
        return;
    }

    if (cert.digest(QCryptographicHash::Sha1) != cit->sha1) {
        cit->errorString = QString::fromLatin1("Downloaded certificate has SHA-1 %1, \
                but %2 was expected")
                           \
                .arg(QString::fromLatin1(toHexDots(cert.digest(QCryptographicHash::Sha1))),
                
                                QString::fromLatin1(toHexDots(cit->sha1)));
        return;
    }

    if (cert.effectiveDate().toUTC().date() != cit->effectiveDate) {
        qWarning() << "WARNING Certificate" << cit->name << "has effective date" << \
cert.effectiveDate().toString()  << "expected" << cit->effectiveDate.toString();
    }

    if (cert.expiryDate().toUTC().date() != cit->expiryDate) {
        qWarning() << "WARNING Certificate" << cit->name << "has effective date" << \
cert.expiryDate().toString()  << "expected" << cit->expiryDate.toString();
    }

    QString certName = \
decodeEscapedString(cert.subjectInfo(QSslCertificate::CommonName));  if \
(!certName.isEmpty() && certName != cit->name) {  cit->name = certName;
    }

    // Certificate is valid
    cit->cert = cert;
}

void SslBundleApp::certDownloaded(QNetworkReply *reply)
{
    parseCertificate(reply);
}

void SslBundleApp::downloadFinished(QNetworkReply *reply)
{
    pendingDownloads -= reply;
    reply->deleteLater();
    if (reply->request().attribute(QNetworkRequest::User).isValid())
        certDownloaded(reply);
    else
        masterXmlDownloaded(reply);
    if (pendingDownloads.isEmpty())
        writeAllCertificates();
}

void SslBundleApp::timerEvent(QTimerEvent *)
{
    qWarning() << "Timing out download with" << pendingDownloads.size()
               << "downloads pending:";
    foreach (QNetworkReply *reply, pendingDownloads)
        qWarning() << "  from" << reply->url();
    pendingDownloads.clear();
    writeAllCertificates();
}

void SslBundleApp::addQtDefaultCaCertificates()
{
    const QList<QSslCertificate> list = QSslCertificate::fromPath(bundleFile);
    Certificate ours;
    QSslCertificate &cert = ours.cert;
    ours.url = QUrl::fromEncoded("qrc:/trolltech/network/ssl/qt-ca-bundle.crt");

    qDebug() << "Loading" << list.size() << "certificates from Qt's current bundle";
    QDateTime now = QDateTime::currentDateTime();
    foreach (cert, list) {
        ours.sha1 = cert.digest(QCryptographicHash::Sha1);
        ours.name = decodeEscapedString(cert.subjectInfo(QSslCertificate::CommonName));


        if (cert.expiryDate() < now) {
            qWarning() << "Certificate" << ours.name << toHexDots(ours.sha1) << "has \
expired, skipping";  continue;
        }
        if (!cert.isValid()) {
            qWarning() << "Certificate" << ours.name << toHexDots(ours.sha1) << "is \
invalid!! Skipping";  continue;
        }
        if (certificates.contains(ours.sha1)) {
            qWarning() << "Certificate" << ours.name << toHexDots(ours.sha1) << "is \
duplicated in the store. Skipping.";  continue;
        }
        qDebug() << "Appending certificate from Qt's bundle:" << ours.name << \
toHexDots(ours.sha1);  certificates.insert(ours.sha1, ours);
    }
    qDebug() << certificates.size() << "certificates loaded";
}

void SslBundleApp::writeAllCertificates()
{
    static const char boilerplate[] =
            "##\n"
            "##  qt-ca-bundle.crt -- Bundle of CA Certificates\n"
            "##\n"
            "##  This is a bundle of X.509 certificates of public\n"
            "##  Certificate Authorities (CA)\n"
            "##\n"
            "##  This file was last generated on: ";

    // tell the event loop to exit after we're done
    QCoreApplication::instance()->quit();

    // Sort the certificates by name
    QMap<QByteArray, Certificate> sortedCertificates;
    QDateTime now = QDateTime::currentDateTime();
    qDebug() << "Sorting" << certificates.size() << "certificates";
    foreach (const Certificate &cert, certificates) {
        if (!cert.cert.isValid()) {
            qDebug() << "SKIPPED: Certificate" << cert.name << "from" << cert.url << \
cert.errorString;  continue;
        }
        if (cert.cert.expiryDate() < now) {
            qDebug() << "SKIPPED: Certificate" << cert.name << "from" << cert.url  << \
"expired on" << cert.cert.expiryDate();  continue;
        }

        QByteArray key = cert.name.toUtf8();
        key += '\0';
        key += cert.sha1;
        sortedCertificates.insert(key, cert);
    }
    qDebug() << sortedCertificates.size() << "certificates remaining after sorting";

    QFile f(bundleFile);
    if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
        qWarning() << "Could not save to file" << bundleFile << ":" << \
f.errorString();  return;
    }
    QTextStream s(&f);
    s.setCodec("UTF-8");

    // write boilerplate
    s << boilerplate
      << now.toUTC().toString(Qt::ISODate) << endl
      << "##  Number of certificates on this file: " << sortedCertificates.size() << \
endl  << endl;

    foreach (const Certificate &cert, sortedCertificates) {
        s << "# Certificate for: " << cert.name << endl;
        s << "# Source: " << cert.url.toEncoded() << endl;
        s << "# Data:" << endl;
        s << "#    Version: " << cert.cert.version() << endl;
        s << "#    Serial number: " << cert.cert.serialNumber() << endl;
        s << "#    Subject: CN=" << \
                decodeEscapedString(cert.cert.subjectInfo(QSslCertificate::CommonName)) \
                << endl
          << "#      C=" << \
                decodeEscapedString(cert.cert.subjectInfo(QSslCertificate::CountryName))
                
          << ", ST=" << \
                decodeEscapedString(cert.cert.subjectInfo(QSslCertificate::StateOrProvinceName))
                
          << ", L=" << \
                decodeEscapedString(cert.cert.subjectInfo(QSslCertificate::LocalityName))
                
          << ", O=" << \
                decodeEscapedString(cert.cert.subjectInfo(QSslCertificate::Organization))
                
          << ", OU=" << \
decodeEscapedString(cert.cert.subjectInfo(QSslCertificate::OrganizationalUnitName))  \
                << endl;
        s << "#    Issuer: CN=" << \
                decodeEscapedString(cert.cert.issuerInfo(QSslCertificate::CommonName)) \
                << endl
          << "#      C=" << \
                decodeEscapedString(cert.cert.issuerInfo(QSslCertificate::CountryName))
                
          << ", ST=" << \
                decodeEscapedString(cert.cert.issuerInfo(QSslCertificate::StateOrProvinceName))
                
          << ", L=" << \
                decodeEscapedString(cert.cert.issuerInfo(QSslCertificate::LocalityName))
                
          << ", O=" << \
                decodeEscapedString(cert.cert.issuerInfo(QSslCertificate::Organization))
                
          << ", OU=" << \
decodeEscapedString(cert.cert.issuerInfo(QSslCertificate::OrganizationalUnitName))  \
                << endl;
        s << "#    SHA-1 checksum: " << \
                toHexDots(cert.cert.digest(QCryptographicHash::Sha1)) << endl;
        s << "#    Effective: " << \
                cert.cert.effectiveDate().toUTC().toString(Qt::ISODate) << endl;
        s << "#    Expires: " << cert.cert.expiryDate().toUTC().toString(Qt::ISODate) \
                << endl;
        s << "#    Public key size: " << cert.cert.publicKey().length() << " bits" << \
endl;  s << cert.cert.toPem() << endl;
    }
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    SslBundleApp app;

    return a.exec();
}

#include "main.moc"


["signature.asc" (application/pgp-signature)]

[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic