☰
mise packslip forget:解除项目签名者固定,安全应对签名身份轮换
2026/10/9 12:06:11 网站建设 项目流程

mise packslip forget:解除项目签名者固定,安全应对签名身份轮换

【免费下载链接】misedev tools, env vars, task runner项目地址: https://gitcode.com/GitHub_Trending/mi/mise

导读

mise packslip forget是 mise 中用于**解除某个 packslip 项目已固定的签名者(pinned signer)**的命令。当通过packslip:后端安装工具时,mise 会像 SSH 记住主机那样记住首次接受的签名身份,后续凡是签名者变化、签名方案变弱、厂商变重打包者或丢失构建出处(provenance)的发布,都会被拒绝,直到用户明确表态。本文围绕 docs/cli/packslip/forget.md 这一命令文档,深入讲解它的使用场景、参数与输出,并结合 src/cli/packslip/forget.rs、src/packslip_pins.rs 等源码与 e2e 测试,讲清"固定—校验—解除—重新固定"的完整信任模型。读完你将掌握:何时必须执行forget、项目名如何书写、命令背后的数据落盘位置与降级拒绝逻辑,以及如何用mise packslip pins验证操作结果。

命令概览

  • 用法:mise packslip forget <PROJECT>
  • 效果:修改状态(write)
  • 源码位置:src/cli/packslip/forget.rs
  • 所属命令组:docs/cli/packslip.md(另有子命令 docs/cli/packslip/pins.md)

该命令的作用是:忘记某个项目固定的签名者,使下一次被接受的发布重新设定它。也就是说,forget只清除本机记住的信任记忆,不会改变 vendor 的签名事实,也不会让未经验证的发布自动通过——下一次安装依然要经历完整的签名校验,只是不再与旧的固定记录做连续性比对。

命令定义源码 src/cli/packslip/forget.rs 中有明确注释:

/// Forget a project's pinned signer, so the next release accepted sets it again /// /// Do this when the vendor has announced a new signing identity or key.

即:当 vendor 官方宣布新的签名身份(identity)或密钥(key)时执行。

参数

  • <PROJECT>— 要解除固定的项目名。

项目名必须按 packslip 后端命名规则书写,支持以下形式(均可带或不带packslip:前缀):

形式示例说明
完整仓库地址github.com/owner/repoGitHub 仓库全名
简写仓库owner/repogithub.com被隐含补全
独立域名主机tool.example.com使用.well-known/packslip.json签名发布列表的项目
带前缀packslip:github.com/owner/repo前缀会被剥离后解析

从实现看,src/cli/packslip/forget.rs 会先剥离packslip:前缀,再调用project_name归一化项目名:

pub(super) fn run(self) -> Result<()> { let name = self .project .strip_prefix("packslip:") .unwrap_or(&self.project); let project = project_name(name)?; if packslip_pins::forget(&project)? { miseprintln!("forgot the pinned signer of packslip:{project}"); } else { miseprintln!("packslip:{project} had no pinned signer"); } Ok(()) }

project_name的归一化规则定义在 src/backend/packslip.rs:先去掉首尾的/,若第一段不含.则补全为github.com/…;若项目既不在已知 forge 上、又没有仓库路径,则报错提示"使用github.com/owner/repo、owner/repo或诸如tool.example.com的主机名"。因此mise packslip forget jdx/packslip与mise packslip forget github.com/jdx/packslip指向同一个项目。

标志

  • -h --help— 打印帮助信息。

forget不接受其他标志。它只是提交单个项目名,行为是确定性的:要么"忘记了",要么"本来就没有固定记录"。

适用场景:何时需要 forget

命令文档给出的核心场景只有一句话:"当 vendor 宣布了新的签名身份或密钥时"。结合 src/packslip_pins.rs 的降级拒绝逻辑,可以展开为以下几类具体情形:

  1. 签名者更换(signer change):项目从 GitHub Actions 工作流 A 切到工作流 B 签名(工作流路径不同即视为不同签名者),或者从无密钥签名(keyless)切换到显式公钥(key)。
  2. 签名方案变弱(weaker scheme):例如从sigstore-oidc切到sigstore-key,代码中把"方案变更"与"签名者变更"同等对待(见 src/packslip_pins.rs)。
  3. 厂商直签变成重打包者(vendor → repackager):当已接受过 vendor 直签的发布,后来者却是 repackager 签的,会被拒绝。
  4. 丢失构建出处(provenance):之前每个构件都链接了构建出处,后来的发布不再链接,会被拒绝。
  5. 撤销误伤后的恢复:vendor 曾撤回某版本、或补充分发列表消失被拦截(见 src/packslip_pins.rs),在确认信任策略后需要重新初始化。

这些拒绝都来自连续性校验check_against(src/packslip_pins.rs):

if pin.scheme != observed.scheme || pin.signer != signer { problems.push(format!( "is signed by {signer} ({}), but {} ({}) signed what mise accepted before", observed.scheme, pin.signer, pin.scheme )); } if pin.attested_by == "vendor" && observed.attested_by == "repackager" { problems.push( "is attested by a repackager, but the vendor's own packslip was accepted before".into(), ); } if pin.provenance && !observed.provenance { problems.push("drops the build provenance every artifact linked before".into()); }

当上述任一情况被触发时,错误信息会直接提示补救命令(src/packslip_pins.rs):

packslip:{project}: this release {问题列表}. If the vendor announced the change, run `mise packslip forget {project}` and install again; the next release accepted sets the pin.

这正是forget命令在真实故障场景中的入口。

固定(pin)与"像 SSH 记住主机"的信任模型

要理解forget的价值,先理解它删除的是什么。命令组文档 docs/cli/packslip.md 阐述了整体模型:

通过packslip:后端安装的工具,会按项目名所隐含的身份进行验证,mise 随后记住它接受的签名者,就像 SSH 记住主机一样。之后来自另一个签名者、更弱方案、厂商曾直签过的重打包者、或丢失构建出处的发布,都会被拒绝,直到有人明确表态。

从源码看,每次成功校验并接受一个发布后,mise 会调用record(src/packslip_pins.rs)写入固定记录,内容包括:

字段含义备注
scheme签名方案sigstore-oidc或sigstore-key
signer签名者标识工作流路径(去掉 ref)或密钥 id
issuerOIDC 签发者可选
attested_by认证方vendor或repackager
provenance是否链接构建出处布尔
unlogged是否接受过无透明日志条目的 bundle布尔
pinned_at固定时间RFC 3339

这些记录保存在状态目录下的packslip/pins.toml中(src/packslip_pins.rs):

pub(crate) fn pins_file() -> PathBuf { dirs::STATE.join("packslip").join("pins.toml") }

文件内容大致如下:

[pins."github.com/owner/repo"] scheme = "sigstore-oidc" signer = "https://github.com/owner/repo/.github/workflows/release.yml" issuer = "https://token.actions.githubusercontent.com" attested_by = "vendor" provenance = true unlogged = false pinned_at = "2026-09-01T12:00:00Z" [sequences."github.com/owner/repo"] # 该机器见过的最高发布列表序号

注意几个关键设计(均可从 src/packslip_pins.rs 注释确认):

  • keyless 签名者取"去 ref 的工作流路径":signer_of(src/packslip_pins.rs)对sigstore-oidc且以https://开头的身份,会切掉最后一个@之后的 ref。因此同一工作流的新 tag 视为同一签名者;而邮箱、SPIFFE URI 等身份会保留全部字符,避免alice@example.com与alice@example.invalid被合并。
  • 降级被拒、升级被记住:record在已有 pin 时只允许"更强"的方向变化——例如 provenance 从无到有会被记住为新基线,反之则被拒(src/packslip_pins.rs)。
  • 并发安全:读改写全程持有pins.toml旁边的.lock文件锁,避免两个安装进程互相覆盖(src/packslip_pins.rs)。
  • 拒绝的发布不落盘:check是只读的,只有发布整体通过后才record,被拒的安装不会留下 pin 痕迹(src/packslip_pins.rs)。

forget删除的正是以上整套记忆:既包括项目的 pin,也包括其sequences(发布列表序号),见 src/packslip_pins.rs:

/// Drop a project's pin and sequence, so the next release accepted sets /// them again. Returns whether there was one. pub(crate) fn forget(project: &str) -> Result<bool> { forget_at(&pins_file(), project) } pub(crate) fn forget_at(path: &Path, project: &str) -> Result<bool> { let _lock = locked(path)?; let mut pins = load(path)?; let had = pins.pins.remove(project).is_some() | pins.sequences.remove(project).is_some(); if had { save(path, &pins)?; } Ok(had) }

forget_at返回的布尔值正好对应命令输出:返回true打印forgot the pinned signer of packslip:{project},返回false打印packslip:{project} had no pinned signer。也就是说,重复对同一项目执行 forget 是无害的——第二次只是提示"本来就没有固定记录"。

操作示例

典型轮换流程

假设厂商宣布更换签名工作流,你安装新版时收到类似错误:

packslip:github.com/owner/repo: this release is signed by <新工作流> (sigstore-oidc), but <旧工作流> (sigstore-oidc) signed what mise accepted before. If the vendor announced the change, run `mise packslip forget github.com/owner/repo` and install again; the next release accepted sets the pin.

执行:

mise packslip forget github.com/owner/repo

预期输出:

forgot the pinned signer of packslip:github.com/owner/repo

再次安装:

mise install packslip:github.com/owner/repo@最新版本

这一次校验通过后,新签名者会被记录为新 pin,后续连续性检查从新基线开始。

简写与前缀

以下写法等价(github.com被隐含补全、packslip:前缀被剥离):

mise packslip forget jdx/packslip mise packslip forget github.com/jdx/packslip mise packslip forget packslip:jdx/packslip mise packslip forget packslip:github.com/jdx/packslip

对无记录的项目的操作

mise packslip forget tool.example.com

如果该项目从未被固定过,输出:

packslip:tool.example.com had no pinned signer

验证操作结果

forget之后用只读命令 docs/cli/packslip/pins.md 检查剩余固定记录:

mise packslip pins # 人类可读列表 mise packslip pins --json # JSON 格式,便于脚本处理

当所有记录都被清除时,pins会提示"no packslip signers pinned"。

端到端测试验证

仓库 e2e 测试 e2e/backend/test_packslip 完整演示了"固定 → 检查 → 忘记 → 再检查"的闭环:

# The signer is pinned, the way SSH remembers a host, and can be forgotten. assert_contains "mise packslip pins" "https://github.com/jdx/packslip/.github/workflows/release.yml" assert_contains "mise packslip forget jdx/packslip" "forgot the pinned signer" assert_contains "mise packslip pins" "no packslip signers pinned"

该测试还展示了其他相关行为:

  • 安装packslip:github.com/jdx/packslip@0.2.0后,packslip pins能列出其签名工作流(keyless 签名的https://github.com/jdx/packslip/.github/workflows/release.yml);
  • 用简写jdx/packslip执行 forget 同样生效,印证了项目名归一化;
  • forget 后 pins 输出变为"no packslip signers pinned",印证了删除语义;
  • 该测试也验证了packslip子命令组默认行为是pins(src/cli/packslip/mod.rs)。

单元测试层面,src/packslip_pins.rs 验证了 forget 的幂等性与"解除后新签名者可进入一次":

// Forgetting lets a new signer in, once. assert!(forget_at(&path, project).unwrap()); assert!(!forget_at(&path, project).unwrap()); record_at(&path, project, keyed).unwrap(); assert_eq!(load(&path).unwrap().pins[project].scheme, "sigstore-key");

第一次 forget 返回true(确有记录),第二次返回false(已无记录),随后原本被拒的密钥签名发布可以重新被接受并写入新 pin。另一个测试 src/packslip_pins.rs 则验证了"已接受的发布列表在 forget 之前不允许消失"——即 forget 也是解除列表连续性拦截的途径之一。

与锁文件承诺的关系

需要特别强调:forget 只清除本机状态目录下的pins.toml,不会修改项目锁文件mise.lock中的签名者承诺。锁文件会记录每个平台构件的 URL、校验和以及签名者与认证方承诺(见 docs/dev-tools/packslip-verification.md):

mise.lock— 记录项目签名者与认证方承诺,以及每个平台的构件 URL 和校验和,包括在另一台机器上首次安装时。

因此完整的签名轮换流程应为:

  1. 确认 vendor 官方公告的新签名身份或密钥;
  2. mise packslip forget <PROJECT>清除本机固定记录;
  3. 更新/重新生成mise.lock(例如重新mise install让新签名者写入锁文件),或让锁文件随新安装更新;
  4. 用mise packslip pins确认新 pin 已写入。

另外文档明确警告:直接删除pins.toml会重置所有项目的本地连续性,这不是常规故障修复手段,且不会移除锁文件中的签名者承诺(docs/dev-tools/packslip-verification.md)。相比之下,mise packslip forget <PROJECT>是精准的、单项目的、可审计的官方操作。

注意事项与最佳实践

  • 先确认厂商公告再 forget:连续性检查存在的意义是防止静默的信任漂移。只有当 vendor 官方宣布更换签名身份或密钥时才应解除固定;否则一个被劫持或重打包的发布可能借此混入。
  • forget 不是绕过验证的手段:解除固定后,下一次安装仍会执行完整的签名验证(bundle 签名、透明日志、项目/版本一致性、构件摘要与大小),只是不再与旧 pin 比较。错误信息中"不同阶段的问题要用不同手段修复"同样适用:forget 不能修复摘要不匹配、签名无效或主机要求不满足(docs/dev-tools/packslip-verification.md)。
  • 幂等安全:对无记录的项目的 forget 会输出had no pinned signer,不会报错、不会修改任何文件。
  • 善用pins做变更前快照:执行 forget 前先运行mise packslip pins --json记录旧 pin(签名者、方案、时间戳),便于事后审计与对比。
  • 注意列表连续性联动:forget同时删除项目的序列号记录(sequences),因此它也能解除"已接受的签名发布列表消失"这一拦截(src/packslip_pins.rs 中的错误提示正是建议"恢复列表或显式 forget 该项目的 pin")。

参考文档

  • mise packslip forget 命令文档
  • mise packslip 命令组文档
  • mise packslip pins 命令文档
  • Packslip 验证与策略指南
  • packslip 后端实现
  • 签名者固定记录与校验逻辑
  • packslip 后端 e2e 测试

【免费下载链接】misedev tools, env vars, task runner项目地址: https://gitcode.com/GitHub_Trending/mi/mise

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

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

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

立即咨询