Saltar al contenido
ecdsa.com
← Todos los artículos

Firmar versiones con Sigstore: cosign, bundles e identidad

Sigstore sustituye las claves de firma de larga duración por un certificado de diez minutos vinculado a un flujo de trabajo de CI o a un correo electrónico, más una entrada en un registro público. Firmar es lo fácil; donde se suele fallar es en la verificación. Este es el flujo completo, comprobado con versiones reales el 20 de octubre de 2026.

Respuesta breve. Con la firma «keyless» de Sigstore, el firmante demuestra una identidad con un token OIDC (un flujo de trabajo de GitHub Actions, una cuenta de Google o de Microsoft). Fulcio emite un certificado para una clave efímera, válido durante unos diez minutos, que contiene esa identidad. La firma se anota en el registro de transparencia público Rekor, y todo lo necesario para verificarla acaba en un bundle (.sigstore.json). La verificación tiene que comprobar quién firmó, no solo si la firma es válida. En cosign eso significa --certificate-identity y --certificate-oidc-issuer, que cosign exige en modo keyless. npm provenance y las atestaciones de artefactos de GitHub usan el mismo mecanismo: npm audit signatures y gh attestation verify las comprueban por ti.

Por qué la firma keyless

La firma clásica de versiones con GPG o con una clave de firma de código de larga duración tiene dos problemas crónicos. Los usuarios tienen que saber cuál es la clave correcta, así que la distribución de claves es el verdadero problema de confianza. Y la clave hay que guardarla en algún sitio durante años, normalmente en un secreto de la CI, donde puede filtrarse. Sigstore sustituye la clave por una identidad que la gente ya entiende, como «el flujo de trabajo de publicación del repositorio X» o «alice@example.com». Y sustituye la vida útil de la clave por una entrada en el registro que demuestra cuándo se hizo la firma.

Sigstore se compone de servicios públicos que gestiona la comunidad OpenSSF (la «instancia pública», public-good instance) y de herramientas cliente:

  • Fulcio es una autoridad de certificación que emite certificados X.509 de corta duración para identidades OIDC.
  • Rekor es el registro de transparencia de firmas y atestaciones.
  • Los metadatos de TUF distribuyen a los clientes las raíces de confianza (la CA de Fulcio, la clave de Rekor, las autoridades de sellado de tiempo).
  • cosign es la CLI para blobs e imágenes de contenedor. sigstore-python, sigstore-js, sigstore-go y sigstore-java son bibliotecas para las mismas tareas.

Qué ocurre al firmar

  1. El cliente crea en memoria un par de claves efímero.
  2. Obtiene un token OIDC: en GitHub Actions, del runner (id-token: write); en un equipo personal, mediante un inicio de sesión en el navegador.
  3. Fulcio comprueba el token y emite un certificado que vincula la clave pública a la identidad: el correo electrónico o el URI del flujo de trabajo va en el nombre alternativo del sujeto (SAN); el emisor y los detalles de la CI, en extensiones.
  4. El cliente firma el resumen del artefacto y envía la firma y el certificado a Rekor, que devuelve una entrada del registro con una prueba de inclusión y una marca de tiempo firmada.
  5. La clave privada efímera se desecha. El bundle guarda el certificado, la firma y las pruebas del registro.

El certificado del bundle que cosign publica con su propia versión v3.1.3 muestra lo breve que es su vida útil:

openssl x509 con el certificado incluido en 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

Diez minutos de validez. La firma sigue siendo verificable después, porque la entrada de Rekor demuestra que se hizo dentro de esa ventana. El integratedTime de Rekor de esta entrada es 2026-08-06 01:03:25 UTC.

El bundle

Un bundle es un documento JSON que contiene todo lo que necesita un verificador, salvo el artefacto y las raíces de confianza. Este es el bundle de la versión de cosign, abreviado:

cosign-linux-arm64.sigstore.json (recurso de la versión v3.1.3, valores acortados)
{
  "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 es el certificado hoja de Fulcio, con la identidad.
  • tlogEntries contiene la entrada de Rekor: una prueba de inclusión respecto a un punto de control (checkpoint) firmado y una marca de tiempo firmada de la entrada. Con ellas, el verificador no necesita contactar con Rekor en absoluto.
  • timestampVerificationData contiene sellos de tiempo RFC 3161 opcionales de una autoridad de sellado de tiempo.
  • El contenido es messageSignature, para una firma simple de un blob, o dsseEnvelope, para una atestación, es decir, una declaración in-toto firmada.

El verificador de bundles de Sigstore comprueba todas estas partes en el navegador y muestra la identidad del certificado, que es lo primero que hay que mirar cuando falla una verificación.

Verificación: lo que importa es la identidad

cosign verifica el binario de su propia versión (ghcr.io/sigstore/cosign/cosign:v3.1.3, 20 de octubre de 2026). La versión se compila en infraestructura de Google, así que la identidad es una cuenta de servicio de Google y el emisor es Google, no GitHub:

cosign verify-blob: la identidad 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

Ahora, los casos de fallo. Cada uno tiene un mensaje que indica exactamente qué comprobación falló:

cosign verify-blob: así se ven los fallos
--- 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

El tercer caso es una decisión de diseño deliberada. Cualquiera puede obtener un certificado de Fulcio para su propia identidad y firmar lo que quiera. Una firma solo tiene sentido cuando compruebas que la identidad es la que esperas. cosign se niega a verificar firmas keyless sin una identidad (página del error). El último caso muestra el aspecto de una manipulación (página del error).

¿De dónde sacas la identidad esperada? De la documentación del proyecto, o leyéndola una vez de un bundle en el que confías y fijándola en tu script de verificación. --certificate-identity-regexp es útil para etiquetas (por ejemplo, ...release.yml@refs/tags/v.*), pero ancla la expresión en ambos extremos. Un patrón como .*sigstore.* coincide con identidades que cualquiera puede crear.

Firma en GitHub Actions

En la CI, la identidad es el flujo de trabajo. Fulcio pone el archivo del flujo de trabajo y la ref en el SAN, y los detalles en extensiones. El bundle de procedencia de npm de sigstore@5.0.0 muestra lo que lleva un certificado de GitHub Actions, decodificado de sus extensiones X.509 con un pequeño script:

Extensiones de Fulcio en el certificado de procedencia de 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

cosign también puede comprobar estos claims (declaraciones), además del SAN:

cosign verify-blob-attestation con claims de 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 firmar tus propias versiones en GitHub Actions, el trabajo (job) necesita permiso para solicitar un token OIDC. El README de actions/attest de GitHub da esta configuración mínima para la procedencia de compilación de un archivo:

GitHub Actions: procedencia de compilación de un archivo de la versión (del 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'

Los repositorios públicos usan la instancia pública de Sigstore. Los privados usan la instancia propia de GitHub, que requiere GitHub Enterprise Cloud. actions/attest-build-provenance sigue funcionando, pero desde la v4 es solo un envoltorio de actions/attest. Para firmar un blob con cosign directamente en el trabajo, el comando es cosign sign-blob --bundle app.sigstore.json app.tar.gz --yes. La ayuda de --oidc-provider de cosign incluye github-actions entre las fuentes de tokens que prueba cuando no se indica ninguna. No hicimos ninguna firma keyless para esta página, porque cada firma keyless en la instancia pública es una entrada permanente y pública del registro.

Una firma de un flujo de trabajo es tan fiable como el propio flujo de trabajo. Cualquiera que pueda hacer push a la rama, o lanzar el flujo de trabajo, puede producir una firma válida con esa identidad. Protege los flujos de trabajo de publicación con protección de ramas y etiquetas, revisiones obligatorias y aprobaciones de entorno, y fija la verificación a una ref de etiqueta (@refs/tags/...) cuando publiques desde etiquetas.

npm provenance

npm publica dos tipos de pruebas de Sigstore para los paquetes compilados en una CI compatible: una atestación de publicación firmada por el registro y una atestación de procedencia SLSA firmada por la compilación. npm audit signatures comprueba ambas en todo lo instalado:

npm 11.19.1: npm audit signatures (sigstore@5.0.0 y semver@7.7.2 con sus dependencias)
$ 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)

Las atestaciones de una versión son públicas, en https://registry.npmjs.org/-/npm/v1/attestations/<name>@<version>. Para sigstore@5.0.0:

Atestaciones de sigstore@5.0.0, decodificadas
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

El resumen del sujeto es el SHA-512 del tarball (849a897e...), así que el bundle de procedencia se puede verificar con cosign frente al .tgz descargado, como se muestra más arriba. Con la publicación de confianza de npm (npm CLI 11.5.1 o posterior), los paquetes publicados desde GitHub Actions o GitLab CI mediante OIDC obtienen la procedencia automáticamente, sin la opción --provenance y sin un token de npm de larga duración. CircleCI es compatible con la publicación de confianza, pero todavía no con la procedencia. Fuera de una CI compatible, npm publish --provenance falla con Automatic provenance generation not supported for provider: null. El comprobador de procedencia de npm muestra el repositorio, el flujo de trabajo y el commit de cualquier versión de un paquete.

Atestaciones de artefactos de GitHub

GitHub guarda atestaciones de los recursos de las versiones y las pone a disposición a través de su API. El tarball de la versión de la CLI gh, gh_2.102.0_linux_arm64.tar.gz (SHA-256 7862c86c...), tenía dos atestaciones el 20 de octubre de 2026: la procedencia SLSA del flujo de trabajo de despliegue de cli/cli y una atestación de versión firmada por el propio GitHub. Con el bundle de procedencia guardado localmente, gh attestation verify funciona sin iniciar sesión e imprime la política que aplicó:

gh 2.102.0: gh attestation verify con un bundle local (sin colores)
$ 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

Con --repo example/cli, la misma comprobación termina en ✗ Sigstore verification failed. La política predeterminada (propietario, repositorio, prefijo del SAN) es un buen punto de partida. Para automatizar las publicaciones, añade --signer-workflow para fijar el archivo exacto del flujo de trabajo, y --deny-self-hosted-runners si tus compilaciones solo se ejecutan en runners hospedados por GitHub. El mismo bundle también se verifica con cosign: cosign verify-blob-attestation --type slsaprovenance1 con el SAN https://github.com/cli/cli/.github/workflows/deployment.yml@refs/heads/trunk imprimió Verified OK.

La segunda atestación es distinta. Su tipo de predicado es https://in-toto.io/attestation/release/v0.2. El certificado nombra URI:https://dotcom.releases.github.com y lo emitió O=GitHub, Inc., CN=Fulcio Intermediate l1, el Fulcio propio de GitHub, con un sello de tiempo RFC 3161 en lugar de una entrada de Rekor. GitHub la crea para los repositorios que tienen activadas las versiones inmutables. Demuestra que el recurso pertenece a la versión, no cómo se compiló. gh release verify y gh release verify-asset la comprueban. En nuestra prueba, gh release verify-asset necesitó un gh con la sesión iniciada: sin token, gh 2.102.0 se detuvo con To get started with GitHub CLI, please run: gh auth login.

Cuándo no encaja la firma keyless

Compilaciones en entornos aislados (air-gapped), artefactos privados que no deben aparecer en un registro público o políticas que exigen una clave en un HSM: cosign también firma con claves, y la subida al registro se puede desactivar. Probado con cosign v3.1.3:

cosign con un par de claves y sin registro de transparencia
$ 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

El bundle contiene entonces solo una pista publicKey, y vuelves a tener que distribuir y proteger una clave de larga duración, justo el problema que elimina la firma keyless. Para una configuración privada pero aún auditable, ejecuta tu propia instancia de Sigstore (Fulcio, Rekor, TUF) o usa la de un proveedor. GitHub lo hace así para los repositorios privados.

El estado de la infraestructura en 2026

  • Rekor v2, un registro basado en teselas (tiles), alcanzó la disponibilidad general en octubre de 2025. Los verificadores necesitan cosign 2.6.0 o 3.0.1 y versiones posteriores. Rekor v2 eliminó el índice de búsqueda.
  • En junio de 2026, los mantenedores anunciaron que la instancia pública «seguirá usando Rekor v1 como registro predeterminado en el futuro previsible». Rekor v2 sigue en producción para los ecosistemas que opten por él.
  • Un Sigstore poscuántico independiente con ML-DSA está «previsto para mediados de 2027», según el anuncio de octubre de 2026. El plan es que la instancia clásica siga funcionando hasta un cierre «programado provisionalmente para 2029 o 2030». Los bundles siguen siendo el formato en cualquier caso.

Por el lado de la verificación apenas cambia nada: mantén los clientes actualizados, verifica a partir de bundles y fija identidades, no las URL del registro.

Errores frecuentes

  • Comprobar la firma, pero no la identidad. Una firma de Sigstore válida solo dice que alguien firmó.
  • Patrones de identidad demasiado amplios. Expresiones regulares sin anclar, o una comprobación del emisor sin comprobación del SAN.
  • Fijar una rama cuando publicas desde etiquetas. @refs/heads/main acepta cualquier cosa compilada desde main, incluida una compilación que nadie pretendía publicar.
  • Dar por hecho que la firma demuestra la compilación. Una firma de un blob demuestra quién firmó los bytes. La procedencia (SLSA, in-toto) dice cómo y a partir de qué se compilaron. Verifica la que necesite tu política.
  • Firmar versiones personales con un correo electrónico personal sin darse cuenta de que el correo está en el certificado y, por tanto, en un registro público y permanente.
  • Distribuir los archivos .sig y .pem separados que generaban las versiones antiguas de cosign. Publica el bundle .sigstore.json. Es el formato que entienden todos los verificadores actuales.

Fuentes

La verificación se ejecutó el 20 de octubre de 2026 en ghcr.io/sigstore/cosign/cosign:v3.1.3, con npm 11.19.1 y gh 2.102.0 (el tarball oficial de la versión) en node:24.22.0-trixie.