1
\$\begingroup\$

This is my solution to a question I asked on stackoverflow.

The scenario is as follows: I got my X509 certificate and a pool of other X509 certificates. Now I want to calculate the chain of trust, starting from my certificate based on the pool.

I realized that I can use openSSL in addition to Qt in the project, after digging in the deeps of their documentation, I came up with the following solution. I am a bit concerned that I get memory leaks somewhere. Also, and this might be the most critical part, I don't know if I used the flag paramter for the SSL_CTX_build_cert_chain call correctly.

Btw I also asked for the theoretical background how such a chain is calculated in general (based on the subject and issuer, what fields have to match, e.g. only common name?), so if you know the answer, head over there and share it please :)!

Finally, here is my code:

QList< QSslCertificate > buildSslChain(const QSslCertificate &leaf, const QList< QSslCertificate > &pool) {
    QList< QSslCertificate > chain;
    if (leaf.isNull()) {
        return chain;
    }
    chain << leaf;
    if (pool.isEmpty()) {
        return chain;
    }

    // Convert the leaf to der format and create an openssl x509 from it
    QByteArray qbaLeaf = leaf.toDer();
    int maxDerSize     = qbaLeaf.size();
    BIO *mem           = BIO_new_mem_buf(qbaLeaf.data(), maxDerSize);
    Q_UNUSED(BIO_set_close(mem, BIO_NOCLOSE));
    X509 *leaf_x509 = d2i_X509_bio(mem, nullptr);
    BIO_free(mem);

    // Prepare a ssl context, the method should not matter, so just go with TLS_method()
    SSL_CTX *ctx = SSL_CTX_new(TLS_method());

    // Add the leaf
    SSL_CTX_use_certificate(ctx, leaf_x509);

    // Construct openssl x509 for the pool and add each to the ctx
    foreach (const QSslCertificate &cert, pool) {
        QByteArray qbaCert = cert.toDer();
        int s              = qbaCert.size();
        maxDerSize         = maxDerSize < s ? s : maxDerSize;
        BIO *mem           = BIO_new_mem_buf(qbaCert.data(), s);
        Q_UNUSED(BIO_set_close(mem, BIO_NOCLOSE));
        X509 *x509 = d2i_X509_bio(mem, nullptr);
        BIO_free(mem);
        SSL_CTX_add0_chain_cert(ctx, x509);
    }

    // Do the actual chain building
    int flags = SSL_BUILD_CHAIN_FLAG_CHECK | SSL_BUILD_CHAIN_FLAG_IGNORE_ERROR; // Am I using the correct flags?!
    int ret   = SSL_CTX_build_cert_chain(ctx, flags);

    // Check if we were succesfull, since we use the SSL_BUILD_CHAIN_FLAG_IGNORE_ERROR flag
    // 2 is a acceptable return value, too
    if (ret == 1 || ret == 2) {
        // Retrieve the chain
        STACK_OF(X509) *stack = nullptr;
        SSL_CTX_get0_chain_certs(ctx, &stack);

        // Copy the chain back to qt
        // Instead of allocating a new buffer every time i2d_X509 is called, we allocate a shared buffer
        // and because we know the maxDerSize, we konw how big this needs to be
        unsigned char *buffer = (unsigned char *) malloc(maxDerSize);
        while (sk_X509_num(stack) > 0) {
            X509 *next     = sk_X509_shift(stack);
            int actualSize = i2d_X509(next, &buffer);
            X509_free(next);
            if (actualSize == -1) {
                // Failed to der encode certificate in openssl
                chain.clear();
                break;
            }
            // i2d_X509 altered our buffer pointer, so we need to set it back manually
            buffer -= actualSize;
            QByteArray array            = QByteArray::fromRawData((char *) buffer, actualSize);
            QList< QSslCertificate > ql = QSslCertificate::fromData(array, QSsl::EncodingFormat::Der);
            if (ql.size() == 1) {
                chain << ql;
            } else {
                // Data from openssl must contain exactly one certificate!
                chain.clear();
                break;
            }
        }

        // Clean up
        free(buffer);
    } else {
        chain.clear();
    }
    // pool certificates where added with add0 option (not add1),
    // so they will automatically be freed if the ctx is freed.
    // Same for the stack, which was obtain with the set0 verison.
    SSL_CTX_free(ctx);
    X509_free(leaf_x509);

    // Drain OpenSSL's per-thread error queue
    // to ensure that errors from the operations
    // we've done in here do not leak out into
    // Qt's SSL module.
    //
    // If an error leaks, it can break all connections
    // to the server because each invocation of Qt's SSL
    // read callback checks OpenSSL's per-thread error
    // queue (albeit indirectly, via SSL_get_error()).
    // Qt expects any errors returned from SSL_get_error()
    // to be related to the QSslSocket it is currently
    // processing -- which is the obvious thing to expect:
    // SSL_get_error() takes a pointer to an SSL object
    // and the return code of the failed operation.
    // However, it is also documented as:
    //
    //  "In addition to ssl and ret, SSL_get_error()
    //   inspects the current thread's OpenSSL error
    //   queue."
    //
    // So, if any OpenSSL operation on the main thread
    // forgets to clear the error queue, those errors
    // *will* leak into other things that *do* error
    // checking. In our case, into Qt's SSL read callback,
    // resulting in all clients being disconnected.
    ERR_clear_error();

    return chain;
}

Edit: I realized that it should probably be sk_X509_shift instead of sk_X509_pop so I changed that.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.