perf sentinelperf sentineldocs
FRENGitHub
Documentation / 10 · Sigstore & SLSA

Signature Sigstore et attestation SLSA

Les primitives cryptographiques ci-dessous s'ajoutent au schéma perf-sentinel-report/v1.0 à partir de v0.7.0. Le but est de permettre à un consommateur de vérifier de bout en bout une divulgation périodique publiée sans avoir à faire confiance à perf-sentinel ni à l'organisation publiante au-delà de ce qui est ancré dans l'infrastructure publique Sigstore.

Pourquoi deux couches

Les rapports Perf Sentinel reposent sur deux signatures complémentaires :

  • Signature Sigstore sur le rapport, ancrée dans le journal de transparence Rekor. Prouve que le rapport a été signé par une identité autorisée par l'organisation publiante et n'a pas été modifié depuis.
  • Provenance SLSA sur le binaire Perf Sentinel, produite par le workflow GitHub Actions de release du projet. Prouve que le binaire ayant calculé le rapport a été construit depuis le code source officiel par un builder reconnu, pas par un build personnalisé ou trafiqué.

Un consommateur qui vérifie les deux obtient une chaîne de confiance complète :

code source -> attestation SLSA -> binaire -> rapport -> signature Sigstore

Les deux couches sont indépendantes : un opérateur peut signer un rapport produit par un binaire non officiel (la signature prouve toujours la paternité et l'intégrité, l'attestation binaire est absente du rapport). Ou un binaire officiel peut produire un rapport jamais signé (hash-only). Le schéma rend les deux états explicites via integrity.integrity_level :

niveaucontent_hashsignaturebinary_attestation
noneabsentabsentabsent
hash-onlyprésentabsentabsent
signedprésentprésentabsent
signed-with-attestationprésentprésentprésent
audited (réservé)n/an/an/a

Le flux d'attestation

Pour une divulgation intent = "official", le workflow opérateur est :

  1. Scoring : le daemon écrit les archives par fenêtre en NDJSON sur la période (la signature n'intervient pas).
  2. Disclose : perf-sentinel disclose --intent official ... --output report.json --emit-attestation attestation.intoto.jsonl produit deux fichiers. Le integrity.content_hash du rapport reçoit le SHA-256 canonique. L'attestation est un statement in-toto v1 dont le subject.digest.sha256 est ce même hash canonique, sans son préfixe sha256:.
  3. Signer : l'opérateur lance cosign sign-blob --bundle bundle.sig --new-bundle-format attestation.intoto.jsonl contre Sigstore public. La signature est envoyée automatiquement à Rekor (le projet refuse les bundles sans preuve d'inclusion Rekor au moment de la vérification). Le Statement est signé tel quel, sans enveloppe supplémentaire. Utiliser cosign attest-blob --predicate ici envelopperait le Statement déjà formé dans un nouveau predicate-of-Statement, produisant une entrée malformée permanente dans le journal Rekor public.
  4. Mettre à jour le localisateur de signature du rapport : l'opérateur édite report.json pour ajouter integrity.signature avec les métadonnées qui permettent aux vérifieurs de localiser le bundle et l'entrée Rekor, puis fait passer integrity_level de hash-only à signed ou signed-with-attestation. Cette étape est manuelle aujourd'hui. Une future sous-commande perf-sentinel sign pourrait l'automatiser.
  5. Publier : les trois fichiers (report.json, attestation.intoto.jsonl, bundle.sig) sont publiés à l'URL de transparence de l'opérateur.

Un consommateur télécharge les trois fichiers et lance perf-sentinel verify-hash --report report.json --attestation attestation.intoto.jsonl --bundle bundle.sig ou, plus court, perf-sentinel verify-hash --url https://example.fr/report.json qui récupère les sidecars par convention.

Format du statement in-toto v1

L'attestation produite par disclose --emit-attestation est un document in-toto v1 à statement unique. Forme :

json
{
  "_type": "https://in-toto.io/Statement/v1",
  "predicateType": "https://perf-sentinel.io/attestation/v1",
  "subject": [
    {
      "name": "report.json",
      "digest": { "sha256": "<64-hex content_hash>" }
    }
  ],
  "predicate": {
    "perf_sentinel_version": "0.7.0",
    "report_uuid": "...",
    "period": { "from_date": "2026-01-01", "to_date": "2026-03-31" },
    "intent": "official",
    "confidentiality_level": "public",
    "organisation": {
      "name": "Example SAS",
      "country": "FR",
      "identifiers": { "siren": "...", "domain": "..." }
    },
    "methodology_summary": {
      "sci_specification": "ISO/IEC 21031:2024",
      "conformance": "core-required",
      "calibration_applied": true,
      "period_coverage": 0.91,
      "core_patterns_count": 4,
      "enabled_patterns_count": 10,
      "disabled_patterns_count": 0,
      "core_patterns_hash": "<64-hex SHA-256>"
    },
    "subject_binding": "content_hash"
  }
}

predicateType utilise le namespace perf-sentinel.io par convention. Le projet ne possède pas formellement le domaine aujourd'hui. Cette convention est la pratique standard pour les predicates in-toto personnalisés. Les vérifieurs identifient le predicate par correspondance exacte de chaîne.

Le subject.digest.sha256 est le content_hash canonique du rapport, pas le SHA-256 des octets du fichier. L'étape 4 modifie report.json après la signature du statement, ce qui change ses octets mais pas le hash canonique, puisque le localisateur de signature et le niveau d'intégrité sont vidés avant le calcul. Le statement signé continue donc de désigner les chiffres publiés, et verify-hash vérifie qu'il le fait. predicate.subject_binding le déclare : "content_hash" sur chaque statement écrit par disclose.

Les statements écrits avant l'apparition de subject_binding fixaient le SHA-256 du fichier rapport au moment du disclose, puis l'étape 4 changeait ces octets. Rien ne reliait alors une signature valide aux chiffres avec lesquels elle était publiée. verify-hash accepte encore un tel statement quand les octets du rapport n'ont pas bougé, et le déclare impossible à relier sinon.

Les trois champs de comptage (core_patterns_count, enabled_patterns_count, disabled_patterns_count) permettent à un consommateur qui ne lit que le predicate signé de détecter un rapport qui revendique conformance: "core-required" tout en ayant retiré un des quatre patterns core a posteriori. L'invariant enabled_patterns_count >= core_patterns_count est imposé par le validateur côté intent = "official" (validate_official refuse toute divulgation où un pattern core est absent de l'ensemble activé), donc toute divulgation officielle conforme respecte cet invariant par construction.

Le champ core_patterns_hash (SHA-256 sur les noms triés et joints par :) complète les compteurs pour détecter la substitution : un attaquant qui remplace n_plus_one_sql par slow_sql garde core_patterns_count = 4 mais change le hash. Le consommateur recalcule le hash sur la liste canonique core_patterns_required() de la version Perf Sentinel déclarée dans perf_sentinel_version (actuellement quatre : n_plus_one_sql, n_plus_one_http, redundant_sql, redundant_http) et le compare au hash signé.

verify-hash automatise cette vérification croisée : il hache la liste core canonique embarquée dans le binaire vérifieur local, hache le methodology.core_patterns_required du rapport, et affiche une ligne [FAIL] Core patterns si les deux divergent. La vérification tourne à chaque invocation verify-hash, aucun flag supplémentaire requis. Un consommateur qui fait tourner la même version Perf Sentinel que le signataire détecte donc une tentative de substitution sans table de référence externe. Une divergence contre un binaire vérifieur d'une autre version est signalée avec une indication ("verifying binary is a different perf-sentinel version") pour que le consommateur relance avec une version correspondante.

Commande cosign

Pour la signature Sigstore publique en keyless OIDC, la commande recommandée pour les opérateurs est :

bash
cosign sign-blob \
    --bundle bundle.sig \
    --new-bundle-format \
    attestation.intoto.jsonl

L'issuer OIDC (flux navigateur ou token GitHub Actions) enregistre l'identité du signataire dans le bundle. Les opérateurs qui utilisent une instance Rekor privée passent --rekor-url https://rekor.internal.example.fr qui correspond à leur config [reporting.sigstore].rekor_url.

Piège à éviter. Ne pas utiliser cosign attest-blob --predicate attestation.intoto.jsonl ... ici. attest-blob --predicate traite son argument comme un predicate brut et l'enveloppe dans un nouveau Statement in-toto v1 à la volée. Comme le pipeline disclose émet déjà un Statement complet, le résultat est un Statement-of-Statement que Rekor enregistre de façon permanente dans le journal de transparence public. Utiliser sign-blob pour signer le Statement déjà formé tel quel, avec --new-bundle-format correspondant pour que le bundle porte la preuve d'inclusion Rekor dans le format que verify-blob attend.

cosign 2.4+ est requis pour le flag --new-bundle-format. Les versions cosign antérieures émettent un bundle à l'ancien format que cosign verify-blob refuse. Les opérateurs sur cosign <2.4 devraient mettre à jour cosign avant de signer pour la transparence.

Le chemin de vérification ne prend pas en charge le flag --no-tlog-upload : un bundle sans preuve d'inclusion Rekor est refusé avec un message d'erreur clair. L'auditabilité publique est une propriété du format, pas une option.

Flux de vérification

perf-sentinel verify-hash chaîne jusqu'à quatre vérifications :

  1. Content hash (Rust pur, toujours lancé). Recalcule le SHA-256 canonique du rapport et compare à integrity.content_hash.
  2. Liaison de l'attestation (Rust pur, lancée quand une attestation est fournie). Vérifie que le _type, le predicateType et le predicate.report_uuid du statement correspondent au rapport, et que son subject est le content hash recalculé. La signature porte sur le statement, pas sur le rapport : c'est ce contrôle qui relie une signature valide à ces chiffres.
  3. Signature (déléguée à cosign verify-blob). Lancée quand integrity.signature est présent dans le rapport et que l'opérateur passe --attestation et --bundle (ou que le mode --url les récupère automatiquement).
  4. Attestation binaire (déléguée à gh attestation verify à partir de v0.7.1, slsa-verifier verify-artifact sur l'ancienne release v0.7.0). La sortie verify-hash affiche un résumé des métadonnées et la commande de vérification exacte à lancer contre le binaire téléchargé depuis integrity.binary_verification_url. La migration 0.7.1 a déplacé le stockage de l'attestation d'un asset de release (multiple.intoto.jsonl) vers l'API attestations GitHub via actions/attest-build-provenance. Récupérer et vérifier le binaire en une seule commande est un travail futur.

Codes de sortie :

CodeSignification
0TRUSTED
1UNTRUSTED (une vérification a retourné un échec dur)
2PARTIAL (pas d'échec dur, au moins une vérification n'a pas pu aboutir)
3INPUT_ERROR
4NETWORK_ERROR (mode --url uniquement)

La séparation entre UNTRUSTED (1) et PARTIAL (2) permet à un script d'encapsulation de différencier une tentative de falsification d'un outil manquant. Un gate naïf verify-hash && deploy rejette toujours PARTIAL parce que le code de sortie est non nul.

Confidentialité sur Rekor public

Chaque signature envoyée à Rekor Sigstore public produit une entrée permanente, lisible par tous dans le journal de transparence. L'entrée contient :

  • L'identité signataire enregistrée par l'issuer OIDC (par exemple un email Google, une URL de workflow GitHub Actions avec org/repo).
  • Le hash du payload signé (le statement in-toto ici).
  • Un horodatage.

L'entrée ne contient ni le rapport lui-même ni son contenu. Les opérateurs préoccupés par la fuite d'identité signataire devraient envisager :

  • Utiliser l'email d'un compte de service dédié pour la signature.
  • Faire tourner une instance Rekor privée ([reporting.sigstore].rekor_url).
  • Signer avec un workflow GitHub Actions dont l'URL d'identité est pré-divulguée par l'organisation.

Pour la plupart des usages de transparence publique, faire fuiter l'identité signataire est le résultat voulu : le consommateur veut savoir quelle identité se porte garante du rapport.

Modes d'échec

Ce qu'un consommateur doit conclure quand chaque vérification échoue :

  • Content hash FAIL : le fichier est corrompu ou a été trafiqué après publication. Non fiable.
  • Attestation binding FAIL avec content_hash valide : l'attestation désigne d'autres chiffres, un autre rapport, ou n'est pas un statement perf-sentinel. Le cas typique est un rapport dont les chiffres ont été modifiés et le hash recalculé après la signature. Une signature valide sur cette attestation ne garantit que les chiffres d'origine. Non fiable.
  • Attestation binding SKIP : le statement est antérieur aux subjects en content hash et fixe des octets du rapport qui ont changé depuis, en général quand le localisateur de signature a été ajouté. Rien ne le relie à ces chiffres. Produire l'attestation à nouveau avec un disclose récent et la signer à nouveau.
  • Signature FAIL avec content_hash valide : le rapport lui-même est intact mais n'a plus de preuve Sigstore valide. Il est probable que le bundle ait été remplacé, que l'entrée Rekor ait été révoquée, ou que l'identité du certificat ne corresponde pas au signataire revendiqué. Non fiable.
  • Signature SKIP parce que cosign n'est pas installé : installer cosign et réessayer. Le rapport n'est pas nécessairement non fiable mais ne peut pas être vérifié dans l'installation actuelle de l'utilisateur. Le content hash seul est une garantie plus faible.
  • Binary attestation NotProvided : le rapport a été produit par un binaire qui ne porte pas de métadonnées de provenance SLSA (par exemple un build de développement local). Content hash + signature Sigstore tiennent toujours, mais le consommateur ne peut pas vérifier ce qui a produit le rapport.
  • Binary attestation FAIL : le binaire référencé par integrity.binary_verification_url ne correspond pas à l'attestation SLSA, ou le source-uri ne correspond pas à github.com/Institut-du-Numerique-Responsable/perf-sentinel. Traiter comme non fiable.

Le verdict global apparaît comme TRUSTED (content hash, liaison de l'attestation et signature OK), PARTIAL (aucun FAIL, mais l'un d'eux NotProvided ou Skip), ou UNTRUSTED (un FAIL).

Outillage : hash-bake

La sous-commande hash-bake (0.7.2+) calcule le content_hash canonique d'un rapport et l'écrit dans integrity.content_hash sans passer par le pipeline disclose complet. Elle existe pour la génération de fixtures de test et pour déboguer les rapports dont le hash a divergé du canonique après des modifications manuelles. Par défaut elle refuse d'opérer sur un rapport portant déjà une integrity.signature pour ne pas masquer une erreur de workflow. Voir Divulgation § "Calculer un content hash canonique avec hash-bake" pour la référence côté opérateur.

Renvois

  • Schéma documente la forme sur le fil de integrity.signature et integrity.binary_attestation.
  • Divulgation est le workflow de signature côté opérateur.
  • Chaîne d’appro. couvre l'intégration du générateur SLSA dans le workflow GitHub Actions de release.
  • docs/schemas/perf-sentinel-report-v1.json porte les définitions JSON Schema de référence.