#include "config.h"

#include "image_verify.hpp"

#include "version.hpp"

#include <fcntl.h>
#include <openssl/err.h>
#include <sys/stat.h>

#include <phosphor-logging/elog-errors.hpp>
#include <phosphor-logging/elog.hpp>
#include <phosphor-logging/log.hpp>
#include <xyz/openbmc_project/Common/error.hpp>

#include <fstream>
#include <set>

namespace openpower
{
namespace software
{
namespace image
{

using namespace phosphor::logging;
using namespace openpower::software::updater;
using InternalFailure =
    sdbusplus::xyz::openbmc_project::Common::Error::InternalFailure;

constexpr auto keyTypeTag = "KeyType";
constexpr auto hashFunctionTag = "HashType";

Signature::Signature(const std::filesystem::path& imageDirPath,
                     const std::string& pnorFileName,
                     const std::filesystem::path& signedConfPath) :
    imageDirPath(imageDirPath),
    pnorFileName(pnorFileName), signedConfPath(signedConfPath)
{
    std::filesystem::path file(imageDirPath / MANIFEST_FILE);

    auto keyValues =
        Version::getValue(file, {{keyTypeTag, " "}, {hashFunctionTag, " "}});
    keyType = keyValues.at(keyTypeTag);
    hashType = keyValues.at(hashFunctionTag);
}

AvailableKeyTypes Signature::getAvailableKeyTypesFromSystem() const
{
    AvailableKeyTypes keyTypes{};

    // Find the path of all the files
    if (!std::filesystem::is_directory(signedConfPath))
    {
        log<level::ERR>("Signed configuration path not found in the system");
        elog<InternalFailure>();
    }

    // Look for all the hash and public key file names get the key value
    // For example:
    // /etc/activationdata/OpenPOWER/publickey
    // /etc/activationdata/OpenPOWER/hashfunc
    // /etc/activationdata/GA/publickey
    // /etc/activationdata/GA/hashfunc
    // Set will have OpenPOWER, GA

    for (const auto& p :
         std::filesystem::recursive_directory_iterator(signedConfPath))
    {
        if ((p.path().filename() == HASH_FILE_NAME) ||
            (p.path().filename() == PUBLICKEY_FILE_NAME))
        {
            // extract the key types
            // /etc/activationdata/GA/  -> get GA from the path
            auto key = p.path().parent_path();
            keyTypes.insert(key.filename());
        }
    }

    return keyTypes;
}

inline KeyHashPathPair Signature::getKeyHashFileNames(const Key_t& key) const
{
    std::filesystem::path hashpath(signedConfPath / key / HASH_FILE_NAME);
    std::filesystem::path keyPath(signedConfPath / key / PUBLICKEY_FILE_NAME);

    return std::make_pair(std::move(hashpath), std::move(keyPath));
}

bool Signature::verify()
{
    try
    {
        // Verify the MANIFEST and publickey file using available
        // public keys and hash on the system.
        if (false == systemLevelVerify())
        {
            log<level::ERR>("System level Signature Validation failed");
            return false;
        }

        // image specific publickey file name.
        std::filesystem::path publicKeyFile(imageDirPath / PUBLICKEY_FILE_NAME);

        // Validate the PNOR image file.
        // Build Image File name
        std::filesystem::path file(imageDirPath);
        file /= pnorFileName;

        // Build Signature File name
        std::string fileName = file.filename();
        std::filesystem::path sigFile(imageDirPath);
        sigFile /= fileName + SIGNATURE_FILE_EXT;

        // Verify the signature.
        auto valid = verifyFile(file, sigFile, publicKeyFile, hashType);
        if (valid == false)
        {
            log<level::ERR>("Image file Signature Validation failed",
                            entry("IMAGE=%s", pnorFileName.c_str()));
            return false;
        }

        log<level::DEBUG>("Successfully completed Signature vaildation.");

        return true;
    }
    catch (const InternalFailure& e)
    {
        return false;
    }
    catch (const std::exception& e)
    {
        log<level::ERR>(e.what());
        return false;
    }
}

bool Signature::systemLevelVerify()
{
    // Get available key types from the system.
    auto keyTypes = getAvailableKeyTypesFromSystem();
    if (keyTypes.empty())
    {
        log<level::ERR>("Missing Signature configuration data in system");
        elog<InternalFailure>();
    }

    // Build publickey and its signature file name.
    std::filesystem::path pkeyFile(imageDirPath / PUBLICKEY_FILE_NAME);
    std::filesystem::path pkeyFileSig(pkeyFile);
    pkeyFileSig.replace_extension(SIGNATURE_FILE_EXT);

    // Build manifest and its signature file name.
    std::filesystem::path manifestFile(imageDirPath / MANIFEST_FILE);
    std::filesystem::path manifestFileSig(manifestFile);
    manifestFileSig.replace_extension(SIGNATURE_FILE_EXT);

    auto valid = false;

    // Verify the file signature with available key types
    // public keys and hash function.
    // For any internal failure during the key/hash pair specific
    // validation, should continue the validation with next
    // available Key/hash pair.
    for (const auto& keyType : keyTypes)
    {
        auto keyHashPair = getKeyHashFileNames(keyType);
        auto keyValues = Version::getValue(keyHashPair.first,
                                           {{hashFunctionTag, " "}});
        auto hashFunc = keyValues.at(hashFunctionTag);

        try
        {
            // Verify manifest file signature
            valid = verifyFile(manifestFile, manifestFileSig,
                               keyHashPair.second, hashFunc);
            if (valid)
            {
                // Verify publickey file signature.
                valid = verifyFile(pkeyFile, pkeyFileSig, keyHashPair.second,
                                   hashFunc);
                if (valid)
                {
                    break;
                }
            }
        }
        catch (const InternalFailure& e)
        {
            valid = false;
        }
    }
    return valid;
}

bool Signature::verifyFile(const std::filesystem::path& file,
                           const std::filesystem::path& sigFile,
                           const std::filesystem::path& publicKey,
                           const std::string& hashFunc)
{
    // Check existence of the files in the system.
    if (!(std::filesystem::exists(file) && std::filesystem::exists(sigFile)))
    {
        log<level::ERR>("Failed to find the Data or signature file.",
                        entry("FILE=%s", file.c_str()));
        elog<InternalFailure>();
    }

    // Create RSA.
    auto publicRSA = createPublicRSA(publicKey);
    if (!publicRSA)
    {
        log<level::ERR>("Failed to create RSA",
                        entry("FILE=%s", publicKey.c_str()));
        elog<InternalFailure>();
    }

    // Initializes a digest context.
    EVP_MD_CTX_Ptr rsaVerifyCtx(EVP_MD_CTX_new(), ::EVP_MD_CTX_free);

    // Adds all digest algorithms to the internal table
    OpenSSL_add_all_digests();

    // Create Hash structure.
    auto hashStruct = EVP_get_digestbyname(hashFunc.c_str());
    if (!hashStruct)
    {
        log<level::ERR>("EVP_get_digestbynam: Unknown message digest",
                        entry("HASH=%s", hashFunc.c_str()));
        elog<InternalFailure>();
    }

    auto result = EVP_DigestVerifyInit(rsaVerifyCtx.get(), nullptr, hashStruct,
                                       nullptr, publicRSA.get());

    if (result <= 0)
    {
        log<level::ERR>("Error occurred during EVP_DigestVerifyInit",
                        entry("ERRCODE=%lu", ERR_get_error()));
        elog<InternalFailure>();
    }

    // Hash the data file and update the verification context
    auto size = std::filesystem::file_size(file);
    auto dataPtr = mapFile(file, size);

    result = EVP_DigestVerifyUpdate(rsaVerifyCtx.get(), dataPtr(), size);
    if (result <= 0)
    {
        log<level::ERR>("Error occurred during EVP_DigestVerifyUpdate",
                        entry("ERRCODE=%lu", ERR_get_error()));
        elog<InternalFailure>();
    }

    // Verify the data with signature.
    size = std::filesystem::file_size(sigFile);
    auto signature = mapFile(sigFile, size);

    result = EVP_DigestVerifyFinal(
        rsaVerifyCtx.get(), reinterpret_cast<unsigned char*>(signature()),
        size);

    // Check the verification result.
    if (result < 0)
    {
        log<level::ERR>("Error occurred during EVP_DigestVerifyFinal",
                        entry("ERRCODE=%lu", ERR_get_error()));
        elog<InternalFailure>();
    }

    if (result == 0)
    {
        log<level::ERR>("EVP_DigestVerifyFinal:Signature validation failed",
                        entry("PATH=%s", sigFile.c_str()));
        return false;
    }
    return true;
}

inline EVP_PKEY_Ptr
    Signature::createPublicRSA(const std::filesystem::path& publicKey)
{
    auto size = std::filesystem::file_size(publicKey);

    // Read public key file
    auto data = mapFile(publicKey, size);

    BIO_MEM_Ptr keyBio(BIO_new_mem_buf(data(), -1), &::BIO_free);
    if (keyBio.get() == nullptr)
    {
        log<level::ERR>("Failed to create new BIO Memory buffer");
        elog<InternalFailure>();
    }

    return {PEM_read_bio_PUBKEY(keyBio.get(), nullptr, nullptr, nullptr),
            &::EVP_PKEY_free};
}

CustomMap Signature::mapFile(const std::filesystem::path& path, size_t size)
{
    CustomFd fd(open(path.c_str(), O_RDONLY));

    return CustomMap(mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd(), 0),
                     size);
}

} // namespace image
} // namespace software
} // namespace openpower