主题:供应链安全。本文讨论发布物来源证明、验证策略和准入控制,不讨论攻击投递或真实入侵步骤。
结论先说清楚
Provenance(来源证明)不是“这个包安全”的证书。它最多回答三个更窄的问题:这个发布物声称由哪个仓库、哪个工作流、哪个提交和哪组构建指令产生;这个声明是否被可信签名链保护;当前消费者是否愿意接受这条构建路径。
因此,企业落地 npm Trusted Publishing、GitHub Artifact Attestations 或 SLSA Provenance 时,不能把目标写成“开启供应链安全”。更准确的目标是:把“构建来源”从事后审计材料变成发布、入库、部署前的准入条件。
这会改变治理重心:
- 不是只问“有没有签名”,而是问“签名身份是否绑定到预期仓库和工作流”;
- 不是只问“有没有 SLSA Provenance”,而是问“predicate 中的 builder、subject、commit、workflow 是否满足策略”;
- 不是只问“包管理器能不能展示 provenance 徽章”,而是问“CI、制品库、部署入口是否会拒绝不符合策略的制品”。
资料和本地验证边界
本次资料检索覆盖 GitHub Artifact Attestations、actions/attest、npm Trusted Publishing、npm provenance、SLSA provenance、Sigstore/cosign。为了避免把文档理解写成空泛结论,我在本地做了最小可复现检查:
## Tool/environment checks
Tue Jun 9 21:01:29 UTC 2026
v22.22.2
10.9.7
/usr/bin/bash: line 9: gh: command not found
/usr/bin/bash: line 10: jq: command not found
/usr/bin/bash: line 11: cosign: command not found
## npm package metadata and attestation API ([email protected] sample)
{
"dist.tarball": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"dist.integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"repository.url": "git+https://github.com/npm/node-semver.git"
}继续用 Python 直接访问 npm registry 和 GitHub API,得到一组更可用的边界证据:
## Python registry/API checks
[email protected] attestation_count= 2
predicate_types= ['https://github.com/npm/attestation/tree/main/specs/publish/v0.1', 'https://slsa.dev/provenance/v1']
bundle_keys= ['mediaType', 'verificationMaterial', 'dsseEnvelope']
bundle_keys= ['mediaType', 'verificationMaterial', 'dsseEnvelope']
semver-7.6.3.tgz bytes= 27678 sha256= 376d2ca2c941fc5a37e9ac3ec65302e5e421e2cc1ee3dee57a854d2bd9bee125
repo= actions/attest default_branch= main stars= 120 pushed_at= 2026-06-09T16:31:17Z latest_release= v4.1.0
repo= sigstore/cosign default_branch= main stars= 6025 pushed_at= 2026-06-09T15:45:31Z latest_release= v3.1.1
repo= slsa-framework/slsa-github-generator default_branch= main stars= 575 pushed_at= 2026-03-29T18:57:33Z latest_release= v2.1.0这段输出只能证明:当前环境可访问 npm registry 和 GitHub API;[email protected] 的 npm attestation API 返回了两类 predicate;本机没有安装 gh、jq、cosign,所以本文没有伪装成完成了本地 cryptographic verification。真正的密码学验证需要在安装相应工具、固定可信根和策略参数后执行。
Provenance 解决的是“来源可验证”,不是“内容无风险”
SLSA 对 provenance 的定义非常克制:它是可验证信息,用来把一个 artifact 追溯到复杂供应链中“它从哪里来、何时产生、如何产生”。SLSA 还区分 build provenance 和 source provenance:前者追踪构建输出回到源代码和构建过程,后者追踪源码修订及变更管理过程。
这一区分非常关键。一个 npm 包具备 build provenance,只能说明 tarball 与某次构建声明之间存在可验证关系;它不能自动证明:
- 源码没有恶意逻辑;
- 依赖树没有已知漏洞;
- 维护者账号没有被接管;
- workflow 本身没有执行过高风险脚本;
- 构建环境没有从网络拉取未固定的二进制工具;
- 发布物符合企业内部许可证、隐私和数据流要求。
GitHub Docs 也明确提示,artifact attestations 不是 artifact 安全性的保证,而是把消费者链接到产生该 artifact 的源码和构建指令。安全收益来自后续策略判断,而不是 attestation 文件本身。
可以把它抽象成四层:
如果企业只做到 D 层,把 bundle 生成出来但不在 E/F 层执行策略,这套机制就停留在“可审计但不拦截”。
npm Trusted Publishing:减少长效 token,不等于减少所有发布风险
npm Trusted Publishing 的核心价值是用 OIDC 让 CI/CD 工作流获得短期发布能力,替代长期 npm token。GitHub 2025 年 7 月的变更说明中提到,npm trusted publishing with OIDC 已经 GA;使用 trusted publishing 时,npm CLI 会自动生成并发布 provenance attestations,不再需要额外加 --provenance 参数。
这解决了一个非常具体的痛点:长期 token 放在 CI secrets 中,泄露后可能被复用;OIDC 则把发布身份绑定到特定 CI 运行上下文,令 registry 根据短期断言发放临时能力。
但它不会自动解决这些问题:
| 风险点 | Trusted Publishing 能改善什么 | 仍需额外控制什么 |
|---|---|---|
| 长期 npm token 泄露 | 减少或移除发布 token 存储 | 仍要限制能触发 release workflow 的人和分支 |
| 发布来源不透明 | 自动生成 provenance,便于追溯仓库和 workflow | 消费方必须验证 predicate 与预期策略匹配 |
| Workflow 被篡改 | OIDC 能说明“哪个 workflow 运行了” | 需要分支保护、CODEOWNERS、复用工作流和审计 |
| 构建中拉取未固定依赖 | provenance 可记录构建上下文 | 仍要 lockfile、固定 digest、离线/镜像策略 |
| 包内容恶意或漏洞 | 不直接解决 | 需要 SCA、代码审计、行为分析、权限沙箱 |
npm provenance 的一个实际边界可以从 [email protected] 样本看到:registry attestation API 返回的不只是 SLSA predicate,还有 npm publish predicate。也就是说,消费者不能简单地“拿到 attestations 数组就通过”,而要明确选择 predicateType == https://slsa.dev/provenance/v1 或符合内部策略的其他 predicate。
predicate_types= [
'https://github.com/npm/attestation/tree/main/specs/publish/v0.1',
'https://slsa.dev/provenance/v1'
]这类细节很适合进入制品准入策略:没有 SLSA provenance 不通过;有 provenance 但 subject digest 不匹配不通过;issuer、repository、workflow 不在允许列表不通过。
GitHub Artifact Attestations:生成只是第一步,验证入口更重要
GitHub Artifact Attestations 允许在 GitHub Actions 中为二进制、容器镜像或 SBOM 生成签名 attestation。官方文档给出的二进制最小配置大致是:
permissions:
id-token: write
contents: read
attestations: write
steps:
- name: Generate artifact attestation
uses: actions/attest@v4
with:
subject-path: 'PATH/TO/ARTIFACT'如果是容器镜像,则需要 subject-name 与 subject-digest,并且文档明确提醒 subject-name 应该是 fully-qualified image name,不应包含 tag;digest 必须是 sha256:HEX_DIGEST 形式。这个要求背后的安全逻辑很简单:tag 是可移动指针,digest 才是内容地址。
actions/attest 的 README 还补充了几个实现边界:
- attestation 使用 in-toto 格式,把 subject 与 predicate 绑定;
- 签名使用短期 Sigstore 证书;
- public repository 使用 Sigstore public good instance;
- private/internal repository 使用 GitHub private Sigstore instance;
- 私有仓库使用 artifact attestations 需要 GitHub Enterprise Cloud;GitHub Enterprise Server 不支持。
这意味着验证策略不能只写一套“公有 Sigstore + Rekor”的固定逻辑。组织内部至少要区分三种场景:
| 场景 | 可信根/透明日志边界 | 推荐验证入口 | 常见误判 |
|---|---|---|---|
| Public GitHub repo artifact | Sigstore public good instance,公共 transparency log | gh attestation verify 或 cosign bundle 验证 | 以为有公共日志就代表内容可信 |
| Private GitHub repo artifact | GitHub private Sigstore instance,无公共 Rekor | GitHub CLI + 组织权限上下文 | 用 public Rekor 逻辑硬套私有制品 |
| npm package provenance | npm registry attestation API + Sigstore bundle | npm CLI / cosign / 自定义策略 | 不区分 publish predicate 与 SLSA predicate |
一个可落地的准入策略应该检查什么
真正有用的 provenance 验证不是“能不能 verify OK”,而是“verify OK 之后是否满足组织策略”。建议把策略拆成六组条件。
| 策略维度 | 检查对象 | 通过条件示例 | 失败处理 |
|---|---|---|---|
| Artifact identity | 文件 hash、package name、image digest | subject digest 与下载内容一致;镜像只接受 digest,不接受 tag | 隔离制品,重新拉取或拒绝部署 |
| Issuer | OIDC issuer / Sigstore trusted root | GitHub Actions issuer 或组织批准的 CI issuer | 拒绝;检查是否来自未知 CI |
| Source | repository、owner、commit SHA | 仓库在 allowlist;commit 属于受保护分支或 release tag | 进入人工审查或要求重建 |
| Workflow | workflow path、reusable workflow、trigger | 使用固定 release workflow;禁止临时 workflow 发布 | 拒绝或降级为不可发布 |
| Predicate | SLSA predicate type、builder id、materials | predicateType 符合策略;materials 可追踪 | 记录证据,触发供应链审计 |
| Time and revocation | 签名时间、证书有效期、透明日志/时间戳 | 构建时间在 release 窗口内;证书链可验证 | 拒绝或要求安全团队复核 |
这组检查可以落到三个入口:
- 发布入口:release workflow 结束前生成 attestation,并把 artifact、SBOM、attestation 作为一组不可拆的发布材料;
- 制品库入口:自建 registry/proxy 只接受通过策略验证的包或镜像;
- 部署入口:Kubernetes admission、CD pipeline 或 IaC plan 阶段验证 digest 与 attestation,不允许“手动替换镜像 tag”。
最小工程实现:先做“证据闭环”,再谈高等级 SLSA
很多团队会一上来讨论 SLSA Build Level 3 或更复杂的 isolated builder,但第一阶段更应该把证据闭环做扎实。一个可执行的落地路径如下。
第一步:发布物必须有不可变身份
npm 包要记录 tarball URL、integrity、sha256;容器镜像要记录 digest;二进制要记录 checksums 文件。没有不可变 subject,attestation 就没有稳定锚点。
可复现实验可以从只读检查开始:
npm view <package>@<version> dist.tarball dist.integrity repository.url --json
python3 - <<'PY'
import hashlib, urllib.request
url='https://registry.npmjs.org/semver/-/semver-7.6.3.tgz'
data=urllib.request.urlopen(url, timeout=30).read()
print(len(data), hashlib.sha256(data).hexdigest())
PY本文环境对 [email protected] 得到的 SHA256 是:
376d2ca2c941fc5a37e9ac3ec65302e5e421e2cc1ee3dee57a854d2bd9bee125它只代表本次下载内容的 hash,不代表该包安全,也不代表未来网络路径一定返回同一内容;生产系统应结合 registry metadata、lockfile 和 attestation subject 做一致性校验。
第二步:发布 workflow 要收敛
不要让每个仓库都随手写一份 release YAML。更稳妥的是使用组织级 reusable workflow,业务仓库只传入 package path、artifact name、release tag 等有限参数。这样做的收益有两个:
- 审计范围从 N 份 release workflow 收敛到少量受控模板;
- provenance 中的 workflow identity 更容易写入 allowlist。
GitHub Docs 提到,artifact attestations 本身提供 SLSA v1.0 Build Level 2;如果构建发生在跨组织共享且经过审查的 reusable workflow 中,可以进一步接近 Build Level 3 的隔离和受控构建指令目标。这里的重点不是追逐等级数字,而是让“构建指令可审计、可复用、可限制”。
第三步:验证策略要版本化
验证命令不应该散落在 wiki 里,而应该进入代码库,例如:
policy/
provenance-policy.yaml
allowed-builders.yaml
allowed-workflows.yaml
verify-artifact.sh策略至少要能回答:
- 允许哪些 package scope / image repository;
- 允许哪些 GitHub owner/repo;
- 允许哪些 workflow path 或 reusable workflow;
- 是否要求 SLSA provenance;
- 是否要求 SBOM attestation;
- 验证失败是 hard fail、quarantine,还是只记录 warning。
第四步:失败要可排错
Provenance 准入失败不能只输出 “verification failed”。对工程团队有用的失败模式应该拆成具体原因:
| 失败模式 | 可能原因 | 排查证据 |
|---|---|---|
| digest mismatch | 下载内容变化、subject 指错、tag 被移动 | 本地 hash、registry metadata、attestation subject |
| unknown issuer | 来自未批准 CI、验证工具可信根配置错 | OIDC issuer、certificate chain、trusted root |
| workflow mismatch | release workflow 路径变化、临时 workflow 发布 | workflow path、run id、commit diff |
| predicate missing | 没有生成 SLSA provenance、只生成 publish predicate | predicateType 列表、registry attestation API |
| private attestation verify failed | 使用了 public Sigstore 验证逻辑 | GitHub private Sigstore trusted root、企业权限 |
| policy expired | 旧仓库迁移、builder id 变更 | policy 版本、例外审批记录 |
这张表比“开启 provenance”更接近生产运维:它让开发、平台和安全团队知道应该去哪里找证据。
不要把 cosign / gh / npm CLI 当成同一种验证器
Sigstore Blog 展示了 cosign v2.4.0 之后可以验证 npm provenance、GitHub Artifact Attestations、Homebrew provenance 使用的 bundle 格式。它的示例模式是:拿到 artifact,拿到 bundle,再用 --certificate-oidc-issuer 和 --certificate-identity-regexp 指定期望来源。
这说明 cosign 可以作为底层验证工具,但工程上仍要注意三点:
- 工具入口不同:npm CLI、GitHub CLI、cosign 的默认拉取位置、可信根、输出结构并不完全相同;
- 公私有边界不同:public GitHub repo 与 private GitHub repo 的 Sigstore 实例和透明日志语义不同;
- verify OK 不是最终策略:还要解析 predicate,执行组织自己的 allowlist、denylist、例外和审计规则。
本文本地环境没有安装 cosign、gh、jq,所以没有给出“已实测 verify OK”的结论。更严谨的生产验证应在固定工具版本后记录:工具版本、artifact hash、bundle 来源、trusted root、验证命令、输出摘要和策略版本。
安全收益与剩余风险
Provenance 的主要收益是减少“来源不可知”的供应链风险,并让事后调查更快定位构建路径。但剩余风险仍然很现实:
| 剩余风险 | Provenance 的帮助 | 仍需的补偿控制 |
|---|---|---|
| 维护者提交恶意源码 | 可追踪到 commit 和 workflow | 代码审查、分支保护、行为检测 |
| 构建脚本下载动态依赖 | 可暴露材料和构建指令线索 | 依赖锁定、网络 egress 控制、内部镜像 |
| CI runner 被污染 | 可记录 builder 身份但不必然证明 runner 干净 | ephemeral runner、隔离构建池、runner hardening |
| 策略例外泛滥 | 可产生拒绝证据 | 例外过期、审批留痕、定期回收 |
| 消费方不验证 | 没有实际拦截收益 | 制品库/部署准入强制验证 |
所以最准确的定位是:Provenance 是供应链控制面的“证据层”和“准入输入”,不是漏洞扫描器、不是沙箱、不是代码审计替代品。
结论
把构建来源当成准入条件,核心是把『信任』从人转移到可验证的自动化流程。从资产盘点到发布收敛,从身份绑定到 attestation 生成,从验证策略到准入执行,每一步都需要明确的验收证据。最危险的场景不是攻击者伪造了 artifact,而是团队根本不知道自己依赖了哪些来源。
最后判断
如果团队只是为了让 npm 页面多一个 provenance 标识,收益有限;如果把 provenance 作为制品准入条件,它会迫使发布链路回答一组原本经常被忽略的问题:谁构建的、用什么 workflow 构建的、构建了什么 digest、消费者为什么信任这条路径。
这才是 provenance 的工程价值:不是承诺“软件一定安全”,而是把不可见的构建来源变成可验证、可拒绝、可复盘的控制面输入。
