PD-264 — Plan d'implémentation¶
0. Contexte et décisions architecturales¶
Constats d'audit Gate 3 intégrés¶
Le plan adresse les 11 constats résiduels de l'audit Gate 3 v3 :
| Constat | Gravité | Résolution dans ce plan |
|---|---|---|
| R01 — Stratégie migration données existantes | Majeur | TASK-1 : migration en 3 phases avec conversion hex→binary |
| R02 — Pas de TC migration | Majeur | TASK-7 : TC-MIG-01/02/03 dédiés |
| R03 — Trigger PD-55 post-migration | Majeur | Confirmé non impactant : trigger timestamp_token_immutable() est type-agnostic (RAISE EXCEPTION inconditionnel, ne lit aucune colonne) |
| R04 — CHECK "recommandée" vs obligatoire | Mineur | TASK-1 : CHECK constraint implémentée comme OBLIGATOIRE (défense en profondeur) |
| R05 — Verdict ESCALADE en CI | Mineur | TASK-6 : convention exit code + artifact |
| R06 — RESPONSE_REJECTED transitoire/persisté | Mineur | Design §2 : état in-memory, TC-264-11 testé via tentative de méthode (pas de mutation DB) |
| R07 — Nightly TSA sans SLA | Mineur | TASK-6 : retry + skip conditionnel avec artifact de diagnostic |
| R08 — Timeout REQUEST_EMITTED | Mineur | Hors périmètre PD-264 (confirmé spec §4) ; documenté H-06 |
| R09 — Chi² orphelin | Mineur | Accepté comme contrôle supplémentaire de qualité CSPRNG, non bloquant |
| R10 — INV-264-06 vs INV-264-13 | Mineur | Design §2 : séquence explicite (validation → INSERT atomique → post-commit async) |
| R11 — INV-264-09 couverture mock | Mineur | TASK-6 : TC-264-14 exécuté avec fixture DER réelle, pas mock auto-généré |
Phase 0 — Go/No-Go¶
| Hypothèse | Vérification | Statut |
|---|---|---|
Table timestamp_tokens existe | Migration 1733900000000-CreateTsaTables.ts confirmée | OK |
| Trigger immutabilité type-agnostic | Fonction timestamp_token_immutable() : RAISE EXCEPTION inconditionnel sans lecture de colonne | OK — migration BYTEA ne casse pas le trigger |
| Module TSA NestJS disponible | src/modules/tsa/ confirmé avec entities, services, processors | OK |
| BullMQ processor placeholder | batch-timestamp.processor.ts : placeholder QTSA_UNREACHABLE, prêt à intégrer | OK |
Enum TsaErrorCode extensible | Fichier enum confirmé, pas de NONCE_MISMATCH existant à ajouter | OK |
| Module Merkle persistence (PD-237) | src/modules/merkle/ avec merkle_trees, merkle_leaves | OK |
| Aucune donnée de production existante | Environnements dev/test uniquement, pas de tokens persistés avec nonce VARCHAR | À confirmer (voir H-01) |
Verdict Go/No-Go : GO — Aucune hypothèse bloquante.
1. Découpage en composants¶
TASK-1 — Migration DDL (nonce VARCHAR(64) → BYTEA NOT NULL)¶
Responsabilité : Migrer la colonne nonce de VARCHAR(64) nullable vers BYTEA NOT NULL (16 octets) avec contrainte CHECK et index unique.
Fichiers : - src/database/migrations/XXXXXXXXX-PD264NonceByteaMigration.ts (nouveau)
Opérations DDL (3 phases dans une même migration) :
Phase A — Conversion des données existantes :
-- Convertir les nonces hex existants en binaire
UPDATE vault_secure.timestamp_tokens
SET nonce = decode(nonce, 'hex')
WHERE nonce IS NOT NULL AND nonce != '';
-- Pour les lignes sans nonce (NULL), générer un nonce de remplissage traçable
-- Le nonce sera 16 octets de zéros + marqueur dans metadata
UPDATE vault_secure.timestamp_tokens
SET nonce = E'\\x00000000000000000000000000000000'
WHERE nonce IS NULL OR nonce = '';
Note : La Phase A nécessite la désactivation temporaire du trigger d'immutabilité. Cette opération est documentée en TASK-1 et exécutée dans la migration TypeORM avec réactivation immédiate.
Phase B — ALTER COLUMN :
ALTER TABLE vault_secure.timestamp_tokens
ALTER COLUMN nonce SET DATA TYPE BYTEA USING nonce::bytea,
ALTER COLUMN nonce SET NOT NULL;
Phase C — Contraintes et index :
-- CHECK constraint taille exacte (défense en profondeur, réponse R04)
ALTER TABLE vault_secure.timestamp_tokens
ADD CONSTRAINT chk_nonce_length CHECK (octet_length(nonce) = 16);
-- Index unique anti-rejeu
CREATE UNIQUE INDEX idx_timestamp_token_nonce
ON vault_secure.timestamp_tokens(nonce);
Down migration :
DROP INDEX IF EXISTS vault_secure.idx_timestamp_token_nonce;
ALTER TABLE vault_secure.timestamp_tokens DROP CONSTRAINT IF EXISTS chk_nonce_length;
ALTER TABLE vault_secure.timestamp_tokens
ALTER COLUMN nonce SET DATA TYPE VARCHAR(64) USING encode(nonce, 'hex'),
ALTER COLUMN nonce DROP NOT NULL;
Observable : Migration exécutable sans erreur sur schéma PD-55 existant (npm run typeorm migration:run).
TASK-2 — Entity TypeORM (TimestampToken)¶
Responsabilité : Adapter l'entité TypeORM pour refléter le nouveau type BYTEA NOT NULL.
Fichiers : - src/modules/tsa/entities/timestamp-token.entity.ts (modification)
Changements :
// Avant (PD-55)
@Column({ type: 'varchar', length: 64, nullable: true })
nonce?: string;
// Après (PD-264)
@Column({ type: 'bytea', nullable: false })
nonce!: Buffer;
Observable : Compilation TypeScript sans erreur ; aucune référence à nonce comme string dans le module TSA.
TASK-3 — Service de génération de nonce (NonceService)¶
Responsabilité : Générer des nonces cryptographiques 128 bits via CSPRNG, valider format et unicité.
Fichiers : - src/modules/tsa/services/nonce.service.ts (nouveau)
Interface publique :
@Injectable()
export class NonceService {
/**
* Génère un nonce cryptographique de 128 bits (16 octets).
* Utilise crypto.randomBytes (CSPRNG).
* INV-264-01 : fail-closed, aucune exception de mode.
*/
generate(): Buffer;
/**
* Valide la présence et la taille d'un nonce.
* INV-264-01, INV-264-02 : fail-closed.
*/
validatePresence(nonce: Buffer | null | undefined): void;
/**
* Compare deux nonces en temps constant.
* INV-264-03, INV-264-04 : crypto.timingSafeEqual.
*/
compareEqual(nonceRequest: Buffer, nonceResponse: Buffer): boolean;
/**
* Vérifie l'unicité du nonce en base.
* INV-264-05 : défense en profondeur (complément de l'index unique).
*/
checkUniqueness(nonce: Buffer, repository: Repository<TimestampToken>): Promise<void>;
}
Contraintes : - generate() : crypto.randomBytes(16) — CSPRNG exclusif (INV-264-01) - compareEqual() : crypto.timingSafeEqual(a, b) — temps constant obligatoire (INV-264-04) - validatePresence() : throw NonceValidationError si null/undefined/taille != 16 - checkUniqueness() : throw NonceDuplicateError si nonce déjà en base
Observable : Tests unitaires sur génération (taille, type Buffer) et comparaison (temps constant vérifié).
TASK-4 — Service de validation nonce TSA (NonceValidationService)¶
Responsabilité : Orchestrer la validation complète du nonce dans le flux TSA (garde d'entrée, validation réponse, unicité, automate d'états).
Fichiers : - src/modules/tsa/services/nonce-validation.service.ts (nouveau)
Interface publique :
@Injectable()
export class NonceValidationService {
/**
* Garde d'entrée : vérifie qu'une requête contient un nonce valide
* AVANT d'entrer dans l'automate d'états.
* INV-264-01, CA-264-01 : fail-closed, garde d'entrée.
*/
validateRequestNonce(nonce: Buffer | null | undefined): void;
/**
* Validation complète de la réponse TSA :
* 1. Présence nonce en réponse (INV-264-02)
* 2. Égalité binaire temps constant (INV-264-03, INV-264-04)
* 3. Unicité en base (INV-264-05)
*
* @returns ValidationResult avec état ACCEPTED ou REJECTED + motif
*/
validateResponseNonce(
nonceRequest: Buffer,
nonceResponse: Buffer | null | undefined,
repository: Repository<TimestampToken>,
): Promise<NonceValidationResult>;
}
États de validation (enum) :
export enum NonceValidationStatus {
ACCEPTED = 'ACCEPTED',
REJECTED_ABSENT = 'REJECTED_ABSENT',
REJECTED_MISMATCH = 'REJECTED_MISMATCH',
REJECTED_DUPLICATE = 'REJECTED_DUPLICATE',
REJECTED_INVALID_SIZE = 'REJECTED_INVALID_SIZE',
}
Transitions d'état (réponse R06) : - RESPONSE_RECEIVED → RESPONSE_ACCEPTED : validation complète OK - RESPONSE_RECEIVED → RESPONSE_REJECTED : validation échouée (motif tracé) - RESPONSE_REJECTED est un état in-memory transitoire — aucune écriture DB ; les transitions interdites (INV-264-12) sont testées via tentative d'appel aux méthodes de transition qui refusent l'opération
Observable : Chaque rejet produit un log structuré { event: 'NONCE_REJECTED', reason: string, batchId: string }.
TASK-5 — Intégration dans le BatchTimestampProcessor¶
Responsabilité : Activer le nonce dans le flux de timestamping existant (processor BullMQ).
Fichiers : - src/modules/tsa/processors/batch-timestamp.processor.ts (modification) - src/modules/tsa/enums/tsa-error-code.enum.ts (modification : ajout codes nonce)
Nouveaux codes d'erreur :
// Nonce Errors
NONCE_MISSING_IN_REQUEST = 'TSA_NONCE_MISSING_IN_REQUEST',
NONCE_MISSING_IN_RESPONSE = 'TSA_NONCE_MISSING_IN_RESPONSE',
NONCE_MISMATCH = 'TSA_NONCE_MISMATCH',
NONCE_DUPLICATE = 'TSA_NONCE_DUPLICATE',
NONCE_INVALID_SIZE = 'TSA_NONCE_INVALID_SIZE',
Séquence dans le processor (INV-264-06, INV-264-13, réponse R10) :
1. Vérifier batch SEALED
2. nonceService.generate() → nonce 16 octets
3. nonceValidation.validateRequestNonce(nonce) → garde d'entrée
4. → État REQUEST_EMITTED
5. Appeler QTSA avec nonce dans TimeStampReq
6. → État RESPONSE_RECEIVED
7. nonceValidation.validateResponseNonce(nonceReq, nonceResp, repo)
├─ ACCEPTED → continuer
└─ REJECTED → log + return erreur (aucune écriture DB/audit/Merkle)
8. → État RESPONSE_ACCEPTED
9. Transaction DB atomique :
├─ INSERT timestamp_tokens (avec nonce BYTEA)
├─ UPDATE timestamp_batches SET status = 'TIMESTAMPED'
└─ COMMIT
10. → État TOKEN_PERSISTED
11. Post-commit asynchrone : append-only journal + agrégation Merkle
(via événement BullMQ ou callback, réconciliation PD-55 en cas d'échec)
Observable : Log structuré à chaque transition d'état ; absence de writes DB si rejet nonce.
TASK-6 — Suite de tests contractuels¶
Responsabilité : Implémenter les 18 TC + 5 NR + 3 TC migration.
Fichiers : - src/modules/tsa/services/nonce.service.spec.ts (nouveau) - src/modules/tsa/services/nonce-validation.service.spec.ts (nouveau) - src/modules/tsa/processors/batch-timestamp.processor.spec.ts (modification) - src/modules/tsa/__tests__/nonce-integration.spec.ts (nouveau) - src/modules/tsa/__tests__/nonce-timing.spec.ts (nouveau) - src/modules/tsa/__tests__/nonce-migration.spec.ts (nouveau)
Mapping TC → Niveau de test :
| TC | Niveau | Fichier |
|---|---|---|
| TC-264-01 | Unit | nonce-validation.service.spec.ts |
| TC-264-02 | Unit | nonce-validation.service.spec.ts |
| TC-264-03 | Unit | nonce-validation.service.spec.ts |
| TC-264-04 | Integration | nonce-integration.spec.ts |
| TC-264-05 | Integration | nonce-integration.spec.ts |
| TC-264-06 | Integration | nonce-integration.spec.ts |
| TC-264-07 | Security/Perf | nonce-timing.spec.ts |
| TC-264-08 | Unit + Static | nonce.service.spec.ts |
| TC-264-09 | Integration (PG réel) | nonce-integration.spec.ts |
| TC-264-10 | Integration | nonce-integration.spec.ts |
| TC-264-11 | Unit | nonce-validation.service.spec.ts |
| TC-264-12 | Integration | batch-timestamp.processor.spec.ts |
| TC-264-13 | Integration | batch-timestamp.processor.spec.ts |
| TC-264-14 | E2E (openssl) | nonce-integration.spec.ts |
| TC-264-15 | Formal | (Prolog, hors Jest) |
| TC-264-16 | CI | (pipeline, mock TSA) |
| TC-264-17 | Nightly | (pipeline nightly, TSA réelle) |
| TC-264-18 | Audit | nonce.service.spec.ts |
| TC-264-19 | Integration (fault injection) | nonce-integration.spec.ts |
| TC-MIG-01 | Migration | nonce-migration.spec.ts |
| TC-MIG-02 | Migration | nonce-migration.spec.ts |
| TC-MIG-03 | Migration | nonce-migration.spec.ts |
TC supplémentaires migration (réponse R01, R02, R03) :
- TC-MIG-01 : Migration up sur schéma PD-55 vierge (aucun token existant) → succès, schema cible atteint.
- TC-MIG-02 : Migration up sur schéma PD-55 avec tokens existants (nonce hex + nonce NULL) → conversion correcte, CHECK constraint respectée, down migration restaure l'état.
- TC-MIG-03 : Post-migration, trigger
trg_timestamp_token_immutabletoujours actif → UPDATE rejeté.
Convention TC-264-07 ESCALADE en CI (réponse R05) : - Exit code 0 si critères statistiques atteints - Exit code 0 + artifact timing-escalation.json si premier échec, relance nécessaire - Exit code 1 + artifact timing-audit-required.json si deuxième échec → bloquant Gate 8, non bloquant Gate ⅗ - Pipeline CI : le job timing est dans un stage séparé security-timing, marqué allow_failure: true pour Gate ⅗, allow_failure: false pour Gate 8
Convention TC-264-17 Nightly (réponse R07) : - Retry automatique (3 tentatives, backoff exponentiel) - Si TSA indisponible après 3 tentatives : skip avec artifact nightly-tsa-unavailable.json contenant diagnostic - Le skip ne compte pas comme succès : le nightly reste unstable
Observable : Rapport Jest avec couverture ≥80% sur le code PD-264.
TASK-7 — Intégration Prolog (vérification formelle)¶
Responsabilité : Mettre à jour les règles Prolog RFC 3161 pour atteindre 18/18.
Fichiers : - formal-verification/prolog/rfc3161-rules.pl (modification) — ou emplacement équivalent - formal-verification/prolog/pd264-nonce.pl (nouveau)
Observable : Exécution suite Prolog → 18/18 (CA-264-10, TC-264-15).
2. Flux techniques¶
Flux nominal (happy path)¶
┌────────────────────────────────────────────────────────────────────┐
│ BatchTimestampProcessor.handleTimestamp(job) │
│ │
│ 1. batch = batchService.getBatch(batchId) │
│ ├─ batch.status != SEALED → throw BATCH_NOT_SEALED │
│ └─ OK │
│ │
│ 2. nonce = nonceService.generate() ← crypto.randomBytes(16) │
│ └─ nonce: Buffer (16 octets) │
│ │
│ 3. nonceValidation.validateRequestNonce(nonce) │
│ ├─ null/undefined/length!=16 → throw NONCE_MISSING_IN_REQUEST │
│ └─ OK → [STATE: REQUEST_EMITTED] │
│ │
│ 4. tsaResponse = qtsaClient.requestTimestamp({ │
│ messageImprint: batch.rootHash, │
│ hashAlgorithm: SHA-256, │
│ nonce: nonce ← injecté dans TimeStampReq │
│ }) │
│ └─ [STATE: RESPONSE_RECEIVED] │
│ │
│ 5. validationResult = nonceValidation.validateResponseNonce( │
│ nonce, tsaResponse.nonce, timestampTokenRepo │
│ ) │
│ ├─ REJECTED_ABSENT → log + return error (aucune écriture) │
│ ├─ REJECTED_MISMATCH → log + return error (aucune écriture) │
│ ├─ REJECTED_DUPLICATE → log + return error (aucune écriture) │
│ └─ ACCEPTED → [STATE: RESPONSE_ACCEPTED] │
│ │
│ 6. BEGIN TRANSACTION │
│ ├─ INSERT INTO timestamp_tokens (..., nonce, ...) │
│ ├─ UPDATE timestamp_batches SET status='TIMESTAMPED' │
│ └─ COMMIT │
│ └─ [STATE: TOKEN_PERSISTED] │
│ │
│ 7. Post-commit (async, idempotent) : │
│ ├─ Append-only journal (vault_merkle.merkle_leaves) │
│ ├─ Agrégation Merkle (vault_merkle.merkle_trees) │
│ └─ Échec → réconciliation via worker PD-55 │
│ │
│ 8. return { batchId, success: true, tokenId, genTime } │
└────────────────────────────────────────────────────────────────────┘
Flux de rejet (nonce invalide)¶
┌────────────────────────────────────────────────────────────────────┐
│ Validation nonce échouée (étape 5) │
│ │
│ validationResult.status = REJECTED_* │
│ │
│ 1. Logger.warn({ event: 'NONCE_REJECTED', reason, batchId }) │
│ 2. Aucun INSERT dans timestamp_tokens │
│ 3. Aucun append dans vault_merkle.merkle_leaves │
│ 4. Aucune inclusion Merkle dans vault_merkle.merkle_trees │
│ 5. Batch status → FAILED (avec errorCode NONCE_*) │
│ 6. return { batchId, success: false, errorCode, errorMessage } │
└────────────────────────────────────────────────────────────────────┘
Flux de crash (INV-264-13)¶
┌────────────────────────────────────────────────────────────────────┐
│ Cas A : crash AVANT commit (étape 6) │
│ → Transaction rollback automatique (PostgreSQL) │
│ → Aucun token persisté, aucun append, aucun Merkle │
│ → État cohérent │
│ │
│ Cas B : crash APRÈS commit, AVANT post-commit (étape 7) │
│ → Token persisté en base (valide) │
│ → Append-only/Merkle potentiellement absents │
│ → Worker de réconciliation PD-55 détecte le manque │
│ → Rattrapage idempotent (sans doublon) │
└────────────────────────────────────────────────────────────────────┘
2b. Diagrammes Mermaid¶
Graphe de dépendances entre composants (TASKs)¶
graph TD
TASK1["TASK-1<br/>Migration DDL<br/>nonce VARCHAR→BYTEA"]
TASK2["TASK-2<br/>Entity TypeORM<br/>TimestampToken"]
TASK3["TASK-3<br/>NonceService<br/>generate / compare / uniqueness"]
TASK4["TASK-4<br/>NonceValidationService<br/>orchestration validation"]
TASK5["TASK-5<br/>BatchTimestampProcessor<br/>intégration flux TSA"]
TASK6["TASK-6<br/>Tests contractuels<br/>18 TC + 5 NR + 3 MIG"]
TASK7["TASK-7<br/>Prolog RFC 3161<br/>vérification formelle"]
TASK1 --> TASK2
TASK2 --> TASK3
TASK3 --> TASK4
TASK2 --> TASK5
TASK4 --> TASK5
TASK1 --> TASK6
TASK3 --> TASK6
TASK4 --> TASK6
TASK5 --> TASK6
TASK5 --> TASK7
PD55["PD-55<br/>Module TSA existant<br/>trigger immutabilité"]
PD39["PD-39<br/>Architecture TSA<br/>entities, enums"]
PD237["PD-237<br/>Merkle persistence<br/>merkle_trees, merkle_leaves"]
PD55 -.->|hérité| TASK1
PD55 -.->|hérité| TASK2
PD39 -.->|hérité| TASK5
PD237 -.->|post-commit| TASK5
style TASK1 fill:#e6f3ff,stroke:#0066cc
style TASK2 fill:#e6f3ff,stroke:#0066cc
style TASK3 fill:#fff2e6,stroke:#cc6600
style TASK4 fill:#fff2e6,stroke:#cc6600
style TASK5 fill:#ffe6e6,stroke:#cc0000
style TASK6 fill:#e6ffe6,stroke:#006600
style TASK7 fill:#f2e6ff,stroke:#6600cc
style PD55 fill:#f0f0f0,stroke:#999
style PD39 fill:#f0f0f0,stroke:#999
style PD237 fill:#f0f0f0,stroke:#999 Diagramme de séquence — Flux nominal multi-service¶
sequenceDiagram
participant BullMQ as BullMQ Job
participant Processor as BatchTimestampProcessor
participant NonceS as NonceService
participant NonceV as NonceValidationService
participant QTSA as QTSA Client (mock)
participant PG as PostgreSQL
participant Merkle as Merkle Worker (PD-237)
BullMQ->>Processor: handleTimestamp(job)
Processor->>Processor: batchService.getBatch(batchId)
Note over Processor: Vérifie batch.status == SEALED
Processor->>NonceS: generate()
NonceS-->>Processor: nonce: Buffer(16)
Note over NonceS: crypto.randomBytes(16) — INV-264-01
Processor->>NonceV: validateRequestNonce(nonce)
Note over NonceV: Garde d'entrée — CA-264-01
NonceV-->>Processor: OK
Note over Processor: [STATE: REQUEST_EMITTED]
Processor->>QTSA: requestTimestamp({rootHash, nonce})
QTSA-->>Processor: TimeStampResp {token, nonce}
Note over Processor: [STATE: RESPONSE_RECEIVED]
Processor->>NonceV: validateResponseNonce(nonceReq, nonceResp, repo)
NonceV->>NonceS: compareEqual(nonceReq, nonceResp)
Note over NonceS: crypto.timingSafeEqual — INV-264-04
NonceS-->>NonceV: true
NonceV->>PG: SELECT COUNT(*) WHERE nonce = ?
Note over PG: Vérification unicité — INV-264-05
PG-->>NonceV: 0
NonceV-->>Processor: ACCEPTED
Note over Processor: [STATE: RESPONSE_ACCEPTED]
Processor->>PG: BEGIN TRANSACTION
Processor->>PG: INSERT timestamp_tokens (nonce BYTEA)
Processor->>PG: UPDATE timestamp_batches SET status='TIMESTAMPED'
Processor->>PG: COMMIT
Note over PG: Atomicité — INV-264-13
Note over Processor: [STATE: TOKEN_PERSISTED]
Processor-->>Merkle: post-commit async
Merkle->>PG: INSERT merkle_leaves
Merkle->>PG: UPDATE merkle_trees
Processor-->>BullMQ: {success: true, tokenId, genTime} Diagramme de séquence — Flux de rejet (nonce mismatch)¶
sequenceDiagram
participant Processor as BatchTimestampProcessor
participant NonceV as NonceValidationService
participant NonceS as NonceService
participant PG as PostgreSQL
Note over Processor: [STATE: RESPONSE_RECEIVED]
Processor->>NonceV: validateResponseNonce(nonceReq, nonceResp, repo)
NonceV->>NonceS: compareEqual(nonceReq, nonceResp)
NonceS-->>NonceV: false (mismatch)
NonceV-->>Processor: REJECTED_MISMATCH
Note over Processor: Logger.warn({event: 'NONCE_REJECTED', reason: 'MISMATCH'})
Note over Processor: Aucun INSERT timestamp_tokens
Note over Processor: Aucun append merkle_leaves
Note over Processor: Aucune agrégation merkle_trees
Processor->>PG: UPDATE timestamp_batches SET status='FAILED', errorCode='NONCE_MISMATCH'
Processor-->>Processor: return {success: false, errorCode} 3. Mapping invariants → mécanismes¶
| Invariant ID | Exigence | Mécanisme | Composant | Observable | Risque |
|---|---|---|---|---|---|
| INV-264-01 | Nonce 128 bits obligatoire (fail-closed) | crypto.randomBytes(16) dans NonceService.generate() ; garde d'entrée dans NonceValidationService.validateRequestNonce() | TASK-3, TASK-4 | Test unitaire taille Buffer = 16 ; analyse statique absence Math.random() | CSPRNG indisponible (système) → exception runtime |
| INV-264-02 | Présence nonce en réponse | NonceValidationService.validateResponseNonce() : check null/undefined | TASK-4 | Test TC-264-02 : mock réponse sans nonce → REJECTED_ABSENT | Mock TSA non conforme pourrait masquer le check |
| INV-264-03 | Égalité stricte binaire | NonceService.compareEqual() via crypto.timingSafeEqual | TASK-3 | Test TC-264-03 : nonce_A vs nonce_B → REJECTED_MISMATCH | Conversion de type implicite (string vs Buffer) |
| INV-264-04 | Comparaison temps constant | crypto.timingSafeEqual dans NonceService.compareEqual() | TASK-3 | Test TC-264-07 : vérification statique + protocole statistique | Faux positifs statistiques sur infra bruyante |
| INV-264-05 | Unicité globale | Index UNIQUE sur nonce (TASK-1) + vérification applicative NonceService.checkUniqueness() (TASK-3) | TASK-1, TASK-3 | Test TC-264-05 (métier) + TC-264-06 (concurrence DB) | Race condition → défense en profondeur DB |
| INV-264-06 | Validation avant écriture | Séquence processor : validation nonce → puis INSERT atomique | TASK-5 | Test TC-264-02/03/05 : absence writes DB après rejet | Bug de séquençage dans processor |
| INV-264-07 | Rejet strict non-conformes | NonceValidationService : rejet déterministe + log structuré | TASK-4 | Tests TC-264-02/03/05 : rejet avec motif tracé | Rejet silencieux sans log |
| INV-264-08 | Immutabilité post-insertion | Trigger trg_timestamp_token_immutable (PD-55, hérité) | Hérité | Test TC-264-09 : UPDATE SQL direct → exception PostgreSQL | Trigger désactivé par erreur de migration |
| INV-264-09 | Conformité ASN.1/DER | Validation structurelle du TST dans le processor (existant PD-39) + fixture DER réelle pour TC-264-14 | TASK-5, TASK-6 | Test TC-264-14 : openssl ts -verify sur token accepté | Fixture obsolète |
| INV-264-10 | Interopérabilité openssl | TC-264-14 avec openssl ts -verify en CI | TASK-6 | Exit code openssl = 0 | Version openssl incompatible en CI |
| INV-264-11 | Invariant formel (composite) | Composition de INV-264-02 + INV-264-03 + INV-264-05 | TASK-3, TASK-4 | Tests TC-264-04 + TC-264-05 | Couverture partielle si un composant manque |
| INV-264-12 | Transitions inverses interdites | Logique d'automate dans NonceValidationService : pas de méthode pour transitions inverses | TASK-4 | Test TC-264-11 : 5 transitions interdites → refus systématique | État mutable accessible par autre chemin |
| INV-264-13 | Atomicité ACCEPTED→PERSISTED (scope DB) | Transaction PostgreSQL englobant INSERT timestamp_tokens + UPDATE timestamp_batches ; post-commit async pour append/Merkle | TASK-5 | Test TC-264-19 : fault injection pré/post-commit | Timeout de transaction long |
| INV-264-envelope | Non applicable | Pas d'artefact secret dans PD-264 | N/A | Test TC-264-18 : audit code path, absence DEK/ReKey | N/A |
4. Mapping critères d'acceptation → mécanismes¶
| Critère ID | Mécanisme(s) | Composant | Observable | Risque |
|---|---|---|---|---|
| CA-264-01 | validateRequestNonce() + garde d'entrée processor | TASK-4, TASK-5 | TC-264-01 : requête sans nonce → rejet avant REQUEST_EMITTED | Garde d'entrée contournée |
| CA-264-02 | validateResponseNonce() check présence | TASK-4 | TC-264-02 : réponse sans nonce → REJECTED_ABSENT, 0 writes DB | |
| CA-264-03 | compareEqual() via timingSafeEqual | TASK-3 | TC-264-03 : nonce_A ≠ nonce_B → REJECTED_MISMATCH, 0 writes DB | |
| CA-264-04 | Validation complète + INSERT atomique | TASK-4, TASK-5 | TC-264-04 : nonce identique inédit → ACCEPTED → TOKEN_PERSISTED | |
| CA-264-05 | checkUniqueness() + index UNIQUE DB | TASK-3, TASK-1 | TC-264-05 + TC-264-06 : nonce déjà persisté → rejet | |
| CA-264-06 | Séquence processor : validation AVANT tout INSERT | TASK-5 | TC-264-02/03/05/19 : COUNT(*) FROM timestamp_tokens WHERE nonce = ? = 0 après rejet | |
| CA-264-07 | Rejet nonce → pas d'événement Merkle/append-only | TASK-5 | TC-264-02/03/05/13/19 : COUNT(*) FROM merkle_leaves inchangé après rejet | |
| CA-264-08 | Column nonce BYTEA NOT NULL + CHECK octet_length = 16 | TASK-1, TASK-2 | TC-264-04 : SELECT octet_length(nonce) FROM timestamp_tokens = 16 | |
| CA-264-09 | Token DER stocké dans tst_raw BYTEA | TASK-5 (hérité) | TC-264-14 : openssl ts -verify succès | |
| CA-264-10 | Règles Prolog RFC 3161 + cas PD-264 | TASK-7 | TC-264-15 : Prolog 18/18 | |
| CA-264-11 | Mock TSA conforme RFC dans CI | TASK-6 | TC-264-16 : résultats identiques sur exécutions répétées | |
| CA-264-12 | Job nightly avec TSA qualifiée réelle | TASK-6 | TC-264-17 : token archivé avec métadonnées probatoires |
5. Mapping tests (TC-*) → mécanismes + observables¶
| Test ID | Référence spec | Mécanisme(s) | Point(s) d'observation | Niveau |
|---|---|---|---|---|
| TC-264-01 | INV-264-01, CA-264-01 | validateRequestNonce(null) → exception ; processor n'atteint pas REQUEST_EMITTED | Log absence + exception NONCE_MISSING_IN_REQUEST | Unit |
| TC-264-02 | INV-264-02, INV-264-06, INV-264-07, CA-264-02, CA-264-06, CA-264-07 | validateResponseNonce(nonce, null, repo) → REJECTED_ABSENT | 0 rows insérées dans timestamp_tokens ; 0 appends dans merkle_leaves | Unit |
| TC-264-03 | INV-264-03, INV-264-06, INV-264-07, CA-264-03, CA-264-06, CA-264-07 | validateResponseNonce(nonce_A, nonce_B, repo) → REJECTED_MISMATCH | 0 rows insérées ; motif MISMATCH dans résultat | Unit |
| TC-264-04 | INV-264-02, INV-264-03, INV-264-05, INV-264-09, INV-264-11, CA-264-04, CA-264-08 | Flux complet nominal : generate → validate → INSERT → vérifier octet_length(nonce) = 16 | 1 row dans timestamp_tokens ; nonce BYTEA 16 octets ; token DER valide | Integration |
| TC-264-05 | INV-264-05, INV-264-06, INV-264-07, INV-264-11, CA-264-05, CA-264-06, CA-264-07 | checkUniqueness(nonce_A) sur nonce déjà en base → REJECTED_DUPLICATE | 0 rows supplémentaires ; 0 appends | Integration |
| TC-264-06 | INV-264-05, CA-264-05 | 2 insertions concurrentes même nonce → exactement 1 réussit (contrainte UNIQUE DB) | SELECT COUNT(*) FROM timestamp_tokens WHERE nonce = ? = 1 | Integration |
| TC-264-07 | INV-264-04 | Vérification statique timingSafeEqual + Welch t-test + Mann-Whitney U (N=100K, α=0.01, Cliff's δ<0.147) | Artifact JSON avec p-values et delta ; exit code selon convention | Security |
| TC-264-08 | INV-264-01 | Vérification statique (grep crypto.randomBytes(16)) + runtime (10K nonces, tous 16 octets) + Chi² (α=0.01) | Rapport statique + runtime + chi² | Unit + Static |
| TC-264-09 | INV-264-08 | UPDATE timestamp_tokens SET nonce = ? WHERE id = ? → exception trigger PostgreSQL | Error message contient append-only | Integration (PG réel) |
| TC-264-10 | INV-264-06 | Flux nominal complet : REQUEST_EMITTED → RESPONSE_RECEIVED → RESPONSE_ACCEPTED → TOKEN_PERSISTED | Transitions dans l'ordre ; pas de transition parasite | Integration |
| TC-264-11 | INV-264-12 | 5 transitions interdites : appel méthode → refus (exception ou retour d'erreur) | Chaque transition → exception TRANSITION_FORBIDDEN | Unit |
| TC-264-12 | Non-régression PD-55 | Token accepté → agrégation Merkle → ancrage (vérifier intégrité pipeline) | Merkle leaf count +1 ; root hash calculé | Integration |
| TC-264-13 | INV-264-06, CA-264-07, NR | Rejet nonce → pipeline probatoire inchangé | Merkle leaf count inchangé ; append-only journal inchangé | Integration |
| TC-264-14 | INV-264-09, INV-264-10, CA-264-09 | openssl ts -verify sur token accepté (fixture DER réelle) | Exit code 0 ; output Verification: OK | E2E |
| TC-264-15 | CA-264-10 | Suite Prolog RFC 3161 avec assertions PD-264 | Score 18/18 | Formal |
| TC-264-16 | CA-264-11 | Pipeline CI : mock TSA fixtures versionnées | Résultats identiques sur N exécutions | CI |
| TC-264-17 | CA-264-12 | Pipeline nightly : TSA qualifiée réelle | Token archivé avec horodatage ; retry 3x si TSA indisponible | Nightly |
| TC-264-18 | INV-264-envelope | Audit code path PD-264 : absence DEK/ReKey/fragment | Grep négatif sur patterns secrets dans code PD-264 | Audit |
| TC-264-19 | INV-264-13, CA-264-06, CA-264-07 | Fault injection : crash avant/après commit | Cas A : 0 tokens ; Cas B : 1 token + rattrapage Merkle | Integration |
6. Gestion des erreurs¶
| Code erreur | Condition | Traitement | Observable | Retryable |
|---|---|---|---|---|
TSA_NONCE_MISSING_IN_REQUEST | Nonce null/undefined/taille≠16 dans requête | Rejet avant REQUEST_EMITTED ; log warn | Log structuré + exception | Non |
TSA_NONCE_MISSING_IN_RESPONSE | Nonce absent dans réponse TSA | RESPONSE_REJECTED ; aucune écriture | Log warn + motif ABSENT | Non |
TSA_NONCE_MISMATCH | Nonce réponse ≠ nonce requête | RESPONSE_REJECTED ; aucune écriture | Log warn + motif MISMATCH | Non |
TSA_NONCE_DUPLICATE | Nonce déjà persisté en base | RESPONSE_REJECTED ; aucune écriture | Log warn + motif DUPLICATE | Non |
TSA_NONCE_INVALID_SIZE | Nonce de taille ≠ 16 octets | RESPONSE_REJECTED ; aucune écriture | Log warn + motif INVALID_SIZE | Non |
| PostgreSQL UNIQUE violation | Race condition : 2 INSERT concurrents même nonce | Erreur capturée → REJECTED_DUPLICATE (défense en profondeur) | Log error + rollback transaction | Non |
| PostgreSQL CHECK violation | octet_length(nonce) ≠ 16 | Erreur capturée → REJECTED_INVALID_SIZE | Log error + rollback | Non |
| PostgreSQL trigger exception | UPDATE/DELETE sur timestamp_tokens | Exception append-only: UPDATE and DELETE operations are forbidden | Log error | Non |
7. Impacts sécurité¶
Risques et mitigations¶
| Risque | Mitigation | INV/CA |
|---|---|---|
| Timing attack sur comparaison nonce | crypto.timingSafeEqual + protocole statistique TC-264-07 | INV-264-04 |
| PRNG non cryptographique (Math.random) | Vérification statique CI : grep/AST absence Math.random() dans chemin nonce | INV-264-01 |
| Rejeu nonce (replay attack) | Contrainte UNIQUE DB + vérification applicative pré-insert | INV-264-05, INV-264-07 |
| Race condition insertion concurrente | Index UNIQUE PostgreSQL = atomicité garantie par le moteur | INV-264-05 |
| Fuite nonce en mémoire | Nonce transitoire en mémoire (Buffer), pas de persistence en clair ni en log | INV-264-envelope |
| Contournement immutabilité | Trigger PostgreSQL trg_timestamp_token_immutable (hérité PD-55) | INV-264-08 |
| Corruption nonce par conversion de type | Colonne BYTEA + CHECK octet_length = 16 + TypeORM Buffer | CA-264-08 |
Journalisation¶
- Chaque rejet nonce → log structuré
warnavec :{ event, reason, batchId, timestamp } - Chaque acceptation nonce → log
infoavec :{ event: 'NONCE_ACCEPTED', batchId, nonceHash }(hash du nonce, pas le nonce brut) - Aucun nonce en clair dans les logs (uniquement hash SHA-256 tronqué pour corrélation)
Conformité¶
- RFC 3161 §2.2 : nonce obligatoire dans
TimeStampReq→ INV-264-01 - RFC 3161 §2.4.2 : vérification nonce dans
TimeStampResp→ INV-264-02, INV-264-03 - RFC 3161 §4.6 : protection anti-rejeu → INV-264-05, INV-264-07
8. Hypothèses techniques¶
| ID | Hypothèse | Impact si faux |
|---|---|---|
| H-01 | Aucun token de production existant avec nonce VARCHAR en base de données de production | Si tokens existants : la migration Phase A doit convertir correctement hex→binary ; le nonce de remplissage (0x00...00) est traçable mais ne respecte pas CSPRNG → acceptable car tokens pré-PD-264 |
| H-02 | Le trigger timestamp_token_immutable() est type-agnostic (RAISE EXCEPTION inconditionnel) | Si le trigger inspectait les types de colonnes : la migration BYTEA casserait le trigger → vérifié : le trigger est bien inconditionnel |
| H-03 | Le worker de réconciliation PD-55 ne fait pas d'hypothèse sur le format du nonce (VARCHAR vs BYTEA) | Si le worker lit/compare le nonce en format spécifique : la migration casserait la réconciliation → à vérifier dans le code du worker |
| H-04 | crypto.randomBytes(16) est disponible dans tous les environnements d'exécution (Node.js ≥ 14) | Si CSPRNG indisponible : le système refuse de démarrer (fail-closed natif Node.js) |
| H-05 | crypto.timingSafeEqual est disponible dans tous les environnements d'exécution (Node.js ≥ 6) | Si indisponible : fallback interdit, fail-closed |
| H-06 | Le timeout de l'appel QTSA HTTP est géré par le client QTSA existant (hors périmètre PD-264, réponse R08) | Si pas de timeout : REQUEST_EMITTED peut rester indéfiniment → timeout BullMQ job comme filet de sécurité |
| H-07 | Les fixtures mock TSA RFC sont conformes ASN.1/DER par construction (mock déterministe) | Si fixture invalide : TC-264-14 détectera le problème avec openssl ts -verify |
| H-08 | L'index UNIQUE sur nonce BYTEA est supporté par PostgreSQL sans limitation de taille (16 octets bien sous les 2704 octets max d'un index btree) | Risque nul pour 16 octets |
9. Points de vigilance (risques, dette, pièges)¶
Risques techniques¶
-
Migration sur données existantes : Si des tokens PD-55 existent avec
nonce = NULL, la Phase A de migration les remplit avec0x00...00. Ces tokens sont identifiables mais ne sont pas conformes INV-264-01 (pas de CSPRNG). C'est acceptable car ils sont antérieurs au contrat PD-264. Le CHECK constraintoctet_length = 16sera respecté. -
Désactivation temporaire du trigger : La Phase A de migration nécessite un UPDATE (pour convertir les données), ce qui est bloqué par
trg_timestamp_token_immutable. La migration doit : (1)DROP TRIGGER trg_timestamp_token_immutable, (2) UPDATE, (3)CREATE TRIGGER trg_timestamp_token_immutable. Ceci est fait dans une seule transaction de migration. Risque : si la migration échoue entre drop et recreate → rollback automatique par PostgreSQL. -
Concurrence TC-264-06 : Le test de concurrence dépend d'un PostgreSQL réel (pas de mock). Nécessite un environnement de test d'intégration avec base réelle.
-
Faux positifs TC-264-07 : Le protocole statistique peut échouer sur infra bruyante (CI partagée). Convention : relance unique sur infra stable avant escalade.
-
TC-264-14 dépendance openssl : La version openssl doit supporter les OID et formats utilisés par la TSA mock. Vérifier la version en CI.
Dette technique identifiée¶
- Le
BatchTimestampProcessorest actuellement un placeholder (QTSA_UNREACHABLE). PD-264 active le nonce mais le client QTSA complet reste hors périmètre (spec §4 exclusions). Les tests utilisent un mock TSA conforme RFC. - L'état
RESPONSE_REJECTEDest in-memory (pas de tablerejected_responses). Si un besoin futur de traçabilité des rejets émerge, une table dédiée sera nécessaire.
Pièges¶
- Ne pas confondre
Buffer.equals()etcrypto.timingSafeEqual():Buffer.equals()n'est PAS en temps constant. Utiliser exclusivementtimingSafeEqual. - Ne pas logger le nonce en clair : Logger uniquement le hash SHA-256 tronqué.
- Ne pas oublier le
2>&1pour les appels openssl en CI (capture stderr). - Ne pas utiliser
Math.random()même pour des IDs de corrélation (learning PD-63 :crypto.randomUUID()obligatoire).
10. Hors périmètre¶
| Élément | Justification |
|---|---|
| Client QTSA complet (appel HTTP TSA externe) | Spec §4 : hors périmètre PD-264, le mock TSA suffit pour les tests |
| Rotation/révocation certificats TSA | Spec §4 : périmètre PD-39/PD-55 |
| Intégration d'une nouvelle TSA externe | Spec §4 : hors périmètre |
| Changement de rétention des jetons (10 ans) | Spec §4 : défini PD-55 |
| Timeout de l'appel QTSA HTTP | Spec §4 : géré par le client QTSA (H-06) ; timeout BullMQ comme filet |
| Table de traçabilité des rejets | RESPONSE_REJECTED est in-memory ; traçabilité assurée par les logs structurés |
| Prolog formellement vérifiable en CI automatique | TC-264-15 est une vérification manuelle/semi-automatique ; l'intégration CI Prolog est hors périmètre |
11. Contraintes techniques¶
Dépendances inter-PD¶
| Story | Statut | Nature de la dépendance |
|---|---|---|
| PD-55 | DONE | Module TSA, entités, trigger immutabilité, worker réconciliation |
| PD-39 | DONE | Architecture TSA, entities, enums, constants |
| PD-237 | DONE | Module Merkle persistence (merkle_trees, merkle_leaves) |
Framework de test¶
- Runner : Jest (standard projet, confirmé par
*.spec.tsexistants) - Tests d'intégration : PostgreSQL réel requis pour TC-264-06 (concurrence), TC-264-09 (trigger), TC-264-19 (fault injection)
- Tests unitaires : mocks TypeORM Repository
- Tests sécurité (TC-264-07) : statistiques via bibliothèque npm (ex:
simple-statisticsou implémentation inline Welch t-test) - Environnement CI :
DATABASE_URL,CI=true,NODE_ENV=test
Compatibilité ESM/CJS¶
- Dépendances ESM-only identifiées : aucune (le module TSA utilise des packages CJS standards)
- Runner : Jest (CJS compatible)
12. Checklist INV/CA pré-Gate 5¶
| Invariant/CA | Tâche couvrant | Test couvrant |
|---|---|---|
| INV-264-01 | TASK-3 (NonceService.generate) + TASK-4 (validateRequestNonce) | TC-264-01, TC-264-08 |
| INV-264-02 | TASK-4 (validateResponseNonce — check présence) | TC-264-02, TC-264-04 |
| INV-264-03 | TASK-3 (NonceService.compareEqual) + TASK-4 (validateResponseNonce) | TC-264-03, TC-264-04 |
| INV-264-04 | TASK-3 (NonceService.compareEqual — timingSafeEqual) | TC-264-07 |
| INV-264-05 | TASK-1 (UNIQUE index) + TASK-3 (checkUniqueness) | TC-264-05, TC-264-06 |
| INV-264-06 | TASK-5 (séquence processor : validation avant INSERT) | TC-264-02, TC-264-03, TC-264-05 |
| INV-264-07 | TASK-4 (rejet strict avec motif) | TC-264-02, TC-264-03, TC-264-05 |
| INV-264-08 | Hérité (trigger PD-55) | TC-264-09 |
| INV-264-09 | TASK-5 (validation DER héritée) + TASK-6 (fixture DER réelle) | TC-264-04, TC-264-14 |
| INV-264-10 | TASK-6 (TC openssl ts -verify) | TC-264-14 |
| INV-264-11 | TASK-3 + TASK-4 (composition) | TC-264-04, TC-264-05 |
| INV-264-12 | TASK-4 (transitions interdites) | TC-264-11 |
| INV-264-13 | TASK-5 (transaction DB + post-commit async) | TC-264-19 |
| INV-264-envelope | N/A | TC-264-18 |
| CA-264-01 | TASK-4 (validateRequestNonce) | TC-264-01 |
| CA-264-02 | TASK-4 (validateResponseNonce — ABSENT) | TC-264-02 |
| CA-264-03 | TASK-3 (compareEqual) | TC-264-03 |
| CA-264-04 | TASK-4 + TASK-5 (flux complet) | TC-264-04 |
| CA-264-05 | TASK-1 (UNIQUE) + TASK-3 (checkUniqueness) | TC-264-05, TC-264-06 |
| CA-264-06 | TASK-5 (séquence processor) | TC-264-02, TC-264-03, TC-264-05, TC-264-19 |
| CA-264-07 | TASK-5 (séquence processor) | TC-264-02, TC-264-03, TC-264-05, TC-264-13, TC-264-19 |
| CA-264-08 | TASK-1 (BYTEA + CHECK) + TASK-2 (entity Buffer) | TC-264-04 |
| CA-264-09 | TASK-5 (hérité) + TASK-6 (openssl) | TC-264-14 |
| CA-264-10 | TASK-7 (Prolog) | TC-264-15 |
| CA-264-11 | TASK-6 (mock TSA CI) | TC-264-16 |
| CA-264-12 | TASK-6 (nightly) | TC-264-17 |
Couverture : 14/14 INV couverts, 12/12 CA couverts. Aucune ligne vide.