跳到主要内容
ecdsa.com
← 全部文章

用 Sigstore 为发布版本签名:cosign、签名包与身份

Sigstore 用一张与 CI 工作流或电子邮件地址绑定、有效期 10 分钟的证书,加上一条公开的日志条目,取代长期有效的签名密钥。签名很简单,容易出错的是验证。下面是完整流程,均已于2026年10月20日对照真实的发布版本验证。

简短回答:使用 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 是完成同样任务的库。

签名时发生了什么

  1. 客户端在内存中创建临时密钥对。
  2. 客户端获取 OIDC 令牌:在 GitHub Actions 中从运行器获取(id-token: write),在笔记本电脑上则通过浏览器登录获取。
  3. Fulcio 检查令牌,并签发一张把公钥与身份绑定的证书:电子邮件地址或工作流 URI 写入主题备用名称(SAN),颁发者和 CI 详细信息写入扩展。
  4. 客户端对制品的摘要签名,并把签名和证书提交给 Rekor;Rekor 返回一条日志条目,附带包含证明(inclusion proof)和带签名的时间戳。
  5. 临时私钥随即丢弃。签名包保存证书、签名和日志证据。

cosign 在自己的 v3.1.3 版本中附带了签名包,从其中的证书可以看出它的有效期有多短:

对 cosign-linux-arm64.sigstore.json 中的证书运行 openssl x509
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 发布版本的签名包(有删节):

cosign-linux-arm64.sigstore.json(v3.1.3 发布资产,值已缩短)
{
  "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 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

下面是失败的情况。每种情况都有一条消息,准确说明是哪项检查未通过:

cosign verify-blob:失败时的输出
--- 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 扩展中解码出来的结果:

sigstore@5.0.0 的 npm 来源证明证书中的 Fulcio 扩展
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 检查 GitHub 声明(cosign v3.1.3)
$ 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 给出了为单个文件生成构建来源证明的最小配置:

GitHub Actions:为发布文件生成构建来源证明(摘自 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 11.19.1:npm audit signatures(sigstore@5.0.0 和 semver@7.7.2 及其依赖)
$ 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 为例:

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 2.102.0:用本地签名包运行 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 使用密钥对、不使用透明日志
$ 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 签名包。这是当前所有验证方都能识别的格式。

参考资料

验证于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 中运行。