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/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 5052b7b..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" },
- 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.",
+ 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: "",
+ 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" },
- 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.",
+ 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: "",
+ 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/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..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.
@@ -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..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** — 24 couvertes, 2 é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 | 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.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.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.