/*
 * SPDX-License-Identifier: GPL-2.0-or-later
 *
 * uefi vars device - AuthVariableLib
 */

#include "qemu/osdep.h"
#include "qemu/error-report.h"
#include "system/dma.h"

#include "hw/uefi/var-service.h"

static const uint16_t name_pk[]           = u"PK";
static const uint16_t name_kek[]          = u"KEK";
static const uint16_t name_db[]           = u"db";
static const uint16_t name_dbx[]          = u"dbx";
static const uint16_t name_setup_mode[]   = u"SetupMode";
static const uint16_t name_sigs_support[] = u"SignatureSupport";
static const uint16_t name_sb[]           = u"SecureBoot";
static const uint16_t name_sb_enable[]    = u"SecureBootEnable";
static const uint16_t name_custom_mode[]  = u"CustomMode";
static const uint16_t name_vk[]           = u"VendorKeys";
static const uint16_t name_vk_nv[]        = u"VendorKeysNv";

static const uint32_t sigdb_attrs =
    EFI_VARIABLE_NON_VOLATILE |
    EFI_VARIABLE_BOOTSERVICE_ACCESS |
    EFI_VARIABLE_RUNTIME_ACCESS |
    EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS;

static void set_secure_boot(uefi_vars_state *uv, uint8_t sb)
{
    uefi_vars_set_variable(uv, EfiGlobalVariable,
                           name_sb, sizeof(name_sb),
                           EFI_VARIABLE_BOOTSERVICE_ACCESS |
                           EFI_VARIABLE_RUNTIME_ACCESS,
                           &sb, sizeof(sb));
}

static void set_secure_boot_enable(uefi_vars_state *uv, uint8_t sbe)
{
    uefi_vars_set_variable(uv, EfiSecureBootEnableDisable,
                           name_sb_enable, sizeof(name_sb_enable),
                           EFI_VARIABLE_NON_VOLATILE |
                           EFI_VARIABLE_BOOTSERVICE_ACCESS,
                           &sbe, sizeof(sbe));
}

static void set_setup_mode(uefi_vars_state *uv, uint8_t sm)
{
    uefi_vars_set_variable(uv, EfiGlobalVariable,
                           name_setup_mode, sizeof(name_setup_mode),
                           EFI_VARIABLE_BOOTSERVICE_ACCESS |
                           EFI_VARIABLE_RUNTIME_ACCESS,
                           &sm, sizeof(sm));
}

static void set_custom_mode(uefi_vars_state *uv, uint8_t cm)
{
    uefi_vars_set_variable(uv, EfiCustomModeEnable,
                           name_custom_mode, sizeof(name_custom_mode),
                           EFI_VARIABLE_NON_VOLATILE |
                           EFI_VARIABLE_BOOTSERVICE_ACCESS,
                           &cm, sizeof(cm));
}

static void set_signature_support(uefi_vars_state *uv)
{
    QemuUUID sigs_support[5];

    sigs_support[0] = EfiCertSha256Guid;
    sigs_support[1] = EfiCertSha384Guid;
    sigs_support[2] = EfiCertSha512Guid;
    sigs_support[3] = EfiCertRsa2048Guid;
    sigs_support[4] = EfiCertX509Guid;

    uefi_vars_set_variable(uv, EfiGlobalVariable,
                           name_sigs_support, sizeof(name_sigs_support),
                           EFI_VARIABLE_BOOTSERVICE_ACCESS |
                           EFI_VARIABLE_RUNTIME_ACCESS,
                           sigs_support, sizeof(sigs_support));
}

static bool setup_mode_is_active(uefi_vars_state *uv)
{
    uefi_variable *var;
    uint8_t *value;

    var = uefi_vars_find_variable(uv, EfiGlobalVariable,
                                  name_setup_mode, sizeof(name_setup_mode));
    if (var) {
        value = var->data;
        if (value[0] == SETUP_MODE) {
            return true;
        }
    }
    return false;
}

static bool custom_mode_is_active(uefi_vars_state *uv)
{
    uefi_variable *var;
    uint8_t *value;

    var = uefi_vars_find_variable(uv, EfiCustomModeEnable,
                                  name_custom_mode, sizeof(name_custom_mode));
    if (var) {
        value = var->data;
        if (value[0] == CUSTOM_SECURE_BOOT_MODE) {
            return true;
        }
    }
    return false;
}

bool uefi_vars_is_sb_pk(uefi_variable *var)
{
    if (qemu_uuid_is_equal(&var->guid, &EfiGlobalVariable) &&
        uefi_str_equal(var->name, var->name_size, name_pk, sizeof(name_pk))) {
        return true;
    }
    return false;
}

static bool uefi_vars_is_sb_kek(uefi_variable *var)
{
    if (qemu_uuid_is_equal(&var->guid, &EfiGlobalVariable) &&
        uefi_str_equal(var->name, var->name_size, name_kek, sizeof(name_kek))) {
        return true;
    }
    return false;
}

static bool uefi_vars_is_sb_db(uefi_variable *var)
{
    if (!qemu_uuid_is_equal(&var->guid, &EfiImageSecurityDatabase)) {
        return false;
    }
    if (uefi_str_equal(var->name, var->name_size, name_db, sizeof(name_db))) {
        return true;
    }
    if (uefi_str_equal(var->name, var->name_size, name_dbx, sizeof(name_dbx))) {
        return true;
    }
    return false;
}

bool uefi_vars_is_sb_any(uefi_variable *var)
{
    if (uefi_vars_is_sb_pk(var) ||
        uefi_vars_is_sb_kek(var) ||
        uefi_vars_is_sb_db(var)) {
        return true;
    }
    return false;
}

static uefi_variable *uefi_vars_find_siglist(uefi_vars_state *uv,
                                             uefi_variable *var)
{
    if (uefi_vars_is_sb_pk(var)) {
        return uefi_vars_find_variable(uv, EfiGlobalVariable,
                                       name_pk, sizeof(name_pk));
    }
    if (uefi_vars_is_sb_kek(var)) {
        return uefi_vars_find_variable(uv, EfiGlobalVariable,
                                       name_pk, sizeof(name_pk));
    }
    if (uefi_vars_is_sb_db(var)) {
        return uefi_vars_find_variable(uv, EfiGlobalVariable,
                                       name_kek, sizeof(name_kek));
    }

    return NULL;
}

static efi_status uefi_vars_check_auth_2_sb(uefi_vars_state *uv,
                                            uefi_variable *var,
                                            mm_variable_access *va,
                                            void *data,
                                            uint64_t data_offset)
{
    variable_auth_2 auth;
    uefi_variable *siglist;

    memcpy(&auth, data, sizeof(auth));
    if (custom_mode_is_active(uv)) {
        /* no authentication in custom mode */
        return EFI_SUCCESS;
    }

    if (setup_mode_is_active(uv) && !uefi_vars_is_sb_pk(var)) {
        /* no authentication in setup mode (except PK) */
        return EFI_SUCCESS;
    }

    if (auth.hdr_length == (sizeof(auth) - sizeof(auth.timestamp))) {
        /* no signature (auth->cert_data is empty) */
        return EFI_SECURITY_VIOLATION;
    }

    siglist = uefi_vars_find_siglist(uv, var);
    if (!siglist && setup_mode_is_active(uv) && uefi_vars_is_sb_pk(var)) {
        /* check PK is self-signed */
        uefi_variable tmp = {
            .guid       = EfiGlobalVariable,
            .name       = (uint16_t *)name_pk,
            .name_size  = sizeof(name_pk),
            .attributes = sigdb_attrs,
            .data       = data + data_offset,
            .data_size  = va->data_size - data_offset,
        };
        return uefi_vars_check_pkcs7_2(&tmp, NULL, NULL, va, data);
    }

    return uefi_vars_check_pkcs7_2(siglist, NULL, NULL, va, data);
}

efi_status uefi_vars_check_auth_2(uefi_vars_state *uv, uefi_variable *var,
                                  mm_variable_access *va, void *data)
{
    variable_auth_2 auth;
    uint64_t data_offset;
    efi_status status;

    if (va->data_size < sizeof(auth)) {
        return EFI_SECURITY_VIOLATION;
    }
    memcpy(&auth, data, sizeof(auth));

    if (auth.hdr_length < (sizeof(auth) - sizeof(auth.timestamp))) {
        return EFI_SECURITY_VIOLATION;
    }
    if (uadd64_overflow(sizeof(efi_time), auth.hdr_length, &data_offset)) {
        return EFI_SECURITY_VIOLATION;
    }
    if (va->data_size < data_offset) {
        return EFI_SECURITY_VIOLATION;
    }

    if (auth.hdr_revision != 0x0200 ||
        auth.hdr_cert_type != WIN_CERT_TYPE_EFI_GUID ||
        !qemu_uuid_is_equal(&auth.guid_cert_type, &EfiCertTypePkcs7Guid)) {
        return EFI_UNSUPPORTED;
    }

    if (uefi_vars_is_sb_any(var)) {
        /* secure boot variables */
        status = uefi_vars_check_auth_2_sb(uv, var, va, data, data_offset);
        if (status != EFI_SUCCESS) {
            return status;
        }
    } else {
        /* other authenticated variables */
        status = uefi_vars_check_pkcs7_2(NULL,
                                         &var->digest, &var->digest_size,
                                         va, data);
        if (status != EFI_SUCCESS) {
            return status;
        }
    }

    /* checks passed, set variable data */
    var->time = auth.timestamp;
    if (va->data_size - data_offset > 0) {
        var->data = g_malloc(va->data_size - data_offset);
        memcpy(var->data, data + data_offset, va->data_size - data_offset);
        var->data_size = va->data_size - data_offset;
    }

    return EFI_SUCCESS;
}

efi_status uefi_vars_check_secure_boot(uefi_vars_state *uv, uefi_variable *var)
{
    uint8_t *value = var->data;

    if (uefi_vars_is_sb_any(var)) {
        if (var->attributes != sigdb_attrs) {
            return EFI_INVALID_PARAMETER;
        }
    }

    /* reject SecureBootEnable updates if force_secure_boot is set */
    if (qemu_uuid_is_equal(&var->guid, &EfiSecureBootEnableDisable) &&
        uefi_str_equal(var->name, var->name_size,
                       name_sb_enable, sizeof(name_sb_enable)) &&
        uv->force_secure_boot &&
        value[0] != SECURE_BOOT_ENABLE) {
        return EFI_WRITE_PROTECTED;
    }

    /* reject CustomMode updates if disable_custom_mode is set */
    if (qemu_uuid_is_equal(&var->guid, &EfiCustomModeEnable) &&
        uefi_str_equal(var->name, var->name_size,
                       name_custom_mode, sizeof(name_custom_mode)) &&
        uv->disable_custom_mode) {
        return EFI_WRITE_PROTECTED;
    }

    return EFI_SUCCESS;
}

/* AuthVariableLibInitialize */
void uefi_vars_auth_init(uefi_vars_state *uv)
{
    uefi_variable *pk_var, *sbe_var;
    uint8_t platform_mode, sb, sbe, vk;

    /* SetupMode */
    pk_var = uefi_vars_find_variable(uv, EfiGlobalVariable,
                                     name_pk, sizeof(name_pk));
    if (!pk_var) {
        platform_mode = SETUP_MODE;
    } else {
        platform_mode = USER_MODE;
    }
    set_setup_mode(uv, platform_mode);

    /* SignatureSupport */
    set_signature_support(uv);

    /* SecureBootEnable */
    sbe = SECURE_BOOT_DISABLE;
    sbe_var = uefi_vars_find_variable(uv, EfiSecureBootEnableDisable,
                                      name_sb_enable, sizeof(name_sb_enable));
    if (sbe_var) {
        if (platform_mode == USER_MODE) {
            sbe = ((uint8_t *)sbe_var->data)[0];
        }
    } else if (platform_mode == USER_MODE) {
        sbe = SECURE_BOOT_ENABLE;
        set_secure_boot_enable(uv, sbe);
    }

    if (uv->force_secure_boot && sbe != SECURE_BOOT_ENABLE) {
        sbe = SECURE_BOOT_ENABLE;
        set_secure_boot_enable(uv, sbe);
    }

    /* SecureBoot */
    if ((sbe == SECURE_BOOT_ENABLE) && (platform_mode == USER_MODE)) {
        sb = SECURE_BOOT_MODE_ENABLE;
    } else {
        sb = SECURE_BOOT_MODE_DISABLE;
    }
    set_secure_boot(uv, sb);

    /* CustomMode */
    set_custom_mode(uv, STANDARD_SECURE_BOOT_MODE);

    vk = 0;
    uefi_vars_set_variable(uv, EfiGlobalVariable,
                           name_vk_nv, sizeof(name_vk_nv),
                           EFI_VARIABLE_NON_VOLATILE |
                           EFI_VARIABLE_BOOTSERVICE_ACCESS |
                           EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS,
                           &vk, sizeof(vk));
    uefi_vars_set_variable(uv, EfiGlobalVariable,
                           name_vk, sizeof(name_vk),
                           EFI_VARIABLE_BOOTSERVICE_ACCESS |
                           EFI_VARIABLE_RUNTIME_ACCESS,
                           &vk, sizeof(vk));

    /* flush to disk */
    uefi_vars_json_save(uv);
}
