开发一个TEE简单存储模块

本文在 OPTEE提供的开源代码 的基础上,讲解了CA和TA的开发细节,并添加了可执行程序、JNI、系统服务等内容,实现了一个相对完整的TEE模块;

 

1 背景知识

TEE是现代操作系统内的一种安全基础设施,其主要特征有:

  • 安全 —— 从芯片架构上支持REE和TEE的隔离,CA内部支持安全存储和密码学算法,对TEE数据的访问严格受限;
  • 不易失 ——重启/OTA/恢复出厂设置不丢失;有些厂家能做到烧版本数据也不丢失(不同厂家不一样,取决于分区表和烧录工具的行为);

OP-TEE_Architecture

 

1.1 术语

  • TrustZone: ARM提出的一种安全解决方案,通过独立的安全操作系统和硬件虚拟化技术,提供可信运行环境;
  • GlobalPlatform: GlobalPlatform(GP)是跨行业的国际标准组织,致力于开发、制定并发布安全芯片的技术标准,以促进多应用产业环境的管理 及其安全、可互操作的业务部署。作为一个国际标准组织,其工作重心主要集中在安全单元(SE)、可信执行环境(TEE)和系统消息(Mobile Messaging)等领域,其成熟的技术规范是建立端到端可信业务解决方案的工具,并服务于产业环境的多个成员,支持多种商业模式。GP是全球基于安全芯片的安全基础设施统一的标准的制定者。
  • OPTEE: 基于ARM TrustZone技术,对GlobalPlatform的TEE规范的一种开源实现;
  • TDK: TEE Develop Kit,Amlogic的文档里有时候会用这个词,指的是厂家提供的可以运行在自身平台上的开发工具组,通常都是基于OPTEE的;
  • REE: Rich Execution Environment,通常指的是Linux、Android等操作系统环境,这里的Rich指的是可以操作的各种资源更丰富;
  • TEE: Trusted Execution Environment 可信运行环境;
  • CA: 有两个语义:(1)TEE的上下文中,指的是Client App,即在REE环境运行的一些不受保护的应用,包含so库、可执行程序、apk等;(2)密码学的上下文中,指的是证书授权中心 Certificate Authority;
  • TA: Trusted App,即运行在TOS内的一个应用程序;
  • TOS: Trust OS,即运行在TEE内的一个微型OS,主要功能是提供TEE内的任务调度、资源分配、以及TEE环境的系统调用;
  • 证书: 将CA颁发给某个持有者的用户公钥,用CA中心的私钥进行签名后的文件;

 

1.2 参考文档

OPTEE的可以从 GlobalPlatform的API文档 以及 readthedocs 获得;

其他各自平台的请向芯片供应商咨询。

 

2 CA和TA

一个最简单的TEE功能,至少包括CA和TA两个互相关联的模块;

我们先简单介绍一下CA和TA之间的交互流程,简单讲解一下TEE开发的基本过程;

 

2.1 CA和TA之间的交互

上面的TrustZone-OPTEE架构图,很清晰地展示了CA和TA分别处于两个世界的客观现实,那么在这种隔离状态下,他们之间的交互怎样完成呢?

问题一:既然REE存在多个CA,TEE/TOS内存在多个TA,那么,CA怎么找到对应自己的TA?

答案一: 不同的TA用UUID来区别,TA通过头文件定义自己的UUID,CA必须include对应的头文件,以便获取UUID;

非常不建议自己编造UUID,可以通过类似 https://www.uuidgenerator.net/ 这样的工具生成;

需要特别强调的是,这个UUID是TA的唯一标志,所以属于产品机密,绝对不要泄露。本文统一以"YOUR_UUID.ta"来代替TA的名字。

 

问题二:Priviledged Mode的系统资源必然不是无限的,那么CA和TA之间的通信,怎样避免长期占用资源?

答案二: TEE用Context和Session的机制来管理CA和TA之间的交互,CA需要初始化一份TEEC_Context,继而用TEEC_Context+UUID来获得TEEC_Session,然后才能执行对TA的操作。

操作完毕之后,要立即将TEEC_Session和TEEC_Context关闭(对称地,创建时的顺序是先TEEC_Context后TEEC_Session,关闭时顺序相反,先关闭TEEC_Session,再关闭TEEC_Context);

而且,据我们在实际开发中发现,同一个TA,不能同时间有两份TEEC_Session,否则设备会直接重启;这可能是OPTEE为了安全和性能的考虑;

 

问题三:一组CA和TA内,很可能是存在多个功能函数的,那么CA怎样告诉TA自己想要执行哪个操作?

答案三: CA调用TA的函数时,需要指定commandID;

commandID通常是一个非负整数,TA开发者确保当前TA范畴内,每个函数有唯一的commandID;

 

问题四:函数的调用必然涉及到参数和返回值,CA怎样将参数告知TA,TA怎样将返回值(执行结果、读文件结果等)返回给CA?

答案四: CA调用TA函数时,除了需要指定上面第三个问题里的commandID外,还需要同时指定一个TEEC_Operation对象,这个TEEC_Operation对象是对待执行的操作的抽象,其核心成员是一个四个元素的TEEC_Parameter数组;

TEEC_Parameter是union类型,可以存储TEEC_TempMemoryReference,TEEC_RegisteredMemoryReference,或者TEEC_Value类型的数据;

如果需要CA传递内存引用给TA,需要使用TEEC_TempMemoryReference;

如果需要TA返回数据给CA,例如读取文件内容,则需要使用 TEEC_RegisteredMemoryReference,其指向TEEC_SharedMemory对象;TEEC_Value用于携带简单的int类型数据;

通过指定四个元素的TEEC_Parameter数组的每个元素的input/output/inout类型,可以定义是否需要携带数据从TA返回CA;

 

2.1.1 CA和TA之间交互的详细流程图

TEE-CA-TA

 

2.2 CA开发

 

2.2.1 CA端代码路径

我们将自己开发的模块命名为 mytee,代码路径如下:

源文件路径功能/内容
host/mytee/mytee_tee_ca.h头文件,主要是LOGE/LOGD等日志函数、read/write/delete_secure_object三个函数声明、以及JNI需要的函数声明等;
host/mytee/mytee_tee_ca.c实际功能代码,包含CA端与TA端交互相关的接口,实际读写删TEE的代码,以及支持JNI的代码,支持编译为可执行程序的main函数;
Android.mk所有CA程序的mk文件,这里添加了编译为so和可执行程序的make脚本,不同平台的编译脚本各有不同,这里不关注这些差异

 

2.2.2 CA端常用数据结构和函数

头文件 tee_client_api.h 描述了当前平台支持哪些API,不同平台上其路径可能有差异;

做任何CA开发之前,请务必将这个api文档一字不落地仔细阅读一下;

tee_client_api.h 内常用到的重要类型和结构体等定义有:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// 所有TEE操作,都会返回这个类型的值,代表执行结果
typedef uint32_t TEEC_Result;

// 各种错误码,作为TEEC_Result类型的函数返回值使用
// 下面这些是最常用到的
#define TEEC_SUCCESS                       0x00000000
#define TEEC_ERROR_GENERIC                 0xFFFF0000
#define TEEC_ERROR_ITEM_NOT_FOUND          0xFFFF0008
#define TEEC_ERROR_SHORT_BUFFER            0xFFFF0010

// 代表客户端和TEE之间的一个连接,具体实现由芯片平台关注,
// 用户只需要使用TEEC_InitializeContext和TEEC_FinalizeContext即可
typedef struct {
        /* Implementation defined */
        int fd; 
        bool reg_mem;
        bool memref_null;
} TEEC_Context;

// 代表一个CA和TA之间的连接,也是由芯片平台实现,
// 用户只需要使用TEEC_OpenSession和TEEC_CloseSession即可
typedef struct {
        /* Implementation defined */
        TEEC_Context *ctx; 
        uint32_t session_id;
} TEEC_Session;

// 携带执行TEEC_InvokeCommand时需要的参数类型,以及每个参数
typedef struct {
        uint32_t started;
        uint32_t paramTypes;
        // TEEC_CONFIG_PAYLOAD_REF_COUNT的值是4,表示最大支持4个参数
        TEEC_Parameter params[TEEC_CONFIG_PAYLOAD_REF_COUNT];
        /* Implementation-Defined */
        TEEC_Session *session; 
} TEEC_Operation;

// TEEC_Parameter是一个union,这样就能很方便地将这块儿内存作为三种用途地任意一种来使用;
typedef union {
        TEEC_TempMemoryReference tmpref;
        TEEC_RegisteredMemoryReference memref;
        TEEC_Value value;
} TEEC_Parameter;

// TEEC_Value是最简单参数类型,注意这里其实可以携带两个int
typedef struct {
        uint32_t a;
        uint32_t b;
} TEEC_Value;

// TEEC_TempMemoryReference 是最简单地一种ref类型
typedef struct {
        void *buffer;
        size_t size;
} TEEC_TempMemoryReference;

// 我的理解是,TEEC_RegisteredMemoryReference中Registered的意思是,
// 向其parent“注册”并使用parent内从offset开始,长度为size的一块儿内存,
// 也就是说同一个TEEC_SharedMemory *parent可以“注册”(其实就是分割)出来很多个 TEEC_RegisteredMemoryReference
// 当然,管理多块儿内存就是程序员自己的责任了,最基本的是避免内存块儿的offset交错导致的内存互相踩踏
typedef struct {
        TEEC_SharedMemory *parent;
        size_t size;
        size_t offset;
} TEEC_RegisteredMemoryReference;

// 上述TEEC_SharedMemory需要通过TEEC_AllocateSharedMemory来分配,
// 通常我们只关心其buffer、size、flags即可,其他的属性都是由实现者(通常是芯片供应商)实现;
// 看到这里我们可以确认,要想使用 TEEC_RegisteredMemoryReference 类型,需要先分配一个 TEEC_SharedMemory 出来,
// 然后将TEEC_RegisteredMemoryReference类型的parent指针指向刚刚获得的TEEC_SharedMemory,
// 这个过程在后面函数 read_secure_object 里由很清楚的体现
typedef struct {
        void *buffer;    //实际存储区域
        size_t size;	 //buffer的大小
        uint32_t flags;  //数据传输方向,TEEC_MEM_INPUT和TEEC_MEM_OUTPUT二者之一或者合并
        // Implementation-Defined
        int id;
        size_t alloced_size;
        void *shadow_buffer;
        int registered_fd;
        union {
                bool dummy;
                uint8_t flags;
        } internal;
} TEEC_SharedMemory;

// 上面TEEC_Operation的 paramTypes 的类型是下面一系列宏定义
// 对应序号的参数不使用
#define TEEC_NONE                   0x00000000
// TEEC_VALUE_开头的表示传递一个值,input/output/inout表征方向
#define TEEC_VALUE_INPUT            0x00000001
#define TEEC_VALUE_OUTPUT           0x00000002
#define TEEC_VALUE_INOUT            0x00000003

// TEEC_MEMREF_开头的表示传递一个内存引用,通常用于携带非数值类信息,具体分为下面三类:

// Temp相关的表示类型是 TEEC_TempMemoryReference
#define TEEC_MEMREF_TEMP_INPUT      0x00000005
#define TEEC_MEMREF_TEMP_OUTPUT     0x00000006
#define TEEC_MEMREF_TEMP_INOUT      0x00000007

// TEEC_MEMREF_WHOLE表示已read only方式将parent内存块作为一个整体使用,
// 类型是TEEC_MemoryReference,一般很少用这种类型
#define TEEC_MEMREF_WHOLE           0x0000000C

// TEEC_MEMREF_PARTIAL_开头的表示类型是 TEEC_RegisteredMemoryReference
#define TEEC_MEMREF_PARTIAL_INPUT   0x0000000D
#define TEEC_MEMREF_PARTIAL_OUTPUT  0x0000000E
#define TEEC_MEMREF_PARTIAL_INOUT   0x0000000F

// 下面两个标记数据传输的方向,当需要双向传输(例如读文件时),可以并起来使用
// 通常用于分配好TEEC_SharedMemory空间后,初始化TEEC_SharedMemory的flags属性
#define TEEC_MEM_INPUT   0x00000001
#define TEEC_MEM_OUTPUT  0x00000002

 

2.2.3 CA调用TA的必备接口

首先看一下CA与TA交互必备的几个接口,按功能大体分成3类:

  • 创建或者释放 Context

    TEEC_InitializeContext:初始化一个TEE Context,所有TEE操作的第一步;

    TEEC_FinalizeContext:释放TEE Context,所有TEE操作的最后一步;

  • 打开或者关闭 Session

    TEEC_OpenSession:会话打开函数;当客户端应用执行 TEEC_OpenSession 时,安全OS会执行对应TA的 TA_OpenSessionEntryPoint ;如果此时TA实例未创建时,需要调用 TA_CreateEntryPoint ;

    TEEC_CloseSession:会话关闭函数;当客户端应用执行 TEEC_CloseSession 时,安全OS会执行对应TA的 TA_CloseSessionEntryPoint ;如果客户端应用关闭的会话为此TA的最后一个会话,则安全OS核心框架会执行 TA_DestroyEntryPoint 函数销毁TA实例;

  • CA调用TA的具体功能

    TEEC_InvokeCommand:使用会话和命令ID通知指定TA执行与命令ID匹配的操作;

 

2.2.4 CA开发与集成的详细步骤

  1. 写每一个具体的功能,以最常用的安全存储为例,至少要包含这三个功能:

    从TEE读取文件

    相比OPTEE的版本,我们这里增加了一个 size_t 类型的参数,用于携带回来“文件的实际大小”这个值*,知道文件实际大小,则读取成功时便对文件做实际处理,因为buff_sz太小而失败时,调用者可以调整buffer;

    (CA无法知晓某个文件的大小,但TA是知道的,如果TA通过value_out的形式告知了CA,则CA就能进一步告知上层调用者;OPTEE的样例代码里,TA就是按照这个逻辑处理的,但CA并没有关注文件实际大小这个信息,我们这里给加上了)

    1
    2
    3
    4
    5
    
    // id:          TEE内的文件id
    // buff:        存储读取的内容的缓冲区
    // buff_sz:     从TEE读取文件时,必须显式指定缓冲区的大小,且缓冲区大小不能小于待读取的文件大小
    // file_sz:     文件实际大小,由TA返回给CA
    TEEC_Result read_secure_object(const char *id, char *buff, size_t buff_sz, size_t* file_sz);

    往TEE内写入文件

    1
    2
    3
    4
    
    // id:          TEE内的文件id
    // data:        存储待写入的内容的缓冲区
    // data_len:    写缓冲区内多少字节的内容到TEE
    TEEC_Result write_secure_object(const char *id, char *data, size_t data_len);

    从TEE删除文件

    1
    2
    
    // id:          TEE内的文件id
    TEEC_Result delete_secure_object(const char *id);
  2. 每个功能方法内部,都需要依次做如下6个步骤:

    下面代码片段来自 read_secure_object 函数(为了体现每一步用到了哪些变量,将部分变量从函数前面的定义位置挪到了第一次使用前,后续代码也有类似排版,望读者知晓,后面不再赘述)

    写文件和删除文件的代码相对比较简单,OPTEE样例代码已经足够了,这里就不再重复贴代码了。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    
    // Step1: 初始化Context
    // name默认只支持配置为NULL,第二个入参即为初始化后的TEEC_Context
    
    // Step2: 打开连接到指定TA的一个TEEC_Session
    // 第一个参数乃是刚才获取的TEEC_Context,第二个参数即为获取的TEEC_Session,
    // 第三个参数非常重要,是TA的UUID,
    // UUID在TA的头文件中定义,在CA的c文件中必须include对应TA的定义UUID的头文件;
    
    // 考虑到每次读写删都需要上面两个步骤,所以OPTEE的样例代码是把这两步抽取到了一个独立的函数里
    TEEC_Result prepare_tee_session(TEEC_Context* ctx, TEEC_Session* sess) {
        TEEC_UUID uuid = TA_SECURE_STORAGE_UUID;
        uint32_t origin;
        TEEC_Result res;
    
        /* Initialize a context connecting us to the TEE */
        res = TEEC_InitializeContext(NULL, ctx);
        if (res != TEEC_SUCCESS) {
            errx(1, "TEEC_InitializeContext failed with code 0x%x", res);
            LOGE("TEEC_InitializeContext failed with code 0x%x", res);
            return res;
        }
    
        /* Open a session with the TA */
        res = TEEC_OpenSession(ctx, sess, &uuid, TEEC_LOGIN_PUBLIC, NULL, NULL, &origin);
        if (res != TEEC_SUCCESS) {
            errx(1, "TEEC_Opensession failed with code 0x%x origin 0x%x", res, origin);
            LOGE("TEEC_Opensession failed with code 0x%x origin 0x%x", res, origin);
            return res;
        }
        return res;
    }
    
    // Step3: 初始化 TEEC_Operation 对象,
    // 如果需要在CA和TA之间传输数据(例如读TEE),还需要初始化TEEC_SharedMemory对象
    TEEC_SharedMemory sm;
    sm.size = buff_sz;
    sm.flags = TEEC_MEM_INPUT | TEEC_MEM_OUTPUT;
    res = TEEC_AllocateSharedMemory(&ctx, &sm);
    if (res != TEEC_SUCCESS) {
        printf("AllocateSharedMemory ERR! res= 0x%x\n", res);
        return res;
    }
    
    TEEC_Operation op;
    memset(&op, 0, sizeof(TEEC_Operation));
    // TEEC_PARAM_TYPES是一个宏,详见tee_client_api.h
    op.paramTypes = TEEC_PARAM_TYPES( TEEC_MEMREF_TEMP_INPUT, 
                     TEEC_MEMREF_PARTIAL_OUTPUT, TEEC_NONE, TEEC_NONE);
    op.params[0].tmpref.size = id_len;
    op.params[0].tmpref.buffer = id;
    // 第二个参数关联到 SharedMemory,用于返回读取的内容,OPTEE代码中使用TEEC_MEMREF_TEMP_OUTPUT 类型携带输出,
    // 且直接使用了read_secure_object 函数的入参作为 tmpref.buffer,笔者实测,某些平台上这样无法获得文件内容,
    // 而使用memref关联到CA自己通过TEEC_AllocateSharedMemory 申请到的内存,是在各个平台都可用的方案;
    // OPTEE的做法:https://github.com/linaro-swg/optee_examples/blob/master/secure_storage/host/main.c
    op.params[1].memref.parent = &sm;
    op.params[1].memref.offset = 0;
    op.params[1].memref.size = sm.size;
    
    // Step4: 通过TEEC_InvokeCommand调用某一个TA内定义好的函数,并处理返回值
    // 第二个参数commandID是函数的唯一标志,需要CA和TA约定好
    res = TEEC_InvokeCommand(&sess, TA_SECURE_STORAGE_CMD_READ_RAW, &op, &origin);
    switch (res) {
    case TEEC_SUCCESS:
        LOGD("TEE read success, file size=%zd", op.params[1].memref.size);
        // op.params[1].memref.size 携带了实际读取到了多少数据,详见TA的代码
        *file_sz = op.params[1].memref.size;
        if(op.params[1].memref.size > 0) {
            // 从TEEC_SharedMemory里面获取读到的内容
            memcpy(buff, sm.buffer, op.params[1].memref.size);
        }
        break;
    case TEEC_ERROR_SHORT_BUFFER:
        // 如果因为buffer太小而失败,也将文件实际大小返回给调用者
        *file_sz = op.params[1].memref.size;
        LOGE("TEE read fail: Short Buffer, file size=%zd", op.params[1].memref.size);
        break;
    case TEEC_ERROR_ITEM_NOT_FOUND:
        LOGE("TEE read fail: Item Not Found");
        break;
    default:
        LOGE("Command READ_RAW failed: 0x%x / %u", res, origin);
    }
    
    // Step5: 关闭TEEC_Session
    // Step6: 终结TEEC_Context
    // 关闭时的顺序正好跟打开时相反,先关闭session,后关闭context,OPTEE也将这两步抽取出来独立做了一个函数
    void terminate_tee_session(TEEC_Context* ctx, TEEC_Session* sess) {
        TEEC_CloseSession(sess);
        TEEC_FinalizeContext(ctx);
    }
  3. 如果需要编译为可执行程序,还需要写一个main函数(事实上我们都是先写main函数来直接本地调试);

    如果需要需要支持JNI(这几乎是必须的),还需要实现JNI相关的函数;

    可执行程序和JNI的内容,我们放到下一章节讲;

  4. 编译,我们通常将CA编译为so库,这里so库的名字是libmytee.so,按照需要可能需要编译32位和64位两个;

  5. 考虑到几乎所有平台上,CA的编译结果都在/vendor/lib、/vendor/lib64、/vendor/bin 目录下;而Android NDK的开发规范要求,vendor可以依赖system,而system不能依赖vendor;所以正式集成时,通常的做法是将编译结果复制一份,作为prebuilt来集成到产品的/system/lib、/system/lib64、/system/bin下面;

  6. 由于几乎所有的CA都会依赖于libteec.so,这个libteec可以被认为是CA的运行依赖库,所以需要将原生代码编译出来的/vendor/lib/libteec.so、/vendor/lib64/libteec.so 复制一份,以prebuilt的形式集成到 /system/lib 和 /system/lib64 里面;

    做一次就可以了,以后更新CA和TA不需要再更新libteec.so;后续如果升级底版本,相当于teec有可能改动,需要重新更新一次;

  7. 将 libmytee.so 和 libteec.so 添加到系统的public-library列表里面,位置在 Overlay/system/core/rootdir/etc/public.libraries.android.txt

    当然,这个也是添加一次后,以后就基本上不需要更新了,除非增加了新的so库作为CA端;

  至此,CA端的工作基本完毕;

 

2.3 TA开发

 

2.3.1 TA端代码路径

源文件的路径和功能:

源文件路径功能/内容
ta/MakefileTA_DIRS变量中添加了一行mytee,告知编译脚本还得编译mytee这个TA;
ta/mytee/Android.mk指定 local_module=YOUR_UUID.ta ,调用 BUILD_OPTEE_MK
ta/mytee/Makefile指定 BINARY=YOUR_UUID
ta/mytee/sub.mk指定全局include目录(即可供CA端include的),指定源文件列表
ta/mytee/include/secure_storage_ta.h供CA端include,主要是指定了TA的UUID和TA支持的所有commandID列表
ta/mytee/secure_storage_ta.c实现具体功能,以及TA必备的5个流程接口(TA的创建和销毁,开关session、调用具体功能)
ta/mytee/user_ta_header_defines.h定义TOS关心的一些宏,包括 TA_UUIDTA_FLAGSTA_STACK_SIZETA_DATA_SIZE

上述源代码列表中,除了 include/secure_storage_ta.h 和 secure_storage_ta.c 这两个文件名可以由TA开发者自定义外,其他文件名都是固定的;

实际工作中,底版本都会自带有helloworld的TA,直接复制一份,然后修改UUID、C文件和头文件的名字等,然后就可以立即上手做开发了,不用太关心基础设施的细节;

 

2.3.2 TA端重要数据结构和函数

GlobalPlatform的API文档 非常详细,务必下载一份随时查阅。

TA端的API包括 tee_api.htee_api_types.h

下面是tee_api_types.h中定义的最常用的数据结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 这是调用TA_InvokeCommandEntryPoint时传输的参数,对比CA的参数类型可以发现,TA端将所有的内存类型统一为一种memref
// 同样是union类型,TEE_Param可以动态的根据入参类型正确获取参数;
typedef union {
    struct {
            void *buffer;
            uint32_t size;
    } memref;
    struct {
            uint32_t a;
            uint32_t b;
    } value;
} TEE_Param;

// 代表TEE内的一个文件对象,代码注释里面写得很清楚,它是opaque的
typedef struct __TEE_ObjectHandle *TEE_ObjectHandle;

// 代表一个TEE对象的信息,我们比较关心的是其中的dataSize的值,即对应文件的大小
// TEE_ObjectInfo是通过下面函数获取的(函数名字中确实带有一个数字1):
// TEE_Result TEE_GetObjectInfo1(TEE_ObjectHandle object, TEE_ObjectInfo *objectInfo);
typedef struct {
        uint32_t objectType;
        __extension__ union {
                uint32_t keySize;       /* used in 1.1 spec */
                uint32_t objectSize;    /* used in 1.1.1 spec */
        };
        __extension__ union {
                uint32_t maxKeySize;    /* used in 1.1 spec */
                uint32_t maxObjectSize; /* used in 1.1.1 spec */
        };
        uint32_t objectUsage;
        uint32_t dataSize;
        uint32_t dataPosition;
        uint32_t handleFlags;
} TEE_ObjectInfo;

tee_api.h中定义的一些涉及到读、写、删的最常用的函数:

再次强调一下,GlobalPlatform的API文档 非常详细,包含下面列举出的所有函数的详细解释,你值得拥有!

部分函数名字中带有数字1,原因是不带数字的旧版本已经废弃不建议使用了;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// ~~~~~~~~ 内存管理相关的函数(TA的内存管理,必须使用TEE提供的函数):
// 申请内存
void *TEE_Malloc(uint32_t size, uint32_t hint);
// 释放内存
void TEE_Free(void *buffer);
// 内存填充
void *TEE_MemFill(void *buff, uint32_t x, uint32_t size);
// 名字具有迷惑性,实际上这是一个内存拷贝函数
void *TEE_MemMove(void *dest, const void *src, uint32_t size);
// 检查当前TA是否有权限访问指定的内存,
// 在读文件时,获取的内存buffer来自于CA,所以实际执行TEE_MemMove之前必须检查一下,
// 如果buffer本身是TA自己刚刚通过TEE_Malloc获取的,一般是不需要检查的;
TEE_Result TEE_CheckMemoryAccessRights(uint32_t accessFlags, void *buffer, uint32_t size);

// ~~~~~~~~ 常用文件对象的函数:
// 获取文件对象的信息,我们最关心的是文件大小,一般用于比较文件大小和来自CA的buffer的大小;
TEE_Result TEE_GetObjectInfo1(TEE_ObjectHandle object, TEE_ObjectInfo *objectInfo);
// 关闭一个文件对象,可以是持久的(已经写入TEE的),也可以是非持久的(还没有写入TEE的);
void TEE_CloseObject(TEE_ObjectHandle object);

// ~~~~~~~~ 持久化对象相关的函数:
// 打开一个“已经存在的”TEE文件对象的handle,所以涉及到读和删的时候,必须先用这个函数打开文件
TEE_Result TEE_OpenPersistentObject(uint32_t storageID, const void *objectID,
                            uint32_t objectIDLen, uint32_t flags, TEE_ObjectHandle *object);

// 基于指定好的属性,创建一个TEE内的文件对象,所以往TEE写文件时,必然会用到这个函数
TEE_Result TEE_CreatePersistentObject(uint32_t storageID, const void *objectID,
                              uint32_t objectIDLen, uint32_t flags,
                              TEE_ObjectHandle attributes, const void *initialData,
                              uint32_t initialDataLen, TEE_ObjectHandle *object);
// 关闭并删除TEE内的文件对象,删除文件时自然必然用到;
// 另一个需要用到的场景是,创建文件,但是创建成功后写文件数据内容时出错,此时也需要将刚刚创建的文件删除掉
TEE_Result TEE_CloseAndDeletePersistentObject1(TEE_ObjectHandle object);

// ~~~~~~~~ 文件数据流访问相关的函数:
// 从object代表的对象的数据流中读取size个字节,写入指定的buffer中,实际读取的数据会被记录到count指向的内存
TEE_Result TEE_ReadObjectData(TEE_ObjectHandle object, void *buffer,
                              uint32_t size, uint32_t *count);
// 将buffer中的size个字节写入object对应的文件内
TEE_Result TEE_WriteObjectData(TEE_ObjectHandle object, const void *buffer, uint32_t size);
// 将object的文件指针reset到指定位置,
// 如果whence是TEE_DATA_SEEK_SET,则被设置到 文件开头+offset 的位置
// 如果whence是TEE_DATA_SEEK_CUR,则被设置到 当前指针+offset 的位置
// 如果whence是TEE_DATA_SEEK_END,则被设置到 文件末尾+offset 的位置
TEE_Result TEE_SeekObjectData(TEE_ObjectHandle object, int32_t offset, TEE_Whence whence);

 

2.3.3 TA响应CA的必备接口

首先看一下做为TA必须实现的5个流程接口,按照功能可以分成3个类别:

  • TA的创建和销毁

    TA_CreateEntryPoint:安全服务的构造函数接口,该接口在客户端应用第一次打开会话并创建TA实例时得到调用。如果此函数执行失败,则该TA实例构建失败。

    TA_DestroyEntryPoint:安全服务的析构函数接口,该接口在客户端应用关闭会话并销毁TA实例时得到调用。

    通常这俩函数不需要做什么特殊处理;

  • 打开和关闭Session

    TA_OpenSessionEntryPoint:当客户端应用打开会话时,安全OS会调用对应TA的此接口在安全世界创建一个会话。

    TA_CloseSessionEntryPoint:当客户端应用关闭会话时,安全OS会调用对应TA的此接口在安全世界关闭会话。

    通常这俩函数不需要做什么特殊处理;

  • 根据CommandID,调用具体的函数

    TA_InvokeCommandEntryPoint:这个函数的参数列表是固定的,command、param_types、TEE_Param数组都是来自于CA端调用 TEEC_InvokeCommand 函数时的参数;

    TA需要实现的是,对于每个commandID,将调用派发到不同的实际功能函数,通常switch-case之类就足够了;

    TA的这套调用规范,也决定了TA内具体干活的函数的入参必然要包括 uint32_t param_typesTEE_Param params[4] 这两项;

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    TEE_Result TA_InvokeCommandEntryPoint(void __unused *session, uint32_t command,
                                    uint32_t param_types, TEE_Param params[4]) {
        switch (command) {
        case TA_SECURE_STORAGE_CMD_WRITE_RAW:
            return create_raw_object(param_types, params);
        case TA_SECURE_STORAGE_CMD_READ_RAW:
            return read_raw_object(param_types, params);
        case TA_SECURE_STORAGE_CMD_DELETE:
            return delete_object(param_types, params);
        default:
            EMSG("Command ID 0x%x is not supported", command);
            return TEE_ERROR_NOT_SUPPORTED;
        }
    }

将TA_InvokeCommandEntryPoint 的派发逻辑写完后,接下来就是每个具体函数需要做的事情了。

下一节我们将依次介绍读写删每个功能的重点;

 

2.3.4 TA开发与集成的详细步骤

 

  1. 处理入参

    为了安全稳定,建议任何函数内,第一步先检查参数类型,仍然以读文件为例:

    类型检查与CA端是对应的,可以回过头看一下 “CA开发与集成的详细步骤”中的 “Step3: 初始化 TEEC_Operation 对象”;

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    // 第1个参数是携带了文件id,仅用于输入,所以类型是 MEMREF_INPUT
    // 第2个参数携带读取到的内容,所以类型是 MEMREF_OUTPUT
    const uint32_t exp_param_types = TEE_PARAM_TYPES(
                TEE_PARAM_TYPE_MEMREF_INPUT,
                TEE_PARAM_TYPE_MEMREF_OUTPUT,
                TEE_PARAM_TYPE_NONE, TEE_PARAM_TYPE_NONE);
    if (param_types != exp_param_types) {
        printf(" ~~~~mytee_ta~~~~ read_raw_object TEE_ERROR_BAD_PARAMETERS");
        return TEE_ERROR_BAD_PARAMETERS;
    }

    从入参TEE_Param params[4] 中获取参数时,如果参数是value类型,则直接使用value.a或value.b即可

    如果参数是MemoryReference类型(TEEC_TempMemoryReference或TEEC_RegisteredMemoryReference),则使用其值之前,必须先用TEE_Malloc申请内存然后用TEE_MemMove的方法复制MemoryReference的buffer或者parent的内容到刚申请的内存中;

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    char *obj_id;
    size_t obj_id_sz;
    char *data;
    size_t data_sz;
    
    // 通过params[0].memref.size 获取内存区域大小,并分配指定的空间供拷贝
    obj_id_sz = params[0].memref.size;
    obj_id = TEE_Malloc(obj_id_sz, 0);
    if (!obj_id) return TEE_ERROR_OUT_OF_MEMORY;
    
    // 将CA通过params[0]传来的数据拷贝到TA内obj_id
    TEE_MemMove(obj_id, (char *)params[0].memref.buffer, obj_id_sz);
    
    // 从params[1]获取CA使用的buff_sz大小,并在TA内分配指定的空间备用
    // 注意,params[1].memref.size 后面被复用,
    // 当CA的buffer太小时,用于存储文件的实际大小;当读取成功时,存储实际读取的数据大小(实际上也等于文件大小)
    data_sz = params[1].memref.size;
    data = TEE_Malloc(buff_sz, 0);
    if (!data) return TEE_ERROR_OUT_OF_MEMORY;

    特别注意,所有使用TEE_Malloc申请的内存,在函数退出前,必须用TEE_Free释放内存;

    关于CA和TA中内存的处理,详见下面的第2.4节 内存相关注意事项;

 

  1. 从TEE内读取文件

    见TA端的函数 static TEE_Result read_raw_object(uint32_t param_types, TEE_Param params[4])

    首先用函数TEE_OpenPersistentObject打开文件,然后用函数TEE_GetObjectInfo1获取TEE_ObjectInfo,包含文件的大小等,并确认指定的缓冲区是否足够读取文件;

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    TEE_ObjectHandle object;
    TEE_ObjectInfo object_info;
    
    res = TEE_OpenPersistentObject(TEE_STORAGE_PRIVATE,
                    obj_id, obj_id_sz,
                    TEE_DATA_FLAG_ACCESS_READ | TEE_DATA_FLAG_SHARE_READ,
                    &object);
    if (res != TEE_SUCCESS) {
        EMSG("Failed to open persistent object, res=0x%08x", res);
        TEE_Free(obj_id);
        return res;
    }
    res = TEE_GetObjectInfo1(object, &object_info);
    if (res != TEE_SUCCESS) {
        EMSG("Failed to create persistent object, res=0x%08x", res);
        goto exit;
    }
    // 如果发现CA提供的buffer太小,
    // 则将文件的实际大小写到params[1].memref.size 并返回错误码 TEE_ERROR_SHORT_BUFFER
    if (object_info.dataSize > buff_sz) {
        // Provided buffer is too short. Return the expected size together with status "short buffer"
        params[1].memref.size = object_info.dataSize;
        res = TEE_ERROR_SHORT_BUFFER;
        goto exit;
    }

    然后用函数TEE_ReadObjectData读取文件内容,第二个参数 data 是存储内容的缓冲区,最后一个参数可以携带实际读取的字节数;如果读取成功,则可以将实际读取的字节数通过复用params[1].memref.size返回给CA端调用者;

    1
    2
    3
    4
    5
    6
    7
    8
    
    res = TEE_ReadObjectData(object, data, object_info.dataSize, &read_bytes);
    if (res != TEE_SUCCESS || read_bytes != object_info.dataSize) {
        EMSG("TEE_ReadObjectData failed 0x%08x, read %" PRIu32 " over %u",
                res, read_bytes, object_info.dataSize);
        goto exit;
    }
    // 如果读取成功,则通过params[1].memref.size 携带实际读取的字节数(实际上必然等于文件大小)
    params[1].memref.size = read_bytes;

    然后,先用函数TEE_CheckMemoryAccessRights检查一下CA端指定的携带返回值的参数是否用写权限,如果可写,则用TEE_MemMove的方式将读取到的 data 中的内容复制到CA指定的区域;

    1
    2
    3
    4
    5
    6
    7
    
    res = TEE_CheckMemoryAccessRights(TEE_MEMORY_ACCESS_WRITE | TEE_MEMORY_ACCESS_ANY_OWNER,
            params[1].memref.buffer, params[1].memref.size);
    if (res != TEE_SUCCESS) {
        EMSG("CheckMemoryAccessRights ERR: 0x%x.", res);
        return res;
    }
    TEE_MemMove(params[1].memref.buffer, data, read_bytes);

    最后的最后,不要忘了释放内存;

    1
    2
    3
    4
    5
    6
    
    exit:
        TEE_CloseObject(object);
        TEE_Free(data);
        TEE_Free(obj_id);
        return res;
    }

     

  2. 向TEE内写入文件

    见TA端的函数 static TEE_Result create_raw_object(uint32_t param_types, TEE_Param params[4])

    从参数中读取内容的方法与读文件时类似:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    char *obj_id;
    size_t obj_id_sz;
    char *data;
    size_t data_sz;
    
    obj_id_sz = params[0].memref.size;
    obj_id = TEE_Malloc(obj_id_sz, 0);
    if (!obj_id) return TEE_ERROR_OUT_OF_MEMORY;
    TEE_MemMove(obj_id, params[0].memref.buffer, obj_id_sz);
    
    data_sz = params[1].memref.size;
    data = TEE_Malloc(data_sz, 0);
    if (!data) return TEE_ERROR_OUT_OF_MEMORY;
    TEE_MemMove(data, params[1].memref.buffer, data_sz);

    然后,调用函数TEE_CreatePersistentObject创建一个 TEE_ObjectHandle 类型的对象;注意其flags参数的配置;

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    obj_data_flag = TEE_DATA_FLAG_ACCESS_READ  /* 允许创建后读 */
        | TEE_DATA_FLAG_ACCESS_WRITE           /* 允许创建后写 */
        | TEE_DATA_FLAG_ACCESS_WRITE_META      /* 允许创建后删除或者重命名 */
        | TEE_DATA_FLAG_OVERWRITE;             /* 原来存在则直接覆盖掉原来的内容 */
    
    res = TEE_CreatePersistentObject(TEE_STORAGE_PRIVATE,
                    obj_id, obj_id_sz,
                    obj_data_flag,
                    TEE_HANDLE_NULL,
                    NULL, 0,
                    &object);
    if (res != TEE_SUCCESS) {
        EMSG("TEE_CreatePersistentObject failed 0x%08x", res);
        TEE_Free(obj_id);
        return res;
    }

    然后用函数TEE_SeekObjectData,seek到文件开头;

    1
    2
    3
    4
    5
    6
    
    res = TEE_SeekObjectData(object, 0, TEE_DATA_SEEK_SET);
    if (res != TEE_SUCCESS) {
        EMSG("SeekObjectData ERR: 0x%x.", res);
        TEE_Free(obj_id);
        return res;
    }

    然后用TEE_WriteObjectData写入数据到TEE_ObjectHandle对象中,注意数据缓冲区必须是TEE_MemMove函数copy的目的地址;

    写入成功则调用TEE_CloseObject关闭文件对象,写入失败的话,需要用 TEE_CloseAndDeletePersistentObject1 来关闭并删除写入错误的文件(TEE内的存储空间有限,避免浪费);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    res = TEE_WriteObjectData(object, data, data_sz);
    if (res != TEE_SUCCESS) {
        EMSG("TEE_WriteObjectData failed 0x%08x", res);
        // 如果写文件失败,则这里必须记得删除刚刚创建的持久文件,否则会造成TEE存储空间泄露
        TEE_CloseAndDeletePersistentObject1(object);
    }
    else {
        TEE_CloseObject(object);
    }

    最后的最后,不要忘了释放内存;代码这里就不赘述了;

     

  3. 删除TEE内的文件

    见TA端的函数 static TEE_Result delete_object(uint32_t param_types, TEE_Param params[4])

    基本过程:

    从参数获取obj_id的过程类似上面两个函数,这里不重复了;

    首先用函数 TEE_OpenPersistentObject 打开待删除的文件,需要注意其第三个参数flags,当需要删除文件时,flags必须包含TEE_DATA_FLAG_ACCESS_WRITE_META,详见GP的文档,最后一个TEE_ObjectHandle 类型的参数object代表被打开的文件;

    然后用 TEE_CloseAndDeletePersistentObject1 删除获取到的object即可;

    GP文档显示,TEE_CloseAndDeletePersistentObject1 是为了替代已经过期了的 TEE_CloseAndDeletePersistentObject 函数;

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    res = TEE_OpenPersistentObject(TEE_STORAGE_PRIVATE,
                    obj_id, obj_id_sz,
                    TEE_DATA_FLAG_ACCESS_READ |
                    TEE_DATA_FLAG_ACCESS_WRITE_META, /* we must be allowed to delete it */
                    &object);
    if (res != TEE_SUCCESS) {
        EMSG("Failed to open persistent object, res=0x%08x", res);
        TEE_Free(obj_id);
        return res;
    }
    
    TEE_CloseAndDeletePersistentObject1(object);
    TEE_Free(obj_id);
    
    return res;

     

  4. 编译TA

    不同平台的编译命令和生成路径各有不同,编译成功后,通常会生成一个 UUID.ta 文件 (UUID就是第2.1节生成的UUID的值);

     

  5. 集成TA到设备

    将上述 UUID.ta 文件,放到项目的prebuilt资源目录;

    修改设备的mk文件,集成UUID.ta到设备的 /vendor/lib/optee_armtz/UUID.ta 即可;

 

2.4 内存相关注意事项

  上面已经零散提到了内存相关的限制或者规定,这里汇总一下:

  1. CA就是普通的C代码,所以可以正常使用C的标准malloc和free函数;

  2. TA的内存的申请和释放,必须用 TEE_MallocTEE_Free

  3. TA使用来自CA的memref类型的入参携带的内存,需要先TEE_Malloc申请一段TA内的内存,然后再用 TEE_MemMove将入参携带的内存信息拷贝到刚才申请到的地址,然后使用TEE_MemMove的目的地内存地址的数据;

  4. 如果期望从TA向CA返回bytes数据,则:

    (1)CA端必须先用TEEC_AllocateSharedMemory方法申请TEEC_SharedMemory类型的一段内存,然后将内存地址给到CA调用 TEEC_InvokeCommand 时某个参数的memref.parent,且配置这个参数类型是OUTPUT的;

    (2)TA端计算完毕后,为了安全起见,必须用TEE_CheckMemoryAccessRights 方法检查CA的参数给的缓冲区是否有可写;如果可写,则用TEE_MemMove 的方式将待返回数据复制到入参的memref.parent(即CA端申请到的TEEC_SharedMemory内存里),然后CA端就能获取返回的内容了;

    【TEE_CheckMemoryAccessRights不是必须的,例如OPTEE的例子里面就没有检查,但还是推荐检查一下】

    (3)CA端获取数据后,程序退出前,必须用 TEEC_ReleaseSharedMemory 的方式释放申请到的 TEEC_SharedMemory 内存;

    具体详见从TEE里面读取文件时CA和TA的代码;

 

3 可执行程序、JNI、Service

 

3.1 可执行程序

为了方便开发和调试,我们通常需要写一个命令行工具(实际上我们一般是先用命令行调试通过后,再编译成so集成的),集成在设备上的 /system/bin/mytee 位置;

通过这个命令行工具,可以实现:

  • 往TEE内写入一行text文本;
  • 往TEE内写入一个文件,指定好Android文件系统路径即可;
  • 从TEE内读取文件,读取结果会以文件的形式保存到Android文件系统内;
  • 从TEE内删除指定的文件;

使用方法:

在adb shell 执行下列命令:

mytee writetext <tee_file_id> <text>

mytee writefile <tee_file_id> <filepath>

mytee read <tee_file_id> <buffer_size>

mytee delete <tee_file_id>

相关代码在CA端的main函数里面,最终也是调用了真正执行读写功能的函数;

代码逻辑相对比较简单,唯一需要注意的是函数fread和fwrite返回值的含义,这些都是纯C代码,与TEE核心逻辑无关;

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
int main(int argc, char *argv[]) {
    int result = 0;

    if(argc == 3) {
        // 删除
        if (strcasecmp(argv[1] , "delete") == 0) {
            int result = delete_secure_object(argv[2]);
            if(result == TEEC_SUCCESS) {
                printf("success: delete %s from TEE \n", argv[2]);
            }
            else if(result == TEEC_ERROR_ITEM_NOT_FOUND) {
                printf("ERROR:   delete %s from TEE, no such file \n", argv[2]);
            }
            else {
                printf("ERROR:   delete %s from TEE, error code = 0x%08x \n", argv[2], result);
            }
            return result;
        }
    }

    if(argc == 4) {

        // 写文本到TEE
        if(strcasecmp(argv[1] , "writetext") == 0) {
            int result = write_secure_object(argv[2], argv[3], strlen(argv[3]));
            if(result == TEEC_SUCCESS) {
                printf("success: write text '%s' into TEE as file %s \n", argv[3], argv[2]);
            }
            else {
                printf("ERROR:   write text '%s' into TEE as file %s, error = 0x%08x \n",
                       argv[3], argv[2], result);
            }
            return result;
        }
 
        // 写文件到TEE
        if(strcasecmp(argv[1] , "writefile") == 0) {
            //Step1 打开文件,获取文件长度
            FILE *input_file = fopen(argv[3], "rb");
            if (input_file == NULL) {
                printf("ERROR: Can't open file: %s \n", argv[3]);
                exit(1);
            }
            fseek(input_file, 0, SEEK_END);
            long input_file_size = ftell(input_file);
            rewind(input_file); //必须重新回到文件流开头!
            if(input_file_size == 0) {
                printf("ERROR: file empty? Are you kidding me? ^_^ \n");
                fclose(input_file);
                exit(2);
            }
            if(input_file_size > MAX_FILESIZE) {
                printf("ERROR: filesize too big, can only accept 16k or smaller file \n");
                fclose(input_file);
                exit(3);
            }
            //Step2 读取文件内容到byte数组
            char* write_buffer = malloc(input_file_size);
            if(write_buffer == NULL) {
                printf("ERROR: no memory to read file \n");
                fclose(input_file);
                exit(4);
            }
            memset(write_buffer, 0, input_file_size);
            // 注意,fread的返回值,并不是读取了多少bytes,而是读取了多少个object,
            // 这里第三个参数为1,即只读取一个大小为input_file_size的object,所以返回值1就是成功
            long read_result = fread(write_buffer, input_file_size, 1, input_file);
            if(read_result != 1) {
                printf("ERROR: read file %s, filesize=%ld, read_result =%ld \n", 
                       argv[3], input_file_size, read_result);
                fclose(input_file);
                exit(5);
            }
            else {
                fclose(input_file);
            }
            //Step3 写入TEE
            result = write_secure_object(argv[2], write_buffer, input_file_size);
            if(result == TEEC_SUCCESS) {
                printf("success: write file '%s' into TEE as file %s \n", argv[3], argv[2]);
            }
            else {
                printf("ERROR:   write file '%s' into TEE as file %s, error code = 0x%08x \n", 
                       argv[3], argv[2], result);
            }
            free(write_buffer);

            return result;
        }

        // 从TEE读取文件到Android文件系统内
        if(strcasecmp(argv[1] , "read") == 0) {
            //Step1 从TEE读取文件到缓冲区
            int buffer_size = atoi(argv[3]);
            if(buffer_size == 0) {
                printf("ERROR:   bad buffer_size \n");
                exit(6);
            }
            char* read_buffer = malloc(buffer_size);
            if(read_buffer == NULL) {
                printf("ERROR:   no memory to read TEE \n");
                exit(7);
            }
            memset(read_buffer, 0, buffer_size);
            size_t file_size = 0;
            result = read_secure_object(argv[2], read_buffer, buffer_size, &file_size);
            //Step2 读取成功则存储到文件系统
            if(result == TEEC_SUCCESS) {
                char output_path[256];
                memset(output_path, 0, 256);
                sprintf(output_path, "/sdcard/tee_%s", argv[2]);
                // wb+表示文件已经存在则清空并覆盖,如果不存在则创建
                FILE *output_file = fopen(output_path, "wb+");
                if (output_file == NULL) {
                    printf("ERROR:   Can't open file to write: %s \n", output_path);
                    exit(8);
                }
                long write_result = fwrite(read_buffer, file_size, 1, output_file);
                // 注意,同fread类似,fwrite的返回值意思是,写了多少个object,而不是写了多少个bytes
                if(write_result == 1) {
                    printf("success: read %s from TEE, and write to %s \n", 
                           argv[2], output_path);
                }
                else {
                    printf("ERROR:   read %s from TEE done, but write to %s failed \n", 
                           argv[2], output_path);
                }
                fclose(output_file);
            }
            // 常见的失败原因,给出提示
            else if(result == TEEC_ERROR_SHORT_BUFFER) {
                // 通过TA返回给CA的值,获得文件实际大小,请看上面CA和TA里读文件的注释
                printf("ERROR:   buffer too small, file size is %ld bytes \n", file_size);
            }
            else if(result == TEEC_ERROR_ITEM_NOT_FOUND) {
                printf("ERROR:   no such file in TEE: %s \n", argv[2]);
            }
            else {
                printf("ERROR:   read from TEE, error code = 0x%08x \n", result);
            }
            free(read_buffer);
            return result;
        }

    }

    // 没有走进前面的流程,则默认打印usage
    print_usage();
    return 0;
}

// 参数列表不符合预期时,打印使用方法
void print_usage() {
    printf("MyTEE cmdline \n");
    printf("Usage: \n");
    printf("mytee writetext  <tee_file_id>  <text>         \n");
    printf("mytee writefile  <tee_file_id>  <filepath>     \n");
    printf("mytee read       <tee_file_id>  <buffer_size>  \n");
    printf("mytee delete     <tee_file_id>                 \n");
}

 

3.2 JNI

我们写一个名为 com.wang.tee.MyTeeClient 的类专门负责java层;

我们定义了三个native方法,以及static代码块来加载so库;

MyTeeClient.java 里:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static final int MAX_FILE_SIZE = 128 * 1024;  //TEE接受最大128k的文件,取决于各个平台TEE空间的限制
private static final int BUFFER_SIZE = 1024;
private static MyTeeClient TEE = new MyTeeClient();
private static final Object LOCK = new Object();      //读写同步锁

// Java层接口:
// 写文件,成功返回0,失败返回TEE错误码或者上层自定义错误码(自定义的不能与TEE错误码重复)
public static int write(String teeDstFileId, String text) {}
public static int write(String teeDstFileId, File reeSrcFile) {}
public static int write(String teeDstFileId, byte[] data) {}
// 删文件,复用CA层返回值,成功返回TEEC_SUCCESS,失败则为各种TEE错误码
public static int delete(String teeFileId) {}
// 读文件,成功则返回文件大小(正值),失败则返回TEE错误码(负值)
public static int read(String teeFileId, byte[] data) {}

// native函数列表,返回值均为:成功为TEEC_SUCCESS,失败则为各种TEE错误码
public native int native_write(String jFilename, byte[] jBuffer);
public native int native_read(String jFilename, byte[] jBuffer);
public native int native_delete(String jFilename);

static {
    System.loadLibrary("mytee");
}

对应地,在C层,即mytee_tee_ca.c 里面,需要添加注册函数的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static const JNINativeMethod gMethods[] = {
//  Method-Name-in-Java;   Method-Signature;            Function-Pointer-in-C
    {"native_write",      "(Ljava/lang/String;[B)I",    (void*)tee_write_file   },
    {"native_read",       "(Ljava/lang/String;[B)I",    (void*)tee_read_file    },
    {"native_delete",     "(Ljava/lang/String;)I",      (void*)tee_delete_file  },
};

static int registerNativeMethods(JNIEnv *env, const char *className,
                                 const JNINativeMethod *methods, int numMethods) {
    int rc;
    jclass clazz;
    LOGE("~~~~~~~~~~~~~ registerNativeMethods ~~~~~~~~~~~~~~~~~~~~~");
    clazz = (*env)->FindClass(env, className);
    if (clazz == NULL) {
        LOGE("Native registration unable to find class '%s'", className);
        return -1;
    }
    if (rc = ((*env)->RegisterNatives(env, clazz, methods, numMethods)) < 0) {
        LOGE("RegisterNatives failed for '%s' %d", className, rc);
        return -1;
    }
    return 0;
}

JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = NULL;
    if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_4) != JNI_OK) {
        LOGE("GetEnv failed!");
        return -1;
    }
    if ( registerNativeMethods(env, "com/wang/tee/MyTeeClient", gMethods, NELEM(gMethods)) < 0 ) {
        LOGE("registerNativeMethods failed!");
        return -1;
    }
    return JNI_VERSION_1_4;
}

 

3.2.1 写文件

Java层:

为了方便,我们提供了写bytes数组、写文件、写字符串的功能,本质上都是调用了 native_write 写一个byte[] 到TEE;

有一点需要特别注意,所有对native方法的调用,都必须放到synchronized块里面。

原因是,我们发现对于同一个TA,如果CA端在尚未关闭所有Session之前就重新打开新的Session,会导致系统重启。文档里似乎并没有强调这一点,我猜测这是为了安全考虑;

更进一步说,我们也可以得到这样的结论:MyTeeClient这个类只能有一个实例,实际项目中,我们一般将其放到某个特定系统服务里。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 写字符串
public static int write(String teeDstFileId, String text) {
    byte[] data = null;
    try {
        data = text.getBytes("UTF-8");
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    }
    return write(teeDstFileId, data);
}
// 写文件
public static int write(String teeDstFileId, File reeSrcFile) {
    return write(teeDstFileId, fileToBytes(reeSrcFile));
}
// 写byte[]
public static int write(String teeDstFileId, byte[] data) {
    int result = -100;
    if(data == null ) {
        return -1;
    }
    if(data.length == 0 || data.length > MAX_FILE_SIZE) {
        return -2;
    }
    synchronized (LOCK) {
       result = TEE.native_write(teeDstFileId, data);
    }
    if (TEEC_SUCCESS == result) {
        Log.d(TAG, "writeBytes success");
    } else {
        Log.e(TAG, "writeBytes fail, result = " + result);
    }
    return result;
}
// 一个简单的工具函数
private byte[] fileToBytes(File f) {
    if (!f.exists() || !f.isFile()) {
        return null;
    }
    byte[] data = new byte[(int) f.length()];
    ByteArrayOutputStream bos = new ByteArrayOutputStream((int) f.length());
    BufferedInputStream in = null;
    try {
        in = new BufferedInputStream(new FileInputStream(f));
        byte[] buffer = new byte[BUFFER_SIZE];
        int len = 0;
        while (-1 != (len = in.read(buffer, 0, BUFFER_SIZE))) {
            bos.write(buffer, 0, len);
        }
        data = bos.toByteArray();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            in.close();
            bos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return data;
}

C层:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
JNIEXPORT jint JNICALL tee_write_file(JNIEnv *env, jobject obj, 
                                      jstring jFilename, jbyteArray jBuffer) {
    TEEC_Result res;
    char * filename = (char *) (*env)->GetStringUTFChars(env, jFilename, 0);
    jbyte* buffer = (*env)->GetByteArrayElements(env, jBuffer, 0);
    if(NULL == buffer) {
        LOGE("tee_write_file: cannot get jBuffer");
        return -1;
    }
    char* buf=(char*)buffer;
    jsize  jBufferSize = (*env)->GetArrayLength(env, jBuffer);
    uint32_t bufferSize = (uint32_t)jBufferSize;

    res = write_secure_object(filename, buf, bufferSize);

    (*env)->ReleaseByteArrayElements(env, jBuffer, buffer, 0);
    (*env)->ReleaseStringUTFChars(env, jFilename, filename);
    return res;
}

 

3.2.1 删文件

删除文件的逻辑最简单,只涉及到一个参数;

同样,对native方法的调用也要做同步处理;

Java层:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public static int delete(String teeFileId) {
    int result = TEEC_ERROR_GENERIC;
    synchronized (LOCK) {
        result = TEE.native_delete(teeFileId);
    }
    if (TEEC_SUCCESS == result) {
        Log.d(TAG, "deleteFile success");
    } else if (TEEC_ERROR_ITEM_NOT_FOUND == result) {
        Log.e(TAG, "deleteFile fail, reason: no such file");
    } else {
        Log.e(TAG, "deleteFile fail, result = " + Integer.toHexString(result));
    }
    return result;
}

C层:

1
2
3
4
5
6
7
8
JNIEXPORT jint JNICALL tee_delete_file(JNIEnv *env, jobject obj, jstring jFilename) {
    TEEC_Result res;
    char * filename = (char *) (*env)->GetStringUTFChars(env, jFilename, 0);
    res = delete_secure_object(filename);
    LOGD("tee_delete_file will return %d", res);
    (*env)->ReleaseStringUTFChars(env, jFilename, filename);
    return res;
}

 

3.2.3 读文件

Java层:

读取文件时,可能面临这样“既要又要”的诉求:如果读取不到,则我希望知道原因,如果读取成功了,我希望能知道实际读取的字节数;怎样让C层通过一个返回值告知我这一切呢?

解决方法也很简单,我们发现TEE执行失败的错误码都是负值,而文件大小都是正值。那我们就可以这样设计:执行失败,则返回错误码,执行成功则返回文件大小。我们根据返回值的正负就能知道执行成功与否,如果成功了,那文件大小也知道了;

这也可能就是为什么错误码一般都是负值的原因吧。既然正值有“读取了多少字节”之类的语义,那将错误码设计成负值就天然地将二者区分开来了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public static int read(String teeFileId, byte[] data) {
    int result = TEEC_ERROR_GENERIC;
    synchronized (LOCK) {
        result = TEE.native_read(teeFileId, data);
    }
    if (result > 0) {
        Log.d(TAG, "deleteFile success, file size=" + result);
    } else if (TEEC_ERROR_ITEM_NOT_FOUND == result) {
        Log.e(TAG, "deleteFile fail, reason: no such file");
    } else {
        Log.e(TAG, "deleteFile fail, result = " + Integer.toHexString(result));
    }
    return result;
}

C层:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
JNIEXPORT jint JNICALL tee_read_file(JNIEnv *env, jobject obj, 
                                     jstring jFilename, jbyteArray jBuffer) {
    TEEC_Result res;
    size_t file_size = 0;
    jsize buffer_size = (*env)->GetArrayLength(env, jBuffer);
    // 最后一个参数isCopy=JNI_FALSE表示不拷贝,直接使用Java堆中的内存
    jbyte *buffer = (*env)->GetByteArrayElements(env, jBuffer, JNI_FALSE);
    if (buffer == NULL) {  // 一般不会出现这种情况
        return TEEC_ERROR_OUT_OF_MEMORY;
    }
    char * filename = (char *) (*env)->GetStringUTFChars(env, jFilename, 0);

    res = read_secure_object(filename, buffer, buffer_size, &file_size);
    // 释放内存
    (*env)->ReleaseStringUTFChars(env, jFilename, filename);
    // 最后一个参数mode=0表示要求将对数组的修改返回到Java层入参jBuffer里
    (*env)->ReleaseByteArrayElements(env, jBuffer, buffer, 0);

    // 读取成功,则返回读取的字节数(实际上就是文件大小),此时返回值为正数
    // 其他任何失败,返回错误码,此时返回值为负数,Java层可以根据返回值的正负来做对应处理
    return TEEC_SUCCESS == res ? file_size : res;
}

 

关于JNI里数组处理地一点引申:

上面C层处理Java层数组时用到了函数GetByteArrayElements,关于这个话题,有必要多说几句。

在JNI中,下面三个函数都用来访问Java中的字节数组(byte[])的,但它们的使用方式和效率有所不同。

1
2
3
void GetByteArrayRegion(JNIEnv *env, jbyteArray array, jsize start, jsize len, jbyte *buf);
jbyte* GetByteArrayElements(JNIEnv *env, jbyteArray array, jboolean *isCopy);
void* GetPrimitiveArrayCritical(JNIEnv *env, jobject array, jboolean *isCopy);

它们的区别有:

GetByteArrayRegion:

对java数组的处理是“只读”的,即每次都从Java层拷贝一份到Native层,所以Native层对拷贝所得的jbyteArray 做任何处理也不会影响到java层的数据;

因为存在拷贝,其效率也是最低的;

GetByteArrayElements:

返回一个指向Java字节数组的直接指针,允许在本地代码中访问和修改数组的内容;

如果指定参数isCopy为JNI_TRUE,则会创建数据的副本(此时对副本的修改不影响Java堆中的数据);如果指定参数isCopy为JNI_FALSE,则Native层会直接使用Java堆中的直接内存;

使用后需要调用 ReleaseByteArrayElements() 来释放本地指针并同步数据回Java数组;【特别是创建时参数isCopy为JNI_TRUE的情况,必须调用ReleaseByteArrayElements且确保最后一个参数mode必须是0】;

GetPrimitiveArrayCritical:

该函数提供对Java原始数组(如 byte[]int[] 等)元素的“临界访问”。它为本地代码提供对数组的高效访问,并且通过禁止Java垃圾回收器在访问期间移动该数组来避免性能损失(也就是说这个函数调用期间JVM是无法执行gc的,可能会影响JVM的性能)。

因为不存在数据拷贝且调用期间禁用了gc,所以这种访问方式是最高效的,但相对较危险,因为它会直接操控原始内存。

在使用后需要调用 ReleasePrimitiveArrayCritical() 来释放资源。

 

3.3 Service

作为一个完整的解决方案,系统需要向app提供API以实现对TEE的读写,一般项目中,我们都是将其放到一个自定义的系统级Service里;

怎样添加系统级Service是Android系统开发经常遇到的一个需求。网上写这个的太多了,就不再赘述其实现逻辑了。

这里简单提供一个图来描述这些关系:

TEE-Hierarchy

这个图实际上描述了实际项目中云端校验设备端证书的过程,即证书在工厂生产阶段执行初始化,然后所有app都通过系统服务MyCompanyMS来读取证书并提交给云端校验设备合法性;

除了mytee这个调试用的可执行程序外,所有的对TEE的访问都是通过系统服务来完成的,所以实际项目中,mytee这样的可执行程序只能集成到userdebug版本里。一方面是为了安全性,另一方面mytee的存在实际上是破坏了所有调用Session都从系统服务中来的原则,mytee和系统服务并发时,可能导致设备重启等异常问题。

 

至此,一个完整的TEE模块我们就开发完毕了。

希望上述这些内容对大家有帮助。

0%