PD-39 — Plan d'Implémentation TSA RFC 3161 par Batch Cryptographique
Version : 3.0 Date : 2025-12-23 Modification : Architecture NTS via ntpd-rs (RFC 8915)
0. Prérequis Infrastructure — ntpd-rs (Horloge NTS)
0.1 Contexte
Les invariants 8 et 9 de la spécification exigent une horloge de référence authentifiée (NTS, RFC 8915). Il n'existe pas de client NTS viable en Node.js. La solution retenue est d'utiliser ntpd-rs (Pendulum), un démon NTS mature en Rust, pour discipliner l'horloge système.
0.2 Architecture ntpd-rs
┌─────────────────────────────────────────────────────────────────┐
│ Serveur Application │
├─────────────────────────────────────────────────────────────────┤
│ ┌──────────────────┐ ┌─────────────────────────────────┐ │
│ │ ReferenceClockService│────▶│ Horloge Système (disciplinée) │ │
│ └──────────────────┘ └─────────────────────────────────┘ │
│ │ ▲ │
│ ▼ │ │
│ ┌──────────────────┐ │ │
│ │ /var/run/ntpd-rs/│◀───────────────────┤ │
│ │ metrics.sock │ Synchronisation │
│ └──────────────────┘ │ │
├─────────────────────────────────────────────────────────────────┤
│ ntpd-rs daemon │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ • NTS-KE (TLS 1.3) + NTP avec cookies AEAD │ │
│ │ • Pool NTS: time.cloudflare.com, nts.netnod.se │ │
│ │ • Métriques: offset, jitter, stratum, leap indicator │ │
│ │ • Fichier statut: /var/run/ntpd-rs/observe.json │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
0.3 Configuration ntpd-rs requise
Fichier /etc/ntpd-rs/ntpd-rs.toml :
# Configuration ntpd-rs pour ProbatioVault (PD-39)
# NTS obligatoire, pas de fallback NTP non authentifié
[[source]]
mode = "nts"
address = "time.cloudflare.com:4460"
[[source]]
mode = "nts"
address = "nts.netnod.se:4460"
[[source]]
mode = "nts"
address = "nts.time.nl:4460"
[observability]
# Socket Unix pour métriques
metrics-exporter-listen = "/var/run/ntpd-rs/metrics.sock"
# Fichier JSON de statut (polling par ReferenceClockService)
observation-path = "/var/run/ntpd-rs/observe.json"
[synchronization]
# Seuil de panic si dérive > 1000ms
panic-threshold = { value = 1000 }
# Pas de step, uniquement slew pour éviter sauts
single-step-panic-threshold = { forward = "off", backward = "off" }
[source-defaults]
# Minimum 2 sources NTS valides pour synchronisation
min-agreeing-sources = 2
0.4 Validation de la synchronisation
Le ReferenceClockService valide la synchronisation via le fichier observe.json :
interface NtpdRsObservation {
version: number;
time: {
offset: number; // Offset en secondes (doit être < 0.005 = 5ms)
uncertainty: number; // Incertitude en secondes
};
system: {
stratum: number; // Doit être ≤ 3
leap: 'none' | 'insert' | 'delete' | 'unknown';
root_delay: number;
root_dispersion: number;
};
sources: Array<{
address: string;
poll_interval: number;
reach: number; // Bitmask de réussite (0xFF = 8/8)
state: 'active' | 'unreachable' | 'rejected';
}>;
}
Critères de validation : - offset < 5ms (0.005s) - stratum ≤ 3 - Au moins 2 sources avec state === 'active' - leap !== 'unknown'
0.5 Monitoring et Alertes
| Métrique | Seuil Warning | Seuil Critique | Action |
offset | > 1ms | > 5ms | REJET opérations TSA |
stratum | > 2 | > 3 | Alerte ops |
| Sources actives | < 3 | < 2 | REJET opérations TSA |
| Fichier observe.json | Âge > 10s | Âge > 30s | REJET opérations TSA |
0.6 Déploiement
# Installation ntpd-rs (Debian/Ubuntu)
curl -sSL https://github.com/pendulum-project/ntpd-rs/releases/latest/download/ntpd-rs_amd64.deb -o ntpd-rs.deb
sudo dpkg -i ntpd-rs.deb
# Désactiver NTP/chronyd existant
sudo systemctl disable --now systemd-timesyncd
sudo systemctl disable --now chronyd
# Configurer et démarrer ntpd-rs
sudo cp /path/to/ntpd-rs.toml /etc/ntpd-rs/ntpd-rs.toml
sudo systemctl enable --now ntpd-rs
# Vérifier statut
cat /var/run/ntpd-rs/observe.json | jq '.time.offset, .sources[].state'
1. Découpage en Composants
1.1 Architecture Modulaire
src/modules/tsa/
├── tsa.module.ts # Module NestJS principal
├── constants/
│ ├── tsa.constants.ts # Algorithmes autorisés
│ └── qtsa-registry.ts # Registre QTSA qualifiées (OID, certificats)
├── enums/
│ ├── batch-status.enum.ts # OPEN, SEALED, TIMESTAMPED, FAILED
│ ├── validation-result.enum.ts # Résultats de validation TST
│ └── revocation-status.enum.ts # GOOD, REVOKED, UNKNOWN, INDETERMINATE
├── entities/
│ ├── timestamp-batch.entity.ts # Entité batch horodatage
│ ├── batch-item.entity.ts # Élément individuel du batch
│ ├── timestamp-token.entity.ts # Jeton TST stocké
│ ├── batch-seal.entity.ts # Scellement cryptographique du batch
│ └── clock-attestation.entity.ts # Attestation d'horloge de référence
├── dto/
│ ├── create-batch.dto.ts # Création de batch
│ ├── add-item.dto.ts # Ajout d'élément au batch
│ ├── seal-batch.dto.ts # Scellement de batch
│ └── inclusion-proof.dto.ts # Preuve d'inclusion (format RFC 9162)
├── interfaces/
│ ├── qtsa-client.interface.ts # Interface client QTSA
│ ├── merkle-tree.interface.ts # Interface arbre Merkle normalisé
│ ├── reference-clock.interface.ts # Interface horloge de référence
│ └── seal-provider.interface.ts # Interface scellement cryptographique
├── services/
│ ├── batch.service.ts # Gestion du cycle de vie batch
│ ├── batch-seal.service.ts # Scellement cryptographique probatoire
│ ├── merkle.service.ts # Construction arbre Merkle (RFC 6962/9162)
│ ├── tsa-client.service.ts # Client RFC 3161
│ ├── tst-validator.service.ts # Validation des jetons
│ ├── inclusion-proof.service.ts # Génération/vérification preuves
│ ├── trusted-list.service.ts # Validation QTSA via Trusted List
│ ├── qtsa-qualification.service.ts # Vérification qualification QTSA
│ ├── reference-clock.service.ts # Horloge de référence NTS
│ ├── clock-attestation.service.ts # Génération attestations temporelles
│ └── revocation.service.ts # CRL/OCSP avec politique probatoire
├── processors/
│ └── batch-timestamp.processor.ts # Worker BullMQ pour horodatage
├── guards/
│ └── batch-sealed.guard.ts # Empêche modification post-scellement
└── utils/
├── hash.utils.ts # Fonctions de hachage normalisées
├── asn1.utils.ts # Parsing/encodage ASN.1 pour TST
└── canonical-json.utils.ts # Sérialisation JSON canonique (RFC 8785)
1.2 Description des Composants
| Composant | Responsabilité | Dépendances |
| BatchService | Gestion cycle de vie: création, ajout, demande de scellement | BatchSealService, MerkleService |
| BatchSealService | Scellement cryptographique probatoire avec signature interne | HSM/KMS, MerkleService |
| MerkleService | Construction arbre RFC 6962, calcul empreinte agrégée | HashUtils, CanonicalJson |
| TsaClientService | Émission requêtes RFC 3161, réception TST | HTTP, ASN1Utils |
| TstValidatorService | Validation complète du jeton | QtsaQualificationService, RevocationService |
| QtsaQualificationService | Vérification qualification eIDAS via Trusted List | TrustedListService |
| InclusionProofService | Génération et vérification preuves RFC 9162 | MerkleService |
| TrustedListService | Téléchargement et validation Trusted Lists ETSI | Signature ETSI |
| ReferenceClockService | Temps de référence NTS via ntpd-rs (horloge système disciplinée) | Fichier observe.json ntpd-rs |
| ClockAttestationService | Preuve d'usage de l'horloge de référence, attestation signée | ReferenceClockService, HSM |
| RevocationService | CRL/OCSP avec politique probatoire stricte | HTTP, Cache |
2. Flux Techniques
2.1 Flux Nominal — Constitution, Scellement et Horodatage d'un Batch
sequenceDiagram
participant Client
participant BatchService
participant BatchSealService
participant MerkleService
participant ClockAttestation
participant TsaClient
participant QTSA
participant TstValidator
participant QtsaQualification
participant DB
Client->>BatchService: createBatch()
BatchService->>DB: INSERT batch (status=OPEN)
BatchService-->>Client: batchId
loop Pour chaque donnée
Client->>BatchService: addItem(batchId, hash)
BatchService->>DB: Vérifier status=OPEN
BatchService->>DB: INSERT batch_item
end
Client->>BatchService: sealBatch(batchId)
BatchService->>DB: Vérifier status=OPEN, items > 0
BatchService->>MerkleService: buildTree(items)
MerkleService->>MerkleService: Tri lexicographique
MerkleService->>MerkleService: Encodage RFC 6962 MTH
MerkleService-->>BatchService: { rootHash, treeHead, treeData }
BatchService->>BatchSealService: seal(batchId, rootHash, treeData)
BatchSealService->>BatchSealService: Génération seal_signature (HSM/KMS)
BatchSealService->>ClockAttestation: attest(now())
ClockAttestation-->>BatchSealService: { nts_response, attestation_id }
BatchSealService->>DB: INSERT batch_seal (signature, attestation)
BatchSealService->>DB: UPDATE batch (status=SEALED)
BatchSealService-->>BatchService: { sealId, rootHash }
BatchService-->>Client: { sealId, rootHash, sealed_at }
Client->>BatchService: timestamp(batchId)
BatchService->>DB: Vérifier status=SEALED
BatchService->>QtsaQualification: verifyQualification(qtsaConfig)
QtsaQualification->>QtsaQualification: Vérifier Trusted List
QtsaQualification->>QtsaQualification: Vérifier certificat TSA
QtsaQualification->>QtsaQualification: Vérifier service type "QTS"
QtsaQualification-->>BatchService: { qualified: true, evidence }
BatchService->>TsaClient: requestTimestamp(rootHash)
TsaClient->>QTSA: HTTP POST (TSQ)
QTSA-->>TsaClient: TSR (TimeStampResponse)
TsaClient-->>BatchService: TST (TimeStampToken)
BatchService->>TstValidator: validate(TST, rootHash, sealTime)
TstValidator->>TstValidator: Vérifier RFC 3161
TstValidator->>TstValidator: Vérifier messageImprint == rootHash
TstValidator->>TstValidator: Vérifier signature
TstValidator->>TstValidator: Vérifier chaîne certificats
TstValidator->>TstValidator: Vérifier CRL/OCSP (politique probatoire)
TstValidator->>TstValidator: Vérifier Policy OID qualifiée
TstValidator->>ClockAttestation: validateTemporalCoherence(genTime)
ClockAttestation-->>TstValidator: { coherent, drift, attestation }
TstValidator-->>BatchService: ValidationResult
BatchService->>DB: UPDATE batch (status=TIMESTAMPED)
BatchService->>DB: INSERT timestamp_token
BatchService-->>Client: { tstId, genTime }
2.2 Flux de Scellement Cryptographique (Clôture Normative)
sequenceDiagram
participant BatchService
participant BatchSealService
participant HSM
participant ClockAttestation
participant ReferenceClockService
participant NtpdRs as ntpd-rs<br/>(observe.json)
participant DB
BatchService->>BatchSealService: seal(batchId, rootHash, treeData)
Note over BatchSealService: Construction payload canonique
BatchSealService->>BatchSealService: payload = canonicalJson({<br/> batch_id,<br/> root_hash,<br/> item_count,<br/> tree_algorithm: "RFC6962_MTH",<br/> sealed_at_nts<br/>})
BatchSealService->>ClockAttestation: requestAttestation()
ClockAttestation->>ReferenceClockService: getAuthenticatedTime()
ReferenceClockService->>NtpdRs: Lire observe.json
NtpdRs-->>ReferenceClockService: { offset, stratum, sources[] }
ReferenceClockService->>ReferenceClockService: Valider synchro NTS<br/>(offset < 5ms, stratum ≤ 3, sources ≥ 2)
alt Synchronisation valide
ReferenceClockService-->>ClockAttestation: { timestamp, sync_status, observation_snapshot }
else Synchronisation invalide
ReferenceClockService-->>ClockAttestation: ERREUR: REFERENCE_CLOCK_NOT_SYNCHRONIZED
ClockAttestation-->>BatchSealService: REJET
end
ClockAttestation->>HSM: sign(observation_snapshot)
HSM-->>ClockAttestation: attestation_signature
ClockAttestation->>DB: INSERT clock_attestation
ClockAttestation-->>BatchSealService: { attestation_id, timestamp, signed_observation }
BatchSealService->>BatchSealService: payload.sealed_at_nts = attestation
BatchSealService->>HSM: sign(payload, key_id)
HSM-->>BatchSealService: signature (ECDSA P-384 ou Ed25519)
BatchSealService->>DB: INSERT batch_seal {<br/> batch_id,<br/> payload_canonical,<br/> signature,<br/> attestation_id,<br/> seal_algorithm<br/>}
BatchSealService->>DB: UPDATE batch SET status='SEALED'
Note over DB: À partir de ce point:<br/>- Aucun INSERT batch_item autorisé<br/>- Aucun UPDATE sur payload/signature<br/>- Vérifiable par tiers
2.3 Flux de Vérification de Qualification QTSA
sequenceDiagram
participant QtsaQualification
participant TrustedListService
participant TLCache
participant HTTP
participant DB
QtsaQualification->>TrustedListService: getQualifiedServices()
alt Cache valide (< 24h)
TrustedListService->>TLCache: getTrustedList(country)
TLCache-->>TrustedListService: cachedList
else Cache expiré ou absent
TrustedListService->>HTTP: GET eu-lotl.xml (List of Trusted Lists)
HTTP-->>TrustedListService: LOTL XML
TrustedListService->>TrustedListService: Vérifier signature LOTL
loop Pour chaque pays
TrustedListService->>HTTP: GET national-tl.xml
HTTP-->>TrustedListService: National TL XML
TrustedListService->>TrustedListService: Vérifier signature TL
end
TrustedListService->>TLCache: store(lists)
end
TrustedListService-->>QtsaQualification: trustedServices[]
QtsaQualification->>QtsaQualification: Filtrer ServiceTypeIdentifier = "QCertESig"<br/>ou "QTimestamp"
QtsaQualification->>QtsaQualification: Vérifier ServiceStatus = "granted"
QtsaQualification->>QtsaQualification: Extraire certificat TSA
QtsaQualification->>QtsaQualification: Comparer avec certificat du TST
QtsaQualification->>DB: INSERT qualification_evidence {<br/> tsa_cert_hash,<br/> trusted_list_hash,<br/> service_name,<br/> policy_oid,<br/> verification_time<br/>}
QtsaQualification-->>BatchService: { qualified: true, evidence_id }
2.4 Flux de Génération de Preuve d'Inclusion (RFC 9162)
sequenceDiagram
participant Client
participant InclusionProofService
participant MerkleService
participant DB
Client->>InclusionProofService: getProof(batchId, itemHash)
InclusionProofService->>DB: SELECT batch, seal, items, tree_data
InclusionProofService->>InclusionProofService: Vérifier status IN (SEALED, TIMESTAMPED)
InclusionProofService->>MerkleService: computeInclusionProof(treeData, itemHash)
MerkleService->>MerkleService: Localiser feuille (index)
MerkleService->>MerkleService: Construire chemin d'audit
MerkleService-->>InclusionProofService: { leaf_index, audit_path[] }
InclusionProofService->>DB: SELECT timestamp_token WHERE batch_id
Note over InclusionProofService: Format RFC 9162 InclusionProofDataV2
InclusionProofService->>InclusionProofService: proof = {<br/> log_id: batch_id,<br/> tree_size: item_count,<br/> leaf_index,<br/> inclusion_path: audit_path,<br/> leaf_hash: itemHash,<br/> root_hash,<br/> timestamp_token: TST<br/>}
InclusionProofService-->>Client: InclusionProofDataV2
3. Mapping Invariants → Mécanismes
| # | Invariant | Mécanisme d'Implémentation | Preuve/Vérifiabilité |
| 1 | QTSA qualifiée eIDAS sur Trusted List | QtsaQualificationService vérifie: (1) signature de la LOTL EU, (2) signature de la TL nationale, (3) ServiceTypeIdentifier contient "QTimestamp", (4) ServiceStatus = "granted", (5) certificat TSA présent dans la TL. Stockage de qualification_evidence pour audit. | Evidence stockée avec hash TL, reproductible |
| 2 | Policy OID vérifiée | Extraction OID depuis TSTInfo.policy. Vérification contre registre qtsa-registry.ts extrait des TL (pas de whitelist manuelle). L'OID doit correspondre à une policy qualifiée eIDAS. | OID tracée dans validation_result |
| 3 | Conformité RFC 3161 | TstValidatorService.validateRfc3161Structure() vérifie structure ASN.1 complète via pkijs: PKIStatus=0, ContentType=SignedData, eContentType=TSTInfo, hashAlgorithm autorisé, nonce si fourni. | Validation ASN.1 déterministe |
| 4 | Empreinte agrégée unique, algorithme autorisé | MerkleService applique RFC 6962 Merkle Tree Hash (MTH). Algorithme SHA-256 (seul autorisé pour compatibilité CT). ALLOWED_HASH_ALGORITHMS = ['SHA-256']. | Algorithme normé, reproductible |
| 5 | Batch scellé avant horodatage | Transition OPEN → SEALED via BatchSealService.seal() qui génère signature cryptographique HSM + attestation NTS. BatchSealedGuard vérifie signature avant toute opération post-scellement. Aucune extension possible : FK sur batch_id avec check status=OPEN. | Scellement signé vérifiable |
| 6 | Structure d'agrégation déterministe et ordonnée | Format RFC 6962 MTH : (1) feuilles = SHA-256(0x00 \ | \ |
| 7 | Preuve d'inclusion normalisée et vérifiable | Format RFC 9162 InclusionProofDataV2: {log_id, tree_size, leaf_index, inclusion_path[], leaf_hash, root_hash}. Chemin d'audit = liste de hashes avec position (L/R). | Standard CT, outils tiers compatibles |
| 8 | Pas d'heure locale comme référence | ReferenceClockService lit l'horloge système disciplinée par ntpd-rs (NTS RFC 8915). Validation via observe.json: offset < 5ms, stratum ≤ 3, ≥ 2 sources NTS actives. REJET si synchronisation non valide. Pas de fallback NTP non authentifié. | Horloge OS synchronisée NTS, observation signée |
| 9 | Validations via horloge de référence | ClockAttestationService.validateTemporalCoherence() compare genTime TST avec attestation signée. L'attestation inclut le snapshot observe.json signé par HSM. Tolérance: 5 min. Chaque opération génère une attestation signée stockée prouvant l'état de synchronisation NTS au moment T. | Attestation signée HSM + snapshot ntpd-rs |
| 10 | Éléments acceptés immuables | Triple protection: (1) Scellement cryptographique (signature HSM), (2) Triggers PostgreSQL interdisant UPDATE/DELETE sur batch_seal, batch_item après scellement, (3) Row-level security + audit PD-17 sur tentatives. Option: stockage S3 Glacier avec Object Lock (WORM). | Signature + DB locks + audit |
4. Gestion des Erreurs
4.1 Codes d'Erreur Spécifiques
enum TsaErrorCode {
// Erreurs de batch
BATCH_NOT_FOUND = 'TSA_BATCH_NOT_FOUND',
BATCH_ALREADY_SEALED = 'TSA_BATCH_ALREADY_SEALED',
BATCH_NOT_SEALED = 'TSA_BATCH_NOT_SEALED',
BATCH_ALREADY_TIMESTAMPED = 'TSA_BATCH_ALREADY_TIMESTAMPED',
BATCH_EMPTY = 'TSA_BATCH_EMPTY',
BATCH_SEAL_INVALID = 'TSA_BATCH_SEAL_INVALID',
// Erreurs QTSA
QTSA_UNREACHABLE = 'TSA_QTSA_UNREACHABLE',
QTSA_NOT_QUALIFIED = 'TSA_QTSA_NOT_QUALIFIED',
QTSA_QUALIFICATION_EXPIRED = 'TSA_QTSA_QUALIFICATION_EXPIRED',
QTSA_RESPONSE_INVALID = 'TSA_QTSA_RESPONSE_INVALID',
// Erreurs de validation TST
TST_RFC3161_INVALID = 'TSA_TST_RFC3161_INVALID',
TST_SIGNATURE_INVALID = 'TSA_TST_SIGNATURE_INVALID',
TST_CHAIN_INVALID = 'TSA_TST_CHAIN_INVALID',
TST_REVOKED = 'TSA_TST_REVOKED',
TST_REVOCATION_INDETERMINATE = 'TSA_TST_REVOCATION_INDETERMINATE',
TST_POLICY_NOT_QUALIFIED = 'TSA_TST_POLICY_NOT_QUALIFIED',
TST_HASH_MISMATCH = 'TSA_TST_HASH_MISMATCH',
TST_TEMPORAL_DRIFT = 'TSA_TST_TEMPORAL_DRIFT',
// Erreurs de preuve
PROOF_ITEM_NOT_IN_BATCH = 'TSA_PROOF_ITEM_NOT_IN_BATCH',
PROOF_VERIFICATION_FAILED = 'TSA_PROOF_VERIFICATION_FAILED',
// Erreurs système probatoires
TRUSTED_LIST_SIGNATURE_INVALID = 'TSA_TRUSTED_LIST_SIGNATURE_INVALID',
TRUSTED_LIST_EXPIRED = 'TSA_TRUSTED_LIST_EXPIRED',
SEAL_SIGNATURE_FAILED = 'TSA_SEAL_SIGNATURE_FAILED',
HSM_UNAVAILABLE = 'TSA_HSM_UNAVAILABLE',
// Erreurs ntpd-rs / horloge de référence
REFERENCE_CLOCK_NOT_SYNCHRONIZED = 'TSA_REFERENCE_CLOCK_NOT_SYNCHRONIZED',
REFERENCE_CLOCK_OFFSET_TOO_HIGH = 'TSA_REFERENCE_CLOCK_OFFSET_TOO_HIGH',
REFERENCE_CLOCK_STRATUM_TOO_HIGH = 'TSA_REFERENCE_CLOCK_STRATUM_TOO_HIGH',
REFERENCE_CLOCK_INSUFFICIENT_SOURCES = 'TSA_REFERENCE_CLOCK_INSUFFICIENT_SOURCES',
NTPD_RS_OBSERVE_FILE_MISSING = 'TSA_NTPD_RS_OBSERVE_FILE_MISSING',
NTPD_RS_OBSERVE_FILE_STALE = 'TSA_NTPD_RS_OBSERVE_FILE_STALE',
}
4.2 Politique Probatoire pour Statuts Indéterminés
| Situation | Comportement Probatoire | Justification |
| CRL indisponible | REJET si cache > 24h, sinon cache accepté avec warning | Statut révocation doit être vérifiable |
| OCSP indisponible | Fallback CRL obligatoire. Si CRL aussi indispo → REJET | Pas de "soft-fail" en contexte probatoire |
| OCSP = UNKNOWN | REJET avec code TST_REVOCATION_INDETERMINATE | Unknown ≠ non-révoqué |
| Trusted List expirée | REJET si > 7 jours, warning si > 48h | Qualification doit être vérifiable |
| ntpd-rs non synchronisé | REJET de l'opération si: offset > 5ms, stratum > 3, ou < 2 sources actives | Invariant 8/9 non négociable |
| Fichier observe.json absent/périmé | REJET si fichier absent ou âge > 30s | ntpd-rs doit être opérationnel |
| HSM indisponible | REJET du scellement, batch reste OPEN | Pas de scellement sans signature |
4.3 Stratégies de Récupération
| Erreur | Stratégie | Retry | Fallback |
| QTSA_UNREACHABLE | Retry exponentiel | 3x (1s, 5s, 30s) | QTSA secondaire qualifiée |
| TRUSTED_LIST_EXPIRED | Refresh immédiat | 2x | Cache si < 7 jours + alerte |
| REFERENCE_CLOCK_NOT_SYNCHRONIZED | Attendre resync ntpd-rs | Polling 5s, max 30s | Aucun (rejet après timeout) |
| NTPD_RS_OBSERVE_STALE | Vérifier service ntpd-rs | Alerte ops | Aucun (rejet) |
| HSM_UNAVAILABLE | Retry | 3x (1s, 5s, 15s) | Aucun (rejet) |
| TST_REVOCATION_INDETERMINATE | Log + rejet | Non | Aucun (rejet) |
4.4 Circuit Breaker pour QTSA
interface QtsaCircuitBreaker {
state: 'CLOSED' | 'OPEN' | 'HALF_OPEN';
failureCount: number;
failureThreshold: 5;
resetTimeout: 60000; // 1 minute
lastFailure: Date;
// Nouveau: basculement automatique
alternateQtsa?: QtsaConfig;
failoverEnabled: boolean;
}
5. Impacts Sécurité
5.1 Surfaces d'Attaque et Mitigations
| Surface | Risque | Mitigation |
| Replay TST | Réutilisation d'un TST pour un autre batch | Vérification messageImprint == rootHash calculé depuis scellement |
| Manipulation Merkle post-scellement | Altération de l'arbre après clôture | Signature HSM du scellement + triggers DB interdisant modification |
| Accès DB privilégié | Modification directe en base | Triggers PostgreSQL + Row-Level Security + audit PD-17 + option WORM Glacier |
| Man-in-the-Middle QTSA | Interception/modification requête/réponse | TLS 1.3 obligatoire, vérification certificat vs Trusted List |
| Horloge falsifiée | Manipulation de l'heure | NTS authentifié uniquement, attestations stockées |
| NTP spoofing | Injection de temps falsifié | NTS obligatoire (authentification cryptographique), pas de fallback NTP |
| Trusted List compromise | QTSA non qualifiée acceptée | Vérification signature LOTL/TL, stockage evidence |
| Déni de service batch | Création massive de batchs | Rate limiting, quota par organisation |
5.2 Données Sensibles et Protection
| Donnée | Classification | Protection | Rétention |
root_hash | Métadonnée cryptographique | Intégrité via scellement signé | Durée de vie batch |
seal_signature | Preuve probatoire | HSM pour génération, lecture seule | Idem TST |
tst_raw | Preuve probatoire | Stockage chiffré AES-256-GCM | 10 ans minimum |
tree_data (CBOR) | Structure Merkle | Intégrité via seal, backup | Idem TST |
clock_attestation | Preuve temporelle | NTS response stockée intégralement | Idem TST |
qualification_evidence | Preuve qualification | Hash TL + timestamp | Idem TST |
| Clés HSM | Secret | HSM uniquement, jamais exportées | N/A |
5.3 Audit Trail (PD-17)
Actions auditées avec contexte probatoire:
| Action | Métadonnées Capturées |
BATCH_CREATED | batch_id, creator_id, organization_id |
BATCH_ITEM_ADDED | batch_id, item_hash, item_index |
BATCH_SEALED | batch_id, root_hash, seal_id, attestation_id |
BATCH_SEAL_VERIFIED | batch_id, seal_valid, verifier_id |
BATCH_TIMESTAMPED | batch_id, tst_id, genTime, qtsa_id |
TST_VALIDATED | tst_id, validation_result, evidence_id |
TST_VALIDATION_FAILED | tst_id, error_code, details |
INCLUSION_PROOF_GENERATED | batch_id, item_hash, proof_hash |
INCLUSION_PROOF_VERIFIED | proof_hash, valid, verifier_id |
QUALIFICATION_VERIFIED | qtsa_id, qualified, tl_hash |
IMMUTABILITY_VIOLATION_ATTEMPT | batch_id, operation, actor_id, ALERTE |
6. Spécifications Normatives
Conformité RFC 6962 Section 2.1 (Certificate Transparency):
MTH (Merkle Tree Hash):
MTH({}) = SHA-256() // arbre vide
MTH({d(0)}) = SHA-256(0x00 || d(0)) // feuille unique
Pour n > 1, k = 2^(floor(log2(n-1))):
MTH(D[n]) = SHA-256(0x01 || MTH(D[0:k]) || MTH(D[k:n]))
Règles de construction : 1. Préfixe feuille : 0x00 (distingue feuilles des nœuds internes) 2. Préfixe nœud : 0x01 3. Ordre des feuilles : Tri lexicographique croissant des hash en bytes 4. Encodage hash : Hexadécimal lowercase 5. Algorithme : SHA-256 exclusivement (compatibilité CT)
Sérialisation arbre (CBOR, RFC 8949) :
{
"version": 1,
"algorithm": "SHA-256",
"tree_size": <uint>,
"root_hash": <bytes32>,
"leaves": [<bytes32>, ...], // ordonnées
"nodes": [<bytes32>, ...] // ordre level-order
}
interface InclusionProofDataV2 {
/** Identifiant unique du log (batch_id) */
log_id: string;
/** Taille de l'arbre au moment de la preuve */
tree_size: number;
/** Index de la feuille (0-based) */
leaf_index: number;
/** Chemin d'audit: liste de hashes frères */
inclusion_path: Array<{
hash: string; // hex lowercase
position: 'L' | 'R'; // position du frère
}>;
/** Hash de l'élément prouvé */
leaf_hash: string;
/** Racine Merkle */
root_hash: string;
/** Jeton d'horodatage RFC 3161 (base64) */
timestamp_token: string;
/** Scellement du batch (optionnel pour vérification) */
batch_seal?: {
payload_canonical: string; // JSON canonique
signature: string; // base64
algorithm: string; // ex: "ECDSA-P384"
};
}
Algorithme de vérification :
1. hash = SHA-256(0x00 || leaf_data)
2. Pour chaque (sibling, position) dans inclusion_path:
Si position == 'L':
hash = SHA-256(0x01 || sibling || hash)
Sinon:
hash = SHA-256(0x01 || hash || sibling)
3. Vérifier: hash == root_hash
4. Vérifier: root_hash == messageImprint du TST
5. Valider TST (signature, chaîne, révocation, qualification)
interface BatchSealPayload {
/** Version du format */
version: 1;
/** Identifiant batch */
batch_id: string;
/** Racine Merkle */
root_hash: string;
/** Nombre d'éléments */
item_count: number;
/** Algorithme Merkle */
tree_algorithm: "RFC6962_SHA256_MTH";
/** Horodatage NTS authentifié */
sealed_at: {
timestamp: string; // ISO 8601
nts_server: string;
nts_cookie_hash: string; // SHA-256 du cookie AEAD
};
/** Hash de l'arbre sérialisé */
tree_data_hash: string;
}
interface BatchSeal {
/** Payload canonique (RFC 8785 JCS) */
payload_canonical: string;
/** Signature du payload */
signature: string; // base64
/** Algorithme de signature (HsmService PD-36) */
algorithm: "ECDSA_SHA384"; // SignatureAlgorithm.ECDSA_SHA384 (P-384, 192-bit security)
/** Label clé HSM */
key_label: string; // Label dans HsmService
/** Référence attestation NTS */
clock_attestation_id: string;
}
7. Politique de Conservation et Archivage
7.1 Durées de Rétention
| Élément | Durée Minimale | Justification |
| TST (jeton horodatage) | 10 ans | Valeur probatoire légale |
| Scellement batch | 10 ans | Preuve de clôture |
| Arbre Merkle (CBOR) | 10 ans | Nécessaire pour preuves d'inclusion |
| Attestations NTS | 10 ans | Preuve d'usage horloge référence |
| Evidence qualification | 10 ans | Preuve QTSA qualifiée au moment T |
| Audit logs | 10 ans | Traçabilité |
| Éléments du batch | Selon politique métier | Min = durée TST |
Option WORM recommandée : AWS S3 Glacier avec Object Lock (Compliance mode)
interface ArchivePackage {
/** Métadonnées */
metadata: {
batch_id: string;
archived_at: string;
format_version: 1;
checksums: {
sha256: string;
sha384: string;
};
};
/** Contenu archivé */
content: {
tst_raw: Buffer; // DER
batch_seal: BatchSeal;
tree_data: Buffer; // CBOR
clock_attestations: ClockAttestation[];
qualification_evidence: QualificationEvidence;
audit_log_extract: AuditEntry[];
};
}
7.3 Intégrité et Vérification Périodique
- Checksum quotidien sur TST stockés
- Vérification mensuelle : re-calcul racine Merkle vs stockée
- Vérification annuelle : validation complète TST (signature, chaîne)
- Alerte si divergence détectée
8. Hypothèses Techniques
8.1 Dépendances Externes
| Dépendance | Hypothèse | Validation | Fallback |
| LOTL EU | https://ec.europa.eu/tools/lotl/eu-lotl.xml accessible | Test démarrage | Cache 7 jours max |
| TL nationales | URLs dans LOTL accessibles | Test démarrage | Cache 7 jours max |
| ntpd-rs | Démon NTS actif, observe.json accessible en lecture | Health check 10s | Aucun (rejet si non synchronisé) |
| QTSA | Endpoint RFC 3161 sur HTTPS, réponse < 10s | Health check 5min | QTSA secondaire |
| CRL/OCSP | Endpoints dans certificats accessibles | Cache 24h | Rejet si indisponible |
| HSM (PD-36) | HsmService initialisé, clés provisionnées | Health check 1min | Aucun (rejet) |
8.2 Infrastructure
| Élément | Exigence |
| Redis | Disponible pour BullMQ (PD-21) |
| PostgreSQL | Support BYTEA, triggers, RLS |
| HSM | HsmService (PD-36) — CloudHSM via PKCS#11 |
| S3 | Optionnel: Glacier avec Object Lock pour WORM |
| Réseau | Accès Internet sortant pour QTSA, TL, NTS |
8.3 Bibliothèques
| Fonctionnalité | Bibliothèque | Justification |
| Arbre Merkle | @noble/hashes + custom RFC 6962 | Audit sécurité, pas de dépendances |
| ASN.1 / RFC 3161 | pkijs + asn1js | Standard PKI, maintenu |
| NTS (horloge) | ntpd-rs (externe) + lecture observe.json | Pas de lib Node.js NTS viable, démon Rust mature |
| CBOR | cbor-x | Performant, TypeScript |
| JSON canonique | json-canonicalize | RFC 8785 JCS |
| XML Trusted Lists | fast-xml-parser | Parsing TL eIDAS |
| HSM | HsmService (PD-36) — src/modules/crypto/hsm/ | Module interne existant, PKCS#11 via pkcs11js |
HSM — Infrastructure existante (PD-36)
Le module HSM est déjà implémenté dans src/modules/crypto/hsm/. PD-39 réutilise cette infrastructure :
// Interfaces disponibles (pkcs11.interface.ts)
enum SignatureAlgorithm {
ECDSA_SHA256 = 'ECDSA_SHA256',
ECDSA_SHA384 = 'ECDSA_SHA384', // ← Utilisé pour scellement PD-39 (P-384, 192-bit)
RSA_PSS_SHA256 = 'RSA_PSS_SHA256',
RSA_PKCS_SHA256 = 'RSA_PKCS_SHA256',
}
enum EcdsaCurve {
P256 = 'P256',
P384 = 'P384', // ← Courbe pour PD-39 (sécurité long terme)
}
// HsmService (hsm.service.ts)
class HsmService {
sign(data: Buffer, keyLabel: string, algorithm: SignatureAlgorithm): Promise<SignatureResult>;
verify(data: Buffer, signature: Buffer, keyLabel: string, algorithm: SignatureAlgorithm): Promise<VerificationResult>;
getPublicKey(keyLabel: string): Promise<Buffer | null>;
generateKeyPair(keyType: KeyType, attributes: KeyAttributes): Promise<KeyGenerationResult>;
}
// Génération clé P-384 :
// hsmService.generateKeyPair(KeyType.ECDSA, { label: 'tsa-batch-seal-key', id: '...', keyType: KeyType.ECDSA, curve: EcdsaCurve.P384 });
Configuration requise (variables d'environnement) :
CLOUDHSM_LIBRARY_PATH — /opt/cloudhsm/lib/libcloudhsm_pkcs11.so CLOUDHSM_PIN — PIN Crypto User CLOUDHSM_SLOT — Slot ID (défaut: 0)
Clés à provisionner dans le HSM :
| Label | Type | Usage |
tsa-batch-seal-key | ECDSA P-384 | Signature scellement batch |
tsa-clock-attestation-key | ECDSA P-384 | Signature attestations NTS |
9. Points de Vigilance
9.1 Risques Résiduels
| Risque | Probabilité | Impact | Mitigation | Acceptation |
| Compromission HSM | Très faible | Critique | Audit HSM, rotation clés | Risque résiduel accepté |
| Indisponibilité NTS globale | Très faible | Bloquant | Multi-serveurs, monitoring | Opérations suspendues |
| Révocation QTSA | Faible | Élevé | Monitoring TL, QTSA secondaire | Nouveau TST sur autre QTSA |
| Corruption stockage | Faible | Élevé | Checksums, backups, WORM | Restauration backup |
9.2 Tests Critiques
| Test | Type | Criticité | Couverture Invariant |
| Scellement empêche ajout | Unitaire | Critique | 5, 10 |
| Signature scellement valide | Unitaire | Critique | 5, 10 |
| Trigger DB bloque UPDATE | Intégration | Critique | 10 |
| Preuve inclusion valide RFC 9162 | Unitaire | Critique | 6, 7 |
| Rejet élément non inclus | Unitaire | Critique | 7 |
| Validation QTSA via TL | Intégration | Critique | 1, 2 |
| Rejet QTSA non qualifiée | Unitaire | Élevée | 1 |
| Rejet OID non qualifiée | Unitaire | Élevée | 2 |
| Validation TST RFC 3161 | Intégration | Critique | 3 |
| Rejet si NTS auth échoue | Unitaire | Critique | 8, 9 |
| Attestation NTS stockée | Unitaire | Élevée | 9 |
| Rejet si OCSP=UNKNOWN | Unitaire | Élevée | Politique probatoire |
| Rejet si CRL expirée | Unitaire | Élevée | Politique probatoire |
| Circuit breaker QTSA | Intégration | Élevée | Disponibilité |
9.3 Points Clarifiés (vs section 10 spécification)
| Point | Décision |
| Algorithme empreinte agrégée | SHA-256 (RFC 6962 MTH) |
| Format structure d'agrégation | RFC 6962 Merkle Tree Hash, sérialisation CBOR |
| Format preuve d'inclusion | RFC 9162 InclusionProofDataV2 |
| Politique de conservation | 10 ans minimum, archivage WORM optionnel |
| Référentiel Policy OID | Extraction automatique des Trusted Lists eIDAS |
10. Estimation Effort par Composant
| Composant | Complexité | Priorité | Dépendances |
| BatchService + Entités | Moyenne | P0 | - |
| BatchSealService + HSM | Élevée | P0 | HSM disponible |
| MerkleService (RFC 6962) | Moyenne | P0 | - |
| TsaClientService | Moyenne | P0 | - |
| TstValidatorService | Élevée | P0 | QtsaQualificationService |
| QtsaQualificationService | Élevée | P0 | TrustedListService |
| TrustedListService | Élevée | P0 | - |
| InclusionProofService (RFC 9162) | Moyenne | P0 | MerkleService |
| ReferenceClockService (NTS) | Élevée | P0 | - |
| ClockAttestationService | Moyenne | P0 | ReferenceClockService |
| RevocationService | Moyenne | P1 | - |
| Triggers PostgreSQL | Faible | P0 | - |
| Tests d'intégration | Élevée | P0 | Tous composants |
Fin du plan d'implémentation PD-39 v3.0 (Architecture ntpd-rs).