Concepts

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

Triple verrou :
  1. Trigger PL/pgSQL audit_entry_block_modifications qui RAISE EXCEPTION sur UPDATE/DELETE/TRUNCATE.
  2. Rôle SQL validation_app qui n'a même pas les GRANTs UPDATE/DELETE sur la table. Le runtime applicatif utilise ce rôle, pas postgres superuser.
  3. Trigger audit_entry_chain_check qui vérifie à l'INSERT : prev_hash = entry_hash de 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_passphrase via 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

Continue (canary)

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.

Hors-ligne (verifier Python)

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.