diff --git a/ta/qcom_pas/include/pas_auth.h b/ta/qcom_pas/include/pas_auth.h new file mode 100644 index 000000000..d24c7f2e6 --- /dev/null +++ b/ta/qcom_pas/include/pas_auth.h @@ -0,0 +1,79 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +/* + * Copyright (c) 2026, Qualcomm Technologies, Inc. and/or its subsidiaries. + */ + +#ifndef _PAS_AUTH_H +#define _PAS_AUTH_H + +#include +#include +#include + +/* + * PAS firmware authentication helpers. + * + * Verify that each loadable ELF program segment of the peripheral firmware + * matches the corresponding entry in the firmware hash table. This proves + * image integrity: the loaded image is exactly the one described by the + * accompanying hash table. + * + * The cryptographic primitives are provided by the TEE Internal Core API + * (digest operations); these helpers do not call any crypto library directly. + */ + +/* Maximum supported segment hash size (SHA-384). */ +#define PAS_AUTH_MAX_HASH_SIZE 48U + +/* + * struct pas_auth_hash_ctx - per-image authentication context + * @hash_algo: TEE_ALG_SHA256 or TEE_ALG_SHA384 + * @hash_size: digest size in bytes (32 for SHA-256, 48 for SHA-384) + * @hash_table: pointer to the authenticated hash table (one digest per + * firmware segment, laid out contiguously) + * @num_entries: number of digest entries in @hash_table + * + * The hash table is owned by the caller for the lifetime of the verification. + */ +struct pas_auth_hash_ctx { + uint32_t hash_algo; + uint32_t hash_size; + const uint8_t *hash_table; + uint32_t num_entries; +}; + +/* + * pas_auth_verify_hash() - hash a buffer and compare to an expected digest + * @hash_algo: TEE_ALG_SHA256 or TEE_ALG_SHA384 + * @data: input buffer to hash + * @data_len: length of @data in bytes + * @expected: expected digest to match against + * @hash_size: digest size in bytes (must match @hash_algo) + * + * Computes the digest of @data using the OP-TEE digest API and compares it, + * in constant time, against @expected. + * + * Return TEE_SUCCESS if the digest matches, TEE_ERROR_SECURITY on mismatch, + * or an appropriate error on failure. + */ +TEE_Result pas_auth_verify_hash(uint32_t hash_algo, const uint8_t *data, + size_t data_len, const uint8_t *expected, + size_t hash_size); + +/* + * pas_auth_verify_segments() - per-segment hash verification + * @ctx: authentication context holding the hash table and algorithm + * @fw: firmware ELF image buffer + * @fw_size: size of @fw in bytes + * + * Parses the program headers of @fw (ELFCLASS32 or ELFCLASS64) and, for each + * PT_LOAD segment, computes its digest and compares it (constant time) to the + * matching entry in @ctx->hash_table. Fails closed on the first mismatch. + * + * Return TEE_SUCCESS if every segment matches, TEE_ERROR_SECURITY on any + * mismatch, or an appropriate error on malformed input. + */ +TEE_Result pas_auth_verify_segments(const struct pas_auth_hash_ctx *ctx, + const uint8_t *fw, size_t fw_size); + +#endif /* _PAS_AUTH_H */ diff --git a/ta/qcom_pas/src/pas_auth.c b/ta/qcom_pas/src/pas_auth.c new file mode 100644 index 000000000..2aff41da1 --- /dev/null +++ b/ta/qcom_pas/src/pas_auth.c @@ -0,0 +1,232 @@ +// SPDX-License-Identifier: BSD-2-Clause +/* + * Copyright (c) 2026, Qualcomm Technologies, Inc. and/or its subsidiaries. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * Firmware hash verification. + * + * The firmware delivered for PAS bring-up is an ELF image accompanied by a + * hash table holding one digest per loadable program segment. Each segment is + * hashed and compared against its hash-table entry using the TEE Internal Core + * digest API, so the loaded image is guaranteed to match the hash table. + */ + +TEE_Result pas_auth_verify_hash(uint32_t hash_algo, const uint8_t *data, + size_t data_len, const uint8_t *expected, + size_t hash_size) +{ + TEE_OperationHandle op = TEE_HANDLE_NULL; + uint8_t digest[PAS_AUTH_MAX_HASH_SIZE] = { }; + size_t digest_len = sizeof(digest); + TEE_Result res = TEE_ERROR_GENERIC; + + if (!data || !expected || !hash_size || hash_size > sizeof(digest)) + return TEE_ERROR_BAD_PARAMETERS; + + res = TEE_AllocateOperation(&op, hash_algo, TEE_MODE_DIGEST, 0); + if (res != TEE_SUCCESS) + return res; + + res = TEE_DigestDoFinal(op, data, data_len, digest, &digest_len); + if (res != TEE_SUCCESS) + goto out; + + if (digest_len != hash_size) { + res = TEE_ERROR_SECURITY; + goto out; + } + + /* Constant-time compare to avoid leaking the match position */ + if (consttime_memcmp(digest, expected, hash_size) != 0) { + res = TEE_ERROR_SECURITY; + goto out; + } + + res = TEE_SUCCESS; +out: + TEE_FreeOperation(op); + memzero_explicit(digest, sizeof(digest)); + + return res; +} + +/* + * Bounds-check that [off, off + len) lies fully inside [0, total). + * Guards against integer overflow in the addition. + */ +static bool range_within(size_t off, size_t len, size_t total) +{ + size_t end = 0; + + if (ADD_OVERFLOW(off, len, &end)) + return false; + + return end <= total; +} + +/* + * struct pas_elf_info - ELF class-agnostic view of the program-header table + * @phoff: program-header table file offset + * @phentsize: size of one program-header entry + * @phnum: number of program-header entries + * @is_64: true for ELFCLASS64, false for ELFCLASS32 + */ +struct pas_elf_info { + size_t phoff; + size_t phentsize; + size_t phnum; + bool is_64; +}; + +/* + * Parse and validate the ELF identification and the program-header table + * location for both ELFCLASS32 and ELFCLASS64 images. + */ +static TEE_Result pas_parse_elf(const uint8_t *fw, size_t fw_size, + struct pas_elf_info *info) +{ + const unsigned char *e_ident = fw; + + if (fw_size < EI_NIDENT) + return TEE_ERROR_BAD_FORMAT; + + if (e_ident[EI_MAG0] != ELFMAG0 || e_ident[EI_MAG1] != ELFMAG1 || + e_ident[EI_MAG2] != ELFMAG2 || e_ident[EI_MAG3] != ELFMAG3) + return TEE_ERROR_BAD_FORMAT; + + if (e_ident[EI_CLASS] == ELFCLASS64) { + const Elf64_Ehdr *ehdr = (const void *)fw; + + if (fw_size < sizeof(*ehdr)) + return TEE_ERROR_BAD_FORMAT; + + info->is_64 = true; + info->phoff = ehdr->e_phoff; + info->phentsize = ehdr->e_phentsize; + info->phnum = ehdr->e_phnum; + + if (info->phentsize < sizeof(Elf64_Phdr)) + return TEE_ERROR_BAD_FORMAT; + } else if (e_ident[EI_CLASS] == ELFCLASS32) { + const Elf32_Ehdr *ehdr = (const void *)fw; + + if (fw_size < sizeof(*ehdr)) + return TEE_ERROR_BAD_FORMAT; + + info->is_64 = false; + info->phoff = ehdr->e_phoff; + info->phentsize = ehdr->e_phentsize; + info->phnum = ehdr->e_phnum; + + if (info->phentsize < sizeof(Elf32_Phdr)) + return TEE_ERROR_BAD_FORMAT; + } else { + return TEE_ERROR_BAD_FORMAT; + } + + return TEE_SUCCESS; +} + +/* + * Read the type, file offset and file size of program header @idx, handling + * both ELF classes. + */ +static void pas_get_phdr(const uint8_t *fw, const struct pas_elf_info *info, + size_t idx, uint32_t *p_type, size_t *p_offset, + size_t *p_filesz) +{ + const uint8_t *p = fw + info->phoff + idx * info->phentsize; + + if (info->is_64) { + const Elf64_Phdr *phdr = (const void *)p; + + *p_type = phdr->p_type; + *p_offset = phdr->p_offset; + *p_filesz = phdr->p_filesz; + } else { + const Elf32_Phdr *phdr = (const void *)p; + + *p_type = phdr->p_type; + *p_offset = phdr->p_offset; + *p_filesz = phdr->p_filesz; + } +} + +TEE_Result pas_auth_verify_segments(const struct pas_auth_hash_ctx *ctx, + const uint8_t *fw, size_t fw_size) +{ + struct pas_elf_info info = { }; + size_t i = 0; + size_t seg_idx = 0; + TEE_Result res = TEE_ERROR_GENERIC; + + if (!ctx || !ctx->hash_table || !fw) + return TEE_ERROR_BAD_PARAMETERS; + + if (ctx->hash_size > PAS_AUTH_MAX_HASH_SIZE || !ctx->hash_size) + return TEE_ERROR_BAD_PARAMETERS; + + res = pas_parse_elf(fw, fw_size, &info); + if (res != TEE_SUCCESS) + return res; + + /* The whole program-header table must lie within the image */ + if (!range_within(info.phoff, info.phentsize * info.phnum, fw_size)) + return TEE_ERROR_BAD_FORMAT; + + for (i = 0; i < info.phnum; i++) { + uint32_t p_type = 0; + size_t p_offset = 0; + size_t p_filesz = 0; + const uint8_t *expected = NULL; + + pas_get_phdr(fw, &info, i, &p_type, &p_offset, &p_filesz); + + if (p_type != PT_LOAD) + continue; + + /* A loadable segment with no file content has no hash */ + if (!p_filesz) + continue; + + /* The segment bytes must lie within the image */ + if (!range_within(p_offset, p_filesz, fw_size)) + return TEE_ERROR_BAD_FORMAT; + + /* There must be a hash-table entry for this segment */ + if (seg_idx >= ctx->num_entries) + return TEE_ERROR_SECURITY; + + expected = ctx->hash_table + seg_idx * ctx->hash_size; + + res = pas_auth_verify_hash(ctx->hash_algo, fw + p_offset, + p_filesz, expected, ctx->hash_size); + if (res != TEE_SUCCESS) { + EMSG("PAS segment %zu hash check failed", seg_idx); + return res; + } + + seg_idx++; + } + + if (!seg_idx) { + /* No loadable segment was verified - treat as malformed */ + EMSG("PAS auth: no loadable segments found"); + return TEE_ERROR_BAD_FORMAT; + } + + DMSG("PAS auth: %zu segment(s) hash-verified", seg_idx); + + return TEE_SUCCESS; +} diff --git a/ta/qcom_pas/src/qcom_pas.c b/ta/qcom_pas/src/qcom_pas.c index b0661d33c..6ed1b7008 100644 --- a/ta/qcom_pas/src/qcom_pas.c +++ b/ta/qcom_pas/src/qcom_pas.c @@ -3,16 +3,103 @@ * Copyright (c) 2026, Qualcomm Technologies, Inc. and/or its subsidiaries. */ +#include +#include #include +#include #include #include #include #include #include +/* + * Firmware metadata captured at TA_QCOM_PAS_INIT_IMAGE and consumed at + * TA_QCOM_PAS_AUTH_AND_RESET to verify the image before the peripheral is + * brought out of reset. The metadata carries the per-segment hash table. + */ +static void *pas_fw_metadata; +static size_t pas_fw_metadata_size; + static size_t session_refcount; static TEE_TASessionHandle pta_session; +/* + * Keep a TA-private copy of the firmware metadata received with INIT_IMAGE so + * the hash table is verified against an immutable reference that the REE + * cannot modify between INIT_IMAGE and AUTH_AND_RESET. + */ +static TEE_Result qcom_pas_save_metadata(uint32_t pt, + TEE_Param params[TEE_NUM_PARAMS]) +{ + const uint32_t exp_pt = TEE_PARAM_TYPES(TEE_PARAM_TYPE_VALUE_INPUT, + TEE_PARAM_TYPE_MEMREF_INPUT, + TEE_PARAM_TYPE_NONE, + TEE_PARAM_TYPE_NONE); + void *copy = NULL; + size_t size = 0; + + if (pt != exp_pt) + return TEE_ERROR_BAD_PARAMETERS; + + size = params[1].memref.size; + if (size) { + copy = TEE_Malloc(size, TEE_MALLOC_FILL_ZERO); + if (!copy) + return TEE_ERROR_OUT_OF_MEMORY; + memcpy(copy, params[1].memref.buffer, size); + } + + TEE_Free(pas_fw_metadata); + pas_fw_metadata = copy; + pas_fw_metadata_size = size; + + return TEE_SUCCESS; +} + +static TEE_Result qcom_pas_init_image(uint32_t pt, + TEE_Param params[TEE_NUM_PARAMS]) +{ + TEE_Result res = TEE_SUCCESS; + + if (IS_ENABLED(CFG_QCOM_PAS_AUTH_LOCAL)) { + res = qcom_pas_save_metadata(pt, params); + if (res != TEE_SUCCESS) + return res; + } + + return TEE_InvokeTACommand(pta_session, TEE_TIMEOUT_INFINITE, + PTA_QCOM_PAS_INIT_IMAGE, pt, params, NULL); +} + +/* + * Verify the firmware image against the per-segment hash table held in the + * metadata captured at INIT_IMAGE. The image to verify is passed in + * params[2] (memref). Verification must succeed before the peripheral is + * brought out of reset. + */ +static TEE_Result qcom_pas_verify_firmware(TEE_Param params[TEE_NUM_PARAMS]) +{ + struct pas_auth_hash_ctx ctx = { }; + const uint8_t *fw = params[2].memref.buffer; + size_t fw_size = params[2].memref.size; + + if (!pas_fw_metadata || !pas_fw_metadata_size) { + EMSG("PAS auth: missing firmware metadata (INIT_IMAGE first)"); + return TEE_ERROR_BAD_STATE; + } + + if (!fw || !fw_size) + return TEE_ERROR_BAD_PARAMETERS; + + ctx.hash_algo = TEE_ALG_SHA384; + ctx.hash_size = TEE_SHA384_HASH_SIZE; + ctx.hash_table = pas_fw_metadata; + ctx.num_entries = pas_fw_metadata_size / ctx.hash_size; + + return pas_auth_verify_segments(&ctx, fw, fw_size); +} + static TEE_Result qcom_pas_auth_and_reset(uint32_t pt, TEE_Param params[TEE_NUM_PARAMS]) { @@ -20,10 +107,18 @@ static TEE_Result qcom_pas_auth_and_reset(uint32_t pt, TEE_PARAM_TYPE_VALUE_INPUT, TEE_PARAM_TYPE_MEMREF_INPUT, TEE_PARAM_TYPE_NONE); + TEE_Result res = TEE_SUCCESS; + if (pt != exp_pt) return TEE_ERROR_BAD_PARAMETERS; - /* Firmware authentication - TODO */ + if (IS_ENABLED(CFG_QCOM_PAS_AUTH_LOCAL)) { + res = qcom_pas_verify_firmware(params); + if (res != TEE_SUCCESS) { + EMSG("PAS firmware auth failed: %#"PRIx32, res); + return res; + } + } return TEE_InvokeTACommand(pta_session, TEE_TIMEOUT_INFINITE, PTA_QCOM_PAS_AUTH_AND_RESET, @@ -101,9 +196,7 @@ TEE_Result TA_InvokeCommandEntryPoint(void *sess __unused, uint32_t cmd_id, PTA_QCOM_PAS_CAPABILITIES, pt, params, NULL); case TA_QCOM_PAS_INIT_IMAGE: - return TEE_InvokeTACommand(pta_session, TEE_TIMEOUT_INFINITE, - PTA_QCOM_PAS_INIT_IMAGE, - pt, params, NULL); + return qcom_pas_init_image(pt, params); case TA_QCOM_PAS_MEM_SETUP: return TEE_InvokeTACommand(pta_session, TEE_TIMEOUT_INFINITE, PTA_QCOM_PAS_MEM_SETUP, diff --git a/ta/qcom_pas/src/sub.mk b/ta/qcom_pas/src/sub.mk index f572b0463..b1c612c69 100644 --- a/ta/qcom_pas/src/sub.mk +++ b/ta/qcom_pas/src/sub.mk @@ -1 +1,3 @@ +global-incdirs-y += ../include srcs-y += qcom_pas.c +srcs-y += pas_auth.c diff --git a/ta/qcom_pas/user_ta.mk b/ta/qcom_pas/user_ta.mk index 61a059b98..820c9ff08 100644 --- a/ta/qcom_pas/user_ta.mk +++ b/ta/qcom_pas/user_ta.mk @@ -2,3 +2,8 @@ user-ta-uuid := cff7d191-7ca0-4784-af13-48223b9a4fbe # PAS TA heap size can be customized if 4kB is not enough CFG_PAS_TA_HEAP_SIZE ?= (4 * 1024) + +# When enabled, the TA verifies the firmware image locally in OP-TEE before +# the peripheral is brought out of reset. Disable on configurations where the +# TA is not responsible for verifying the image. +CFG_QCOM_PAS_AUTH_LOCAL ?= y