/* * EIF (Enclave Image Format) related helpers * * Copyright (c) 2024 Dorjoy Chowdhury * * This work is licensed under the terms of the GNU GPL, version 2 or * (at your option) any later version. See the COPYING file in the * top-level directory. */ #include "qemu/osdep.h" #include "qemu/bswap.h" #include "qapi/error.h" #include "crypto/hash.h" #include "crypto/x509-utils.h" #include /* for crc32 */ #include #include "hw/core/eif.h" #define MAX_SECTIONS 32 /* members are ordered according to field order in .eif file */ typedef struct EifHeader { uint8_t magic[4]; /* must be .eif in ascii i.e., [46, 101, 105, 102] */ uint16_t version; uint16_t flags; uint64_t default_memory; uint64_t default_cpus; uint16_t reserved; uint16_t section_cnt; uint64_t section_offsets[MAX_SECTIONS]; uint64_t section_sizes[MAX_SECTIONS]; uint32_t unused; uint32_t eif_crc32; } QEMU_PACKED EifHeader; /* members are ordered according to field order in .eif file */ typedef struct EifSectionHeader { /* * 0 = invalid, 1 = kernel, 2 = cmdline, 3 = ramdisk, 4 = signature, * 5 = metadata */ uint16_t section_type; uint16_t flags; uint64_t section_size; } QEMU_PACKED EifSectionHeader; enum EifSectionTypes { EIF_SECTION_INVALID = 0, EIF_SECTION_KERNEL = 1, EIF_SECTION_CMDLINE = 2, EIF_SECTION_RAMDISK = 3, EIF_SECTION_SIGNATURE = 4, EIF_SECTION_METADATA = 5, EIF_SECTION_MAX = 6, }; static const char *section_type_to_string(uint16_t type) { const char *str; switch (type) { case EIF_SECTION_INVALID: str = "invalid"; break; case EIF_SECTION_KERNEL: str = "kernel"; break; case EIF_SECTION_CMDLINE: str = "cmdline"; break; case EIF_SECTION_RAMDISK: str = "ramdisk"; break; case EIF_SECTION_SIGNATURE: str = "signature"; break; case EIF_SECTION_METADATA: str = "metadata"; break; default: str = "unknown"; break; } return str; } static bool read_eif_header(FILE *f, EifHeader *header, uint32_t *crc, Error **errp) { size_t got; size_t header_size = sizeof(*header); got = fread(header, 1, header_size, f); if (got != header_size) { error_setg(errp, "Failed to read EIF header"); return false; } if (memcmp(header->magic, ".eif", 4) != 0) { error_setg(errp, "Invalid EIF image. Magic mismatch."); return false; } /* Exclude header->eif_crc32 field from CRC calculation */ *crc = crc32(*crc, (uint8_t *)header, header_size - 4); header->version = be16_to_cpu(header->version); header->flags = be16_to_cpu(header->flags); header->default_memory = be64_to_cpu(header->default_memory); header->default_cpus = be64_to_cpu(header->default_cpus); header->reserved = be16_to_cpu(header->reserved); header->section_cnt = be16_to_cpu(header->section_cnt); for (int i = 0; i < MAX_SECTIONS; ++i) { header->section_offsets[i] = be64_to_cpu(header->section_offsets[i]); } for (int i = 0; i < MAX_SECTIONS; ++i) { header->section_sizes[i] = be64_to_cpu(header->section_sizes[i]); } header->unused = be32_to_cpu(header->unused); header->eif_crc32 = be32_to_cpu(header->eif_crc32); return true; } static bool read_eif_section_header(FILE *f, EifSectionHeader *section_header, uint32_t *crc, Error **errp) { size_t got; size_t section_header_size = sizeof(*section_header); got = fread(section_header, 1, section_header_size, f); if (got != section_header_size) { error_setg(errp, "Failed to read EIF section header"); return false; } *crc = crc32(*crc, (uint8_t *)section_header, section_header_size); section_header->section_type = be16_to_cpu(section_header->section_type); section_header->flags = be16_to_cpu(section_header->flags); section_header->section_size = be64_to_cpu(section_header->section_size); return true; } /* * Upon success, the caller is responsible for unlinking and freeing *tmp_path. */ static bool get_tmp_file(const char *template, char **tmp_path, Error **errp) { int tmp_fd; *tmp_path = NULL; tmp_fd = g_file_open_tmp(template, tmp_path, NULL); if (tmp_fd < 0 || *tmp_path == NULL) { error_setg(errp, "Failed to create temporary file for template %s", template); return false; } close(tmp_fd); return true; } static void safe_fclose(FILE *f) { if (f) { fclose(f); } } static void safe_unlink(char *f) { if (f) { unlink(f); } } /* * Upon success, the caller is reponsible for unlinking and freeing *kernel_path */ static bool read_eif_kernel(FILE *f, uint64_t size, char **kernel_path, uint8_t *kernel, uint32_t *crc, Error **errp) { size_t got; FILE *tmp_file = NULL; *kernel_path = NULL; if (!get_tmp_file("eif-kernel-XXXXXX", kernel_path, errp)) { goto cleanup; } tmp_file = fopen(*kernel_path, "wb"); if (tmp_file == NULL) { error_setg_errno(errp, errno, "Failed to open temporary file %s", *kernel_path); goto cleanup; } got = fread(kernel, 1, size, f); if ((uint64_t) got != size) { error_setg(errp, "Failed to read EIF kernel section data"); goto cleanup; } got = fwrite(kernel, 1, size, tmp_file); if ((uint64_t) got != size) { error_setg(errp, "Failed to write EIF kernel section data to temporary" " file"); goto cleanup; } *crc = crc32(*crc, kernel, size); fclose(tmp_file); return true; cleanup: safe_fclose(tmp_file); safe_unlink(*kernel_path); g_free(*kernel_path); *kernel_path = NULL; return false; } static bool read_eif_cmdline(FILE *f, uint64_t size, char *cmdline, uint32_t *crc, Error **errp) { size_t got = fread(cmdline, 1, size, f); if ((uint64_t) got != size) { error_setg(errp, "Failed to read EIF cmdline section data"); return false; } *crc = crc32(*crc, (uint8_t *)cmdline, size); return true; } static bool read_eif_ramdisk(FILE *eif, FILE *initrd, uint64_t size, uint8_t *ramdisk, uint32_t *crc, Error **errp) { size_t got; got = fread(ramdisk, 1, size, eif); if ((uint64_t) got != size) { error_setg(errp, "Failed to read EIF ramdisk section data"); return false; } got = fwrite(ramdisk, 1, size, initrd); if ((uint64_t) got != size) { error_setg(errp, "Failed to write EIF ramdisk data to temporary file"); return false; } *crc = crc32(*crc, ramdisk, size); return true; } static bool get_signature_fingerprint_sha384(FILE *eif, uint64_t size, uint8_t *sha384, uint32_t *crc, Error **errp) { size_t got; g_autofree uint8_t *sig = NULL; g_autofree uint8_t *cert = NULL; cbor_item_t *item = NULL; cbor_item_t *pcr0 = NULL; size_t len; size_t hash_len = QCRYPTO_HASH_DIGEST_LEN_SHA384; struct cbor_pair *pair; struct cbor_load_result result; bool ret = false; sig = g_malloc(size); got = fread(sig, 1, size, eif); if ((uint64_t) got != size) { error_setg(errp, "Failed to read EIF signature section data"); goto cleanup; } *crc = crc32(*crc, sig, size); item = cbor_load(sig, size, &result); if (!item || result.error.code != CBOR_ERR_NONE) { error_setg(errp, "Failed to load signature section data as CBOR"); goto cleanup; } if (!cbor_isa_array(item) || cbor_array_size(item) < 1) { error_setg(errp, "Invalid signature CBOR"); goto cleanup; } pcr0 = cbor_array_get(item, 0); if (!pcr0) { error_setg(errp, "Failed to get PCR0 signature"); goto cleanup; } if (!cbor_isa_map(pcr0) || cbor_map_size(pcr0) != 2) { error_setg(errp, "Invalid signature CBOR"); goto cleanup; } pair = cbor_map_handle(pcr0); if (!cbor_isa_string(pair->key) || cbor_string_length(pair->key) != 19 || memcmp(cbor_string_handle(pair->key), "signing_certificate", 19) != 0) { error_setg(errp, "Invalid signautre CBOR"); goto cleanup; } if (!cbor_isa_array(pair->value)) { error_setg(errp, "Invalid signature CBOR"); goto cleanup; } len = cbor_array_size(pair->value); if (len == 0) { error_setg(errp, "Invalid signature CBOR"); goto cleanup; } cert = g_malloc(len); for (int i = 0; i < len; ++i) { cbor_item_t *tmp = cbor_array_get(pair->value, i); if (!tmp) { error_setg(errp, "Invalid signature CBOR"); goto cleanup; } if (!cbor_isa_uint(tmp) || cbor_int_get_width(tmp) != CBOR_INT_8) { cbor_decref(&tmp); error_setg(errp, "Invalid signature CBOR"); goto cleanup; } cert[i] = cbor_get_uint8(tmp); cbor_decref(&tmp); } if (qcrypto_get_x509_cert_fingerprint(cert, len, QCRYPTO_HASH_ALGO_SHA384, sha384, &hash_len, errp)) { goto cleanup; } ret = true; cleanup: if (pcr0) { cbor_decref(&pcr0); } if (item) { cbor_decref(&item); } return ret; } /* Expects file to have offset 0 before this function is called */ static long get_file_size(FILE *f, Error **errp) { long size; if (fseek(f, 0, SEEK_END) != 0) { error_setg_errno(errp, errno, "Failed to seek to the end of file"); return -1; } size = ftell(f); if (size == -1) { error_setg_errno(errp, errno, "Failed to get offset"); return -1; } if (fseek(f, 0, SEEK_SET) != 0) { error_setg_errno(errp, errno, "Failed to seek back to the start"); return -1; } return size; } static bool get_SHA384_digest(GList *list, uint8_t *digest, Error **errp) { size_t digest_len = QCRYPTO_HASH_DIGEST_LEN_SHA384; size_t list_len = g_list_length(list); struct iovec *iovec_list = g_new0(struct iovec, list_len); bool ret = true; GList *l; int i; for (i = 0, l = list; l != NULL; l = l->next, i++) { iovec_list[i] = *(struct iovec *) l->data; } if (qcrypto_hash_bytesv(QCRYPTO_HASH_ALGO_SHA384, iovec_list, list_len, &digest, &digest_len, errp) < 0) { ret = false; } g_free(iovec_list); return ret; } static void free_iovec(struct iovec *iov) { if (iov) { g_free(iov->iov_base); g_free(iov); } } /* * Upon success, the caller is reponsible for unlinking and freeing * *kernel_path, *initrd_path and freeing *cmdline. */ bool read_eif_file(const char *eif_path, const char *machine_initrd, char **kernel_path, char **initrd_path, char **cmdline, uint8_t *image_sha384, uint8_t *bootstrap_sha384, uint8_t *app_sha384, uint8_t *fingerprint_sha384, bool *signature_found, Error **errp) { FILE *f = NULL; FILE *machine_initrd_f = NULL; FILE *initrd_path_f = NULL; long machine_initrd_size; uint32_t crc = 0; EifHeader eif_header; bool seen_sections[EIF_SECTION_MAX] = {false}; /* kernel + ramdisks + cmdline sha384 hash */ GList *iov_PCR0 = NULL; /* kernel + boot ramdisk + cmdline sha384 hash */ GList *iov_PCR1 = NULL; /* application ramdisk(s) hash */ GList *iov_PCR2 = NULL; uint8_t *ptr = NULL; struct iovec *iov_ptr = NULL; *signature_found = false; *kernel_path = *initrd_path = *cmdline = NULL; f = fopen(eif_path, "rb"); if (f == NULL) { error_setg_errno(errp, errno, "Failed to open %s", eif_path); goto cleanup; } if (!read_eif_header(f, &eif_header, &crc, errp)) { goto cleanup; } if (eif_header.version < 4) { error_setg(errp, "Expected EIF version 4 or greater"); goto cleanup; } if (eif_header.flags != 0) { error_setg(errp, "Expected EIF flags to be 0"); goto cleanup; } if (eif_header.section_cnt > MAX_SECTIONS) { error_setg(errp, "EIF header section count must not be greater than " "%d but found %d", MAX_SECTIONS, eif_header.section_cnt); goto cleanup; } for (int i = 0; i < eif_header.section_cnt; ++i) { EifSectionHeader hdr; uint16_t section_type; if (fseek(f, eif_header.section_offsets[i], SEEK_SET) != 0) { error_setg_errno(errp, errno, "Failed to offset to %" PRIu64 " in EIF file", eif_header.section_offsets[i]); goto cleanup; } if (!read_eif_section_header(f, &hdr, &crc, errp)) { goto cleanup; } if (hdr.flags != 0) { error_setg(errp, "Expected EIF section header flags to be 0"); goto cleanup; } if (eif_header.section_sizes[i] != hdr.section_size) { error_setg(errp, "EIF section size mismatch between header and " "section header: header %" PRIu64 ", section header %" PRIu64, eif_header.section_sizes[i], hdr.section_size); goto cleanup; } section_type = hdr.section_type; switch (section_type) { case EIF_SECTION_KERNEL: if (seen_sections[EIF_SECTION_KERNEL]) { error_setg(errp, "Invalid EIF image. More than 1 kernel " "section"); goto cleanup; } ptr = g_malloc(hdr.section_size); iov_ptr = g_malloc(sizeof(struct iovec)); iov_ptr->iov_base = ptr; iov_ptr->iov_len = hdr.section_size; iov_PCR0 = g_list_append(iov_PCR0, iov_ptr); iov_PCR1 = g_list_append(iov_PCR1, iov_ptr); if (!read_eif_kernel(f, hdr.section_size, kernel_path, ptr, &crc, errp)) { goto cleanup; } break; case EIF_SECTION_CMDLINE: { uint64_t size; uint8_t *cmdline_copy; if (seen_sections[EIF_SECTION_CMDLINE]) { error_setg(errp, "Invalid EIF image. More than 1 cmdline " "section"); goto cleanup; } size = hdr.section_size; *cmdline = g_malloc(size + 1); if (!read_eif_cmdline(f, size, *cmdline, &crc, errp)) { goto cleanup; } (*cmdline)[size] = '\0'; /* * We make a copy of '*cmdline' for putting it in iovecs so that * we can easily free all the iovec entries later as we cannot * free '*cmdline' which is used by the caller. */ cmdline_copy = g_memdup2(*cmdline, size); iov_ptr = g_malloc(sizeof(struct iovec)); iov_ptr->iov_base = cmdline_copy; iov_ptr->iov_len = size; iov_PCR0 = g_list_append(iov_PCR0, iov_ptr); iov_PCR1 = g_list_append(iov_PCR1, iov_ptr); break; } case EIF_SECTION_RAMDISK: { if (!seen_sections[EIF_SECTION_RAMDISK]) { /* * If this is the first time we are seeing a ramdisk section, * we need to create the initrd temporary file. */ if (!get_tmp_file("eif-initrd-XXXXXX", initrd_path, errp)) { goto cleanup; } initrd_path_f = fopen(*initrd_path, "wb"); if (initrd_path_f == NULL) { error_setg_errno(errp, errno, "Failed to open file %s", *initrd_path); goto cleanup; } } ptr = g_malloc(hdr.section_size); iov_ptr = g_malloc(sizeof(struct iovec)); iov_ptr->iov_base = ptr; iov_ptr->iov_len = hdr.section_size; iov_PCR0 = g_list_append(iov_PCR0, iov_ptr); /* * If it's the first ramdisk, we need to hash it into bootstrap * i.e., iov_PCR1, otherwise we need to hash it into app i.e., * iov_PCR2. */ if (!seen_sections[EIF_SECTION_RAMDISK]) { iov_PCR1 = g_list_append(iov_PCR1, iov_ptr); } else { iov_PCR2 = g_list_append(iov_PCR2, iov_ptr); } if (!read_eif_ramdisk(f, initrd_path_f, hdr.section_size, ptr, &crc, errp)) { goto cleanup; } break; } case EIF_SECTION_SIGNATURE: *signature_found = true; if (!get_signature_fingerprint_sha384(f, hdr.section_size, fingerprint_sha384, &crc, errp)) { goto cleanup; } break; default: /* other sections including invalid or unknown sections */ { uint8_t *buf; size_t got; uint64_t size = hdr.section_size; buf = g_malloc(size); got = fread(buf, 1, size, f); if ((uint64_t) got != size) { g_free(buf); error_setg(errp, "Failed to read EIF %s section data", section_type_to_string(section_type)); goto cleanup; } crc = crc32(crc, buf, size); g_free(buf); break; } } if (section_type < EIF_SECTION_MAX) { seen_sections[section_type] = true; } } if (!seen_sections[EIF_SECTION_KERNEL]) { error_setg(errp, "Invalid EIF image. No kernel section."); goto cleanup; } if (!seen_sections[EIF_SECTION_CMDLINE]) { error_setg(errp, "Invalid EIF image. No cmdline section."); goto cleanup; } if (!seen_sections[EIF_SECTION_RAMDISK]) { error_setg(errp, "Invalid EIF image. No ramdisk section."); goto cleanup; } if (eif_header.eif_crc32 != crc) { error_setg(errp, "CRC mismatch. Expected %u but header has %u.", crc, eif_header.eif_crc32); goto cleanup; } /* * Let's append the initrd file from "-initrd" option if any. Although * we pass the crc pointer to read_eif_ramdisk, it is not useful anymore. * We have already done the crc mismatch check above this code. */ if (machine_initrd) { machine_initrd_f = fopen(machine_initrd, "rb"); if (machine_initrd_f == NULL) { error_setg_errno(errp, errno, "Failed to open initrd file %s", machine_initrd); goto cleanup; } machine_initrd_size = get_file_size(machine_initrd_f, errp); if (machine_initrd_size == -1) { goto cleanup; } ptr = g_malloc(machine_initrd_size); iov_ptr = g_malloc(sizeof(struct iovec)); iov_ptr->iov_base = ptr; iov_ptr->iov_len = machine_initrd_size; iov_PCR0 = g_list_append(iov_PCR0, iov_ptr); iov_PCR2 = g_list_append(iov_PCR2, iov_ptr); if (!read_eif_ramdisk(machine_initrd_f, initrd_path_f, machine_initrd_size, ptr, &crc, errp)) { goto cleanup; } } if (!get_SHA384_digest(iov_PCR0, image_sha384, errp)) { goto cleanup; } if (!get_SHA384_digest(iov_PCR1, bootstrap_sha384, errp)) { goto cleanup; } if (!get_SHA384_digest(iov_PCR2, app_sha384, errp)) { goto cleanup; } /* * We only need to free iov_PCR0 entries because iov_PCR1 and * iov_PCR2 iovec entries are subsets of iov_PCR0 iovec entries. */ g_list_free_full(iov_PCR0, (GDestroyNotify) free_iovec); g_list_free(iov_PCR1); g_list_free(iov_PCR2); fclose(f); fclose(initrd_path_f); safe_fclose(machine_initrd_f); return true; cleanup: g_list_free_full(iov_PCR0, (GDestroyNotify) free_iovec); g_list_free(iov_PCR1); g_list_free(iov_PCR2); safe_fclose(f); safe_fclose(initrd_path_f); safe_fclose(machine_initrd_f); safe_unlink(*kernel_path); g_free(*kernel_path); *kernel_path = NULL; safe_unlink(*initrd_path); g_free(*initrd_path); *initrd_path = NULL; g_free(*cmdline); *cmdline = NULL; return false; }