// Copyright (C) 2018 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant reason:default

#include "qwindowscarootfetcher_p.h"
#include "qx509_openssl_p.h"
#include "qopenssl_p.h"

#include <QtCore/QThread>
#include <QtGlobal>

#include <QtCore/qscopeguard.h>

#ifdef QSSLSOCKET_DEBUG
#include <QtNetwork/private/qtlsbackend_p.h> // for debug categories
#include <QtCore/QElapsedTimer>
#endif

QT_BEGIN_NAMESPACE

class QWindowsCaRootFetcherThread : public QThread
{
public:
    QWindowsCaRootFetcherThread()
    {
        qRegisterMetaType<QSslCertificate>();
        setObjectName(QStringLiteral("QWindowsCaRootFetcher"));
        start();
    }
    ~QWindowsCaRootFetcherThread()
    {
        quit();
        wait(15500); // worst case, a running request can block for 15 seconds
    }
};

Q_GLOBAL_STATIC(QWindowsCaRootFetcherThread, windowsCaRootFetcherThread);

namespace {

const QList<QSslCertificate> buildVerifiedChain(const QList<QSslCertificate> &caCertificates,
                                                PCCERT_CHAIN_CONTEXT chainContext,
                                                const QString &peerVerifyName)
{
    // We ended up here because OpenSSL verification failed to
    // build a chain, with intermediate certificate missing
    // but "Authority Information Access" extension present.
    // Also, apparently the normal CA fetching path was disabled
    // by setting custom CA certificates. We convert wincrypt's
    // structures in QSslCertificate and give OpenSSL the second
    // chance to verify the now (apparently) complete chain.
    // In addition, wincrypt gives us a benefit of some checks
    // we don't have in OpenSSL back-end.
    Q_ASSERT(chainContext);

    if (!chainContext->cChain)
        return {};

    QList<QSslCertificate> verifiedChain;

    CERT_SIMPLE_CHAIN *chain = chainContext->rgpChain[chainContext->cChain - 1];
    if (!chain)
        return {};

    if (chain->TrustStatus.dwErrorStatus & CERT_TRUST_IS_PARTIAL_CHAIN)
        return {}; // No need to mess with OpenSSL (the chain is still incomplete).

    for (DWORD i = 0; i < chain->cElement; ++i) {
        CERT_CHAIN_ELEMENT *element = chain->rgpElement[i];
        QSslCertificate cert(QByteArray(reinterpret_cast<const char*>(element->pCertContext->pbCertEncoded),
                             int(element->pCertContext->cbCertEncoded)), QSsl::Der);

        if (cert.isBlacklisted())
            return {};

        if (element->TrustStatus.dwErrorStatus & CERT_TRUST_IS_REVOKED) // Good to know!
            return {};

        if (element->TrustStatus.dwErrorStatus & CERT_TRUST_IS_NOT_SIGNATURE_VALID)
            return {};

        verifiedChain.append(cert);
    }

    // We rely on OpenSSL's ability to find other problems.
    const auto tlsErrors = QTlsPrivate::X509CertificateOpenSSL::verify(caCertificates, verifiedChain, peerVerifyName);
    if (tlsErrors.size())
        verifiedChain.clear();

    return verifiedChain;
}

} // unnamed namespace

QWindowsCaRootFetcher::QWindowsCaRootFetcher(const QSslCertificate &certificate, QSslSocket::SslMode sslMode,
                                             const QList<QSslCertificate> &caCertificates, const QString &hostName)
    : cert(certificate), mode(sslMode), explicitlyTrustedCAs(caCertificates), peerVerifyName(hostName)
{
    moveToThread(windowsCaRootFetcherThread());
}

QWindowsCaRootFetcher::~QWindowsCaRootFetcher()
{
}

void QWindowsCaRootFetcher::start()
{
    QByteArray der = cert.toDer();
    PCCERT_CONTEXT wincert = CertCreateCertificateContext(X509_ASN_ENCODING, (const BYTE *)der.constData(), der.length());
    if (!wincert) {
#ifdef QSSLSOCKET_DEBUG
        qCDebug(lcTlsBackend, "QWindowsCaRootFetcher failed to convert certificate to windows form");
#endif
        emit finished(cert, QSslCertificate());
        deleteLater();
        return;
    }

    CERT_CHAIN_PARA parameters;
    memset(&parameters, 0, sizeof(parameters));
    parameters.cbSize = sizeof(parameters);
    // set key usage constraint
    parameters.RequestedUsage.dwType = USAGE_MATCH_TYPE_AND;
    parameters.RequestedUsage.Usage.cUsageIdentifier = 1;
    LPSTR oid = (LPSTR)(mode == QSslSocket::SslClientMode ? szOID_PKIX_KP_SERVER_AUTH : szOID_PKIX_KP_CLIENT_AUTH);
    parameters.RequestedUsage.Usage.rgpszUsageIdentifier = &oid;

#ifdef QSSLSOCKET_DEBUG
    QElapsedTimer stopwatch;
    stopwatch.start();
#endif
    PCCERT_CHAIN_CONTEXT chain;
    auto additionalStore = createAdditionalStore();
    BOOL result = CertGetCertificateChain(
        nullptr, //default engine
        wincert,
        nullptr, //current date/time
        additionalStore.get(), //default store (nullptr) or CAs an application trusts
        &parameters,
        0, //default dwFlags
        nullptr, //reserved
        &chain);
#ifdef QSSLSOCKET_DEBUG
    qCDebug(lcSsl) << "QWindowsCaRootFetcher" << stopwatch.elapsed() << "ms to get chain";
#endif

    QSslCertificate trustedRoot;
    if (result) {
#ifdef QSSLSOCKET_DEBUG
        qCDebug(lcSsl) << "QWindowsCaRootFetcher - examining windows chains";
        if (chain->TrustStatus.dwErrorStatus == CERT_TRUST_NO_ERROR)
            qCDebug(lcSsl) << " - TRUSTED";
        else
            qCDebug(lcSsl) << " - NOT TRUSTED" << chain->TrustStatus.dwErrorStatus;
        if (chain->TrustStatus.dwInfoStatus & CERT_TRUST_IS_SELF_SIGNED)
            qCDebug(lcSsl) << " - SELF SIGNED";
        qCDebug(lcSsl) << "QWindowsCaRootFetcher - dumping simple chains";
        for (unsigned int i = 0; i < chain->cChain; i++) {
            if (chain->rgpChain[i]->TrustStatus.dwErrorStatus == CERT_TRUST_NO_ERROR)
                qCDebug(lcSsl) << " - TRUSTED SIMPLE CHAIN" << i;
            else
                qCDebug(lcSsl) << " - UNTRUSTED SIMPLE CHAIN" << i << "reason:" << chain->rgpChain[i]->TrustStatus.dwErrorStatus;
            for (unsigned int j = 0; j < chain->rgpChain[i]->cElement; j++) {
                QSslCertificate foundCert(QByteArray((const char *)chain->rgpChain[i]->rgpElement[j]->pCertContext->pbCertEncoded
                    , chain->rgpChain[i]->rgpElement[j]->pCertContext->cbCertEncoded), QSsl::Der);
                qCDebug(lcSsl) << "   - " << foundCert;
            }
        }
        qCDebug(lcSsl) << " - and" << chain->cLowerQualityChainContext << "low quality chains"; //expect 0, we haven't asked for them
#endif

        //based on http://msdn.microsoft.com/en-us/library/windows/desktop/aa377182%28v=vs.85%29.aspx
        //about the final chain rgpChain[cChain-1] which must begin with a trusted root to be valid
        if (chain->TrustStatus.dwErrorStatus == CERT_TRUST_NO_ERROR
            && chain->cChain > 0) {
            const PCERT_SIMPLE_CHAIN finalChain = chain->rgpChain[chain->cChain - 1];
            // http://msdn.microsoft.com/en-us/library/windows/desktop/aa377544%28v=vs.85%29.aspx
            // rgpElement[0] is the end certificate chain element. rgpElement[cElement-1] is the self-signed "root" certificate element.
            if (finalChain->TrustStatus.dwErrorStatus == CERT_TRUST_NO_ERROR
                && finalChain->cElement > 0) {
                    trustedRoot = QSslCertificate(QByteArray((const char *)finalChain->rgpElement[finalChain->cElement - 1]->pCertContext->pbCertEncoded
                        , finalChain->rgpElement[finalChain->cElement - 1]->pCertContext->cbCertEncoded), QSsl::Der);
            }
        } else if (explicitlyTrustedCAs.size()) {
            // Setting custom CA in configuration, and those CAs are not trusted by MS.
#if QT_CONFIG(openssl)
            const auto verifiedChain = buildVerifiedChain(explicitlyTrustedCAs, chain, peerVerifyName);
            if (verifiedChain.size())
                trustedRoot = verifiedChain.last();
#else
            //It's only OpenSSL code-path that can trigger such a fetch.
            Q_UNREACHABLE();
#endif
        }
        CertFreeCertificateChain(chain);
    }
    CertFreeCertificateContext(wincert);

    emit finished(cert, trustedRoot);
    deleteLater();
}

QHCertStorePointer QWindowsCaRootFetcher::createAdditionalStore() const
{
    QHCertStorePointer customStore;
    if (explicitlyTrustedCAs.isEmpty())
        return customStore;

    if (HCERTSTORE rawPtr = CertOpenStore(CERT_STORE_PROV_MEMORY, 0, 0, 0, nullptr)) {
        customStore.reset(rawPtr);

        unsigned rootsAdded = 0;
        for (const QSslCertificate &caCert : explicitlyTrustedCAs) {
            const auto der = caCert.toDer();
            PCCERT_CONTEXT winCert = CertCreateCertificateContext(X509_ASN_ENCODING,
                                                                  reinterpret_cast<const BYTE *>(der.data()),
                                                                  DWORD(der.length()));
            if (!winCert) {
#if defined(QSSLSOCKET_DEBUG)
                qCWarning(lcSsl) << "CA fetcher, failed to convert QSslCertificate"
                                 << "to the native representation";
#endif // QSSLSOCKET_DEBUG
                continue;
            }
            const auto deleter = qScopeGuard([winCert](){
                CertFreeCertificateContext(winCert);
            });
            if (CertAddCertificateContextToStore(customStore.get(), winCert, CERT_STORE_ADD_ALWAYS, nullptr))
                ++rootsAdded;
#if defined(QSSLSOCKET_DEBUG)
            else //Why assert? With flags we're using and winCert check - should not happen!
                Q_ASSERT("CertAddCertificateContextToStore() failed");
#endif // QSSLSOCKET_DEBUG
        }
        if (!rootsAdded) //Useless store, no cert was added.
            customStore.reset();
#if defined(QSSLSOCKET_DEBUG)
    } else {

        qCWarning(lcSsl) << "CA fetcher, failed to create a custom"
                         << "store for explicitly trusted CA certificate";
#endif // QSSLSOCKET_DEBUG
    }

    return customStore;
}

QT_END_NAMESPACE

#include "moc_qwindowscarootfetcher_p.cpp"
