Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
<https://localhost:8443/webui/> (certificat auto-signé).

### Relire le journal d'audit

```bash
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions bin/ca-server/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,9 @@ async fn handle_conformance(State(server): State<Arc<Server>>) -> 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,
Expand Down
2 changes: 1 addition & 1 deletion crates/oe-ca-core/src/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ pub(crate) fn authority_key_identifier(parent_ski: &[u8]) -> Result<Extension, C
}

/// `extendedKeyUsage` est posé à la main plutôt que via un type générique :
/// ETSI EN 319 421 §7.7.2 exige de pouvoir le marquer critique, comme le
/// ETSI EN 319 422 §6.4 (RFC 3161 §2.3) exige de pouvoir le marquer critique, comme le
/// fait `internal/ca.go` (Go) en construisant l'extension lui-même plutôt
/// qu'en passant par `x509.Certificate.ExtKeyUsage`.
pub(crate) fn extended_key_usage(
Expand Down
2 changes: 1 addition & 1 deletion crates/oe-ca-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ impl Issuer {
}

async fn reserve_serial(&self, profile: &str) -> Result<SerialNumber, CaError> {
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];
Expand Down
4 changes: 2 additions & 2 deletions crates/oe-ca-core/src/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub struct Profile {

pub key_usages: der::flagset::FlagSet<KeyUsages>,
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,
Expand Down Expand Up @@ -74,7 +74,7 @@ pub fn eku_oids(p: &Profile) -> Vec<ObjectIdentifier> {
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 {
Expand Down
361 changes: 275 additions & 86 deletions crates/oe-conformance/src/lib.rs

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions crates/oe-raflow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions crates/oe-raflow/tests/flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)"
);
}

Expand Down
2 changes: 1 addition & 1 deletion crates/oe-timesource/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion crates/oe-timesource/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion crates/oe-timesource/src/ntp.rs
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
12 changes: 5 additions & 7 deletions crates/oe-tsa-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
3 changes: 3 additions & 0 deletions deploy/helm/open-eidas/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
6 changes: 3 additions & 3 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
96 changes: 39 additions & 57 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <tête-de-chaîne> -sha256 -no_nonce -out head.tsq
openssl ts -verify -in <jeton-décodé> -queryfile head.tsq \
-CAfile <CA-de-la-TSA-tierce> -untrusted <certificat-de-la-TSA-tierce>
```
— 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-<horodatage>-seq<n>.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

Expand All @@ -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
Expand Down
Loading
Loading