Chaîne d'audit append-only
Chaque action signée est ajoutée à une chaîne organisation-global, immuable au niveau Postgres. Chaque entrée référence le hash de la précédente — modifier une entrée passée invaliderait toutes les suivantes.
Structure d'une entrée
{
"entry_hash": "sha256(organisation_id | sequence_num | session_id | actor_user_id |
event_type | prev_hash | payload_hash |
canonical(action_metadata) | created_at)",
"prev_hash": "← entry_hash de l'entrée n-1 (NULL pour la première)",
"sequence_num": 42,
"organisation_id": "01900000-...",
"session_id": "01900000-..." (optionnel),
"actor_user_id": "user-001" (optionnel),
"event_type": "VALIDATION_ACTION",
"payload_hash": "sha256 du contenu de l'action",
"action_metadata": { /* libre, canonicalisé pour le hash */ },
"organisation_signature": "Ed25519 du entry_hash avec organisation_key",
"master_signature": "Ed25519 du entry_hash avec master_key",
"created_at": "2026-05-22T10:32:14.521+00:00"
}
Pourquoi organisation-global et pas par session ?
Le chaînage par hash est strictement séquentiel sur l'ensemble des entrées de l'organisation. Cela signifie que :
- • Les sessions s'interleavent : actions de session A, B, A, canary, A, B, B…
- • Les canaries floatent au milieu — c'est ce qui rend la preuve plus forte. Voir Ancrage.
- • Re-écrire UNE entrée du passé casse tout le reste : prev_hash de l'entrée n+1 ne correspond plus, la signature ne se vérifie plus, le canary suivant détecte la cassure.
Garanties au niveau base
- Trigger PL/pgSQL
audit_entry_block_modificationsqui RAISE EXCEPTION sur UPDATE/DELETE/TRUNCATE. - Rôle SQL
validation_appqui n'a même pas les GRANTs UPDATE/DELETE sur la table. Le runtime applicatif utilise ce rôle, paspostgressuperuser. - Trigger
audit_entry_chain_checkqui vérifie à l'INSERT :prev_hash=entry_hashde la précédente entrée (verrou cohérence inviolable côté DB).
Co-signature Ed25519
Chaque entrée porte deux signatures Ed25519 (libsodium) :
- organisation_signature — clé Ed25519 par organisation, gérée par le service. Atteste l'origine.
- master_signature — clé Ed25519 du service, dérivée du
master_passphrasevia Argon2id (MODERATE). Atteste de l'intégrité globale et bloque la fabrication d'entrées par une organisation compromis.
Pour falsifier l'historique, un attaquant devrait compromettre les deux clés simultanément ET ré-écrire toutes les entrées suivantes et les canaries intermédiaires et les ancrages Bitcoin/TSP et les emails témoins DKIM-signés stockés dans des boîtes tierces.
Vérification
Un endpoint GET /v1/health/canary + une commande app:canary:run walked la chaîne en arrière (sample de N entrées) en re-vérifiant prev_hash + signatures master. Persisté dans canary_run.
L'outil tools/verify/ vérifie un dossier ASiC-E sans accès au service. Il refait tous les hashes, les signatures, les ancrages — produit un rapport JSON.
La couche témoins — inviolable, y compris contre nous
À chaque ancre confirmée, ProofIt émet une tête de chaîne signée
(checkpoint.created). Votre organisation la contre-signe avec sa propre
clé Ed25519 — que nous ne détenons pas — après avoir vérifié qu'elle prolonge la
précédente (GET /v1/entries/{seq}), puis archive sa copie chez elle. Les
checkpoints co-signés partent dans chaque bundle (CHECKPOINTS.json) et sont
vérifiés par l'outil hors-ligne. Conséquence : réécrire l'historique — même pour l'opérateur —
produirait deux checkpoints contradictoires pour une même taille, une faute
cryptographiquement prouvable.
Scopes checkpoints:read / checkpoints:witness — guide d'implémentation du témoin dans la doc développeur.