TEE

Trusted Firmware-M 安全启动

BL2安全启动源码浅析

Posted by Weizhou on June 30, 2021

前言

TF-M提供了安全启动,加密,验证和安全存储等功能。本文主要对Arm TrustZone Firmware-M的BL2安全启动 模块进行浅析,结合官方文档基于AN521平台的编译配置进行源码浅析。

正文

安全启动与信任链

安全启动是建立系统信任链(Chain of Trust)的基础。信任链(Chain of Trust)是基于根信任(Root of trust)创建的, 而根信任的实现是基于两种技术:不可修改的bootloader和不可被修改的公钥。公钥通常存放在OTP(One-Time-Programmable)内存中, bootloader同常存储在ROM中或者不可修改的Flash内存中。

BL2安全启动相关目录结构

安全启动代码所在路径主要集中与/trusted-firmware-m/bl2文件夹中。bl2文件夹数据结构如下所示:

├── CMakeLists.txt
├── ext
│   └── mcuboot
│       ├── bl2_main.c
│       ├── CMakeLists.txt
│       ├── config
│       │   └── mcuboot-mbedtls-cfg.h
│       ├── flash_map_extended.c
│       ├── flash_map_legacy.c
│       ├── include
│       │   ├── flash_map
│       │   │   └── flash_map.h
│       │   ├── flash_map_backend
│       │   │   └── flash_map_backend.h
│       │   ├── hal
│       │   │   └── hal_flash.h
│       │   ├── mcuboot_config
│       │   │   ├── mcuboot_config.h.in
│       │   │   └── mcuboot_logging.h
│       │   ├── os
│       │   │   └── os_malloc.h
│       │   ├── sysflash
│       │   │   └── sysflash.h
│       │   └── target.h
│       ├── keys.c
│       ├── root-RSA-2048_1.pem
│       ├── root-RSA-2048.pem
│       ├── root-RSA-3072_1.pem
│       ├── root-RSA-3072.pem
│       ├── scripts
│       │   ├── assemble.py
│       │   ├── macro_parser.py
│       │   ├── __pycache__
│       │   │   └── macro_parser.cpython-38.pyc
│       │   ├── requirements.txt
│       │   └── wrapper
│       │       └── wrapper.py
│       └── signing_layout.c.in
├── include
│   └── boot_hal.h
└── src
    ├── flash_map.c
    ├── security_cnt.c
    ├── shared_data.c
    └── shared_symbol_template.txt

当编译时启用了bl2模块,即-DBL2=ON,会启用安全启动模块。其中bl2_main.c文件中的main函数是安全启动的主要功能入口函数。
在执行cmake配置后,进入目录进行make编译后会在bin文件夹下生成如下文件:

bl2.asm  bl2.bin  bl2.hex  tfm_ns.asm  tfm_ns.bin  tfm_ns.hex  tfm_ns_signed.bin  tfm_s.axf  tfm_s.elf  tfm_s.map            tfm_s_signed.bin
bl2.axf  bl2.elf  bl2.map  tfm_ns.axf  tfm_ns.elf  tfm_ns.map  tfm_s.asm          tfm_s.bin  tfm_s.hex  tfm_s_ns_signed.bin

如上所示bl2.bin文件是安全启动的二进制文件,改二进制文件应该烧录到不可被修改的片上区域,因为其中会存放预加载镜像的公钥用于校验被加载文件的安全一致性。相较于非开启BL2安全启动的编译方式会多出tfm_s_signed.bintfm_s_ns_signed.bin两个文件,分别对应的是被签名的安全世界镜像和被签名的非安全世界镜像。

镜像地址分布

镜像的分布可分为两种:单一镜像和多镜像。单一镜像是指安全镜像(tfm_s_signed.bin)和非安全镜像(tfm_ns_signed.bin)文件组合成为一个单一镜像,在镜像升级时需要将安全镜像和非安全镜像同时升级更新;多镜像是指安全镜像和非安全镜像相互独立,升级更新时能够独立对安全镜像或者非安全镜像进行升级。当前仅考虑多镜像场景,在AN521平台下地址分布如下所示:

- 0x0000_0000 - 0x0007_FFFF:    BL2 bootloader - MCUBoot
- 0x0008_0000 - 0x000F_FFFF:    Primary slot : Secure image
  - 0x0008_0000 - 0x0008_03FF:  Secure image header
  - 0x0008_0400 - 0x000x_xxxx:  Secure image
  - 0x000x_xxxx - 0x000x_xxxx:  Hash value(SHA256), RSA signature and other
                                metadata of secure image

- 0x0010_0000 - 0x0017_FFFF:    Primary slot : Non-secure image
  - 0x0010_0000 - 0x0010_03FF:  Non-secure image header
  - 0x0010_0400 - 0x001x_xxxx:  Non-secure image
  - 0x001x_xxxx - 0x001x_xxxx:  Hash value(SHA256), RSA signature and other
                                metadata of non-secure image

- 0x0018_0000 - 0x001F_FFFF:    Secondary slot : Secure image
- 0x0020_0000 - 0x0027_FFFF:    Secondary slot : Non-secure image

- 0x0028_0000 - 0x002F_FFFF:    Scratch area, only used during image
                                swapping, used for secure and non-secure
                                image as well

镜像存储在Flash内存中,TF-M提供了两个镜像槽(slot),分别时用于存储当前需要加载的镜像的Primary Slot,以及用于更新镜像使用的Secondary Slot。Scratch内存空间用于固件进行Swap更新,作为新镜像与原镜像替换的中介地址。

固件安全更新

TF-M提供了四种固件更新的方式:覆盖更新(Overwrite operation),置换更新(Swapping operation),XIP(Direct execute-in-place operation)运行,和RAM加载更新。无论哪种更新,更新镜像首先都要存储在Secondary Slot中。覆盖更新是将Secondary Slot中的镜像覆盖Primary Slot中的镜像;置换更新是将Primary Slot中的镜像与Secondary Slot中的镜像互换位置,与覆盖更新的差异是置换更新能够将已经更新的系统会退到镜像更新前的版本,并且在替换镜像时会使用Scratch内存地址空间临时存储被替换的镜像。

bl2 main 函数

系统启动后通过Reset_Handler函数直接跳转至main函数,使用BL2安全启动的系统main函数在bl_main.c文件中,其主要逻辑分为以下几个部分:

int main(void)
{
    struct boot_rsp rsp;
    fih_int fih_rc = FIH_FAILURE;

    /* Initialise the mbedtls static memory allocator so that mbedtls allocates
     * memory from the provided static buffer instead of from the heap.
     */
    mbedtls_memory_buffer_alloc_init(mbedtls_mem_buf, BL2_MBEDTLS_MEM_BUF_LEN);

#if MCUBOOT_LOG_LEVEL > MCUBOOT_LOG_LEVEL_OFF
    stdio_init();
#endif

    /* Perform platform specific initialization */
    if (boot_platform_init() != 0) {
        BOOT_LOG_ERR("Platform init failed");
        FIH_PANIC;
    }

    BOOT_LOG_INF("Starting bootloader");

    FIH_CALL(boot_nv_security_counter_init, fih_rc);
    if (fih_not_eq(fih_rc, FIH_SUCCESS)) {
        BOOT_LOG_ERR("Error while initializing the security counter");
        FIH_PANIC;
    }

    FIH_CALL(boot_go, fih_rc, &rsp);
    if (fih_not_eq(fih_rc, FIH_SUCCESS)) {
        BOOT_LOG_ERR("Unable to find bootable image");
        FIH_PANIC;
    }

    BOOT_LOG_INF("Bootloader chainload address offset: 0x%x",
                 rsp.br_image_off);
    BOOT_LOG_INF("Jumping to the first image slot");
    do_boot(&rsp);

    BOOT_LOG_ERR("Never should get here");
    FIH_PANIC;
}

  • mbedtls_memory_buffer_alloc_init函数初始化mbedtls使用的静态内存,由于还没有进行OS内存初始化,所以当前还没有指定的堆空间能够进行内存的动态分配。
  • boot_nv_security_counter_init函数用于初始化安全计数器,对于需要进行可靠时间测量的应用非常重要。在TF-M中安全计数器会用于防回滚攻击,后文会进行详细的说明。
  • boot_go函数主要用于初始化rsp结构体变量,构造安全启动被加载镜像的对应结构体boot_rsp,该数据结构内容如下所示:
struct boot_rsp {
    /** A pointer to the header of the image to be executed. */
    const struct image_header *br_hdr;

    /**
     * The flash offset of the image to execute.  Indicates the position of
     * the image header within its flash device.
     */
    uint8_t br_flash_dev_id;
    uint32_t br_image_off;
};

struct image_header {
    uint32_t ih_magic;
    uint32_t ih_load_addr;
    uint16_t ih_hdr_size;           /* Size of image header (bytes). */
    uint16_t ih_protect_tlv_size;   /* Size of protected TLV area (bytes). */
    uint32_t ih_img_size;           /* Does not include header. */
    uint32_t ih_flags;              /* IMAGE_F_[...]. */
    struct image_version ih_ver;
    uint32_t _pad1;
};

boot_rsp中包含镜像文件的image_header结构体,该结构体内容如右图所示在boot_go函数中会被构建。在读取镜像结束后会使用RSA算法对镜像的一致性进行校验,以及对已加密的镜像进行解密。

  • boot_go函数对镜像结构体进行构造后将rsp结构体传递至do_boot函数进行后续的系统启动。启动时系统处于安全世界模式。

boot_go 函数

fih_int
boot_go(struct boot_rsp *rsp)
{
    fih_int fih_rc = FIH_FAILURE;
    FIH_CALL(context_boot_go, fih_rc, &boot_data, rsp);
    FIH_RET(fih_rc);
}

boot_go函数中实际调用context_boot_go函数。

context_boot_go 函数

context_boot_go函数中主要有3个循环IMAGES_ITER(BOOT_CURR_IMG(state)),循环次数为2,每个循环都是对Slot中的其中一个镜像做处理,当前每个Slot中各有两个镜像:安全镜像和非安全镜像。三个循环体的功能分别是:

  • 准备和配置各个镜像的更新相关数据和状态
  • 根据配置的更新策略对需要更新的镜像进行更新,当前设置能单独更新安全镜像和非安全镜像
  • 对各镜像分别进行校验
第一个循环

代码如下所示:

IMAGES_ITER(BOOT_CURR_IMG(state)) {

    image_index = BOOT_CURR_IMG(state);

    BOOT_IMG(state, BOOT_PRIMARY_SLOT).sectors =
        primary_slot_sectors[image_index];
    BOOT_IMG(state, BOOT_SECONDARY_SLOT).sectors =
        secondary_slot_sectors[image_index];
#if MCUBOOT_SWAP_USING_SCRATCH
    state->scratch.sectors = scratch_sectors;
#endif

    /* Open primary and secondary image areas for the duration
     * of this call.
     */
    for (slot = 0; slot < BOOT_NUM_SLOTS; slot++) {
        fa_id = flash_area_id_from_multi_image_slot(image_index, slot);
        rc = flash_area_open(fa_id, &BOOT_IMG_AREA(state, slot));
        assert(rc == 0);
    }
#if MCUBOOT_SWAP_USING_SCRATCH
    rc = flash_area_open(FLASH_AREA_IMAGE_SCRATCH,
                         &BOOT_SCRATCH_AREA(state));
    assert(rc == 0);
#endif

    /* Determine swap type and complete swap if it has been aborted. */
    boot_prepare_image_for_update(state, &bs);

    if (BOOT_IS_UPGRADE(BOOT_SWAP_TYPE(state))) {
        has_upgrade = true;
    }
}

首先通过flash_area_id_from_multi_image_slot函数和flash_area_open函数将Primary Slot和Secondary Slot的安全镜像和非安全镜像的flash对应的区域参数赋值给state的镜像区成员变量state->imgs.areastateboot_loader_state结构体,包含了镜像文件的属性和配置:

struct boot_loader_state {
    struct {
        struct image_header hdr;
        const struct flash_area *area;
        boot_sector_t *sectors;
        size_t num_sectors;
    } imgs[BOOT_IMAGE_NUMBER][BOOT_NUM_SLOTS];

#if MCUBOOT_SWAP_USING_SCRATCH
    struct {
        const struct flash_area *area;
        boot_sector_t *sectors;
        size_t num_sectors;
    } scratch;
#endif

    uint8_t swap_type[BOOT_IMAGE_NUMBER];
    uint32_t write_sz;

#if defined(MCUBOOT_ENC_IMAGES)
    struct enc_key_data enc[BOOT_IMAGE_NUMBER][BOOT_NUM_SLOTS];
#endif

#if (BOOT_IMAGE_NUMBER > 1)
    uint8_t curr_img_idx;
#endif
};

如上所示,镜像结构体imgs中包含了image_headerflash_area。其中BOOT_IMAGE_NUMBER大小为2,表示为安全镜像和非安全镜像;BOOT_NUM_SLOTS值为2,表示系统中使用两个Slot。flash_area结构体后续用于通过flash驱动接口从镜像地址空间中读取数据。
boot_prepare_image_for_update是该循环中的主要功能,用于决定镜像的更新方式。该函书比较复杂,简要概括,步骤和功能如下:

  • 通过调用boot_read_image_headers函数尝试读取各Slot中的安全和非安全镜像,若Secondary Slot中的镜像头读取失败,则将state->swap_type[PRIMARY_SLOT]设置为无需更新。
  • 读取Primary Slot和Secondary Slot中的各sector的大小并进行比较两各Slot的各sector是否匹配,若不匹配则无需更新。
  • 当Primary Slot和Secondary Slot中都存在有效镜像,会通过swap_read_status函数从Primary Slot中读取安全镜像和非安全镜像的swap状态,即是否需要进行Swap相关的更新操作。但无论是否需要进行镜像Swap操作,都对Secondary Slot中的镜像进行签名校验,签名校验的逻辑在第三个循环中会进一步讲解。
    除此之外还有其他的复杂状态设置和判断用于处理在更新镜像到一半进程时中断的场景,这部分的更新冗余逻辑比较复杂适合进行源码阅读。
第二个循环

第二个循环核心代码如下所示:

switch (BOOT_SWAP_TYPE(state)) {
    case BOOT_SWAP_TYPE_NONE:
        break;

    case BOOT_SWAP_TYPE_TEST:          /* fallthrough */
    case BOOT_SWAP_TYPE_PERM:          /* fallthrough */
    case BOOT_SWAP_TYPE_REVERT:
        rc = boot_perform_update(state, &bs);
        assert(rc == 0);
        break;

    case BOOT_SWAP_TYPE_FAIL:
        /* The image in secondary slot was invalid and is now erased. Ensure
         * we don't try to boot into it again on the next reboot. Do this by
         * pretending we just reverted back to primary slot.
         */

        /* image_ok needs to be explicitly set to avoid a new revert. */
        rc = swap_set_image_ok(BOOT_CURR_IMG(state));
        if (rc != 0) {
            BOOT_SWAP_TYPE(state) = BOOT_SWAP_TYPE_PANIC;
        }

        break;

    default:
        BOOT_SWAP_TYPE(state) = BOOT_SWAP_TYPE_PANIC;
}

context_boot_go函数中的第二个循环主要功能是对需要更新的镜像进行更新,核心逻辑如上图所示,通过对在boot_prepare_image_for_update函数中所设状态的判断,调用boot_perform_update函数对镜像进行更新。
boot_perform_update函数核心逻辑如下:

  • 首先调用boot_swap_image函数置换镜像,若使能了MCUBOOT_ENC_IMAGES宏,表示更新的镜像已被加密,则在更新镜像的时首先要通过调用boot_enc_load获取镜像加密密钥的密文并进行解密,然后通过boot_enc_set_key函数设置镜像解密密钥;然后执行swap_run -> boot_swap_sectors -> boot_copy_region -> boot_encrypt最终执行到boot_encrypt函数对加密的镜像进行解密,没看错boot_encrypt函数可通过传参能控制进行加密或解密操作。当Secondary Slot中一个Sector解密完成后然后再拷贝到Primary Slot对应的sector内存。
  • 更新镜像状态:若执行的操作是Revert操作,即恢复至前一个镜像版本,则需要对当前镜像的状态设置为已完成操作,防止下一次启动时再次置换镜像。
  • 若编译时使能了放回滚攻击,即定义了MCUBOOT_HW_ROLLBACK_PROT宏,将新更新镜像头中的counter值更新至对应镜像编号的安全计数器中。
第三个循环

第三个循环核心代码逻辑:

IMAGES_ITER(BOOT_CURR_IMG(state)) {
    if (BOOT_SWAP_TYPE(state) != BOOT_SWAP_TYPE_NONE) {
        /* Attempt to read an image header from each slot. Ensure that image
         * headers in slots are aligned with headers in boot_data.
         */
        rc = boot_read_image_headers(state, false, &bs);
        if (rc != 0) {
            goto out;
        }
        /* Since headers were reloaded, it can be assumed we just performed
         * a swap or overwrite. Now the header info that should be used to
         * provide the data for the bootstrap, which previously was at
         * secondary slot, was updated to primary slot.
         */
    }


    FIH_CALL(boot_validate_slot, fih_rc, state, BOOT_PRIMARY_SLOT, NULL);
    if (fih_not_eq(fih_rc, FIH_SUCCESS)) {
        goto out;
    }
    ... ...
}

第三个循环的核心目的是对当前Primary Slot中的镜像通过boot_validate_slot函数进行校验。该函数被MCUBOOT_VALIDATE_PRIMARY_SLOT宏包含。当使能该宏时表示每次启动时都会对Primary Slot中的镜像进行校验。不使能该宏则只会在更新时候对更新镜像进行校验。
boot_validate_slot -> boot_image_check -> bootutil_img_validate,最终调用bootutil_img_validate函数对镜像进行校验,核心逻辑;

  • 计算镜像的hash值,与镜像中存储的Hash进行比较。
  • 对公钥进行校验:对公钥进行哈希计算,获取公钥hash,并于硬件中存储的hash值进行比较。
  • 校验镜像签名:使用验证过的公钥和镜像hash,对镜像的签名进行校验,保证该镜像的签名是使用有效公钥的对应私钥生成的。
  • 编译时若配置了防回滚攻击选项,在上述逻辑结束后会比对镜像中的counter值是否小于安全硬件中存储的对应硬件counter值,若小于表示系统已遭受到了回滚攻击。

    基于上述解析,镜像加密时采用的策略是先签名再加密的方式,关于先加密再签名和先签名再加密两种方式哪种更安全可参考文章:https://zhuanlan.zhihu.com/p/290711693

do_boot 函数

do_boot函数主要作用是配置启动安全世界镜像使用的vector向量,然后进行跳转,函数实现如下所示:

static void do_boot(struct boot_rsp *rsp)
{
    struct boot_arm_vector_table *vt;
    uintptr_t flash_base;
    int rc;

    /* The beginning of the image is the ARM vector table, containing
     * the initial stack pointer address and the reset vector
     * consecutively. Manually set the stack pointer and jump into the
     * reset vector
     */
    rc = flash_device_base(rsp->br_flash_dev_id, &flash_base);
    assert(rc == 0);

    if (rsp->br_hdr->ih_flags & IMAGE_F_RAM_LOAD) {
       /* The image has been copied to SRAM, find the vector table
        * at the load address instead of image's address in flash
        */
        vt = (struct boot_arm_vector_table *)(rsp->br_hdr->ih_load_addr +
                                         rsp->br_hdr->ih_hdr_size);
    } else {
        /* Using the flash address as not executing in SRAM */
        vt = (struct boot_arm_vector_table *)(flash_base +
                                         rsp->br_image_off +
                                         rsp->br_hdr->ih_hdr_size);
    }

#if MCUBOOT_LOG_LEVEL > MCUBOOT_LOG_LEVEL_OFF
    stdio_uninit();
#endif

    /* This function never returns, because it calls the secure application
     * Reset_Handler().
     */
    boot_platform_quit(vt);
}

首先配置vt变量,vt是boot_arm_vector_table结构体对象包含了mspReset_Handler地址,然后将vt作为入参传递给boot_platform_quit函数进行后续跳转。boot_platform_quit函数如下所示:

void boot_platform_quit(struct boot_arm_vector_table *vt)
{
    /* Clang at O0, stores variables on the stack with SP relative addressing.
     * When manually set the SP then the place of reset vector is lost.
     * Static variables are stored in 'data' or 'bss' section, change of SP has
     * no effect on them.
     */
    static struct boot_arm_vector_table *vt_cpy;

    vt_cpy = vt;

#if RTE_FLASH0
    Driver_FLASH0.Uninitialize();
#endif

#if RTE_QSPI0
    Driver_FLASH1.Uninitialize();
#endif

#if defined(__ARM_ARCH_8M_MAIN__) || defined(__ARM_ARCH_8M_BASE__)
    /* Restore the Main Stack Pointer Limit register's reset value
     * before passing execution to runtime firmware to make the
     * bootloader transparent to it.
     */
    __set_MSPLIM(0);
#endif
    __set_MSP(vt_cpy->msp);
    __DSB();
    __ISB();

    boot_jump_to_next_image(vt_cpy->reset);
}

设置msp主栈指针,然后通过boot_jump_to_next_image函数跳转至安全镜像的Reset_Handler函数:

__WEAK __attribute__((naked)) void boot_jump_to_next_image(uint32_t reset_handler_addr)
{
    __ASM volatile(
#if !defined(__ICCARM__)
        ".syntax unified                 \n"
#endif
        "mov     r7, r0                  \n"
        "bl      boot_clear_bl2_ram_area \n" /* Clear RAM before jump */
        "movs    r0, #0                  \n" /* Clear registers: R0-R12, */
        "mov     r1, r0                  \n" /* except R7 */
        "mov     r2, r0                  \n"
        "mov     r3, r0                  \n"
        "mov     r4, r0                  \n"
        "mov     r5, r0                  \n"
        "mov     r6, r0                  \n"
        "mov     r8, r0                  \n"
        "mov     r9, r0                  \n"
        "mov     r10, r0                 \n"
        "mov     r11, r0                 \n"
        "mov     r12, r0                 \n"
        "mov     lr,  r0                 \n"
        "bx      r7                      \n" /* Jump to Reset_handler */
    );
}

reset_handler_addr以第一个入参,即r0寄存器赋值给r7,然后执行清理bl2的ram内存函数boot_clear_bl2_ram_area函数,清理之后清空r0-r12寄存器,最后执行bx r7跳转到安全镜像的Reset_Handler函数。

结语

该文仅对TF-M BL2安全启动进行比较粗粒度浅析,其中很多的细节还是需要反复琢磨和推敲,需要投入更多的精力进行深入解读,才能真正理解其中的安全编码的逻辑之美。