Aller au contenu
ecdsa.com
← Tous les articles

Versions signées avec Sigstore : cosign, bundles, identité

Sigstore remplace les clés de signature à longue durée de vie par un certificat valable dix minutes, lié à un workflow de CI ou à une adresse e-mail, et par une entrée dans un journal public. Signer est facile ; c’est la vérification que l’on fait mal. Voici le processus complet, vérifié sur des versions réelles le 20 octobre 2026.

En bref. Avec la signature « keyless » (sans clé) de Sigstore, le signataire prouve une identité au moyen d’un jeton OIDC (un workflow GitHub Actions, un compte Google ou Microsoft). Fulcio émet pour une clé éphémère un certificat valable une dizaine de minutes, qui contient cette identité. La signature est inscrite dans le journal de transparence public Rekor, et tout ce qu’il faut pour la vérifier se retrouve dans un bundle (.sigstore.json). La vérification doit contrôler qui a signé, pas seulement que la signature est valide. Dans cosign, cela signifie --certificate-identity et --certificate-oidc-issuer, que cosign exige en mode sans clé. La provenance npm et les attestations d’artefacts GitHub reposent sur le même mécanisme : npm audit signatures et gh attestation verify les vérifient pour vous.

Pourquoi signer sans clé

La signature classique des versions, avec GPG ou une clé de signature de code à longue durée de vie, pose deux problèmes chroniques. Les utilisateurs doivent savoir quelle clé est la bonne : la distribution des clés est donc le véritable problème de confiance. Et la clé doit être stockée quelque part pendant des années, généralement dans un secret de CI, d’où elle peut fuiter. Sigstore remplace la clé par une identité que chacun comprend déjà, comme « le workflow de publication du dépôt X » ou « alice@example.com ». Il remplace la durée de vie de la clé par une entrée de journal qui prouve quand la signature a été faite.

Sigstore se compose de services publics exploités par la communauté OpenSSF (l’« instance publique », public-good instance) et d’outils clients :

  • Fulcio est une autorité de certification qui émet des certificats X.509 à courte durée de vie pour des identités OIDC.
  • Rekor est le journal de transparence des signatures et des attestations.
  • Les métadonnées TUF distribuent aux clients les racines de confiance (l’AC de Fulcio, la clé de Rekor, les autorités d’horodatage).
  • cosign est la CLI pour les blobs et les images de conteneurs. sigstore-python, sigstore-js, sigstore-go et sigstore-java sont des bibliothèques qui remplissent les mêmes tâches.

Ce qui se passe quand vous signez

  1. Le client crée une paire de clés éphémères en mémoire.
  2. Il obtient un jeton OIDC : dans GitHub Actions auprès du runner (id-token: write), sur un ordinateur portable par une connexion dans le navigateur.
  3. Fulcio vérifie le jeton et émet un certificat qui lie la clé publique à l’identité : l’adresse e-mail ou l’URI du workflow va dans le nom alternatif du sujet (Subject Alternative Name, SAN), l’émetteur et les détails de la CI dans des extensions.
  4. Le client signe le condensat de l’artefact et soumet la signature et le certificat à Rekor, qui renvoie une entrée de journal avec une preuve d’inclusion et un horodatage signé.
  5. La clé privée éphémère est supprimée. Le bundle conserve le certificat, la signature et les preuves issues du journal.

Le certificat du bundle que cosign publie avec sa propre version, v3.1.3, montre à quel point sa durée de vie est courte :

openssl x509 sur le certificat contenu dans 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

Dix minutes de validité. La signature reste vérifiable au-delà, car l’entrée Rekor prouve qu’elle a été faite pendant cette fenêtre. Le champ integratedTime de cette entrée Rekor vaut 2026-08-06 01:03:25 UTC.

Le bundle

Un bundle est un document JSON qui contient tout ce dont un vérificateur a besoin, sauf l’artefact et les racines de confiance. Voici le bundle de la version de cosign, abrégé :

cosign-linux-arm64.sigstore.json (fichier de la version v3.1.3, valeurs raccourcies)
{
  "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 est le certificat feuille émis par Fulcio, avec l’identité.
  • tlogEntries contient l’entrée Rekor : une preuve d’inclusion par rapport à un checkpoint signé et un horodatage d’entrée signé. Avec ces éléments, un vérificateur n’a pas du tout besoin de contacter Rekor.
  • timestampVerificationData contient des horodatages RFC 3161 facultatifs émis par une autorité d’horodatage.
  • Le contenu est soit messageSignature pour une simple signature de blob, soit dsseEnvelope pour une attestation, c’est-à-dire une déclaration in-toto signée.

Le vérificateur de bundles Sigstore contrôle toutes ces parties dans le navigateur et affiche l’identité inscrite dans le certificat, la première chose à regarder quand une vérification échoue.

Vérifier : l’identité est l’essentiel

Voici cosign qui vérifie le binaire de sa propre version (ghcr.io/sigstore/cosign/cosign:v3.1.3, 20 octobre 2026). Cette version est construite sur l’infrastructure de Google : l’identité est donc un compte de service Google, et l’émetteur est Google, pas GitHub :

cosign verify-blob : l’identité attendue
$ 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

Passons aux cas d’échec. Chacun produit un message qui indique précisément quelle vérification a échoué :

cosign verify-blob : à quoi ressemble un échec
--- 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

Le troisième cas est un choix de conception délibéré. N’importe qui peut obtenir un certificat Fulcio pour sa propre identité et signer n’importe quoi. Une signature n’a de sens qu’une fois que vous avez vérifié que l’identité est bien celle que vous attendez. cosign refuse de vérifier des signatures sans clé en l’absence d’identité (page d’erreur). Le dernier cas montre à quoi ressemble une falsification (page d’erreur).

Où trouver l’identité attendue ? Dans la documentation du projet, ou en la lisant une fois dans un bundle auquel vous faites confiance et en l’épinglant dans votre script de vérification. --certificate-identity-regexp est utile pour les tags (par exemple ...release.yml@refs/tags/v.*), mais ancrez l’expression au début et à la fin. Un motif comme .*sigstore.* correspond à des identités que n’importe qui peut créer.

Signer dans GitHub Actions

En CI, l’identité est le workflow. Fulcio inscrit le fichier de workflow et sa référence Git dans le SAN, et les détails dans des extensions. Le bundle de provenance npm de sigstore@5.0.0 montre ce que contient un certificat GitHub Actions, décodé à partir de ses extensions X.509 avec un petit script :

Extensions Fulcio du certificat de provenance 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 peut aussi vérifier ces claims, en plus du SAN :

cosign verify-blob-attestation avec les claims 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

Pour signer vos propres versions dans GitHub Actions, le job doit avoir l’autorisation de demander un jeton OIDC. Le README de actions/attest, publié par GitHub, donne cette configuration minimale pour la provenance de build d’un fichier :

GitHub Actions : provenance de build d’un fichier de version (d’après le README d’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'

Les dépôts publics utilisent l’instance publique de Sigstore. Les dépôts privés utilisent l’instance propre à GitHub, qui nécessite GitHub Enterprise Cloud. actions/attest-build-provenance fonctionne toujours, mais depuis la v4 ce n’est plus qu’une surcouche d’actions/attest. Pour signer un blob avec cosign directement dans le job, la commande est cosign sign-blob --bundle app.sigstore.json app.tar.gz --yes. L’aide de l’option --oidc-provider de cosign cite github-actions parmi les sources de jeton que cosign essaie lorsqu’aucune n’est indiquée. Nous n’avons pas effectué de signature sans clé pour cette page, car chaque signature sans clé sur l’instance publique devient une entrée de journal publique et permanente.

Une signature issue d’un workflow n’est digne de confiance que dans la mesure où le workflow l’est. Toute personne qui peut pousser sur la branche, ou déclencher le workflow, peut produire une signature valide avec cette identité. Protégez les workflows de publication par la protection des branches et des tags, les revues obligatoires et les approbations d’environnement, et épinglez la vérification sur une référence de tag (@refs/tags/...) lorsque vous publiez depuis des tags.

Provenance npm

npm publie deux types de preuves Sigstore pour les paquets construits dans une CI prise en charge : une attestation de publication signée par le registre et une attestation de provenance SLSA signée par le build. npm audit signatures vérifie les deux pour tout ce qui est installé :

npm 11.19.1 : npm audit signatures (sigstore@5.0.0 et semver@7.7.2 avec leurs dépendances)
$ 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)

Les attestations d’une version sont publiques, à l’adresse https://registry.npmjs.org/-/npm/v1/attestations/<name>@<version>. Pour sigstore@5.0.0 :

Attestations de sigstore@5.0.0, décodées
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

Le condensat du sujet est le SHA-512 du tarball (849a897e...) : le bundle de provenance peut donc être vérifié avec cosign par rapport au .tgz téléchargé, comme montré plus haut. Avec la publication de confiance (trusted publishing) de npm, disponible à partir de npm CLI 11.5.1, les paquets publiés depuis GitHub Actions ou GitLab CI via OIDC reçoivent automatiquement la provenance, sans l’option --provenance et sans jeton npm à longue durée de vie. CircleCI est pris en charge pour la publication de confiance, mais pas encore pour la provenance. En dehors d’une CI prise en charge, npm publish --provenance échoue avec Automatic provenance generation not supported for provider: null. Le vérificateur de provenance npm affiche le dépôt, le workflow et le commit de n’importe quelle version d’un paquet.

Attestations d’artefacts GitHub

GitHub stocke des attestations pour les ressources des versions (release assets) et les met à disposition via son API. Le 20 octobre 2026, le tarball de la version de la CLI gh, gh_2.102.0_linux_arm64.tar.gz (SHA-256 7862c86c...), avait deux attestations : une provenance SLSA issue du workflow de déploiement de cli/cli, et une attestation de version signée par GitHub lui-même. Avec le bundle de provenance enregistré localement, gh attestation verify fonctionne sans authentification et affiche la politique qu’il a appliquée :

gh 2.102.0 : gh attestation verify avec un bundle local (couleurs supprimées)
$ 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

Avec --repo example/cli, la même vérification se termine par ✗ Sigstore verification failed. La politique par défaut (propriétaire, dépôt, préfixe du SAN) est un bon point de départ. Pour l’automatisation des publications, ajoutez --signer-workflow pour épingler le fichier de workflow exact, et --deny-self-hosted-runners si vos builds s’exécutent uniquement sur des runners hébergés par GitHub. Le même bundle se vérifie aussi avec cosign : cosign verify-blob-attestation --type slsaprovenance1 avec le SAN https://github.com/cli/cli/.github/workflows/deployment.yml@refs/heads/trunk a affiché Verified OK.

La seconde attestation est différente. Son type de prédicat est https://in-toto.io/attestation/release/v0.2. Le certificat nomme URI:https://dotcom.releases.github.com et a été émis par O=GitHub, Inc., CN=Fulcio Intermediate l1, le Fulcio propre à GitHub, avec un horodatage RFC 3161 au lieu d’une entrée Rekor. GitHub la crée pour les dépôts où les versions immuables (immutable releases) sont activées. Elle prouve que la ressource appartient à la version, pas la façon dont elle a été construite. gh release verify et gh release verify-asset la vérifient. Lors de notre test, gh release verify-asset a exigé un gh connecté : sans jeton, gh 2.102.0 s’est arrêté avec To get started with GitHub CLI, please run: gh auth login.

Quand la signature sans clé ne convient pas

Builds isolés du réseau (air-gapped), artefacts privés qui ne doivent pas apparaître dans un journal public, ou politiques qui exigent une clé dans un HSM : cosign signe aussi avec des clés, et l’envoi au journal peut être désactivé. Testé avec cosign v3.1.3 :

cosign avec une paire de clés et sans journal de transparence
$ 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

Le bundle ne contient alors qu’une indication publicKey, et vous voilà de nouveau à distribuer et à protéger une clé à longue durée de vie, le problème même que la signature sans clé élimine. Pour une configuration privée mais toujours auditable, exploitez votre propre instance Sigstore (Fulcio, Rekor, TUF) ou utilisez celle d’un fournisseur. C’est ce que fait GitHub pour les dépôts privés.

L’état de l’infrastructure en 2026

  • Rekor v2, un journal à base de tuiles (tile-based), est passé en disponibilité générale en octobre 2025. Les vérificateurs ont besoin de cosign 2.6.0 ou 3.0.1 et versions ultérieures. Rekor v2 a abandonné l’index de recherche.
  • En juin 2026, les mainteneurs ont annoncé que l’instance publique « continuera d’utiliser Rekor v1 comme journal par défaut dans un avenir prévisible ». Rekor v2 reste en production pour les écosystèmes qui choisissent de l’adopter.
  • Un Sigstore post-quantique distinct, fondé sur ML-DSA, est « visé pour la mi-2027 », selon l’annonce d’octobre 2026. L’instance classique doit continuer à fonctionner jusqu’à un arrêt « provisoirement prévu pour 2029 ou 2030 ». Les bundles restent le format dans les deux cas.

Côté vérification, presque rien ne change : maintenez vos clients à jour, vérifiez à partir des bundles et épinglez des identités, pas des URL de journaux.

Erreurs fréquentes

  • Vérifier la signature mais pas l’identité. Une signature Sigstore valide indique seulement que quelqu’un a signé.
  • Des motifs d’identité trop larges. Des expressions régulières non ancrées, ou une vérification de l’émetteur sans vérification du SAN.
  • Épingler une branche alors que vous publiez depuis des tags. @refs/heads/main accepte tout ce qui est construit à partir de main, y compris un build que personne n’avait l’intention de publier.
  • Croire que la signature prouve le build. Une signature de blob prouve qui a signé les octets. La provenance (SLSA, in-toto) indique comment et à partir de quoi ils ont été construits. Vérifiez celle des deux dont votre politique a besoin.
  • Signer des versions personnelles avec une adresse e-mail personnelle sans se rendre compte que cette adresse figure dans le certificat, et donc dans un journal public et permanent.
  • Distribuer des fichiers .sig et .pem détachés, produits par les anciennes versions de cosign. Publiez le bundle .sigstore.json. C’est le format que comprennent tous les vérificateurs actuels.

Sources

La vérification a été effectuée le 20 octobre 2026 dans ghcr.io/sigstore/cosign/cosign:v3.1.3, avec npm 11.19.1 et gh 2.102.0 (le tarball officiel de la version) dans node:24.22.0-trixie.