ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于OP-TEE的M核固件认证与安全启动实践

基于OP-TEE的M核固件认证与安全启动实践 1. 为什么M核固件认证不能只放在BootROM里在 LAT6028 上把 M 核固件认证交给 OP-TEE 之后我才真正理解一个道理所谓“安全启动”不是把前面几级启动镜像验完签就结束了。M 核如果不在 BootROM 的直接管控范围内那它就是一个容易被忽略的后门。1.1 一次“假安全启动”暴露的问题我先说一个我实际接触过的场景。某个带 LAT6028 的设备A 核那边 BootROM、BL2、ATF、OP-TEE 全链路都验过签了Linux 内核也正常起来安全启动的日志一串绿。结果呢M 核固件还是被人换了。问题出在 M 核的加载方式上。LAT6028 是典型的异构双核设计Cortex-A 侧跑 Linux OP-TEEM 核侧跑裸机或者 RTOS 实时任务。M 核镜像并不像内核那样由 BootROM 直接引导而是先放在文件系统里等 Linux 启动到用户态之后再由某个驱动或者脚本搬运到约定的内存地址然后触发 M 核复位。攻击者一旦拿到 root根本不需要去碰 A 核固件直接替换文件系统里的 M 核镜像再触发一次 M 核启动就把 RTOS 里的控制逻辑换成了自己的代码。这个后门非常隐蔽因为 A 侧安全启动的完整性日志完全正常M 核的异常只有在业务层面才会暴露而业务层面通常已经晚了。所以M 核固件认证不能想当然地认为“BootROM 管了”。BootROM 管不到运行时才加载的 M 核镜像它只负责最前面那几级引导。M 核固件的可信性必须由另外一个运行在安全环境里的组件来负责也就是 OP-TEE。1.2 OP-TEE在LAT6028启动链上的实际位置把 OP-TEE 放进来看LAT6028 的启动链其实分两条线。一条是 A 核主链BootROM - BL2 - ATF BL31 - OP-TEE - Linux。另一条是 M 核独立域M 核可以从自己的复位向量启动也可以由 A 核通过寄存器释放复位。通常产品会采用“A 核准备好镜像后释放 M 核”的方式于是 M 核固件就变成了 A 核软件栈里的一块数据。OP-TEE 在 A 核安全世界里跑起来之后提供了一组可信服务。我的做法是把 M 核镜像的验签逻辑实现为一个 OP-TEE Trusted ApplicationTA然后让 Linux 侧在释放 M 核之前调用这个 TA 完成镜像校验。只有校验通过OP-TEE 侧的 Secure Service 才会允许写 M 核复位控制寄存器。这里需要厘清一个容易混淆的点OP-TEE 本身跑在 A 核的 TrustZone 安全世界它并不直接跑在 M 核上。所谓的“用 OP-TEE 做 M 核固件认证”本质是把 M 核的启动决策权收进安全世界让 M 核能不能被释放由安全世界说了算。这也是 LAT6028 这类异构平台最常见的安全改造思路。2. 认证方案选型为什么最终落在OP-TEE TA里在动手写代码之前我先把方案比了一圈。M 核固件认证可以放的位置有好几个BL2、Linux 驱动、OP-TEE core、OP-TEE TA。选型不完全是技术问题更多是安全边界和维护成本的权衡。2.1 先定义清楚“认证”是验什么“固件认证”这四个字在不同人嘴里含义不一样。在 LAT6028 这个场景里我定义得很明确第一镜像没有被篡改第二镜像来自受信任的厂商私钥。完整性用 SHA-256 做来源和抗伪造用 RSA-2048 签名做。镜像可以明文存储但签名必须验。为什么不直接上对称密钥的 HMAC因为 M 核镜像放在文件系统里设备被人拿走拆开后flash 是可以被读取的。如果用对称密钥做 tag攻击者只要从一台设备里提取出密钥就能给任意伪造镜像重新计算合法的 tag所有设备都会放行。非对称签名里验签公钥留在设备上签名私钥留在离线环境攻击者拿到设备也改不了镜像。“认证”和“加密”也要分开。如果业务上担心 M 核固件被人逆向分析那还得再加 AES-GCM 解密但那是另一件事。认证只保证“这个镜像是厂商发布的且没被改过”。2.2 三种实现位置的对比我先列了一张表把候选方案摊开看实现位置优点缺点BL2 阶段启动最早信任链最长BL2 环境太简单内存和算法支持都很受限后续换镜像要重新进 BL2Linux 普通驱动实现容易调试方便Linux 本身可能被攻破验签逻辑和签名密钥都暴露在攻击面里OP-TEE core安全世界底层逻辑最难绕过开发和升级成本高每次改动都要重新验证整个 OP-TEE OSOP-TEE TA隔离在安全世界可单独升级需要设计好 TA 与 Secure Service 的调用关系稍微绕一点BL2 方案我一开始很动心因为它能提前到操作系统起来之前就把 M 核镜像验掉。但 LAT6028 的 M 核镜像有接近 1MBBL2 那个阶段既不方便读文件系统也不方便处理大块数据更别说做 RSA 验签了。硬塞进 BL2会让 BL2 变成一个大泥潭。Linux 驱动方案看起来省事但安全边界不成立。root 权限下要么绕过驱动直接写 M 核寄存器要么 hook 掉验签函数等于没验。最后我选了 OP-TEE TA。逻辑上验签代码在安全世界执行密钥也在安全世界解析功能上又能按产品需求单独出版本不用把 TA 升级和 OP-TEE OS 升级绑死。对 LAT6028 这种既要灵活性又要安全边界的平台这是最合理的折中点。3. LAT6028上基于OP-TEE的M核固件认证实现拆解整个实现我拆成四块镜像签名、公钥部署、TA 验签、Linux 侧调用。每块都不复杂但拼起来需要把边界看清楚。3.1 镜像签名与公钥部署签名这步在 PC 上做属于发布流程的一部分。我这里用的是 RSA-2048 SHA-256实际命令很简单# 生成签名私钥生产环境建议放到 HSM 里 openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out mcore_sign_key.pem # 导出公钥 DER 格式后续作为 C 数组编进 TA openssl rsa -in mcore_sign_key.pem -pubout -outform DER -out mcore_pub.der # 把 DER 转成 C 数组 xxd -i mcore_pub.der mcore_pub_der.c # 签名 M 核镜像 openssl dgst -sha256 -sign mcore_sign_key.pem -out mcore_image.bin.sig mcore_image.bin这里有个关键点签名不能只签固件原始内容最好把镜像头里影响加载的属性一起签进去否则攻击者可能不动固件内容只改镜像头里的加载地址、大小或版本号。比如下面的头结构struct mcore_image_header { uint32_t magic; // 比如 0x4D434F52 uint32_t version; // 固件版本用于防回滚 uint32_t key_id; // 用于密钥轮换 uint32_t img_size; // M 核代码段大小 uint32_t load_addr; // 加载地址 uint8_t signature[256]; };签名时把magic version key_id img_size load_addr image bytes作为被签数据signature字段留空。验签时也按同样规则拼接这样固件头里的任何关键字段被改动都会导致验签失败。公钥部署我用了两层。第一层公钥 DER 数组编进 TA 里第二层公钥的 SHA-256 哈希烧到 LAT6028 的 eFuse/OTP 区域。TA 启动时可以读 OTP 里的哈希对自身内置公钥做一次比对。这样即使有人改了 TA 镜像、换了一把自己的公钥只要 OTP 哈希对不上验签服务照样不可用。3.2 TA 内部实现要点TA 实现里最核心的是一个verify_mcore_image命令。输入参数是 M 核镜像在共享内存里的地址和大小输出是验签结果。我用 OP-TEE TA 的 GP TEE Internal API 做内存引用管理验签逻辑直接复用 mbedTLS这样和 Linux 侧的验签代码可以保持同一套密码库减少“平台之间行为不一致”的坑。核心代码逻辑示意如下#define CMD_VERIFY_MCORE_IMAGE 0 static TEE_Result verify_mcore_image(uint32_t param_types, TEE_Param params[4]) { uint32_t exp_types TEE_PARAM_TYPE_MEMREF_INPUT | TEE_PARAM_TYPE_MEMREF_INPUT | TEE_PARAM_TYPE_VALUE_OUTPUT; if (param_types ! exp_types) return TEE_ERROR_BAD_PARAMETERS; uint8_t *img params[0].memref.buffer; uint32_t img_size params[0].memref.size; uint8_t *sig params[1].memref.buffer; uint32_t sig_size params[1].memref.size; // 1. 解析镜像头校验 magic版本号不能低于当前最低版本 struct mcore_image_header *hdr (struct mcore_image_header *)img; if (hdr-magic ! MCORE_MAGIC || hdr-version MIN_MCORE_VERSION) { return TEE_ERROR_SECURITY; } // 2. 计算被签数据的 SHA-256注意要分块不能一次性大头快照 mbedtls_sha256_context ctx; uint8_t digest[32]; mbedtls_sha256_init(ctx); mbedtls_sha256_starts(ctx, 0); mbedtls_sha256_update(ctx, (uint8_t *)hdr, offsetof(struct mcore_image_header, signature)); mbedtls_sha256_update(ctx, img sizeof(*hdr), img_size - sizeof(*hdr)); mbedtls_sha256_finish(ctx, digest); mbedtls_sha256_free(ctx); // 3. 解析公钥并验签 mbedtls_pk_context pk; mbedtls_pk_init(pk); int ret mbedtls_pk_parse_public_key(pk, mcore_pub_der, mcore_pub_der_size); if (ret ! 0) { mbedtls_pk_free(pk); return TEE_ERROR_SECURITY; } ret mbedtls_pk_verify(pk, MBEDTLS_MD_SHA256, digest, 0, sig, sig_size); mbedtls_pk_free(pk); if (ret ! 0) return TEE_ERROR_SECURITY; params[2].value.a hdr-version; return TEE_SUCCESS; }这个 TA 的边界作用就是“验签”它不负责加载镜像也不负责释放 M 核。加载镜像依然是 Linux 侧的工作TA 只输出一个“可信”的结论。真正在系统里把它变成强制动作的是 TA 背后的 Secure Service。3.3 Linux侧调用与M核释放的联动Linux 侧我用了标准的 OP-TEE Client API。大致流程是驱动先从文件系统读出 M 核镜像到一段 DMA 内存然后调用 TA 验签验签通过后再通过一个安全的 SMC 服务来释放 M 核。TEEC_Context ctx; TEEC_Session session; TEEC_Operation op; TEEC_InitializeContext(NULL, ctx); TEEC_OpenSession(ctx, session, ta_uuid, TEEC_LOGIN_PUBLIC, NULL, NULL, NULL); memset(op, 0, sizeof(op)); op.paramTypes TEEC_PARAM_TYPES(TEEC_MEMREF_TEMP_INPUT, TEEC_MEMREF_TEMP_INPUT, TEEC_VALUE_OUTPUT, TEEC_NONE); op.params[0].tmpref.buffer mcore_image_buf; op.params[0].tmpref.size mcore_image_size; op.params[1].tmpref.buffer mcore_sig_buf; op.params[1].tmpref.size mcore_sig_size; TEEC_InvokeCommand(session, CMD_VERIFY_MCORE_IMAGE, op, NULL);这里有个非常重要的安全设计M 核的复位控制寄存器必须被放在安全世界里不能直接暴露给 Linux 的非安全世界。实际操作中我在 OP-TEE 侧加了一个 Secure Service只有验签通过的 TA 会话才能往一个内部标志位写“允许启动”状态然后 Secure Service 才会去操作 M 核 release 寄存器。这样即使 Linux 被攻破攻击者也只能把镜像传到共享内存里但触发不了 release 动作。部署时TA 需要编译成.ta文件放进文件系统并在 OP-TEE 的配置里让 TA 签名使能打开否则 TA 本身没有完整性保护整个验证逻辑也站不住。make CROSS_COMPILEarm-linux-gnueabihf- \ TA_DEV_KIT_DIR$HOME/optee/optee_os/out/arm-plat-lat6028/export-ta_arm323.4 编译和部署中的几个细节TA 编译不是难点但有三个细节容易翻车。第一TA 的 heap 大小要改默认配置往往只有 4KB 左右验签过程要分配 mbedTLS 上下文和公钥结构很容易堆溢出我在ta_manifest里把堆调到了 64KB。第二mbedtls_pk_parse_public_key需要 DER 编码的 SubjectPublicKeyInfo不是裸的 RSA 公钥这个后面讲坑的时候展开。第三TEE_MEMREF_TEMP_INPUT和TEE_MEMREF_PARTIAL_INPUT用混了会导致 TA 拿到的 buffer 不是实际镜像数据我在第一次联调时就被这个坑白白折腾了半天。4. 调试过程中最值得记录的四个坑代码跑通前后我踩了不少坑。每个坑单独看都不算大但四个叠在一起足够让人怀疑人生。4.1 公钥格式DER编码不一样验签直接翻车第一个坑出现在公钥部署阶段。我用openssl rsa -in key.pem -pubout -outform DER导出的公钥拿给 TA 里的mbedtls_pk_parse_public_key解析一直返回MBEDTLS_ERR_PK_INVALID_PUBKEY。排查了半天才发现问题-pubout导出的 DER 是 SubjectPublicKeyInfo 结构也就是AlgorithmIdentifier BIT STRING这没问题但 mbedTLS 的pk_parse_public_key也能解析裸的 PKCS#1 RSAPublicKey。问题出在我另一个脚本里用了openssl rsa -RSAPublicKey_out -outform DER重新导出了一份两份 DER 长度不一样我在 TA 里编进去的是后者解析器却按前者去解析。解决的办法很简单统一用openssl rsa -pubout -outform DER导出并在编译前用openssl asn1parse -inform DER -in mcore_pub.der确认结构。这里也提醒一句公钥转成 C 数组后最好在 TA 里加一个启动自检重新解析一遍公钥如果结构不合法直接拒绝启动验签服务。否则错误会拖到真正验签时才暴露。4.2 大镜像塞不进TEE内存哈希必须分块M 核镜像在我的项目里通常几百 KB 到 1MB 左右。第一次实现时我图省事在 TA 里把整个镜像拷进了一个用TEE_AllocateTransientObject申请的大缓冲区然后再一次性算 SHA-256。结果镜像一超过一定大小TA 就报内存不足。原因很直接TA 所在的安全世界内存不像 Linux 用户态那么宽裕镜像数据实际上是放在共享内存里的TA 内部再复制一份不仅浪费还会撞上透明对象大小上限。正确做法是分块哈希。TEE 的 memref buffer 允许 TA 直接访问共享内存里的数据所以我可以反复对同一块地址做mbedtls_sha256_update。镜像整体不用进 TA 私有堆只有 32 字节的摘要和 256 字节的签名在 TA 内部流转。这个改动之后镜像大小对 TA 内存就不再敏感了。4.3 mbedTLS裁剪配置验签返回不一致的错误第三个坑是 mbedTLS 的配置裁剪。我的 TA 工程为了节省空间编译 mbedTLS 时裁剪了不少 feature其中把MBEDTLS_MD_C和MBEDTLS_SHA256_C相关的一些宏给裁掉了。结果mbedtls_pk_verify返回的不是“算法不支持”而是随机的MBEDTLS_ERR_RSA_VERIFY_FAILED让人误以为签名错误。这个坑特别迷惑人。排查的时候我先换了另一组签名数据还是失败又换公钥依然失败。最后打开 mbedTLS 的调试输出才看到底层md_info_from_type返回了空指针。所以 TA 里用 mbedTLS 时编译配置里至少要保留MBEDTLS_SHA256_C、MBEDTLS_MD_C、MBEDTLS_PKCS1_V15和MBEDTLS_RSA_C。别迷信“裁剪优化”除非你非常清楚 mbedTLS 的运行依赖链否则裁剪带来的性能提升远小于它带来的排查成本。4.4 验签通过不代表防回滚版本号被绕过了这不算代码 bug而是设计遗漏。早期版本里我验签只校验“签名是否有效”没校验“版本是否是最新”。结果攻击者拿旧版本的合法固件刷回去验签照样通过产品就运行在带已知漏洞的旧固件上。业务方还反馈说“认证没问题为什么功能变了”。所以镜像头里的version字段不是摆设。我在 TA 里加了最低版本检查并把当前允许的最低版本存在 RPMB 安全存储里。每次固件升级由升级服务在验签通过后更新这个最低版本号。这样即使攻击者手里有旧的合法签名镜像也过不了版本检查。这里还要提一个细节摘要比较和版本比较不要用裸的memcmp尤其在安全环境里最好使用常数时间比较函数。侧信道攻击虽然不常见但安全代码里养成好习惯成本很低。5. 密钥轮换与回滚保护认证之后的下一步验签跑通只是第一步真正要落地到产品还得考虑密钥轮换和回滚保护。否则今天能用不代表明年被攻击时还扛得住。5.1 镜像头里的key_id是怎么用的我前面提到镜像头里有key_id字段它对应一把公钥索引。每次签名时发布工具会把key_id写进镜像头TA 验签时先读key_id再从 TA 内置的公钥表里选对应公钥。这样做的好处是密钥轮换不用一次改所有设备上的 TA 代码。实际部署时公钥表一般内置两三把公钥一把是当前生效主密钥一把是备用密钥。轮换流程基本是先升级 TA把新公钥加入公钥表再用旧私钥签一个“密钥切换授权”镜像设备验签通过后把公钥表中的主密钥索引切到新索引。整个流程不需要现场烧录 eFuse也不影响已经部署的设备。5.2 OTP和RPMB的分工我把设备上的安全存储分成两层。OTP/eFuse 里只放“根信任”信息比如公钥表的哈希、是否已经锁定的熔丝位。RPMB 里放动态状态比如最新的最低固件版本、密钥轮换标记。这个分工很关键。OTP 是小容量的不能频繁写适合烧录后长期不变的数据RPMB 可以重复写但是重放攻击风险高所以我在 RPMB 里写数据时一定会带上一个单调计数器。LAT6028 的 OP-TEE 支持 RPMB 安全存储直接用就能满足需求。5.3 实测效果与性能参考M 核固件认证的耗时主要花在 SHA-256 和 RSA verify 上。SHA-256 分块计算大镜像很快RSA-2048 验签本身也就几毫秒到十几毫秒整体对启动时间的影响基本可以忽略。如果后续 M 核镜像继续变大可以考虑先对镜像做“分块哈希 签名摘要”的方式但当前这种“整镜像签名”的方式最直观也最容易排查问题。在 LAT6028 上我把这个过程从 TA 创建、验签、到 release M 核整体收敛在一个启动脚本里。实测验签 1MB 镜像加启动 M 核总耗时在几十毫秒级别完全满足产品启动时序要求。最后分享一个我在实际项目中的体会M 核固件认证这种事技术实现并不是最难的难的是把“谁可信、谁负责启动、谁持有密钥”这几个边界跟固件、系统、业务三方的同学对齐。只要边界不清再强的验签代码也可能在某个集成环节被绕过。把这个想清楚OP-TEE 才能从“Trusted OS”变成真正的“信任锚点”。
返回列表