Skip to content

feat(actions): audit de la chaîne du registre des opérateurs (operators audit) - #25

Merged
PhilippeVienne merged 1 commit into
devfrom
feat/operators-audit
Sep 21, 2026
Merged

PhilippeVienne merged 1 commit into
devfrom
feat/operators-audit

Conversation

@PhilippeVienne

Copy link
Copy Markdown
Contributor

PR empilée sur #24 (pile : #19 → #20 → #21 → #22 → #24 → celle-ci). Base : feat/recover-admin. À rebrancher sur dev après chaque merge : gh api -X PATCH repos/open-eidas/open-eidas/pulls/N -f base=dev.

Contenu

Deuxième tranche du §21 de docs/WEBUI.md : ca-server operators audit [--journal <fichier>]. Pour chaque clé active, elle refait le chemin par lequel la clé est entrée dans le registre, sans croire la base sur parole.

  • Clé confirmée : il faut une action confirm_key exécutée, au corps inchangé (body_hash), engageant bien cette clé, signée par assez d'opérateurs distincts. Chaque signature est re-vérifiée (ES256, via openssl, déjà lié par webauthn-rs) sur authenticatorData ‖ SHA-256(clientDataJSON), avec le challenge émis pour elle, contre la clé du signataire, qui a lui-même une chaîne valide jusqu'à une ancre (sans cycle, sans auto-confirmation). Qui insère une ligne en SQL ne peut pas fabriquer la signature d'une clé existante.
  • Clé d'ancre (amorçage, récupération) : la contrainte du registre l'admet sans confirmation, donc n'importe qui pouvant écrire en base pourrait s'en donner une. Elle doit figurer au journal chaîné.
  • Lien corps ↔ signature : la signature porte sur le challenge, pas sur le corps (décision O7). Un attaquant qui réécrit le corps et son empreinte, cohérents entre eux, passerait la cryptographie seule. Le corps doit donc être celui que le journal a consigné avant la signature (operators.action_challenge_issued).
  • Une confirmation absente du journal est signalée même si sa signature est bonne : le journal fait foi pour le registre.

Codes de sortie : 0 registre sain ; 1 constats (une ligne KO par clé) ; 2 journal illisible ou rompu (le registre n'est pas jugé contre un journal dont l'intégrité n'est pas garantie). Pointer --journal vers la copie répliquée hors de l'hôte est le meilleur usage.

oe-audit gagne read() : relit le journal, en contrôle la chaîne et rend les enregistrements (réutilisable par reconcile).

Limites assumées

  • Seul ES256 est vérifiable : une autre clé est signalée, pas tenue pour bonne.
  • Le rôle qu'avaient les signataires à l'époque n'est pas jugé (les rôles changent).
  • Pas de lancement au démarrage ni de blocage des actions : ils relèvent de operators reconcile (TODO), avec /healthz en 503 et le refus d'exécuter une action signée par une clé sans chaîne valide. L'ancre ne peut s'établir qu'avec le journal, d'où ce découpage plutôt qu'un blocage à moitié livré.

Vérifications

  • cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace (avec PostgreSQL, 69 suites), cargo audit --ignore RUSTSEC-2023-0071 : verts.
  • 11 tests sur PostgreSQL réel avec authentificateur logiciel : chaîne authentique saine (avec de vraies signatures ES256), clé insérée en SQL, ancre forgée (amorçage et récupération), preuve fabriquée sans vraie signature, signature ou corps altérés, corps réécrit avec son empreinte, challenge d'une autre cérémonie, confirmation d'une autre clé, trop peu de signataires, journal comme référence, clés révoquées ignorées. 1 test de la commande (codes 0 / 1 / 2). 2 tests de oe_audit::read.
  • Contrôle par mutation : signature non vérifiée, challenge non comparé, ancre non vérifiée au journal, empreinte du corps non contrôlée, lien corps ↔ journal ignoré, empreinte de la clé ignorée, auto-confirmation permise, nombre de signataires ignoré. Trois de ces mutants ont d'abord survécu (challenge, empreinte de la clé, nombre de signataires) : j'ai ajouté un test pour chacun, les huit sont maintenant détectés.

…rs audit)

ca-server operators audit [--journal <fichier>] refait, pour chaque clé
active, le chemin par lequel elle est entrée dans le registre
(docs/WEBUI.md §21), sans croire la base sur parole :

- une clé confirmée doit avoir une action confirm_key exécutée : corps
  inchangé (empreinte body_hash), engageant bien cette clé, signée par assez
  d'opérateurs distincts. Chaque signature est re-vérifiée (ES256, openssl)
  sur authenticatorData || SHA-256(clientDataJSON), avec le challenge émis
  pour elle, contre la clé du signataire, qui a lui-même une chaîne valide
  jusqu'à une ancre, sans cycle ni auto-confirmation ;
- une clé d'ancre (amorçage, récupération) ne se prouve pas dans la base,
  la contrainte du registre l'admettant sans confirmation : elle doit
  figurer au journal chaîné ;
- le corps de chaque action doit être celui que le journal a consigné avant
  la signature. La signature porte sur le challenge, pas sur le corps
  (décision O7) : un corps réécrit avec son empreinte, cohérents entre eux,
  passerait la cryptographie seule ;
- une confirmation absente du journal est signalée même si sa signature est
  bonne : le journal fait foi pour le registre.

Codes de sortie : 0 registre sain, 1 constats (une ligne KO par clé), 2
journal illisible ou rompu (le registre n'est pas jugé contre un journal dont
l'intégrité n'est pas garantie).

Seul ES256 est vérifiable : une autre clé est signalée, pas tenue pour bonne.

oe-audit : read() relit le journal, en contrôle la chaîne et rend les
enregistrements.
@PhilippeVienne
PhilippeVienne changed the base branch from feat/recover-admin to dev September 21, 2026 10:03
@PhilippeVienne
PhilippeVienne merged commit e78b606 into dev Sep 21, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant