Regardez la vidéo de la leçon : Sécuriser les agents IA avec des reçus cryptographiques
(Vidéo de la leçon et vignette à ajouter par l’équipe de contenu Microsoft après fusion, suivant le modèle des leçons 14 / 15.)
Cette leçon couvrira :
Après avoir suivi 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, appelle une API de vols pour rechercher des options, et réserve des sièges au nom 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 remettez vos fichiers de journalisation. L’auditeur les consulte et pose une 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 moyens ne peut répondre à la question de l’auditeur sans que celui-ci ait à 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 de travail régulées (finance, santé, tout ce qui est soumis au règlement européen sur l’IA), ce n’est pas le cas.
Les reçus cryptographiques résolvent ce problème en rendant chaque action d’agent indépendamment vérifiable. 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 --> E[Signer les octets canoniques avec Ed25519]
E --> F[Reçu avec signature]
F --> G[L'auditeur vérifie hors ligne]
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 assurent le fonctionnement :
La signature. Le reçu est signé par la passerelle de l’agent à l’aide d’une clé privée Ed25519. Toute personne disposant de la clé publique correspondante peut vérifier la signature hors ligne. Toute modification d’un champ invalide la signature.
Encodage canonique. Avant la signature, le reçu est sérialisé selon le JSON Canonicalization Scheme (JCS, RFC 8785). Cela garantit que deux implémentations produisant le même reçu logique produisent une sortie identique au niveau octet. Sans canonisation, différents sérialiseurs JSON produiraient des signatures différentes pour un même contenu.
Chaînage par hachage. Le champ previous_receipt_hash relie chaque reçu au précédent. La suppression ou le réordonnancement d’un reçu casse tous les reçus qui suivent. La falsification 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 nécessite quelques dizaines de lignes de Python.
Les exercices pratiques dans code_samples/18-signed-receipts.ipynb parcourent tout le processus. 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 dépôt de 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,
}
# Canoniciser et signer directement les octets JCS. PureEdDSA hache en interne.
canonical_bytes = canonicalize(payload)
signature_bytes = signing_key.sign(canonical_bytes).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 détaillent 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
# Reconstruire la charge utile qui a été effectivement signée (tout sauf la signature).
payload = {k: v for k, v in receipt.items() if k != "signature"}
canonical_bytes = canonicalize(payload)
try:
verify_key = signing.VerifyKey(b64url_decode(sig_obj["public_key"]))
verify_key.verify(canonical_bytes, 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 à un service, pas besoin de faire confiance à un tiers.
Pour voir la détection de falsification en action, le notebook détaille :
tool_args_hash.C’est la démonstration pratique que les reçus sont évidents à falsifier : toute modification, si minime soit-elle, casse la signature.
Un reçu signé protège une action unique. 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 stockée dans un coffre-fort matériel et que vous publiez la clé publique avec chaque reçu, aucune des attaques n’est réalisable sans détection.
Le notebook détaille :
previous_receipt_hash de chaque reçu correspond bien au hachage réel du reçu précédent.C’est ainsi que vous produisez une piste d’audit qu’un auditeur externe peut vérifier sans 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 importe pour deux raisons :
Une erreur fréquente est de supposer 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 bancaires), les cadres de gouvernance exigent de plus en plus précisément cette affirmation absente, qui est produite 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, avec la même structure enveloppe que les reçus de la leçon (une charge utile typée signée par Ed25519 sur ses octets JCS canoniques, avec l’objet signature en dehors des octets signés). Un approbateur nommé signe l’action canonique complète et son digest avant exécution ; le reçu d’action de l’agent porte le même digest d’action et un parent_approval_ref, le receipt_hash de l’approbation, la même convention que previous_receipt_hash dans la chaîne construite ci-dessus. Une seule fonction verify_chain valide les deux artefacts sous des registres de clés épinglés séparés (clés d’approbateurs vs clés d’agents), donc le chemin de code est partagé mais les autorités ne le sont jamais.
La propriété obtenue, formulée soigneusement : l’humain a approuvé cette action exacte, et l’agent a exécuté exactement cette action approuvée. Les simulations de refus du notebook rendent cette propriété réelle plutôt que simplement affirmée :
Chaque échec refuse avec une raison distincte, donc un auditeur lisant un refus peut savoir si l’autorité est périmée ou si l’action exécutée a changé. La règle enseignée dans le notebook : une approbation signée n’est pas une autorité en soi. L’autorité existe seulement si les deux reçus lient toujours à la même action canonique au moment de l’exécution. Le reçu d’approbation humaine est une composition éducative définie par cette leçon, pas un type de reçu défini par draft-farley-acta-signed-receipts.
Le code Python de cette leçon est volontairement minimal pour que vous puissiez lire chaque ligne et comprendre précisément ce qui se passe. En production, vous avez deux options :
Construire directement sur les primitives cryptographiques. Les 50 lignes vues ci-dessus 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 prête à l’emploi. Plusieurs projets open source implémentent ce même modèle avec des fonctionnalités supplémentaires (rotation de clés, vérification par lots, distribution de jeu de clés JWK, intégration avec moteurs de politique) :
draft-farley-acta-signed-receipts, révision 02). Le reçu plat éducatif de cette leçon diffère de l’enveloppe {payload, signature} du draft et n’est pas présenté comme une implémentation conforme. Le draft publie une suite de conformité partagée (agent-governance-testvectors) pour implémentations ciblant ce format.protect-mcp (npm) et @veritasacta/verify (npm) fournissent une implémentation Node de signature de reçus et vérification hors ligne, destinés à envelopper tout serveur MCP avec une piste d’audit évidente à falsifier, incluant un flux held-for-co-sign dans lequel une action mise en pause émet un reçu d’approbation lié au digest de l’action (soutenu par WebAuthn dans le flux desktop), le même modèle d’approbation que celui du 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é contribué via OWASP PR #2210.Le choix entre développer soi-même et utiliser une bibliothèque reflète celui entre écrire sa propre bibliothèque JWT et en utiliser une testée : les deux sont raisonnables ; la bibliothèque fait gagner du temps et réduit la surface d’audit ; la méthode from-scratch vous force à comprendre chaque primitive. Cette leçon enseigne la voie from-scratch pour vous donner la base dans les deux cas.
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 dispose seulement de 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 soumis à 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 et résultats bruts ?
4. Le champ previous_receipt_hash lie chaque reçu à son prédécesseur. Si un attaquant supprime silencieusement un reçu du milieu d’une chaîne, qu’est-ce qui devient invalide ?
5. Un reçu est vérifié avec succès. Cela prouve-t-il que l’action de l’agent était correcte, valide 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 de reçu avec un champ supplémentaire de votre choix (par exemple, un ID de requête pour le traçage), mettez à jour la logique de signature canonique pour l’inclure, et confirmez que le reçu passe toujours la vérification bidirectionnelle. Puis modifiez ce champ après signature et confirmez l’échec de la vérification. Cela vous force à comprendre comment chaque octet de l’encodage canonique contribue à la signature.
Défi supplémentaire 2 : Hachez avec SHA-256 deux de vos reçus ensemble (concaténez leurs octets canoniques dans un ordre déterministe) et intégrez le digest résultant comme un nouveau champ sur un troisième reçu avant de le signer. Vérifiez que les trois reçus passent toujours la vérification bidirectionnelle. Vous avez ainsi construit une preuve d’inclusion en une étape : toute personne possédant le troisième reçu peut prouver que les deux premiers existaient au moment de sa signature, sans devoir révéler leur contenu. C’est le modèle utilisé à grande échelle dans 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 la base pour ces couches. Lorsque vous déployez des agents dans des environnements réglementés, des flux de travail multi-organisation, ou tout contexte où un futur auditeur ne peut pas être supposé vous faire confiance, les reçus sont la manière de rendre la piste d’audit honnête.
L’essentiel à retenir : les reçus prouvent qui a dit quoi, et quand. Ils ne prouvent pas que ce qui a été dit était vrai ou juste. Gardez bien cette distinction. 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 signant des reçus en environnement réel :
https://your-org.example.com/.well-known/agent-keys.json.Rejoignez le Microsoft Foundry Discord pour rencontrer d’autres apprenants, assister 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 hachages. Les mêmes primitives s’assemblent en plusieurs modèles plus avancés que vous pouvez rencontrer à mesure que votre posture de gouvernance mûrit :
authorization_*) et post-exécution (result_*), avec signatures indépendantes, utile lorsque la décision d’autorisation et le résultat observé sont produits par différents acteurs ou à différents moments. Cela s’ajoute de façon additive au format de reçu enseigné dans cette leçon.result_hash. Les charges utiles réelles sont souvent plus riches qu’un simple résultat d’appel d’outil : raisonnement pré-décision (prédiction modèle, options considérées, preuves et leur complétude, posture de risque, chaîne de responsabilité, résultat du gate) peuvent tous vivre dans la charge utile, scellée par un seul reçu. Cela maintient le format de reçu minimal tout en laissant évoluer les schémas de charge utile domaine par domaine.signature.alg peut porter ML-DSA-65 (standard de signature post-quantique NIST) lorsque vous devez migrer. Prévoyez une période de transition où les reçus sont signés en double.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.