☰
BongoCat 更新清单传输信封契约解析:ADR-0022 的字节级设计、边界约束与后续演化
2026/10/1 2:40:47 网站建设 项目流程
  • 桌面应用

【免费下载链接】BongoCat

🐱 BongoCat — A cross-platform interactive desktop pet that brings fun to your desktop!

项目地址:https://gitcode.com/gh_mirrors/bong/BongoCat
点击查看免费下载

本文以仓库 docs/adr/0022-update-manifest-transport-envelope.md 为主体,结合相邻 ADR 与bongocat-updatecrate 的源码与测试,完整讲解 BongoCat 更新系统中"签名 manifest 如何在 HTTP 传输层被无歧义地搬运"这一核心问题:为什么需要传输信封(transport envelope)、信封对 endpoint/body/header 施加了哪些精确约束、UpdateManifestEnvelope的职责边界如何划分,以及这套契约在 ADR-0025 / ADR-0029 / ADR-0034 中如何演化与退役。读者读完可获得一份可复用的"离线验签 + 网络传输解耦"契约设计清单。

一、导读:一份只负责"无损搬运"的传输契约

BongoCat 将自动/手动更新列为首发行为,但更新的信任判断(验签、版本、target、hash)必须建立在与网络层完全隔离的基础之上。ADR-0021 已经用 detached Ed25519 签名固定了"先验签、后解析"的离线验证边界,但彼时尚未定义:网络响应如何把 key ID、detached signature 和原始 body无损地交到验签器手里。

ADR-0022(Update Manifest Transport Envelope)回答的正是这个"最后一公里"问题:它定义了一个字节级精确的 HTTP 传输信封契约——endpoint 只能用 HTTPS GET 且不跟随 redirect,body 是未经任何处理的原始 manifest bytes,key ID 与 Ed25519 signature 通过两个固定命名的 header 携带,transport adapter 只做"有限 header + 精确 body →UpdateManifestEnvelope"的转换,绝不参与验签、状态写入、下载或安装。它的目标是把"传输库的类型"彻底挡在信任判断之外,让任何 HTTP client、设置服务或发布脚本都无法各自发明 JSON wrapper、base64 或 redirect 策略,从而杜绝待验签 bytes 的歧义。

需要特别说明的是:本文讨论的 ADR-0022 在当前仓库中属于历史契约(状态标注为"已被 ADR-0029 取代,2026-09-13"),其后的信任模型经历了 ADR-0025(ureq 传输)、ADR-0029(self_update + zipsign)再到 ADR-0034(detached minisign +cargo-packager-updater)的多次换实现。但 ADR-0022 确立的设计方法论——字节无歧义、边界清晰、失败关闭、错误码稳定——在今天crates/bongocat-update的实现中依然清晰可见。文章将同时呈现契约原文与演化脉络,避免读者把已退役的 header/body 协议误当作现行协议。

二、背景:离线验签之后,还有"字节歧义"问题

ADR-0021 的决策明确了更新信任边界:客户端必须先对收到的原始 manifest bytes 执行严格验签,成功后才反序列化;未知字段、非 v1 schema、超过 1 MiB 或无效 JSON 均拒绝;manifest 固定包含 channel、SemVer release、最低可升级版本、单调release_sequence、发布时间与 artifact 列表(详见 docs/adr/0021-signed-update-manifest.md)。

但这套离线验证边界有一个前提缺口:验签器拿到的 bytes 必须与发布时签名的 bytes 完全一致。ADR-0022 的背景部分直白地指出了风险:

让 HTTP client、设置服务或发布脚本各自选择 JSON wrapper、base64 或 redirect 策略会使待验签 bytes 产生歧义,也会把传输库类型带入信任判断。

这句话拆开看有两个独立问题:

  1. 字节歧义:同一个 manifest,A 实现用 JSON 字段包一层再 base64,B 实现直接透传原始响应,C 实现跟随 redirect 后拿到的是 CDN 重写过的内容——验签器无法确定"哪个字节序列才是被签名的那一份",签名必然时灵时不灵。
  2. 类型泄漏:如果传输层把 HTTP status text、TLS 细节、client 类型、URL 字符串直接暴露给上层,上层代码就会开始依赖这些传输细节做判断(比如"状态码 302 说明有新版本"),传输库一换,信任逻辑就崩。

ADR-0022 的解法是把网络响应收敛为一个标准化的信封:固定 endpoint 语义、固定 body 语义、固定 header 语义、固定转换边界,其余一切(验签、状态、调度、下载、安装)都在信封之外另行实现。

三、核心契约一:endpoint 的强制语义

ADR-0022 对 manifest endpoint 给出了四条不可协商的约束:

约束内容目的
协议只能经 HTTPSGET获取传输层加密是信任链的地基
重定向客户端不跟随 redirect避免"最终拿到哪份 bytes"由 CDN/中间层决定,保证待验签字节无歧义
状态码只接受 HTTP200非 200(重定向、错误页、代理拦截页)一律视为获取失败
来源endpoint 自身由不可变环境发布配置提供,不能来自用户 config、CLI 或运行时输入把"去哪里取更新"固化为构建期事实,杜绝用户配置污染信任根

第四点尤其关键:endpoint 不属于用户可配置项。这与 ADR-0021"verifier 由不可变构建环境构造"的设计一脉相承——如果用户 config 能改 endpoint,那么"这个 endpoint 是官方发布的"这一信任前提就不成立了。该原则在后续演化中也被继承:ADR-0025 规定UreqUpdateManifestSource只接受UpdateManifestEndpoint,endpoint 最多 2 KiB、必须是无 credentials、无 fragment 且具有 host 的 HTTPS URL,同样"绝不来自用户 config、CLI 或运行时输入";ADR-0034 落地为固定的releases/latest/download/latest.json共享 manifest 地址(crates/bongocat-update/src/runtime/mod.rs中的RELEASE_MANIFEST_NAME等编译期常量)。

四、核心契约二:body 必须是"未经处理的原始 bytes"

ADR-0022 规定:

响应 body 是不经 JSON 解析、字符转换、解压或重编码的 manifest 原始 bytes,最大为 1 MiB。

逐条拆解这条约束:

  • 不 JSON 解析:HTTP client 不得把响应按 JSON 读入再序列化输出。任何"读-改-写"都可能改变字节序列(键序、转义、空白),而 Ed25519 detached signature 覆盖的是精确的原始字节。
  • 不字符转换:不按 charset 解码再编码。UTF-8 之外或声明了其他 charset 的内容,一律按原始字节看待。
  • 不解压:即便服务器用 gzip 传输(Content-Encoding 或 transparent compression),解压后的字节不是网络上的原始字节。这一风险在 ADR-0025 中被再次点名:transparent compression 会改变 detached Ed25519 signature 必须覆盖的原始 bytes,因此 ADR-0025 在ureq 3.4.0上显式关闭 gzip、charset、cookie 与 proxy feature。
  • 不重编码:不做任何字节重排。
  • 1 MiB 上限:与 ADR-0021 的 manifest 大小上限一致,防止内存与带宽被异常响应耗尽;ADR-0025 进一步给出实现手法——Read::take(1 MiB + 1)有界读取,超过上限与空 body 均通过既有manifest_too_largeverifier code 失败关闭。

从实现角度,这条约束的工程含义是:transport 层应当把响应 body 视为不透明的字节流,原样搬运到信封里,任何语义解读都留给验签成功之后的反序列化阶段。这正是"传输与信任解耦"的直接体现。

五、核心契约三:两个 header 的精确语法

manifest 的签名信息不放在 body 里(body 必须是纯粹的原始 manifest bytes),而是通过两个固定命名的 HTTP response header 携带:

Header语法约束语义
bongocat-update-key-id最多 64 字节 ASCII,只允许字母、数字、-、_和.公钥标识,用于在编译期信任公钥集中定位对应公钥(ADR-0021 以 key ID + channel + sequence 有效窗绑定公钥)
bongocat-update-signature-ed25519恰好 128 个小写十六进制字符64-byte detached Ed25519 signature 的 hex 编码

对两个 header 的失败处理是失败关闭(fail closed):缺失、过长或语法无效的 header,一律以稳定的 update error code 失败关闭,绝不"宽松解析、猜一个默认值继续"。注意这里的失败关闭原则——"拿不到签名信息就拒绝继续"——从 ADR-0022 一路延续到当前实现:ADR-0034 规定RELEASE_SIGNING_KEY为None、空串或纯空白时,runtime 在发出任何请求之前返回update_signature_key_missing,系统菜单不会在没有有效公钥时显示"检查更新"入口(crates/bongocat-update/src/runtime/mod.rs的configured_signing_key门禁与 crates/bongocat-update/src/diagnostics.rs 的SignatureKeyMissing错误码)。

语法约束的设计动机值得展开:key ID 被限制为"字母数字 +-_."的 ASCII 子集,是为了让它可以安全地出现在日志、诊断导出或任何文本通道中而不引入注入风险;signature 被固定为恰好 128 个小写 hex(而非大小写混用或 base64),是为了让字节长度与字符集都可被严格校验——任何偏离都意味着传输被篡改或实现有 bug,理应失败而不是被宽容处理。

六、核心契约四:UpdateManifestEnvelope的职责边界

信封转换由transport adapter完成。ADR-0022 用一句话划定了它的全部职责:

transport adapter 只把有限 header 和精确 body 转为UpdateManifestEnvelope。

并且明确列出 adapter不做的事:

  • 不验证 manifest(验签、schema 校验属于 ADR-0021 的 verifier);
  • 不写入 sequence state(防降级状态的提交属于验证成功后的 session 职责);
  • 不下载 artifact;
  • 不调用 installer。

envelope 只能被交给 ADR-0021 的 verifier/session,由后者完成验签、严格 schema 验证与 sequence 推进。同时:

HTTP、TLS、代理、状态文本和 response 类型不离开 transport adapter。自动/手动调度、取消、下载与安装另行实现。

这条"信息不出边界"原则是整份 ADR 的灵魂:传输层可以把"连接失败""TLS 错误""代理不可达""非 200 状态"翻译成有限的、匿名的错误类别向上传递,但绝不把底层的 URL、status text、TLS 细节或 client 类型泄漏给上层——否则上层代码一旦依赖这些细节,换传输库就是一次信任链重构。这一原则在 ADR-0025 中具体化为"endpoint、status、transport 和 body-read 错误只公开固定匿名 transport code;底层 URL、HTTP status text、TLS 或 client 类型不离开 update crate"。

七、验证策略:单元测试锁契约,网络测试留后续

ADR-0022 的验证段落给出了两阶段的测试策略,这也是"契约先行"工程方法的范例:

阶段一(契约落地时),bongocat-update单元测试必须覆盖:

  1. envelope 的 body 上限(1 MiB 边界内/外);
  2. key ID / signature 编码拒绝(非法字符、长度错误、大小写错误);
  3. exact-byte 保留(envelope 内 body 与网络响应 body 逐字节一致);
  4. 通过 envelope 的验签路径(把信封交给 verifier 能成功完成验签)。

阶段二(网络 adapter 补上后),要求补充真实网络条件下的测试:

  • 真实 HTTPS;
  • redirect(确认不跟随);
  • 代理;
  • 非-200 响应;
  • 截断 body;
  • 超限 body。

这个"先锁字节语义、再测网络行为"的顺序是有意的:字节语义是契约的核心,可以在无网络环境下用单元测试钉死;网络行为依赖具体 adapter 实现,属于后续边界。从仓库现状看,crates/bongocat-update/tests/release_manifest_capability.rs正是这种"离线能力测试"思路的延续——它用 loopback HTTP 服务把生产端的签名器(cargo_packager::sign::sign_file)与应用实际运行的验签器放在一起跑,覆盖共享 manifest 的平台键查找、被篡改的载荷、未知密钥签名的载荷、空公钥与 macOS 整包替换,不依赖真实网络与真实 GitHub。

八、后续边界:这份 ADR 刻意不做的决定

ADR-0022 在结尾明确列出了一份"不决策清单",这些决定被推迟给后续 ADR:

  • 不选择 HTTP crate(具体 client 由 ADR-0025 定为ureq 3.4.0);
  • 不选择 Production endpoint(线上地址属于发布基础设施决策);
  • 不选择公钥注入方式(编译期公钥集由 ADR-0021 定义);
  • 不授权下载、包签名、installer 权限或回滚行为(分别属于传输之后、安装之后的独立 contract)。

这种"每份 ADR 只封一段边界"的做法,让整个更新系统的安全论证可以被逐段审查:传输信封(0022)→ HTTPS transport(0025)→ 第三方库边界(0029)→ detached minisign 信任模型(0034),每一层只承诺自己职责范围内的事情,跨层问题在边界处显式声明"不属于本 ADR"。

九、演化与现状:从自研信封到第三方库的信任模型变迁

ADR-0022 的传输信封契约属于第一代自研更新栈的一部分(与 ADR-0021 的 manifest v1 + detached Ed25519、ADR-0025 的 ureq 传输共同组成)。此后信任模型经历了三次换实现,当前仓库的bongocat-update已不再存在UpdateManifestEnvelope与那两个 header——搜索源码仅能在历史 ADR 文档中找到它们。演化轨迹如下:

阶段ADR信任模型传输方式信封/header 契约
第一代ADR-0021 / ADR-0022 / ADR-0025detached Ed25519 清单签名,先验签后解析自研:固定ureq 3.4.0,HTTPS-only、无 redirect、仅 200、禁透明压缩UpdateManifestEnvelope+bongocat-update-key-id/bongocat-update-signature-ed25519header,body 1 MiB 原始字节
第二代ADR-0029zipsign 归档内嵌 ed25519 签名self_update 1.3.0的 ureq feature退役;manifest v1 schema 与 1 MiB 上限一并废除
第三代(现行)ADR-0034detached minisign 签名(BLAKE2b-512 预哈希 + 8 字节 key ID)cargo-packager-updater 0.2.3(default-features = false,仅rustls-tls)退役;改为一份共享 static 形状 manifest(顶层version+<os>-<arch>的platforms映射),每个条目含url、signature、format

换实现的直接原因是能力缺口而非实现缺陷:ADR-0029 暴露 zipsign 只能签.zip与.tar.gz,裸.exe/.dmg必然验签失败,且self_update的 replace-and-verify 语义不适用于 NSIS 系统安装器(docs/adr/0034-detached-minisign-update-trust-model.md 对这两条有完整论证)。值得注意的是,minisign 与 zipsign密码学上不互通(预哈希算法与 key ID 语义不同),所以换模型等于换信任模型,不是换封装。

从当前源码看(crates/bongocat-update/src/lib.rs),第三代实现的职责划分与 ADR-0022 的"边界思想"一脉相承:

  • 传输与信任决策由库(cargo-packager-updater)拥有;
  • bongocat-updatecrate 只保留 BongoCat 特有的策略:不可变的ReleaseConfiguration(repository、channel、target)、ReleaseChannel门禁(Development 构建不允许联网安装)、匿名UpdateDiagnostics契约;
  • 验签发生在安装之前:Update::download()读完 payload 后立即调用verify_signature,install()只接受已验签的字节;
  • 第三方错误、配置与平台类型全部映射为项目自有类型,不扩散为公共 API——diagnostics.rs开头的模块注释明确写着"本模块刻意不包含任何cargo_packager_updater类型",应用诊断边界(ADR-0016 / ADR-0027)只能看到稳定的、无路径的错误码与匿名计数器。

十、仍然有效的遗产:三条穿越三代实现的设计原则

尽管 ADR-0022 的 header/body 协议已退役,它所确立的方法论在现行实现中依然成立,可以直接作为其他项目的契约设计清单:

  1. 待验签字节必须无歧义:验签器只能接收"签名时的那份字节"。无论传输层换成 ureq、self_update 还是 cargo-packager-updater,这个前提从未改变——第三代实现中库在验签前把整个 payloadread_to_end进内存,正是为了确保验签对象是完整、未改动的载荷。
  2. 失败关闭而非失败开放:ADR-0022 要求缺失/非法 header 以稳定 error code 失败;ADR-0034 在RELEASE_SIGNING_KEY缺失时于任何网络请求之前返回update_signature_key_missing。注意第三代还专门补了一道防线:库对空 pubkey 本身就会解码失败(fail-closed),项目仍保留自己的门禁,因为要产出的是稳定、无路径的错误码而不是库错误。
  3. 第三方类型不得离开边界:ADR-0022 要求 HTTP/TLS/代理/status text/response 类型不离开 transport adapter;今天的 crates/bongocat-update/src/diagnostics.rs 用 14 个稳定错误码(UpdateErrorCode::ALL,从update_not_configured到update_internal_failed)和 10 项匿名计数器封住诊断导出边界,sanitized()方法会把任何未被识别的 provider 文本过滤掉——"未知错误细节可以丢弃,但绝不能原样泄漏"。

十一、延伸阅读

  • 信任边界与验签语义:docs/adr/0021-signed-update-manifest.md
  • HTTPS transport 实现(已退役):docs/adr/0025-signed-manifest-https-transport.md
  • 第三方库边界与替换(已退役):docs/adr/0029-third-party-update-library-boundary.md
  • 现行 detached minisign 信任模型:docs/adr/0034-detached-minisign-update-trust-model.md
  • 现行实现:crates/bongocat-update/src/lib.rs、crates/bongocat-update/src/runtime/mod.rs、crates/bongocat-update/src/diagnostics.rs
  • 离线能力测试(签名器 × 验签器互操作):crates/bongocat-update/tests/release_manifest_capability.rs
  • 桌面应用

【免费下载链接】BongoCat

🐱 BongoCat — A cross-platform interactive desktop pet that brings fun to your desktop!

项目地址:https://gitcode.com/gh_mirrors/bong/BongoCat
点击查看免费下载

相关推荐

上一篇:Jupytext 将 Wolfram 语言 Notebook 转换为 Markdown:格式、语言标记与 .wolfram 扩展名解析
下一篇:easy-vibe 实战指南:如何判断一个好点子——从用户痛点找到愿意付费的产品方向

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询