NF203 & chaîne HMAC
Périmètre NF203
Section intitulée « Périmètre NF203 »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)
Chaîne HMAC (canonical_v1)
Section intitulée « Chaîne HMAC (canonical_v1) »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.
Propriétés
Section intitulée « Propriétés »- Genesis non-forgeable : le premier entry de chaque chaîne a un
prev_hmacspé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 :
RotatingSecretStoresupporte 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
Rotation du secret
Section intitulée « Rotation du secret »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.
Chaîne d’audit control-plane
Section intitulée « Chaîne d’audit control-plane »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 parpms_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)
Grand total NF203
Section intitulée « Grand total NF203 »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
Ancrage Merkle WORM (ADR-00D79)
Section intitulée « Ancrage Merkle WORM (ADR-00D79) »À 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.
Vérification
Section intitulée « Vérification »La console admin expose GET /admin/audit/verification qui recalcule la racine Merkle et la compare à l’ancrage stocké. Toute divergence est signalée.
Fitness function lint:audit
Section intitulée « Fitness function lint:audit »La fitness lint:audit vérifie automatiquement l’inaltérabilité en analysant le code source :
- Zéro
UPDATEsur les tables append-only (audit_entry,reservation_versions,folio_entries…) - Zéro
DELETEsur ces mêmes tables - Bloquante en CI
FEC et exports fiscaux
Section intitulée « FEC et exports fiscaux »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 estcash_receipts(non supporté — fail-closed)
Inaltérabilité par GRANT (rappel)
Section intitulée « Inaltérabilité par GRANT (rappel) »La chaîne NF203 est renforcée par des contraintes de base de données :
-- pms_app n'a que INSERT + SELECTGRANT 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 uniquementGRANT UPDATE (status) ON pms.folios TO pms_app;Même si un bug applicatif tentait un DELETE ou un UPDATE, Postgres le refuserait.