/* SPDX-License-Identifier: BSD-3-Clause */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <curl/curl.h>
#include <openssl/buffer.h>
#include <openssl/evp.h>
#include <openssl/sha.h>
#include <json-c/json.h>

#include "fapi_crypto.h"
#include "ifapi_helpers.h"

#define LOGMODULE fapi
#include "util/log.h"
#include "util/aux_util.h"

typedef struct tpm_getekcertificate_ctx tpm_getekcertificate_ctx;
struct tpm_getekcertificate_ctx {
    char *ec_cert_path;
    FILE *ec_cert_file_handle;
    char *ek_server_addr;
    unsigned int SSL_NO_VERIFY;
    char *ek_path;
    bool verbose;
    bool is_tpm2_device_active;
    TPM2B_PUBLIC *out_public;
};

static tpm_getekcertificate_ctx ctx = {
    .is_tpm2_device_active = true,
};

/** Compute the SHA256 hash from the public key of an EK.
 *
 * @param[in]  ek_public The public information of the EK.
 * @retval unsigned_char* The hash value.
 * @retval NULL If the computation of the hash fails.
 */
static unsigned char *hash_ek_public(TPM2B_PUBLIC *ek_public) {

    unsigned char *hash = (unsigned char *)malloc(SHA256_DIGEST_LENGTH);
    if (!hash) {
        LOG_ERROR("OOM");
        return NULL;
    }

    SHA256_CTX sha256;
    int is_success = SHA256_Init(&sha256);
    if (!is_success) {
        LOG_ERROR("SHA256_Init failed");
        goto err;
    }

    switch (ek_public->publicArea.type) {
    case TPM2_ALG_RSA:
        /* Add public key to the hash. */
        is_success = SHA256_Update(&sha256,
                                   ek_public->publicArea.unique.rsa.buffer,
                                   ek_public->publicArea.unique.rsa.size);
        if (!is_success) {
            LOG_ERROR("SHA256_Update failed");
            goto err;
        }

        /* Add exponent to the hash. */
        if (ek_public->publicArea.parameters.rsaDetail.exponent != 0) {
            LOG_ERROR("non-default exponents unsupported");
            goto err;
        }
        /* Exponent 65537 will be added. */
        BYTE buf[3] = { 0x1, 0x00, 0x01 };
        is_success = SHA256_Update(&sha256, buf, sizeof(buf));
        if (!is_success) {
            LOG_ERROR("SHA256_Update failed");
            goto err;
        }
        break;

    case TPM2_ALG_ECC:
        is_success = SHA256_Update(&sha256,
                                   ek_public->publicArea.unique.ecc.x.buffer,
                                   ek_public->publicArea.unique.ecc.x.size);
        if (!is_success) {
            LOG_ERROR("SHA256_Update failed");
            goto err;
        }

        /* Add public key to the hash. */
        is_success = SHA256_Update(&sha256,
                                   ek_public->publicArea.unique.ecc.y.buffer,
                                   ek_public->publicArea.unique.ecc.y.size);
        if (!is_success) {
            LOG_ERROR("SHA256_Update failed");
            goto err;
        }
        break;

    default:
        LOG_ERROR("unsupported EK algorithm");
        goto err;
    }

    is_success = SHA256_Final(hash, &sha256);
    if (!is_success) {
        LOG_ERROR("SHA256_Final failed");
        goto err;
    }

    LOG_TRACE("public-key-hash:");
    LOG_TRACE("  sha256: ");
    LOGBLOB_TRACE(&hash[0], SHA256_DIGEST_LENGTH, "Hash");
    return hash;
err:
    free(hash);
    return NULL;
}

/** Calculate the base64 encoding of the hash of the Endorsement Public Key.
 *
 * @param[in] buffer The hash of the endorsement public key.
 * @retval char* The base64 encoded string.
 * @retval NULL if the encoding fails.
 */
static char *
base64_encode(const unsigned char* buffer)
{
    BIO *bio, *b64;
    BUF_MEM *buffer_pointer;

    LOG_INFO("Calculating the base64_encode of the hash of the Endorsement"
             "Public Key:");

    if (buffer == NULL) {
        LOG_ERROR("hash_ek_public returned null");
        return NULL;
    }

    b64 = BIO_new(BIO_f_base64());
    bio = BIO_new(BIO_s_mem());
    bio = BIO_push(b64, bio);
    BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
    BIO_write(bio, buffer, SHA256_DIGEST_LENGTH);
    (void)(BIO_flush(bio));
    BIO_get_mem_ptr(bio, &buffer_pointer);

    /* these are not NULL terminated */
    char *b64text = buffer_pointer->data;
    size_t len = buffer_pointer->length;

    size_t i;
    for (i = 0; i < len; i++) {
        if (b64text[i] == '+') {
            b64text[i] = '-';
        }
        if (b64text[i] == '/') {
            b64text[i] = '_';
        }
    }

    char *final_string = NULL;

    CURL *curl = curl_easy_init();
    if (curl) {
        char *output = curl_easy_escape(curl, b64text, len);
        if (output) {
            final_string = strdup(output);
            curl_free(output);
        }
    }
    curl_easy_cleanup(curl);
    curl_global_cleanup();
    BIO_free_all(bio);

    /* format to a proper NULL terminated string */
    return final_string;
}

/** Decode a base64 encoded certificate into binary form.
 *
 * @param[in]  buffer The base64 encoded certificate.
 * @param[in]  len The length of the encoded certificate.
 * @param[out] new_len The lenght of the binary certificate.
 * @retval char* The binary data of the certificate.
 * @retval NULL if the decoding fails.
 */
static char *
base64_decode(unsigned char* buffer, size_t len, size_t *new_len)
{
    size_t i, unescape_len, r;
    char *binary_data = NULL, *unescaped_string = NULL;

    LOG_INFO("Decoding the base64 encoded cert into binary form");

    if (buffer == NULL) {
        LOG_ERROR("Cert buffer is null");
        return NULL;
    }

    for (i = 0; i < len; i++) {
        if (buffer[i] == '-') {
            buffer[i] = '+';
        }
        if (buffer[i] == '_') {
            buffer[i] = '/';
        }
    }

    CURL *curl = curl_easy_init();
    if (curl) {
        /* Convert URL encoded string to a "plain string" */
        char *output = curl_easy_unescape(curl, (char *)buffer,
                                          len, (int *)&unescape_len);
        if (output) {
            unescaped_string = strdup(output);
            curl_free(output);
        }
    }
    curl_easy_cleanup(curl);
    curl_global_cleanup();
    if (unescaped_string == NULL)
        return NULL;

    binary_data = calloc(1, unescape_len);
    if (binary_data == NULL) {
        free (unescaped_string);
        return NULL;
    }

    BIO *bio, *b64;
    bio = BIO_new_mem_buf(unescaped_string, -1);
    b64 = BIO_new(BIO_f_base64());
    bio = BIO_push(b64, bio);
    BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);

    if ((r = BIO_read(bio, binary_data, unescape_len)) <= 0) {
        LOG_ERROR("BIO_read base64 encoded cert failed");
        free(binary_data);
        binary_data = NULL;
    }
    *new_len = r;

    free (unescaped_string);
    BIO_free_all(bio);
    return binary_data;
}

/** Get endorsement certificate from the WEB.
 *
 * The base64 encoded public endorsement key will be added to the INTEL
 * server address and used as URL to retrieve the certificate.
 * The certificate will be retrieved via curl.
 *
 * @param[in]  b64h The base64 encoded public key.
 * @param[out] buffer The json encoded certificate.
 * @param[out] cert_size The size of the certificate.
 */
int retrieve_endorsement_certificate(char *b64h, unsigned char ** buffer,
                                     size_t *cert_size) {
    int ret = -1;

    size_t len = 1 + strlen(b64h) + strlen(ctx.ek_server_addr);
    char *weblink = (char *) malloc(len);

    if (!weblink) {
        LOG_ERROR("oom");
        return ret;
    }

    snprintf(weblink, len, "%s%s", ctx.ek_server_addr, b64h);

    CURLcode rc =  ifapi_get_curl_buffer((unsigned char *)weblink,
                                         buffer, cert_size);
    free(weblink);
    return rc;
}

/** Get INTEL certificate for EK
 *
 * Using the base64 encoded public endorsement key the JSON encoded certificate
 * will be downloaded.
 * The JSON certificate will be parsed and the base64 encoded certificate
 * will be converted into binary format.
 *
 *
 * @param[in] context The FAPI context with the configuration data.
 * @param[in] ek_public The out public data of the EK.
 * @param[out] cert_buffer the der encoded certificate.
 * @param[out] cert_size The size of the certificate buffer.
 *
 * @retval TSS2_RC_SUCCESS on success.
 * @retval TSS2_FAPI_RC_NO_CERT If an error did occur during certificate downloading.
 * @retval TSS2_FAPI_RC_GENERAL_FAILURE if an internal error occured.
 * @retval TSS2_FAPI_RC_MEMORY if not enough memory can be allocated.
 */
TSS2_RC
ifapi_get_intl_ek_certificate(FAPI_CONTEXT *context, TPM2B_PUBLIC *ek_public,
                              unsigned char ** cert_buffer, size_t *cert_size)
{
    int rc = 1;
    unsigned char *hash = hash_ek_public(ek_public);
    char *cert_ptr;
    char *cert_start = NULL, *cert_bin = NULL;
    char *b64 = base64_encode(hash);

    if (!b64) {
        LOG_ERROR("base64_encode returned null");
        goto out;
    }
    if (context->config.intel_cert_service)
        ctx.ek_server_addr = context->config.intel_cert_service;
    else
        ctx.ek_server_addr = "https://ekop.intel.com/ekcertservice/";

    LOG_INFO("%s", b64);

    /* Download the JSON encoded certificate. */
    rc = retrieve_endorsement_certificate(b64, cert_buffer, cert_size);
    free(b64);
    goto_if_error(rc, "Retrieve endorsement certificate", out);
    cert_ptr = (char *)*cert_buffer;
    LOGBLOB_DEBUG((uint8_t *)cert_ptr, *cert_size, "%s", "Certificate");

    /* Parse certificate data out of the json structure */
    struct json_object *jso_cert, *jso = json_tokener_parse(cert_ptr);
    if (jso == NULL)
        goto_error(rc, TSS2_FAPI_RC_GENERAL_FAILURE,
                   "Failed to parse EK cert data", out_free_json);

    if (!json_object_object_get_ex(jso, "certificate", &jso_cert))
        goto_error(rc, TSS2_FAPI_RC_GENERAL_FAILURE,
                   "Could not find cert object", out_free_json);

    if (!json_object_is_type(jso_cert, json_type_string))
        goto_error(rc, TSS2_FAPI_RC_GENERAL_FAILURE,
                   "Invalid EK cert data", out_free_json);

    cert_start = strdup(json_object_get_string(jso_cert));
    if (!cert_start) {
        SAFE_FREE(cert_ptr);
        goto_error(rc, TSS2_FAPI_RC_MEMORY,
                   "Failed to duplicate cert", out_free_json);
    }

    *cert_size = strlen(cert_start);

    /* Base64 decode buffer into binary PEM format */
    cert_bin = base64_decode((unsigned char *)cert_start,
                             *cert_size, cert_size);
    SAFE_FREE(cert_ptr);
    SAFE_FREE(cert_start);

    if (cert_bin == NULL) {
        goto_error(rc, TSS2_FAPI_RC_GENERAL_FAILURE,
                   "Invalid EK cert data", out_free_json);
    }
    LOG_DEBUG("Binary cert size %zu", *cert_size);
    *cert_buffer = (unsigned char *)cert_bin;

out_free_json:
    json_object_put(jso);

out:
    /* In some case this call was necessary after curl usage */
    OpenSSL_add_all_algorithms();

    free(hash);
    if (rc == 0) {
        return TSS2_RC_SUCCESS;
    } else {
        LOG_ERROR("Get INTEL EK certificate.");
        return TSS2_FAPI_RC_NO_CERT;
    }
}
