// SPDX-FileCopyrightText: (C) 2023 Francesco Pretto <ceztko@gmail.com>
// SPDX-License-Identifier: (LGPL-2.0-or-later WITH cryptsetup-OpenSSL-exception) OR MPL-2.0

// This file contains functions and data structures that were inspired
// from OpenSSL 1.1 internal code, heavily adapted to extract the digest
// to be signed before inserting it into the CMS structure

#include <podofo/private/PdfDeclarationsPrivate.h>
#include "OpenSSLInternal.h"

using namespace std;
using namespace PoDoFo;

// This is a recreation of X509_SIG structure
struct MY_X509_SIG
{
    X509_ALGOR* algor;
    ASN1_OCTET_STRING* digest;
};

static void computeHashToSign(CMS_SignerInfo* si, unsigned char hash[], unsigned& len);
static EVP_MD_CTX* findDigestContext(BIO* bio, X509_ALGOR* digestAlg);
static void encodePKCS1(X509_ALGOR* digestAlg,
    const unsigned char* m, unsigned int m_len, charbuff& outbuff);

static X509_ALGOR* getDigestAlgorithm(CMS_SignerInfo* si);
static const ASN1_OBJECT* getASN1Object(X509_ALGOR* alg);
static STACK_OF(X509_ATTRIBUTE)* getSignedAttributesDeepCopy(CMS_SignerInfo* si);

// The following is using for reordering attributes during serialization
ASN1_ITEM_TEMPLATE(CMS_Attributes_Sign) =
ASN1_EX_TEMPLATE_TYPE(ASN1_TFLG_SET_ORDER, 0, CMS_ATTRIBUTES, X509_ATTRIBUTE)
ASN1_ITEM_TEMPLATE_END(CMS_Attributes_Sign)

ASN1_SEQUENCE(MY_X509_SIG) = {
        ASN1_SIMPLE(MY_X509_SIG, algor, X509_ALGOR),
        ASN1_SIMPLE(MY_X509_SIG, digest, ASN1_OCTET_STRING)
} ASN1_SEQUENCE_END(MY_X509_SIG);

ASN1_SEQUENCE(MY_ESS_CERT_ID_V2) = {
        ASN1_OPT(MY_ESS_CERT_ID_V2, hash_alg, X509_ALGOR),
        ASN1_SIMPLE(MY_ESS_CERT_ID_V2, hash, ASN1_OCTET_STRING),
} ASN1_SEQUENCE_END(MY_ESS_CERT_ID_V2)

ASN1_SEQUENCE(MY_ESS_SIGNING_CERT_V2) = {
        ASN1_SEQUENCE_OF(MY_ESS_SIGNING_CERT_V2, cert_ids, MY_ESS_CERT_ID_V2),
        ASN1_SEQUENCE_OF_OPT(MY_ESS_SIGNING_CERT_V2, policy_info, POLICYINFO)
} ASN1_SEQUENCE_END(MY_ESS_SIGNING_CERT_V2)

IMPLEMENT_ASN1_FUNCTIONS(MY_ESS_CERT_ID_V2)

IMPLEMENT_ASN1_FUNCTIONS(MY_ESS_SIGNING_CERT_V2)

// Inspired from "cms_SignerInfo_content_sign" in crypto/cms/cms_sd.c
void ssl::ComputeHashToSign(CMS_SignerInfo* si, BIO* chain, bool doWrapDigest, charbuff& hashToSign)
{
    ASN1_OBJECT* ctype;
    unsigned char hash[EVP_MAX_MD_SIZE];
    unsigned hashlen;

    // Extract the hash computed in the signer info using the
    // internal BIO chain, which allows streaming
    unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)> digestCtx(EVP_MD_CTX_new(), EVP_MD_CTX_free);
    auto matchedCtx = findDigestContext(chain, getDigestAlgorithm(si));

    // Copy the found context to the destination
    if (EVP_MD_CTX_copy_ex(digestCtx.get(), matchedCtx) <= 0)
        goto Error;

    // Compute the final digest value
    if (EVP_DigestFinal_ex(digestCtx.get(), hash, &hashlen) <= 0)
        goto Error;

    // Add the digest as a signed attribute
    if (!CMS_signed_add1_attr_by_NID(si, NID_pkcs9_messageDigest,
        V_ASN1_OCTET_STRING, hash, hashlen))
        goto Error;

    // Add the "contentType" signed attribute, specifying the
    // content to be signed is raw binary data (pkcs7-data)
    ctype = OBJ_nid2obj(NID_pkcs7_data);
    if (CMS_signed_add1_attr_by_NID(si, NID_pkcs9_contentType,
        V_ASN1_OBJECT, ctype, -1) <= 0)
        goto Error;

    computeHashToSign(si, hash, hashlen);
    if (doWrapDigest)
    {
        // We also need to encode the digest in ANS1 structure
        encodePKCS1(getDigestAlgorithm(si), hash, hashlen, hashToSign);
    }
    else
    {
        hashToSign.resize(hashlen);
        std::memcpy(hashToSign.data(), hash, hashlen);
    }

    return;

Error:
    PODOFO_RAISE_ERROR_INFO(PdfErrorCode::OpenSSLError, "Error while computing the MessageDigest");
}

void ssl::WrapDigestPKCS1(const bufferview& hash, PdfHashingAlgorithm hashing, charbuff& output)
{
    unique_ptr<X509_ALGOR, decltype(&X509_ALGOR_free)> x509Algor(X509_ALGOR_new(), X509_ALGOR_free);
    if (x509Algor == nullptr)
        PODOFO_RAISE_ERROR_INFO(PdfErrorCode::OpenSSLError, "Error X509_ALGOR_new");

    X509_ALGOR_set_md(x509Algor.get(), ssl::GetEVP_MD(hashing));
    encodePKCS1(x509Algor.get(), (const unsigned char*)hash.data(), (unsigned)hash.size(), output);
}


// Inspired from "cms_DigestAlgorithm_find_ctx" in crypto/cms/cms_lib.c
// Find the digest context that matches the algorithm in the BIO chain
EVP_MD_CTX* findDigestContext(BIO* bio, X509_ALGOR* digestAlg)
{
    // Convert the digest algorithm to a NID for easier comparison
    int digestNid;
    auto digestOid = getASN1Object(digestAlg);
    digestNid = OBJ_obj2nid(digestOid);

    while (true)
    {
        // Search for BIO of type digest in the chain
        EVP_MD_CTX* digestCtx;
        bio = BIO_find_type(bio, BIO_TYPE_MD);
        if (bio == nullptr)
            break;

        // Extract the context and check if it matches the digest algorithm
        // NOTE: Some implementations use signature algorithm OID instead of digest.
        BIO_get_md_ctx(bio, &digestCtx);
        if (EVP_MD_CTX_type(digestCtx) == digestNid
            || EVP_MD_pkey_type(EVP_MD_CTX_get0_md(digestCtx)) == digestNid)
        {
            return digestCtx;
        }

        bio = BIO_next(bio);
    }

    PODOFO_RAISE_ERROR_INFO(PdfErrorCode::OpenSSLError, "No matching digest context found");
}

void computeHashToSign(CMS_SignerInfo* si, unsigned char hash[], unsigned& hashlen)
{
    auto mctx = CMS_SignerInfo_get0_md_ctx(si);
    STACK_OF(X509_ATTRIBUTE)* signedAttrs = nullptr;
    unsigned char* buf = nullptr;
    unsigned len;
    const EVP_MD* sign_md;

    sign_md = EVP_get_digestbyobj(getDigestAlgorithm(si)->algorithm);
    if (EVP_DigestInit(mctx, sign_md) <= 0)
        goto Error;

    // Prepare the DER structure to sign, reordering attributes.
    // We do a deep copy to avoid internally corrupting the signer info structures
    signedAttrs = getSignedAttributesDeepCopy(si);
    len = (unsigned)ASN1_item_i2d((ASN1_VALUE*)signedAttrs, &buf,
        ASN1_ITEM_rptr(CMS_Attributes_Sign));
    // Free both the structure and the copied attributes
    sk_X509_ATTRIBUTE_pop_free(signedAttrs, X509_ATTRIBUTE_free);
    if (buf == nullptr)
        goto Error;

    // Compute the hash to be signed
    if (EVP_DigestUpdate(mctx, buf, len) <= 0)
        goto Error;
    OPENSSL_free(buf);
    buf = nullptr;

    if (EVP_DigestFinal(mctx, hash, &hashlen) <= 0)
        goto Error;

    EVP_MD_CTX_reset(mctx);
    return;

Error:
    OPENSSL_free(buf);
    EVP_MD_CTX_reset(mctx);
    PODOFO_RAISE_ERROR_INFO(PdfErrorCode::OpenSSLError, "Error while computing the MessageDigest");
}

// Inspired from "encode_pkcs1" in crypto/rsa/rsa_sign.c
// Encodes a hash as described in RFC 3447 "9.2 EMSA-PKCS1-v1_5"
// step 2, without padding. See https://datatracker.ietf.org/doc/html/rfc3447
void encodePKCS1(X509_ALGOR* digestAlg,
    const unsigned char* hash, unsigned int hashLen, charbuff& outbuff)
{
    // NOTE: X509_SIG is opaque in OpenSSL, so we need to recreate it
    // here to be able to encode the structure as needed

    MY_X509_SIG sig;
    X509_ALGOR algor{ };
    ASN1_TYPE parameter{ };
    ASN1_OCTET_STRING digest{ };

    sig.algor = digestAlg;
    sig.algor = &algor;
    sig.algor->algorithm = const_cast<ASN1_OBJECT*>(getASN1Object(digestAlg));
    parameter.type = V_ASN1_NULL;
    parameter.value.ptr = NULL;
    sig.algor->parameter = &parameter;

    sig.digest = &digest;
    sig.digest->data = const_cast<unsigned char*>(hash);
    sig.digest->length = hashLen;

    // This is the expansion of IMPLEMENT_ASN1_FUNCTIONS
    // NOTE: buf must be a local null pointer otherwise
    // the function will try to reuse the memory
    unsigned char* buf = nullptr;
    int bufLen = ASN1_item_i2d((ASN1_VALUE*)&sig, &buf, ASN1_ITEM_rptr(MY_X509_SIG));
    if (bufLen < 0)
        PODOFO_RAISE_ERROR_INFO(PdfErrorCode::OutOfMemory, "EncodeDigestPKCS1: Out of memory");

    try
    {
        outbuff.resize(bufLen);
    }
    catch (...)
    {
        OPENSSL_free(buf);
        throw;
    }

    std::memcpy(outbuff.data(), buf, bufLen);
    OPENSSL_free(buf);
}

X509_ALGOR* getDigestAlgorithm(CMS_SignerInfo* si)
{
    EVP_PKEY* pkey;
    X509* cert;
    X509_ALGOR* digestAlgorithm;
    X509_ALGOR* signingAlgorithm;
    CMS_SignerInfo_get0_algs(si, &pkey, &cert, &digestAlgorithm, &signingAlgorithm);
    return digestAlgorithm;
}

const ASN1_OBJECT* getASN1Object(X509_ALGOR* alg)
{
    const ASN1_OBJECT* obj;
    X509_ALGOR_get0(&obj, nullptr, nullptr, alg);
    return obj;
}

STACK_OF(X509_ATTRIBUTE)* getSignedAttributesDeepCopy(CMS_SignerInfo* si)
{
    STACK_OF(X509_ATTRIBUTE)* ret = nullptr;
    int count = CMS_signed_get_attr_count(si);
    for (int i = 0; i < count; i++)
    {
        auto attr = CMS_signed_get_attr(si, i);
        if (X509at_add1_attr(&ret, attr) == nullptr)
            goto Error;
    }

    return ret;

Error:
    // Free both the structure and the copied attributes
    sk_X509_ATTRIBUTE_pop_free(ret, X509_ATTRIBUTE_free);
    PODOFO_RAISE_ERROR_INFO(PdfErrorCode::OutOfMemory, "GetSignedAttributes: Out of memory");
}
