//    OpenVPN -- An application to securely tunnel IP networks
//               over a single port, with support for SSL/TLS-based
//               session authentication and key exchange,
//               packet encryption, packet authentication, and
//               packet compression.
//
//    Copyright (C) 2012- OpenVPN Inc.
//
//    SPDX-License-Identifier: MPL-2.0 OR AGPL-3.0-only WITH openvpn3-openssl-exception
//
//
//
//  Generic functions for extracting X.509 Certificate info from
//  OpenSSL X509 objects

#pragma once

#include <cstring>
#include <string>
#include <vector>

#include <openssl/ssl.h>
#include <openssl/bio.h>
#include <openssl/x509v3.h>
#include <openssl/x509.h>

#include "openvpn/common/hexstr.hpp"
#include "openvpn/common/uniqueptr.hpp"

namespace openvpn::OpenSSLPKI {

/**
 *  Retrieve the complete X.509 Certificate Subject field
 *
 *  OpenSSL supports two ways of representing the subject line.  The old
 *  format is deprecated, but there might be code expecting this old format.
 *  The old format looks like this:
 *
 *      /C=KG/ST=NA/O=OpenVPN-TEST/CN=Test-Server/emailAddress=me@myhost.mydomain
 *
 *  The new format is UTF-8 compliant and has a different formatting scheme:
 *
 *      C=KG, ST=NA, O=OpenVPN-TEST, CN=Test-Server,
 *emailAddress=me@myhost.mydomain
 *
 *
 * @param cert         Pointer to a native OpenSSL X509 object containing the
 *                     certificate
 * @param new_format   (optional, default: false) Which format to use,
 *                     true indicates the new format
 *
 * @return Returns a std::string containing the complete certificate subject.
 *         If it was not possible to retrieve the subject, and empty string
 *         is returned.
 */
static inline std::string x509_get_subject(::X509 *cert, bool new_format = false)
{
    if (!new_format)
    {
        unique_ptr_del<char> subject(
            X509_NAME_oneline(X509_get_subject_name(cert), nullptr, 0),
            [](char *p)
            { OPENSSL_free(p); });
        if (subject)
            return std::string(subject.get());
        else
            return std::string("");
    }

    unique_ptr_del<BIO> subject_bio(BIO_new(BIO_s_mem()),
                                    [](BIO *p)
                                    { BIO_free(p); });
    if (subject_bio == nullptr)
    {
        return std::string("");
    }

    X509_NAME_print_ex(subject_bio.get(),
                       X509_get_subject_name(cert),
                       0,
                       XN_FLAG_SEP_CPLUS_SPC
                           | XN_FLAG_FN_SN
                           | ASN1_STRFLGS_UTF8_CONVERT
                           | ASN1_STRFLGS_ESC_CTRL);
    if (BIO_eof(subject_bio.get()))
    {
        return std::string("");
    }

    BUF_MEM *subject_mem = nullptr;
    BIO_get_mem_ptr(subject_bio.get(), &subject_mem);
    return std::string(subject_mem->data,
                       subject_mem->data + subject_mem->length);
}

static inline std::string X509_get_pem_encoding(::X509 *cert)
{
    char *data;
    BIO *bio = BIO_new(BIO_s_mem());
    /* Even though PEM_write_bio_X509 should not modify the argument the official API does not have a const argument */
    PEM_write_bio_X509(bio, cert);
    size_t len = BIO_get_mem_data(bio, &data);
    std::string certpem{data, len};
    BIO_free(bio);
    return certpem;
}

/**
 * Retrives the algorithm used to sign a X509 certificate
 * @param cert 	OpenSSL certificate
 * @return
 */
static inline std::string x509_get_signature_algorithm(const ::X509 *cert)
{
    int nid = X509_get_signature_nid(cert);
    const char *sig = OBJ_nid2sn(nid);

    if (sig)
    {
        return sig;
    }
    else
        return "(error getting signature algorithm)";
}

/**
 *  Retrieves a specific portion of the X.509 Certificate subject field
 *
 * @param cert     Pointer to a native OpenSSL X509 object containing the
 *                 certificate
 * @param nid      Subject name ID to retrieve.  See openssl/obj_mac.h for
 *                 list of valid NID_* references.
 *
 * @return Returns the contents of the extracted field on success.  The
 *         resulting string may be empty if the extraction failed or the field
 *         is empty.
 */
static inline std::string x509_get_field(::X509 *cert, const int nid)
{
    static const char nullc = '\0';
    std::string ret;
    X509_NAME *x509_name = X509_get_subject_name(cert);
    int i = X509_NAME_get_index_by_NID(x509_name, nid, -1);
    if (i >= 0)
    {
        X509_NAME_ENTRY *ent = X509_NAME_get_entry(x509_name, i);
        if (ent)
        {
            ASN1_STRING *val = X509_NAME_ENTRY_get_data(ent);
            unsigned char *buf;
            buf = (unsigned char *)1; // bug in OpenSSL 0.9.6b ASN1_STRING_to_UTF8
                                      // requires this workaround
            const int len = ASN1_STRING_to_UTF8(&buf, val);
            if (len > 0)
            {
                if (std::strlen((char *)buf) == static_cast<unsigned int>(len))
                    ret = (char *)buf;
                OPENSSL_free(buf);
            }
        }
    }
    else
    {
        i = X509_get_ext_by_NID(cert, nid, -1);
        if (i >= 0)
        {
            X509_EXTENSION *ext = X509_get_ext(cert, i);
            if (ext)
            {
                BIO *bio = BIO_new(BIO_s_mem());
                if (bio)
                {
                    if (X509V3_EXT_print(bio, ext, 0, 0))
                    {
                        if (BIO_write(bio, &nullc, 1) == 1)
                        {
                            char *str;
                            const long len = BIO_get_mem_data(bio, &str);
                            if (std::strlen(str) == static_cast<size_t>(len))
                                ret = str;
                        }
                    }
                    BIO_free(bio);
                }
            }
        }
    }
    return ret;
}

/**
 *  Retrieves the X.509 certificate serial number
 *
 * @param cert     Pointer to a native OpenSSL X509 object containing the
 *                 certificate
 *
 * @return Returns the numeric representation of the certificate serial number
 *         as a std::string.
 */
static inline std::string x509_get_serial(::X509 *cert)
{
    const ASN1_INTEGER *asn1_i = X509_get_serialNumber(cert);
    BIGNUM *bignum = ASN1_INTEGER_to_BN(asn1_i, NULL);
    char *openssl_serial = BN_bn2dec(bignum);
    BN_free(bignum);

    if (openssl_serial)
    {
        const std::string ret = openssl_serial;
        OPENSSL_free(openssl_serial);
        return ret;
    }
    return std::string();
}

/**
 *  Retrieves the X.509 certificate serial number as hexadecimal
 *
 * @param cert     Pointer to a native OpenSSL X509 object containing the
 *                 certificate
 *
 * @return Returns the hexadecimal representation of the certificate
 *         serial number as a std::string.
 */
static inline std::string x509_get_serial_hex(::X509 *cert)
{
    const ASN1_INTEGER *asn1_i = X509_get_serialNumber(cert);
    return render_hex_sep(asn1_i->data, asn1_i->length, ':', false);
}

/**
 *  Retrieves the X.509 certificate SHA256 fingerprint as binary
 *
 * @return Returns a uint8_t std:vector containing the binary representation
 *         of the certificate's SHA256 fingerprint.
 */
static inline std::size_t x509_fingerprint_size()
{
    return EVP_MD_size(EVP_sha256());
}

static inline std::vector<uint8_t> x509_get_fingerprint(const ::X509 *cert)
{
    std::vector<uint8_t> fingerprint;
    fingerprint.resize(x509_fingerprint_size());

    if (::X509_digest(cert, EVP_sha256(), fingerprint.data(), NULL) != 1)
        throw OpenSSLException("OpenSSL error while calling X509_digest()");

    return fingerprint;
}

} // namespace openvpn::OpenSSLPKI
