Aller au contenu

NF203 & chaîne HMAC

Logmis est conforme à la NF 203 (logiciels de gestion et de réservation d’hébergement). Ce n’est pas la NF 525 (logiciels de caisse) — ce périmètre relève d’OsmoVente, module séparé.

La NF203 impose :

  • Inaltérabilité des données comptables (pas de DELETE/UPDATE sur les entrées de folio, factures, journal)
  • Séquentialité de la numérotation des factures (pas de trou, pas de doublon)
  • Traçabilité (piste d’audit nominative et horodatée)
  • Clôture de période (Night Audit = journée comptable)

Chaque entrée d’audit est chaînée par HMAC :

entry_n.hmac = HMAC-SHA256(
canonical_v1(entry_n),
rotatingSecret
)
canonical_v1(entry) = "<id>|<tenantId>|<type>|<payload_hash>|<prev_hmac>|<occurred_at>"

Le format canonical_v1 est gelé avant le premier append en production. Toute modification nécessite la création d’un canonical_v2 — la v1 reste lisible et vérifiable.

  • Genesis non-forgeable : le premier entry de chaque chaîne a un prev_hmac spécial (valeur constante, pas de zéro ou de null)
  • Advisory lock par tenant : un seul thread peut écrire dans la chaîne d’un tenant à la fois
  • Secret rotatif : RotatingSecretStore supporte la rotation de la clé HMAC sans invalider les entrées passées
  • KAT + golden test : les Known Answer Tests verrouillent la valeur canonical_v1 — tout changement de format fait échouer le test

La rotation est transparente grâce à RotatingSecretStore : les nouvelles entrées utilisent le nouveau secret, les anciennes restent vérifiables avec l’ancien. La vérification tente les deux secrets.

Distincte de la chaîne tenant, avec son propre canonical :

  • canonical_control_plane_v1 — gelé également
  • Stockée dans pms.control_plane_audit_entry (accessible uniquement par pms_control_plane)
  • pms_app = zéro accès (séparation physique des privilèges)
  • L’API admin expose les entrées sans fuite de signature/prevHash (données internes non exposées)

Le grand total est la somme cumulée de toutes les factures par operating_mandate_id. Il est :

  • Incrémentiel (jamais recalculé — append-only)
  • Partitionné par mandat fiscal (pas par établissement — cf. Invariants money)
  • Accessible dans le FEC/JET-PAF exporté pour les administrations fiscales

À chaque Night Audit (étape 6c), la racine Merkle de la chaîne d’audit est ancrée en stockage objet WORM :

Arbre Merkle des audit_entry depuis le dernier ancrage
→ racine SHA-256
→ PUT vers MinIO avec Object Lock COMPLIANCE
→ durée de rétention : 10 ans
→ clé déterministe : audit/<tenantId>/<businessDate>

L’Object Lock COMPLIANCE empêche toute suppression ou modification, même par l’administrateur MinIO. Seule la date d’expiration est non révocable.

La console admin expose GET /admin/audit/verification qui recalcule la racine Merkle et la compare à l’ancrage stocké. Toute divergence est signalée.

La fitness lint:audit vérifie automatiquement l’inaltérabilité en analysant le code source :

  • Zéro UPDATE sur les tables append-only (audit_entry, reservation_versions, folio_entries…)
  • Zéro DELETE sur ces mêmes tables
  • Bloquante en CI

Le FEC (Fichier des Écritures Comptables) est généré par FiscalController via GET /v1/fiscal/fec :

  • Rôle requis : accountant
  • Accessible via pool dédié pms_fiscal_reader (SELECT uniquement sur les tables fiscales)
  • Garde fec-vat-basis-guard : refuse l’export si le régime TVA est cash_receipts (non supporté — fail-closed)

La chaîne NF203 est renforcée par des contraintes de base de données :

-- pms_app n'a que INSERT + SELECT
GRANT SELECT, INSERT ON pms.folio_entries TO pms_app;
GRANT SELECT, INSERT ON pms.audit_entry TO pms_app;
GRANT SELECT, INSERT ON pms.reservation_versions TO pms_app;
-- etc.
-- UPDATE(status) restreint pour la clôture folio uniquement
GRANT UPDATE (status) ON pms.folios TO pms_app;

Même si un bug applicatif tentait un DELETE ou un UPDATE, Postgres le refuserait.