在 Sigstore 和 GPG 之间做选择,就是在两种为发布制品、容器和软件包签名的方式之间做选择。使用 OpenPGP(GnuPG 及兼容工具)时,维护者持有一个长期私钥,公布它的指纹,并在每个文件旁边附上一个分离签名。使用 Sigstore 时,签名者通过 OpenID Connect 完成身份验证,Fulcio 证书颁发机构签发一张有效期为 10 分钟的证书,把一个新生成的密钥绑定到这个身份上;签名被记录到 Rekor 透明日志中,随后密钥即被丢弃。验证者检查的是身份,而不是密钥。
概览
| Sigstore(无密钥) | OpenPGP(GPG) | |
|---|---|---|
| 验证者信任什么 | OIDC 身份(电子邮件或工作流 URI)及其颁发者,通过 Fulcio 的根证书 | 一个特定的公钥,通过带外渠道获取并核对 |
| 签名密钥有效期 | 临时;证书有效期 10 分钟 | 数年;到期时间由所有者设定和延长 |
| 密钥管理 | 签名者无需管理 | 生成、备份、保护、轮换、吊销 |
| 签名时间证明 | Rekor 日志条目(以及可选的 RFC 3161 时间戳) | 签名者自行声明的签名创建时间 |
| 透明度 | 每个签名都公开记录在日志中 | 无 |
| 吊销 | 临时密钥无需吊销;被盗用的身份通过策略处理 | 所有者必须发布的吊销证书 |
| 离线签名 | 不支持(需要 Fulcio 和 Rekor,公共或私有实例均可) | 支持 |
| 制品格式 | Sigstore 签名包(.sigstore.json):证书、签名或 DSSE 信封、日志证明 | .asc 或 .sig 文件 |
| 典型工具 | cosign、sigstore-python、sigstore-js、gitsign、GitHub attest-build-provenance | gpg、Sequoia(sq、sqv)、rpm 和 apt 工具链 |
| 在哪些地方是标准做法 | npm 来源证明(2023年9月正式发布)、PyPI 证明(2024年11月)、GitHub 制品证明(2024年6月正式发布)、CPython 自 3.11 起(从 3.14 起是唯一选项)、Kubernetes 以及许多容器镜像 | Maven Central(强制要求)、Debian 和 RPM 软件仓库、Linux 内核标签、许多源码压缩包 |
| 隐私 | 签名者身份(电子邮件或仓库)在日志中公开 | 只有签名者自己发布时,密钥和用户 ID 才会公开 |
无密钥签名包含什么
对于带来源证明发布的包,npm 会保存它们的 Sigstore 签名包。下面的脚本从公共注册表 API 获取 @noble/curves 2.3.0 的证明,并解码签名证书(2026年10月20日,curl、jq 1.8.1、OpenSSL 3.6.5)。
#!/bin/sh
curl -s "https://registry.npmjs.org/-/npm/v1/attestations/@noble%2fcurves@2.3.0" -o att.json
jq -r '.attestations[].predicateType' att.json
bundle='.attestations[] | select(.predicateType == "https://slsa.dev/provenance/v1") | .bundle'
jq -r "$bundle | .mediaType" att.json
jq -r "$bundle | .verificationMaterial.tlogEntries[0] | \"Rekor log index \(.logIndex), integrated \(.integratedTime | tonumber | todate)\"" att.json
jq -r "$bundle | .verificationMaterial.certificate.rawBytes" att.json | openssl base64 -d -A > leaf.der
openssl x509 -inform DER -in leaf.der -noout -issuer -dates -ext subjectAltName
openssl x509 -inform DER -in leaf.der -noout -text | grep -A1 "57264.1.1:" | tail -1 | sed 's/^ */OIDC issuer: /'
rm -f att.json leaf.der
https://github.com/npm/attestation/tree/main/specs/publish/v0.1
https://slsa.dev/provenance/v1
application/vnd.dev.sigstore.bundle.v0.3+json
Rekor log index 2357213216, integrated 2026-08-06T07:37:48Z
issuer=O=sigstore.dev, CN=sigstore-intermediate
notBefore=Aug 6 07:37:48 2026 GMT
notAfter=Aug 6 07:47:48 2026 GMT
X509v3 Subject Alternative Name: critical
URI:https://github.com/paulmillr/jsbt/.github/workflows/release.yml@5320e209ca8cc9b1b0b569675e164bdd2a139d71
OIDC issuer: https://token.actions.githubusercontent.com
这个包带有两份证明:npm 自己的发布证明,以及在 CI 中签名的 SLSA 来源证明声明。证书的有效期正好是 10 分钟,并在签发的同一秒记录到了 Rekor 中。证书的主体既不是某个人,也不是某个密钥,而是实际运行的工作流——这里是另一个仓库中固定到某个提交的共享发布工作流;Fulcio 的其他扩展还记录了源仓库、提交、标签和工作流运行。Sigstore 签名包验证工具和 npm 来源证明检测可以为任何包解码同样的数据。
验证方式有何不同
验证 GPG 签名只需一条命令:gpg --verify file.asc file,但结果意味着什么,取决于在它之前发生的一切:验证者是如何获得密钥的,指纹有没有对照可信来源核对过,以及密钥此后是否已被吊销。CI 流水线中的大多数“Good signature”结果,信任的不过是几行之前刚导入的某个密钥。
验证 Sigstore 签名时必须指明预期身份,这就把策略问题直接写进了命令。下面的脚本用官方的 cosign v3.1.3 容器镜像,对 cosign 自身 v3.1.3 版本的校验和文件检查三次:第一次对照 Sigstore 项目为其发布版本公布的身份(一个 Google 服务账号,颁发者为 https://accounts.google.com),第二次对照一个并未对该文件签名的 GitHub Actions 工作流身份,第三次在文件末尾追加一个字节之后再检查。
url=https://github.com/sigstore/cosign/releases/download/v3.1.3
curl -sSLO "$url/cosign_checksums.txt"
curl -sSLO "$url/cosign_checksums.txt.sigstore.json"
cosign() { docker run --rm -v "$PWD":/work:ro -w /work ghcr.io/sigstore/cosign/cosign:v3.1.3 "$@"; }
echo "== expected identity"
cosign verify-blob cosign_checksums.txt \
--bundle cosign_checksums.txt.sigstore.json \
--certificate-identity keyless@projectsigstore.iam.gserviceaccount.com \
--certificate-oidc-issuer https://accounts.google.com
echo "== any other identity"
cosign verify-blob cosign_checksums.txt \
--bundle cosign_checksums.txt.sigstore.json \
--certificate-identity "https://github.com/sigstore/cosign/.github/workflows/release.yml@refs/tags/v3.1.3" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
echo "== modified file"
printf 'x' >> cosign_checksums.txt
cosign verify-blob cosign_checksums.txt \
--bundle cosign_checksums.txt.sigstore.json \
--certificate-identity keyless@projectsigstore.iam.gserviceaccount.com \
--certificate-oidc-issuer https://accounts.google.com
== expected identity
Verified OK
== any other identity
Error: failed to verify certificate identity: no matching CertificateIdentity found, last error: expected SAN value "https://github.com/sigstore/cosign/.github/workflows/release.yml@refs/tags/v3.1.3", got "keyless@projectsigstore.iam.gserviceaccount.com"
error during command execution: failed to verify certificate identity: no matching CertificateIdentity found, last error: expected SAN value "https://github.com/sigstore/cosign/.github/workflows/release.yml@refs/tags/v3.1.3", got "keyless@projectsigstore.iam.gserviceaccount.com"
== modified file
Error: failed to verify signature: could not verify message: invalid signature when validating ASN.1 encoded signature
error during command execution: failed to verify signature: could not verify message: invalid signature when validating ASN.1 encoded signature
得出第一个结果时,cosign 检查了通向 Sigstore 根(通过 TUF 分发)的证书链、文件上的签名、Rekor 包含证明,以及日志时间是否落在证书 10 分钟的有效期内。第二次运行正是身份固定的意义所在:签名在密码学上有效,但它不是由验证者要求的身份生成的,因此被拒绝;第三次则是签名本身验证失败。对于在 GitHub Actions 中构建的制品,身份是工作流文件及其引用(ref),例如上面 npm 来源证明证书中的 release.yml@5320e209… 工作流,颁发者则是 https://token.actions.githubusercontent.com。
各个包生态系统把这些检查封装进了自己的工具。npm 会同时验证所有已安装包的注册表签名和 Sigstore 证明:
cd "$(mktemp -d)" && npm init -y > /dev/null
npm install --ignore-scripts --no-fund --no-audit @noble/curves@2.3.0 @noble/hashes@2.3.0
npm audit signatures
added 2 packages in 589ms
audited 2 packages in 0s
2 packages have verified registry signatures
2 packages have verified attestations
(use --json --include-attestations to view attestation details)
gh attestation verify 对 GitHub 制品证明做同样的检查;PyPI 则在证明上传时,对照项目的可信发布者检查它们。
需要了解的权衡
- 信任转移到了身份提供商和 Sigstore 的基础设施上:控制了维护者 GitHub 账号或发布工作流的攻击者,可以获得有效的签名。透明度让这种情况可以被察觉(维护者可以在日志中监控自己的身份),但并不能让它无法发生。
- 元数据公开:个人电子邮件身份会出现在公共日志中;工作流身份会暴露仓库名称。私有 Sigstore 部署可以同时避免这两点。
- 可用性:签名时需要 Fulcio 和 Rekor 在线。可信根缓存到本地之后,签名包的验证可以离线完成。
- 运维仍在演进:基于瓦片(tile)的日志 Rekor v2 自2025年10月起已正式发布,但在维护者推进后量子就绪工作期间,公共实例仍默认使用 Rekor v1。
- GPG 的成本来自人为因素:密钥会丢失,会在无人察觉时过期,或者从未被下游用户验证过——这正是 Python、npm 和 PyPI 弃用它的主要原因。
如何选择
- 在 GitHub Actions 或 GitLab CI 中为 npm、PyPI、crates 或容器构建的包:通过可信发布和来源证明使用 Sigstore。
- 容器镜像:cosign 签名和证明,由准入策略验证。
- Maven Central:OpenPGP 是强制要求;同时再附上 Sigstore 签名包,Central 自2025年1月起会验证它。
- Linux 发行版软件包和软件仓库:OpenPGP,这是发行版工具链所预期的方式。
- 物理隔离环境或离线签名:OpenPGP(或者使用自有身份提供商的私有 Sigstore 部署)。
- Git 提交和标签:SSH 或 OpenPGP 密钥,或者用 gitsign 对提交做无密钥签名(参见 SSH 与 GPG 提交签名对比)。
迁移说明
- 在 CI 中签名,而不是在笔记本电脑上签名:把发布流程迁移到能获得 OIDC 令牌的工作流(GitHub Actions 中的
id-token: write),并在那里签名。 - 过渡期内两种签名都发布:同时提供
.asc和.sigstore.json文件,并在旧密钥指纹旁边注明预期的身份和颁发者。 - 使用注册表原生机制(如有):npm 的
--provenance和可信发布、PyPI 可信发布者、GitHub 制品证明。 - 在验证策略中精确固定身份:仓库、工作流文件以及标签或分支,而不只是组织。
- 监控日志中与你的身份相关的条目,并保留 OpenPGP 密钥的吊销证书,直到用它签名的最后一个版本不再受支持。