signed-audit-trails-recipe

Par wshobson · agents

Guide pas à pas pour mettre en place des pistes d'audit signées cryptographiquement sur les appels d'outils Claude Code. À utiliser pour expliquer, évaluer ou démontrer le pattern avant de s'engager sur les hooks runtime protect-mcp. Couvre les politiques Cedar, les reçus Ed25519, la vérification hors ligne, la détection de falsification, l'intégration CI/CD et la composition SLSA.

npx skills add https://github.com/wshobson/agents --skill signed-audit-trails-recipe

Traces d'audit signées pour les appels d'outils Claude Code

Walkthrough de style cookbook pour les reçus cryptographiquement signés sur chaque appel d'outil Claude Code. Ceci est la skill d'enseignement. Pour l'implémentation runtime, installez le plugin protect-mcp.

Ce que cela vous donne

Chaque appel d'outil (Bash, Edit, Write, WebFetch) est :

  1. Évalué par rapport à une politique Cedar avant son exécution. Si la politique refuse l'appel, l'outil ne s'exécute pas.
  2. Signé en tant que reçu Ed25519 après son exécution. Les reçus sont JCS-canoniques, chaînés en hash, et vérifiables hors ligne par quiconque dispose de la clé publique.

Un auditeur, un régulateur ou une contrepartie peut vérifier la chaîne complète ultérieurement avec une seule commande CLI (npx @veritasacta/verify receipts/*.json). Pas d'appel réseau, pas de recherche auprès du fournisseur, pas de confiance envers l'opérateur.

Quand utiliser ce pattern

  • Environnements réglementés (finance, santé, infrastructure critique) où vous avez besoin de preuves inviolables du comportement de l'agent
  • Pipelines CI/CD où vous voulez prouver qu'une barrière de politique a tenu pour chaque étape de build automatisée
  • Collaboration multipartite où une contrepartie veut vérifier le comportement de votre agent sans faire confiance à votre opérateur
  • Contextes de conformité (EU AI Act Article 12, SLSA provenance pour les logiciels construits par agent) où la journalisation standard n'est pas suffisante

Étape 1 : Installer la configuration du hook

Créez .claude/settings.json à la racine de votre projet :

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": ".*",
        "hook": {
          "type": "command",
          "command": "npx protect-mcp@latest evaluate --policy ./protect.cedar --tool \"$TOOL_NAME\" --input \"$TOOL_INPUT\" --fail-on-missing-policy false"
        }
      }
    ],
    "PostToolUse": [
      {
        "matcher": ".*",
        "hook": {
          "type": "command",
          "command": "npx protect-mcp@latest sign --tool \"$TOOL_NAME\" --input \"$TOOL_INPUT\" --output \"$TOOL_OUTPUT\" --receipts ./receipts/ --key ./protect-mcp.key"
        }
      }
    ]
  }
}

La première exécution de protect-mcp sign génère ./protect-mcp.key (clé privée Ed25519) s'il n'en existe pas. Commitez l'empreinte de la clé publique (visible dans le champ public_key de tout reçu) ; ne commitez pas la clé privée.

Ajoutez la clé privée et le répertoire des reçus à .gitignore :

echo "./protect-mcp.key" >> .gitignore
echo "./receipts/" >> .gitignore

Étape 2 : Écrire une politique Cedar

Créez ./protect.cedar :

// Autoriser tous les outils orientés lecture par défaut.
permit (
    principal,
    action in [Action::"Read", Action::"Glob", Action::"Grep", Action::"WebSearch"],
    resource
);

// Autoriser les commandes Bash uniquement à partir d'une liste sûre.
permit (
    principal,
    action == Action::"Bash",
    resource
) when {
    context.command_pattern in [
        "git", "npm", "pnpm", "yarn", "ls", "cat", "pwd",
        "echo", "test", "node", "python", "make"
    ]
};

// Refus explicite des commandes destructrices. Le refus Cedar est autoritaire.
forbid (
    principal,
    action == Action::"Bash",
    resource
) when {
    context.command_pattern in ["rm -rf", "dd", "mkfs", "shred"]
};

// Restreindre les écritures au répertoire du projet.
permit (
    principal,
    action in [Action::"Write", Action::"Edit"],
    resource
) when {
    context.path_starts_with == "./"
};

Quatre règles :

  • Les outils orientés lecture toujours autorisés
  • Bash autorisé pour les motifs de commandes sûrs (git, npm, etc.)
  • Bash rm -rf et commandes destructrices similaires explicitement refusées
  • Écritures autorisées uniquement dans le projet (préfixe ./)

Les règles Cedar forbid ont priorité sur les règles permit, donc les commandes destructrices ne peuvent pas être contournées par une règle permissive ultérieure.

Étape 3 : Utiliser Claude Code normalement

Démarrez Claude Code. Chaque appel d'outil passe par les deux hooks :

You: Please read the README and summarize it.

Claude: I will read README.md.
  [PreToolUse: Read ./README.md -> allow]
  [Tool: Read executes]
  [PostToolUse: receipt rcpt-a8f3c9d2 signed to ./receipts/]

... summary of README ...

Une session de 20 appels d'outils produit 20 reçus, chacun chaîné en hash à son prédécesseur.

Étape 4 : Inspecter un reçu

cat ./receipts/$(ls -t ./receipts/ | head -1)
{
  "receipt_id": "rcpt-a8f3c9d2",
  "receipt_version": "1.0",
  "issuer_id": "claude-code-protect-mcp",
  "event_time": "2026-04-17T12:34:56.123Z",
  "tool_name": "Read",
  "input_hash": "sha256:a3f8c9d2e1b7465f...",
  "decision": "allow",
  "policy_id": "protect.cedar",
  "policy_digest": "sha256:b7e2f4a6c8d0e1f3...",
  "parent_receipt_id": "rcpt-3d1ab7c2",
  "public_key": "4437ca56815c0516...",
  "signature": "4cde814b7889e987..."
}

Chaque champ sauf signature et public_key est couvert par la signature Ed25519. Modifier un champ quelconque après la signature invalide la signature.

Étape 5 : Vérifier la chaîne de reçus

npx @veritasacta/verify ./receipts/*.json

Codes de sortie :

Code Signification
0 Tous les reçus vérifiés ; chaîne intacte
1 Un reçu a échoué la vérification de signature (altéré, ou mauvaise clé)
2 Un reçu était mal formé

Étape 6 : Démontrer la détection d'altération

Modifiez le champ decision de n'importe quel reçu de allow à deny :

python3 -c "
import json, os
path = './receipts/' + sorted(os.listdir('./receipts'))[-1]
r = json.loads(open(path).read())
r['decision'] = 'deny'
open(path, 'w').write(json.dumps(r))
"

npx @veritasacta/verify ./receipts/*.json

Le vérificateur se termine avec le code 1 et rapporte quel reçu a échoué. La signature Ed25519 ne correspond plus aux octets JCS-canoniques de la charge utile altérée.

Restaurez le champ et la vérification réussit à nouveau.

Comment fonctionne la cryptographie

Trois invariants rendent les reçus vérifiables hors ligne dans toute implémentation conforme :

  1. Canonicalisation JCS (RFC 8785) avant la signature. Clés triées, espaces blancs minimisés, chaînes normalisées en NFC. Deux implémentations indépendantes produisent des charges utiles de signature identiques en octets pour le même contenu de reçu.
  2. Signatures Ed25519 (RFC 8032) sur les octets canoniques. Déterministes, taille fixe, pas de dépendance au nonce.
  3. Chaînage des hashes. Le parent_receipt_hash de chaque reçu est le SHA-256 de la forme canonique du prédécesseur. Les insertions, suppressions et réordonnancements invalident les reçus ultérieurs.

Pour le format de câble formel, voir draft-farley-acta-signed-receipts.

Interopérabilité inter-implémentations

Le format de reçu compte quatre implémentations indépendantes aujourd'hui :

Implémentation Langage Cas d'usage
protect-mcp TypeScript Claude Code, Cursor, hôtes MCP
protect-mcp-adk Python Google Agent Development Kit
sb-runtime Rust Sandbox au niveau OS (Landlock + seccomp)
APS governance hook Python CrewAI, LangChain

Un reçu produit par l'une d'elles se vérifie par rapport à @veritasacta/verify. L'auditeur n'a pas besoin de faire confiance au choix d'outillage de l'opérateur : le format est le contrat.

Intégration CI/CD

Limitez les fusions à la vérification de la chaîne de reçus afin qu'aucun build ne fasse surface avec une chaîne de preuves rompue :

# .github/workflows/verify-receipts.yml
name: Verify Decision Receipts
on: [push, pull_request]

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - name: Run governed agent
        run: python scripts/run_agent.py > receipts.jsonl
      - name: Verify receipt chain
        run: npx @veritasacta/verify receipts.jsonl

Archivez les reçus en tant qu'artefact afin que la chaîne persiste au-delà de l'exécution du job :

      - name: Upload receipts
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: decision-receipts
          path: receipts/

Composition avec la provenance SLSA pour les logiciels construits par agent

Quand Claude Code construit et publie des logiciels (en exécutant npm install, npm build, npm publish en tant qu'appels d'outil), la chaîne de reçus est le journal de build par étape. SLSA Provenance v1 a un point d'extension pour cela : le champ byproducts peut référencer la chaîne de reçus aux côtés de l'attestation de build.

Le agent-commit build type documente le pattern en utilisant la forme ResourceDescriptor :

{
  "name": "decision-receipts",
  "digest": { "sha256": "..." },
  "uri": "oci://registry/org/build-xyz/receipts:sha256-...",
  "annotations": {
    "predicateType": "https://veritasacta.com/attestation/decision-receipt/v0.1",
    "signerRole": "supervisor-hook"
  }
}

La provenance SLSA est signée par l'identité du constructeur ; l'attestation de reçu est signée par l'identité supervisor-hook. Deux domaines de confiance, référencés croisés à la couche byproduct. Voir slsa-framework/slsa#1594 pour la discussion de composition.

Pièges courants

Clé privée en contrôle de version. La clé ./protect-mcp.key générée ne doit pas être commitée. Les exemples ci-dessus l'ajoutent à .gitignore. Si une clé est accidentellement commitée, effectuez une rotation immédiatement (supprimez le fichier de clé et laissez le hook le régénérer à la prochaine exécution).

Guillemets de la commande du hook. Les hooks reçoivent $TOOL_NAME et $TOOL_INPUT en tant que variables d'environnement. Conservez les guillemets "$TOOL_INPUT" afin que les entrées contenant des espaces ou des caractères spéciaux passent intactes.

Répertoire des reçus en CI. Si Claude Code s'exécute en CI, chargez les reçus en tant qu'artefact à la fin du job sinon la chaîne est perdue à la fin du job.

La politique est manquante. Le hook PreToolUse d'exemple utilise --fail-on-missing-policy false afin qu'un ./protect.cedar absent ne casse pas Claude Code de manière prête à l'emploi. Supprimez cet indicateur en production afin qu'une politique manquante soit traitée comme une défaillance critique.

Contenu connexe dans cette marketplace

  • protect-mcp — l'implémentation du hook runtime (utilisez ce plugin en production)
  • review-agent-governance — exiger l'approbation humaine avant les actions de surface d'examen ; se compose avec protect-mcp

Références

Skills similaires