Khorina · 2023年11月14日

安全驱动怎么设计?

Perface

之前分享过很多关于TEEOS的内容,最近需要添加一个安全设备的Driver,这里来做个记录,梳理一下流程,与大家一起学习一下。

我们知道设备如果要被访问需要将其驱动放在kernel的初始化过程,不过不清楚也没事,我会专门和大家一起学习,写一篇笔记。

这里先来看看安全驱动怎么设计。支持TruztZone技术的芯片可将某个特定外部设备配置成安全设备,使其只能被处于安全世界状态(SWS)的ARM核访问。如果要在OP-TEE中使用安全设备,需要在OP-TEE OS中集成该安全设备的驱动程序,TA或OP-TEE OS可使用该安全驱动提供的接口来使用该安全设备。

要完成一个驱动的设计:向外暴露被调用的接口,将驱动集成。

在设计安全驱动的时候,先回顾一下,为什么可将某个特定外部设备配置成安全设备?

1、安全设备的硬件安全隔离

系统的外部设备一般是通过APB总线挂接到AXI总线上的,APB总线不支持ARM核对设备访问时进行安全检查的功能,故如果要将某个外部设备配置成安全设备,则需在SOC中添加TZPC组件和AXI-to-APB桥。

image.png

TZPC组件负责将某个特定外部设备配置成安全设备,并为该安全设备提供额外的安全信号,AXI-to-APB桥使用TZPC配置给该外部设备的安全信号来校验ARM核发送的访问请求中的安全状态位(NS bit)是否与之匹配,如果TZPC给外部设备提供的信号为安全信号而ARM核的访问请求是在正常世界状态(NWS)发起的,则AXI-to-APB总线会判定访问失败,从而实现系统对该外部设备的安全隔离。

TZPC组件主要有两个作用,一是为TZMA提供安全区域大小配置信号,用于TZMA来配置片上SRAM、ROM安全区域的大小,另外一个作用是接入到AXI-to-APB总线上,为外部设备提供安全信号,将某个外部设备配置成安全设备。TZPC组件的信号连接图如图22-1所示。

image.png

TZPCR0SIZE信号线将被连接到TZMA组件上,用于TZMA配置片上SRAM、ROM安全区域的大小,

TZPCDECPROTx信号线将连接到AXI-to-APB桥上,用于配置某个外设是否为安全设备。

TZPC最多支持将24个外部设备设定为安全设备,且需将TZPC组件的寄存器地址设定在安全地址范围内。

TZPC提供了18个寄存器,这些寄存器用于配置安全信号。这些寄存器的相关信息如表22-1所示。

image.png

通过配置TZPC组件寄存器的值可设定特定的安全信号的输出组合,用于将外部设备设定成安全设备。

TZPC的基地址并不固定,但需确保TZPC的基地址是在安全地址区域中的。

知道这个安全驱动硬件是实现安全驱动的基础,那么下一步就来看看怎么软件实现安全驱动。

2、OP-TEE中安全驱动的框架

OP-TEE中的安全驱动是OP-TEE操作安全设备的载体。

TA通过调用某个安全驱动的接口就可实现对特定安全设备的操作。安全驱动在OP-TEE中的软件框架如图22-2所示。

image.png

(其实这里,你要搞清楚linux kernel与驱动的关系,那真的还是蛮简单好理解的。但是我不知道~嘤嘤嘤) 系统服务层并非必需的,主要是为方便管理和上层使用。例如OP-TEE提供了各种各样的密码学算法,每一种算法的实现可通过不同的硬件引擎来完成。

为统一管理,可将这些硬件引擎驱动提供的操作接口统一集成到一个系统服务中,而上层用户只需调用系统服务暴露的接口就可实现对硬件引擎的调用。(可能是通过输入的参数进行分发)

下面展开讲讲这个框架的细节。

2.1 系统服务层

系统服务层在OP-TEE启动过程中由initcall段的代码进行初始化和启动,一个系统服务的初始化函数则是通过使用service_init宏来进行定义并在编译时链接到OP-TEE的镜像文件中。

在编译OP-TEE时,该初始化函数将被保存到OP-TEE镜像文件的initcall段中。

至于系统服务的初始化函数所要执行的内容则由开发者自行决定,一般是在系统服务的初始化函数中进行该服务的配置状态量的初始化以及系统服务提供给上层调用的操作接口变量的初始化,系统服务提供的结构体变量会包含用于实现具体功能的函数指针变量,这些函数指针变量指向的函数就是安全驱动提供给TA调用的操作接口。

2.2 驱动层

OP-TEE启动过程中会执行各安全驱动的初始化,驱动的初始化函数是在OP-TEE执行initcall段中的内容时被调用的,在OP-TEE中通过使用driver_init宏来告诉编译器,在编译时将driver_init宏传入的函数作为某个驱动的入口函数保存在镜像文件的initcall段中。

安全驱动的初始化主要用来完成安全设备的寄存器的配置以及私有数据的初始化。

如果某个安全驱动需要系统服务的配合,则还需要将驱动提供的操作接口连接到系统服务中的操作接口变量中。

若该驱动不需以系统服务的方式向上层提供操作接口,则不用将对应接口暴露给系统服务,而是由TA通过系统调用的方式直接调用安全驱动的接口来操作安全设备。

driver_init service_init

2.3 驱动文件在源代码中的位置

安全驱动需要被编译到OP-TEE镜像文件中,OP-TEE中有专门的目录来存放驱动和系统服务的源代码。

将驱动编译到OP-TEE镜像文件之前还需修改对应的sub.mk文件,OP-TEE中保存驱动和系统服务源代码的目录对应关系如表22-2所示。

image.png
下面开始创建一个实例吧!

来看看基于上文的概念,创建一个安全驱动的流程是什么?

3、来整个栗子--安全驱动的开发过程和示例

在OP-TEE中,若需要使用系统服务的方式为上层TA提供操作接口,则一个完整的安全驱动需要实现TA接口部分、系统调用部分、系统服务部分、驱动实现部分。本节将结合实际的示例介绍这四部分的开发流程。

  • TA接口部分
  • 系统调用部分
  • 系统服务部分
  • 驱动实现部分

如果不使用系统服务,就少了一个步骤。

3.1 示例代码获取和集成

本示例中的驱动只实现了对内存的读写操作,并提供了测试使用的TA和CA。

读者可使用如下指令从GitHub上获取到示例源代码:

        git clone https://GitHub.com/shuaifengyun/opentee_driver.git

下载完代码后就需要将该TA和CA集成到OP-TEE中,需修改OP-TEE源代码build目录下的qemu.mk(开发者板级对应的mk文件)和common.mk文件,同时也需要将安全驱动集成到OP-TEE的内核中。

然后编译整体OP-TEE后就能够使用该份示例代码来验证本书提供的安全驱动示例是否运行正常。

获取到示例代码后将opentee_driver/my_test目录全部复制到op-tee的根目录下,再切换到根目录的build目录中,然后使用git apply命令合入补丁文件后就可完成测试使用的TA和CA集成到OP-TEE,合入全部补丁的操作步骤如下:

1)将示例代码中的my_test_common_3.0.0.patch文件和my_test_qemu_3.0.0.patch文件复制到build目录中,将0001-Integrate-secure-driver-test-into-op-tee.patch文件复制到optee_os目录中。

2)切换到build目录,使用如下命令合入补丁:

        git apply my_test_common_3.0.0.patch

3)切换到optee_os目录,使用如下命令合入安全驱动在内核中的补丁:

        git am 0001-Integrate-secure-driver-test-into-op-tee.patch

将补丁合入完成后就可使用make -f qemu.mk all编译整个工程,然后使用make -f qemu.mk run-only来启动OP-TEE,在启动的正常世界状态的终端执行secStorTest命令就能实现该示例的CA对TA的调用。示例代码的运行效果如图22-3所示。

image.png

3.2 驱动实现

开发一个安全驱动时,需要在optee_os/core/drivers目录中建立该安全驱动的源文件,在源文件中实现驱动的初始化函数、操作设备的接口函数(read、write、ioctl),具体的接口函数由开发者自行定义。

若该驱动需要在系统启动过程中执行一些初始化操作则可使用driver_init宏进行定义,编译完成后需要被执行的内容将会被保存到镜像文件的initcall段中,这些使用driver_init宏定义的内容将在OP-TEE启动时被调用。(相当于提前为驱动的调用准备了环境与初始条件)

示例源代码中的driver_test.c文件需要放在optee_os/core/drivers目录中,然后修改optee_os/core/drivers目录下的sub.mk文件,将driver_test.c文件添加编译系统中。在sub. mk文件中添加如下内容:

        osrcs-y += driver_test.c

若需要使用宏的方式来控制该驱动的编译,可将添加到sub.mk的内容修改成“srcs-$(CFG_XXX) += driver_test.c”,然后在optee_os/mk/config.mk文件中定义CFG_XXX变量,通过将CFG_XXX变量赋值成y或n来控制该驱动是否需要被编译进系统。(这个还是蛮有用的)

该驱动对应的头文件driver_test.h文件需保存到optee_os/core/inlcude/drivers目录中,该文件中声明了该驱动暴露给外界调用的接口和相关结构体。

实现完驱动接口实现,现在来实现添加系统服务

3.3 添加系统服务

系统服务的添加不是必需的,为方便对底层驱动的管理和对外部设备的扩展,可将安全驱动的接口接入到某个系统服务中,通过系统服务向外界暴露调用接口,以便上层TA可以使用该安全驱动。

在本示例中建立的系统服务的源代码为tee_test.c文件,需将该文件保存到optee_os/core/tee目录中,同时将tee_test.h文件保存到optee_os/core/include/tee目录中,然后修改optee_os/core/tee目录中的sub.mk文件,添加“srcs-y += tee_test.c”,将tee_test.c集成到编译系统中。

也可使用宏来控制该系统服务的编译,其实现方法与上一节相同。

在tee_test.c文件中使用service_init宏来定义该系统服务的初始化函数(tee_test_init),该初始化函数将会被编译到OP-TEE的初始化段中,OP-TEE启动时将会执行服务段中包含的函数,调用tee_test_init函数初始化该系统服务。

到这里明白了driver_init和service_init的差异。一个是直接注册驱动的系统调用,一个注册到系统服务。

3.4 添加系统调用

上层的TA运行于OP-TEE的用户空间,如果上层的TA需要调用安全驱动,则需通过调用系统调用接口的方式来调用安全驱动提供的操作接口。

若要在TA中使用本示例中的安全驱动,则还需在OP-TEE中增加该驱动对应的系统调用。包括用户空间接口的定义和内核空间接口的定义,关于OP-TEE中系统调用的实现原理可参阅第16章。(小的马上后面补上)

image.png

1.用户空间代码的修改

修改optee_os/lib/libutee/arch/arm/utee_syscalls_asm.S文件,添加如下内容:

        UTEE_SYSCALL utee_testDriver_write, TEE_SCN_TESTDRIVER_WRITE, 3

utee_testDriver_xxx是在TA中调用该驱动时使用的函数,

TEE_SCN_TESTDRIVER_XXX是该系统调用对应的索引值。

上层TA调用utee_testDriver_xxx函数后会进入OP-TEE的内核空间,系统通过查找TEE_SCN_TESTDRIVER_XXX对应的接口来找到该功能在OP-TEE内核中的实现。

最后的数字表示调用该接口时需要代入的参数的个数。

修改optee_os/lib/libutee/include/utee_syscalls.h文件,添加如下内容,申明上述三个函数接口。在TA的源代码中包含该头文件后就可调用这三个接口来对该安全驱动进行调用。

        TEE_Result utee_testDriver_write(void *buf, size_t blen, size_t offset);

修改optee_os/lib/libutee/include/tee_syscall_numbers.h文件,添加上述三个系统调用接口的索引值,并修改TEE_SCN_MAX的值,需要修改和添加的内容如下:

        #define TEE_SCN_TESTDRIVER_WRITE              71
2.内核空间代码的修改

修改optee_os/core/arch/arm/tee/arch_svc.c文件中系统调用数组变量tee_svc_syscall_table的内容,将上述系统调用对应的内核层接口添加到该数组中,并包含申明这三个接口的头文件,在该文件中添加的内容如下:

        SYSCALL_ENTRY(syscall_testDriver_write),

上述三个函数的具体实现在tee_test.c文件中,读者可自行查阅这三个接口函数的实现。

3.5 测试使用的TA和CA

将该示例的测试TA和CA添加到OP-TEE中需要修改读者开发环境对应的mk文件中。以使用QEMU方式运行OP-TEE为例,则需要修改qemu.mk文件添加该示例代码的编译目标,修改步骤如下:

1)添加my_test的编译目标:

        ############################################################################
        # secure driver test TA--my_test
        ############################################################################
        my_test: my_test-common
        my_test-clean: my_test-clean-common

2)将my_test和my_test-clean添加到全局的all和clean目标依赖关系中:

        all: bios-qemu qemu soc-term optee-examples my_test
        clean: bios-qemu-clean busybox-clean linux-clean optee-os-clean \
              optee-client-clean qemu-clean soc-term-clean check-clean \
              optee-examples-clean my_test-clean

添加部分的主要作用是定义my_test目标并建立该编译目标与all的依赖关系,在编译整个OP-TEE工程时会被使用到。修改完板级编译的mk文件后,还需修改build/common.mk文件。修改的内容主要是将my_test的编译目标集成到系统编译中,需要修改的内容如下:

1)定义my_test路径变量:

        MY_TEST_PATH  ? = $(ROOT)/my_test

2)添加my_test的目标依赖,修改filelist-tee-common目标的依赖关系如下:

        filelist-tee-common: optee-client xtest optee-examples my_test

3)增加TA和CA的common目标:

        ############################################################################
        # my_test
        ###########################################################################
        MY_TEST_COMMON_FLAGS ? =     HOST_CROSS_COMPILE=$(CROSS_COMPILE_NS_USER)\
            TA_CROSS_COMPILE=$(CROSS_COMPILE_S_USER) \
            TA_DEV_KIT_DIR=$(OPTEE_OS_TA_DEV_KIT_DIR) \
            TEEC_EXPORT=$(OPTEE_CLIENT_EXPORT)
        .PHONY: my_test-common
        my_test-common: optee-os optee-client
            $(MAKE) -C $(MY_TEST_PATH) $(MY_TEST_COMMON_FLAGS)
        MY_TEST_CLEAN_COMMON_FLAGS ? = TA_DEV_KIT_DIR=$(OPTEE_OS_TA_DEV_KIT_DIR)
        .PHONY: my_test-clean-common
        my_test-clean-common:
            $(MAKE) -C $(MY_TEST_PATH) $(MY_TEST_CLEAN_COMMON_FLAGS) clean

4)添加clean操作的依赖关系:

        optee-os-clean-common: xtest-clean optee-examples-clean my_test-clean

5)在filelist-tee-common中添加TA和CA镜像需要被打包到文件系统中的操作:

@echo“#secure driver test TA“ >> $(fl)
        @if [ -e $(MY_TEST_PATH)/host/my_test ]; then \
            echo "file /bin/my_test" \
                "$(MY_TEST_PATH)/host/my_test 755 0 0" >> $(fl); \
            echo "file /lib/optee_armtz/9269fadd-99d5-4afb-a1dc-ee3e9c61b04c.ta" \
                "$(MY_TEST_PATH)/ta/9269fadd-99d5-4afb-a1dc-ee3e9c61b04c.ta 444 0 0" \
                >> $(fl); \
        fi

2.4 安全驱动示例的测试

完成所有修改之后,编译整个OP-TEE工程然后运行。在OP-TEE的启动日志中能看见示例中的系统服务和驱动启动的日志,启动的日志如图22-4所示。

image.png
系统启动后,在REE侧的终端中输入对应的指令就可通过TA调用到该示例的安全驱动,指令说明如下。

1.向驱动中写入数据

        my_test writeDev [offset] [len]

offset:表示需将数据写入驱动提供的buffer中的偏移位置。

len:表示需要写入驱动中数据的长度。写入驱动中的数据在CA源代码中被设定,读者可通过修改CA源代码中g_WriteData变量中的值将不同的内容写入该安全驱动中。

2.读取驱动中的数据

        my_test readDev [offset] [len]

offset:表示从驱动中buffer的哪个位置开始读取。

len:表示需要从驱动中读取的内容长度。

3.打印出驱动中的数据

        my_test dumpDev [len]

len:表示需要打印的数据的长度。

用于测试添加的模拟安全驱动的TA和CA运行的效果如图22-5所示。

image.png

小结一下啦

前辈的《手机安全和可信应用开发指南》确实写的很好啊,但是这里估计有linux的驱动开发经历,会更加深映像一些。这个part的学习解决了我的一些问题。

一起学习了OP-TEE中安全设备驱动的开发以及实现安全设备的安全隔离的原理。

当需要在系统中增加安全设备时,除了需在OP-TEE中开发该设备对应的安全驱动之外,还需修改TZPC的配置为该设备提供安全信号。

TA通过调用系统调用接口的方式陷入OP-TEE的内核空间来使用驱动,如需对多个安全设备进行统一管理,则可添加一个系统服务,将各安全驱动提供的接口集成到该系统服务中,使该系统服务封装接口暴露给上层使用。

齐活!!!上班!!!

作者:Hcoco
文章来源:TrustZone

推荐阅读

更多物联网安全,PSA等技术干货请关注平台安全架构(PSA)专栏。欢迎添加极术小姐姐微信(id:aijishu20)加入PSA技术交流群,请备注研究方向。
推荐阅读
关注数
4569
内容数
186
Arm发布的PSA旨在为物联网安全提供一套全面的安全指导方针,使从芯片制造商到设备开发商等价值链中的每位成员都能成功实现安全运行。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息