// Copyright (C) 2021 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

#include <QtNetwork/private/qsslcertificate_p.h>
#include <QtNetwork/private/qssl_p.h>

#include "qasn1element_p.h"
#include "qx509_generic_p.h"

#include <QtNetwork/qhostaddress.h>

#include <QtCore/qendian.h>
#include <QtCore/qhash.h>

#include <memory>

QT_BEGIN_NAMESPACE

using namespace Qt::StringLiterals;

namespace QTlsPrivate {

namespace {

QByteArray colonSeparatedHex(const QByteArray &value)
{
    const int size = value.size();
    int i = 0;
    while (i < size && !value.at(i)) // skip leading zeros
       ++i;

    return value.mid(i).toHex(':');
}

} // Unnamed namespace.

bool X509CertificateGeneric::isEqual(const X509Certificate &rhs) const
{
    const auto &other = static_cast<const X509CertificateGeneric &>(rhs);
    return derData == other.derData;
}

bool X509CertificateGeneric::isSelfSigned() const
{
    if (null)
        return false;

    return subjectMatchesIssuer;
}

QMultiMap<QSsl::AlternativeNameEntryType, QString> X509CertificateGeneric::subjectAlternativeNames() const
{
    return saNames;
}

#define BEGINCERTSTRING "-----BEGIN CERTIFICATE-----"
#define ENDCERTSTRING "-----END CERTIFICATE-----"

QByteArray X509CertificateGeneric::toPem() const
{
    QByteArray array = toDer();
    // Convert to Base64 - wrap at 64 characters.
    array = array.toBase64();
    QByteArray tmp;
    for (int i = 0; i <= array.size() - 64; i += 64) {
        tmp += QByteArray::fromRawData(array.data() + i, 64);
        tmp += '\n';
    }
    if (int remainder = array.size() % 64) {
        tmp += QByteArray::fromRawData(array.data() + array.size() - remainder, remainder);
        tmp += '\n';
    }

    return BEGINCERTSTRING "\n" + tmp + ENDCERTSTRING "\n";
}

QByteArray X509CertificateGeneric::toDer() const
{
    return derData;
}

QString X509CertificateGeneric::toText() const
{
    Q_UNIMPLEMENTED();
    return {};
}

Qt::HANDLE X509CertificateGeneric::handle() const
{
    Q_UNIMPLEMENTED();
    return nullptr;
}

size_t X509CertificateGeneric::hash(size_t seed) const noexcept
{
    return qHash(toDer(), seed);
}

QList<QSslCertificate> X509CertificateGeneric::certificatesFromPem(const QByteArray &pem, int count)
{
    QList<QSslCertificate> certificates;
    int offset = 0;
    while (count == -1 || certificates.size() < count) {
        int startPos = pem.indexOf(BEGINCERTSTRING, offset);
        if (startPos == -1)
            break;
        startPos += sizeof(BEGINCERTSTRING) - 1;
        if (!matchLineFeed(pem, &startPos))
            break;

        int endPos = pem.indexOf(ENDCERTSTRING, startPos);
        if (endPos == -1)
            break;

        offset = endPos + sizeof(ENDCERTSTRING) - 1;
        if (offset < pem.size() && !matchLineFeed(pem, &offset))
            break;

        QByteArray decoded = QByteArray::fromBase64(
            QByteArray::fromRawData(pem.data() + startPos, endPos - startPos));
        certificates << certificatesFromDer(decoded, 1);;
    }

    return certificates;
}

QList<QSslCertificate> X509CertificateGeneric::certificatesFromDer(const QByteArray &der, int count)
{
    QList<QSslCertificate> certificates;

    QByteArray data = der;
    while (count == -1 || certificates.size() < count) {
        QSslCertificate cert;
        auto *certBackend = QTlsBackend::backend<X509CertificateGeneric>(cert);
        if (!certBackend->parse(data))
            break;

        certificates << cert;
        data.remove(0, certBackend->derData.size());
    }

    return certificates;
}

bool X509CertificateGeneric::parse(const QByteArray &data)
{
    QAsn1Element root;

    QDataStream dataStream(data);
    if (!root.read(dataStream) || root.type() != QAsn1Element::SequenceType)
        return false;

    QDataStream rootStream(root.value());
    QAsn1Element cert;
    if (!cert.read(rootStream) || cert.type() != QAsn1Element::SequenceType)
        return false;

    // version or serial number
    QAsn1Element elem;
    QDataStream certStream(cert.value());
    if (!elem.read(certStream))
        return false;

    if (elem.type() == QAsn1Element::Context0Type) {
        QDataStream versionStream(elem.value());
        if (!elem.read(versionStream)
            || elem.type() != QAsn1Element::IntegerType
            || elem.value().isEmpty())
            return false;

        versionString = QByteArray::number(elem.value().at(0) + 1);
        if (!elem.read(certStream))
            return false;
    } else {
        versionString = QByteArray::number(1);
    }

    // serial number
    if (elem.type() != QAsn1Element::IntegerType)
        return false;
    serialNumberString = colonSeparatedHex(elem.value());

    // algorithm ID
    if (!elem.read(certStream) || elem.type() != QAsn1Element::SequenceType)
        return false;

    // issuer info
    if (!elem.read(certStream) || elem.type() != QAsn1Element::SequenceType)
        return false;

    QByteArray issuerDer = data.mid(dataStream.device()->pos() - elem.value().length(), elem.value().length());
    issuerInfoEntries = elem.toInfo();

    // validity period
    if (!elem.read(certStream) || elem.type() != QAsn1Element::SequenceType)
        return false;

    QDataStream validityStream(elem.value());
    if (!elem.read(validityStream) || (elem.type() != QAsn1Element::UtcTimeType && elem.type() != QAsn1Element::GeneralizedTimeType))
        return false;

    notValidBefore = elem.toDateTime();
    if (!notValidBefore.isValid())
        return false;

    if (!elem.read(validityStream) || (elem.type() != QAsn1Element::UtcTimeType && elem.type() != QAsn1Element::GeneralizedTimeType))
        return false;

    notValidAfter = elem.toDateTime();
    if (!notValidAfter.isValid())
        return false;


    // subject name
    if (!elem.read(certStream) || elem.type() != QAsn1Element::SequenceType)
        return false;

    QByteArray subjectDer = data.mid(dataStream.device()->pos() - elem.value().length(), elem.value().length());
    subjectInfoEntries = elem.toInfo();
    subjectMatchesIssuer = issuerDer == subjectDer;

    // public key
    qint64 keyStart = certStream.device()->pos();
    if (!elem.read(certStream) || elem.type() != QAsn1Element::SequenceType)
        return false;

    publicKeyDerData.resize(certStream.device()->pos() - keyStart);
    QDataStream keyStream(elem.value());
    if (!elem.read(keyStream) || elem.type() != QAsn1Element::SequenceType)
        return false;


    // key algorithm
    if (!elem.read(elem.value()) || elem.type() != QAsn1Element::ObjectIdentifierType)
        return false;

    const QByteArray oid = elem.toObjectId();
    if (oid == RSA_ENCRYPTION_OID)
        publicKeyAlgorithm = QSsl::Rsa;
    else if (oid == DSA_ENCRYPTION_OID)
        publicKeyAlgorithm = QSsl::Dsa;
    else if (oid == EC_ENCRYPTION_OID)
        publicKeyAlgorithm = QSsl::Ec;
    else
        publicKeyAlgorithm = QSsl::Opaque;

    certStream.device()->seek(keyStart);
    certStream.readRawData(publicKeyDerData.data(), publicKeyDerData.size());

    // extensions
    while (elem.read(certStream)) {
        if (elem.type() == QAsn1Element::Context3Type) {
            if (elem.read(elem.value()) && elem.type() == QAsn1Element::SequenceType) {
                QDataStream extStream(elem.value());
                while (elem.read(extStream) && elem.type() == QAsn1Element::SequenceType) {
                    X509CertificateExtension extension;
                    if (!parseExtension(elem.value(), extension))
                        return false;

                    if (extension.oid == "2.5.29.17"_L1) {
                        // subjectAltName

                        // Note, parseExtension() returns true for this extensions,
                        // but considers it to be unsupported and assigns a useless
                        // value. OpenSSL also treats this extension as unsupported,
                        // but properly creates a map with 'name' and 'value' taken
                        // from the extension. We only support 'email', 'IP' and 'DNS',
                        // but this is what our subjectAlternativeNames map can contain
                        // anyway.
                        QVariantMap extValue;
                        QAsn1Element sanElem;
                        if (sanElem.read(extension.value.toByteArray()) && sanElem.type() == QAsn1Element::SequenceType) {
                            QDataStream nameStream(sanElem.value());
                            QAsn1Element nameElem;
                            while (nameElem.read(nameStream)) {
                                switch (nameElem.type()) {
                                case QAsn1Element::Rfc822NameType:
                                    saNames.insert(QSsl::EmailEntry, nameElem.toString());
                                    extValue[QStringLiteral("email")] = nameElem.toString();
                                    break;
                                case QAsn1Element::DnsNameType:
                                    saNames.insert(QSsl::DnsEntry, nameElem.toString());
                                    extValue[QStringLiteral("DNS")] = nameElem.toString();
                                    break;
                                case QAsn1Element::IpAddressType: {
                                    QHostAddress ipAddress;
                                    QByteArray ipAddrValue = nameElem.value();
                                    switch (ipAddrValue.length()) {
                                    case 4: // IPv4
                                        ipAddress = QHostAddress(qFromBigEndian(*reinterpret_cast<quint32 *>(ipAddrValue.data())));
                                        break;
                                    case 16: // IPv6
                                        ipAddress = QHostAddress(reinterpret_cast<quint8 *>(ipAddrValue.data()));
                                        break;
                                    default: // Unknown IP address format
                                        break;
                                    }
                                    if (!ipAddress.isNull()) {
                                        saNames.insert(QSsl::IpAddressEntry, ipAddress.toString());
                                        extValue[QStringLiteral("IP")] = ipAddress.toString();
                                    }
                                    break;
                                }
                                default:
                                    break;
                                }
                            }
                            extension.value = extValue;
                            extension.supported = true;
                        }
                    }

                    extensions << extension;
                }
            }
        }
    }

    derData = data.left(dataStream.device()->pos());
    null = false;
    return true;
}

bool X509CertificateGeneric::parseExtension(const QByteArray &data, X509CertificateExtension &extension)
{
    bool ok = false;
    bool critical = false;
    QAsn1Element oidElem, valElem;

    QDataStream seqStream(data);

    // oid
    if (!oidElem.read(seqStream) || oidElem.type() != QAsn1Element::ObjectIdentifierType)
        return false;

    const QByteArray oid = oidElem.toObjectId();
    // critical and value
    if (!valElem.read(seqStream))
        return false;

    if (valElem.type() == QAsn1Element::BooleanType) {
        critical = valElem.toBool(&ok);

        if (!ok || !valElem.read(seqStream))
            return false;
    }

    if (valElem.type() != QAsn1Element::OctetStringType)
        return false;

    // interpret value
    QAsn1Element val;
    bool supported = true;
    QVariant value;
    if (oid == "1.3.6.1.5.5.7.1.1") {
        // authorityInfoAccess
        if (!val.read(valElem.value()) || val.type() != QAsn1Element::SequenceType)
            return false;
        QVariantMap result;
        const auto elems = val.toList();
        for (const QAsn1Element &el : elems) {
            const auto items = el.toList();
            if (items.size() != 2)
                return false;
            const QString key = QString::fromLatin1(items.at(0).toObjectName());
            switch (items.at(1).type()) {
            case QAsn1Element::Rfc822NameType:
            case QAsn1Element::DnsNameType:
            case QAsn1Element::UniformResourceIdentifierType:
                result[key] = items.at(1).toString();
                break;
            }
        }
        value = result;
    } else if (oid == "2.5.29.14") {
        // subjectKeyIdentifier
        if (!val.read(valElem.value()) || val.type() != QAsn1Element::OctetStringType)
            return false;
        value = colonSeparatedHex(val.value()).toUpper();
    } else if (oid == "2.5.29.19") {
        // basicConstraints
        if (!val.read(valElem.value()) || val.type() != QAsn1Element::SequenceType)
            return false;

        QVariantMap result;
        const auto items = val.toList();
        if (items.size() > 0) {
            result[QStringLiteral("ca")] = items.at(0).toBool(&ok);
            if (!ok)
                return false;
        } else {
            result[QStringLiteral("ca")] = false;
        }
        if (items.size() > 1) {
            result[QStringLiteral("pathLenConstraint")] = items.at(1).toInteger(&ok);
            if (!ok)
                return false;
        }
        value = result;
    } else if (oid == "2.5.29.35") {
        // authorityKeyIdentifier
        if (!val.read(valElem.value()) || val.type() != QAsn1Element::SequenceType)
            return false;
        QVariantMap result;
        const auto elems = val.toList();
        for (const QAsn1Element &el : elems) {
            if (el.type() == 0x80) {
                const QString key = QStringLiteral("keyid");
                result[key] = el.value().toHex();
            } else if (el.type() == 0x82) {
                const QString serial = QStringLiteral("serial");
                result[serial] = colonSeparatedHex(el.value());
            }
        }
        value = result;
    } else {
        supported = false;
        value = valElem.value();
    }

    extension.critical = critical;
    extension.supported = supported;
    extension.oid = QString::fromLatin1(oid);
    extension.name = QString::fromLatin1(oidElem.toObjectName());
    extension.value = value;

    return true;
}

} // namespace QTlsPrivate

QT_END_NAMESPACE
