From 1eb46c1e6411ad7e5d888696769954e6df62828d Mon Sep 17 00:00:00 2001 From: Philippe Vienne Date: Sun, 27 Sep 2026 08:59:39 +0200 Subject: [PATCH 1/2] docs: aligner la documentation et la matrice sur le code en service (D-1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Constat D-1 de l'audit du 2026-09-25 : la documentation revendiquait des mécanismes absents. Mieux vaut un écart déclaré qu'un contrôle revendiqué et introuvable. - Matrice : EN 319 401 §7.11 passe en écart (contreseing et réplication écrits mais appelés par aucun binaire, cible J-1) ; §7.10 aussi (durée de conservation contrôlée côté CA seulement). CONFORMITE-ETSI.md régénéré : 22 couvertes, 4 écarts, 2 hors périmètre. - ARCHITECTURE.md §7 et §8 : ce que le journal consigne réellement, l'écart J-3 en cours (PR #52), la limite du chaînage sans clé ; scellement, contreseing et réplication décrits comme non câblés. - README : plus de contreseing FreeTSA/DigiCert revendiqué ni d'interface webui (reste d'OpenXPKI). - CA.md (modèle de menace, continuité), API.md, docker-compose.yml, values.yaml : les variables de scellement et de réplication sont lues mais pas encore exploitées. - oe-tsa-core : l'en-tête disait CheckTSUCertificate non portée, elle l'est. Co-authored-by: Claude --- README.md | 14 ++--- crates/oe-conformance/src/lib.rs | 12 ++-- crates/oe-tsa-core/src/lib.rs | 12 ++-- deploy/helm/open-eidas/values.yaml | 3 + docker-compose.yml | 2 + docs/API.md | 6 +- docs/ARCHITECTURE.md | 96 ++++++++++++------------------ docs/CA.md | 10 ++-- docs/CONFORMITE-ETSI.md | 6 +- 9 files changed, 73 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index bcb3f65..c4a9a77 100644 --- a/README.md +++ b/README.md @@ -69,9 +69,12 @@ Ce dépôt héberge le prototype **fonctionnel et vérifiable** du premier servi - une **heure traçable jusqu'à UTC** : le service recoupe deux serveurs de laboratoires de métrologie (Observatoire de Paris, PTB) et **cesse d'émettre** dès que la dérive dépasse le seuil annoncé, comme l'exige ETSI EN 319 421 ; -- un **journal d'audit chaîné par hachage, contresigné par des TSA tierces - publiques** (FreeTSA.org, DigiCert) : chaque scellement se vérifie avec les - outils RFC 3161 standards, sans dépendre de la confiance en Open eIDAS ; +- un **journal d'audit chaîné par hachage**, écrit *avant* chaque signature : + si le journal ne peut pas être écrit, aucun jeton ne sort. Le scellement de + la tête de chaîne par des TSA tierces publiques et la réplication hors site + existent en bibliothèques (`oe-crosstsa`, `oe-replicate`) mais **ne sont pas + encore câblés dans les services** — écart déclaré dans + [docs/CONFORMITE-ETSI.md](docs/CONFORMITE-ETSI.md) ; - le tout orchestré en `docker compose`, démarrable en une commande. Ce n'est pas encore une TSA qualifiée : les écarts avec le référentiel eIDAS @@ -120,9 +123,6 @@ awk '/BEGIN CERTIFICATE/{n++} {print > (n == 1 ? "tsu.pem" : "ca.pem")}' tsa-cha openssl ts -verify -in facture.tsr -queryfile facture.tsq -CAfile ca.pem ``` -L'interface d'administration de la PKI est disponible sur - (certificat auto-signé). - ### Relire le journal d'audit ```bash @@ -167,7 +167,7 @@ crates/oe-castore/ registre de la CA (PostgreSQL, et mémoire pour les tests crates/oe-hsm/ accès PKCS#11 aux clés de signature (seule crate autorisant `unsafe`) crates/oe-timesource/ surveillance de la traçabilité de l'heure crates/oe-audit/ journal d'audit chaîné par hachage -crates/oe-crosstsa/ contreseing du journal par des TSA tierces publiques +crates/oe-crosstsa/ contreseing du journal par des TSA tierces publiques (pas encore câblé) crates/oe-enroll/ client d'enrôlement auprès de la CA crates/oe-httpapi/ endpoints HTTP (RFC 3161 + façade JSON) deploy/ images des trois services, chart Helm diff --git a/crates/oe-conformance/src/lib.rs b/crates/oe-conformance/src/lib.rs index 5052b7b..c5de71f 100644 --- a/crates/oe-conformance/src/lib.rs +++ b/crates/oe-conformance/src/lib.rs @@ -630,10 +630,10 @@ pub fn system_matrix() -> Matrix { }, Entry { requirement: Requirement { standard: "ETSI EN 319 401", clause: "§7.10", title: "Journalisation des événements et durée de conservation" }, - status: Status::Covered, - mechanism: "Journal JSON Lines chaîné par SHA-256 (oe-audit) ; durée de conservation contrôlée à la configuration par oe_conformance::check_audit_retention, branché sur bin/ca-server::Config::load.", + status: Status::Gap, + mechanism: "Journal JSON Lines chaîné par SHA-256 (oe-audit), écrit avant chaque émission dans les deux services ; durée de conservation contrôlée à la configuration par oe_conformance::check_audit_retention, mais côté CA seulement (bin/ca-server::Config::load).", test: "crates/oe-audit/src/lib.rs (deux_ecrivains_partagent_la_meme_chaine), crates/oe-conformance/src/lib.rs (check_audit_retention_accepts_the_minimum, check_audit_retention_rejects_unconfigured_and_short_durations)", - target: "", + target: "Contrôler aussi la durée de conservation au démarrage de tsa-server (docs/CPS.md B.3).", }, Entry { requirement: Requirement { standard: "ETSI EN 319 401", clause: "§7.9", title: "Intégrité démontrable des enregistrements d'audit" }, @@ -644,10 +644,10 @@ pub fn system_matrix() -> Matrix { }, Entry { requirement: Requirement { standard: "ETSI EN 319 401", clause: "§7.11", title: "Continuité d'activité et reprise après sinistre" }, - status: Status::Covered, - mechanism: "Contreseing du journal par une TSA tierce (oe-crosstsa) et réplication WebDAV hors site (oe-replicate), validés contre un vrai serveur.", + status: Status::Gap, + mechanism: "Contreseing du journal par une TSA tierce (oe-crosstsa) et réplication WebDAV hors site (oe-replicate) écrits et testés contre un vrai serveur, mais appelés par aucun binaire : l'intégrité et la survie du journal reposent sur le contrôle d'accès et la sauvegarde de son volume.", test: "crates/oe-crosstsa/tests/against_local_server.rs (seals_a_digest_against_a_real_rfc3161_server), crates/oe-replicate/tests/against_local_server.rs (replicates_content_via_webdav_put)", - target: "", + target: "Constat J-1 : câbler dans tsa-server serve puis ca-server serve le scellement périodique (log.sealed), le contreseing tiers (log.cross_sealed) et la copie hors site (PR #49, #50 pour le stockage S3), testés de bout en bout sur le binaire ; dégrader /healthz sur échec prolongé.", }, Entry { requirement: Requirement { standard: "ETSI EN 319 401", clause: "§7.12", title: "Plan de cessation d'activité" }, diff --git a/crates/oe-tsa-core/src/lib.rs b/crates/oe-tsa-core/src/lib.rs index af712cd..46e5166 100644 --- a/crates/oe-tsa-core/src/lib.rs +++ b/crates/oe-tsa-core/src/lib.rs @@ -5,13 +5,11 @@ //! (`Clock`, équivalent du moniteur `oe-timesource`, jalon J4) pour produire //! et signer un jeton d'horodatage réel. //! -//! **Écart assumé face à `internal/conformance`** : la vérification du -//! profil du certificat TSU (`conformance.CheckTSUCertificate`, EN 319 421 -//! §7.7.2) n'est pas encore portée (`oe-conformance` la déclare `Gap`, -//! jalon J3) — `Authority::new` ne la reproduit donc pas encore. Ce qui est -//! bien vérifié ici : la correspondance clé publique du token ↔ certificat, -//! et la fenêtre de validité temporelle du certificat, comme dans `tsa.New` -//! (Go). +//! `Authority::new` vérifie la correspondance clé publique du token ↔ +//! certificat, la fenêtre de validité temporelle du certificat, comme dans +//! `tsa.New` (Go), et le profil du certificat TSU +//! (`oe_conformance::check_tsu_certificate`, portage de +//! `conformance.CheckTSUCertificate`). use std::sync::Arc; diff --git a/deploy/helm/open-eidas/values.yaml b/deploy/helm/open-eidas/values.yaml index 1f551b8..6b228df 100644 --- a/deploy/helm/open-eidas/values.yaml +++ b/deploy/helm/open-eidas/values.yaml @@ -53,6 +53,9 @@ tsa: sources: "ntp.obspm.fr,ptbtime1.ptb.de" minSources: 2 maxOffset: 500ms + # Scellement du journal et contreseing par des TSA tierces : valeurs + # transmises au service, qui ne les exploite PAS encore (écart J-1, voir + # docs/ARCHITECTURE.md §7). crossTSA: urls: "https://freetsa.org/tsr,http://timestamp.digicert.com" audit: diff --git a/docker-compose.yml b/docker-compose.yml index a66cdc4..ad909db 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -131,6 +131,8 @@ services: OPENEIDAS_TIME_MIN_SOURCES: "2" OPENEIDAS_TIME_MAX_OFFSET: 500ms OPENEIDAS_AUDIT_FILE: /var/lib/open-eidas/state/audit.log + # Scellement, contreseing tiers et réplication du journal : lus par la + # configuration mais PAS encore exploités (écart J-1, docs/ARCHITECTURE.md §7). OPENEIDAS_AUDIT_SEAL_INTERVAL: 1h OPENEIDAS_CROSS_TSA_URLS: ${OPENEIDAS_CROSS_TSA_URLS:-https://freetsa.org/tsr,http://timestamp.digicert.com} OPENEIDAS_AUDIT_REPLICA_URL: http://audit-replica/ diff --git a/docs/API.md b/docs/API.md index cf39f61..544c528 100644 --- a/docs/API.md +++ b/docs/API.md @@ -197,10 +197,10 @@ Toutes les options sont pilotées par variables d'environnement. | `OPENEIDAS_SIGNING_DIGEST` | `sha256` | Empreinte utilisée pour signer le jeton | | `OPENEIDAS_MAX_REQUEST_BYTES` | `65536` | Taille maximale d'une requête | | `OPENEIDAS_AUDIT_FILE` | `/var/lib/open-eidas/audit.log` | Journal d'audit chaîné par hachage | -| `OPENEIDAS_AUDIT_SEAL_INTERVAL` | `1h` | Période de scellement de la tête de chaîne (`0` désactive) | -| `OPENEIDAS_CROSS_TSA_URLS` | `https://freetsa.org/tsr,http://timestamp.digicert.com` | TSA tierces contresignant chaque scellement, séparées par des virgules | +| `OPENEIDAS_AUDIT_SEAL_INTERVAL` | `1h` | Période de scellement de la tête de chaîne (`0` désactive). **Lue mais pas encore exploitée** : aucun scellement n'a lieu (écart J-1) | +| `OPENEIDAS_CROSS_TSA_URLS` | `https://freetsa.org/tsr,http://timestamp.digicert.com` | TSA tierces contresignant chaque scellement, séparées par des virgules. **Lue mais pas encore exploitée** (écart J-1) | | `OPENEIDAS_CROSS_TSA_TIMEOUT` | `15s` | Délai d'attente par TSA tierce | -| `OPENEIDAS_AUDIT_REPLICA_URL` | — | Base WebDAV où répliquer le journal à chaque scellement (vide = désactivé) | +| `OPENEIDAS_AUDIT_REPLICA_URL` | — | Base WebDAV où répliquer le journal à chaque scellement (vide = désactivé). **Lue mais pas encore exploitée** (écart J-1) | | `OPENEIDAS_AUDIT_REPLICA_USER` | — | Utilisateur WebDAV | | `OPENEIDAS_AUDIT_REPLICA_PASSWORD` | — | Mot de passe WebDAV | | `OPENEIDAS_AUDIT_REPLICA_TIMEOUT` | `30s` | Délai d'attente de la réplication | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 312aac3..60a222c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -194,67 +194,49 @@ quiconque relit le fichier — y compris sans accès au service : docker compose exec tsa tsa-server verify-audit ``` -Y sont consignés l'ouverture du journal, chaque jeton émis (numéro de série, -`genTime`, politique, empreinte soumise, présence d'un nonce), chaque refus -avec son `failureInfo`, chaque mesure de temps avec l'écart par source, et -chaque enrôlement de certificat. Le jeton émis est **relu avant d'être -consigné** : le journal enregistre ce que contient réellement le jeton, pas ce -que le service croit y avoir mis. +Y sont consignés l'ouverture du journal, chaque jeton émis +(`timestamp.granted` : `genTime`, politique, présence d'un nonce, et le numéro +de série du **certificat** TSU), chaque refus avec son `failureInfo` +(`timestamp.rejected`) et chaque mesure de temps avec l'écart par source +(`time.measurement`). La CA tient son propre journal chaîné, où figurent les +demandes d'enrôlement, les décisions RA, les émissions et les révocations. + +**Écart en cours de correction** (constat J-3 de l'audit du 2026-09-25, +PR #52) : le journal de la TSA ne consigne pas encore le numéro de série du +**jeton**, ni l'empreinte soumise, et le jeton n'est pas relu avant d'être +consigné. Deux propriétés rendent le dispositif exploitable : -- **Une écriture ratée annule l'émission.** Si le journal ne peut pas être - écrit, la requête échoue. Un jeton non tracé ne sort jamais du service. +- **Une écriture ratée annule l'émission.** L'événement est écrit *avant* la + signature : si le journal ne peut pas être écrit, la requête échoue et rien + n'est signé. Un jeton non tracé ne sort jamais du service. Il en va de même + côté CA pour l'émission, la révocation et la publication de la CRL. - **Un journal altéré empêche le démarrage.** La chaîne est vérifiée intégralement à l'ouverture. -La tête de chaîne est **scellée périodiquement** -(`OPENEIDAS_AUDIT_SEAL_INTERVAL`, une heure par défaut) : la TSU horodate sa -propre empreinte de tête et le jeton obtenu est inscrit au journal, ce qui -date son contenu. - -### Contreseing par des TSA tierces - -Le scellement ci-dessus reste auto-référentiel : il ne prouve l'antériorité à -un tiers que si l'on fait déjà confiance à la TSU elle-même. Le service -soumet donc la même tête de chaîne à une ou plusieurs **TSA publiques -indépendantes** (`OPENEIDAS_CROSS_TSA_URLS`, par défaut FreeTSA.org et -DigiCert), via le protocole RFC 3161 standard, et consigne chaque attestation -obtenue (`log.cross_sealed`) : émetteur, date, numéro de série et jeton -complet en base64. - -Un auditeur n'a besoin de rien d'Open eIDAS pour vérifier une attestation : le -certificat de la TSA tierce est public, et les outils standards suffisent — -```bash -openssl ts -query -digest -sha256 -no_nonce -out head.tsq -openssl ts -verify -in -queryfile head.tsq \ - -CAfile -untrusted -``` -— une réponse `Verification: OK` établit que la tête de chaîne, donc tout le -journal qu'elle couvre par construction, existait à la date attestée par une -autorité qui n'a aucun lien avec Open eIDAS. - -L'indisponibilité d'une TSA tierce est journalisée mais non bloquante : le -scellement propre au service continue, et les autres TSA configurées -prennent le relais. - -### Réplication hors site - -Un journal chaîné et contresigné ne protège que contre l'altération — pas -contre la perte de l'instance elle-même (panne disque, compromission, -suppression accidentelle). À chaque scellement, le service dépose donc une -copie complète et datée du journal (`audit--seq.log`) sur un -serveur **WebDAV** distant (`OPENEIDAS_AUDIT_REPLICA_URL`) : Nextcloud, un -stockage d'objets exposé en WebDAV, ou tout hébergeur souverain qui l'offre — -aucun fournisseur particulier n'est imposé. - -Chaque copie est un journal complet et vérifiable indépendamment : -```bash -tsa-server verify-audit audit-20260906T145600Z-seq000030.log -``` -retrouve exactement la même chaîne de hachage que sur l'instance d'origine, -jusqu'au numéro de séquence capturé. L'échec de la réplication est -journalisé mais non bloquant, comme pour le contreseing tiers. +### Limite : l'intégrité repose sur l'hôte (écart déclaré) + +Le chaînage SHA-256 n'utilise pas de clé : quiconque peut écrire dans le +fichier peut en tronquer la fin ou le réécrire en recalculant la chaîne, sans +que `verify-audit` le voie. Ce qui ancre la tête de chaîne hors du système +**n'est pas encore en service** (constat J-1) : + +- **Scellement périodique** par la TSU elle-même (`log.sealed`) : le format + est prévu (`oe_audit::EVENT_SEALED`, compté par `verify-audit`), mais aucun + service ne scelle. +- **Contreseing par des TSA tierces** indépendantes (`oe-crosstsa`, protocole + RFC 3161 standard, vérifiable par `openssl ts -verify` sans rien d'Open + eIDAS) : bibliothèque écrite et testée contre un vrai serveur, non câblée. +- **Réplication hors site** : `oe-replicate` (WebDAV) est écrit et testé, non + câblé ; une copie vers un stockage objet compatible S3 auto-hébergé est en + cours (PR #49 et #50). + +Les variables `OPENEIDAS_AUDIT_SEAL_INTERVAL`, `OPENEIDAS_CROSS_TSA_URLS` et +`OPENEIDAS_AUDIT_REPLICA_*` du `docker-compose.yml` et du chart Helm sont +lues sans être exploitées. D'ici là, l'intégrité du journal revient au +contrôle d'accès de son volume (voir [CPS.md](CPS.md) B.3 et +[CONFORMITE-ETSI.md](CONFORMITE-ETSI.md)). ## 8. Écarts assumés du prototype vis-à-vis d'une TSA qualifiée @@ -269,10 +251,10 @@ ci-dessous en donne la lecture d'ensemble. | Source de temps | Surveillance NTP de deux sources UTC(k) avec suspension automatique de l'émission | Réception redondante et indépendante, calibration documentée, journal des mesures conservé et audité | | Cérémonie de clé | Scriptée, idempotente, procès-verbal consigné au journal d'audit (empreintes, opérateur, horodatage) — mais sans double contrôle ni témoin | Double contrôle, témoin indépendant, HSM certifié, racine hors ligne après cérémonie (voir [CA.md](CA.md)) | | Approbation RA | Point d'approbation réellement actif : aucun chemin du code ne mène à l'émission sans décision d'un opérateur identifié, consignée en base et au journal. Automatisée sous un compte technique pour que la démonstration/CI s'amorce sans opérateur humain | Revue humaine réelle par un opérateur RA nominatif, à la place de l'approbation automatisée | -| Journalisation | Journal chaîné par hachage, contresigné par des TSA tierces publiques et répliqué hors site à chaque scellement ; durée de conservation contrôlée au démarrage | Politique de conservation formalisée, réplication multi-région | +| Journalisation | Journal chaîné par hachage, écrit avant chaque émission ; durée de conservation contrôlée au démarrage côté CA seulement. Scellement, contreseing tiers et réplication hors site **non câblés** (écart J-1) | Scellement et contreseing périodiques, copie hors site, conservation contrôlée côté TSA, politique de conservation formalisée | | Politique d'horodatage | OID de test `1.3.6.1.4.1.99999.1.1.1` ; brouillon de Policy/Practice Statement dans [CPS.md](CPS.md) | OID sous l'arc PEN de l'association, [CPS.md](CPS.md) adopté formellement et publié | | Profils de certificat | Structures Rust compilées et testées ; le certificat émis est relu depuis son DER et re-contrôlé avant délivrance ; CDP, AIA `ca_issuers` et répondeur OCSP réellement publiés et vérifiés | OID de politique de certification propre | -| Continuité | Instance unique ; registre PostgreSQL sauvegardable, journal répliqué hors site | Redondance active/active, sauvegarde et restauration testées, plan de cessation d'activité engagé (voir [CA.md](CA.md)) | +| Continuité | Instance unique ; registre PostgreSQL sauvegardable ; journal **non** répliqué hors site (écart J-1) | Redondance active/active, sauvegarde et restauration testées, plan de cessation d'activité engagé (voir [CA.md](CA.md)) | | Audit | Aucun | Évaluation par un organisme accrédité (LSTI, Apave), inscription à la liste de confiance | Le prototype refuse de démarrer sur les écarts qui rendraient les jetons ou diff --git a/docs/CA.md b/docs/CA.md index 457325e..817006a 100644 --- a/docs/CA.md +++ b/docs/CA.md @@ -381,12 +381,12 @@ l'URL, le serveur qui publie et le répondeur OCSP qui va chercher la CRL. |---|---|---| | Vol de la clé de la CA émettrice | La clé ne quitte jamais le token PKCS#11 ; seul le conteneur `ca` y accède | SoftHSM2 est logiciel : qui obtient le volume et le PIN obtient la clé. Un HSM certifié lève ce point | | Vol du secret HMAC d'enrôlement | Il authentifie le demandeur, il ne décide pas : toute demande reste soumise à approbation | Une clé volée permet de déposer des demandes, pas d'en faire émettre | -| Compromission d'un opérateur RA | Chaque décision est consignée avec son auteur ; le journal est chaîné, scellé et répliqué hors site | Un opérateur seul peut approuver : il n'y a pas de double validation | +| Compromission d'un opérateur RA | Chaque décision est consignée avec son auteur dans le journal chaîné | Un opérateur seul peut approuver : il n'y a pas de double validation. Le journal n'est ni scellé ni répliqué hors site (écart J-1) : un accès en écriture à son volume permet de le réécrire | | Numéro de série prédit ou rejoué | 128 bits sur `crypto/rand`, unicité portée par la clé primaire du registre | — | | Certificat non conforme émis | Le DER produit est relu et re-contrôlé avant d'être enregistré ; l'émission est annulée sinon | — | | CRL ancienne rejouée pour masquer une révocation | `CRLNumber` strictement croissant, servi par une séquence PostgreSQL ; le répondeur OCSP refuse de répondre plutôt que de servir un statut obsolète | — | -| Altération du journal d'audit | Chaînage par hachage vérifié à l'ouverture ; un journal altéré empêche le démarrage | — | -| Perte de l'instance | Registre PostgreSQL sauvegardable, journal répliqué hors site à chaque scellement | Sauvegarde et restauration non encore testées de bout en bout | +| Altération du journal d'audit | Chaînage par hachage vérifié à l'ouverture ; un journal altéré empêche le démarrage | Chaînage sans clé : une réécriture complète avec recalcul de la chaîne n'est pas détectée tant que la tête n'est pas scellée hors du système (écart J-1) | +| Perte de l'instance | Registre PostgreSQL sauvegardable | Journal non répliqué hors site (écart J-1) ; sauvegarde et restauration non encore testées de bout en bout | ## 8. Continuité et cessation d'activité @@ -398,8 +398,8 @@ Ce qui doit être sauvegardé, et suffit à reconstituer l'autorité : définitivement perdue ; 2. la **base PostgreSQL** — registre des certificats, demandes, historique des CRL ; -3. le **journal d'audit** (volume `castate`), déjà répliqué hors site à chaque - scellement. +3. le **journal d'audit** (volume `castate`) — pas encore répliqué hors site + (écart J-1) : à sauvegarder avec le reste. État actuel : instance unique, sauvegarde non automatisée. C'est un écart documenté ; la cible est une redondance active/active, une sauvegarde diff --git a/docs/CONFORMITE-ETSI.md b/docs/CONFORMITE-ETSI.md index 0a5dcf0..f28e5e9 100644 --- a/docs/CONFORMITE-ETSI.md +++ b/docs/CONFORMITE-ETSI.md @@ -5,7 +5,7 @@ correction se fait dans le code, pour que la matrice publiée reste celle que le système applique réellement. --> -**28 exigences** — 24 couvertes, 2 écarts documentés, 2 hors périmètre logiciel. +**28 exigences** — 22 couvertes, 4 écarts documentés, 2 hors périmètre logiciel. Trois statuts seulement, pour qu'aucune zone grise ne puisse s'y loger : @@ -18,9 +18,9 @@ Trois statuts seulement, pour qu'aucune zone grise ne puisse s'y loger : | Clause | Exigence | Statut | Mécanisme | Vérification / cible | |---|---|---|---|---| | §7.4 | Gestion des clés du prestataire dans un module cryptographique | couvert | Toutes les clés vivent dans un token PKCS#11 et n'en sortent jamais : oe-hsm::Pkcs11Token, validé contre un vrai token SoftHSM2 (crates/oe-hsm/tests/pkcs11_integration.rs). | crates/oe-hsm/tests/pkcs11_integration.rs | -| §7.10 | Journalisation des événements et durée de conservation | couvert | Journal JSON Lines chaîné par SHA-256 (oe-audit) ; durée de conservation contrôlée à la configuration par oe_conformance::check_audit_retention, branché sur bin/ca-server::Config::load. | crates/oe-audit/src/lib.rs (deux_ecrivains_partagent_la_meme_chaine), crates/oe-conformance/src/lib.rs (check_audit_retention_accepts_the_minimum, check_audit_retention_rejects_unconfigured_and_short_durations) | +| §7.10 | Journalisation des événements et durée de conservation | écart documenté | Journal JSON Lines chaîné par SHA-256 (oe-audit), écrit avant chaque émission dans les deux services ; durée de conservation contrôlée à la configuration par oe_conformance::check_audit_retention, mais côté CA seulement (bin/ca-server::Config::load). | **Cible :** Contrôler aussi la durée de conservation au démarrage de tsa-server (docs/CPS.md B.3). | | §7.9 | Intégrité démontrable des enregistrements d'audit | couvert | Chaînage par hachage vérifié intégralement à l'ouverture ; verrou de fichier partagé entre plusieurs écrivains d'un même processus : oe-audit::Log. | crates/oe-audit/src/lib.rs (deux_ecrivains_partagent_la_meme_chaine, verify_detects_modified_record, verify_detects_truncated_and_rewritten_tail) | -| §7.11 | Continuité d'activité et reprise après sinistre | couvert | Contreseing du journal par une TSA tierce (oe-crosstsa) et réplication WebDAV hors site (oe-replicate), validés contre un vrai serveur. | crates/oe-crosstsa/tests/against_local_server.rs (seals_a_digest_against_a_real_rfc3161_server), crates/oe-replicate/tests/against_local_server.rs (replicates_content_via_webdav_put) | +| §7.11 | Continuité d'activité et reprise après sinistre | écart documenté | Contreseing du journal par une TSA tierce (oe-crosstsa) et réplication WebDAV hors site (oe-replicate) écrits et testés contre un vrai serveur, mais appelés par aucun binaire : l'intégrité et la survie du journal reposent sur le contrôle d'accès et la sauvegarde de son volume. | **Cible :** Constat J-1 : câbler dans tsa-server serve puis ca-server serve le scellement périodique (log.sealed), le contreseing tiers (log.cross_sealed) et la copie hors site (PR #49, #50 pour le stockage S3), testés de bout en bout sur le binaire ; dégrader /healthz sur échec prolongé. | | §7.12 | Plan de cessation d'activité | hors périmètre logiciel | Procédure organisationnelle décrite dans docs/CA.md, indépendante du langage d'implémentation. | **Cible :** Engagement juridique de l'association, dépôt auprès de l'organe de contrôle, séquestre des journaux. | | §6.1 | Politique de service et déclaration des pratiques publiées | écart documenté | docs/CPS.md porte un brouillon structuré, déjà indépendant du langage d'implémentation du service. | **Cible :** Adoption formelle de docs/CPS.md par l'association (organisationnel, non affecté par le portage Rust). | From d25ee8cdb822d5501368da186e063bd71a192a76 Mon Sep 17 00:00:00 2001 From: Philippe Vienne Date: Sun, 27 Sep 2026 09:16:12 +0200 Subject: [PATCH 2/2] =?UTF-8?q?docs(conformance):=20versions=20de=20normes?= =?UTF-8?q?,=20identifiants=20d'exigence=20et=20clauses=20v=C3=A9rifi?= =?UTF-8?q?=C3=A9es=20(D-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Constat D-3 de l'audit du 2026-09-25 : la matrice ne citait aucune version de norme ni identifiant d'exigence, et plusieurs lignes citaient une clause qui ne porte pas l'exigence annoncée. Chaque clause et identifiant a été relu dans le texte officiel, dernière version publiée sur etsi.org : EN 319 401 V3.2.1, EN 319 411-1 V1.5.1, EN 319 421 V1.3.1, EN 319 422 V1.1.1, TS 119 312 V2.1.1, EN 319 403-1 V2.3.1. - Requirement gagne `version` et `ids` ; validate() échoue si la version d'une norme ETSI manque, si une ligne de 401/411-1/421 n'a pas d'identifiant, si un identifiant ne relève pas de la clause citée, ou si une norme est citée en deux versions. - Les 10 clauses signalées sont corrigées, plus 4 non relues par l'audit : EN 319 412-1 §4 et §4.1 ne portent pas les exigences citées (→ 411-1 GEN-6.6.1-02, GEN-6.3.3-02A), TS 119 312 V2.1.1 a renuméroté (§6.2 → §8.4, §6.1 → §7.3). - Nouvelles lignes, en écart avec leur cible (PR ouvertes ou constats) : ARL (C-1), OCSP non émis (O-1), MFA (R-1/R-2), exactitude (T-1), horloge (T-2), secondes intercalaires (T-4), expiration de la clé TSU (T-3), jetons affectés (J-3). Une ligne visée par une NC ouverte n'est plus déclarée couverte. 36 exigences : 17 couvertes, 17 écarts, 2 hors périmètre. - Rendu : version dans le titre de section, identifiants dans la colonne Clause ; /api/v1/conformance expose `version` et `identifiants`. - Renvois de clauses corrigés dans les commentaires et la doc (la migration 0001 n'est pas touchée : sqlx en contrôle la somme). Co-authored-by: Claude --- bin/ca-server/src/http.rs | 2 + crates/oe-ca-core/src/extensions.rs | 2 +- crates/oe-ca-core/src/lib.rs | 2 +- crates/oe-ca-core/src/profile.rs | 4 +- crates/oe-conformance/src/lib.rs | 351 +++++++++++++++++++++------- crates/oe-raflow/src/lib.rs | 6 +- crates/oe-raflow/tests/flow.rs | 4 +- crates/oe-timesource/Cargo.toml | 2 +- crates/oe-timesource/src/lib.rs | 2 +- crates/oe-timesource/src/ntp.rs | 2 +- docs/CA.md | 4 +- docs/CONFORMITE-ETSI.md | 71 +++--- docs/CPS.md | 2 +- docs/WEBUI.md | 2 +- tests/fixtures/rfc3161/README.md | 2 +- 15 files changed, 326 insertions(+), 132 deletions(-) diff --git a/bin/ca-server/src/http.rs b/bin/ca-server/src/http.rs index 0c8fe6c..d6328fc 100644 --- a/bin/ca-server/src/http.rs +++ b/bin/ca-server/src/http.rs @@ -469,7 +469,9 @@ async fn handle_conformance(State(server): State>) -> impl IntoRespo .map(|e| { serde_json::json!({ "norme": e.requirement.standard, + "version": e.requirement.version, "clause": e.requirement.clause, + "identifiants": e.requirement.ids, "exigence": e.requirement.title, "statut": e.status.label(), "mecanisme": e.mechanism, diff --git a/crates/oe-ca-core/src/extensions.rs b/crates/oe-ca-core/src/extensions.rs index c1c337c..2a36f28 100644 --- a/crates/oe-ca-core/src/extensions.rs +++ b/crates/oe-ca-core/src/extensions.rs @@ -84,7 +84,7 @@ pub(crate) fn authority_key_identifier(parent_ski: &[u8]) -> Result Result { - const SERIAL_BYTES: usize = 16; // 128 bits, ETSI EN 319 412-1 §4.1. + const SERIAL_BYTES: usize = 16; // 128 bits, ETSI EN 319 411-1 GEN-6.3.3-02A. const ATTEMPTS: u32 = 5; for _ in 0..ATTEMPTS { let mut bytes = [0u8; SERIAL_BYTES]; diff --git a/crates/oe-ca-core/src/profile.rs b/crates/oe-ca-core/src/profile.rs index 18d458c..8cc4cd1 100644 --- a/crates/oe-ca-core/src/profile.rs +++ b/crates/oe-ca-core/src/profile.rs @@ -37,7 +37,7 @@ pub struct Profile { pub key_usages: der::flagset::FlagSet, pub eku: &'static [&'static str], - /// ETSI EN 319 421 §7.7.2 exige `extendedKeyUsage` critique pour un + /// ETSI EN 319 422 §6.4 (RFC 3161 §2.3) exige `extendedKeyUsage` critique pour un /// certificat de TSU : sans criticité, un vérificateur peut ignorer la /// restriction d'usage. pub eku_critical: bool, @@ -74,7 +74,7 @@ pub fn eku_oids(p: &Profile) -> Vec { p.eku.iter().map(|s| oid(s)).collect() } -/// Reproduit le profil ETSI EN 319 422 / EN 319 421 §7.7.2 de l'unité +/// Reproduit le profil ETSI EN 319 422 §6 de l'unité /// d'horodatage. pub fn tsa_signer() -> Profile { Profile { diff --git a/crates/oe-conformance/src/lib.rs b/crates/oe-conformance/src/lib.rs index c5de71f..399e40d 100644 --- a/crates/oe-conformance/src/lib.rs +++ b/crates/oe-conformance/src/lib.rs @@ -21,25 +21,50 @@ use std::collections::BTreeMap; use std::fmt; -/// Identifie une clause normative précise, citée telle qu'elle apparaît dans -/// la norme. +/// Identifie une exigence précise : la norme, sa version, la clause et les +/// identifiants d'exigence qu'elle numérote (constat D-3 de l'audit du +/// 2026-09-25 : c'est la grille qu'un organisme d'évaluation suit). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Requirement { pub standard: &'static str, + /// Version et date de publication de la norme citée, telles qu'en tête + /// du texte (« V3.2.1 (2026-01) ») ; vide pour une RFC. + pub version: &'static str, pub clause: &'static str, + /// Identifiants d'exigence (`REQ-7.10-08`, `TIS-7.6.7-01`…), pour les + /// normes qui en numérotent (voir [`STANDARDS_WITH_IDS`]). + pub ids: &'static [&'static str], pub title: &'static str, } +/// Normes dont chaque exigence porte un identifiant : une ligne de matrice +/// qui les cite doit nommer les identifiants, pas seulement la clause. +pub const STANDARDS_WITH_IDS: &[&str] = + &["ETSI EN 319 401", "ETSI EN 319 411-1", "ETSI EN 319 421"]; + impl fmt::Display for Requirement { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.clause.is_empty() { - write!(f, "{}", self.standard) - } else { - write!(f, "{} {}", self.standard, self.clause) + write!(f, "{}", self.standard)?; + if !self.clause.is_empty() { + write!(f, " {}", self.clause)?; + } + if !self.ids.is_empty() { + write!(f, " ({})", self.ids.join(", "))?; } + Ok(()) } } +/// Numéro de clause porté par un identifiant d'exigence : `7.10` pour +/// `REQ-7.10-08`, `7.6.7` pour `TIS-7.6.7-01`. +fn id_clause(id: &str) -> Option<&str> { + let mut parts = id.split('-'); + let _prefix = parts.next()?; + let clause = parts.next()?; + parts.next()?; + Some(clause) +} + /// Distingue ce qui interdit une opération de ce qui doit seulement être /// signalé. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -172,11 +197,37 @@ impl Matrix { pub fn validate(&self) -> Result<(), String> { let mut problems = Vec::new(); let mut seen = std::collections::HashSet::new(); + let mut versions: BTreeMap<&str, &str> = BTreeMap::new(); for e in &self.0 { let key = e.requirement.to_string(); if !seen.insert(key.clone()) { problems.push(format!("{key}: exigence déclarée deux fois")); } + let r = &e.requirement; + if r.standard.starts_with("ETSI") && r.version.is_empty() { + problems.push(format!("{key}: version de la norme non précisée")); + } + if let Some(previous) = versions.insert(r.standard, r.version) { + if previous != r.version { + problems.push(format!( + "{key}: {} citée en deux versions ({previous}, {})", + r.standard, r.version + )); + } + } + if STANDARDS_WITH_IDS.contains(&r.standard) && r.ids.is_empty() { + problems.push(format!("{key}: aucun identifiant d'exigence")); + } + // L'identifiant porte sa clause : la clause citée doit la contenir + // (c'est ainsi que 10 lignes citaient une clause erronée, D-3). + for id in r.ids { + match id_clause(id) { + Some(c) if r.clause.split(", ").any(|cl| cl == format!("§{c}")) => {} + _ => problems.push(format!( + "{key}: l'identifiant {id} ne relève pas de la clause citée" + )), + } + } match e.status { Status::Covered => { if e.mechanism.is_empty() { @@ -244,7 +295,16 @@ pub fn render_markdown(m: &Matrix) -> String { b.push_str("- **hors périmètre logiciel** — exigence organisationnelle, qu'aucun code ne peut établir seul.\n\n"); for standard in m.standards() { - b.push_str(&format!("## {standard}\n\n")); + let version = + m.0.iter() + .find(|e| e.requirement.standard == standard) + .map(|e| e.requirement.version) + .unwrap_or_default(); + if version.is_empty() { + b.push_str(&format!("## {standard}\n\n")); + } else { + b.push_str(&format!("## {standard} {version}\n\n")); + } b.push_str("| Clause | Exigence | Statut | Mécanisme | Vérification / cible |\n"); b.push_str("|---|---|---|---|---|\n"); for e in &m.0 { @@ -256,9 +316,18 @@ pub fn render_markdown(m: &Matrix) -> String { } else { e.test.to_string() }; + let clause = if e.requirement.ids.is_empty() { + cell(e.requirement.clause) + } else { + format!( + "{} — {}", + cell(e.requirement.clause), + cell(&e.requirement.ids.join(", ")) + ) + }; b.push_str(&format!( "| {} | {} | {} | {} | {} |\n", - cell(e.requirement.clause), + clause, cell(e.requirement.title), cell(e.status.label()), cell(e.mechanism), @@ -304,7 +373,7 @@ pub const MAX_OCSP_LIFETIME: time::Duration = time::Duration::days(6 * 30); pub const MAX_ISSUING_CA_LIFETIME: time::Duration = time::Duration::days(15 * 365); pub const MAX_ROOT_CA_LIFETIME: time::Duration = time::Duration::days(25 * 365); -/// ETSI EN 319 411-1 §6.3.2 : la durée de vie effective d'un certificat déjà +/// ETSI EN 319 421 §7.6.5 (TIS-7.6.5-01) : la durée de vie effective d'un certificat déjà /// signé (pas celle que son profil visait) ne doit pas dépasser le plafond /// applicable à sa catégorie. pub fn check_certificate_lifetime( @@ -335,7 +404,7 @@ const ADMITTED_SIGNATURE_ALGORITHM_OIDS: &[&str] = &[ "1.2.840.113549.1.1.13", // sha512WithRSAEncryption ]; -/// ETSI TS 119 312 §6.1 : l'algorithme de signature d'un objet (certificat, +/// ETSI TS 119 312 §7.3 : l'algorithme de signature d'un objet (certificat, /// CSR, CRL) doit figurer parmi les suites admises — SHA-1 et MD5 en sont /// exclus explicitement, jamais tolérés par omission. pub fn check_signature_algorithm(subject: &str, algorithm_oid: &str) -> Result<(), String> { @@ -362,7 +431,7 @@ fn find_extension<'a>( .find(|e| e.extn_id == target) } -/// ETSI EN 319 421 §7.7.2 : le certificat de l'unité d'horodatage relit et +/// ETSI EN 319 422 §6 : le certificat de l'unité d'horodatage relit et /// re-contrôlé (pas seulement construit une fois à l'émission) doit porter /// CA:FALSE, un `extendedKeyUsage` critique contenant **seulement** /// id-kp-timeStamping, un `keyUsage` restreint à @@ -622,201 +691,257 @@ pub fn check_internal_server_certificate( pub fn system_matrix() -> Matrix { Matrix(vec![ Entry { - requirement: Requirement { standard: "ETSI EN 319 401", clause: "§7.4", title: "Gestion des clés du prestataire dans un module cryptographique" }, + requirement: Requirement { standard: "ETSI EN 319 401", version: "V3.2.1 (2026-01)", clause: "§7.5", ids: &["REQ-7.5-01"], title: "Gestion des clés du prestataire dans un module cryptographique" }, status: Status::Covered, mechanism: "Toutes les clés vivent dans un token PKCS#11 et n'en sortent jamais : oe-hsm::Pkcs11Token, validé contre un vrai token SoftHSM2 (crates/oe-hsm/tests/pkcs11_integration.rs).", test: "crates/oe-hsm/tests/pkcs11_integration.rs", target: "", }, Entry { - requirement: Requirement { standard: "ETSI EN 319 401", clause: "§7.10", title: "Journalisation des événements et durée de conservation" }, + requirement: Requirement { standard: "ETSI EN 319 401", version: "V3.2.1 (2026-01)", clause: "§7.10", ids: &["REQ-7.10-01", "REQ-7.10-07"], title: "Journalisation des événements et durée de conservation" }, status: Status::Gap, mechanism: "Journal JSON Lines chaîné par SHA-256 (oe-audit), écrit avant chaque émission dans les deux services ; durée de conservation contrôlée à la configuration par oe_conformance::check_audit_retention, mais côté CA seulement (bin/ca-server::Config::load).", test: "crates/oe-audit/src/lib.rs (deux_ecrivains_partagent_la_meme_chaine), crates/oe-conformance/src/lib.rs (check_audit_retention_accepts_the_minimum, check_audit_retention_rejects_unconfigured_and_short_durations)", - target: "Contrôler aussi la durée de conservation au démarrage de tsa-server (docs/CPS.md B.3).", + target: "Constat J-3 (PR #52) : consigner la série du jeton et l'empreinte soumise ; contrôler aussi la durée de conservation au démarrage de tsa-server (docs/CPS.md B.3).", }, Entry { - requirement: Requirement { standard: "ETSI EN 319 401", clause: "§7.9", title: "Intégrité démontrable des enregistrements d'audit" }, - status: Status::Covered, - mechanism: "Chaînage par hachage vérifié intégralement à l'ouverture ; verrou de fichier partagé entre plusieurs écrivains d'un même processus : oe-audit::Log.", + requirement: Requirement { standard: "ETSI EN 319 401", version: "V3.2.1 (2026-01)", clause: "§7.10", ids: &["REQ-7.10-02", "REQ-7.10-08"], title: "Intégrité démontrable des enregistrements d'audit" }, + status: Status::Gap, + mechanism: "Chaînage par hachage vérifié intégralement à l'ouverture ; verrou de fichier partagé entre plusieurs écrivains : oe-audit::Log. Le chaînage n'a pas de clé : une réécriture complète du fichier avec recalcul de la chaîne n'est pas détectée tant que la tête n'est pas ancrée hors du système.", test: "crates/oe-audit/src/lib.rs (deux_ecrivains_partagent_la_meme_chaine, verify_detects_modified_record, verify_detects_truncated_and_rewritten_tail)", - target: "", + target: "Constat J-1 : sceller périodiquement la tête de chaîne (TSU et TSA tierce) et en déposer une copie hors site, depuis les binaires en service.", }, Entry { - requirement: Requirement { standard: "ETSI EN 319 401", clause: "§7.11", title: "Continuité d'activité et reprise après sinistre" }, + requirement: Requirement { standard: "ETSI EN 319 401", version: "V3.2.1 (2026-01)", clause: "§7.11.1, §7.11.2", ids: &["REQ-7.11.1-01", "REQ-7.11.2-01"], title: "Copies de sauvegarde et plan de sauvegarde" }, status: Status::Gap, mechanism: "Contreseing du journal par une TSA tierce (oe-crosstsa) et réplication WebDAV hors site (oe-replicate) écrits et testés contre un vrai serveur, mais appelés par aucun binaire : l'intégrité et la survie du journal reposent sur le contrôle d'accès et la sauvegarde de son volume.", test: "crates/oe-crosstsa/tests/against_local_server.rs (seals_a_digest_against_a_real_rfc3161_server), crates/oe-replicate/tests/against_local_server.rs (replicates_content_via_webdav_put)", target: "Constat J-1 : câbler dans tsa-server serve puis ca-server serve le scellement périodique (log.sealed), le contreseing tiers (log.cross_sealed) et la copie hors site (PR #49, #50 pour le stockage S3), testés de bout en bout sur le binaire ; dégrader /healthz sur échec prolongé.", }, Entry { - requirement: Requirement { standard: "ETSI EN 319 401", clause: "§7.12", title: "Plan de cessation d'activité" }, + requirement: Requirement { standard: "ETSI EN 319 401", version: "V3.2.1 (2026-01)", clause: "§7.12", ids: &["REQ-7.12-02", "REQ-7.12-10"], title: "Plan de cessation d'activité" }, status: Status::OutOfScope, mechanism: "Procédure organisationnelle décrite dans docs/CA.md, indépendante du langage d'implémentation.", test: "", target: "Engagement juridique de l'association, dépôt auprès de l'organe de contrôle, séquestre des journaux.", }, Entry { - requirement: Requirement { standard: "ETSI EN 319 411-1", clause: "§6.6.1", title: "Profil du certificat d'autorité de certification" }, + requirement: Requirement { standard: "ETSI EN 319 401", version: "V3.2.1 (2026-01)", clause: "§6.1", ids: &["REQ-6.1-01", "REQ-6.1-03", "REQ-6.1-05"], title: "Politique de service et déclaration des pratiques publiées" }, + status: Status::Gap, + mechanism: "docs/CPS.md porte un brouillon structuré, déjà indépendant du langage d'implémentation du service.", + test: "", + target: "Adoption formelle de docs/CPS.md par l'association (organisationnel, non affecté par le portage Rust).", + }, + Entry { + requirement: Requirement { standard: "ETSI EN 319 411-1", version: "V1.5.1 (2025-04)", clause: "§6.6.1", ids: &["GEN-6.6.1-01"], title: "Profil du certificat d'autorité de certification" }, status: Status::Covered, mechanism: "Cérémonie produisant une racine et une CA émettrice au profil contrôlé (CA:TRUE critique, keyCertSign+cRLSign, SKI/AKI) : oe_ca_core::ceremony::run_ceremony, chaîne revérifiée par openssl.", test: "crates/oe-ca-core/tests/issuance.rs (ceremony_is_idempotent, ceremony_rejects_mismatched_signer_on_replay, openssl_accepts_the_chain_and_honors_revocation)", target: "", }, Entry { - requirement: Requirement { standard: "ETSI EN 319 411-1", clause: "§6.2.1", title: "Enregistrement et responsabilité de la décision d'émission" }, + requirement: Requirement { standard: "ETSI EN 319 411-1", version: "V1.5.1 (2025-04)", clause: "§6.6.1", ids: &["GEN-6.6.1-02"], title: "Certificats émis selon le profil applicable" }, status: Status::Covered, + mechanism: "Profils définis en structures Rust compilées, pas en configuration interprétée : oe_ca_core::profile. Contrôle de criticité (basicConstraints, keyUsage, EKU) posé à la main, vérifié par openssl.", + test: "crates/oe-ca-core/tests/issuance.rs (openssl_accepts_the_chain_and_honors_revocation)", + target: "", + }, + Entry { + requirement: Requirement { standard: "ETSI EN 319 411-1", version: "V1.5.1 (2025-04)", clause: "§6.3.2, §6.4.5", ids: &["REG-6.3.2-00B", "REG-6.4.5-03", "REG-6.4.5-04"], title: "Enregistrement et responsabilité de la décision d'émission" }, + status: Status::Gap, mechanism: "Aucune transition vers Approved n'existe sans identité d'opérateur : oe_raflow::Decider::approve/reject. L'identité est consignée en base et au journal d'audit.", test: "crates/oe-raflow/tests/flow.rs (decide_without_operator_identity_is_refused, approve_then_resubmit_issues_a_certificate_signed_by_the_issuing_key)", - target: "", + target: "Constat R-2 (PR #57) : approbation automatique désactivée par défaut dans le chart Helm, refusée avec production: true.", }, Entry { - requirement: Requirement { standard: "ETSI EN 319 411-1", clause: "§6.3.1", title: "Authentification de la demande de certificat" }, - status: Status::Covered, + requirement: Requirement { standard: "ETSI EN 319 411-1", version: "V1.5.1 (2025-04)", clause: "§6.3.2", ids: &["REG-6.3.2-01"], title: "Authentification de la demande de certificat" }, + status: Status::Gap, mechanism: "HMAC-SHA256 sur la CSR DER, vérifié en temps constant, et vérification de l'auto-signature de la CSR (preuve de possession) : oe_raflow::Flow::submit.", test: "crates/oe-raflow/tests/flow.rs (submit_without_valid_hmac_is_unauthenticated, submit_opens_a_pending_request_idempotently)", - target: "", + target: "Constat R-3 : un secret, ou mieux une identité mTLS, par demandeur, liée aux profils qu'il peut demander ; ne révoquer au renouvellement que les certificats du même demandeur.", }, Entry { - requirement: Requirement { standard: "ETSI EN 319 411-1", clause: "§6.3.2", title: "Durée de vie du certificat plafonnée" }, + requirement: Requirement { standard: "ETSI EN 319 411-1", version: "V1.5.1 (2025-04)", clause: "§6.3.3", ids: &["GEN-6.3.3-02A"], title: "Numéro de série aléatoire" }, status: Status::Covered, - mechanism: "oe_conformance::check_certificate_lifetime relit la validité du certificat réellement signé et la compare à un plafond indépendant du profil (MAX_END_ENTITY_LIFETIME/MAX_OCSP_LIFETIME) ; appelé via le champ Profile::check de oe_ca_core::Issuer::issue, comme profile.Check (Go).", - test: "crates/oe-conformance/src/lib.rs (check_certificate_lifetime_accepts_within_the_ceiling, check_certificate_lifetime_rejects_beyond_the_ceiling), crates/oe-conformance/tests/tsu_certificate.rs", + mechanism: "Numéro de série de 128 bits tiré sur rand::thread_rng et réservé de façon atomique (contrainte d'unicité en base) : oe_ca_core::Issuer::reserve_serial, oe_castore::Store::reserve_serial.", + test: "crates/oe-castore/src/lib.rs (reserve_serial_twice_conflicts), crates/oe-castore/tests/postgres.rs (reserve_serial_twice_conflicts)", target: "", }, Entry { - requirement: Requirement { standard: "ETSI EN 319 411-1", clause: "§6.3.9", title: "Motif de révocation consigné" }, + requirement: Requirement { standard: "ETSI EN 319 411-1", version: "V1.5.1 (2025-04)", clause: "§6.4.5", ids: &["REV-6.4.5-09"], title: "Demandes de révocation et motif consignés" }, status: Status::Covered, mechanism: "Motif RFC 5280 obligatoire à la révocation (Issuer::revoke), persisté et repris dans chaque entrée de CRL avec son extension cRLReason.", test: "crates/oe-ca-core/tests/issuance.rs (revoke_is_idempotent_and_keeps_first_reason, revoke_then_publish_crl_lists_the_certificate)", target: "", }, Entry { - requirement: Requirement { standard: "ETSI EN 319 411-1", clause: "§6.3.10", title: "Publication régulière de l'état de révocation" }, + requirement: Requirement { standard: "ETSI EN 319 411-1", version: "V1.5.1 (2025-04)", clause: "§6.3.9", ids: &["CSS-6.3.9-05", "CSS-6.3.9-06"], title: "Publication de la CRL au moins toutes les 24 heures, avec nextUpdate" }, status: Status::Covered, mechanism: "oe_ca_core::Issuer::publish_crl produit une CRL signée, republiable même vide ; bin/ca-server::http::Server republie à intervalle régulier et dégrade /healthz (503) dès que la CRL servie est périmée, plutôt que de se déclarer sain sans pouvoir dire ce qui est révoqué.", test: "crates/oe-ca-core/tests/issuance.rs (revoke_then_publish_crl_lists_the_certificate), bin/ca-server/tests/crl_publication.rs (crl_is_republished_periodically, healthz_degrades_when_the_published_crl_is_stale)", target: "", }, Entry { - requirement: Requirement { standard: "RFC 6960", clause: "§2.1", title: "Service d'état de révocation interrogeable en ligne" }, - status: Status::Covered, - mechanism: "Répondeur OCSP RFC 6960 s'appuyant sur la CRL publiée par la CA : oe_ocsp_core::Responder.", - test: "crates/oe-ocsp-core/tests/against_real_crl.rs (reports_good_status_for_a_non_revoked_certificate, reports_revoked_status_for_a_revoked_certificate)", - target: "", + requirement: Requirement { standard: "ETSI EN 319 411-1", version: "V1.5.1 (2025-04)", clause: "§6.3.9, §6.3.10", ids: &["CSS-6.3.9-12", "CSS-6.3.9-13", "CSS-6.3.10-01"], title: "Révocation d'une autorité et ARL" }, + status: Status::Gap, + mechanism: "Aucune : la CA émettrice ne porte ni CDP ni AIA vers la racine, et la racine ne publie aucune ARL.", + test: "", + target: "Constat C-1 (PR #56) : CDP/AIA vers la racine, `ca-server authority revoke|publish-arl`, ARL servie, émission refusée par une émettrice révoquée.", }, Entry { - requirement: Requirement { standard: "ETSI EN 319 411-1", clause: "§6.5.1", title: "Cérémonie de génération des clés d'autorité" }, + requirement: Requirement { standard: "ETSI EN 319 411-1", version: "V1.5.1 (2025-04)", clause: "§6.6.3", ids: &["OVR-6.6.3-02"], title: "Statut OCSP d'un certificat jamais émis" }, + status: Status::Gap, + mechanism: "Le répondeur OCSP ne connaît que la CRL : un numéro de série absent de la CRL est déclaré good, y compris jamais émis.", + test: "", + target: "Constat O-1 (PR #55) : publier les numéros émis avec la CRL et répondre unknown pour un numéro absent.", + }, + Entry { + requirement: Requirement { standard: "ETSI EN 319 411-1", version: "V1.5.1 (2025-04)", clause: "§6.5.1", ids: &["GEN-6.5.1-04", "GEN-6.5.1-11", "GEN-6.5.1-13"], title: "Cérémonie de génération des clés d'autorité" }, status: Status::Gap, mechanism: "Cérémonie scriptée et idempotente (`ca-server ceremony`), produisant un procès-verbal consigné au journal d'audit (empreintes de clés, opérateur, date) : oe_ca_core::ceremony.", test: "crates/oe-ca-core/tests/issuance.rs (every_authority_decision_is_recorded)", target: "Cérémonie en double contrôle, sous témoin indépendant, sur HSM certifié, avec procès-verbal contresigné — écart organisationnel, pas seulement logiciel.", }, Entry { - requirement: Requirement { standard: "ETSI EN 319 412-1", clause: "§4", title: "Structures communes du profil de certificat" }, - status: Status::Covered, - mechanism: "Profils définis en structures Rust compilées, pas en configuration interprétée : oe_ca_core::profile. Contrôle de criticité (basicConstraints, keyUsage, EKU) posé à la main, vérifié par openssl.", - test: "crates/oe-ca-core/tests/issuance.rs (openssl_accepts_the_chain_and_honors_revocation)", - target: "", + requirement: Requirement { standard: "ETSI EN 319 411-1", version: "V1.5.1 (2025-04)", clause: "§6.5.5", ids: &["GEN-6.5.5-04", "CSS-6.5.5-06"], title: "Authentification multifacteur des comptes capables de provoquer une émission ou une révocation" }, + status: Status::Gap, + mechanism: "Actions d'opérateur signées par WebAuthn (clé attestée, rôle lu dans le registre, quorum pour la révocation) : oe-actions, derrière le lien interne mTLS de ca-server. Le CLI de secours (ra approve|reject, revoke) reste ouvert sans second facteur.", + test: "", + target: "Constats R-1/R-2 : relayer les actions signées depuis ra-console (docs/WEBUI.md §15, étape 3) ; tracer et revoir la voie de secours CLI (PR #58, #59) ; approbation automatique désactivée par défaut (PR #57).", }, Entry { - requirement: Requirement { standard: "ETSI EN 319 412-1", clause: "§4.1", title: "Numéro de série positif et imprévisible" }, + requirement: Requirement { standard: "ETSI EN 319 421", version: "V1.3.1 (2025-07)", clause: "§7.7.1, §7.7.2", ids: &["TIS-7.7.1-04", "TIS-7.7.1-07", "TIS-7.7.2-01", "TIS-7.7.2-06"], title: "Traçabilité de l'heure jusqu'à UTC et suspension en cas de dérive" }, status: Status::Covered, - mechanism: "Numéro de série de 128 bits tiré sur rand::thread_rng et réservé de façon atomique (contrainte d'unicité en base) : oe_ca_core::Issuer::reserve_serial, oe_castore::Store::reserve_serial.", - test: "crates/oe-castore/src/lib.rs (reserve_serial_twice_conflicts), crates/oe-castore/tests/postgres.rs (reserve_serial_twice_conflicts)", + mechanism: "Surveillance NTP multi-sources avec quorum, seuil de dérive (MaxOffset) et péremption (MaxAge) ; la politique enforce fait refuser chaque demande avec timeNotAvailable : oe_timesource::Monitor.", + test: "crates/oe-timesource/src/lib.rs (now_refuses_untraceable_time_in_enforce_mode, now_allows_untraceable_time_in_monitor_mode, new_rejects_quorum_larger_than_source_count), crates/oe-tsa-core/src/lib.rs (test_timestamp_refuses_when_time_is_not_traceable)", target: "", }, Entry { - requirement: Requirement { standard: "RFC 5280", clause: "§4.2.1.1-4.2.1.2", title: "Identifiants de clé de sujet et d'autorité présents" }, - status: Status::Covered, - mechanism: "subjectKeyIdentifier (SHA-1 de la clé, méthode 1) et authorityKeyIdentifier (pointant vers le SKI de l'émetteur) posés sans condition à l'émission et dans la cérémonie : oe_ca_core::extensions, oe_ca_core::signing::subject_key_id.", - test: "crates/oe-ca-core/tests/issuance.rs (issue_produces_a_certificate_signed_by_the_issuing_key, assert_ski_and_aki_present_and_linked)", - target: "", + requirement: Requirement { standard: "ETSI EN 319 421", version: "V1.3.1 (2025-07)", clause: "§7.7.1, §7.7.2", ids: &["TIS-7.7.1-05", "TIS-7.7.1-06", "TIS-7.7.2-03"], title: "Exactitude de l'heure du jeton (1 seconde ou mieux)" }, + status: Status::Gap, + mechanism: "genTime est tronqué à la seconde alors que la dérive tolérée atteint OPENEIDAS_TIME_MAX_OFFSET : l'écart réel peut dépasser l'exactitude annoncée.", + test: "", + target: "Constat T-1 (PR #53) : genTime à la milliseconde, et refus de démarrer si accuracy < max_offset + résolution.", }, Entry { - requirement: Requirement { standard: "ETSI EN 319 421", clause: "§7.6", title: "Traçabilité de l'heure jusqu'à UTC et suspension en cas de dérive" }, - status: Status::Covered, - mechanism: "Surveillance NTP multi-sources avec quorum, seuil de dérive (MaxOffset) et péremption (MaxAge) ; la politique enforce fait refuser chaque demande avec timeNotAvailable : oe_timesource::Monitor.", - test: "crates/oe-timesource/src/lib.rs (now_refuses_untraceable_time_in_enforce_mode, now_allows_untraceable_time_in_monitor_mode, new_rejects_quorum_larger_than_source_count), crates/oe-tsa-core/src/lib.rs (test_timestamp_refuses_when_time_is_not_traceable)", - target: "", + requirement: Requirement { standard: "ETSI EN 319 421", version: "V1.3.1 (2025-07)", clause: "§7.7.2", ids: &["TIS-7.7.2-04", "TIS-7.7.2-05"], title: "Protection de l'horloge contre un changement non détecté" }, + status: Status::Gap, + mechanism: "Plusieurs sources UTC(k) avec quorum et seuil de dérive (oe_timesource::Monitor), mais client NTP sans contrôle d'origine, de l'indicateur de seconde intercalaire ni NTS.", + test: "", + target: "Constat T-2 : contrôler l'origine et le LI des réponses NTP, envisager NTS (RFC 8915).", }, Entry { - requirement: Requirement { standard: "ETSI EN 319 421", clause: "§7.7.2", title: "Profil du certificat de l'unité d'horodatage" }, - status: Status::Covered, - mechanism: "oe_conformance::check_tsu_certificate (id-kp-timeStamping seul et critique, CA:FALSE, keyUsage restreint, durée de vie plafonnée) est appliquée à l'émission (Profile::check) ET re-contrôlée au démarrage de tsa-server (oe_tsa_core::Authority::new) — un certificat chargé depuis le disque peut venir d'ailleurs.", - test: "crates/oe-conformance/tests/tsu_certificate.rs (accepts_a_certificate_issued_with_the_tsa_signer_profile, rejects_a_certificate_issued_with_the_ocsp_responder_profile)", - target: "", + requirement: Requirement { standard: "ETSI EN 319 421", version: "V1.3.1 (2025-07)", clause: "§7.7.2", ids: &["TIS-7.7.2-07", "TIS-7.7.2-08", "TIS-7.7.2-09"], title: "Secondes intercalaires" }, + status: Status::Gap, + mechanism: "Aucune : la seconde intercalaire n'est ni détectée ni consignée.", + test: "", + target: "Constat T-4 : détecter l'annonce (LI), consigner l'instant du changement au journal.", }, Entry { - requirement: Requirement { standard: "ETSI EN 319 421", clause: "§7.7.1", title: "Génération de la clé TSU dans le module cryptographique" }, - status: Status::Covered, + requirement: Requirement { standard: "ETSI EN 319 421", version: "V1.3.1 (2025-07)", clause: "§7.6.2", ids: &["TIS-7.6.2-03"], title: "Génération de la clé TSU dans le module cryptographique" }, + status: Status::Gap, mechanism: "La bi-clé est générée dans le token PKCS#11 (oe_hsm::Pkcs11Token::generate_rsa_key) et ne manipule qu'un SigningToken ; la clé privée n'est jamais extraite.", test: "crates/oe-hsm/tests/pkcs11_integration.rs", + target: "Constat H-1 : vérifier au chargement que la clé a été générée dans le module (CKA_LOCAL, CKA_NEVER_EXTRACTABLE).", + }, + Entry { + requirement: Requirement { standard: "ETSI EN 319 421", version: "V1.3.1 (2025-07)", clause: "§7.6.5", ids: &["TIS-7.6.5-01"], title: "Durée de vie du certificat de l'unité d'horodatage plafonnée" }, + status: Status::Covered, + mechanism: "oe_conformance::check_certificate_lifetime relit la validité du certificat réellement signé et la compare à un plafond indépendant du profil (MAX_END_ENTITY_LIFETIME/MAX_OCSP_LIFETIME) ; appelé via le champ Profile::check de oe_ca_core::Issuer::issue, comme profile.Check (Go).", + test: "crates/oe-conformance/src/lib.rs (check_certificate_lifetime_accepts_within_the_ceiling, check_certificate_lifetime_rejects_beyond_the_ceiling), crates/oe-conformance/tests/tsu_certificate.rs", target: "", }, Entry { - requirement: Requirement { standard: "ETSI EN 319 422", clause: "§5", title: "Profil du jeton d'horodatage" }, + requirement: Requirement { standard: "ETSI EN 319 421", version: "V1.3.1 (2025-07)", clause: "§7.6.7, §7.7.1", ids: &["TIS-7.6.7-01", "TIS-7.6.7-02", "TIS-7.6.7-04", "TIS-7.6.7-06", "TIS-7.6.7-07", "TIS-7.6.7-09", "TIS-7.7.1-09"], title: "Date d'expiration de la clé TSU, refus d'émettre au-delà" }, + status: Status::Gap, + mechanism: "Validité du certificat TSU contrôlée au démarrage seulement ; aucune date d'expiration de clé définie.", + test: "", + target: "Constat T-3 (PR #54) : privateKeyUsagePeriod posé à l'émission, contrôlé à chaque signature ; nouvelle clé à chaque renouvellement.", + }, + Entry { + requirement: Requirement { standard: "ETSI EN 319 421", version: "V1.3.1 (2025-07)", clause: "§7.13", ids: &["OVR-7.13-05"], title: "Identification des jetons affectés par une compromission" }, + status: Status::Gap, + mechanism: "Le journal consigne genTime et la série du certificat TSU, pas celle du jeton : les jetons émis ne sont pas identifiables un par un.", + test: "", + target: "Constat J-3 (PR #52) : consigner la série du jeton, l'empreinte soumise et l'état de l'horloge.", + }, + Entry { + requirement: Requirement { standard: "ETSI EN 319 422", version: "V1.1.1 (2016-03)", clause: "§6", ids: &[], title: "Profil du certificat de l'unité d'horodatage" }, status: Status::Covered, - mechanism: "TSTInfo complet (politique, imprint, série, genTime UTC, précision), assemblé en CMS SignedData signé par le token ; le jeton est relu avant d'être consigné : oe_rfc3161_asn1, oe_tsa_core::Authority::timestamp.", - test: "crates/oe-tsa-core/tests/end_to_end.rs (produces_tokens_accepted_by_openssl_for_every_granted_case_in_the_corpus)", + mechanism: "oe_conformance::check_tsu_certificate (id-kp-timeStamping seul et critique, CA:FALSE, keyUsage restreint, durée de vie plafonnée) est appliquée à l'émission (Profile::check) ET re-contrôlée au démarrage de tsa-server (oe_tsa_core::Authority::new) — un certificat chargé depuis le disque peut venir d'ailleurs.", + test: "crates/oe-conformance/tests/tsu_certificate.rs (accepts_a_certificate_issued_with_the_tsa_signer_profile, rejects_a_certificate_issued_with_the_ocsp_responder_profile)", target: "", }, Entry { - requirement: Requirement { standard: "ETSI EN 319 422", clause: "§7", title: "Protocole d'horodatage RFC 3161 sur HTTP" }, + requirement: Requirement { standard: "ETSI EN 319 422", version: "V1.1.1 (2016-03)", clause: "§5.2", ids: &[], title: "Profil du jeton d'horodatage" }, + status: Status::Gap, + mechanism: "TSTInfo complet (politique, imprint, série, genTime UTC, précision), assemblé en CMS SignedData signé par le token : oe_rfc3161_asn1, oe_tsa_core::Authority::timestamp.", + test: "crates/oe-tsa-core/tests/end_to_end.rs (produces_tokens_accepted_by_openssl_for_every_granted_case_in_the_corpus)", + target: "Constat T-1 (PR #53) : genTime avec fraction de seconde (§5.2.2), cohérent avec la précision annoncée.", + }, + Entry { + requirement: Requirement { standard: "ETSI EN 319 422", version: "V1.1.1 (2016-03)", clause: "§7", ids: &[], title: "Protocole d'horodatage RFC 3161 sur HTTP" }, status: Status::Covered, mechanism: "Endpoint /tsa acceptant application/timestamp-query, refus protocolaires rendus en TimeStampResp valides : oe_httpapi, bin/tsa-server.", test: "crates/oe-httpapi/tests/end_to_end.rs (serves_a_verifiable_token_over_http, vérification croisée openssl ts -verify)", target: "", }, Entry { - requirement: Requirement { standard: "ETSI TS 119 312", clause: "§6.2", title: "Longueur de clé suffisante pour la durée de vie visée" }, + requirement: Requirement { standard: "ETSI TS 119 312", version: "V2.1.1 (2026-06)", clause: "§8.4", ids: &[], title: "Longueur de clé suffisante pour la durée de vie visée" }, status: Status::Covered, mechanism: "oe-config et bin/ca-server/src/config.rs imposent OPENEIDAS_KEY_BITS >= 3072 à la configuration (clé des autorités elles-mêmes) ; oe_raflow::parse_and_verify_csr applique la même exigence à la clé publique portée par une CSR soumise à l'enrôlement.", test: "crates/oe-config/src/lib.rs (load_fails_on_undersized_key_bits), crates/oe-raflow/tests/flow.rs (submit_rejects_a_csr_with_an_undersized_key)", target: "", }, Entry { - requirement: Requirement { standard: "ETSI TS 119 312", clause: "§6.1", title: "Algorithme de signature et fonction de hachage admis" }, + requirement: Requirement { standard: "ETSI TS 119 312", version: "V2.1.1 (2026-06)", clause: "§7.3", ids: &[], title: "Algorithme de signature et fonction de hachage admis" }, status: Status::Covered, mechanism: "oe_conformance::check_signature_algorithm vérifie explicitement l'OID de signature d'un certificat contre la liste des suites admises (SHA-256/384/512 avec RSA), appelée via Profile::check à l'émission et à la re-vérification.", test: "crates/oe-conformance/src/lib.rs (check_signature_algorithm_accepts_sha256_with_rsa, check_signature_algorithm_rejects_sha1)", target: "", }, Entry { - requirement: Requirement { standard: "ETSI TS 119 312", clause: "§5.1", title: "Fonction de hachage admise pour l'empreinte soumise" }, + requirement: Requirement { standard: "ETSI TS 119 312", version: "V2.1.1 (2026-06)", clause: "§5.1", ids: &[], title: "Fonction de hachage admise pour l'empreinte soumise" }, status: Status::Covered, mechanism: "oe_hsm::DigestAlg restreint la signature à SHA-256/384/512 ; une empreinte SHA-1 est refusée avec le failureInfo RFC 3161 badAlg : oe_tsa_core::Authority::timestamp.", test: "crates/oe-tsa-core/src/lib.rs (test_timestamp_rejects_sha1)", target: "", }, Entry { - requirement: Requirement { standard: "RFC 5280", clause: "§5.1", title: "Liste de révocation signée, numérotée et datée" }, + requirement: Requirement { standard: "RFC 6960", version: "", clause: "§2.1", ids: &[], title: "Service d'état de révocation interrogeable en ligne" }, status: Status::Covered, - mechanism: "CRL régénérée avec cRLNumber, thisUpdate/nextUpdate et signature, republiée même vide : oe_ca_core::Issuer::publish_crl. La signature et le motif de révocation sont revérifiés par openssl.", - test: "crates/oe-ca-core/tests/issuance.rs (revoke_then_publish_crl_lists_the_certificate, openssl_accepts_the_chain_and_honors_revocation)", + mechanism: "Répondeur OCSP RFC 6960 s'appuyant sur la CRL publiée par la CA : oe_ocsp_core::Responder.", + test: "crates/oe-ocsp-core/tests/against_real_crl.rs (reports_good_status_for_a_non_revoked_certificate, reports_revoked_status_for_a_revoked_certificate)", target: "", }, Entry { - requirement: Requirement { standard: "RFC 6960", clause: "§4.2.2.2", title: "Profil du certificat de signature du répondeur OCSP" }, + requirement: Requirement { standard: "RFC 6960", version: "", clause: "§4.2.2.2", ids: &[], title: "Profil du certificat de signature du répondeur OCSP" }, status: Status::Covered, mechanism: "Profil ocsp_responder (id-pkix-ocsp-nocheck, pas de CDP/AIA, durée de vie courte) appliqué à l'émission, re-contrôlé après signature par oe_conformance::check_ocsp_responder_certificate (Profile::check).", test: "crates/oe-ca-core/tests/issuance.rs (revoke_then_publish_crl_lists_the_certificate, qui émet avec ce profil), crates/oe-conformance/tests/tsu_certificate.rs (rejects_a_certificate_issued_with_the_ocsp_responder_profile, qui prouve que check_ocsp_responder_certificate distingue bien ce profil de tsa_signer)", target: "", }, Entry { - requirement: Requirement { standard: "ETSI EN 319 403-1", clause: "§7", title: "Évaluation par un organisme d'évaluation de la conformité accrédité" }, + requirement: Requirement { standard: "RFC 5280", version: "", clause: "§4.2.1.1-4.2.1.2", ids: &[], title: "Identifiants de clé de sujet et d'autorité présents" }, + status: Status::Covered, + mechanism: "subjectKeyIdentifier (SHA-1 de la clé, méthode 1) et authorityKeyIdentifier (pointant vers le SKI de l'émetteur) posés sans condition à l'émission et dans la cérémonie : oe_ca_core::extensions, oe_ca_core::signing::subject_key_id.", + test: "crates/oe-ca-core/tests/issuance.rs (issue_produces_a_certificate_signed_by_the_issuing_key, assert_ski_and_aki_present_and_linked)", + target: "", + }, + Entry { + requirement: Requirement { standard: "RFC 5280", version: "", clause: "§5.1", ids: &[], title: "Liste de révocation signée, numérotée et datée" }, + status: Status::Covered, + mechanism: "CRL régénérée avec cRLNumber, thisUpdate/nextUpdate et signature, republiée même vide : oe_ca_core::Issuer::publish_crl. La signature et le motif de révocation sont revérifiés par openssl.", + test: "crates/oe-ca-core/tests/issuance.rs (revoke_then_publish_crl_lists_the_certificate, openssl_accepts_the_chain_and_honors_revocation)", + target: "", + }, + Entry { + requirement: Requirement { standard: "ETSI EN 319 403-1", version: "V2.3.1 (2020-06)", clause: "§7", ids: &[], title: "Évaluation par un organisme d'évaluation de la conformité accrédité" }, status: Status::OutOfScope, mechanism: "Le dépôt est intégralement public ; cette matrice fournit le point d'entrée d'un audit, indépendamment du langage d'implémentation.", test: "", target: "Audit par un organisme accrédité (LSTI, Apave), puis inscription à la liste de confiance nationale.", }, - Entry { - requirement: Requirement { standard: "ETSI EN 319 401", clause: "§6.1", title: "Politique de service et déclaration des pratiques publiées" }, - status: Status::Gap, - mechanism: "docs/CPS.md porte un brouillon structuré, déjà indépendant du langage d'implémentation du service.", - test: "", - target: "Adoption formelle de docs/CPS.md par l'association (organisationnel, non affecté par le portage Rust).", - }, ]) } @@ -905,7 +1030,9 @@ mod tests { let bad = Matrix(vec![Entry { requirement: Requirement { standard: "X", + version: "", clause: "1", + ids: &[], title: "t", }, status: Status::Covered, @@ -915,4 +1042,66 @@ mod tests { }]); assert!(bad.validate().is_err()); } + + fn covered(requirement: Requirement) -> Entry { + Entry { + requirement, + status: Status::Covered, + mechanism: "m", + test: "t", + target: "", + } + } + + const OK: Requirement = Requirement { + standard: "ETSI EN 319 401", + version: "V3.2.1 (2026-01)", + clause: "§7.10", + ids: &["REQ-7.10-08"], + title: "t", + }; + + /// Constat D-3 : une ligne sans version, sans identifiant (pour une norme + /// qui en numérote), avec un identifiant d'une autre clause, ou une norme + /// citée en deux versions, rend la matrice incohérente. + #[test] + fn detects_missing_versions_ids_and_mismatched_clauses() { + assert!(Matrix(vec![covered(OK)]).validate().is_ok()); + + let no_version = Requirement { version: "", ..OK }; + let err = Matrix(vec![covered(no_version)]).validate().unwrap_err(); + assert!(err.contains("version"), "{err}"); + + let no_ids = Requirement { ids: &[], ..OK }; + let err = Matrix(vec![covered(no_ids)]).validate().unwrap_err(); + assert!(err.contains("aucun identifiant"), "{err}"); + + // La faute d'origine : §7.9 cité pour une exigence du §7.10. + let wrong_clause = Requirement { + clause: "§7.9", + ..OK + }; + let err = Matrix(vec![covered(wrong_clause)]).validate().unwrap_err(); + assert!(err.contains("ne relève pas de la clause"), "{err}"); + + let other_version = Requirement { + version: "V3.1.1 (2024-06)", + ids: &["REQ-7.10-02"], + ..OK + }; + let err = Matrix(vec![covered(OK), covered(other_version)]) + .validate() + .unwrap_err(); + assert!(err.contains("deux versions"), "{err}"); + + // Une RFC ne porte ni version ETSI ni identifiant. + let rfc = Requirement { + standard: "RFC 5280", + version: "", + clause: "§5.1", + ids: &[], + title: "t", + }; + assert!(Matrix(vec![covered(rfc)]).validate().is_ok()); + } } diff --git a/crates/oe-raflow/src/lib.rs b/crates/oe-raflow/src/lib.rs index 56743aa..f3063bf 100644 --- a/crates/oe-raflow/src/lib.rs +++ b/crates/oe-raflow/src/lib.rs @@ -18,7 +18,7 @@ //! automatique, pas d'auto-approbation, pas de chemin dérobé ; //! - [`Decider::approve`]/[`Decider::reject`] exigent l'identité de //! l'opérateur, consignée en base et au journal d'audit (ETSI EN -//! 319 411-1 §6.2.1). +//! 319 411-1 §6.3.2, §6.4.5). //! //! **Écart assumé** : `Decider` ne détient jamais la clé de l'autorité — //! approuver, c'est décider, pas signer (à l'identique du Go : la commande @@ -110,7 +110,7 @@ fn verify_hmac(csr_der: &[u8], secret: &str, signature_hex: &str) -> Result<(), .map_err(|_| RaflowError::Unauthenticated) } -/// ETSI TS 119 312 §6.2 : longueur de clé RSA minimale, la même exigence +/// ETSI TS 119 312 §8.4 : longueur de clé RSA minimale, la même exigence /// qu'`OPENEIDAS_CA_KEY_BITS`/`OPENEIDAS_KEY_BITS` imposent déjà à la /// configuration des autorités elles-mêmes — appliquée ici à la clé /// publique portée par la demande d'un tiers, que la configuration ne @@ -213,7 +213,7 @@ impl Decider { /// Fait passer une demande de PENDING à APPROVED. C'est la SEULE /// transition qui y mène, et elle exige l'identité de l'opérateur : - /// c'est ce qui rend la décision imputable (ETSI EN 319 411-1 §6.2.1). + /// c'est ce qui rend la décision imputable (ETSI EN 319 411-1 §6.4.5, REG-6.4.5-04). pub async fn approve( &self, transaction_id: &str, diff --git a/crates/oe-raflow/tests/flow.rs b/crates/oe-raflow/tests/flow.rs index 18aa77d..6699b36 100644 --- a/crates/oe-raflow/tests/flow.rs +++ b/crates/oe-raflow/tests/flow.rs @@ -127,7 +127,7 @@ async fn submit_without_valid_hmac_is_unauthenticated() { assert!(matches!(err, Err(RaflowError::Unauthenticated))); } -/// ETSI TS 119 312 §6.2 : une CSR authentifiée et correctement signée, mais +/// ETSI TS 119 312 §8.4 : une CSR authentifiée et correctement signée, mais /// dont la clé publique est trop courte, doit tout de même être refusée — /// l'authentification HMAC prouve l'identité du demandeur, pas que sa clé /// est acceptable. @@ -141,7 +141,7 @@ async fn submit_rejects_a_csr_with_an_undersized_key() { .await; assert!( err.is_err(), - "une clé RSA de 2048 bits doit être refusée (< 3072 bits, ETSI TS 119 312 §6.2)" + "une clé RSA de 2048 bits doit être refusée (< 3072 bits, ETSI TS 119 312 §8.4)" ); } diff --git a/crates/oe-timesource/Cargo.toml b/crates/oe-timesource/Cargo.toml index d0fbea2..b6c252d 100644 --- a/crates/oe-timesource/Cargo.toml +++ b/crates/oe-timesource/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true rust-version.workspace = true license.workspace = true repository.workspace = true -description = "Surveillance NTP multi-sources conforme ETSI EN 319 421 §7.6 (portage de internal/timesource)." +description = "Surveillance NTP multi-sources conforme ETSI EN 319 421 §7.7.2 (portage de internal/timesource)." [lints] workspace = true diff --git a/crates/oe-timesource/src/lib.rs b/crates/oe-timesource/src/lib.rs index 406a943..3e82203 100644 --- a/crates/oe-timesource/src/lib.rs +++ b/crates/oe-timesource/src/lib.rs @@ -1,5 +1,5 @@ //! Portage de `internal/timesource` (surveillance NTP multi-sources, ETSI EN -//! 319 421 §7.6) — jalon J4 du plan de migration +//! 319 421 §7.7.2) — jalon J4 du plan de migration //! (`/home/philippe/.claude/plans/witty-hopping-nest.md`). //! //! ETSI EN 319 421 exige que l'heure d'un jeton soit traçable jusqu'à UTC et diff --git a/crates/oe-timesource/src/ntp.rs b/crates/oe-timesource/src/ntp.rs index db0f9d0..989b244 100644 --- a/crates/oe-timesource/src/ntp.rs +++ b/crates/oe-timesource/src/ntp.rs @@ -1,7 +1,7 @@ //! Client SNTP minimal (RFC 4330 / RFC 5905, mode client, sans authentification). //! //! Le plan de migration recommande d'écrire ce client plutôt que d'aligner une -//! dépendance tierce sur les exigences ETSI EN 319 421 §7.6 : le protocole +//! dépendance tierce sur les exigences ETSI EN 319 421 §7.7.2 : le protocole //! côté client est simple (une requête, une réponse, 48 octets), et l'écrire //! ici réduit la surface de confiance et le rend directement auditable. diff --git a/docs/CA.md b/docs/CA.md index 817006a..6e16f63 100644 --- a/docs/CA.md +++ b/docs/CA.md @@ -84,7 +84,7 @@ cérémonie destinée à la qualification exige, en plus : conservé en coffre, ressorti uniquement pour renouveler la CA émettrice. Ces points sont portés comme écarts dans la matrice -(ETSI EN 319 401 §7.4, EN 319 411-1 §6.5.1). +(ETSI EN 319 401 §7.5, EN 319 411-1 §6.5.1). ## 3. Profils émis @@ -288,7 +288,7 @@ Deux propriétés structurent la machine à états d'éligibilité, ni auto-approbation, ni contournement — c'est exactement ce qu'OpenXPKI faisait silencieusement, et que ce moteur supprime ; - **toute décision exige une identité d'opérateur**, consignée en base et au - journal (ETSI EN 319 411-1 §6.2.1). La contrainte est portée par le schéma + journal (ETSI EN 319 411-1 §6.3.2, §6.4.5). La contrainte est portée par le schéma PostgreSQL autant que par le code : une décision anonyme est refusée par la base elle-même. diff --git a/docs/CONFORMITE-ETSI.md b/docs/CONFORMITE-ETSI.md index f28e5e9..86382a1 100644 --- a/docs/CONFORMITE-ETSI.md +++ b/docs/CONFORMITE-ETSI.md @@ -5,7 +5,7 @@ correction se fait dans le code, pour que la matrice publiée reste celle que le système applique réellement. --> -**28 exigences** — 22 couvertes, 4 écarts documentés, 2 hors périmètre logiciel. +**36 exigences** — 17 couvertes, 17 écarts documentés, 2 hors périmètre logiciel. Trois statuts seulement, pour qu'aucune zone grise ne puisse s'y loger : @@ -13,63 +13,66 @@ Trois statuts seulement, pour qu'aucune zone grise ne puisse s'y loger : - **écart documenté** — l'exigence n'est pas satisfaite en l'état ; la mesure compensatoire en place et la cible sont indiquées ; - **hors périmètre logiciel** — exigence organisationnelle, qu'aucun code ne peut établir seul. -## ETSI EN 319 401 +## ETSI EN 319 401 V3.2.1 (2026-01) | Clause | Exigence | Statut | Mécanisme | Vérification / cible | |---|---|---|---|---| -| §7.4 | Gestion des clés du prestataire dans un module cryptographique | couvert | Toutes les clés vivent dans un token PKCS#11 et n'en sortent jamais : oe-hsm::Pkcs11Token, validé contre un vrai token SoftHSM2 (crates/oe-hsm/tests/pkcs11_integration.rs). | crates/oe-hsm/tests/pkcs11_integration.rs | -| §7.10 | Journalisation des événements et durée de conservation | écart documenté | Journal JSON Lines chaîné par SHA-256 (oe-audit), écrit avant chaque émission dans les deux services ; durée de conservation contrôlée à la configuration par oe_conformance::check_audit_retention, mais côté CA seulement (bin/ca-server::Config::load). | **Cible :** Contrôler aussi la durée de conservation au démarrage de tsa-server (docs/CPS.md B.3). | -| §7.9 | Intégrité démontrable des enregistrements d'audit | couvert | Chaînage par hachage vérifié intégralement à l'ouverture ; verrou de fichier partagé entre plusieurs écrivains d'un même processus : oe-audit::Log. | crates/oe-audit/src/lib.rs (deux_ecrivains_partagent_la_meme_chaine, verify_detects_modified_record, verify_detects_truncated_and_rewritten_tail) | -| §7.11 | Continuité d'activité et reprise après sinistre | écart documenté | Contreseing du journal par une TSA tierce (oe-crosstsa) et réplication WebDAV hors site (oe-replicate) écrits et testés contre un vrai serveur, mais appelés par aucun binaire : l'intégrité et la survie du journal reposent sur le contrôle d'accès et la sauvegarde de son volume. | **Cible :** Constat J-1 : câbler dans tsa-server serve puis ca-server serve le scellement périodique (log.sealed), le contreseing tiers (log.cross_sealed) et la copie hors site (PR #49, #50 pour le stockage S3), testés de bout en bout sur le binaire ; dégrader /healthz sur échec prolongé. | -| §7.12 | Plan de cessation d'activité | hors périmètre logiciel | Procédure organisationnelle décrite dans docs/CA.md, indépendante du langage d'implémentation. | **Cible :** Engagement juridique de l'association, dépôt auprès de l'organe de contrôle, séquestre des journaux. | -| §6.1 | Politique de service et déclaration des pratiques publiées | écart documenté | docs/CPS.md porte un brouillon structuré, déjà indépendant du langage d'implémentation du service. | **Cible :** Adoption formelle de docs/CPS.md par l'association (organisationnel, non affecté par le portage Rust). | +| §7.5 — REQ-7.5-01 | Gestion des clés du prestataire dans un module cryptographique | couvert | Toutes les clés vivent dans un token PKCS#11 et n'en sortent jamais : oe-hsm::Pkcs11Token, validé contre un vrai token SoftHSM2 (crates/oe-hsm/tests/pkcs11_integration.rs). | crates/oe-hsm/tests/pkcs11_integration.rs | +| §7.10 — REQ-7.10-01, REQ-7.10-07 | Journalisation des événements et durée de conservation | écart documenté | Journal JSON Lines chaîné par SHA-256 (oe-audit), écrit avant chaque émission dans les deux services ; durée de conservation contrôlée à la configuration par oe_conformance::check_audit_retention, mais côté CA seulement (bin/ca-server::Config::load). | **Cible :** Constat J-3 (PR #52) : consigner la série du jeton et l'empreinte soumise ; contrôler aussi la durée de conservation au démarrage de tsa-server (docs/CPS.md B.3). | +| §7.10 — REQ-7.10-02, REQ-7.10-08 | Intégrité démontrable des enregistrements d'audit | écart documenté | Chaînage par hachage vérifié intégralement à l'ouverture ; verrou de fichier partagé entre plusieurs écrivains : oe-audit::Log. Le chaînage n'a pas de clé : une réécriture complète du fichier avec recalcul de la chaîne n'est pas détectée tant que la tête n'est pas ancrée hors du système. | **Cible :** Constat J-1 : sceller périodiquement la tête de chaîne (TSU et TSA tierce) et en déposer une copie hors site, depuis les binaires en service. | +| §7.11.1, §7.11.2 — REQ-7.11.1-01, REQ-7.11.2-01 | Copies de sauvegarde et plan de sauvegarde | écart documenté | Contreseing du journal par une TSA tierce (oe-crosstsa) et réplication WebDAV hors site (oe-replicate) écrits et testés contre un vrai serveur, mais appelés par aucun binaire : l'intégrité et la survie du journal reposent sur le contrôle d'accès et la sauvegarde de son volume. | **Cible :** Constat J-1 : câbler dans tsa-server serve puis ca-server serve le scellement périodique (log.sealed), le contreseing tiers (log.cross_sealed) et la copie hors site (PR #49, #50 pour le stockage S3), testés de bout en bout sur le binaire ; dégrader /healthz sur échec prolongé. | +| §7.12 — REQ-7.12-02, REQ-7.12-10 | Plan de cessation d'activité | hors périmètre logiciel | Procédure organisationnelle décrite dans docs/CA.md, indépendante du langage d'implémentation. | **Cible :** Engagement juridique de l'association, dépôt auprès de l'organe de contrôle, séquestre des journaux. | +| §6.1 — REQ-6.1-01, REQ-6.1-03, REQ-6.1-05 | Politique de service et déclaration des pratiques publiées | écart documenté | docs/CPS.md porte un brouillon structuré, déjà indépendant du langage d'implémentation du service. | **Cible :** Adoption formelle de docs/CPS.md par l'association (organisationnel, non affecté par le portage Rust). | -## ETSI EN 319 403-1 +## ETSI EN 319 403-1 V2.3.1 (2020-06) | Clause | Exigence | Statut | Mécanisme | Vérification / cible | |---|---|---|---|---| | §7 | Évaluation par un organisme d'évaluation de la conformité accrédité | hors périmètre logiciel | Le dépôt est intégralement public ; cette matrice fournit le point d'entrée d'un audit, indépendamment du langage d'implémentation. | **Cible :** Audit par un organisme accrédité (LSTI, Apave), puis inscription à la liste de confiance nationale. | -## ETSI EN 319 411-1 +## ETSI EN 319 411-1 V1.5.1 (2025-04) | Clause | Exigence | Statut | Mécanisme | Vérification / cible | |---|---|---|---|---| -| §6.6.1 | Profil du certificat d'autorité de certification | couvert | Cérémonie produisant une racine et une CA émettrice au profil contrôlé (CA:TRUE critique, keyCertSign+cRLSign, SKI/AKI) : oe_ca_core::ceremony::run_ceremony, chaîne revérifiée par openssl. | crates/oe-ca-core/tests/issuance.rs (ceremony_is_idempotent, ceremony_rejects_mismatched_signer_on_replay, openssl_accepts_the_chain_and_honors_revocation) | -| §6.2.1 | Enregistrement et responsabilité de la décision d'émission | couvert | Aucune transition vers Approved n'existe sans identité d'opérateur : oe_raflow::Decider::approve/reject. L'identité est consignée en base et au journal d'audit. | crates/oe-raflow/tests/flow.rs (decide_without_operator_identity_is_refused, approve_then_resubmit_issues_a_certificate_signed_by_the_issuing_key) | -| §6.3.1 | Authentification de la demande de certificat | couvert | HMAC-SHA256 sur la CSR DER, vérifié en temps constant, et vérification de l'auto-signature de la CSR (preuve de possession) : oe_raflow::Flow::submit. | crates/oe-raflow/tests/flow.rs (submit_without_valid_hmac_is_unauthenticated, submit_opens_a_pending_request_idempotently) | -| §6.3.2 | Durée de vie du certificat plafonnée | couvert | oe_conformance::check_certificate_lifetime relit la validité du certificat réellement signé et la compare à un plafond indépendant du profil (MAX_END_ENTITY_LIFETIME/MAX_OCSP_LIFETIME) ; appelé via le champ Profile::check de oe_ca_core::Issuer::issue, comme profile.Check (Go). | crates/oe-conformance/src/lib.rs (check_certificate_lifetime_accepts_within_the_ceiling, check_certificate_lifetime_rejects_beyond_the_ceiling), crates/oe-conformance/tests/tsu_certificate.rs | -| §6.3.9 | Motif de révocation consigné | couvert | Motif RFC 5280 obligatoire à la révocation (Issuer::revoke), persisté et repris dans chaque entrée de CRL avec son extension cRLReason. | crates/oe-ca-core/tests/issuance.rs (revoke_is_idempotent_and_keeps_first_reason, revoke_then_publish_crl_lists_the_certificate) | -| §6.3.10 | Publication régulière de l'état de révocation | couvert | oe_ca_core::Issuer::publish_crl produit une CRL signée, republiable même vide ; bin/ca-server::http::Server republie à intervalle régulier et dégrade /healthz (503) dès que la CRL servie est périmée, plutôt que de se déclarer sain sans pouvoir dire ce qui est révoqué. | crates/oe-ca-core/tests/issuance.rs (revoke_then_publish_crl_lists_the_certificate), bin/ca-server/tests/crl_publication.rs (crl_is_republished_periodically, healthz_degrades_when_the_published_crl_is_stale) | -| §6.5.1 | Cérémonie de génération des clés d'autorité | écart documenté | Cérémonie scriptée et idempotente (`ca-server ceremony`), produisant un procès-verbal consigné au journal d'audit (empreintes de clés, opérateur, date) : oe_ca_core::ceremony. | **Cible :** Cérémonie en double contrôle, sous témoin indépendant, sur HSM certifié, avec procès-verbal contresigné — écart organisationnel, pas seulement logiciel. | - -## ETSI EN 319 412-1 - -| Clause | Exigence | Statut | Mécanisme | Vérification / cible | -|---|---|---|---|---| -| §4 | Structures communes du profil de certificat | couvert | Profils définis en structures Rust compilées, pas en configuration interprétée : oe_ca_core::profile. Contrôle de criticité (basicConstraints, keyUsage, EKU) posé à la main, vérifié par openssl. | crates/oe-ca-core/tests/issuance.rs (openssl_accepts_the_chain_and_honors_revocation) | -| §4.1 | Numéro de série positif et imprévisible | couvert | Numéro de série de 128 bits tiré sur rand::thread_rng et réservé de façon atomique (contrainte d'unicité en base) : oe_ca_core::Issuer::reserve_serial, oe_castore::Store::reserve_serial. | crates/oe-castore/src/lib.rs (reserve_serial_twice_conflicts), crates/oe-castore/tests/postgres.rs (reserve_serial_twice_conflicts) | - -## ETSI EN 319 421 +| §6.6.1 — GEN-6.6.1-01 | Profil du certificat d'autorité de certification | couvert | Cérémonie produisant une racine et une CA émettrice au profil contrôlé (CA:TRUE critique, keyCertSign+cRLSign, SKI/AKI) : oe_ca_core::ceremony::run_ceremony, chaîne revérifiée par openssl. | crates/oe-ca-core/tests/issuance.rs (ceremony_is_idempotent, ceremony_rejects_mismatched_signer_on_replay, openssl_accepts_the_chain_and_honors_revocation) | +| §6.6.1 — GEN-6.6.1-02 | Certificats émis selon le profil applicable | couvert | Profils définis en structures Rust compilées, pas en configuration interprétée : oe_ca_core::profile. Contrôle de criticité (basicConstraints, keyUsage, EKU) posé à la main, vérifié par openssl. | crates/oe-ca-core/tests/issuance.rs (openssl_accepts_the_chain_and_honors_revocation) | +| §6.3.2, §6.4.5 — REG-6.3.2-00B, REG-6.4.5-03, REG-6.4.5-04 | Enregistrement et responsabilité de la décision d'émission | écart documenté | Aucune transition vers Approved n'existe sans identité d'opérateur : oe_raflow::Decider::approve/reject. L'identité est consignée en base et au journal d'audit. | **Cible :** Constat R-2 (PR #57) : approbation automatique désactivée par défaut dans le chart Helm, refusée avec production: true. | +| §6.3.2 — REG-6.3.2-01 | Authentification de la demande de certificat | écart documenté | HMAC-SHA256 sur la CSR DER, vérifié en temps constant, et vérification de l'auto-signature de la CSR (preuve de possession) : oe_raflow::Flow::submit. | **Cible :** Constat R-3 : un secret, ou mieux une identité mTLS, par demandeur, liée aux profils qu'il peut demander ; ne révoquer au renouvellement que les certificats du même demandeur. | +| §6.3.3 — GEN-6.3.3-02A | Numéro de série aléatoire | couvert | Numéro de série de 128 bits tiré sur rand::thread_rng et réservé de façon atomique (contrainte d'unicité en base) : oe_ca_core::Issuer::reserve_serial, oe_castore::Store::reserve_serial. | crates/oe-castore/src/lib.rs (reserve_serial_twice_conflicts), crates/oe-castore/tests/postgres.rs (reserve_serial_twice_conflicts) | +| §6.4.5 — REV-6.4.5-09 | Demandes de révocation et motif consignés | couvert | Motif RFC 5280 obligatoire à la révocation (Issuer::revoke), persisté et repris dans chaque entrée de CRL avec son extension cRLReason. | crates/oe-ca-core/tests/issuance.rs (revoke_is_idempotent_and_keeps_first_reason, revoke_then_publish_crl_lists_the_certificate) | +| §6.3.9 — CSS-6.3.9-05, CSS-6.3.9-06 | Publication de la CRL au moins toutes les 24 heures, avec nextUpdate | couvert | oe_ca_core::Issuer::publish_crl produit une CRL signée, republiable même vide ; bin/ca-server::http::Server republie à intervalle régulier et dégrade /healthz (503) dès que la CRL servie est périmée, plutôt que de se déclarer sain sans pouvoir dire ce qui est révoqué. | crates/oe-ca-core/tests/issuance.rs (revoke_then_publish_crl_lists_the_certificate), bin/ca-server/tests/crl_publication.rs (crl_is_republished_periodically, healthz_degrades_when_the_published_crl_is_stale) | +| §6.3.9, §6.3.10 — CSS-6.3.9-12, CSS-6.3.9-13, CSS-6.3.10-01 | Révocation d'une autorité et ARL | écart documenté | Aucune : la CA émettrice ne porte ni CDP ni AIA vers la racine, et la racine ne publie aucune ARL. | **Cible :** Constat C-1 (PR #56) : CDP/AIA vers la racine, `ca-server authority revoke\|publish-arl`, ARL servie, émission refusée par une émettrice révoquée. | +| §6.6.3 — OVR-6.6.3-02 | Statut OCSP d'un certificat jamais émis | écart documenté | Le répondeur OCSP ne connaît que la CRL : un numéro de série absent de la CRL est déclaré good, y compris jamais émis. | **Cible :** Constat O-1 (PR #55) : publier les numéros émis avec la CRL et répondre unknown pour un numéro absent. | +| §6.5.1 — GEN-6.5.1-04, GEN-6.5.1-11, GEN-6.5.1-13 | Cérémonie de génération des clés d'autorité | écart documenté | Cérémonie scriptée et idempotente (`ca-server ceremony`), produisant un procès-verbal consigné au journal d'audit (empreintes de clés, opérateur, date) : oe_ca_core::ceremony. | **Cible :** Cérémonie en double contrôle, sous témoin indépendant, sur HSM certifié, avec procès-verbal contresigné — écart organisationnel, pas seulement logiciel. | +| §6.5.5 — GEN-6.5.5-04, CSS-6.5.5-06 | Authentification multifacteur des comptes capables de provoquer une émission ou une révocation | écart documenté | Actions d'opérateur signées par WebAuthn (clé attestée, rôle lu dans le registre, quorum pour la révocation) : oe-actions, derrière le lien interne mTLS de ca-server. Le CLI de secours (ra approve\|reject, revoke) reste ouvert sans second facteur. | **Cible :** Constats R-1/R-2 : relayer les actions signées depuis ra-console (docs/WEBUI.md §15, étape 3) ; tracer et revoir la voie de secours CLI (PR #58, #59) ; approbation automatique désactivée par défaut (PR #57). | + +## ETSI EN 319 421 V1.3.1 (2025-07) | Clause | Exigence | Statut | Mécanisme | Vérification / cible | |---|---|---|---|---| -| §7.6 | Traçabilité de l'heure jusqu'à UTC et suspension en cas de dérive | couvert | Surveillance NTP multi-sources avec quorum, seuil de dérive (MaxOffset) et péremption (MaxAge) ; la politique enforce fait refuser chaque demande avec timeNotAvailable : oe_timesource::Monitor. | crates/oe-timesource/src/lib.rs (now_refuses_untraceable_time_in_enforce_mode, now_allows_untraceable_time_in_monitor_mode, new_rejects_quorum_larger_than_source_count), crates/oe-tsa-core/src/lib.rs (test_timestamp_refuses_when_time_is_not_traceable) | -| §7.7.2 | Profil du certificat de l'unité d'horodatage | couvert | oe_conformance::check_tsu_certificate (id-kp-timeStamping seul et critique, CA:FALSE, keyUsage restreint, durée de vie plafonnée) est appliquée à l'émission (Profile::check) ET re-contrôlée au démarrage de tsa-server (oe_tsa_core::Authority::new) — un certificat chargé depuis le disque peut venir d'ailleurs. | crates/oe-conformance/tests/tsu_certificate.rs (accepts_a_certificate_issued_with_the_tsa_signer_profile, rejects_a_certificate_issued_with_the_ocsp_responder_profile) | -| §7.7.1 | Génération de la clé TSU dans le module cryptographique | couvert | La bi-clé est générée dans le token PKCS#11 (oe_hsm::Pkcs11Token::generate_rsa_key) et ne manipule qu'un SigningToken ; la clé privée n'est jamais extraite. | crates/oe-hsm/tests/pkcs11_integration.rs | +| §7.7.1, §7.7.2 — TIS-7.7.1-04, TIS-7.7.1-07, TIS-7.7.2-01, TIS-7.7.2-06 | Traçabilité de l'heure jusqu'à UTC et suspension en cas de dérive | couvert | Surveillance NTP multi-sources avec quorum, seuil de dérive (MaxOffset) et péremption (MaxAge) ; la politique enforce fait refuser chaque demande avec timeNotAvailable : oe_timesource::Monitor. | crates/oe-timesource/src/lib.rs (now_refuses_untraceable_time_in_enforce_mode, now_allows_untraceable_time_in_monitor_mode, new_rejects_quorum_larger_than_source_count), crates/oe-tsa-core/src/lib.rs (test_timestamp_refuses_when_time_is_not_traceable) | +| §7.7.1, §7.7.2 — TIS-7.7.1-05, TIS-7.7.1-06, TIS-7.7.2-03 | Exactitude de l'heure du jeton (1 seconde ou mieux) | écart documenté | genTime est tronqué à la seconde alors que la dérive tolérée atteint OPENEIDAS_TIME_MAX_OFFSET : l'écart réel peut dépasser l'exactitude annoncée. | **Cible :** Constat T-1 (PR #53) : genTime à la milliseconde, et refus de démarrer si accuracy < max_offset + résolution. | +| §7.7.2 — TIS-7.7.2-04, TIS-7.7.2-05 | Protection de l'horloge contre un changement non détecté | écart documenté | Plusieurs sources UTC(k) avec quorum et seuil de dérive (oe_timesource::Monitor), mais client NTP sans contrôle d'origine, de l'indicateur de seconde intercalaire ni NTS. | **Cible :** Constat T-2 : contrôler l'origine et le LI des réponses NTP, envisager NTS (RFC 8915). | +| §7.7.2 — TIS-7.7.2-07, TIS-7.7.2-08, TIS-7.7.2-09 | Secondes intercalaires | écart documenté | Aucune : la seconde intercalaire n'est ni détectée ni consignée. | **Cible :** Constat T-4 : détecter l'annonce (LI), consigner l'instant du changement au journal. | +| §7.6.2 — TIS-7.6.2-03 | Génération de la clé TSU dans le module cryptographique | écart documenté | La bi-clé est générée dans le token PKCS#11 (oe_hsm::Pkcs11Token::generate_rsa_key) et ne manipule qu'un SigningToken ; la clé privée n'est jamais extraite. | **Cible :** Constat H-1 : vérifier au chargement que la clé a été générée dans le module (CKA_LOCAL, CKA_NEVER_EXTRACTABLE). | +| §7.6.5 — TIS-7.6.5-01 | Durée de vie du certificat de l'unité d'horodatage plafonnée | couvert | oe_conformance::check_certificate_lifetime relit la validité du certificat réellement signé et la compare à un plafond indépendant du profil (MAX_END_ENTITY_LIFETIME/MAX_OCSP_LIFETIME) ; appelé via le champ Profile::check de oe_ca_core::Issuer::issue, comme profile.Check (Go). | crates/oe-conformance/src/lib.rs (check_certificate_lifetime_accepts_within_the_ceiling, check_certificate_lifetime_rejects_beyond_the_ceiling), crates/oe-conformance/tests/tsu_certificate.rs | +| §7.6.7, §7.7.1 — TIS-7.6.7-01, TIS-7.6.7-02, TIS-7.6.7-04, TIS-7.6.7-06, TIS-7.6.7-07, TIS-7.6.7-09, TIS-7.7.1-09 | Date d'expiration de la clé TSU, refus d'émettre au-delà | écart documenté | Validité du certificat TSU contrôlée au démarrage seulement ; aucune date d'expiration de clé définie. | **Cible :** Constat T-3 (PR #54) : privateKeyUsagePeriod posé à l'émission, contrôlé à chaque signature ; nouvelle clé à chaque renouvellement. | +| §7.13 — OVR-7.13-05 | Identification des jetons affectés par une compromission | écart documenté | Le journal consigne genTime et la série du certificat TSU, pas celle du jeton : les jetons émis ne sont pas identifiables un par un. | **Cible :** Constat J-3 (PR #52) : consigner la série du jeton, l'empreinte soumise et l'état de l'horloge. | -## ETSI EN 319 422 +## ETSI EN 319 422 V1.1.1 (2016-03) | Clause | Exigence | Statut | Mécanisme | Vérification / cible | |---|---|---|---|---| -| §5 | Profil du jeton d'horodatage | couvert | TSTInfo complet (politique, imprint, série, genTime UTC, précision), assemblé en CMS SignedData signé par le token ; le jeton est relu avant d'être consigné : oe_rfc3161_asn1, oe_tsa_core::Authority::timestamp. | crates/oe-tsa-core/tests/end_to_end.rs (produces_tokens_accepted_by_openssl_for_every_granted_case_in_the_corpus) | +| §6 | Profil du certificat de l'unité d'horodatage | couvert | oe_conformance::check_tsu_certificate (id-kp-timeStamping seul et critique, CA:FALSE, keyUsage restreint, durée de vie plafonnée) est appliquée à l'émission (Profile::check) ET re-contrôlée au démarrage de tsa-server (oe_tsa_core::Authority::new) — un certificat chargé depuis le disque peut venir d'ailleurs. | crates/oe-conformance/tests/tsu_certificate.rs (accepts_a_certificate_issued_with_the_tsa_signer_profile, rejects_a_certificate_issued_with_the_ocsp_responder_profile) | +| §5.2 | Profil du jeton d'horodatage | écart documenté | TSTInfo complet (politique, imprint, série, genTime UTC, précision), assemblé en CMS SignedData signé par le token : oe_rfc3161_asn1, oe_tsa_core::Authority::timestamp. | **Cible :** Constat T-1 (PR #53) : genTime avec fraction de seconde (§5.2.2), cohérent avec la précision annoncée. | | §7 | Protocole d'horodatage RFC 3161 sur HTTP | couvert | Endpoint /tsa acceptant application/timestamp-query, refus protocolaires rendus en TimeStampResp valides : oe_httpapi, bin/tsa-server. | crates/oe-httpapi/tests/end_to_end.rs (serves_a_verifiable_token_over_http, vérification croisée openssl ts -verify) | -## ETSI TS 119 312 +## ETSI TS 119 312 V2.1.1 (2026-06) | Clause | Exigence | Statut | Mécanisme | Vérification / cible | |---|---|---|---|---| -| §6.2 | Longueur de clé suffisante pour la durée de vie visée | couvert | oe-config et bin/ca-server/src/config.rs imposent OPENEIDAS_KEY_BITS >= 3072 à la configuration (clé des autorités elles-mêmes) ; oe_raflow::parse_and_verify_csr applique la même exigence à la clé publique portée par une CSR soumise à l'enrôlement. | crates/oe-config/src/lib.rs (load_fails_on_undersized_key_bits), crates/oe-raflow/tests/flow.rs (submit_rejects_a_csr_with_an_undersized_key) | -| §6.1 | Algorithme de signature et fonction de hachage admis | couvert | oe_conformance::check_signature_algorithm vérifie explicitement l'OID de signature d'un certificat contre la liste des suites admises (SHA-256/384/512 avec RSA), appelée via Profile::check à l'émission et à la re-vérification. | crates/oe-conformance/src/lib.rs (check_signature_algorithm_accepts_sha256_with_rsa, check_signature_algorithm_rejects_sha1) | +| §8.4 | Longueur de clé suffisante pour la durée de vie visée | couvert | oe-config et bin/ca-server/src/config.rs imposent OPENEIDAS_KEY_BITS >= 3072 à la configuration (clé des autorités elles-mêmes) ; oe_raflow::parse_and_verify_csr applique la même exigence à la clé publique portée par une CSR soumise à l'enrôlement. | crates/oe-config/src/lib.rs (load_fails_on_undersized_key_bits), crates/oe-raflow/tests/flow.rs (submit_rejects_a_csr_with_an_undersized_key) | +| §7.3 | Algorithme de signature et fonction de hachage admis | couvert | oe_conformance::check_signature_algorithm vérifie explicitement l'OID de signature d'un certificat contre la liste des suites admises (SHA-256/384/512 avec RSA), appelée via Profile::check à l'émission et à la re-vérification. | crates/oe-conformance/src/lib.rs (check_signature_algorithm_accepts_sha256_with_rsa, check_signature_algorithm_rejects_sha1) | | §5.1 | Fonction de hachage admise pour l'empreinte soumise | couvert | oe_hsm::DigestAlg restreint la signature à SHA-256/384/512 ; une empreinte SHA-1 est refusée avec le failureInfo RFC 3161 badAlg : oe_tsa_core::Authority::timestamp. | crates/oe-tsa-core/src/lib.rs (test_timestamp_rejects_sha1) | ## RFC 5280 diff --git a/docs/CPS.md b/docs/CPS.md index f57e020..a9f42c3 100644 --- a/docs/CPS.md +++ b/docs/CPS.md @@ -277,7 +277,7 @@ couvertes par une CP/CPS générique de CA. | Algorithmes d'empreinte acceptés | SHA-256, SHA-384, SHA-512 — SHA-1 refusé (`badAlg`) | | Ordonnancement (`ordering`) | Non garanti entre jetons | -### B.2 Traçabilité de l'heure (ETSI EN 319 421 §7.6) +### B.2 Traçabilité de l'heure (ETSI EN 319 421 §7.7.1, §7.7.2) | | | |---|---| diff --git a/docs/WEBUI.md b/docs/WEBUI.md index 8bf7fe6..12e5cab 100644 --- a/docs/WEBUI.md +++ b/docs/WEBUI.md @@ -1617,7 +1617,7 @@ Pourquoi la CA émettrice plutôt qu'une petite CA interne dédiée : docker-compose et en Helm. Ces clés sont logicielles, et ce n'est pas une contradiction avec « les clés -vivent dans un token PKCS#11 » (EN 319 401 §7.4 dans la matrice). Cette +vivent dans un token PKCS#11 » (EN 319 401 §7.5, REQ-7.5-01, dans la matrice). Cette exigence vise les clés qui produisent le service de confiance : signature de certificats, de jetons d'horodatage, de réponses OCSP. Les deux clés ci-dessus n'authentifient qu'un canal interne. Leur compromission ne permet diff --git a/tests/fixtures/rfc3161/README.md b/tests/fixtures/rfc3161/README.md index 3579f2b..255fc54 100644 --- a/tests/fixtures/rfc3161/README.md +++ b/tests/fixtures/rfc3161/README.md @@ -16,7 +16,7 @@ non-régression contre l'implémentation d'origine. certificat TSU de test, dérivés d'une graine fixe (`math/rand` seedé). Ce n'est **pas** un secret : ne jamais réutiliser ce matériel hors de ce corpus. La fenêtre de validité du certificat est relative à l'instant de - génération (elle ne peut pas être figée : ETSI EN 319 411-1 §6.3.2 plafonne + génération (elle ne peut pas être figée : ETSI EN 319 421 §7.6.5 plafonne la durée de vie d'un certificat TSU à 28080h). - Un répertoire par cas (`/`) : - `request.der` — `TimeStampReq` DER.