Pular para o conteúdo
ecdsa.com
← Todos os artigos

Assinar releases com Sigstore: cosign, bundles, identidade

O Sigstore substitui as chaves de assinatura de longa duração por um certificado de dez minutos vinculado a um fluxo de trabalho de CI ou a um e-mail, mais uma entrada em um log público. Assinar é fácil; o que as pessoas erram é a verificação. Aqui está o fluxo completo, verificado com releases reais em 20 de outubro de 2026.

Resposta curta. Na assinatura “keyless” (sem chave) do Sigstore, o signatário comprova uma identidade com um token OIDC (um fluxo de trabalho do GitHub Actions, uma conta Google ou Microsoft). O Fulcio emite um certificado para uma chave efêmera, válido por cerca de dez minutos, com essa identidade dentro. A assinatura é registrada no log de transparência público Rekor, e tudo o que a verificação exige acaba em um bundle (.sigstore.json). A verificação precisa conferir quem assinou, e não apenas se a assinatura é válida. No cosign, isso significa --certificate-identity e --certificate-oidc-issuer, que o cosign exige no modo keyless. A proveniência do npm e os atestados de artefatos do GitHub usam o mesmo mecanismo: npm audit signatures e gh attestation verify fazem essa verificação por você.

Por que assinar no modo keyless

A assinatura clássica de releases com GPG ou com uma chave de assinatura de código de longa duração tem dois problemas crônicos. Os usuários precisam saber qual é a chave certa, então a distribuição de chaves é o verdadeiro problema de confiança. E a chave precisa ficar guardada em algum lugar por anos, em geral em um segredo do CI, de onde pode vazar. O Sigstore substitui a chave por uma identidade que as pessoas já entendem, como “o fluxo de trabalho de release do repositório X” ou “alice@example.com”. E substitui a vida útil da chave por uma entrada de log que prova quando a assinatura foi feita.

O Sigstore é formado por serviços públicos mantidos pela comunidade OpenSSF (a instância pública, a “public-good instance”) e por ferramentas cliente:

  • O Fulcio é uma autoridade certificadora que emite certificados X.509 de curta duração para identidades OIDC.
  • O Rekor é o log de transparência de assinaturas e atestados.
  • Os metadados TUF distribuem aos clientes as raízes de confiança (a CA do Fulcio, a chave do Rekor, as autoridades de carimbo do tempo).
  • O cosign é a CLI para blobs e imagens de contêiner. As bibliotecas sigstore-python, sigstore-js, sigstore-go e sigstore-java fazem as mesmas tarefas.

O que acontece quando você assina

  1. O cliente cria um par de chaves efêmero na memória.
  2. Ele obtém um token OIDC: no GitHub Actions, do runner (id-token: write); em um notebook, por meio de um login no navegador.
  3. O Fulcio verifica o token e emite um certificado que vincula a chave pública à identidade: o e-mail ou o URI do fluxo de trabalho vai para o Subject Alternative Name; o emissor e os detalhes do CI, para extensões.
  4. O cliente assina o digest do artefato e envia a assinatura e o certificado ao Rekor, que devolve uma entrada de log com uma prova de inclusão e um carimbo do tempo assinado.
  5. A chave privada efêmera é descartada. O bundle guarda o certificado, a assinatura e as evidências do log.

O certificado do bundle que o cosign publica com a própria release v3.1.3 mostra como essa validade é curta:

openssl x509 aplicado ao certificado dentro de cosign-linux-arm64.sigstore.json
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

Dez minutos de validade. A assinatura continua verificável depois disso, porque a entrada no Rekor prova que ela foi feita dentro dessa janela. O integratedTime dessa entrada no Rekor é 2026-08-06 01:03:25 UTC.

O bundle

Um bundle é um documento JSON que contém tudo de que um verificador precisa, exceto o artefato e as raízes de confiança. Este é o bundle da release do cosign, resumido:

cosign-linux-arm64.sigstore.json (arquivo da release v3.1.3, valores encurtados)
{
  "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 é o certificado folha do Fulcio, com a identidade.
  • tlogEntries contém a entrada do Rekor: uma prova de inclusão em relação a um checkpoint assinado e um carimbo do tempo assinado da entrada (signed entry timestamp). Com eles, o verificador não precisa consultar o Rekor em momento algum.
  • timestampVerificationData contém carimbos do tempo RFC 3161 opcionais, emitidos por uma autoridade de carimbo do tempo.
  • O conteúdo é messageSignature, para uma assinatura simples de blob, ou dsseEnvelope, para um atestado — uma declaração in-toto assinada.

O verificador de bundles do Sigstore confere todas essas partes no navegador e mostra a identidade do certificado, que é a primeira coisa a olhar quando uma verificação falha.

Verificação: o que importa é a identidade

Veja o cosign verificando o binário da própria release (ghcr.io/sigstore/cosign/cosign:v3.1.3, 20 de outubro de 2026). A release é gerada na infraestrutura do Google, então a identidade é uma conta de serviço do Google e o emissor é o Google, não o GitHub:

cosign verify-blob: a identidade esperada
$ 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

Agora, os casos de falha. Cada um tem uma mensagem que diz exatamente qual verificação falhou:

cosign verify-blob: como é uma falha
--- 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

O terceiro caso é uma escolha deliberada de design. Qualquer pessoa pode obter um certificado do Fulcio para a própria identidade e assinar qualquer coisa. Uma assinatura só tem significado depois que você confere se a identidade é a que você espera. O cosign se recusa a verificar assinaturas keyless sem uma identidade (página de erro). O último caso é o que uma adulteração produz (página de erro).

Onde conseguir a identidade esperada? Na documentação do projeto, ou lendo-a uma vez em um bundle em que você confia e fixando-a no seu script de verificação. A opção --certificate-identity-regexp é útil para tags (por exemplo, ...release.yml@refs/tags/v.*), mas ancore a expressão nas duas extremidades. Um padrão como .*sigstore.* corresponde a identidades que qualquer pessoa pode criar.

Assinatura no GitHub Actions

No CI, a identidade é o fluxo de trabalho. O Fulcio coloca o arquivo do fluxo de trabalho e a ref no SAN, e os detalhes em extensões. O bundle de proveniência npm de sigstore@5.0.0 mostra o que um certificado do GitHub Actions carrega, decodificado das extensões X.509 com um pequeno script:

Extensões do Fulcio no certificado de proveniência npm de sigstore@5.0.0
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

O cosign também pode verificar essas claims, além do SAN:

cosign verify-blob-attestation com claims do 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

Para assinar as próprias releases no GitHub Actions, o job precisa de permissão para solicitar um token OIDC. O README do actions/attest, do GitHub, traz esta configuração mínima para a proveniência de build de um arquivo:

GitHub Actions: proveniência de build de um arquivo de release (do README de actions/attest)
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'

Repositórios públicos usam a instância pública do Sigstore. Repositórios privados usam a instância própria do GitHub, que exige o GitHub Enterprise Cloud. O actions/attest-build-provenance continua funcionando, mas, desde a v4, é só um wrapper em torno do actions/attest. Para assinar um blob com o cosign diretamente no job, o comando é cosign sign-blob --bundle app.sigstore.json app.tar.gz --yes. A ajuda da opção --oidc-provider do cosign lista github-actions entre as fontes de token que ele tenta quando nenhuma é definida. Não fizemos uma assinatura keyless para esta página, porque toda assinatura keyless na instância pública vira uma entrada de log permanente e pública.

Uma assinatura feita por um fluxo de trabalho só é tão confiável quanto o próprio fluxo de trabalho. Qualquer pessoa que consiga fazer push para o branch, ou disparar o fluxo de trabalho, consegue produzir uma assinatura válida com essa identidade. Proteja os fluxos de trabalho de release com proteção de branches e de tags, revisões obrigatórias e aprovações de ambiente, e fixe a verificação em uma ref de tag (@refs/tags/...) quando você publicar releases a partir de tags.

Proveniência do npm

O npm publica dois tipos de evidência do Sigstore para pacotes gerados em um CI compatível: um atestado de publicação, assinado pelo registro, e um atestado de proveniência SLSA, assinado pelo build. O npm audit signatures verifica os dois para tudo o que está instalado:

npm 11.19.1: npm audit signatures (sigstore@5.0.0 e semver@7.7.2 com dependências)
$ 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)

Os atestados de uma versão são públicos, em https://registry.npmjs.org/-/npm/v1/attestations/<name>@<version>. Para sigstore@5.0.0:

Atestados de sigstore@5.0.0, decodificados
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

O digest do subject é o SHA-512 do tarball (849a897e...), então o bundle de proveniência pode ser verificado com o cosign em relação ao .tgz baixado, como mostrado acima. Com a publicação confiável (trusted publishing) do npm (npm CLI 11.5.1 ou posterior), os pacotes publicados a partir do GitHub Actions ou do GitLab CI via OIDC recebem proveniência automaticamente, sem a flag --provenance e sem um token npm de longa duração. O CircleCI tem suporte para a publicação confiável, mas ainda não para a proveniência. Fora de um CI compatível, npm publish --provenance falha com Automatic provenance generation not supported for provider: null. O verificador de proveniência do npm mostra o repositório, o fluxo de trabalho e o commit de qualquer versão de pacote.

Atestados de artefatos do GitHub

O GitHub armazena atestados para os arquivos das releases e os disponibiliza pela própria API. O tarball de release da CLI gh, gh_2.102.0_linux_arm64.tar.gz (SHA-256 7862c86c...), tinha dois atestados em 20 de outubro de 2026: a proveniência SLSA do fluxo de trabalho de implantação do cli/cli e um atestado de release assinado pelo próprio GitHub. Com o bundle de proveniência salvo localmente, o gh attestation verify funciona sem login e imprime a política que aplicou:

gh 2.102.0: gh attestation verify com um bundle local (sem cores)
$ 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

Com --repo example/cli, a mesma verificação termina em ✗ Sigstore verification failed. A política padrão (proprietário, repositório, prefixo do SAN) é um bom começo. Para automatizar releases, acrescente --signer-workflow para fixar o arquivo exato do fluxo de trabalho, e --deny-self-hosted-runners se seus builds rodam só em runners hospedados pelo GitHub. O mesmo bundle também passa na verificação do cosign: cosign verify-blob-attestation --type slsaprovenance1, com o SAN https://github.com/cli/cli/.github/workflows/deployment.yml@refs/heads/trunk, imprimiu Verified OK.

O segundo atestado é diferente. O tipo de predicado dele é https://in-toto.io/attestation/release/v0.2. O certificado nomeia URI:https://dotcom.releases.github.com e foi emitido por O=GitHub, Inc., CN=Fulcio Intermediate l1, o Fulcio próprio do GitHub, com um carimbo do tempo RFC 3161 em vez de uma entrada no Rekor. O GitHub o cria para repositórios com releases imutáveis ativadas. Ele prova que o arquivo pertence à release, e não como ele foi gerado. Os comandos gh release verify e gh release verify-asset fazem essa verificação. No nosso teste, o gh release verify-asset precisou de um gh com login feito: sem token, o gh 2.102.0 parou com To get started with GitHub CLI, please run: gh auth login.

Quando o keyless não serve

Builds em ambiente isolado (air-gapped), artefatos privados que não devem aparecer em um log público ou políticas que exigem uma chave em um HSM: o cosign também assina com chaves, e o envio ao log pode ser desligado. Testado com o cosign v3.1.3:

cosign com um par de chaves e sem log de transparência
$ 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

Nesse caso, o bundle contém só uma indicação publicKey, e você volta a distribuir e proteger uma chave de longa duração — justamente o problema que a assinatura keyless elimina. Para uma configuração privada, mas ainda auditável, rode sua própria instância do Sigstore (Fulcio, Rekor, TUF) ou use a de um fornecedor. O GitHub faz isso para repositórios privados.

O estado da infraestrutura em 2026

  • O Rekor v2, um log baseado em tiles, chegou à disponibilidade geral em outubro de 2025. Os verificadores precisam do cosign 2.6.0 ou 3.0.1, ou de uma versão posterior. O Rekor v2 abandonou o índice de busca.
  • Em junho de 2026, os mantenedores anunciaram que a instância pública “continuará usando o Rekor v1 como log padrão no futuro previsível”. O Rekor v2 continua em produção para os ecossistemas que optarem por ele.
  • Um Sigstore pós-quântico separado, com ML-DSA, tem “previsão para meados de 2027”, segundo o anúncio de outubro de 2026. A instância clássica deve continuar funcionando até uma desativação “provisoriamente programada para 2029 ou 2030”. De um jeito ou de outro, o formato continua sendo o bundle.

Do lado da verificação, quase nada muda: mantenha os clientes atualizados, verifique a partir de bundles e fixe identidades, não URLs de log.

Erros comuns

  • Verificar a assinatura, mas não a identidade. Uma assinatura válida do Sigstore só diz que alguém assinou.
  • Padrões de identidade amplos demais. Expressões regulares sem âncoras, ou uma verificação do emissor sem verificação do SAN.
  • Fixar em um branch quando você publica releases a partir de tags. @refs/heads/main aceita qualquer coisa gerada a partir do main, inclusive um build que ninguém pretendia publicar.
  • Supor que a assinatura prova o build. Uma assinatura de blob prova quem assinou os bytes. A proveniência (SLSA, in-toto) diz como e a partir de quê eles foram gerados. Verifique aquela que sua política exige.
  • Assinar releases pessoais com um e-mail pessoal sem perceber que o e-mail fica no certificado e, portanto, em um log público e permanente.
  • Distribuir os arquivos desanexados .sig e .pem das versões antigas do cosign. Publique o bundle .sigstore.json. É o formato que todos os verificadores atuais entendem.

Fontes

A verificação foi feita em 20 de outubro de 2026 em ghcr.io/sigstore/cosign/cosign:v3.1.3, com o npm 11.19.1 e o gh 2.102.0 (o tarball oficial da release) em node:24.22.0-trixie.