Skip to content
Merged
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
12 changes: 6 additions & 6 deletions crates/oe-conformance/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -630,10 +630,10 @@ pub fn system_matrix() -> Matrix {
},
Entry {
requirement: Requirement { standard: "ETSI EN 319 401", clause: "§7.10", title: "Journalisation des événements et durée de conservation" },
status: Status::Covered,
mechanism: "Journal JSON Lines chaîné par SHA-256 (oe-audit) ; durée de conservation contrôlée à la configuration par oe_conformance::check_audit_retention, branché sur bin/ca-server::Config::load.",
status: Status::Gap,
mechanism: "Journal JSON Lines chaîné par SHA-256 (oe-audit), écrit avant chaque émission dans les deux services ; durée de conservation contrôlée à la configuration par oe_conformance::check_audit_retention, mais côté CA seulement (bin/ca-server::Config::load).",
test: "crates/oe-audit/src/lib.rs (deux_ecrivains_partagent_la_meme_chaine), crates/oe-conformance/src/lib.rs (check_audit_retention_accepts_the_minimum, check_audit_retention_rejects_unconfigured_and_short_durations)",
target: "",
target: "Contrôler aussi la durée de conservation au démarrage de tsa-server (docs/CPS.md B.3).",
},
Entry {
requirement: Requirement { standard: "ETSI EN 319 401", clause: "§7.9", title: "Intégrité démontrable des enregistrements d'audit" },
Expand All @@ -644,10 +644,10 @@ pub fn system_matrix() -> Matrix {
},
Entry {
requirement: Requirement { standard: "ETSI EN 319 401", clause: "§7.11", title: "Continuité d'activité et reprise après sinistre" },
status: Status::Covered,
mechanism: "Contreseing du journal par une TSA tierce (oe-crosstsa) et réplication WebDAV hors site (oe-replicate), validés contre un vrai serveur.",
status: Status::Gap,
mechanism: "Contreseing du journal par une TSA tierce (oe-crosstsa) et réplication WebDAV hors site (oe-replicate) écrits et testés contre un vrai serveur, mais appelés par aucun binaire : l'intégrité et la survie du journal reposent sur le contrôle d'accès et la sauvegarde de son volume.",
test: "crates/oe-crosstsa/tests/against_local_server.rs (seals_a_digest_against_a_real_rfc3161_server), crates/oe-replicate/tests/against_local_server.rs (replicates_content_via_webdav_put)",
target: "",
target: "Constat J-1 : câbler dans tsa-server serve puis ca-server serve le scellement périodique (log.sealed), le contreseing tiers (log.cross_sealed) et la copie hors site (PR #49, #50 pour le stockage S3), testés de bout en bout sur le binaire ; dégrader /healthz sur échec prolongé.",
},
Entry {
requirement: Requirement { standard: "ETSI EN 319 401", clause: "§7.12", title: "Plan de cessation d'activité" },
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 @@ -198,10 +198,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
10 changes: 5 additions & 5 deletions docs/CA.md
Original file line number Diff line number Diff line change
Expand Up @@ -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é

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