OTA2 校验流程

SecureLink OTA2 · 从打包到设备落地,每一步比对了什么值 · 2026-09-15

一个 OTA2 固件包从打包到写进设备,一共经过五道校验:构建期一道、App 侧两道、设备侧两道。同一组值被反复检查,不是冗余——每道的信任前提不同。

总览

竖线是参与方,自环箭头是本地校验。

打包脚本 固件服务器 App 设备 一 · 构建期 1 校验 partition.csv 摘要与信任策略登记值一致 2 校验私钥导出的公钥与策略登记的公钥一致 3 校验请求的特权 flag 在策略授权范围内 4 P-256 签名:域分隔符‖产品摘要‖分区摘要‖包头‖载荷 5 上传 ota2.bin 与 manifest.json 二 · 取包 6 查询 latest 7 返回 manifest 与固件包 三 · App 侧包校验(离线,不连设备) 8 解码包头,逐字段结构断言 9 比对 payloadHash 与载荷实际 sha256 10 用 productIdHash 查本地产品目录 11 比对 keyId、签名方案、特权 flag 与信任策略 12 比对策略摘要前 4 字节与包头提示 13 P-256 验签(32 字节摘要取自本地策略表) 四 · App 预检(需要设备信息) 14 读取设备信息与能力 15 产品哈希、硬件版本、当前版本、能力集 16 七项预检,任一不过立即中止,不发起 OFFER 五 · 设备 OFFER 预检 17 OFFER_V4(未签名) 18 十项预检 19 接受,或返回错误码与恢复动作 六 · 传输 20 DATA 帧(循环,逐帧) 21 校验 CRC、序号、长度 22 收满包头长度即比对包头与 offer 七 · 设备终检(收全之后) 23 整包 sha256 == offer.packageHash 24 载荷哈希匹配 25 策略未变,flag 仍在授权内 26 解码并 P-256 验签(摘要取自设备自己的策略) 27 用已签名包头复核 offer 的八个字段 28 平台级镜像校验 29 进入 READY_TO_APPLY 八 · 应用 30 APPLY_V4

三个关键认识

完整的 32 字节摘要不在包里。包头只带 productIdHash 和 partitionHash 各 4 字节。验签时用的完整 productDigest 与 partitionDigest,由验证方从自己的信任策略表取。所以一个包只在「验证方登记的产品与分区布局」下签名才成立——换个产品或换套 flash 布局,同一把密钥签出来的包也验不过。

OFFER 是未签名的。设备在 OFFER 阶段做的十项预检,依据全是 App 声称的值。所以验签通过之后,设备必须拿已签名的包头把 offer 里声称过的八个字段重新核一遍(headerMatchesOffer),否则预检等于白做。

App 的校验不是安全边界。App 侧那两道的作用是尽早失败、少传一次几百 KB,以及给用户可读的失败原因。真正的安全边界在设备端——设备不信任 App 的任何结论。

一 · 构建期

打包脚本。失败即构建中止,不产出包。

校验比对什么失败信息
分区摘要存在product.partitionDigestHex 非空has no frozen partition digest
分区身份sha256(partition.csv) 同时等于产品登记值和 policy.partitionDigestpartition source does not match trust policy
特权 flagflags & ~policy.allowedPrivilegedFlags == 0does not authorize requested privileged flags
签名密钥私钥导出的公钥 == policy.publicKeySec1signing key does not match policy

写进包头的值

字段来源
productIdHash 4Bsha256(productId) 前 4 字节
partitionHash 4Bpolicy.partitionDigest 前 4 字节
keyId 1Bpolicy.keyId
signatureScheme 1Bpolicy.signatureScheme
payloadHash 32B / payloadSize载荷本身
hwRevMask 2B由目标硬件版本的永久位号算出
targetVersion / minFromVersion发版参数
flags 4B命令行请求的特权位,须在策略授权内

签名覆盖什么

签名对象是这串字节的 SHA-256,再用 P-256 私钥签:

域分隔符 ‖ productDigest[32] ‖ partitionDigest[32] ‖ 包头 ‖ 载荷

两个 32 字节摘要参与签名但不随包传输。这就是上面说的绑定机制。

二 · App 侧包校验

不需要连接设备,纯离线。任一步失败即抛错,包不进入后续流程。

顺序校验比对什么
1最小长度字节数 ≥ 包头 + 签名 + 1
2包头结构flags 无保留位;payloadSize 在 1..上限;payloadHash 32B;productIdHash 4B;partitionHash 4B;hwRevMask 在 1..0xFFFF;signatureScheme 与契约一致;keyId 在 1..255
3载荷完整性sha256(载荷) == 包头的 payloadHash
4产品已登记用 productIdHash 在本地产品目录查得到;查不到即拒
5取信任策略正式模式只接受 production 策略;产品没有策略即拒
6策略匹配policy.keyId == 包头 keyId,且签名方案一致
7特权 flagflags & ~policy.allowedPrivilegedFlags == 0
8摘要提示一致policy.productDigest 前 4 字节 == 包头 productIdHash;policy.partitionDigest 前 4 字节 == 包头 partitionHash
9验签P-256 验签,公钥用 policy.publicKeySec1,两个 32 字节摘要取自策略
10身份指纹算整包 packageHash 与 identity fingerprint,供后续流程引用

三 · App 预检

需要设备信息。七项检查放在一张清单里,任一项不过就抛错,不会发起 OFFER。

检查项比对什么
artifact-signature上一步已验,此处恒真,只为在报告里留一行
trust-policy包头 keyId == policy.keyId
product设备自报的产品哈希 == 包头 productIdHash,且该产品在发版支持列表内
partition产品目录的 partitionHashHex == 包头 partitionHash
hardware设置了 SKIP_HARDWARE_CHECK,或包头 hwRevMask 含设备硬件版本对应的位
minimum-versionminFromVersion 为全零,或设备当前版本 ≥ 它
target-version设置了 ALLOW_DOWNGRADE,或 targetVersion > 设备当前版本
capabilities设备能力集满足要求

四 · 设备 OFFER 预检

设备收到未签名的 OFFER_V4。十项检查按顺序走,第一个不过就返回错误码与恢复动作。

检查比对什么错误码恢复
包大小packageSize ≤ 设备可暂存上限0x12 PAYLOAD_TOO_LARGESTOP
产品offer.productIdHash == 设备策略 productDigest 前 4 字节0x13 PRODUCT_MISMATCHSTOP
分区offer.partitionHash == 设备策略 partitionDigest 前 4 字节0x14 PARTITION_MISMATCHSTOP
签名方案与设备策略一致0x19 SIGNATURE_SCHEME_UNSUPPORTEDSTOP
密钥编号offer.keyId == policy.keyId0x1A KEY_ID_UNKNOWNSTOP
特权 flag全部在 allowedPrivilegedFlags 内0x1C PRIVILEGED_FLAG_NOT_AUTHORIZEDSTOP
最低来源版本minFromVersion 为零,或当前版本 ≥ 它0x15 MIN_FROM_NOT_METSTOP
目标版本允许降级,或 targetVersion > 当前版本0x10 VERSION_TOO_OLDSTOP
硬件版本跳过硬件检查,或 hwRevMask 含本机位号0x11 HARDWARE_MISMATCHSTOP
数据帧协商能协商出可用的帧长0x1B DATA_FRAME_SIZE_UNSUPPORTEDSTOP

这十项全部返回 STOP——它们描述的是「这个包和这台设备根本不匹配」,重试没有意义。

五 · 传输期

逐帧写入。这一层的错误大多可恢复,恢复动作是查询后续传而不是终止。

检查错误码恢复
帧 CRC0x21 CRC_FAILQUERY_AND_RESUME
序号断档0x26 SEQ_GAPQUERY_AND_RESUME
序号重复0x25 SEQ_DUPLICATEQUERY_AND_RESUME
帧格式 / 长度0x23 DATA_FRAME_MALFORMED · 0x24 DATA_LENGTH_INVALIDQUERY_AND_RESUME
流控违规0x29 FLOW_CONTROL_VIOLATIONQUERY_AND_RESUME
收满包头长度时,包头与 offer 不符0x17 PACKAGE_FORMAT_INVALIDSTOP
传输期间信任策略变更0x1D TRUST_POLICY_CHANGEDSTOP
写 flash 失败0x27 FLASH_WRITE_FAILEDSTOP

注意第 6 行:设备一旦收够包头那么多字节,不等包收完就立刻比对包头与 offer。这样一个声称与实物不符的传输在开头就被砍掉,不用白传几百 KB。

六 · 设备终检

包收全之后。这是唯一真正决定能不能刷的地方,顺序有讲究。

顺序校验比对什么失败
1整包哈希sha256(收到的全部字节) == offer.packageHash0x30 HASH_MISMATCH
2载荷哈希载荷部分的哈希与包头声明一致0x30 HASH_MISMATCH
3策略未变当前信任策略仍与传输开始时一致0x1D TRUST_POLICY_CHANGED
4特权 flag仍在 allowedPrivilegedFlags 内0x1C PRIVILEGED_FLAG_NOT_AUTHORIZED
5结构解码能解出合法的 OTA2 结构0x31 SIGNATURE_INVALID
6验签P-256 验签,公钥与两个 32 字节摘要全部取自设备自己的信任策略0x31 SIGNATURE_INVALID
7复核 offer用已签名的包头逐一比对 offer 声称的八个字段0x13 PRODUCT_MISMATCH
8版本与硬件复核用签名包头里的值重算 minFrom / target / hwRevMask0x15 · 0x10 · 0x11
9平台镜像校验芯片平台自己的镜像合法性检查0x17 PACKAGE_FORMAT_INVALID

第 7 步复核的八个字段

flags · hwRevMask · keyId · minFromVersion · partitionHash · productIdHash · signatureScheme · targetVersion

全部相等才继续。这一步存在的唯一理由是:OFFER 不受签名保护,而第 4 阶段的十项预检全部建立在 OFFER 之上。

为什么同一组值要查这么多遍

以 partitionHash 为例,它一共被比了四次:App 包校验时比策略摘要前缀、App 预检时比产品目录、设备 OFFER 预检时比设备策略、设备终检时比已签名的包头。

四次的信任前提各不相同。前两次信任 App 自己的本地数据,目的是尽早失败;第三次信任的是 App 送来的声称值,目的是在传输前砍掉明显不匹配的请求;只有第四次,比对的两端都在签名保护之下,才构成真正的安全判断。

值从哪来

值打包脚本App设备
productDigest 32B由 productId 算出本地信任策略表编译进固件的策略
partitionDigest 32B分区表文件的 sha256本地信任策略表编译进固件的策略
公钥由签名私钥导出并与策略核对本地信任策略表编译进固件的策略
keyId策略登记值,写进包头与策略比对与策略比对
hwRevMask由目标硬件版本位号算出与设备自报版本比对与本机位号比对
当前版本—从设备读自己持有

三方的完整摘要都不来自固件包,而来自各自持有的一份信任策略。三份必须一致,这个校验体系才成立;任何一份漂移,结果都是升级被拒而不是被放行。