第二章:TEE与安全隔离:可信执行环境原理与鸿蒙实现

各位同学,咱们今天聊聊TEE。

说实话,我在做安全方案评审时,经常遇到有人把TEE当成万能药。仿佛只要用了TEE,所有安全问题就都解决了。嗯,这想法很危险。TEE是个好工具,但得用对地方。

2.1 什么是TEE?为什么需要它?

先问个问题:你的手机里,指纹数据存在哪?支付密钥存在哪?

如果这些敏感数据直接放在普通操作系统里,那就像把银行卡密码写在便利贴上,贴在电脑屏幕旁边。一旦系统被攻破,数据就全裸奔了。

TEE(Trusted Execution Environment,可信执行环境)就是来解决这个问题的。它提供了一个与普通操作系统(REE,Rich Execution Environment)隔离的安全区域。

核心思想:即使REE被完全攻破,攻击者也拿不到TEE里的数据。

我在项目中遇到过一件事:某厂商的支付模块跑在REE里,结果被一个恶意App通过内存dump拿到了密钥。后来他们改用了TEE方案,同样类型的攻击就再也无效了。说白了,这就是隔离的价值。

2.2 TEE的工作原理

TEE的实现依赖于硬件支持。目前主流方案有ARM的TrustZone、Intel的SGX、RISC-V的Keystone等。鸿蒙系统主要基于ARM TrustZone。

简单来说,TrustZone把CPU分成了两个世界:

  • 普通世界(Normal World):运行普通操作系统,比如鸿蒙的REE侧
  • 安全世界(Secure World):运行TEE操作系统,比如鸿蒙的TEE侧

这两个世界通过硬件强制隔离。普通世界无法直接访问安全世界的内存和寄存器。切换世界需要经过安全监控器(Secure Monitor),这是由硬件控制的。

我个人习惯:把TEE想象成一个保险箱。普通世界是办公室,保险箱是TEE。你可以把重要文件放保险箱里,但只有持有钥匙的人才能打开。而且保险箱本身是焊在地板上的,搬不走。

2.3 鸿蒙的TEE实现

鸿蒙的TEE实现,我总结为三个层次:

层次 组件 说明
硬件层 ARM TrustZone / 其他TEE硬件 提供隔离基础
内核层 鸿蒙TEE内核(基于微内核) 管理安全世界资源
应用层 TA(Trusted Application) 运行在TEE中的安全应用

鸿蒙的TEE内核是基于微内核架构的。为什么用微内核?你想想看,TEE里跑的代码越少,攻击面就越小。微内核只提供最基本的功能:内存管理、进程调度、IPC。其他服务都跑在用户态。

我曾经参与过一个项目,需要把一套复杂的加密算法移植到TEE里。刚开始我们想直接在TEE内核里加功能,后来发现这违背了微内核的设计原则。正确的做法是写一个TA,通过IPC调用内核服务。嗯,这里要注意,TA之间的通信也要经过严格权限检查。

2.4 鸿蒙TEE的关键技术

2.4.1 安全启动

安全启动是TEE的第一道防线。从BootROM开始,逐级验证镜像签名。如果任何一级验证失败,系统就拒绝启动。

我记得有一次,客户反馈说设备无法启动。排查后发现是TEE镜像的签名密钥被更换了,但BootROM里烧录的还是旧公钥。这就是典型的密钥管理问题。

避坑指南:我曾经见过有人为了调试方便,关闭了安全启动。结果产品上市后被攻击者刷入了恶意TEE镜像。安全启动一旦关闭,TEE的信任根基就没了。生产环境务必开启。

2.4.2 安全存储

TEE里的数据需要持久化存储。但TEE本身没有文件系统,怎么办?

鸿蒙的做法是:TEE把加密后的数据交给REE侧的文件系统保存。TEE只保存一个很小的密钥,用于加密和解密这些数据。

流程大概是这样的:

1. TA调用安全存储API,传入明文数据
2. TEE内核用设备唯一密钥加密数据
3. 加密后的密文传给REE侧,由REE写入文件系统
4. 读取时,REE把密文传给TEE,TEE解密后返回给TA

这样做的好处是:即使REE侧的文件系统被完全控制,攻击者拿到的也只是密文,没有TEE里的密钥,无法解密。

2.4.3 安全IPC

REE和TEE之间需要通信。鸿蒙使用SMC(Secure Monitor Call)指令来实现世界切换。

但频繁切换世界是有性能开销的。我建议尽量减少跨世界通信的次数。比如,把多个小数据合并成一个大包,一次传输。

我的经验:有一次优化支付模块,发现每次支付要调用5次TEE接口。后来我们重新设计了协议,把5次合并成2次,性能提升了30%。

2.5 鸿蒙TEE的编程模型

开发TEE应用,你需要了解两个角色:

  • CA(Client Application):运行在REE侧,调用TEE功能
  • TA(Trusted Application):运行在TEE侧,提供安全服务

CA和TA通过TEE Client API通信。鸿蒙兼容GlobalPlatform TEE标准,所以如果你在其他平台上开发过TEE应用,迁移起来会比较顺畅。

下面是一个简单的TA示例:

// TA入口函数
TEE_Result TA_CreateEntryPoint(void) {
    // 初始化TA资源
    return TEE_SUCCESS;
}

// 处理CA请求
TEE_Result TA_InvokeCommandEntryPoint(
    void *sessionContext,
    uint32_t commandID,
    uint32_t paramTypes,
    TEE_Param params[4]) {
    
    switch (commandID) {
        case CMD_ENCRYPT:
            // 执行加密操作
            return encrypt_data(params);
        case CMD_DECRYPT:
            // 执行解密操作
            return decrypt_data(params);
        default:
            return TEE_ERROR_BAD_PARAMETERS;
    }
}

// TA销毁入口
void TA_DestroyEntryPoint(void) {
    // 释放TA资源
}

你可能会问:TA里能做什么,不能做什么?

TA运行在TEE侧,可以访问TEE的内存和外设。但TA不能直接访问REE侧的内存,也不能调用REE侧的系统调用。所有跨世界的数据交换,都必须通过TEE内核的IPC机制。

2.6 鸿蒙TEE的安全隔离机制

隔离是TEE的核心。鸿蒙实现了多层隔离:

  1. 硬件隔离:TrustZone硬件强制划分世界
  2. 内存隔离:TEE有自己的物理内存区域,REE无法访问
  3. 外设隔离:某些外设(如安全键盘、指纹传感器)可以配置为只响应TEE
  4. 进程隔离:TEE内的不同TA之间相互隔离,一个TA无法访问另一个TA的内存

我记得有一次做安全审计,发现某个TA的堆栈没有做边界检查。理论上,一个恶意的CA可以通过精心构造的输入,让TA发生缓冲区溢出。虽然TEE内的溢出不会影响REE,但可能破坏其他TA的数据。后来我们加上了堆栈保护机制。

警告:不要以为TEE就绝对安全。TEE只是缩小了攻击面,但TEE内的漏洞同样致命。比如2019年发现的TrustZone漏洞CVE-2019-2215,就允许攻击者从REE侧提升权限到TEE侧。

2.7 实战:在鸿蒙上开发一个简单的TEE应用

咱们来点实际的。假设我们要开发一个安全存储TA,用于保存用户的支付密码。

第一步:编写TA代码

#include "tee_internal_api.h"

#define CMD_STORE_PASSWORD 1
#define CMD_GET_PASSWORD   2

static uint8_t stored_password[32];
static bool password_stored = false;

TEE_Result TA_InvokeCommandEntryPoint(
    void *sessionContext,
    uint32_t commandID,
    uint32_t paramTypes,
    TEE_Param params[4]) {
    
    TEE_Result result;
    
    switch (commandID) {
        case CMD_STORE_PASSWORD:
            // 检查参数类型
            if (paramTypes != TEE_PARAM_TYPES(
                TEE_PARAM_TYPE_MEMREF_INPUT,
                TEE_PARAM_TYPE_NONE,
                TEE_PARAM_TYPE_NONE,
                TEE_PARAM_TYPE_NONE)) {
                return TEE_ERROR_BAD_PARAMETERS;
            }
            // 复制密码到安全存储区
            memcpy(stored_password, params[0].memref.buffer, 
                   MIN(params[0].memref.size, 32));
            password_stored = true;
            return TEE_SUCCESS;
            
        case CMD_GET_PASSWORD:
            if (!password_stored) {
                return TEE_ERROR_ITEM_NOT_FOUND;
            }
            // 返回存储的密码
            memcpy(params[0].memref.buffer, stored_password, 32);
            return TEE_SUCCESS;
            
        default:
            return TEE_ERROR_BAD_PARAMETERS;
    }
}

第二步:编写CA代码

#include "tee_client_api.h"

TEEC_Result store_password(const char *password) {
    TEEC_Context context;
    TEEC_Session session;
    TEEC_Operation operation;
    TEEC_Result result;
    
    // 初始化TEE上下文
    result = TEEC_InitializeContext(NULL, &context);
    if (result != TEEC_SUCCESS) return result;
    
    // 打开TA会话
    result = TEEC_OpenSession(&context, &session,
                              &TA_UUID,
                              TEEC_LOGIN_PUBLIC,
                              NULL, NULL, NULL);
    if (result != TEEC_SUCCESS) {
        TEEC_FinalizeContext(&context);
        return result;
    }
    
    // 准备参数
    memset(&operation, 0, sizeof(operation));
    operation.paramTypes = TEEC_PARAM_TYPES(
        TEEC_MEMREF_TEMP_INPUT,
        TEEC_NONE, TEEC_NONE, TEEC_NONE);
    operation.params[0].tmpref.buffer = (void*)password;
    operation.params[0].tmpref.size = strlen(password) + 1;
    
    // 调用TA
    result = TEEC_InvokeCommand(&session, CMD_STORE_PASSWORD,
                                &operation, NULL);
    
    // 清理
    TEEC_CloseSession(&session);
    TEEC_FinalizeContext(&context);
    
    return result;
}

第三步:编译和部署

鸿蒙提供了专门的编译工具链。TA需要编译成独立的镜像,签名后部署到TEE侧。CA则作为普通应用,部署在REE侧。

提示:调试TEE应用比较麻烦,因为不能直接打日志。我一般用两种方法:一是通过安全存储写调试信息,二是用TEE提供的调试接口(但生产环境要关闭)。

2.8 总结与思考

TEE不是银弹,但它确实是移动设备安全的重要基石。鸿蒙的TEE实现,结合了微内核的安全性和TrustZone的硬件隔离能力,为敏感数据提供了可靠的保护。

最后说几点我的体会:

  • 最小化原则:TEE里跑的代码越少越好。能放REE的就别放TEE
  • 接口设计:TA的接口要尽量简单,参数要严格校验
  • 密钥管理:TEE里的密钥要分级管理,设备密钥、应用密钥、会话密钥要分开
  • 安全审计:定期做TEE的安全审计,别以为部署了就万事大吉

下一章,咱们聊聊鸿蒙的数据加密体系。到时候会讲到怎么在TEE里安全地管理密钥,以及如何设计一个实用的加密方案。