构建可更新的设备
支持空中(OTA)更新的设备,要做的不只是下载一个文件。本指南带你走读一台小型参考设备,它完成了真正的联网产品必须做的事:遵循更新协议,拒绝该拒绝的内容,替换自己的可执行文件,并向平台证明实际运行的是哪个版本。
参考设备位于仓库的 examples/ota-device。它是一个用 Go 编写的程序,一个下午就能读完,只依据已发布的协议构建,即连接设备中的内容,所以一次成功的更新说明的是该协议本身,而不是平台的内部实现。它不导入平台的任何代码,若这一点被打破,会有测试失败。
它与仓库的其余部分一起构建和测试,但从不发布:没有发布的二进制文件,也没有兼容性承诺。用文件重命名来充当引导加载程序只是教学手段,不能直接用于产品。参见真实设备还必须补上什么。
你需要什么
- 一个运行着更新服务、并且能向设备发放下载链接的实例。参见第一次更新之前。
- 一台通过 MQTT 连接、拥有自己凭据的设备。只有 MQTT 设备能接收托管更新;参见哪些设备可以更新。
- 一个签名者,以及由它签名的制品。除非实例允许未验证的制品,否则没有人签名的制品不能被分配。参见签名更新。
- 设备上存放签名者公钥的 PEM 文件。它就是设备的信任集:一个或多个串联在一起的
PUBLIC KEY块。
启动器与固件
这个示例由两个程序组成,因为安装新镜像意味着重启后必须由另一个程序来启动。
| 程序 | 对应 | 职责 |
|---|---|---|
cmd/ota-boot | 引导加载程序 | 选择要运行的槽位。刚安装的槽位只有一次尝试机会:如果它没有确认自己,启动器会再次运行上一个已确认的槽位,并留下一条说明。它会终止挂起而未确认的新槽位,并且从不通过空中更新自己。 |
cmd/ota-device | 固件 | 以一台设备的身份通过 MQTT 连接实例,遵循更新协议,把校验过的镜像安装到非活动槽位,并以状态码 75(“重启”)退出,好让启动器把它拉起。 |
两个槽位 a 和 b 各存放一个镜像。状态目录以原子方式写入的文件记录哪个槽位是已确认、哪个是待定,以及待定槽位还剩几次尝试。更新总是写入当前没有运行的槽位,所以运行中的镜像在运行期间不会被改动。
设备报告的“正在运行的版本”,是编译进当前运行的二进制文件的那个版本,它报告的 digest 则是这个文件本身的 SHA-256。它从不读取配置,也从不从分配中复制,因此无法让设备声称它运行着实际并未运行的东西。
检查的顺序
分配到达时,设备按顺序做下面这些事。顺序才是重点:在镜像的完整性和真实性被检查之前,下载下来的任何内容都不会被运行。
- 策略检查,在下载任何内容之前。部件必须是这台设备的。未签名的分配会被拒绝,除非已告知设备接受。版本必须是有效的语义化版本,并且严格比正在运行的版本更新;相同的版本是重放,不是 semver 的版本会被拒绝,而不是去猜。
- 磁盘空间足够容纳尚未下载的部分。
- 下载,使用
Range请求,写入非活动槽位中的部分文件。起始位置不是所请求偏移量的响应会被拒绝。超出声明大小一个字节就是失败,部分文件会被删除。授权过期时会重新申请,次数有限。 - **摘要。**已持有字节的 SHA-256 等于
artifact.digest。此时没有运行任何东西。 - **签名。**针对 32 个原始摘要字节的分离签名,要能通过设备自己信任集中某个密钥的校验。消息里的
keyId只用来从该信任集中选取密钥;绝不从消息里取密钥。此时没有运行任何东西。 - **身份。**到这时才会运行镜像,且只运行一次,运行模式是打印它的产品和版本后退出,环境为空,工作目录是临时目录,并带有期限。它必须声明是这台设备的产品,以及分配中所指明的版本。
- **安装。**镜像放入非活动槽位,标记为待定,设备报告
REBOOTING并退出,由启动器把它拉起。
每个阶段都先写入磁盘,然后才执行并报告。所以链路断开永远不会改变设备的行为:它会继续安装,重启后报告它已到达的阶段,跳过报告已丢失的阶段,协议允许这样做。任何时刻重启,都会从已持有的字节继续。设备只有在自己是已确认槽位且没有待定项时才会安装。
回滚
重启后,新进程会在平台已得知当前运行内容之后确认自己,把槽位从待定变为已确认。如果有更新正在进行,只有当运行的是该更新的版本和文件时才会确认。随后它报告 RUNNING,带上编译进去的版本和自身文件的摘要。
如果新镜像没能成功,启动器会拉起旧镜像。旧镜像报告 FAILED,代码为 ROLLED_BACK,详情类似 booted 1.0.1, expected 1.0.2。它绝不会为没有生效的更新报告 RUNNING,所以平台上的这次尝试以失败结束,而不是以一个无法证实的成功结束。
拒绝降级和错误的部件
签名只覆盖摘要。它不覆盖版本、部件或产品,因此没有什么能阻止签名正确的旧镜像,或为另一个产品构建的镜像,成为一个完全有效的已签名镜像。任何有权发布制品的人,都可以把旧镜像以新版本重新声明,它的旧签名仍然能通过校验。参见签名不涵盖什么。
因此,设备是唯一能强制执行这些检查的地方,参考设备做了两次。下载前,它把分配中的部件和版本与自己的比较。下载后,它运行一次镜像,检查镜像声明的是这台设备的产品和分配所指明的版本,这样谎报自己身份的镜像就会被拒绝。除了明确的实验室选项 --allow-downgrade,没有其他办法关闭降级检查。
固定下载来源
下载链接由平台选定,并通过 broker 送达。设备只从自己被配置的来源下载:方案、主机和端口都要一致,不带用户信息,路径是 /updates/download/;任何重定向也必须留在该来源内。因此,即使有配置错误或已被攻破的组件能向设备的主题发布消息,也无法让设备从其他主机下载,或把它的授权发送过去。
链接的查询字符串中带有一项授权,它是持有者机密。设备从不记录链接及其查询参数,并会从传输错误中去除链接。没有任何开关可以关闭证书校验。除非给出 --allow-insecure-download,否则明文 http 来源会被拒绝;没有 TLS 的 broker URL 同样会被拒绝,除非给出 --allow-insecure-broker;这两者只用于没有 TLS 的精简安装实验环境。
运行它
在 examples/ota-device 目录下构建两个程序,并把第一个镜像放入槽位 a:
ROOT=$PWD/state
go build -ldflags "-X devicechain.io/examples/ota-device/internal/buildinfo.Version=1.0.0" -o bin/ota-device ./cmd/ota-device
go build -o bin/ota-boot ./cmd/ota-boot
mkdir -p "$ROOT/slots/a" && cp bin/ota-device "$ROOT/slots/a/"
在 Windows 上请构建 ota-device.exe;启动器要找的就是这个名字。
把签名者的公钥放入信任文件,然后在启动器下启动设备:
openssl pkey -in signer.pem -pubout -out trust.pem
export OTA_CREDENTIAL_SECRET='<设备凭据密钥>'
bin/ota-boot --root "$ROOT" -- \
--root "$ROOT" \
--broker ssl://mqtt.example.com:8883 --ca instance-ca.crt \
--instance devicechain --tenant acme --device pump-7 --credential-id <credentialId> \
--origin https://dc.example.com --trust trust.pem
--credential-id 是凭据 ID,而不是 MQTT 用户名:程序按连接设备所述,把 {tenant}:{credentialId} 作为用户名,把 {instance}:{tenant}:{device} 作为客户端 ID。--tls-server-name 用来按 broker 主机以外的名称校验 broker 证书,适用于证书未覆盖 localhost 的本地集群。
构建第二个版本更高的镜像,以部件 firmware 把它声明为制品,签名后分配给设备。你可以在设备的“更新”选项卡上看到这次尝试从 INITIATED 经 DOWNLOADING、VERIFIED、REBOOTING 走到 UPDATED。如果分配的镜像由设备没有的密钥签名,这次尝试会在下载之后以 FAILED 结束,代码为 UNKNOWN_SIGNER。如果分配的镜像版本并不更新,设备会在下载任何内容之前就拒绝它。
真实设备还必须补上什么
这个示例坦率地说明了它缺少什么:
- **掉电安全的闪存。**真实设备会写入非活动存储区,校验后翻转由硬件保证原子性的存储区选择位。这里用重命名加 fsync 来代替。
- **硬件信任根。**它锚定信任集和启动器。这里的启动器只是一个程序,不是 ROM。
- **回滚保护。**由硬件保存的防回滚计数器,这样就无法靠清除设备状态来强制降级。
- **在沙箱中首次执行。**身份检查只限制了工作目录、环境和时间,没有限制网络或文件系统。真实设备会在最小权限域中运行它,或者干脆校验清单而不运行镜像。
- **别人无法写入的暂存区。**镜像先计算哈希、校验、运行一次、再重命名;如果有人能在其间写入非活动槽位,这些检查就会被绕过。
- 凭据的安全存储。
- **得知签名者被吊销的途径。**平台会拒绝分配只由已吊销签名者签名的制品,但设备只依据它持有的信任集行事。
它也不支持增量更新或加密镜像,不处理配置、LwM2M 或 Sparkplug,只是作为普通 MQTT 设备连接。上面的检查顺序可以原样用到真实设备上。