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/repo | GitHub 仓库全名 |
| 简写仓库 | owner/repo | github.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 的降级拒绝逻辑,可以展开为以下几类具体情形:
- 签名者更换(signer change):项目从 GitHub Actions 工作流 A 切到工作流 B 签名(工作流路径不同即视为不同签名者),或者从无密钥签名(keyless)切换到显式公钥(key)。
- 签名方案变弱(weaker scheme):例如从
sigstore-oidc切到sigstore-key,代码中把"方案变更"与"签名者变更"同等对待(见 src/packslip_pins.rs)。 - 厂商直签变成重打包者(vendor → repackager):当已接受过 vendor 直签的发布,后来者却是 repackager 签的,会被拒绝。
- 丢失构建出处(provenance):之前每个构件都链接了构建出处,后来的发布不再链接,会被拒绝。
- 撤销误伤后的恢复: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 |
issuer | OIDC 签发者 | 可选 |
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 和校验和,包括在另一台机器上首次安装时。
因此完整的签名轮换流程应为:
- 确认 vendor 官方公告的新签名身份或密钥;
mise packslip forget <PROJECT>清除本机固定记录;- 更新/重新生成
mise.lock(例如重新mise install让新签名者写入锁文件),或让锁文件随新安装更新; - 用
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),仅供参考