Aller au contenu

audit

Preuve d'audit qu'un vérificateur peut vérifier hors ligne

Par Olivares AI 14 min de lecture

Votre auditeur demande des preuves de ce que vos agents IA ont fait au cours du dernier trimestre. Vous leur remettez une exportation CSV du journal d’audit. Ils posent une question : « Pouvez-vous prouver que cela n’a pas été modifié après coup ? » La plupart des équipes ne le peuvent pas. Le journal réside dans une base de données mutable, exportée par le même système qui l’a écrit. L’auditeur doit parcourir toute la piste sur la foi, ce qui est précisément la propriété qui fait qu’elle n’est pas une preuve.

Cet article explique comment la plateforme construit des éléments probants qui résistent sans foi. Un article précédent explique pourquoi l’identité par agent et un registre inviolable sont importants pour les déploiements Claude Code et MCP. Celui-ci approfondit les trois propriétés qui rendent les preuves vérifiables de manière indépendante : le chaînage de hachage, la signature cryptographique par événement et l’exportation dans un format de conformité lisible par machine qu’un outil d’audit comprend déjà.

La chaîne de hachage : chaque événement s’engage sur tous les événements précédents

Le grand livre est uniquement ajouté et chaîné par hachage par locataire. Chaque événement porte le hachage SHA-256 de l’événement précédent dans son champ prev_hash. Le hachage de chaîne de l’événement N est calculé sur une pré-image binaire canonique qui comprend : les propres champs de l’événement N (locataire, numéro de séquence, horodatage, acteur, action, cible, résumé des métadonnées, hachage de charge utile) concaténés avec le prev_hash de l’événement N-1. À la séquence 1, prev_hash est entièrement composé de zéros – l’ancre de la genèse.

La propriété critique : modifier, insérer ou supprimer n’importe quel événement au milieu de la chaîne change son hachage, ce qui rompt le lien prev_hash de l’événement suivant, qui rompt le suivant, et ainsi de suite jusqu’à la pointe. Une seule modification est détectable en parcourant la chaîne et en recalculant chaque hachage.

La pré-image est un codage binaire fixe avec un préfixe de longueur avec un séparateur de domaine versionné – et non JSON. Il s’agit d’un choix délibéré contre trois surfaces d’attaque qu’un hachage basé sur JSON laisserait ouvertes :

  • Ordre des clés : l’ordre des clés des objets JSON n’est pas garanti par la plupart des sérialiseurs. Un ordre de clé différent produit un hachage différent, même pour des données sémantiquement identiques, ce qui crée de fausses ruptures de chaîne – ou pire, permet à un attaquant de réorganiser les clés pour forger un hachage correspondant.
  • Formatage des espaces et des nombres : {"seq": 1}, {"seq":1} et {"seq": 1.0} sont des JSON sémantiquement équivalents mais produisent des résumés SHA-256 différents.
  • Faux de concaténation : sans préfixes de longueur, deux champs courts adjacents peuvent être fusionnés pour forger un troisième champ long avec le même hachage. Le préfixe de longueur de chaque champ avec son nombre d’octets (big-endian de 4 octets) ferme cette opération.

L’implémentation dans core/internal/store/canon/canon.go est la seule source de vérité. Append (écriture) et Verify (lecture) appellent la même fonction EventHash. Il n’y a pas de deuxième implémentation à dériver :

func EventHash(e Event) []byte {
    var buf []byte
    buf = lps(buf, domainEvent)       // "olivares.audit.v1"
    buf = lps(buf, e.TenantID)
    var seq [8]byte
    binary.BigEndian.PutUint64(seq[:], uint64(e.Seq))
    buf = append(buf, seq[:]...)
    buf = lps(buf, e.OccurredAt)
    buf = lps(buf, e.Actor)
    buf = lps(buf, e.ActorKind)
    buf = lps(buf, e.Action)
    buf = lps(buf, e.TargetKind)
    buf = lps(buf, e.TargetID)
    buf = append(buf, fixed(e.MetaDigest)...)
    buf = append(buf, fixed(e.PayloadHash)...)
    buf = append(buf, fixed(e.PrevHash)...)
    sum := sha256.Sum256(buf)
    return sum[:]
}

Le séparateur de domaine "olivares.audit.v1" lie le hachage à son objectif et à sa version. Un hachage d’un domaine différent (un point de contrôle, une charge utile, un résumé de métadonnées) ne peut jamais entrer en collision avec un hachage d’événement, même si les octets bruts correspondent.

Signature Ed25519 : chaque événement est son propre point d’ancrage

Une chaîne de hachage prouve la cohérence interne, mais pas l’authenticité. Un attaquant disposant d’un accès brut en écriture à la base de données pourrait recalculer la chaîne entière à partir de zéro avec des événements modifiés et produire une chaîne valide, différente de l’originale, mais cohérente en interne. Les chaînes de hachage détectent la falsification ; ils ne prouvent pas l’origine.

Les signatures Ed25519 par événement clôturent ce gap. Chaque événement ajouté au grand livre est signé au moment de l’écriture. La signature couvre une pré-image séparée par domaine du locataire, un numéro de séquence et le hachage de chaîne de l’événement :

domain ("olivares.audit.event.v1") || tenant || seq (8 bytes, big-endian) || hash

La signature est stockée sur l’événement mais est exclue de la préimage de hachage de chaîne par conception. Ce n’est pas accidentel : si la signature était incluse dans le hachage, signer un événement modifierait le hachage qu’il est censé attester. La signature atteste le hachage sans l’altérer.

Un vérificateur externe détenant uniquement la clé publique peut confirmer chaque événement individuellement : recalculer le hachage de chaîne à partir des champs de l’événement, reconstruire la pré-image et vérifier la signature Ed25519. Si un événement a été modifié après la signature, la vérification de la signature échoue pour cet événement spécifique : le vérificateur n’a pas besoin de faire confiance au système qui a produit la preuve.

La plateforme prend également en charge la rotation des clés. Une chaîne dont la clé de signature a changé en cours de vie vérifie de bout en bout en épinglant la clé actuelle ainsi que les clés publiques des générations précédentes. La fonction de vérification accepte un ensemble de clés candidates et considère un événement valide si un candidat le vérifie.

En plus des signatures par événement, des points de contrôle périodiques légalisent la pointe de la chaîne sous un domaine de signature distinct (olivares.audit.checkpoint.v1). Pour les organisations qui doivent se défendre contre les compromissions au niveau de l’hôte - et pas seulement au niveau de la base de données - les points de contrôle peuvent être signés par une clé KMS/HSM prête à l’emploi (AWS KMS, GCP Cloud KMS, Azure Key Vault) où la clé privée ne réside jamais sur l’hôte. Les signatures par événement gèrent l’attaquant uniquement DB ; les points de contrôle hors boîte gèrent l’attaquant qui compromet l’hôte. Les deux modèles de menace sont distincts ; aucune des deux signatures ne couvre à elle seule les deux.

Le contrat de grand livre : scellé dans la même transaction

Un échec courant dans les systèmes d’audit est la cohérence éventuelle entre la mutation d’état et le dossier d’audit. L’état change, l’écriture d’audit est mise en file d’attente ou par lots, et si l’écriture d’audit échoue, le changement d’état a déjà été validé. Le résultat : des mutations non auditées qui existent dans le système mais pas dans les preuves.

La plateforme applique un contrat plus solide. La mutation d’état et le sceau du grand livre se produisent dans la même transaction de base de données. Si le sceau échoue, la transition entière est annulée – la mutation d’état ne s’engage jamais. Il ne s’agit pas de faire de son mieux ; il est fermé par refus.

Le grand livre d’exécution de session illustre cela. Lorsqu’une session Claude Code change d’état (créée, lancée, arrêtée, arrêtée, échouée), appendRunEvent enregistre la transition à deux endroits de manière atomique :

  1. Le grand livre d’audit mondial chaîné par hachage via sc.Audit().Append — la chaîne inviolable ancrée par un PayloadHash.
  2. Le grand livre interrogeable par session : une projection en annexe uniquement liée à la chaîne mondiale par audit_seq.

Les deux écritures se produisent dans la transaction Mutate de l’appelant. Le commentaire de code dans runtime_ledger.go indique directement l’intention de conception : “le grand livre est le système d’enregistrement, donc si le sceau échoue, toute la transition est annulée - ce n’est PAS le meilleur effort.”

Le PayloadHash lui-même s’engage uniquement sur les faits de transition canoniques et non sensibles : la référence d’exécution, la séquence, le type d’événement, la transition d’état et l’horodatage. Il n’inclut jamais de contenu de transcription, d’invites, de valeurs d’environnement ou de secrets. Le grand livre prouve ce qui s’est passé ; il ne stocke pas ce qui a été dit.

Le même modèle s’applique aux mutations de fichiers d’espace de travail dans workspace_ledger.go. Une écriture, un mkdir, un déplacement ou une suppression de fichier est scellé avant l’exécution de l’opération du système de fichiers. Si la preuve ne peut pas être annexée, la mutation ne s’exécute pas. Le sceau porte le type d’opération, le chemin et un SHA-256 du contenu écrit - jamais les octets du contenu eux-mêmes.

Exportation OSCAL : preuves lisibles par machine ingérées par les outils de votre auditeur

Un registre inviolable est nécessaire mais pas suffisant pour un auditeur. Si les preuves sont dans un format propriétaire, l’auditeur dépend toujours de vos outils pour les interpréter. OSCAL — le langage d’évaluation des contrôles de sécurité ouverts, maintenu par le NIST — est le format qui clôture ce gap.

La plateforme exporte des packages de preuves scellées sous la forme d’un bundle OSCAL contenant trois modèles :

  • Définition des composants : capacités du plan de contrôle exprimées sous forme d’exigences mises en œuvre par rapport à un cadre de conformité (NIST SP 800-53, ISO 27001, EU AI Act et autres). Chaque exigence implémentée porte l’ID de contrôle, les clés de capacité qui en témoignent et le statut réel en tant que propriété personnalisée.
  • Résultats de l’évaluation : résultats par contrôle avec un statut conforme à la norme OSCAL. La cible de chaque résultat porte satisfied ou not-satisfied avec le statut précis du produit conservé dans le champ de motif.
  • Mappage de contrôle : un passage des contrôles du cadre au modèle de référence de capacité de la plateforme, à l’aide du modèle de mappage de contrôle OSCAL 1.2.0. La relation est toujours intersects-with : les fonctionnalités concernent une partie d’un contrôle. Il n’affirme jamais la conformité ; cette affirmation ne vit que dans les résultats d’évaluation, basés sur des preuves opérationnelles réelles.

La contrainte d’honnêteté dans l’export OSCAL mérite d’être mentionnée explicitement. L’énumération d’état de recherche de OSCAL a exactement deux valeurs : satisfied et not-satisfied. Il n’y a pas de « partial » ou « par conception ». Un contrôle partially implémenté, adressé par conception, gapped ou unmapped est mappé à OSCAL not-satisfied, avec le statut réel du produit dans status.reason et une propriété personnalisée sous le propre espace de noms de la plate-forme (https://olivares.ai/ns/oscal). L’exportation ne blanchit jamais un contrôle partially-met dans satisfied. Seuls les contrôles appuyés par des preuves opérationnelles réelles au moment du scellement reçoivent OSCAL satisfied.

Chaque document OSCAL contient des propriétés d’ancrage du grand livre : le hachage du manifeste, le numéro de séquence du grand livre au moment du scellement, le hachage du grand livre et le résultat de la vérification de l’intégrité. Il s’agit du pont entre le document OSCAL que l’auditeur lit dans son outil GRC et la chaîne d’inviolabilité sous-jacente qu’il peut vérifier de manière indépendante.

Activité vers le pipeline de preuves

Ce que signifie concrètement la vérification hors ligne

« Vérification hors ligne » n’est pas une expression marketing. Il décrit une procédure technique spécifique : le vérificateur prend les preuves exportées, exécute un outil de vérification sur une machine air-gapped et confirme l’intégrité des preuves sans aucun accès réseau au système qui les a produites.

L’exportation d’archives de la plateforme écrit une arborescence de répertoires de segments JSONL (une ligne par événement, JSON canonique) plus un manifeste par segment. Le manifeste enregistre la plage de séquences du segment, le nombre d’événements, les premier et dernier hachages de chaîne, un SHA-256 du fichier d’événements et le dernier hachage du segment précédent pour la continuité entre les segments.

Le vérificateur hors ligne (VerifyArchiveDir) effectue ensuite les opérations suivantes, entièrement en mémoire constante, sans appels réseau :

  1. Chargez les manifestes et associez-les à des fichiers d’événements. Un fichier d’événements parasites (pas de manifeste) ou un manifeste dont le fichier d’événements est manquant est un échec. L’unité de preuve est la paire.
  2. Diffusez chaque fichier d’événement ligne par ligne. Pour chaque événement, redérivez le hachage de chaîne des champs archivés à l’aide de la même fonction EventHash que celle utilisée par le système en direct. Comparez-le avec le hachage stocké. Vérifiez la liaison prev_hash et la séquence gap-liberté.
  3. Vérifiez la canonique. Réorganisez chaque ligne analysée et confirmez qu’elle produit une sortie identique aux octets du disque. Cela empêche la contrebande de champs inconnus ou les attaques de clés en double qui passeraient avec succès une vérification de hachage mais transporteraient des données cachées.
  4. Vérifiez les signatures Ed25519 par événement. Pour chaque événement sans point de contrôle, reconstruisez la pré-image de signature et vérifiez par rapport à la ou aux clés publiques épinglées.
  5. Vérifiez les signatures de point de contrôle. Pour chaque événement de point de contrôle, vérifiez la signature sous le domaine du point de contrôle par rapport à la ou aux clés de point de contrôle épinglées. Si seules les clés d’événement sont épinglées (pas de clé de point de contrôle), un événement de point de contrôle est marqué « invérifiable » – fermé par refus, non désactivable.
  6. Vérifiez la continuité entre les segments. Confirmez que la première séquence de chaque segment suit la dernière séquence du segment précédent plus un, et que le dernier hachage du segment précédent correspond au prev_segment_last_hash du segment actuel.
  7. Vérifiez le résumé du fichier d’événements. Le SHA-256 du fichier d’événements calculé pendant la diffusion doit correspondre au events_sha256 du manifeste.

Le vérificateur signale la première incohérence trouvée, avec le numéro de séquence spécifique et une raison lisible par machine : hash-mismatch, prev-mismatch, seq-gap, event-sig-invalid, event-sig-missing, checkpoint-sig-invalid, count-mismatch, events-sha256-mismatch ou segment-link-mismatch.

Une limitation honnête : le vérificateur hors ligne atteste exactement la plage qu’il a vérifiée et rien en dehors. Un préfixe ou une queue supprimé est indétectable hors ligne : le répertoire ne précise pas où la chaîne a commencé ou terminé. Le rapport de vérification comprend un champ Ranges par locataire avec un indicateur StartsMidChain afin que l’auditeur sache exactement ce qui a été attesté. Les points de contrôle signés en chaîne du système actif couvrent la queue ; l’exportation hors ligne couvre la plage archivée. Ensemble, ils forment l’attestation complète.

CalqueCe que ça prouveCe que cela ne prouve pas
Chaîne de hachageCohérence interne ; toute modification brise la chaîneOrigine (qui a écrit les événements)
Par événement Ed25519Origine ; chaque événement a été signé par le détenteur de la cléDéfense contre la compromission au niveau de l’hôte
Point de contrôle hors boîteRésistance à la compromission de l’hôte (clé KMS/HSM jamais sur l’hôte)Granularité par événement (couvre uniquement les points de contrôle)
Exportation OSCALPreuves de conformité lisibles par machine et cartographiées dans un cadreQue tous les contrôles sont pleinement respectés (seules les preuves réelles comptent)
Vérifier les archivesRedérivation hors ligne de tout ce qui précèdeÉvénements avant ou après la plage exportée

Le chemin du code : de la mutation de l’État aux preuves scellées

La séquence d’un changement d’état de session Claude Code à une preuve vérifiable touche trois niveaux. Dans runtime_ledger.go, la fonction appendRunEvent construit un PayloadHash en hachant les champs de transition canoniques avec préfixe de longueur :

func runEventPayloadHash(runRef string, seq int64, event, from, to, detail, atTS string) [32]byte {
    h := sha256.New()
    for _, part := range []string{
        runRef, strconv.FormatInt(seq, 10), event, from, to, detail, atTS,
    } {
        _, _ = h.Write([]byte(strconv.Itoa(len(part))))
        _, _ = h.Write([]byte{':'})
        _, _ = h.Write([]byte(part))
    }
    var sum [32]byte
    copy(sum[:], h.Sum(nil))
    return sum
}

Ce hachage est ensuite transmis à sc.Audit().Append, qui attribue le numéro de séquence suivant par locataire, établit un lien vers le hachage de l’événement précédent, calcule le hachage de chaîne de cet événement via EventHash, le signe avec Ed25519 et insère l’événement scellé, le tout dans la transaction de l’appelant.

Résultat : au moment où la transaction est validée, le changement d’état de la session et son enregistrement d’audit inviolable sont soit tous deux conservés, soit tous deux annulés. Il n’y a aucune fenêtre où l’état a changé, mais pas les preuves.

Liens

Articles liés

Questions fréquentes

Un attaquant qui compromet la base de données peut-il re-signer les événements falsifiés ?

Les signatures Ed25519 par événement protègent contre les compromissions de la base de données uniquement : sauvegardes volées, lignes injectées ou réplique avec un rôle de contournement RLS. La clé de signature réside dans le répertoire de données et non dans la base de données. En cas de compromission de l'hôte, la plate-forme prend en charge la signature de point de contrôle hors boîte via KMS/HSM (AWS KMS, GCP Cloud KMS, Azure Key Vault), où la clé privée ne réside jamais sur l'hôte. Les signatures par événement arrêtent les attaques au niveau de la base de données ; les points de contrôle hors boîte arrêtent ceux au niveau de l'hôte. Ni l’un ni l’autre ne couvre à lui seul les deux modèles de menace.

Que se passe-t-il si l'exportation OSCAL marque un contrôle comme satisfied mais que les preuves opérationnelles changent ultérieurement ?

L'exportation OSCAL mappe l'état du produit à l'énumération de l'état de recherche OSCAL au moment du scellement. Seuls les contrôles appuyés par des preuves opérationnelles réelles à ce moment-là reçoivent le OSCAL satisfied. Un contrôle avec le statut by_design, partial, gap ou unmapped est mappé à OSCAL not-satisfied, avec le statut précis du produit conservé dans le champ de motif et une propriété personnalisée. Chaque paquet de preuves scellé est immuable et horodaté. Si les preuves changent, un nouveau colis est scellé reflétant l’état actuel. L'ancien package reste intact et revérifiable, créant une série chronologique de posture de conformité plutôt qu'une seule assertion écrasable.

Découvrez ce que vos agents peuvent atteindre

Olivares AI est la plateforme ouverte et auto-hébergée pour votre parc informatique d'IA. Déployez-la sur votre propre infrastructure et obtenez la cartographie des accès que réclament vos équipes de sécurité et de plateforme.