业务故事站
P3 Apollo

Bundle 签名与验签:供应链安全

制品不是"一个压缩包传过去"就完了:bundle 自带逐文件 SM3 清单、不可变 digest、SM2 签名与签名者白名单信任锚。看完这 3 个故事,你就明白"验签通过"和"可信"为什么是两回事。

安全合规 平台运维 SM3 SM2 信任锚 共 3 个故事

能 / 不能速览

✅ 这个主题能做
  • 上传文件构建签名 bundle:manifest + 逐文件 SM3 清单 + 不可变 digest + SM2 签名
  • 全链路验签:files_ok / digest_ok / trusted / signature_ok 四项诊断
  • 签名者白名单信任锚:指纹不在白名单(或已禁用)即拒绝应用(403 VERIFY_SIGNER_NOT_TRUSTED)
  • 下载 zip 归档(manifest.json + digest.sig + files/),可离线读回再验
  • 构建时自动把演示签名者占位指纹升级为真实 SM2 公钥指纹并注册白名单
  • 可把某个 bundle 的期望状态指向一键清空(选「未指向」即提交 desired_state_id=0)
⛔ 这个主题做不了
  • verify 是诊断接口;真正应用时的拒绝逻辑在 Spoke 侧白名单预检(本版验签与白名单在控制面)
  • 演示签名者私钥仅存服务器内存,重启后重新生成密钥,指纹会变化(需重新构建 / 更新白名单)
  • SBOM / 漏洞扫描 / 策略门禁已实现(自研内置模拟,非真实 Trivy/Syft,OPA 未实现),但属于"安全合规"主题,bundle 签名层只管来源与完整性
  • 签名者白名单要求密钥分发生命周期管理,当前只有手动 add / enable / disable

适用角色

  • 应用开发工程师:把制品文件打包成 bundle,填 app / version / bundle_version 并上传文件。
  • 安全合规工程师:验签、维护签名者白名单、评估供应链风险——本主题核心使用者。
  • 平台运维 / SRE:构建与下载制品,配合安全团队完成信任锚演练。

能力速览(能做什么)

构建签名 bundle

POST /bundles multipart 上传文件,生成 manifest(文件清单 + 组件清单)+ 不可变 digest + SM2 签名。

全链路验签

POST /bundles/:id/verify:逐文件重算 SM3、重算 digest、白名单查指纹、SM2 验签,四项结果全绿才可信。

签名者白名单

POST /signers 维护信任锚(name + 公钥指纹 + enabled),指纹不在白名单或已禁用即拒绝应用。

不可变 digest

digest = 内容哈希,是 bundle 的唯一连接键(内容变则 digest 变);version 仅作展示语义。

下载与离线校验

GET /bundles/:id/download 返回 zip(manifest.json + digest.sig + files/),落盘后可读回再校验。

占位指纹自动升级

seed 的 demo-signer 占位指纹在首次构建 bundle 时自动替换为真实 SM3(公钥) 指纹,白名单自洽。

界面操作指引(不写代码怎么用)

下面按「打开哪个页面 → 点哪里 → 填什么 → 看到什么」的顺序给出完整操作路径,业务用户照做即可。每步末尾的「API 等价」是给工程师对照用的,可以忽略。

1 打开「制品与渠道」页面 Apollo 控制台(18082)→ 左侧菜单「制品与渠道」
  • 怎么做:用 Apollo 账号登录(演示账号 admin / admin1),在左侧菜单点「制品与渠道」,也可直接访问 /apollo/bundles。
  • 会看到:上半部是「构建 bundle」表单与 bundle 制品列表,下半部并排「发布渠道(channels)」卡与「部署策略(policies)」卡。

API 等价:GET /api/v1/bundles

2 填「构建 bundle」表单并选文件 制品与渠道 → 构建 bundle
  • 怎么做:填「应用」(如 order-service)、「版本」(如 1.0.0)、「bundle_version」(默认 1),签名者保持默认 demo-signer;点「+ 文件行」追加文件行,每行左侧「路径」填该文件在 bundle 内的相对路径(留空则用本地文件名),右侧点选本地文件。
  • 会看到:选中后行右侧出现文件名。单个文件不要超过 8MB——超限会被前端直接拦下(后端对超限内容按截断处理且不报错)。

API 等价:POST /api/v1/bundles(multipart,表单字段名即 bundle 内路径)

3 点「构建 bundle」提交 制品与渠道 → 构建 bundle → 按钮「构建 bundle」
  • 怎么做:确认应用、版本至少填了一项且至少选了一个文件,点「构建 bundle」。
  • 会看到:绿色提示「bundle 构建成功 id=… digest=…(签名者 …)」,列表最上方出现新行,带 digest、签名者指纹、大小。app / version / 文件缺一会被拦下并提示。

API 等价:POST /api/v1/bundles

4 对该 bundle 点「验签」并「下载 zip」 制品与渠道 → bundle 制品列表 → 行内「验签」
  • 怎么做:在 bundle 制品列表找到刚构建的行,点「验签」;需要离线核验时点同行「下载 zip」。
  • 会看到:弹出「验签结果(bundle #id)」窗口,逐项展示 files_ok / digest_ok / trusted / signature_ok 与 signer_name;行内出现绿底「可信」或红底「不可信」徽标。下载得到 manifest.json + digest.sig + files/ 的 zip 归档。

API 等价:POST /api/v1/bundles/:id/verify、GET /api/v1/bundles/:id/download

5 在「签名者白名单」禁用 / 启用信任锚,回来看验签被拒 Apollo 控制台 → 平台管理 → 签名者白名单
  • 怎么做:侧边栏「平台管理」分组下进「签名者白名单」(/apollo/admin/signers),找到 demo-signer 点「禁用」;回到「制品与渠道」对同一 bundle 再点「验签」;演练完回白名单点「启用」恢复。
  • 会看到:禁用后验签返回不可信(trusted=false,红底「不可信」徽标提示 VERIFY_SIGNER_NOT_TRUSTED);启用后重新验签恢复四项全绿。

API 等价:POST /api/v1/signers/:id/disable、POST /api/v1/signers/:id/enable

6 用「指向期望状态」下拉切换或解除发布目标 制品与渠道 → 发布渠道(channels)卡
  • 怎么做:在渠道行的「指向期望状态」下拉里选一条期望状态(只列 active),或选「未指向」并确认。
  • 会看到:切换成功提示「渠道 "…" 已指向期望状态 #…」;选「未指向」则提示「已解除指向(未指向任何期望状态)」,该渠道的发布目标即被清空。

API 等价:POST /api/v1/channels/:id/set-desired-state

提示:「验签」在语义上只是诊断,但后端要求 bundle 写权限,只读角色点它会 403「无权限执行该操作」;演示签名者的私钥仅存服务器内存,服务重启后指纹会变化,需要重新构建 bundle 或更新白名单。

调整指南(怎么调整)

  • 换签名者:构建时表单字段 signer 指定签名者(默认 demo-signer);组件 kind 按文件路径自动识别(config / eval_set / ontology_yaml 等)。
  • 改信任锚:新供应商加入走 POST /signers(name + 公钥指纹);下线走 disable,同名 / 同指纹重复注册会 409 SIGNER_CONFLICT。
  • 改校验粒度:验签四项逐项诊断,哪项红了就定位哪类问题(文件被改 / digest 对不上 / 白名单缺失 / 签名无效)。
  • 改版本语义:bundle_version 建议按发布计划单调递增;bundle digest 变更即新制品,历史 bundle 仍可下载验签。
  • 改分发方式:制品目录(temp/apollo_bundles/<digest>)可整体拷贝到离线环境,配合 zip 下载做二次分发。

做得好的场景

Bundle 签名最擅长"让制品来源与完整性可被证明":
  • 供应链可视:bundle 从"一个包"变成"清单 + 签名 + 指纹",谁打的、含什么、有没有被改,全可查。
  • 防篡改:逐文件 SM3 + digest 双重校验,中间人改一行配置都会被 files_ok / digest_ok 拦住。
  • 防"任意有效签名":白名单信任锚让"验签通过"不等于"可信",从根上堵住 Sigstore 指出的漏洞。
  • 国密合规:SM3 摘要 + SM2 签名,契合信创与国产化要求,离线可验。

限制与不足

以下是明确的边界,使用前先知道:
  • 验签与应用分离:verify 只做诊断;"应用前拒绝"的白名单预检在 Spoke 侧,本版 Spoke 演示为拉取 / 上报。
  • 密钥生命周期:演示签名者私钥仅存内存、重启重建;真实环境需自行管理密钥分发与轮换。
  • SBOM / 漏洞扫描 / 策略门禁在"安全合规"主题:已实现的是自研内置模拟(非真实 Trivy/Syft,OPA 未实现),bundle 签名层不携带漏洞信息。
  • 离线导入回传是骨架:三区双重校验 + 导入报告签名回传(G9)当前为机制骨架,完整流程属 P1 后续。
  • 单文件 8MB 上限:上传的单个文件超过 8MB 时前端直接拦截;后端对超限内容按截断处理,不会报错——大文件请拆包。
  • 验签挂在写权限下:POST /bundles/:id/verify 注册在 bundle 写权限(bundle:write)上,只读角色无法单独触发验签。

场景故事

故事 1 第一次构建签名 bundle:占位指纹自动升级
背景
阿强要把 demo-app 的制品打包成 bundle:web.bin + db.bin 两个文件。demo-signer 是 seed 里的占位签名者(占位指纹 demo-fingerprint-placeholder),第一次构建 bundle 时平台会自动把它升级为真实 SM2 密钥指纹并注册进白名单。
传统做法对比
以前制品就是一个压缩包传来传去,没人能证明"这个包到底是谁打的、有没有被改过",线上跑出问题只能回滚瞎猜;现在 bundle 自带文件清单(SM3)+ 不可变 digest + SM2 签名,来源与完整性一目了然。
角色
阿强(应用开发工程师,打包制品)+ 刘经理(安全合规,随后验收信任锚)。
操作步骤
  1. 进入"制品"页(/apollo/bundles),点击"构建 bundle"
  2. 填 app=demo-app、version=1.0.0、bundle_version=1
  3. 上传 web.bin、db.bin(表单字段名即 bundle 内路径)
  4. 提交,观察记录生成
系统响应
构建成功返回 bundle 记录:
POST /api/v1/bundles
{ "code": 0, "data": { "id": 1,
  "name": "demo-app-1.0.0-b1",
  "digest": "sm3:...", "signer_id": "demo-signer",
  "signer_fingerprint": "..." } }
trusted_signers 里 demo-signer 的占位指纹被替换为真实 SM3(公钥) 指纹。
结果洞察
bundle 不可变 digest 是唯一连接键(内容变则 digest 变);manifest 含逐文件 SM3 校验和与组件清单(组件名取文件基名)。签名者指纹自动注册白名单,保证后续验签信任锚自洽。
调整建议
bundle_version 按发布计划填并保持单调递增;文件路径名会决定组件 kind(含 config / eval_set / ontology_yaml 的路径自动归类),跨产品分发时可复用。
动手试一试
登录:admin / admin1,端口 18082。页面路径:制品 → 构建 bundle。输入内容:app=demo-app、version=1.0.0、上传两个小文件。预期结果:返回 id 与 digest;到"签名者"列表看 demo-signer 指纹已从占位变为真实指纹。
限制提示
文件源不能为空(400 BUNDLE_INVALID);文件路径不允许重复 / 绝对路径 / 目录穿越;构建用服务器内存中的演示签名者私钥,重启后密钥重建,指纹变化需重新构建或更新白名单。
故事 2 验签:SM3 清单 + digest + SM2 全链路体检
背景
bundle 构建完,刘经理要对它做一次完整"体检":逐文件 SM3 校验、digest 一致性、白名单信任、SM2 验签,确认这个包可以放心分发到各环境。
传统做法对比
以前校验制品只能"解压、跑起来看看",无法预防中间人篡改——改一行配置文件就神不知鬼不觉;现在一键验签,四项结果全绿才说明包从签名到现在没被动过。
角色
刘经理(安全合规工程师,主导验签与放行决策)。
操作步骤
  1. 在制品列表找到刚构建的 bundle,点击"验签"
  2. 等待校验链执行
  3. 查看 VerifyResult 四项结果
系统响应
验签返回结构示例:
POST /api/v1/bundles/1/verify
{ "code": 0, "data": { "files_ok": true,
  "digest_ok": true, "signature_ok": true,
  "trusted": true, "signer_name": "demo-signer" } }
结果洞察
校验链四步:①逐文件重算 SM3 对照清单(files_ok)②重算 digest 对照 manifest(digest_ok)③签名者指纹在白名单且 enabled(trusted)④用白名单公钥做 SM2 验签(signature_ok)。任何一步失败即拒绝应用。
调整建议
校验结果作为审计凭据留存;下载 zip(manifest.json + digest.sig + files/)后可离线读回再验,适合给内网 / 离线环境做二次确认。
动手试一试
操作:对刚构建的 bundle 点击"验签"。预期结果:files_ok / digest_ok / signature_ok / trusted 全部 true,signer_name=demo-signer。
限制提示
verify 是诊断接口;真正部署时的"应用前拒绝"逻辑在 Spoke 侧白名单预检;trusted=false 时接口返回 403 VERIFY_SIGNER_NOT_TRUSTED(即使 SM2 验签通过也不放行)。
故事 3 签名者白名单信任锚:未知签名者的包被拒
背景
供应链安全演练:假设攻击者 / 外部供应商用"自己的密钥"签名了一个 bundle,SM2 验签技术上能通过,但它的指纹不在 trusted_signers 白名单里——平台必须拒绝。演示方法:把 demo-signer 禁用,再验签之前的 bundle,观察被拒;最后重新启用恢复。
传统做法对比
只验签不查身份的系统里,"任何持有有效密钥的人都能发布",等于把信任全交给密钥分发环节,这正是 Sigstore 指出的"任意有效签名"漏洞;白名单信任锚 = 除了验签,还要求签名者指纹在预置白名单(enabled=true),从根上杜绝。
角色
刘经理(安全合规,演练主导)+ 陈工(平台运维,操作白名单启用 / 禁用)。
操作步骤
  1. 先构建一个 bundle 并验签成功(demo-signer 在白名单)
  2. 到签名者页把 demo-signer 禁用(POST /signers/:id/disable)
  3. 再次对同一 bundle 验签
  4. 观察被拒,再启用(enable)恢复
系统响应
禁用后验签返回:
HTTP 403  VERIFY_SIGNER_NOT_TRUSTED
"签名者指纹 ... 不在可信白名单(enabled=true)中,拒绝应用"
VerifyResult: { "files_ok": true, "digest_ok": true,
  "signature_ok": true, "trusted": false }
重新启用后验签恢复四项全绿。
结果洞察
信任锚语义:SM2 验签通过 ≠ 可信。白名单(signer_id + 公钥指纹)是信任的根,指纹不在白名单或已禁用,一律拒绝应用。这比"验签通过即放行"更贴近真实供应链安全,也是对 Sigstore 结论的落地。
调整建议
白名单模型已支持环境作用域(trusted_signers.environment_id,验签按环境级优先、回退全局);正式环境建议把签发流程与白名单变更纳入审批审计;同名 / 同指纹重复注册返回 409 SIGNER_CONFLICT。
动手试一试
操作:构建 bundle → 验签全绿 → 禁用 demo-signer → 再验签被 403 拒绝(trusted=false)→ 重新启用 → 验签恢复全绿。预期结果:完整走通"信任锚拒绝 / 恢复"闭环。
限制提示
白名单要求密钥分发生命周期管理(换钥要同步更新指纹);演示签名者私钥仅存内存,重启服务器会重新生成新密钥(指纹变化,需重新构建);当前仅 HTTP / 轮询,gRPC / mTLS 属 V2。

常见问题

bundle 和普通压缩包有什么区别?

普通压缩包只有内容;bundle 是"内容 + 清单 + 签名":manifest 记录逐文件 SM3 校验和与组件清单,digest 是内容哈希(唯一连接键),SM2 签名证明来源,白名单证明签名者可信任。任何一项对不上就拒绝。

为什么"验签通过"还不等于"可信"?

验签只证明"这个包确实是持密钥者签的",但持密钥者是不是可信的签发方是另一回事。白名单信任锚把信任收敛到"签名者指纹在 trusted_signers(enabled=true)",堵住"任意有效签名"漏洞。

demo-signer 的占位指纹是怎么回事?

seed 创建的 demo-signer 初始指纹是占位符(demo-fingerprint-placeholder)。首次构建 bundle 时,平台生成真实 SM2 密钥对,用 SM3(公钥) 计算指纹并替换占位值,保证"构建方"与"白名单"自洽。

换了密钥怎么办?

重新构建 bundle 会得到新指纹;若签名者记录已存在,需要更新 trusted_signers 的指纹与公钥(PUT /signers/:id),或删除重建。注意同名 / 同指纹冲突返回 409,白名单变更建议纳入审批。

验签失败可能是什么原因?

按四项对号入座:files_ok=false 文件被改(SM3 对不上);digest_ok=false 内容与 manifest 不一致;trusted=false 签名者不在白名单或已禁用(403);signature_ok=false SM2 验签失败(签名无效 / 公钥不匹配)。

主题小结

一句话:bundle 供应链安全 = 逐文件 SM3 清单 + 不可变 digest + SM2 签名 + 签名者白名单信任锚。核心认知:验签通过 ≠ 可信,白名单才是信任的根。边界:verify 是诊断接口、演示密钥仅存内存重启重建;SBOM / 漏洞扫描 / 策略门禁已实现(自研内置模拟)但属于 安全合规主题。