Files
story/docs/firmware-rollout.md
Codex Local 0c2151ace9
ci / verify (push) Canceled after 0s
chore: checkpoint 第一版
2026-09-06 17:04:11 +08:00

164 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 固件制品、签名、灰度与回滚开发说明
## 1. 目标与边界
固件发布链路用于向已绑定家庭的兼容数据采集终端下发经过校验的固件制品。平台不保存固件私钥,也不在浏览器或服务端生成生产签名;生产私钥应保存在离线签名机、HSM或云密钥管理服务中。平台只登记受信任的Ed25519公钥、校验签名和摘要,并通过短期私有对象地址向被分配的设备提供下载。
## 2. 数据对象
| 对象 | 关键内容 | 状态 |
|---|---|---|
| `firmware_signing_key` | 租户、密钥编号、Ed25519公钥、创建人 | `ACTIVE`、`REVOKED` |
| `firmware_release` | 家庭、设备型号、硬件版本、版本号、对象引用、大小、SHA-256、签名、密钥编号及安全基线 | `UPLOADING`、`READY`、`PAUSED`、`RETIRED` |
| `firmware_rollout` | 固件版本、批次名称、灰度比例、创建人、开始/完成时间 | `DRAFT`、`RUNNING`、`PAUSED`、`COMPLETED`、`ROLLED_BACK` |
| `firmware_assignment` | 批次、设备、可靠命令、原版本、下载/安装进度、最近回执 | `ASSIGNED`、`DOWNLOADING`、`INSTALLING`、`CONFIRMED`、`FAILED`、`ROLLED_BACK` |
## 3. 签名清单
Ed25519签名输入使用确定性UTF-8文本,字段顺序和末尾换行不可改变:
```text
firmware-manifest-v1
model={设备型号}
hardware_revision={硬件版本}
version={固件版本}
sha256={小写SHA-256}
size={字节数}
```
研发辅助命令`go run ./cmd/firmware-sign <model> <hardware-revision> <version> <file>`会生成临时密钥并输出测试签名,只能用于合成数据和本地冒烟,不能代替生产签名设施。
## 4. 发布流程
1. 所有者或管理员登记受信任的Ed25519公钥;编辑者和查看者无权操作。
2. 管理端提交制品元数据,平台创建`UPLOADING`版本并返回15分钟私有上传地址。
3. 管理端直接上传制品到私有对象存储,业务API不转发大文件。
4. 管理端请求完成版本;平台流式核对实际大小和SHA-256,再使用租户内活动公钥验证清单签名。
5. 只有两项校验均通过,版本才进入`READY`;摘要、大小或签名不匹配均不能进入灰度流程。
6. 网络中断或原上传地址过期时,管理端必须重新选择与原记录大小和SHA-256完全一致的文件;平台仅为仍处于`UPLOADING`的版本重新签发15分钟上传地址,并在覆盖同一私有对象后重复执行完成校验。无法继续的版本可正式转为`RETIRED`并清理对象;退役后允许以相同业务版本重新创建纠正后的制品,但退役历史和审计仍保留。
7. 管理端选择已绑定且型号/硬件兼容的设备和1—100灰度比例。平台按设备编号确定性选样,并至少选择一台目标设备。
8. 每台目标设备生成一条幂等`firmware_update`可靠命令。设备使用HMAC身份拉取命令,再请求5分钟私有下载地址。
9. 设备在本地再次校验摘要和厂商签名,并通过命令回执上报`downloading`、`installing`阶段及0—100进度;云端依次映射为`DOWNLOADING`、`INSTALLING`。
10. 安装完成后设备上报`SUCCEEDED`或`FAILED`,分配记录进入`CONFIRMED`或`FAILED`。全部目标终态后,批次自动收敛为`COMPLETED`或`PAUSED`。
## 5. 暂停与回滚
暂停仅允许作用于`RUNNING`批次。平台将批次改为`PAUSED`,取消尚未投递的`QUEUED`命令,并立即禁止该批次继续取得固件下载地址。已经下载或正在安装的设备不能依赖云端强制中断,设备固件必须实现本地安全停止点和启动失败自动回退。
回滚不是修改原制品,而是选择另一个已经完成摘要及签名验证的`READY`版本,针对原批次全部目标设备创建新的100%可靠批次。这样可以保留完整版本、命令、设备结果和审计链。
## 6. 接口
| 方法与路径 | 用途 |
|---|---|
| `POST /api/v1/households/{id}/firmware-signing-keys` | 登记受信任公钥 |
| `GET /api/v1/households/{id}/firmware-signing-keys` | 查询租户可信公钥台账、指纹、状态及固件引用数量 |
| `POST /api/v1/households/{id}/firmware-signing-keys/revoke` | 在引用保护通过后吊销可信公钥 |
| `POST /api/v1/households/{id}/firmware-releases` | 创建版本和短期上传地址 |
| `GET /api/v1/households/{id}/firmware-releases` | 查询固件版本、校验状态及元数据 |
| `POST /api/v1/firmware-releases/{id}/finalize` | 校验大小、摘要和签名 |
| `GET /api/v1/firmware-releases/{id}/impact` | 汇总关联批次、设备阶段、命令处置和仍需观察设备 |
| `POST /api/v1/firmware-releases/{id}/upload-session` | 为仍处于上传中的版本重新签发15分钟私有上传地址 |
| `POST /api/v1/firmware-releases/{id}/pause` | 暂停已验证版本分发,并暂停关联运行批次、取消未投递命令 |
| `POST /api/v1/firmware-releases/{id}/resume` | 在签名密钥仍活动时恢复版本分发,不自动恢复旧批次 |
| `POST /api/v1/firmware-releases/{id}/retire` | 退役版本;未完成对象尝试清理,已验证对象和历史保留 |
| `POST /api/v1/households/{id}/firmware-rollouts` | 创建灰度批次 |
| `GET /api/v1/households/{id}/firmware-rollouts` | 查询家庭内最近灰度批次及结果汇总 |
| `GET /api/v1/firmware-rollouts/{id}` | 查询批次、逐设备阶段、进度和最近回执 |
| `POST /api/v1/firmware-rollouts/{id}/pause` | 暂停批次并取消未投递命令 |
| `POST /api/v1/firmware-rollouts/{id}/rollback` | 使用已验证版本建立回滚批次 |
| `GET /device/v2/firmware-releases/{id}/download` | 被分配设备取得短期私有下载地址 |
完整字段、错误结构和响应模型以`openapi/openapi.yaml`为准。
设备登记记录中的`minimum_firmware_version`是新通话准入策略,与固件制品声明的`minimum_security_version`用途不同。前者比较设备当前固件并阻止低版本创建通话,后者描述该制品运行所需的安全基线。调整准入策略属于设备注册/运维配置职责;发布一个新制品不会自动抬高所有设备的通话下限,避免灰度期间未升级设备被意外整体阻断。
V37将固件业务版本唯一性调整为仅约束未退役记录,并把空主板版本统一视为同一值。这样同一租户内相同型号、主板和版本不能同时存在两个活动清单;只有原`UPLOADING`版本正式退役后,才允许使用相同业务版本创建纠正后的制品。退役记录不会物理删除,历史审计仍可追溯。完成校验和退役都使用带原状态条件的更新并检查实际影响行数,二者并发时只有一个状态转换能够成功。
可信公钥按租户形成独立台账。查询接口只返回SHA-256指纹、状态、登记/吊销时间以及`UPLOADING`版本、`READY`版本和`RUNNING`批次引用数量,不返回原始公钥。密钥编号一经登记即绑定同一公钥:相同编号和相同公钥可幂等重放,不同公钥不得覆盖,已吊销编号不得重新激活;轮换必须先用新编号登记新公钥,再让新版本引用新编号。存在上传中版本或运行中灰度批次时禁止吊销,避免中断完成校验或在途升级;仅有已就绪历史版本时允许吊销,但吊销后这些版本不能再启动新灰度,且新版本也不能引用该密钥。
已验证版本支持`READY → PAUSED → READY`和`READY/PAUSED → RETIRED`生命周期。暂停版本在同一事务把关联`RUNNING`批次转为`PAUSED`,取消尚未投递的`QUEUED`设备命令;新灰度创建和新下载地址都会因版本不再是`READY`而被拒绝。已经签发的短期下载地址无法远程收回,最长仍可能在原5分钟有效期内使用;已经下载或进入安装的设备继续依靠设备端安全停止点和失败回退,页面需继续观察其状态。恢复版本前重新核验签名密钥仍为`ACTIVE`,且只恢复版本本身,不自动恢复旧批次或重新创建已取消命令。
退役未完成版本仍尝试删除可能已上传的对象,并允许重复调用以修复对象存储瞬时清理失败。退役已验证版本时保留不可变制品对象、清单、历史批次和审计,防止破坏取证与追溯;若仍有关联`RUNNING`批次则返回`FIRMWARE_RELEASE_IN_USE`,要求先执行版本暂停。退役后版本永久不能创建新灰度或签发新下载地址。
版本影响查询使用服务端权威数据实时汇总,不把页面缓存当作处置结果。批次统计区分运行、暂停、完成和已回滚;设备统计区分已分配、下载中、安装中、已确认、失败和已回滚;命令统计单独显示排队、送达、确认、成功、失败、过期以及暂停时取消的未投递命令。仍需观察设备包括`DOWNLOADING`、`INSTALLING`,以及分配仍为`ASSIGNED`但命令已送达、确认或出现结果待对账的设备。接口返回完整汇总和最多100条观察清单,超过100条时设置截断标志,避免页面误把未展示设备理解为已经收敛。
## 7. 验证
日常开发优先执行低强度纵向冒烟:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke-sim-firmware.ps1
```
该脚本只创建一个受信公钥、一个签名制品、一个单设备批次和一条升级命令。模拟终端从固定设备接口取得私有地址,限制下载大小,核对SHA-256,并使用`SIM_FIRMWARE_TRUSTED_KEYS`内预置而非命令携带的Ed25519公钥验证`firmware-manifest-v1`签名;校验完成后上报`ACKNOWLEDGED/DOWNLOADING/100`。脚本随后模拟进程中断,重启后重新拉取保持`ACKNOWLEDGED`的同一命令,重复安全校验并上报安装及`SUCCEEDED/CONFIRMED/100`,最后检查版本影响中的已确认、成功命令、已完成批次和零观察设备。夹具数据库记录与对象存储制品在结束时精确清理。
需要覆盖发布管理全部异常门禁时再运行完整回归:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke-firmware-rollout.ps1
```
脚本验证角色拒绝、合法签名、非法签名拒绝、浏览器跨域直传预检、版本/批次列表、灰度暂停、未投递命令取消、暂停后下载拒绝、已验证版本回滚、设备下载SHA-256、25%下载、80%安装和100%确认状态收敛,并在结束后清理制品及数据库合成数据。
## 8. Web管理页
具有`firmware.manage`能力的家庭成员可在“固件发布”页完成公钥登记、制品选择、浏览器SHA-256计算、私有对象直传、服务端签名校验、灰度目标选择、批次暂停和回滚。生产私钥始终不进入页面或平台:页面只接收离线签名结果和公钥。
登记公钥先在浏览器验证输入是标准Base64编码的32字节Ed25519公钥,并计算SHA-256指纹;页面内受控操作区冻结家庭、密钥编号、长度和完整指纹,要求操作者通过受控渠道核对后再提交。页面会对照可信公钥台账:相同编号和相同指纹只作为幂等登记,不同指纹或已吊销编号直接阻断并提示使用新的轮换编号;服务端仍重新解码公钥、复核不可变编号规则、校验固件管理能力并写入审计。私钥没有输入字段,也不会进入确认区、API请求或平台数据库。
可信公钥台账在页面显示完整指纹、状态、登记/吊销时间和三类引用数量。只有活动密钥可以进入吊销确认区;存在上传中版本或运行中批次时按钮禁用并展示阻断原因。允许吊销时,确认区固定密钥编号、完整指纹和引用统计并要求确认影响。已吊销密钥仍保留在台账中用于历史追溯,但不能登记替换、创建新版本或启动新灰度。
制品上传先由浏览器计算文件SHA-256,并验证离线签名是标准Base64编码的64字节Ed25519签名。页面允许填写最低安全基线和最长2000字符的发布说明;受控操作区冻结家庭、型号、主板、版本、文件名、实际大小、完整摘要、签名密钥编号以及逐字段`firmware-manifest-v1`清单。只有操作者确认离线签名对应这份清单后,页面才创建15分钟私有上传地址、把原始文件直接PUT到对象存储并调用完成校验;任一步失败都不会把版本显示为`READY`,只有服务端流式核对大小、摘要和活动公钥签名全部通过才进入灰度候选。
版本停留在“等待校验”时,版本卡片提供“恢复上传”和“退役未完成版本”两个入口。恢复表单在浏览器重新计算所选本地文件摘要,文件大小或完整SHA-256任一不一致即禁止继续;匹配后再进入受控确认区,确认后依次重新签发地址、PUT同一对象和重复完成校验。退役确认区固定家庭、版本编号、清单摘要和签名密钥,明确已就绪版本不会被清理、先前签发地址最多仍可在原15分钟窗口内写入但不能恢复退役状态。退役接口可安全重复,以便对象存储瞬时失败后再次请求清理。
已验证版本卡片提供“暂停分发”“恢复分发”和“退役版本”。暂停确认区显示版本、完整摘要、签名密钥和运行中批次数,明确暂停会阻断新灰度/新下载、暂停关联批次并取消未投递命令;退役按钮在仍有运行批次时禁用并提示先暂停。已暂停版本只有在签名密钥仍活动时允许恢复,确认区明确旧批次不会自动重启。已验证版本退役确认区明确制品与历史保留、短期地址有效窗口和永久停止分发边界。
每个版本卡片均提供“查看影响”。影响面板显示版本权威状态、计算时间、关联批次、各设备阶段、排队及已取消命令数量,并逐台列出安装中、下载中或回执待收敛设备的批次、阶段、进度、命令状态和最后上报时间。暂停、恢复或退役成功后,页面同时刷新版本、批次、公钥引用和当前影响面板,使操作者能够核对处置结果;查询本身只读且不写审计或业务状态。
启动灰度批次使用页面内受控操作表单。版本选择只列出`READY`制品;候选设备必须已注册到当前家庭,且设备型号、硬件版本与所选制品一致。打开确认区时冻结家庭、批次名称、制品编号与摘要、灰度比例以及候选设备编号和当前版本,修改版本会立即清空旧设备选择。操作者确认兼容性和多设备版本变更影响后才提交;服务端再次核验版本、设备归属和兼容性,再按设备编号确定性分桶,至少命中一台设备,并为实际目标创建24小时有效的可靠升级命令。因而页面显示的候选数不被当成最终命中数,也不能绕过服务端状态校验。
版本列表显示设备型号、硬件版本、固件版本、大小、签名密钥和校验状态;批次详情显示每台设备的`ASSIGNED → DOWNLOADING → INSTALLING → CONFIRMED`进度或失败结果。该页已经在隔离的实际Chrome内完成1440×1000与1024×900渲染检查,未发现横向溢出、控制台错误或失败请求;真实多浏览器兼容性仍属于类生产验收范围。
批次暂停和版本回滚使用页面内受控操作表单。暂停表单固定当前批次编号、状态、完成比例、设备总数及各状态数量;回滚表单还固定用户选择的`READY`目标版本,避免确认过程中目标漂移。两项操作都要求显式确认对尚未执行设备、在途设备和后续回滚指令的影响;取消,或切换页签、家庭,都会清空草稿且不发送请求。服务端继续校验管理角色、租户、批次状态、目标版本可用性和并发版本。
本轮按低强度功能验收策略复用既有服务端签名、上传、下载和状态收敛证据,只执行前端生产构建、Web镜像重建以及真实OIDC Chrome无提交冒烟。冒烟分别验证无效公钥/签名阻断、有效输入启用、指纹和清单目标冻结、影响确认及取消关闭;固件公钥、版本和审计数量保持不变,登记/制品创建/完成校验/对象PUT请求均为0,页面无横向溢出、控制台错误或失败请求。灰度批次创建的同轮轻量冒烟继续验证兼容设备选择、确认门禁和零业务写入;本轮不重复执行完整设备下载、安装、暂停和回滚回归。
上传恢复批次按相同低强度策略执行V37升级、真实PostgreSQL服务/HTTP定向用例、生产构建和一次真实OIDC Chrome无提交冒烟。定向用例覆盖续传地址重签、非管理员拒绝、`READY`禁止续传/退役、退役幂等、退役后禁止完成校验、同业务版本纠正重建及HTTP禁止缓存;浏览器覆盖错误文件阻断、匹配文件启用、恢复/退役目标冻结和取消零写入。临时版本、公钥和审计已清理,页面无溢出、控制台错误或失败请求;完整设备升级回归未重复执行。
可信公钥台账批次继续采用低强度功能验收:执行真实PostgreSQL服务层和HTTP定向用例、前端生产构建、容器镜像重建以及一次真实OIDC Chrome无提交冒烟。定向用例覆盖同密钥幂等登记、同编号替换拒绝、引用统计、上传中/运行中引用阻断吊销、吊销幂等、吊销后禁止创建版本和启动灰度;浏览器覆盖完整指纹与引用展示、阻断原因、轮换编号提示、吊销目标冻结和取消零写入。未重复执行完整设备下载、安装和回滚回归,临时密钥、版本、批次及审计夹具已清理。
版本生命周期批次按同一低强度策略执行真实PostgreSQL服务层和HTTP定向用例、前端生产构建、容器重建以及一次真实OIDC Chrome无提交冒烟。定向用例覆盖暂停/恢复幂等、暂停版本阻断新灰度、运行批次与排队命令同步收敛、运行引用阻断退役、吊销密钥阻断恢复、已验证退役保留对象以及稳定HTTP冲突码;浏览器只核对按钮门禁、目标冻结、影响确认和取消零写入,不重复完整设备下载、安装和回滚回归。
影响预览批次继续采用低强度验证:真实PostgreSQL服务层和HTTP定向用例构造一条排队命令和一台安装中设备,分别核对暂停前后的批次、设备、取消命令和观察清单;前端执行生产构建、容器重建及一次真实OIDC Chrome只读冒烟,核对汇总口径、观察原因、无写请求和页面布局。完整设备升级与回滚回归不重复执行。
模拟终端执行器批次继续遵循低强度策略:定向Go用例覆盖正常下载、摘要和Ed25519签名校验、下载/摘要/签名/安装失败、两处中断及清单边界;真实PostgreSQL定向用例只核对`ACKNOWLEDGED`命令重拉不发生状态回退。随后仅执行一次“校验后中断—进程重启—重复安全校验—安装确认”的真实服务纵向冒烟,验证两次投递、最终批次与影响汇总,并确认临时密钥、版本、批次、命令、审计和对象夹具清理完成;不重复Web、暂停和回滚全量回归。
A/B启动确认批次在OpenAPI 0.62.0下扩展可选回执阶段,不增加旧设备必填字段或主状态枚举。启用设备先将制品写入非活动分区并持久化启动尝试,再依次上报`staging`、`reboot_pending`和`boot_verifying`;健康通过才提交成功,健康失败或超时则恢复旧分区并提交`FAILED/rolled_back`及回退版本。云端将三类中间阶段保持为`INSTALLING`,同时保留完整`last_result`供页面展示。低强度验证只执行定向Go用例、前端类型/生产构建和一次B分区启动失败自动回A分区的纵向冒烟,不把模拟结果作为真实Bootloader验收。详细协议见[设备A/B分区固件升级与启动确认协议](device-firmware-ab-protocol.md)。
OpenAPI 0.63.0和V38进一步为每台固件分配保存去重的阶段事件。直接命令ACK与批量遥测补交在更新命令/分配的同一事务内写入事件,重复结果摘要通过SHA-256唯一键保持幂等;批次详情按设备返回完整时间线,页面显示云端接收时间、阶段、命令状态和稳定结果码。`cmd/firmware-conformance`可把该时间线与设备侧Bootloader/分区证据进行离线一致性校验,详细格式和15项检查见[固件真机联调一致性证据采集与校验说明](firmware-conformance-evidence.md)。
## 9. 维护端租户级固件管理
V46和OpenAPI 0.74.0在保留家庭端固件历史及设备下载协议的前提下,引入维护端租户级固件制品与OTA批次。维护端创建的`firmware_release.household_id`和`firmware_rollout.household_id`为空,`created_source=OPS`;家庭端已有记录继续使用原家庭范围,二者共享租户、型号、硬件版本和业务版本唯一性,避免同一租户发布相互冲突的活动制品。
维护中心“固件管理”页面支持选择客户组织、登记或吊销可信公钥、浏览器计算SHA-256后直传私有对象、恢复中断上传、完成签名校验、暂停、恢复、退役和人工下载。人工下载与设备下载都只签发短期私有地址;人工下载额外记录`ops.firmware.release.download`审计事件,设备下载继续要求设备身份、目标批次、版本状态和分配状态全部通过。
维护端接口使用`X-Tenant-Id`固定客户组织边界:
| 方法与路径 | 用途 |
|---|---|
| `GET/POST /api/v1/ops/firmware-signing-keys` | 查询或登记租户可信公钥 |
| `POST /api/v1/ops/firmware-signing-keys/revoke` | 在引用保护通过后吊销公钥 |
| `GET/POST /api/v1/ops/firmware-releases` | 查询租户固件台账或创建租户级上传记录 |
| `POST /api/v1/ops/firmware-releases/{id}/upload-session` | 恢复未完成的私有上传 |
| `POST /api/v1/ops/firmware-releases/{id}/finalize` | 校验对象和签名并发布版本 |
| `POST /api/v1/ops/firmware-releases/{id}/pause` | 暂停版本分发和关联在途批次 |
| `POST /api/v1/ops/firmware-releases/{id}/resume` | 恢复版本分发,不自动恢复旧批次 |
| `POST /api/v1/ops/firmware-releases/{id}/retire` | 永久退役版本 |
| `GET /api/v1/ops/firmware-releases/{id}/download` | 获取带摘要、签名和过期时间的短期人工下载地址 |
| `GET/POST /api/v1/ops/firmware-rollouts` | 查询OTA批次或按兼容设备和灰度比例创建升级批次 |
| `GET /api/v1/ops/firmware-rollouts/{id}` | 查询逐设备下载、安装和确认进度 |
| `POST /api/v1/ops/firmware-rollouts/{id}/pause` | 暂停批次并取消尚未投递的升级命令 |