# La vérification d’un journal d’audit chiffré peut-elle détecter les suppressions ?

Une chaîne de hachage peut détecter un enregistrement manquant au milieu d’un fichier d’audit, mais elle ne peut pas déterminer de manière fiable si quelqu’un a supprimé des enregistrements à la fin. C’est sur cette distinction que les équipes surestiment parfois ce que leur vérificateur prouve. Elles découvrent ensuite, lors d’un incident, qu’elles ont conservé des preuves d’intégrité sans conserver l’historique complet.

Le chiffrement ne change rien à cette conclusion. Il protège le contenu des enregistrements. Une chaîne construite sur des octets chiffrés peut montrer que ces octets sont toujours reliés comme prévu, même si personne ne peut les déchiffrer lors de la vérification. Elle ne peut pas prouver que le fichier contient tous les enregistrements qui ont existé, sauf si un élément extérieur au fichier conserve la preuve d’un point ultérieur de son historique.

Le NIST décrit une chaîne de hachage comme une structure en ajout seulement où chaque bloc contient le hachage des données précédentes. Modifier un bloc change donc le condensat enregistré par son successeur. C’est une preuve d’altération, pas une machine à remonter le temps.

## La vérification de la chaîne contrôle une relation, pas l’exhaustivité

Un vérificateur contrôle si chaque enregistrement pointe vers le bon prédécesseur et si son condensat correspond aux octets stockés. À partir d’un premier enregistrement fiable, il peut établir la continuité jusqu’à chaque enregistrement encore présent.

Supposons qu’un fichier contienne les enregistrements 1 à 100. L’enregistrement 58 contient le condensat de l’enregistrement 57, et l’enregistrement 59 contient celui de l’enregistrement 58. Si quelqu’un supprime l’enregistrement 58 sans rien changer d’autre, l’enregistrement 59 pointe toujours vers un condensat que le vérificateur ne trouve pas. La vérification échoue au niveau de la lacune.

Supprimons maintenant les enregistrements 91 à 100. L’enregistrement 90 pointe toujours correctement vers l’enregistrement 89. Rien dans le fichier raccourci n’indique que l’enregistrement 90 était autrefois suivi de dix autres enregistrements. Un vérificateur qui commence à l’enregistrement 1 et s’arrête à la fin du fichier peut renvoyer un succès. Il a vérifié l’historique qui lui a été présenté. Il n’a pas vérifié que cet historique était complet.

On regroupe souvent trois affirmations distinctes sous l’expression « le journal est inviolable » :

- Les enregistrements restants n’ont pas été modifiés.
- Aucun enregistrement n’a été supprimé au milieu.
- Le fichier se termine là où l’historique d’origine se terminait.

Ces affirmations exigent des preuves différentes. Une chaîne linéaire répond bien à la première et détecte une forme simple de la deuxième. La troisième nécessite un engagement indépendant effectué ultérieurement, souvent appelé point de contrôle, tête, sceau, reçu ou témoin.

Certificate Transparency fait la même distinction avec une autre structure de données. La RFC 9162 indique qu’une preuve de cohérence Merkle peut montrer qu’un arbre plus récent est en ajout seulement par rapport à un arbre antérieur annoncé. La racine annoncée de cet arbre antérieur joue un rôle essentiel. Sans elle, une racine actuelle ne vous apprend rien sur les enregistrements qu’un opérateur a décidé de ne pas vous montrer.

## Construire un journal de test qui traite les charges utiles comme du texte chiffré opaque

Ne vous exercez pas sur un fichier d’audit de production. Créez un scénario de test jetable dont les charges utiles ressemblent à du texte chiffré pour le vérificateur, puis testez les suppressions sur des enregistrements composés d’octets exacts.

Le script suivant écrit un fichier JSON Lines. Chaque enregistrement contient un numéro de séquence, une charge utile base64 opaque, le condensat du prédécesseur et son propre condensat. La charge utile est volontairement constituée de données de test aléatoires, pas d’un véritable chiffrement. Cela suffit ici, car le vérificateur de chaîne a seulement besoin d’octets opaques stables. Dans un véritable journal chiffré, remplacez-les par l’enregistrement de texte chiffré sérialisé et ses métadonnées authentifiées.

```python
# make_log.py
import base64
import hashlib
import json
import os

OUT = "audit.jsonl"
ZERO = "0" * 64


def canonical(record):
    return json.dumps(record, sort_keys=True, separators=(",", ":")).encode()


def digest(record):
    return hashlib.sha256(canonical(record)).hexdigest()

prev = ZERO
with open(OUT, "w", encoding="utf-8") as f:
    for seq in range(1, 13):
        body = {
            "seq": seq,
            "ciphertext": base64.b64encode(os.urandom(24)).decode(),
            "prev": prev,
        }
        body["hash"] = digest(body)
        f.write(json.dumps(body, sort_keys=True) + "\n")
        prev = body["hash"]

print(f"wrote 12 records to {OUT}")
print(f"head seq=12 hash={prev}")
```

Utilisez un vérificateur distinct plutôt que de demander au programme qui écrit le fichier de valider lui-même sa sortie. Il doit refuser les numéros de séquence en double, les sauts inattendus, les enregistrements mal formés, les références incorrectes au prédécesseur et les mauvais hachages. Un vérificateur qui contrôle uniquement les hachages a un angle mort : il peut accepter un fichier réordonné si l’attaquant a également réorganisé suffisamment de champs associés.

```python
# verify_log.py
import hashlib
import json
import sys

ZERO = "0" * 64


def canonical(record):
    return json.dumps(record, sort_keys=True, separators=(",", ":")).encode()


def digest_without_hash(record):
    copy = dict(record)
    supplied = copy.pop("hash", None)
    return supplied, hashlib.sha256(canonical(copy)).hexdigest()


def fail(message):
    print(f"FAIL {message}")
    raise SystemExit(1)

path = sys.argv[1]
prev = ZERO
expected_seq = 1
last_hash = ZERO

with open(path, encoding="utf-8") as f:
    for line_no, line in enumerate(f, start=1):
        try:
            record = json.loads(line)
        except json.JSONDecodeError:
            fail(f"line={line_no} invalid JSON")

        if record.get("seq") != expected_seq:
            fail(f"line={line_no} expected_seq={expected_seq} got={record.get('seq')}")

        if record.get("prev") != prev:
            fail(f"line={line_no} seq={expected_seq} predecessor mismatch")

        supplied, calculated = digest_without_hash(record)
        if supplied != calculated:
            fail(f"line={line_no} seq={expected_seq} digest mismatch")

        prev = supplied
        last_hash = supplied
        expected_seq += 1

print(f"OK records={expected_seq - 1} head={last_hash}")
```

Exécutez le scénario de test et conservez l’empreinte finale indiquée en dehors du fichier journal :

```text
python3 make_log.py
python3 verify_log.py audit.jsonl
```

Le résultat devrait avoir cette forme, avec une empreinte différente à chaque exécution :

```text
wrote 12 records to audit.jsonl
head seq=12 hash=8f...c2
OK records=12 head=8f...c2
```

La paire finale `records=12` et `head=...` constitue votre point de contrôle. Placez-la dans un fichier de notes de test distinct avant de modifier des copies. Si vous laissez le point de contrôle uniquement dans le fichier que vous prévoyez d’attaquer, vous avez créé une trace pratique de ce que l’attaquant peut modifier.

## La suppression de la fin montre immédiatement la limite

La suppression en fin de fichier est le test qui doit rendre prudent avec le mot « complet ». Copiez le fichier, supprimez les trois dernières lignes et exécutez le même vérificateur.

```text
cp audit.jsonl tail-cut.jsonl
head -n 9 audit.jsonl > tail-cut.jsonl
python3 verify_log.py tail-cut.jsonl
```

Le vérificateur indiquera que les neuf enregistrements sont valides. C’est le résultat attendu. Les enregistrements 1 à 9 forment toujours une chaîne valide. Parler d’échec du vérificateur encouragerait une conception dangereuse dans laquelle les historiques partiels mais valides paraissent corrompus.

Comparez plutôt le résultat au point de contrôle :

```text
OK records=9 head=4a...91
expected records=12 head=8f...c2
```

Vous disposez maintenant d’une preuve de troncature. La preuve vient du désaccord avec un élément capturé lorsque le journal était plus long, pas des liens internes du fichier raccourci.

Un numéro de séquence facilite le diagnostic humain. Il ne crée pas de sécurité à lui seul. Un attaquant capable de réécrire la fin peut remplacer le numéro de séquence final 12 par 9. Ce numéro devient utile lorsque la valeur attendue vient d’une source que l’attaquant ne peut pas réécrire, par exemple un point de contrôle signé conservé par un service distinct, un artefact de publication protégé ou une exportation arrivée dans un autre domaine administratif.

C’est le test que de nombreuses conceptions d’audit négligent parce qu’il semble évident. C’est aussi l’échec qui compte après une action destructive. Un processus malveillant n’a pas besoin de rendre l’historique incohérent. Il lui suffit de l’arrêter assez tôt pour dissimuler l’action.

## Une suppression au milieu doit échouer, mais seulement dans les conditions prévues

Faites une autre copie et supprimez un enregistrement qui possède à la fois un prédécesseur et un successeur. L’enregistrement 6 est une bonne cible.

```text
cp audit.jsonl middle-cut.jsonl
sed '6d' audit.jsonl > middle-cut.jsonl
python3 verify_log.py middle-cut.jsonl
```

Le résultat devrait avoir cette forme :

```text
FAIL line=6 expected_seq=6 got=7
```

Si vous supprimiez le contrôle de séquence, le vérificateur échouerait un test plus tard, car l’enregistrement 7 contient le condensat de l’enregistrement 6 alors que le vérificateur dispose du condensat de l’enregistrement 5. Conservez les deux contrôles. Le décalage de séquence rend le problème évident pour l’opérateur, tandis que le décalage du prédécesseur montre quelle relation cryptographique a été rompue.

Ne prétendez pas qu’une chaîne de hachage détecte toujours une suppression au milieu. Elle détecte une suppression simple lorsque l’attaquant ne peut pas réécrire la chaîne restante. Un algorithme de hachage public permet à n’importe qui de calculer de nouveaux hachages. Si quelqu’un peut supprimer l’enregistrement 6, modifier le champ du prédécesseur de l’enregistrement 7, recalculer son hachage, puis répéter l’opération jusqu’à l’enregistrement 12, le fichier reconstruit peut être validé par rapport à sa nouvelle empreinte finale.

Ce résultat n’est ni une attaque par collision ni une faille de SHA-256. C’est un simple recalcul. Le système a accepté un nouvel historique parce qu’il ne disposait d’aucune preuve protégée de l’existence de l’ancien.

Les recommandations du NIST sur les preuves numériques énoncent clairement le point opérationnel : un hachage stocké doit se trouver dans un emplacement où les personnes ayant accès aux preuves ne peuvent pas le modifier ou l’écraser. Le même rapport cite les chaînes de hachage comme utiles pour sécuriser les hachages, mais le stockage externe de la valeur de référence reste nécessaire.

Un test de suppression au milieu comporte donc deux versions :

1. Supprimer une ligne sans modifier les octets suivants. La chaîne doit échouer.
2. Supprimer une ligne et régénérer tous les condensats suivants. La chaîne reconstruite doit échouer face à un point de contrôle conservé indépendamment.

Si votre plan de test n’exécute que la première version, il teste les dommages au fichier et les manipulations maladroites. Il ne teste pas un attaquant capable d’écrire dans le stockage du journal.

## Le chiffrement et l’intégrité de la chaîne répondent à des questions différentes

Les enregistrements chiffrés permettent une répartition utile des rôles. Les opérateurs peuvent vérifier la chaîne sur le texte chiffré sans lire les secrets, les corps de requête, les sorties de commandes ni les autres contenus sensibles de l’audit. Les enquêteurs disposant des autorisations nécessaires peuvent ensuite déchiffrer les enregistrements concernés. C’est une conception raisonnable lorsque les données d’audit sont elles-mêmes sensibles.

Mais des octets chiffrés ne certifient pas leur propre origine. Un enregistrement de texte chiffré peut être supprimé. Un nouvel enregistrement peut être ajouté par une personne qui contrôle le chiffrement et le chemin d’écriture du journal. Si le mode de chiffrement n’authentifie pas les données, un texte chiffré peut parfois être modifié pour produire des données incohérentes sans que le déchiffreur sache pourquoi. Utilisez un chiffrement authentifié pour assurer la confidentialité et l’intégrité des enregistrements, puis utilisez la chaîne pour les relier dans un historique ordonné.

Séparez ces contrôles dans la conception et le compte rendu d’incident :

- L’authentification d’un enregistrement demande si un enregistrement chiffré a été modifié.
- La vérification de la chaîne demande si chaque enregistrement restant suit le prédécesseur annoncé.
- La comparaison avec un point de contrôle demande si l’historique observé atteint une empreinte finale déjà observée.
- La capture des événements demande si le système a inscrit l’action dans le journal.

Cette dernière question est délicate. Aucun journal cryptographique ne peut prouver qu’un événement a été enregistré si un processus d’écriture compromis a choisi de ne pas produire d’enregistrement. Vous pouvez réduire cette exposition en collectant des preuves à partir de frontières indépendantes, comme un service réseau, une fonction d’audit de l’hôte ou une validation humaine. Vous ne pouvez pas faire disparaître cette lacune en qualifiant le journal local de journal en ajout seulement.

Le modèle d’audit de Sallyport est utile dans ce contexte : ses journaux d’exécutions d’agents et d’appels individuels proviennent d’un même journal d’audit chiffré et chaîné par hachage, et `sp audit verify` peut vérifier hors ligne la chaîne de texte chiffré sans clé de coffre-fort. Un évaluateur peut ainsi contrôler la continuité sans exposer les identifiants ni les données d’action protégés par la trace d’audit.

## Un point de contrôle doit être difficile à réécrire, pas seulement copié

Un point de contrôle décrit l’état d’un journal à un instant donné. Au minimum, incluez un identifiant de journal, le dernier numéro de séquence, le condensat final, la date de création et la version du format ou de l’algorithme. Traitez-le comme un artefact distinct, soumis à sa propre chaîne de conservation.

Ce petit format suffit pour un banc de test :

```json
{
  "log_id": "agent-actions-test-a",
  "sequence": 12,
  "head": "8f...c2",
  "created_at": "2026-07-22T14:30:00Z",
  "format": "jsonl-chain-v1"
}
```

Ne faites pas confiance au seul champ `created_at`. Une horloge locale est utile pour trier les événements et mener une enquête, mais un attaquant qui contrôle la machine peut contrôler cette horloge et le fichier qui la contient. Le point de contrôle tire sa force de l’endroit où il a été envoyé et des personnes qui peuvent le modifier.

Une organisation pratique consiste à faire écrire les enregistrements d’audit par une partie et à faire conserver périodiquement les empreintes finales par une autre. Cette seconde partie n’a pas besoin d’un accès au déchiffrement. Elle doit seulement disposer de suffisamment d’informations pour refuser un fichier ultérieur dont l’empreinte finale et la séquence ne correspondent pas à ce qu’elle avait observé.

Pour un outil destiné aux développeurs, cette partie indépendante peut rester simple : un artefact CI protégé, un système d’attestation de publication, un compte de collecteur distinct ou une exportation quotidienne approuvée par un autre administrateur. Pour les actions plus risquées, émettez le point de contrôle immédiatement après l’action et conservez-le en dehors du poste de travail. La fréquence doit suivre la durée d’exposition que vous êtes prêt à accepter. Un point de contrôle quotidien ne peut pas établir l’exhaustivité des heures comprises entre l’empreinte d’hier et la fin manquante d’aujourd’hui.

Signer un point de contrôle aide les vérificateurs à savoir qui l’a émis. Une signature lie les données du point de contrôle à l’autorité signataire, mais elle ne rend pas le signataire honnête. Si le même processus compromis écrit le journal d’audit et signe des points de contrôle de remplacement, vous avez toujours une seule frontière de confiance. Placez le témoin dans un emplacement que l’auteur initial ne peut pas contrôler discrètement.

## Un stockage sans possibilité de lecture ou de réécriture change l’attaquant testé

Une chaîne stockée dans un emplacement où l’auteur peut modifier librement les anciens enregistrements n’a pas le même modèle de menace qu’une chaîne conservée via un chemin d’ajout sans possibilité de réécriture. La première conception a besoin de témoins externes pour détecter les réécritures de l’historique. La seconde cherche d’abord à empêcher l’auteur d’effectuer ces réécritures.

Soyez précis avec l’expression « sans possibilité de réécriture ». Elle doit signifier que le composant qui soumet un nouvel enregistrement ne peut pas lire ou modifier à volonté les anciens enregistrements chiffrés du journal. Cela ne signifie pas que le disque ne peut pas tomber en panne, qu’un administrateur ne peut pas supprimer un fichier ou que le système d’exploitation ne peut pas être compromis. Cette disposition réduit une voie d’attaque : réécrire une partie choisie de l’historique après l’avoir consultée.

Cette distinction influence vos cas de test. Pour un accès ordinaire aux fichiers, testez la suppression suivie d’un nouveau calcul des hachages, car un attaquant peut lire les données et calculer les hachages. Pour un stockage sans possibilité de réécriture, vérifiez si un appelant peut demander la suppression, le remplacement ou le retour en arrière, ou créer un second journal sous la même identité. Testez également la façon dont le vérificateur identifie le journal visé. Une chaîne parfaite issue du mauvais fichier reste une mauvaise preuve.

L’erreur la plus courante consiste à traiter les autorisations de stockage comme une ancre cryptographique. Un attaquant suffisamment privilégié peut modifier ces autorisations. Elles restent importantes, car elles réduisent les dommages accidentels et limitent les processus autorisés à agir, mais elles ne remplacent pas un point de contrôle qui a franchi une frontière hors du contrôle de l’attaquant.

## Tester le retour en arrière séparément de la troncature

Un retour en arrière ressemble à une troncature de la fin avec un ancien fichier plausible. Une machine peut restaurer le fichier d’audit valide de la veille après une panne, une restauration de sauvegarde ou un remplacement volontaire. Le fichier restauré peut passer sa vérification interne, car il était réellement valide la veille.

Votre point de contrôle détecte ce cas s’il enregistre une empreinte finale plus récente. Comparez le fichier candidat au point de contrôle conservé le plus récent, pas au plus ancien que vous trouvez. Un vérificateur de chaîne ne peut pas déduire quelle version valide doit être actuelle.

Testez directement ce scénario :

1. Enregistrez `audit.jsonl` et son point de contrôle à 12 enregistrements.
2. Générez un autre fichier de test avec davantage d’enregistrements et conservez son point de contrôle plus récent.
3. Remplacez le fichier le plus récent par la copie à 12 enregistrements.
4. Vérifiez le fichier restauré en interne, puis comparez-le au point de contrôle plus récent.

Attendez-vous à deux résultats différents. La vérification interne doit réussir. La comparaison avec le point de contrôle doit échouer, car le fichier se termine à une séquence et une empreinte plus anciennes. Si les deux contrôles échouent, votre scénario a peut-être modifié des octets au lieu de reproduire un retour en arrière. Si les deux réussissent, vous comparez avec le mauvais point de contrôle ou vous n’en avez pas conservé.

Ne « corrigez » pas un retour en arrière en ajoutant de nouveaux enregistrements à l’ancien fichier pour continuer. Préservez d’abord l’état observé. Dès que vous ajoutez des enregistrements, vous mélangez un historique potentiellement incomplet avec une activité postérieure à l’incident. Commencez un nouveau segment de journal avec une limite d’incident enregistrée, ou suivez la procédure de conservation et de récupération déjà approuvée par votre organisation.

## Rendre les résultats du vérificateur utiles pendant une enquête

Un vérificateur qui renvoie seulement `invalid` crée du travail inutile. Il doit identifier la première condition qui échoue sans afficher les charges utiles sensibles. Le numéro de séquence, le décalage en octets ou le numéro de ligne, le condensat attendu du prédécesseur, le condensat observé du prédécesseur et le condensat calculé de l’enregistrement fournissent généralement assez de détails.

Évitez d’inscrire du contenu déchiffré dans un message d’erreur. Un outil de vérification s’exécute souvent dans une CI, un paquet de support ou une capture de terminal. Si une conception d’audit chiffré divulgue du texte en clair dès qu’une vérification échoue, elle transforme un incident d’intégrité en fuite de données.

Une réponse disciplinée à une vérification échouée suit un ordre simple :

- Préserver le fichier original et enregistrer l’empreinte de cette copie préservée.
- Récupérer le point de contrôle le plus récent, les précédents et les éventuelles exportations externes.
- Effectuer la vérification sur des copies et conserver la sortie exacte de chaque commande.
- Comparer l’empreinte finale signalée à chaque point de contrôle conservé.
- Déterminer s’il s’agit d’une corruption, d’une lacune au milieu, d’une perte en fin de fichier, d’un retour en arrière ou d’une chaîne réécrite qui contredit un témoin.

La classification est importante. Une erreur signalant une lacune au milieu indique une contradiction interne du fichier. Une chaîne valide mais plus courte signifie que le fichier peut être complet uniquement jusqu’à son dernier enregistrement. Une chaîne valide qui ne correspond pas à un point de contrôle ultérieur fournit une preuve de retour en arrière, de troncature ou de substitution. Une chaîne réécrite peut sembler propre en interne, mais elle sera en désaccord avec une ancienne empreinte indépendante.

Ne promettez pas plus que ce que les preuves permettent. « La chaîne d’audit a été vérifiée jusqu’à la séquence 90 et ne correspond pas au point de contrôle conservé de la séquence 100 » est une conclusion solide et précise. « Personne n’a modifié le journal » ne l’est pas.

## Le test qui compte est celui qui porte sur votre vraie frontière de confiance

Commencez par les tests jetables de suppression de lignes, car ils enseignent les mécanismes. Exécutez ensuite les mêmes cas sur l’exportation d’audit réelle et avec son véritable vérificateur, en utilisant des copies et les limites de manipulation documentées pour le format. Ne modifiez pas directement un journal chiffré de production simplement pour voir ce qui se passe.

Pour Sallyport, préservez le journal chiffré original et utilisez `sp audit verify` sur une copie avant et après chaque modification contrôlée. Notez si le vérificateur détecte une lacune interne, si une copie raccourcie reste valide et si votre empreinte finale conservée révèle un historique plus court. La réponse à ces trois questions est plus utile qu’une affirmation vague selon laquelle le journal révèle les altérations.

Une chaîne valide signifie que les enregistrements vérifiés concordent entre eux. Une chaîne valide accompagnée d’un point de contrôle ultérieur fiable signifie que les enregistrements atteignent un point connu de l’historique. Concevez et testez votre système pour obtenir cette deuxième garantie lorsque l’absence d’actions aurait des conséquences.
