简短回答:使用 Sigstore 的“无密钥”(keyless)签名时,签名者用 OIDC 令牌(GitHub Actions 工作流、Google 或 Microsoft 账号)证明自己的身份。Fulcio 为临时密钥签发一张证书,有效期约 10 分钟,证书中写有这个身份。签名记录在公开的 Rekor 透明日志中,验证所需的一切最终都放进一个签名包(.sigstore.json)。验证时必须检查签名者是谁,而不只是检查签名是否有效。在 cosign 中,这对应 --certificate-identity 和 --certificate-oidc-issuer,在无密钥模式下 cosign 要求必须提供这两个参数。npm 来源证明和 GitHub 制品证明用的是同一套机制:npm audit signatures 和 gh attestation verify 会替你检查它们。
为什么用无密钥签名
用 GPG 或长期有效的代码签名密钥为发布版本签名,这种传统做法有两个长期存在的问题。用户必须知道哪个密钥才是正确的,所以密钥分发才是真正的信任难题。而且密钥必须在某个地方保存多年,通常是 CI 机密,存放在那里就可能泄露。Sigstore 用人们已经能理解的身份取代密钥,例如“仓库 X 的发布工作流”或“alice@example.com”。它还用一条日志条目取代密钥的有效期,这条日志条目证明签名是在什么时候生成的。
Sigstore 由 OpenSSF 社区运营的公共服务(“公共实例”,public-good instance)和客户端工具组成:
- Fulcio 是一个证书颁发机构,为 OIDC 身份签发短期有效的 X.509 证书。
- Rekor 是记录签名和证明的透明日志。
- TUF 元数据把信任根(Fulcio 的 CA、Rekor 的密钥、时间戳机构)分发给客户端。
- cosign 是处理任意文件(blob)和容器镜像的 CLI。sigstore-python、sigstore-js、sigstore-go 和 sigstore-java 是完成同样任务的库。
签名时发生了什么
- 客户端在内存中创建临时密钥对。
- 客户端获取 OIDC 令牌:在 GitHub Actions 中从运行器获取(
id-token: write),在笔记本电脑上则通过浏览器登录获取。 - Fulcio 检查令牌,并签发一张把公钥与身份绑定的证书:电子邮件地址或工作流 URI 写入主题备用名称(SAN),颁发者和 CI 详细信息写入扩展。
- 客户端对制品的摘要签名,并把签名和证书提交给 Rekor;Rekor 返回一条日志条目,附带包含证明(inclusion proof)和带签名的时间戳。
- 临时私钥随即丢弃。签名包保存证书、签名和日志证据。
cosign 在自己的 v3.1.3 版本中附带了签名包,从其中的证书可以看出它的有效期有多短:
issuer=O=sigstore.dev, CN=sigstore-intermediate
notBefore=Aug 6 01:03:25 2026 GMT
notAfter=Aug 6 01:13:25 2026 GMT
X509v3 Extended Key Usage:
Code Signing
X509v3 Subject Alternative Name: critical
email:keyless@projectsigstore.iam.gserviceaccount.com
1.3.6.1.4.1.57264.1.1:
https://accounts.google.com
有效期只有 10 分钟。过期之后签名仍然可以验证,因为 Rekor 条目证明了签名是在这个时间窗口内生成的。这个条目的 Rekor integratedTime 为2026年8月6日 01:03:25(UTC)。
签名包
签名包是一个 JSON 文档,包含验证方所需的一切,只有制品和信任根除外。下面是 cosign 发布版本的签名包(有删节):
{
"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
"verificationMaterial": {
"certificate": { "rawBytes": "MIIDDzCCApWgAwIBAgIUavXJ..." },
"tlogEntries": [
{
"logIndex": "2352242192",
"kindVersion": { "kind": "hashedrekord", "version": "0.0.1" },
"integratedTime": "1785978205",
"inclusionPromise": { "signedEntryTimestamp": "MEYCIQCvddr55BoX..." },
"inclusionProof": { "logIndex": "2230337930", "treeSize": "2230337938", "rootHash": "62m7TRzwvcKyu+r6...", "hashes": ["..."], "checkpoint": { "envelope": "..." } },
"canonicalizedBody": "eyJhcGlWZXJzaW9u..."
}
],
"timestampVerificationData": { "rfc3161Timestamps": ["..."] }
},
"messageSignature": {
"messageDigest": { "algorithm": "SHA2_256", "digest": "xdMk4JGCaw16eOsW..." },
"signature": "MEUCIECQp5FyO00l..."
}
}
certificate是 Fulcio 签发的叶子证书,其中包含身份。tlogEntries保存 Rekor 条目:一份针对已签名检查点的包含证明,以及一个已签名的条目时间戳。有了这些,验证方完全不需要联系 Rekor。timestampVerificationData保存来自时间戳机构的 RFC 3161 时间戳(可选)。- 内容部分要么是
messageSignature,用于普通的 blob 签名;要么是dsseEnvelope,用于证明,即经过签名的 in-toto 声明。
Sigstore 签名包验证工具在浏览器中检查所有这些部分,并显示证书中的身份——验证失败时,首先要看的就是它。
验证:关键在于身份
下面用 cosign 验证它自己的发布二进制文件(ghcr.io/sigstore/cosign/cosign:v3.1.3,2026年10月20日)。这个版本在 Google 的基础设施上构建,因此身份是一个 Google 服务账号,颁发者是 Google,而不是 GitHub:
$ cosign verify-blob cosign-linux-arm64 --bundle cosign-linux-arm64.sigstore.json \
--certificate-identity keyless@projectsigstore.iam.gserviceaccount.com \
--certificate-oidc-issuer https://accounts.google.com
Verified OK
下面是失败的情况。每种情况都有一条消息,准确说明是哪项检查未通过:
--- right email, wrong issuer
error during command execution: failed to verify certificate identity: no matching CertificateIdentity found, last error: expected issuer value "https://token.actions.githubusercontent.com", got "https://accounts.google.com"
--- a different identity
error during command execution: failed to verify certificate identity: no matching CertificateIdentity found, last error: expected SAN value "release@example.com", got "keyless@projectsigstore.iam.gserviceaccount.com"
--- no identity at all
error during command execution: --certificate-identity or --certificate-identity-regexp is required for verification in keyless mode
--- a modified binary
error during command execution: failed to verify signature: could not verify message: invalid signature when validating ASN.1 encoded signature
第三种情况是有意为之的设计。任何人都可以为自己的身份获取 Fulcio 证书,并用它对任何内容签名。只有确认了身份正是你预期的那个,签名才有意义。没有指定身份时,cosign 拒绝验证无密钥签名(错误页面)。最后一种情况就是内容被篡改时的样子(错误页面)。
预期的身份从哪里来?从项目文档中获取,或者从一个你信任的签名包中读取一次,再把它固定写进验证脚本。--certificate-identity-regexp 适合匹配标签(例如 ...release.yml@refs/tags/v.*),但两端都要加锚点。像 .*sigstore.* 这样的模式会匹配任何人都能创建的身份。
在 GitHub Actions 中签名
在 CI 中,身份就是工作流。Fulcio 把工作流文件和引用(ref)写入 SAN,把其余细节写入扩展。sigstore@5.0.0 的 npm 来源证明签名包展示了 GitHub Actions 证书携带的内容,下面是用一个小脚本从它的 X.509 扩展中解码出来的结果:
1.3.6.1.4.1.57264.1.8 Issuer (V2) https://token.actions.githubusercontent.com
1.3.6.1.4.1.57264.1.9 Build Signer URI https://github.com/sigstore/sigstore-js/.github/workflows/release.yml@refs/heads/main
1.3.6.1.4.1.57264.1.12 Source Repository URI https://github.com/sigstore/sigstore-js
1.3.6.1.4.1.57264.1.13 Source Repository Digest 7d2900eca1c22b3f87c13987c8d4b7c9a29b733a
1.3.6.1.4.1.57264.1.14 Source Repository Ref refs/heads/main
1.3.6.1.4.1.57264.1.18 Build Config URI https://github.com/sigstore/sigstore-js/.github/workflows/release.yml@refs/heads/main
1.3.6.1.4.1.57264.1.20 Build Trigger push
1.3.6.1.4.1.57264.1.21 Run Invocation URI https://github.com/sigstore/sigstore-js/actions/runs/26781866618/attempts/1
除了 SAN,cosign 也能检查这些声明:
$ cosign verify-blob-attestation --bundle provenance.sigstore.json --type slsaprovenance1 \
--certificate-identity-regexp '^https://github\.com/sigstore/sigstore-js/\.github/workflows/release\.yml@' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-github-workflow-repository sigstore/sigstore-js \
--certificate-github-workflow-trigger push \
sigstore-5.0.0.tgz
Verified OK
要在 GitHub Actions 中为自己的发布版本签名,作业需要有申请 OIDC 令牌的权限。GitHub 的 actions/attest README 给出了为单个文件生成构建来源证明的最小配置:
permissions:
id-token: write
attestations: write
artifact-metadata: write
steps:
# ... build steps that produce dist/app.tar.gz ...
- uses: actions/attest@v4
with:
subject-path: 'dist/app.tar.gz'
公共仓库使用 Sigstore 公共实例。私有仓库使用 GitHub 自己的实例,这需要 GitHub Enterprise Cloud。actions/attest-build-provenance 仍然可用,但从 v4 起它只是 actions/attest 的一层封装。要在作业中直接用 cosign 对 blob 签名,命令是 cosign sign-blob --bundle app.sigstore.json app.tar.gz --yes。cosign 中 --oidc-provider 的帮助说明把 github-actions 列为未设置该选项时会尝试的令牌来源之一。我们没有为本文执行无密钥签名,因为在公共实例上,每个无密钥签名都会成为一条永久的公开日志条目。
来自工作流的签名,可信程度取决于工作流本身。任何能向该分支推送或能触发该工作流的人,都能以这个身份生成有效签名。要用分支和标签保护、必需的审查以及环境审批来保护发布工作流;如果你从标签发布,还要把验证固定到标签引用(@refs/tags/...)。
npm 来源证明
对于在受支持的 CI 中构建的包,npm 会发布两类 Sigstore 证据:由注册表签名的发布证明,以及由构建过程签名的 SLSA 来源证明。npm audit signatures 会对所有已安装的包检查这两者:
$ npm audit signatures
audited 52 packages in 2s
52 packages have verified registry signatures
26 packages have verified attestations
(use --json --include-attestations to view attestation details)
每个版本的证明都是公开的,地址为 https://registry.npmjs.org/-/npm/v1/attestations/<name>@<version>。以 sigstore@5.0.0 为例:
https://github.com/npm/attestation/tree/main/specs/publish/v0.1 | bundle application/vnd.dev.sigstore.bundle+json;version=0.2
subject: pkg:npm/sigstore@5.0.0 849a897e81bf7b8a8541a6ae...
https://slsa.dev/provenance/v1 | bundle application/vnd.dev.sigstore.bundle.v0.3+json
subject: pkg:npm/sigstore@5.0.0 849a897e81bf7b8a8541a6ae...
workflow: https://github.com/sigstore/sigstore-js .github/workflows/release.yml refs/heads/main
builder: https://github.com/actions/runner/github-hosted
commit: 7d2900eca1c22b3f87c13987c8d4b7c9a29b733a
主体摘要是 tarball 的 SHA-512(849a897e...),因此可以像上文那样,用 cosign 对照下载的 .tgz 验证这个来源证明签名包。使用 npm 的可信发布(npm CLI 11.5.1 或更高版本)时,通过 OIDC 从 GitHub Actions 或 GitLab CI 发布的包会自动获得来源证明,既不需要 --provenance 参数,也不需要长期有效的 npm 令牌。CircleCI 已支持可信发布,但尚不支持来源证明。在受支持的 CI 之外,npm publish --provenance 会报错:Automatic provenance generation not supported for provider: null。npm 来源证明检测可以显示任意包版本对应的仓库、工作流和提交。
GitHub 制品证明
GitHub 为发布资产保存证明,并通过其 API 提供。gh CLI 的发布 tarball gh_2.102.0_linux_arm64.tar.gz(SHA-256 7862c86c...)在2026年10月20日有两份证明:一份是来自 cli/cli 部署工作流的 SLSA 来源证明,另一份是由 GitHub 自己签名的发布版本证明。把来源证明签名包保存在本地后,gh attestation verify 无需登录即可运行,并会打印它所执行的策略:
$ gh attestation verify gh_2.102.0_linux_arm64.tar.gz --repo cli/cli --bundle gh-1.sigstore.json
Loaded digest sha256:7862c86c72f43df3a2d93ddde6f473285b4e2af61b494849846827e513ef6484 for file:///d/gh_2.102.0_linux_arm64.tar.gz
Loaded 1 attestations from /d/gh-1.sigstore.json
The following policy criteria will be enforced:
- Predicate type must match:................ https://slsa.dev/provenance/v1
- Source Repository Owner URI must match:... https://github.com/cli
- Source Repository URI must match:......... https://github.com/cli/cli
- Subject Alternative Name must match regex: (?i)^https://github\.com/cli/cli/
- OIDC Issuer must match:................... https://token.actions.githubusercontent.com
✓ Verification succeeded!
The following 1 attestation matched the policy criteria
- Attestation #1
- Build repo:..... cli/cli
- Build workflow:. .github/workflows/deployment.yml@refs/heads/trunk
- Signer repo:.... cli/cli
- Signer workflow: .github/workflows/deployment.yml@refs/heads/trunk
换成 --repo example/cli 后,同样的检查会以 ✗ Sigstore verification failed 结束。默认策略(所有者、仓库、SAN 前缀)是个不错的起点。对于发布自动化,还应加上 --signer-workflow 来固定确切的工作流文件;如果你的构建只在 GitHub 托管的运行器上运行,再加上 --deny-self-hosted-runners。同一个签名包也能用 cosign 验证:以 SAN https://github.com/cli/cli/.github/workflows/deployment.yml@refs/heads/trunk 运行 cosign verify-blob-attestation --type slsaprovenance1,输出为 Verified OK。
第二份证明则不同。它的谓词类型是 https://in-toto.io/attestation/release/v0.2。证书中的名称是 URI:https://dotcom.releases.github.com,由 GitHub 自己的 Fulcio(O=GitHub, Inc., CN=Fulcio Intermediate l1)签发,并且附带的是 RFC 3161 时间戳,而不是 Rekor 条目。GitHub 会为启用了不可变发布版本的仓库创建这份证明。它证明的是该资产属于这个发布版本,而不是它是如何构建的。gh release verify 和 gh release verify-asset 可以检查它。在我们的测试中,gh release verify-asset 需要已登录的 gh:没有令牌时,gh 2.102.0 会停止并输出 To get started with GitHub CLI, please run: gh auth login。
不适合无密钥签名的场景
物理隔离(air-gapped)的构建、不得出现在公开日志中的私有制品,或要求密钥存放在 HSM 中的策略:对于这些情况,cosign 也支持用密钥签名,并且可以关闭日志上传。以下用 cosign v3.1.3 测试:
$ cosign generate-key-pair
Private key written to cosign.key
Public key written to cosign.pub
$ cosign sign-blob --key cosign.key --tlog-upload=false --use-signing-config=false --bundle app.sigstore.json app-1.0.0.tar.gz --yes
Using payload from: app-1.0.0.tar.gz
Signing artifact...
Wrote bundle to file app.sigstore.json
$ cosign verify-blob --key cosign.pub --bundle app.sigstore.json --insecure-ignore-tlog=true app-1.0.0.tar.gz
WARNING: Skipping tlog verification is an insecure practice that lacks transparency and auditability verification for the blob.
Verified OK
$ cosign verify-blob --key cosign.pub --bundle app.sigstore.json app-1.0.0.tar.gz
error during command execution: failed to verify log inclusion: not enough verified log entries from transparency log: 0 < 1
这时签名包中只有一个 publicKey 提示,你又回到了分发和保护长期有效密钥的老路上——而这正是无密钥签名要消除的问题。如果需要既私有、又仍可审计的方案,可以运行自己的 Sigstore 实例(Fulcio、Rekor、TUF),或使用供应商提供的实例。GitHub 为私有仓库就是这么做的。
2026年的基础设施现状
- Rekor v2 是瓦片式(tile-based)日志,已于2025年10月正式可用(GA)。验证方需要 cosign 2.6.0 或 3.0.1 及更高版本。Rekor v2 去掉了搜索索引。
- 2026年6月,维护者宣布,公共实例“在可预见的未来将继续使用 Rekor v1 作为默认日志”。对于选择加入的生态系统,Rekor v2 仍在生产环境中运行。
- 根据2026年10月的公告,使用 ML-DSA 的独立后量子 Sigstore“目标是在2027年年中推出”。经典实例将继续运行,直到下线,下线时间“暂定于2029年或2030年”。无论哪种情况,格式都仍然是签名包。
验证这一侧几乎不受影响:保持客户端为最新版本,基于签名包验证,并固定身份,而不是日志 URL。
常见错误
- 只检查签名,不检查身份。有效的 Sigstore 签名只能说明某个人签了名。
- 身份模式过于宽泛。没有锚定的正则表达式,或者只检查颁发者而不检查 SAN。
- 从标签发布,却固定到分支。
@refs/heads/main会接受从main构建出来的任何东西,包括没有人打算发布的构建。 - 以为签名能证明构建过程。对 blob 的签名只证明谁对这些字节签了名。来源证明(SLSA、in-toto)说明的是它们如何构建、由什么构建而来。按你的策略需要,验证相应的那一种。
- 用个人电子邮件地址为个人发布版本签名,却没有意识到这个电子邮件地址会写进证书,因而也会进入一个公开、永久的日志。
- 仍在分发旧版 cosign 生成的分离式
.sig和.pem文件。请改为发布.sigstore.json签名包。这是当前所有验证方都能识别的格式。
参考资料
- Sigstore 文档以及签名包格式。
- cosign v3.1.3 及其发布版本中的签名包。
- Sigstore 博客:Rekor v2 正式可用(2025年10月)、sigstore.dev 与 Rekor 的演进(2026年6月)、Sigstore 的后量子密码(2026年10月)。
- npm:可信发布以及注册表的证明端点。
- GitHub:
actions/attest、gh attestation verify、gh release verify,以及 in-toto 的 release 谓词。
验证于2026年10月20日在 ghcr.io/sigstore/cosign/cosign:v3.1.3 中运行;npm 11.19.1 和 gh 2.102.0(官方发布 tarball)则在 node:24.22.0-trixie 中运行。