Regarder la vidéo de la leçon : Sécurisation des agents IA avec des reçus cryptographiques
(Vidéo de la leçon et miniature à ajouter par l’équipe de contenu Microsoft après la fusion, en suivant le modèle des leçons 14 / 15.)
Cette leçon couvrira :
Après avoir terminé cette leçon, vous saurez comment :
Imaginez que vous avez déployé un agent IA pour Contoso Travel. L’agent lit les demandes des clients, interroge une API de vols pour rechercher des options, et réserve des places pour le compte du client. Le trimestre dernier, l’agent a traité 50 000 réservations.
Aujourd’hui, un auditeur arrive. Il pose une question simple : « Montrez-moi ce que votre agent a fait. »
Vous lui remettez vos fichiers journaux. L’auditeur les examine et pose la question plus difficile : « Comment puis-je savoir que ces journaux n’ont pas été modifiés ? »
C’est le problème de la piste d’audit. La plupart des déploiements d’agents aujourd’hui reposent sur :
Aucun de ces systèmes ne peut répondre à la question de l’auditeur sans que celui-ci ne doive faire confiance à quelqu’un (vous, votre fournisseur cloud, votre vendeur de base de données). Pour un usage interne, cette confiance est souvent acceptable. Pour des charges réglementées (finance, santé, tout ce qui est soumis à l’EU AI Act), ce n’est pas le cas.
Les reçus cryptographiques résolvent ce problème en rendant chaque action de l’agent vérifiable indépendamment. L’auditeur n’a pas besoin de vous faire confiance. Il lui suffit de votre clé publique et du reçu lui-même.
Un reçu est un objet JSON qui enregistre ce qu’un agent a fait, signé avec une signature numérique.
flowchart LR
A[L'agent invoque un outil] --> B[Construire la charge utile du reçu]
B --> C[Canonicaliser JSON RFC 8785]
C --> D[Hachage SHA-256]
D --> E[Signature Ed25519]
E --> F[Reçu avec signature]
F --> G[Vérification hors ligne par l'auditeur]
G --> H{Signature valide ?}
H -- yes --> I[Preuve évidente de falsification]
H -- no --> J[Reçu rejeté]
Un reçu minimal ressemble à ceci :
{
"type": "agent.tool_call.v1",
"agent_id": "contoso-travel-bot",
"tool_name": "lookup_flights",
"tool_args_hash": "sha256:a3f9c1...",
"result_hash": "sha256:7b2e1d...",
"policy_id": "contoso-travel-policy-v3",
"timestamp": "2026-04-25T14:30:00Z",
"sequence": 47,
"previous_receipt_hash": "sha256:9d4e6a...",
"signature": {
"alg": "EdDSA",
"sig": "c5af83...",
"public_key": "8f3b2c..."
}
}
Trois propriétés font le travail :
La signature. Le reçu est signé par la passerelle de l’agent en utilisant une clé privée Ed25519. Toute personne disposant de la clé publique correspondante peut vérifier la signature hors ligne. Toute altération d’un champ invalide la signature.
Encodage canonique. Avant la signature, le reçu est sérialisé en utilisant le JSON Canonicalization Scheme (JCS, RFC 8785). Cela garantit que deux implémentations produisant le même reçu logique génèrent une sortie identique au niveau des octets. Sans canonicalisation, différents sérialiseurs JSON produiraient des signatures différentes pour le même contenu.
Chaînage par hachage. Le champ previous_receipt_hash relie chaque reçu à celui qui le précède. Supprimer ou réarranger un reçu casse chaque reçu qui vient après. Toute altération devient visible au niveau de la chaîne même si les signatures individuelles sont contournées.
Ensemble, ces propriétés fournissent trois garanties :
Vous n’avez pas besoin d’une bibliothèque spéciale pour produire un reçu. Les primitives cryptographiques sont largement disponibles et la logique consiste en quelques dizaines de lignes Python.
Les exercices pratiques dans code_samples/18-signed-receipts.ipynb parcourent le flux complet. La version résumée :
import json
import hashlib
import base64
from nacl import signing
from jcs import canonicalize # JSON canonique RFC 8785
def b64url_nopad(data: bytes) -> str:
return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")
def sha256_canonical(obj) -> str:
"""SHA-256 of a Python object's JCS-canonical JSON form."""
return f"sha256:{hashlib.sha256(canonicalize(obj)).hexdigest()}"
# Générer ou charger une clé de signature (en production, stocker dans un coffre-fort à clés)
signing_key = signing.SigningKey.generate()
verify_key = signing_key.verify_key
# Construire la charge utile du reçu (pas encore de signature)
tool_args = {"origin": "SYD", "destination": "LAX"}
tool_result = [{"flight": "QF11", "price": 1850, "stops": 0}]
payload = {
"type": "agent.tool_call.v1",
"agent_id": "contoso-travel-bot",
"tool_name": "lookup_flights",
"tool_args_hash": sha256_canonical(tool_args),
"result_hash": sha256_canonical(tool_result),
"policy_id": "contoso-travel-policy-v3",
"timestamp": "2026-04-25T14:30:00Z",
"sequence": 0,
"previous_receipt_hash": None,
}
# Canonaliser, hacher, signer.
canonical_bytes = canonicalize(payload)
message_hash = hashlib.sha256(canonical_bytes).digest()
signature_bytes = signing_key.sign(message_hash).signature
# Joindre un objet de signature structuré.
receipt = {
**payload,
"signature": {
"alg": "EdDSA",
"sig": b64url_nopad(signature_bytes),
"public_key": b64url_nopad(bytes(verify_key)),
},
}
C’est toute la chaîne de signature. Les exercices dans le notebook parcourent chaque étape.
La vérification est l’opération inverse :
import base64
import hashlib
from nacl import signing
from nacl.exceptions import BadSignatureError
from jcs import canonicalize
def b64url_decode(s: str) -> bytes:
padding = "=" * ((4 - len(s) % 4) % 4)
return base64.urlsafe_b64decode(s + padding)
def verify_receipt(receipt: dict) -> bool:
# La signature est un objet structuré : {"alg", "sig", "public_key"}.
sig_obj = receipt.get("signature")
if not sig_obj or sig_obj.get("alg") != "EdDSA":
return False
# Reconstruisez la charge utile qui a été réellement signée (tout sauf la signature).
payload = {k: v for k, v in receipt.items() if k != "signature"}
canonical_bytes = canonicalize(payload)
message_hash = hashlib.sha256(canonical_bytes).digest()
try:
verify_key = signing.VerifyKey(b64url_decode(sig_obj["public_key"]))
verify_key.verify(message_hash, b64url_decode(sig_obj["sig"]))
return True
except BadSignatureError:
return False
Cette fonction prend un reçu et retourne True si la signature est valide, False sinon. Pas d’appel réseau, pas de dépendance de service, pas de confiance requise envers un tiers.
Pour voir la détection des altérations en action, le notebook parcourt :
tool_args_hash.Ceci est la démonstration pratique que les reçus sont à preuve d’altération : toute modification, même minime, casse la signature.
Un seul reçu signé protège une action. Une chaîne de reçus protège une séquence.
flowchart LR
R0[Reçu 0<br/>genèse] --> R1[Reçu 1]
R1 --> R2[Reçu 2]
R2 --> R3[Reçu 3]
R1 -. previous_receipt_hash .-> R0
R2 -. previous_receipt_hash .-> R1
R3 -. previous_receipt_hash .-> R2
Chaque reçu enregistre le hachage du reçu précédent. Pour supprimer silencieusement le reçu 2, un attaquant devrait soit :
previous_receipt_hash du reçu 3 (ce qui casse la signature du reçu 3), OUSi la clé privée est dans un coffre-fort matériel et que vous publiez la clé publique avec chaque reçu, aucune de ces attaques n’est faisable sans être détectée.
Le notebook parcourt :
previous_receipt_hash de chaque reçu correspond bien au hachage réel du reçu précédent.Voilà comment vous produisez une piste d’audit qu’un auditeur externe peut vérifier sans avoir besoin de vous faire confiance.
C’est la section la plus importante de cette leçon. Les reçus sont puissants mais leur puissance a des limites.
Les reçus prouvent trois choses :
Les reçus NE prouvent PAS :
policy_id a réellement été évaluée, ou qu’elle aurait permis cette action si elle avait été vérifiée. Le reçu enregistre ce qui a été affirmé, pas ce qui a été appliqué.Cette limite est importante pour deux raisons :
Une erreur fréquente est de penser que « nous avons des reçus » signifie « nous sommes gouvernés ». Ce n’est pas le cas. Les reçus sont une base. La gouvernance est le système que vous construisez par-dessus.
Le point 3 ci-dessus mérite sa propre section : un reçu d’action dit « cette clé a signé ce contenu », jamais « un humain a autorisé cela ». Pour les actions à haut risque (remboursements, suppressions, virements), les cadres de gouvernance exigent de plus en plus cette affirmation manquante, et elle est réalisable avec les mêmes primitives que vous avez déjà construites dans cette leçon.
Le notebook suivant code_samples/human-authorization-receipts.ipynb ajoute un second type de reçu, human.approval.v1, dans la même forme d’enveloppe que les reçus de la leçon (une charge utile typée signée par Ed25519 sur son SHA-256 canonique, avec l’objet signature en dehors des octets signés). Un approbateur nommé signe l’action canonique complète et son condensé avant exécution ; le reçu d’action de l’agent porte le même condensé d’action et une parent_approval_ref, le receipt_hash de l’approbation, la même convention que previous_receipt_hash dans la chaîne que vous avez construite plus haut. Une seule fonction verify_chain parcourt les deux artefacts sous des registres de clés épinglés séparés (clés des approbateurs vs clés de l’agent), donc le chemin de code est partagé mais pas les autorités.
La propriété obtenue, énoncée soigneusement : l’humain a approuvé cette action exacte, et l’agent a exactement exécuté cette action approuvée. Les cas de refus du notebook sont ce qui rend cette propriété réelle plutôt que simplement affirmée :
Chaque échec refuse avec une raison distincte, ainsi un auditeur lisant un refus peut dire si l’autorité a périmé ou si l’action exécutée a changé. La règle enseignée par le notebook : une approbation signée n’est pas une autorité en soi. L’autorité existe seulement si les deux reçus lient encore à la même action canonique au moment de l’exécution. Le chemin de co-signature dans le même Internet-Draft que cette leçon suit (draft-farley-acta-signed-receipts) est la forme officielle de cette approche.
Le code Python de cette leçon est volontairement minimal pour que vous puissiez lire chaque ligne et comprendre exactement ce qui se passe. En production, vous avez deux options :
Construire directement sur les primitives cryptographiques. Les 50 lignes vues plus haut suffisent pour de nombreux cas d’usage. PyNaCl (Ed25519) et le paquet jcs (JSON canonique) sont des bibliothèques bien maintenues et auditées.
Utiliser une bibliothèque de reçus en production. Plusieurs projets open source implémentent le même modèle avec des fonctionnalités supplémentaires (rotation de clés, vérification par lots, distribution de JWK Set, intégration avec moteurs de politique) :
draft-farley-acta-signed-receipts, révision 02) actuellement en cours de standardisation, avec une suite de conformité partagée (agent-governance-testvectors) que les implémentations indépendantes vérifient avec pour sortie canonique identique aux octets près.protect-mcp (npm) et @veritasacta/verify (npm) fournissent une implémentation Node de la signature et vérification hors ligne des reçus, destinée à envelopper tout serveur MCP avec une piste d’audit à preuve d’altération, incluant un flux de tenue pour co-signature dans lequel une action en pause émet un reçu d’approbation lié au condensé d’action (soutenu par WebAuthn dans le flux desktop), le même modèle de reçu d’approbation que dans le notebook d’autorisation humaine ci-dessus.pip install nobulex) fournit le même modèle de signature Ed25519 + JCS en Python avec intégrations LangChain et CrewAI, incluant des vecteurs de test de validation croisée publiés et une cartographie de conformité contributive via OWASP PR #2210.Le choix entre réaliser votre propre solution et utiliser une bibliothèque reflète celui entre écrire votre propre bibliothèque JWT et en utiliser une testée : les deux sont raisonnables ; la bibliothèque économise du temps et réduit la surface d’audit ; l’approche from scratch vous force à comprendre chaque primitive. Cette leçon enseigne la voie from scratch pour que vous ayez la base pour chaque choix.
Testez votre compréhension avant de passer à l’exercice pratique.
1. Un reçu est signé avec la clé privée Ed25519 de l’agent. L’auditeur n’a que la clé publique. L’auditeur peut-il vérifier le reçu hors ligne ?
2. Un attaquant modifie le champ policy_id d’un reçu pour prétendre qu’il était gouverné par une politique plus permissive. La signature portait sur la charge utile originale. Que se passe-t-il lors de la vérification ?
3. Pourquoi le reçu inclut-il un tool_args_hash et un result_hash plutôt que les arguments bruts et le résultat ?
4. Le champ previous_receipt_hash relie chaque reçu à son prédécesseur. Si un attaquant supprime silencieusement un reçu au milieu d’une chaîne, qu’est-ce qui devient invalide ?
5. Un reçu se vérifie correctement. Cela prouve-t-il que l’action de l’agent était correcte, fiable ou conforme à la politique ?
Ouvrez code_samples/18-signed-receipts.ipynb et complétez les quatre sections :
Défi supplémentaire 1 : étendez le schéma du reçu avec un champ supplémentaire de votre choix (par exemple, un identifiant de requête pour le traçage), mettez à jour la logique de signature canonique pour l’inclure, et confirmez que le reçu circule encore correctement lors de la vérification. Puis modifiez le champ après la signature et confirmez que la vérification échoue. Cela vous oblige à comprendre comment chaque octet de l’encodage canonique contribue à la signature.
Défi supplémentaire 2 : Hachez SHA-256 deux de vos reçus ensemble (concaténez leurs octets canoniques dans un ordre déterministe) et intégrez le résultat comme un nouveau champ dans un troisième reçu avant de le signer. Vérifiez que les trois reçus circulent toujours correctement. Vous venez de construire une preuve d’inclusion en un seul niveau : toute personne détenant le troisième reçu peut prouver que les deux premiers existaient au moment de la signature, sans avoir besoin de révéler leur contenu. C’est le modèle utilisé à grande échelle par les reçus à divulgation sélective (engagements de Merkle, RFC 6962).
Les reçus cryptographiques fournissent aux agents IA une piste d’audit qui est :
Ils ne remplacent pas la validation des entrées, l’application des politiques ou l’infrastructure d’identité. Ils constituent une base pour ces couches. Lorsque vous déployez des agents dans des charges régulées, des flux de travail multi-organisationnels ou tout contexte où un auditeur futur ne peut pas être supposé vous faire confiance, les reçus sont la façon dont vous rendez la piste d’audit honnête.
L’enseignement le plus important : les reçus prouvent qui a dit quoi, quand. Ils ne prouvent pas que ce qui a été dit est vrai ou juste. Gardez cette distinction bien présente. C’est la différence entre un système de provenance honnête et un système trompeur.
Lorsque vous êtes prêt à passer de cette leçon au déploiement d’agents signés par reçu en production :
https://your-org.example.com/.well-known/agent-keys.json.Rejoignez le Microsoft Foundry Discord pour rencontrer d’autres apprenants, participer aux heures de bureau, et obtenir des réponses à vos questions sur les agents IA.
Cette leçon couvre la signature d’un seul reçu et les séquences en chaîne de hachage. Les mêmes primitives se combinent en plusieurs modèles plus avancés que vous rencontrerez au fur et à mesure que votre posture de gouvernance mûrit :
authorization_*) et post-exécution (result_*) avec des signatures indépendantes, utile quand la décision d’autorisation et le résultat observé sont produits par des acteurs ou à des moments différents. Cela s’ajoute au format de reçu enseigné ici.result_hash. Les charges réelles sont souvent plus riches que le seul résultat d’un appel d’outil : le raisonnement pré-décision (prédiction du modèle, options considérées, preuves et leur exhaustivité, posture de risque, chaîne de responsabilité, résultat des contrôles) peut tous vivre dans la charge, scellée par un seul reçu. Cela maintient le format du reçu minimal tout en laissant évoluer les schémas de charge par domaine.signature.alg peut porter ML-DSA-65 (la norme de signature post-quantique NIST) dès que vous devez migrer. Prévoyez une période de transition où les reçus sont doublement signés.Avertissement : Ce document a été traduit à l’aide du service de traduction automatique Co-op Translator. Bien que nous nous efforçions d’assurer l’exactitude, veuillez noter que les traductions automatisées peuvent contenir des erreurs ou des inexactitudes. Le document original dans sa langue native doit être considéré comme la source faisant autorité. Pour les informations critiques, il est recommandé de recourir à une traduction professionnelle réalisée par un humain. Nous ne saurions être tenus responsables des malentendus ou erreurs d’interprétation découlant de l’utilisation de cette traduction.