Aller au contenu

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_immutable toujours 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é warn avec : { event, reason, batchId, timestamp }
  • Chaque acceptation nonce → log info avec : { 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

  1. Migration sur données existantes : Si des tokens PD-55 existent avec nonce = NULL, la Phase A de migration les remplit avec 0x00...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 constraint octet_length = 16 sera respecté.

  2. 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.

  3. 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.

  4. Faux positifs TC-264-07 : Le protocole statistique peut échouer sur infra bruyante (CI partagée). Convention : relance unique sur infra stable avant escalade.

  5. 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 BatchTimestampProcessor est 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_REJECTED est in-memory (pas de table rejected_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() et crypto.timingSafeEqual() : Buffer.equals() n'est PAS en temps constant. Utiliser exclusivement timingSafeEqual.
  • Ne pas logger le nonce en clair : Logger uniquement le hash SHA-256 tronqué.
  • Ne pas oublier le 2>&1 pour 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.ts existants)
  • 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-statistics ou 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.