./research / verifiable-audit-layer
Une couche d'audit vérifiable pour l'IA.
Une couche d'audit inviolable pour l'inférence de modèles et l'exécution d'agents : sérialisation canonique, hachage à séparation de domaines, signatures DSSE, un DAG de Merkle par exécution et une Merkle Mountain Range inter-exécutions qui prouve qu'aucune exécution n'a été supprimée. Chaque primitive a été choisie à partir d'un benchmark en tête-à-tête face aux vraies alternatives, avec les données brutes versionnées. Cette note est la conception et les expériences qui la sous-tendent.
Modèle de menace
Nous supposons un adversaire qui contrôle le stockage et peut réécrire des enregistrements après coup, y compris l'opérateur qui les a produits. L'objectif est que toute modification d'une exécution scellée soit détectable, que l'enregistrement reste vérifiable pendant une durée de conservation mesurée en années, et que la vérification n'expose aucune entrée privée. Deux propriétés restent distinctes de bout en bout : l'intégrité d'une seule exécution et la complétude de l'ensemble des exécutions. Un reçu par exécution donne la première et ne dit rien de la seconde, donc les deux sont conçues séparément.
Chaque choix ci-dessous a été fait de la même façon. Nous n'affirmons pas qu'une primitive est la meilleure ; nous la comparons aux alternatives crédibles sur la même machine et versionnons les données brutes, de sorte que tout chiffre ici se reproduit en une commande. Les temps sont le plus rapide de plusieurs exécutions après élimination de la chauffe, en conservant la moyenne et l'écart-type pour qu'une mesure instable soit signalée plutôt que cachée. Les nombres viennent d'un unique hôte AMD64 sous CPython 3.14 et sont significatifs comme ratios, pas comme valeurs absolues.
Expériences et résultats
Chaque jalon fut un test en tête-à-tête face aux vraies alternatives, pas une mesure unique. La vue d'ensemble est ci-dessous ; les résultats détaillés de chacun sont dans les sections qui suivent.
| Jalon | Testé contre | Résultat |
|---|---|---|
| Forme canonique | JSON naïf, CBOR | JCS, identique octet pour octet, ~3x le coût |
| Hachage | SHA-256, BLAKE3, keccak-256 | keccak natif 17x, mêmes octets |
| Cœur natif | backends pip vs Rust | scellage 6x, racine identique octet pour octet |
| Signature | ECDSA, BLS, ML-DSA | Ed25519, 64 B, déterministe |
| Post-quantique | ML-DSA 44 / 65 / 87 | par étapes, vérification à parité, 38x la taille |
| Trace par exécution | 8 attaques de falsification, surcoût | toutes détectées, ~121 us / événement |
| Journal inter-exécutions | reconstruction Merkle simple | MMR, ajout plat, 483x |
Sérialisation canonique
Une capsule doit se hacher vers les mêmes octets dans notre cœur Python, notre client TypeScript et l'outil propre d'un auditeur, sinon un enregistrement valide échoue à la vérification. Nous utilisons RFC 8785 (JSON Canonicalization Scheme), qui fixe le formatage des nombres, l'ordre des clés par unité de code UTF-16 et le rejet des valeurs non finies. Nous l'avons testé face à du JSON à clés triées naïvement et face à CBOR.
| Approche | Python vs Node | Coût |
|---|---|---|
| JSON trié naïf | diverge sur l'Unicode et les nombres | référence |
| RFC 8785 JCS | identique octet pour octet | environ 3x à la sérialisation |
| CBOR | binaire, exige un décodeur pour se lire | compact |
Le JSON naïf diverge entre runtimes sur l'ordre Unicode et le formatage des nombres, cassant ainsi en silence la vérification inter-langages. JCS est identique octet pour octet dans les deux, à environ 3x le temps de sérialisation, payé une fois par hachage et négligeable face au hachage et au travail réseau qui l'entourent. Nous en gardons exactement une implémentation. Quand notre module natif a livré son propre sérialiseur JSON, nous l'avons retiré, car un second canonicaliseur qui s'accorde sur les entrées ordinaires et diverge sur les cas limites qu'un adversaire peut choisir est pire qu'aucun.
Hachage et cœur natif
Les feuilles sont hachées avec BLAKE3 et les nœuds de Merkle avec keccak-256, tous deux avec la séparation de domaines RFC 6962 (préfixes distincts pour feuilles et nœuds internes) pour fermer la classe de seconde préimage où un nœud interne est présenté comme une feuille. keccak-256 n'est pas SHA3-256 ; les deux partagent une permutation mais diffèrent d'un octet de remplissage, et le vérificateur on-chain parle keccak, donc une substitution silencieuse ferait échouer chaque racine on-chain. La couche de hachage calcule donc l'algorithme demandé ou lève une erreur, jamais un sosie, et consigne l'algorithme dans chaque capsule pour qu'un vérificateur reproduise la fonction exacte au lieu de la déduire de ce qui est installé localement.
| Primitive, entrée de 64 B | Débit | Note |
|---|---|---|
| SHA-256 (stdlib) | 1,50M ops/s | référence, toujours présente |
| BLAKE3 (pip) | 1,44M ops/s | à parité avec SHA-256 |
| keccak-256 (pip) | 99k ops/s | 15x plus lent, le goulot |
| keccak-256 (cœur Rust) | 1,69M ops/s | 17x plus rapide que pip |
keccak n'a pas d'implémentation en bibliothèque standard, ce qui a rendu le module natif digne d'être construit. L'écart est le plus large aux entrées de taille d'événement parce que le coût de pycryptodome y est l'allocation d'objets par appel et non le hachage, exactement la taille qu'une trace d'audit hache le plus ; de bout en bout, le cœur natif fait passer le scellage d'une exécution de 1000 événements de 78 à 491 scellages par seconde, environ 6x. Point crucial : activer l'accélérateur ne peut pas changer le résultat : sceller la même exécution avec et sans le crate donne une racine identique bit pour bit, une suite de conformité vérifie les deux chemins face aux vecteurs publiés par les auteurs de l'algorithme, et la CI construit une seule roue à ABI stable (abi3) et vérifie cet unique artefact sur chaque Python de 3.10 à 3.14. L'installer change la vitesse et rien d'autre.
Signatures pour un enregistrement décennal
Une signature sur un enregistrement d'audit doit tenir aussi longtemps que l'enregistrement peut être contesté, et les obligations de journalisation de l'IA tendent désormais vers la décennie. Si le schéma casse alors qu'un enregistrement est encore dans sa fenêtre de conservation, un adversaire peut le falsifier et l'antidater, donc toute l'archive échoue d'un coup. Nous avons évalué Ed25519 face à ECDSA, BLS et la famille post-quantique du NIST ML-DSA, sur le débit et les tailles.
| Schéma | Vérification (ops/s) | Taille de signature |
|---|---|---|
| ECDSA P-256 | 16 887 | ~72 B |
| ML-DSA-44 (post-quantique) | 10 725 | 2420 B |
| Ed25519 | 10 375 | 64 B |
| BLS12-381 | impl. de référence seule | 96 B, agrège |
Deux résultats ont renversé des hypothèses que nous portions, et nous gardons les deux au dossier. Ed25519 n'est pas le plus rapide à vérifier : ECDSA P-256 a mesuré environ 1,6x plus vite, sur de l'assembleur de bibliothèque optimisé. Ed25519 reste le défaut sur la taille de signature et sur le déterminisme (RFC 8032), qui supprime la panne de réutilisation de nonce d'ECDSA qui fuit une clé privée alors que les signatures vérifient encore. Et les signatures post-quantiques ne sont pas lentes à vérifier : ML-DSA-44 a devancé de peu Ed25519, son coût étant la signature (environ 13x plus lent) et la taille (environ 38x les octets). Puisqu'une trace est signée une fois et vérifiée pendant des années, le côté coûteux est celui que nous faisons le moins. BLS a été rejeté sur une exigence, pas sur un chiffre : l'agrégation replie de nombreuses signatures en 96 octets, mais un agrégat vérifie tout-ou-rien et exige chaque clé et message, ce qui est incompatible avec la divulgation d'une branche d'une décision sans révéler le reste.
Le recadrage qui compte : les signatures ne sont pas confidentielles, donc « récolter maintenant, déchiffrer plus tard » ne s'applique pas, et la vraie menace est la falsification rétroactive dans la fenêtre de conservation. Ancrer une racine à un registre public prouve quand un enregistrement a existé indépendamment du schéma de signature, donc une clé cassée coûte la capacité de prouver qui a scellé un enregistrement mais pas quand ni quoi. L'ancrage est donc sur le chemin critique et ML-DSA est étagé derrière lui, l'enveloppe DSSE restant agile en algorithme et l'algorithme consigné par capsule.
La trace d'audit par exécution
Chaque exécution est une séquence d'événements en ajout-seul et chaînée par hachage (appels LLM, appels d'outils, récupérations, créations de sous-agents, approbations humaines, décisions de garde-fou), avec une racine de Merkle sur tous et une signature DSSE sur la racine. Seuls des hachages d'entrées et de sorties sont stockés, jamais les charges utiles, donc la trace préserve la vie privée par défaut. Le vérificateur ne renvoie pas seulement valide ou invalide ; il nomme la garantie qui a cédé.
| Opération (exécution de 102 événements) | Coût | Passage à l'échelle |
|---|---|---|
| Enregistrer un événement | ~121 us | plat, sous 0,01% d'un pas |
| Sceller, bâtir la racine | 0,15 ms | linéaire en événements |
| Vérifier, recalculer toutes les feuilles | 9,2 ms | linéaire, environ 60x le scellage |
Enregistrer représente moins d'un centième de pour cent d'un pas d'agent typique, dominé par l'aller-retour du modèle, donc le surcoût n'est pas une raison d'échantillonner et la trace reste complète plutôt que statistique. La vérification est bien plus lourde que le scellage car elle recalcule chaque feuille à partir du contenu tandis que le scellage ne bâtit l'arbre que sur des hachages déjà présents. Cette asymétrie est correcte pour une trace d'audit, où l'enregistrement est constant et la vérification rare, et elle désigne la revérification en masse comme l'endroit où le cœur natif et le parallelisme paieront ensuite. Pour rendre les garanties concrètes, une démo attaque une exécution scellée de huit façons, chacune rejetée avec un motif précis :
| Attaque | Rejetée comme |
|---|---|
| Réécrire une sortie du modèle | discordance de hachage de feuille |
| Supprimer ou insérer un événement | discordance du nombre d'événements |
| Réordonner des événements | rupture de chaîne |
| Tronquer l'exécution | discordance de tête de chaîne |
| Rétrograder l'algorithme de hachage | discordance de hachage de feuille |
| Présenter sous la mauvaise clé | la signature ne vérifie pas |
Le journal de transparence inter-exécutions
Une capsule par exécution valide ne peut prouver que l'ensemble des exécutions est complet. Un opérateur qui écarte une exécution gênante détient une collection de capsules individuellement parfaites et une histoire trouée. Nous refermons cela avec un unique journal en ajout-seul de racines de capsules, portant des preuves d'inclusion (une exécution est dans le journal) et des preuves de cohérence (le journal à une taille antérieure est un préfixe exact du journal actuel). La cohérence attrape la censure : un journal reconstruit avec une exécution retirée est intrinsèquement bien formé et chaque capsule survivante vérifie encore, mais il échoue à un contrôle de cohérence face à la racine publiée avant, et le même contrôle rejette une insertion antidatée. Cela ne marche que face à une racine actée avant l'altération, donc cette racine doit vivre quelque part que l'opérateur ne peut réviser, comme un ancrage public.
Nous utilisons une Merkle Mountain Range plutôt qu'un arbre de Merkle simple parce qu'un ajout ne fait qu'ajouter des nœuds et n'en réécrit jamais un, ce qui laisse le journal vivre sur du stockage en écriture unique ou objet et garde valides les vieilles preuves d'inclusion à mesure que le journal grandit. Cette propriété est quasi optimale, pas seulement commode : ePrint 2025/234 prouve que tout engagement succinct en ajout-seul doit induire un nombre superlinéaire de mises à jour de témoins, et une variante proche de MMR atteint essentiellement la borne.
| Taille du journal | Construction MMR | Reconstruction simple |
|---|---|---|
| 500 | 1,3 ms | 158 ms (120x) |
| 1000 | 2,6 ms | 618 ms (237x) |
| 2000 | 5,2 ms | 2491 ms (483x) |
L'ajout reste plat à mesure que le journal grandit (environ 390k ajouts par seconde à 100 comme à 10 000 entrées) tandis que reconstruire un arbre simple chute de 8176 à 84 racines par seconde sur la même plage, la signature quadratique-contre-linéarithmique. Le stockage se stabilise à 2,00 hachages stockés par entrée et les preuves d'inclusion ne croissent que de 7 à 17 étapes entre 100 et 100 000 entrées. Où nous perdons : nos preuves de cohérence sont en O(log^2 n), portant un chemin par pic de l'ancien journal au lieu du O(log n) atteignable, quelques kilooctets à 100 000 entrées et un compromis délibéré pour une construction qu'un relecteur peut vérifier en la lisant.
Limites que nous ne franchissons pas
Le hachage donne la preuve de falsification, pas l'inviolabilité. Un adversaire qui reconstruit une exécution et recalcule sa racine produit un enregistrement autocohérent, car une racine de Merkle prouve que les événements correspondent à la racine, non que la racine soit celle qui a été publiée. La signature attrape cela, et l'ancrage public prouve le moment. Le journal de transparence repose à grande échelle sur la même hypothèse : il ne détecte une exécution supprimée que par rapport à une racine actée avant la suppression. Nous énonçons ces limites dans le produit et ses démos, et un test affirme que la démo de falsification continue de montrer l'attaque que nous n'arrêtons pas, pour que le cas honnête ne puisse disparaître en silence.
Reproductibilité
Chaque chiffre ici provient d'un benchmark versionné qui se relance en une commande, aux côtés des tests, des suites de conformité inter-langages et inter-implémentations, et de la matrice CI qui exerce Python 3.10 à 3.14 avec et sans le cœur natif. La méthodologie, le JSON brut et les notes de décision (y compris les dimensions où chaque choix perd) sont dans le dépôt, car une couche qui rend l'IA auditable doit elle-même être auditable.