指纹方案移植做过多轮的工程师都有体会:REE侧驱动、HAL哪怕跑通了,心里还是不踏实,因为真正握着指纹数据和算法的是安全世界里的TA。我这次拿到一台基于骁龙平台的新主板,要把整套指纹识别方案从参考平台挪过来,前面驱动、HAL、指纹服务改得七七八八,剩下最硬核的一步,就是把QSEE侧的木马?不对,是QSEE里的TA给移植过去。这篇文章就围绕这最后一步展开:TA在指纹方案里到底干嘛的、移植前要准备哪些东西、怎么搭编译工程、动手改哪些文件、最后镜像怎么签名落地,以及我在联调时踩过的一堆雷。适合正在做高通平台指纹识别方案、准备碰QSEE的软工和BSP工程师参考。
1. 移植前必须想清楚的四件事
1.1 TA在整个指纹方案里扮演什么角色
先把架构讲明白。在高通的TrustZone生态里,普通世界(REE)和高安全世界(QSEE)是靠ARM TrustZone硬件隔离的。REE这边有指纹内核驱动、指纹HAL、fingerprintd,它们负责和传感器打交道,比如初始化SPI、控制中断、读取原始图像数据,但这些只是“搬运工”。真正干活的是运行在QSEE里的TA(Trusted Application)。
一颗指纹模组从按下手指到最终解锁,流程大致是这样的:REE驱动收到中断后,通知HAL去采集指纹图像;HAL把图像buffer映射到共享内存,然后通过qseecom设备节点发一个命令给TA;TA在安全世界里拿到图像数据,执行特征提取,再和本地模板做比对,返回结果。整个过程里,指纹图像、特征点、模板明文的读写都在安全世界里完成,REE侧拿到的只是“匹配成功/失败”这类结果。模板数据通常也由TA负责加密写入安全存储(RPMB或SFS),算法密钥不会暴露给Android系统。
所以简单总结:TA是这套指纹方案的安全核心,也是整个方案能不能过安全评审的关键。之前我有一次图省事,把模板直接存到REE侧/data分区,结果安全测试一针见血,直接标了高风险。后来老老实实把模板迁移做进TA,才把这个问题关掉。换句话说,如果你只是想把指纹功能跑起来,可能REE侧做点恶就能凑合;但要做成一个能商用的方案,TA这一环是绕不过去的。
1.2 移植前的技术材料清单
除了代码,移植前要准备的东西其实比想象中多。我列一下我自己常用的清单,每一项都踩过坑:
- 指纹传感器型号、规格书(datasheet),SPI工作模式、中断触发电平、复位时序、供电要求。这块决定你在REE侧怎么配置,虽然TA不直接管GPIO,但TA拿到的图像质量依赖REE侧采集链路是否正确。
- 模组厂商提供的算法库版本。同一个模组,算法版本不同,TA内部行为差异可能很大,最好拿到和原平台完全相同的版本再动手。
- 目标平台的TZ软件包。老平台叫tzbsp,新平台一般在vendor BSP里,里面包含QSEE的运行环境和已有的系统TA。
- 参考平台可用的TA,最好同时有源码和二进制。优先用源码,没有源码也要保留二进制,并记清楚它是哪个平台编出来的。
- 目标板原理图和设备树源文件。SPI挂在哪组控制器,中断脚、复位脚各连到哪个GPIO,电源域有哪些,这些要看清楚。
- 一份能正常工作平台的完整启动log和运行时log,用来对照。
我习惯在动手前先把这些资料归档到一个目录,避免移植过程中到处找。指纹方案移植最怕的不是代码复杂,而是资料零散,改到一半发现忘了某根GPIO的实际接法,只能干等。
1.3 确认QSEE版本和TA规范是否匹配
这一条很多新手会忽略,但往往是第一只拦路虎。高通的TrustZone环境不是一成不变的,从MSM8953时代的QSEE 1.0/2.0,到后来的QSEE 4.0,再到新平台常见的“通用TEE”和带有aarch64 TA支持的环境,TA的二进制格式、ABI、系统调用接口都有差异。
最常见的问题就是:参考平台是32位TA,目标平台是64位TA环境,两边ABI不对,签名检测过了也照样加载失败。还有qseecom的ioctl命令号,不同平台有变化,REE侧HAL调用的通信协议如果和TA预期不一致,也会表现成“命令有去无回”或“返回乱码”。
我的做法是在正式移植前,先看一眼目标平台已有的系统TA是怎么编的。比如keymaster、widevine这些TA的格式,确认它们是32位还是64位,加载路径在哪。再用相同工具链尝试编译一个最小TA,加载成功后再把指纹TA放进来,这样能把“环境问题”和“业务逻辑问题”分开。
1.4 选对移植路线:源码移植还是二进制移植
拿到厂商的TA后,首先面临路线选择。有三种常见情况:
第一种,拿到完整源码。这是最舒服的,可以自己改命令号、加log、调heap大小,也能适配不同sensor型号。缺点是编译环境搭建需要点功夫,而且如果算法核心也在源码里,牵扯到授权问题。但总体推荐这个路线,排查问题灵活。
第二种,只有二进制。那问题也不大,但有几个硬性约束:UUID不能改、TA实现的命令列表必须固定、REE侧驱动和HAL得严格配套。二进制TA在目标平台上的加载路径、依赖的SFS文件位置也必须保持兼容,否则跨平台后数据读不出来。
第三种,混合模式。很多指纹模组厂商会把算法封装成一个算法TA提供给你的同时,再给你一个适配TA的源码框架,由适配TA去调用算法TA。这个模式下,你负责把适配TA改到目标平台,算法TA按固定接口加载,两边通过内部session通信。
如果条件允许,我强烈建议在动手前先验证一条“可行性红线”:把原平台的TA(或最小TA)在目标平台上成功加载起来。这一步过了,后面的移植就是拼肉;这一步不过,先搞清楚环境差异再往下走。
2. 搭好工程再动手:QSEE TA的编译环境与源码结构
2.1 源码目录与工程结构快速认识
高通QSEE TA的源码在BSP里一般有固定的摆放位置。老平台的tzbsp包里,路径通常长这样:trustzone_images/core/securemsm/trustzone/qsapps/,下面每一个子目录就是一个TA,比如keymaster、sfs、widevine。指纹TA一般会新建一个自己的目录,里面再分src、include、tools等。
新平台因为Android构建系统的演进,很多厂商把securemsm相关代码挪到了vendor/qcom/proprietary/securemsm下面,或者以单独的安全镜像仓库方式提供。不管放哪,结构上都差不多:一个TA目录里至少要包含源码文件、头文件、一个编译描述文件(Android.mk或者securemsm.mk),以及一份标志TA的配置文件,里面记录UUID、镜像名、构建类型这些信息。
我第一次做指纹TA移植时,犯过一个低级错误:只把源码复制到了新平台的目录下,忘了在镜像列表配置里声明这个TA。结果编译出来的系统固件里根本没有指纹TA镜像,加载时一直报TA找不到,排查大半天。后来才明白,在QSEE这套体系里,不是你放了源码它就会自动编进去,需要显式地把TA注册到构建系统里。
2.2 工具链、编译脚本与关键环境变量
QSEE TA的编译不像普通Android App那样敲个gradle就能完事。它依赖BSP自带的交叉工具链,以及一套指定的链接脚本。老平台常用arm-none-eabi或qcc工具链,新平台则倾向于用clang配合QSEE的链接脚本和库。
编译之前要确认几个环境变量:TARGET_BOARD_PLATFORM对应具体芯片平台名,CHIPSET用来区分同一平台的不同芯片版本,还有一个指向工具链路径的变量。这些变量设置错了,编译会直接报错,或者编出来的镜像格式不对,加载时被TZ拒绝。
这里我建议直接在厂家BSP的编译环境里操作,不要自己手工造轮子。我见过有同事为了图快,把老平台的编译链硬拷到新平台,结果源码头文件的ABI结构对不上,编出来的TA加载必挂。最稳妥的办法是先试着编译一个系统自带的TA,比如sfs或keymaster,确认整个工具链在目标环境里能正常出镜像,再动指纹TA的代码。
2.3 TA入口函数与REE侧接口协议
QSEE里的TA有四个入口函数是必须实现的,分别是TA_CreateEntryTA、TA_CreateSessionTA、TA_InvokeCommandTA、TA_DestroySessionTA。注册TA时,要调用qsee_ta_register接口,把UUID、heap、数据段大小、命令处理函数表传进去。整套体系把TA生命周期管理得比较规范,REE侧通过qseecom驱动发起session,然后才能传命令。
在移植时,需要重点确认三样东西:
- UUID是否和REE侧HAL里定义的一致,不一致直接找不到TA。
- 命令处理函数表是否完整,比如指纹方案常见的enroll、authenticate、remove这些命令,都在表里注册过。
- 共享内存缓冲区的格式。TA从REE侧拿到的数据是通过共享内存映射过来的,如果两边结构体定义不一致,轻则特征提取不出来,重则TA直接panic重启。
接口协议这块,我一般会在移植前把REE侧HAL的发送命令代码和TA侧的命令解析代码放在一起比对一遍。重点看命令号、参数结构体、buffer长度字段,对照一遍基本上就能避免八成以上的通信问题。
3. 动手移植:从新建TA工程到镜像落地的全过程
3.1 新建指纹TA工程并配置UUID、Heap和Stack
一切准备妥当后,开始正式移植。第一步是在目标平台的TA目录下新建指纹TA的工程目录,把厂商提供的源码复制进来。
在构建描述文件里,把TA的UUID写上。UUID不是随便编的,要跟REE侧HAL保持一致,一般由模组厂商或方案商约定。个人改动UUID属于大忌,除非你是整套方案重新设计,否则别乱动。
然后配置heap和stack大小。很多人忽略这两个参数,等到TA在运行到特征提取时报内存不足才想起来。指纹算法在某些型号上跑起来很吃内存,尤其是模板训练阶段,参考值一般要64KB以上。我自己习惯先按原平台的配置给,再把大图反复跑几轮压测,如果稳定就维持原样。heap设太大会拖慢TA加载速度,设太小直接崩,这个平衡要在实测里找。
还有一个容易被忽视的配置是TA是否启用线程支持、是否允许安全世界内部调用某些系统服务。指纹TA一般需要访问安全存储(SFS/RPMB),在配置里要把这些依赖打开。
3.2 对接指纹Sensor的硬件访问逻辑
这一步让很多人产生误解:以为TA移植就是把代码编一下,大不了改改log,实际上硬件适配才是大头。
老一代方案里,QSEE TA可以直接访问部分外设,比如SPI控制器、共享GPIO。这种情况下,指纹图像可以由TA直接通过安全世界的SPI控制器获取,REE侧驱动只是帮忙做电源管理和中断通知。但这种方案配置起来很麻烦,要在TZ侧设置对应的XPU权限和外设映射,稍有不对整个TZ都可能起不来。
现在大多数主流方案,尤其是新平台,倾向于“REE采集、TA处理”分工:REE侧负责SPI读写、采集原始图像、控制中断和复位,然后通过共享内存把图像数据传进TA。TA只负责算法处理和安全存储。好处是硬件适配集中在REE侧,TA移植工作量小一些。
你的目标平台到底属于哪种模式,直接决定你要改哪些东西。如果走“REE采集”模式,那TA侧其实只需要保证共享内存协议没问题;如果走“TA直读外设”模式,那就要在TZ配置里新增SPI控制器的访问权限、IO引脚映射,复杂程度几何级上升。
3.3 指纹模板数据与安全存储的迁移
指纹功能上线前最容易被忽视但也最容易出事的是模板数据。用户已经录入的指纹模板,在旧平台里存的是旧TA加密过的数据,直接搬过去给新TA解密,基本解不出来。
如果平台算法没变,只是换了颗sensor,TA里模板格式可能兼容;如果算法版本变了,或者安全存储密钥发生了变化,那历史模板继续使用就是奢望。这种情况下,产品上一般要做两个方案:
一是量产下线阶段强制用户重新录入指纹。虽然体验不佳,但最干净,不会出现“指纹明明还在,但怎么解不了锁”的诡异问题。
二是提供一个“模板迁移接口”,由新TA识别旧模板版本后重新封装。这个方案听着高级,实际做起来要考虑版本回退、安全密钥变化、RPMB数据越界等一堆问题,除非老平台用户升级需求非常强烈,否则我一般不建议接这个活。
从安全角度讲,模板迁移还涉及一个原则性问题:新平台必须要能确认旧模板确实来自可信来源,不能随便接受一个外部数据就当模板。所以很多安全团队会直接要求禁止迁移,重新录入才能过审。
3.4 编译、签名与镜像放置路径
TA代码改完,接下来就看编译和签名了。QSEE TA的编译产物不是一个简单的二进制,而是一个被签名过的镜像集合,常见格式是先产生一个ELF,再通过工具拆分成.mdt和.b00、b01文件序列。Android系统开机时由引导加载阶段把这一组镜像加载进安全世界。
签名这一步是整个流程里最容易踩坑的地方。开发阶段可以用测试key或permissive签名,但产线固件必须用对应平台的生产私钥来签,而且还受防回滚机制约束。签名不对,TA在加载阶段直接被拒,log里会明确报出安全校验失败。
镜像签名好之后,放置路径也有讲究。常见的位置是/vendor/firmware/,或者老平台放在/system/etc/firmware/。放错地方等于没放,因为引导加载器和TZ在固定路径找TA。
我在实测中经常用的一个检查手段是:固件刷好后,先在目标板shell里ls一下/vendor/firmware,确认指纹TA的.mdt和.bXX文件都在。然后在kernel log里搜TA加载相关的关键词,比如“fingerprint_ta”、“TZ”这些,看有没有加载成功的信息。这一步能过滤掉一大半的“镜像根本没进去”的低级问题。
3.5 REE侧配套改动清单
TA移植从来不是孤立事件,REE侧如果不动,TA跑起来也没用。我每次都会把REE侧的配套改动一起列出,避免联调时来回折腾。
内核侧:确认/dev/qseecom节点正常创建,qseecom驱动加载成功;确认指纹SPI设备树配置正确,中断GPIO可用;指纹驱动注册成功,并能为HAL提供设备节点。
HAL侧:确认libhardware里的fingerprint.default库编译版本与TA配套;HAL里的TA命令号、UUID、buffer格式要和TA侧完全一致;HAL初始化时是否能正常打开qseecom会话。
安全策略侧:检查SELinux的avc denied日志。新平台对qseecom节点访问管控很严,供应商指纹HAL进程如果没加sepolicy规则,也会发生“驱动正常但ioctl被拒”的现象。
这些改动看起来和TA本身没什么关系,但任何一个环节不对,最终现象都会表现为“TA不工作”,容易把排查方向带偏。
4. 联调阶段的坑与排查思路
4.1 高频错误码速查表
联调阶段遇到的错误,大部分都会以log错误码形式出现。我整理了一份高频错误码对照表,遇到直接查:
| 错误码/现象 | 含义 | 常见原因 |
|---|---|---|
| TEE_ERR_TA_NOT_FOUND | 找不到TA | 镜像没编译进去,或放置路径不对 |
| TEE_ERR_TA_BAD_FORMAT | TA格式错误 | 编译工具链与目标环境不匹配 |
| TEE_ERR_SECURITY | 安全校验失败 | 签名key不对,或防回滚冲突 |
| TEE_ERR_MEMORY | 内存不足 | TA的heap/stack配置太小 |
| ioctl返回-1 ENOTTY | 通信通道异常 | qseecom版本不匹配或驱动没注册 |
| avc denied | SELinux拒绝访问 | 指纹HAL进程缺sepolicy规则 |
| EINVAL | 参数错误 | 共享内存buffer格式或命令号不对 |
遇到错误不必慌,照着这个表格定位会快很多。但要注意,同一个错误码在不同平台上可能对应不同细节,还是要结合日志上下文判断。
4.2 分阶段定位:加载失败、通信失败与功能异常
我自己排查TA问题时,习惯把故障分成三个阶段来定位,每个阶段的排查手段完全不同。
第一个阶段是加载阶段。如果TA加载失败,先确认镜像是否编译进固件、路径是否放对、签名是否有效。这段时间主要看kernel log和TZ侧log。我试过一个现象:镜像文件都在,签名也设置了,但log里就是报安全校验失败,最后发现是防回滚版本号对不上,刷了旧版本TZ后再刷新TA就被拒。这属于平台机制问题,不是TA代码的问题。
第二个阶段是通信阶段。TA加载成功,但REE侧调用时没反应,或者返回乱码。优先检查qseecom内核驱动版本、HAL的UUID和命令号,以及共享内存buffer的长度定义。我遇到过HAL里定义的最大模板长度是2048字节,TA侧定义的是4096,结果数据一长就溢出,整个TA崩溃。两边结构体逐一核对,能省很多时间。
第三个阶段是功能阶段。TA能加载,通信也没问题,但指纹图像采集正常、模板录不上、匹配率低。这时候就要怀疑硬件链路了,比如SPI时序不对、中断抖动过大、电源纹波影响成像质量。这种情况下,与其埋头看TA代码,不如用示波器看一遍SPI波形和中断信号,往往能更快找到问题。
4.3 排查过程中必须留意的细节
有几个细节在联调阶段特别重要。第一个是启动顺序。指纹TA依赖的SFS等系统TA如果没先加载,指纹TA可能在运行到某一命令时才报错,排查时会误以为是自身代码问题。建议在启动log里确认安全世界的加载顺序。
第二个是设备状态。调试过程中如果改了安全世界分区,比如写了新的TZ或TA镜像,一定要确认设备真的进入了新版本。有些平台刷写后不会自动生效,需要完整重启或重新在fastboot模式下刷入。
第三个是双分区或多分区固件。新版Android系统的固件可能有AB分区,刷写一侧分区后另一侧还是旧版本,导致现象时好时坏。我在高通平台遇到过类似的“玄学问题”,最后发现是OTA槽位没切换,日志里全是旧版本的。
第四个也顺带提一下:如果调试中把TZ分区刷坏进不了系统,高通平台一般会进入9008紧急下载模式,系统里会看到qualcomm hs-usb qdloader 9008设备。这时候可以通过EDL工具配合原始分区备份救回来。这里要提醒一下,刷TZ分区前一定要先备份原始固件,否则救砖会很难受。
4.4 后续还可以这样扩展
移植完TA、功能稳定之后,并不代表整个安全方案就结束了。如果项目要进一步过安全认证,通常还要补几个东西:对TA内存进行安全审计、增加防重放和防回滚策略、确认模板存储的密钥管理和销毁流程。另外,新平台如果支持动态TA加载,可以把指纹TA改成按需加载,开机时少占内存,也能减少攻击面。
成熟一点的团队还会在TA外面包一层“命令白名单”或“频率限制”,防止恶意应用疯狂调用指纹命令。这些都属于增强项,不在基础移植范围内,但我觉得真正做过商用量产的人,最好在这一步就把设计考虑进去,免得后面再回头改TA又要重新走一遍签名和测试流程。
做指纹方案移植这几年,我最大的体会是:TA移植真正难的其实不是写代码,而是对整个高通安全世界体系的熟悉程度。你只有把加载机制、签名机制、通信机制都摸透了,才能在诡异的现象面前快速定位。上面这些经验都是我在实际项目里一砖一瓦垒出来的,希望对正在做同类方案的朋友有帮助。至少以后再遇到TA加载失败,你可以少走几段弯路。