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

#include <stdarg.h>
#include <stdbool.h>
#include <stdlib.h>

#include "log.h"
#include "tpm2_errata.h"
#include "tpm2_capability.h"

struct tpm2_errata_desc {
    UINT32 spec_level; /* spec level */
    UINT32 spec_rev; /* spec revision */
    UINT32 errata_ver; /* errata version */
    void (*fixup)(va_list *ap); /* errata correction handler */
    const char *name; /* full section name in errata doc */
};

/*
 * Published spec and errata information.
 *
 * Note that TPM2_PT_YEAR and TPM2_PT_DAY_OF_YEAR retrieved
 * from capability query only have the values of the
 * release date of the specification if the TPM does not
 * implement an errata. So the spec info are also given.
 */
static struct tpm2_errata_info {
    UINT32 spec_level;
    UINT32 spec_rev;
    UINT32 errata_ver;
    UINT32 year;
    UINT32 day_of_year;
} known_errata_info[] = {
    /* Specification Revision 1.16 October 30, 2014 */
    { 00, 116, 000, 2014, 303 },
    /* Errata Version 1.2 February 16, 2015 */
    { 00, 116, 120, 2015,   4 },
    /* Errata Version 1.3 June 16, 2015 */
    { 00, 116, 130, 2015, 167 },
    /* Errata Version 1.4 January 15, 2016 */
    { 00, 116, 140, 2016,  15 },
    /* Errata Version 1.5 September 21, 2016 */
    { 00, 116, 150, 2016, 265 },
    /* Specification Revision 1.38 September 29, 2016 */
    { 00, 138, 000, 2016, 273 },
    /* Errata Version 1.0 January 16, 2017 */
    { 00, 138, 100, 2017,  16 },
    /* Errata Version 1.1 March 2, 2017 */
    { 00, 138, 110, 2017,  61 },
};

static struct tpm2_errata_info *this_errata_info;

static void fixup_sign_decrypt_attribute_encoding(va_list *ap);

/*
 * Beware of that each record contains the first errata
 * version with the corresponding correction. This rule
 * allows errata_match() to function properly.
 */
static struct tpm2_errata_desc errata_desc_list[] = {
    [SPEC_116_ERRATA_2_7] = {
        .spec_level = 00,
        .spec_rev = 116,
        .errata_ver = 120,
        .fixup = fixup_sign_decrypt_attribute_encoding,
        .name = "Sign/decrypt attribute encoding",
    },
    /*
     * Append the new errata descriptor here.
     */
};

static bool errata_match(struct tpm2_errata_desc *errata);
static struct tpm2_errata_desc *errata_query(tpm2_errata_index_t index);

/*
 * Request an errata correction.
 * @index: the errata to be queried.
 *
 * This function requests an errata correction to work
 * around a known issue well documented in errata doc.
 * If the request is valid and known, the queried errata
 * will be applied by the corresponding pre-defined errata
 * correction handler. The fixup process is transparent to
 * the callers so there is no return values. Any tools can
 * call this function to apply an errata if necessary.
 *
 * Return value:
 * N/A
 */
void tpm2_errata_fixup(tpm2_errata_index_t index, ...) {

    struct tpm2_errata_desc *errata;

    /*
     * There was no match against the TPMs details to a
     * known errata.
     */
    if (!this_errata_info) {
        return;
    }

    /* Look up what errata the caller wants us to fix. */
    errata = errata_query(index);
    if (!errata) {
        return;
    }

    /*
     * Check to see if that errata matches the tpm's
     * information and thus needs to be applied.
     */
    bool res = errata_match(errata);
    if (res == false) {
        return;
    }

    va_list ap;

    va_start(ap, index);
    errata->fixup(&ap);
    va_end(ap);

    LOG_INFO("Errata %s applied", errata->name);
}

static void process(TPMS_CAPABILITY_DATA *capability_data) {
    /* Distinguish current spec level 0 */
    UINT32 spec_level = -1;
    UINT32 spec_rev = 0;
    UINT32 day_of_year = 0;
    UINT32 year = 0;
    TPML_TAGGED_TPM_PROPERTY *properties = &capability_data->data.tpmProperties;
    size_t i;
    for (i = 0; i < properties->count; ++i) {
        TPMS_TAGGED_PROPERTY *property = properties->tpmProperty + i;

        if (property->property == TPM2_PT_LEVEL) {
            spec_level = property->value;
        } else if (property->property == TPM2_PT_REVISION) {
            spec_rev = property->value;
        } else if (property->property == TPM2_PT_DAY_OF_YEAR) {
            day_of_year = property->value;
        } else if (property->property == TPM2_PT_YEAR) {
            year = property->value;
            /* Short circuit because this is the last item we need */
            break;
        } else if (property->property > TPM2_PT_YEAR) {
            break;
        }
    }

    if (!spec_rev || !day_of_year || !year) {
        LOG_WARN("Invalid TPM_SPEC parameter");
        return;
    }

    /* Determine the TPM spec and errata */
    for (i = 0; i < ARRAY_LEN(known_errata_info); ++i) {
         if (known_errata_info[i].day_of_year == day_of_year &&
             known_errata_info[i].year == year &&
             known_errata_info[i].spec_rev == spec_rev &&
             known_errata_info[i].spec_level == spec_level) {
             this_errata_info = known_errata_info + i;

            LOG_INFO("TPM_SPEC: spec level %d, spec rev %f, errata ver %f",
                    this_errata_info->spec_level,
                    (float )this_errata_info->spec_rev / 100,
                    (float )this_errata_info->errata_ver / 100);
            return;
        }
    }

    LOG_INFO("Unknown TPM_SPEC. spec_level: %d, spec_rev: 0x%x, "
            "year: %d, day_of_year: %d", spec_level, spec_rev, year,
            day_of_year);
}

void tpm2_errata_init(ESYS_CONTEXT *ctx) {

    TPMS_CAPABILITY_DATA *capability_data;
    tool_rc rc = tpm2_capability_get(ctx, TPM2_CAP_TPM_PROPERTIES,
            TPM2_PT_LEVEL, TPM2_PT_YEAR - TPM2_PT_LEVEL + 1, &capability_data);
    if (rc != tool_rc_success) {
        LOG_ERR("Failed to GetCapability: capability: 0x%x, property: 0x%x, ",
                TPM2_CAP_TPM_PROPERTIES, TPM2_PT_LEVEL);
        return;
    }

    process(capability_data);
    free(capability_data);
}

static void fixup_sign_decrypt_attribute_encoding(va_list *ap) {

    TPMA_OBJECT *attrs = va_arg(*ap, TPMA_OBJECT *);

    *attrs &= ~TPMA_OBJECT_SIGN_ENCRYPT;
}

static bool errata_match(struct tpm2_errata_desc *errata) {

    return errata->errata_ver > this_errata_info->errata_ver
            && errata->spec_rev >= this_errata_info->spec_rev
            && errata->spec_level == this_errata_info->spec_level;
}

static struct tpm2_errata_desc *errata_query(tpm2_errata_index_t index) {

    if ((size_t) index >= ARRAY_LEN(errata_desc_list)) {
        LOG_WARN("Invalid errata index queried: %u", (unsigned int )index);
        return NULL;
    }

    return &errata_desc_list[index];
}
