Aller au contenu

PD-277 — Plan d'implémentation

1. Découpage en composants

C1 — Migration DDL (AddNonceAndCertificateColumns)

Responsabilité : Ajouter les 3 colonnes contractuelles à vault_secure.legal_rekey via TypeORM migration.

Colonne Type Contrainte Default
used_nonces JSONB NOT NULL '[]'::jsonb
owner_certificate_id VARCHAR(255) NOT NULL '' (transitoire, cf. H-277-T01, contractualisé §6.1 DDL)
recipient_certificate_id VARCHAR(255) NOT NULL '' (transitoire, cf. H-277-T01, contractualisé §6.1 DDL)
  • Up : ALTER TABLE vault_secure.legal_rekey ADD COLUMN ...
  • Down : ALTER TABLE vault_secure.legal_rekey DROP COLUMN ...

Justification DEFAULT transitoire : Le constat majeur résiduel de Gate 3 (constat 2) identifie que NOT NULL sans DEFAULT échoue si des LegalReKey pré-existants existent. On ajoute un DEFAULT '' pour les certificats et DEFAULT '[]'::jsonb pour les nonces. Les ReKeys pré-existants auront des certificats vides, ce qui est cohérent car ils n'ont jamais eu de binding PKI. Toute opération ultérieure sur ces ReKeys échouera en fail-closed si les certificats sont invalides (INV-277-04).

Contractualisation du DEFAULT '' (décision de plan autonome) : Le DEFAULT '' est une décision d'implémentation du plan, pas une exigence de la spécification. La spec §6 impose NOT NULL sur les colonnes certificats mais ne traite pas le cas des enregistrements pré-existants (hors de son périmètre fonctionnel). Le plan introduit DEFAULT '' comme mécanisme de compatibilité de migration (ALTER TABLE sur une table non vide exige un DEFAULT pour les colonnes NOT NULL). Cette décision est : - Justifiée par H-277-T01 (LegalReKey pré-existants possibles) - Protégée par le contrôle fail-closed en F2 étape 3b (ReKeys avec certificats vides → rejet) - Temporaire : le DEFAULT sera supprimé lors du backfill (story séparée) - Non contradictoire avec la spec : la spec impose NOT NULL (respecté), le plan ajoute DEFAULT '' pour la migration (niveau d'implémentation, pas de spécification)

C2 — Extension entité LegalReKey

Responsabilité : Ajouter les 3 propriétés TypeORM à l'entité LegalReKey.

Propriété Decorator Options
usedNonces @Column('jsonb', { name: 'used_nonces', default: () => "'[]'" }) string[]
ownerCertificateId @Column('varchar', { name: 'owner_certificate_id', length: 255 }) string
recipientCertificateId @Column('varchar', { name: 'recipient_certificate_id', length: 255 }) string

C3 — Contrôle anti-rejeu nonce dans LegalReKeyManagerService

Responsabilité : Implémenter la vérification et persistance atomique du nonce dans reEncrypt().

Mécanisme : 1. Valider le format nonce (UUID v4 lowercase ASCII, 36 chars) — fail-closed. 2. Dans une transaction SERIALIZABLE : a. Charger le LegalReKey avec verrou. b. Vérifier que le nonce n'est pas dans used_nonces (opérateur JSONB @>). c. Insérer le nonce dans used_nonces (opérateur JSONB ||). d. Appeler preService.reEncrypt(). 3. Rollback complet si une étape échoue. 4. Retourner le résultat seulement après commit.

Nouveau méthode : reEncryptWithNonce(legalReKeyId: string, nonce: string, capsule: PreCapsuleArtefact, kfragIndex: number): Promise<PreCFragArtefact>

C4 — Contrôle PKI certificate binding dans LegalReKeyManagerService

Responsabilité : Enrichir generateLegalReKey() pour résoudre et persister les certificats PKI.

Mécanisme : 1. Après vérification bobIdentity (étape existante 4), extraire les IDs certificats du résultat TSP. 2. Valider que ownerCertificateId et recipientCertificateId sont non vides et non nuls. 3. Valider que les certificats sont valides (non expirés, non révoqués, compatibles avec le mandat). Si le TspVerificationResult indique un statut invalide (expired, revoked) ou une incompatibilité avec le mandat courant → rejet PRE_CERTIFICATE_BINDING_FAILED. En contexte stub, le stub retourne toujours des certificats valides sauf configuration de test explicite (TC-ERR-06, TC-NEG-05). 4. Les persister dans le LegalReKey à la création (immuables après).

C5 — Garde d'immuabilité certificats

Responsabilité : Empêcher toute modification de owner_certificate_id et recipient_certificate_id après création.

Code contract : Module pd277-rekey-repository (fichier src/modules/legal-pre/repositories/legal-rekey.repository.ts).

Mécanisme applicatif : dans LegalReKeyRepository, toute méthode updateStatus() ne doit jamais toucher les champs certificats. Ajouter une vérification explicite dans le service si un DTO de mise à jour contient ces champs → rejet fail-closed.

C6 — Codes d'erreur PD-277

Responsabilité : Ajouter les codes d'erreur spécifiques à PD-277 dans legal-pre.exception.ts.

Code Constante HTTP Description
ERR-NONCE-MISSING ERR_NONCE_MISSING 400 Nonce absent de la requête
ERR-NONCE-FORMAT ERR_NONCE_FORMAT 400 Nonce hors format UUID v4 lowercase
PRE_NONCE_REPLAY_DETECTED ERR_NONCE_REPLAY 409 Nonce déjà utilisé pour ce LegalReKey
PRE_CERTIFICATE_BINDING_FAILED ERR_CERTIFICATE_BINDING 400 Certificat absent/invalide/incompatible
ERR-PERSISTENCE-CONTROL ERR_PERSISTENCE_CONTROL 500 Échec persistance données de contrôle

C7 — Extension stubs TSP pour certificats

Responsabilité : Enrichir TspVerifierStub pour retourner des certificateId dans le résultat de vérification, permettant le binding PKI en contexte de test.

Extension de TspVerificationResult : - ownerCertificateId?: string - recipientCertificateId?: string

C8 — Régénération faits Prolog

Responsabilité : Vérifier que extract-facts.py (dans ProbatioVault-doc) détecte les nouvelles colonnes et génère les faits canoniques attendus par checks 23/24.

Faits attendus :

entity_column(legal_re_key, used_nonces, jsonb).
entity_column(legal_re_key, owner_certificate_id, varchar).
entity_column(legal_re_key, recipient_certificate_id, varchar).

extract-facts.py utilise un EntityExtractor qui parse les décorateurs @Column dans les fichiers .entity.ts. L'ajout des 3 @Column dans C2 suffit pour que les faits soient automatiquement générés. Aucune modification de extract-facts.py requise — vérification uniquement.

C9 — Tests unitaires et d'intégration

Responsabilité : Couvrir les scénarios TC-NOM-01 à TC-NOM-05, TC-ERR-01 à TC-ERR-10, TC-INV-03/05/06/08, TC-NEG-01 à TC-NEG-06, TC-NR-01 à TC-NR-04.


2. Flux techniques

F1 — generateReKey avec PKI binding (modifié)

Client → LegalPreController.activateLegalAccess()
  → LegalPreOrchestratorService.activateLegalAccess()
    → LegalReKeyManagerService.generateLegalReKey()
      1. Vérifier contextId (INV-81-12)
      2. Vérifier TTL (ERR-81-09)
      3. Vérifier scopeDocumentIds non vide
      4. verifyBobIdentity(bobPublicKey) → tspResult [existant]
      ──── NOUVEAU PD-277 ────
      5. Extraire ownerCertificateId depuis tspResult.certificateChainRef
      6. Extraire recipientCertificateId depuis tspResult (bob certificate)
      7. Valider non-nullité des 2 IDs → sinon PRE_CERTIFICATE_BINDING_FAILED
      ──── FIN NOUVEAU ────
      8. preService.generateReKey() [existant]
      9. encryptKfrags() [existant]
      10. Persister LegalReKey AVEC ownerCertificateId + recipientCertificateId + usedNonces=[]
      11. Émettre événement probatif [existant]

F2 — reEncrypt avec anti-rejeu nonce (nouveau)

Client → LegalPreController (endpoint existant, aucune modification controller)
  → LegalPreOrchestratorService (routage existant)
    → LegalReKeyManagerService.reEncryptWithNonce()
    1. Valider format nonce (UUID v4 lowercase, 36 chars)
       → ERR-NONCE-MISSING si absent
       → ERR-NONCE-FORMAT si hors format
    2. Ouvrir transaction SERIALIZABLE
    3. Charger LegalReKey (le contrôle de statut ACTIVE est hérité de PD-81, hors scope PD-277)
    3b. Vérifier binding PKI valide : ownerCertificateId et recipientCertificateId
        non vides et non nuls → sinon PRE_CERTIFICATE_BINDING_FAILED (fail-closed)
        Ceci protège contre les ReKeys hérités (pré-PD-277) qui ont des certificats
        vides (DEFAULT ''). Ces ReKeys ne peuvent PAS être utilisés pour reEncrypt.
    4. Vérifier nonce ∉ used_nonces (JSONB @> operator)
       → PRE_NONCE_REPLAY_DETECTED si déjà présent
    5. UPDATE used_nonces = used_nonces || '["<nonce>"]'::jsonb
    6. Charger kfrags via findOneWithKfrags()
    7. Décrypter kfrags (decryptKfrags)
    8. preService.reEncrypt({ reKey, capsule, kfragIndex, contextId })
    9. COMMIT
    10. Retourner PreCFragArtefact
    ── Si erreur à 4-8 → ROLLBACK complet, aucun nonce persisté

F3 — Régénération faits Prolog

CI/CD Pipeline (extract-facts.py)
  → EntityExtractor parse legal-rekey.entity.ts
    → Détecte @Column('jsonb', { name: 'used_nonces' })
    → Détecte @Column('varchar', { name: 'owner_certificate_id' })
    → Détecte @Column('varchar', { name: 'recipient_certificate_id' })
  → Génère _generated-facts.pl avec les 3 entity_column
  → swipl charge _generated-facts.pl + pv_pre_compliance.pl
  → run_audit.
    → CHECK 23 → entity_column(legal_re_key, used_nonces, _) → OK
    → CHECK 24 → entity_column(legal_re_key, owner_certificate_id, _)
                + entity_column(legal_re_key, recipient_certificate_id, _) → OK
  → 24/24 bloquants OK

F4 — Migration up/down

Up:
  ALTER TABLE vault_secure.legal_rekey
    ADD COLUMN used_nonces JSONB NOT NULL DEFAULT '[]'::jsonb,
    ADD COLUMN owner_certificate_id VARCHAR(255) NOT NULL DEFAULT '',
    ADD COLUMN recipient_certificate_id VARCHAR(255) NOT NULL DEFAULT '';

Down:
  ALTER TABLE vault_secure.legal_rekey
    DROP COLUMN used_nonces,
    DROP COLUMN owner_certificate_id,
    DROP COLUMN recipient_certificate_id;

Diagrammes Mermaid

Graphe de dépendances des composants

graph TD
    C1["C1 — Migration DDL<br/>AddNonceAndCertificateColumns"]
    C2["C2 — Extension entité<br/>LegalReKey"]
    C3["C3 — Contrôle anti-rejeu nonce<br/>LegalReKeyManagerService"]
    C4["C4 — Contrôle PKI certificate binding<br/>LegalReKeyManagerService"]
    C5["C5 — Garde immuabilité certificats<br/>LegalReKeyRepository"]
    C6["C6 — Codes d'erreur PD-277<br/>legal-pre.exception.ts"]
    C7["C7 — Extension stubs TSP<br/>TspVerifierStub"]
    C8["C8 — Régénération faits Prolog<br/>extract-facts.py"]
    C9["C9 — Tests unitaires et intégration"]

    C1 --> C2
    C2 --> C3
    C2 --> C4
    C2 --> C5
    C2 --> C8
    C6 --> C3
    C6 --> C4
    C7 --> C4
    C3 --> C9
    C4 --> C9
    C5 --> C9
    C8 --> C9

    subgraph "Module legal-pre"
        C3
        C4
        C5
        C6
    end

    subgraph "Entité / Migration"
        C1
        C2
    end

    subgraph "Externe"
        C7
        C8
    end

    style C1 fill:#e8f4fd,stroke:#1a73e8
    style C2 fill:#e8f4fd,stroke:#1a73e8
    style C3 fill:#fce8e6,stroke:#d93025
    style C4 fill:#fce8e6,stroke:#d93025
    style C5 fill:#fce8e6,stroke:#d93025
    style C6 fill:#fce8e6,stroke:#d93025
    style C7 fill:#fef7e0,stroke:#f9ab00
    style C8 fill:#fef7e0,stroke:#f9ab00
    style C9 fill:#e6f4ea,stroke:#137333

Diagramme de séquence — F1 : generateReKey avec PKI binding

sequenceDiagram
    participant Client
    participant Controller as LegalPreController
    participant Orch as LegalPreOrchestratorService
    participant Manager as LegalReKeyManagerService
    participant TSP as TspVerifier (stub)
    participant PRE as PreService (PD-41)
    participant DB as PostgreSQL

    Client->>Controller: activateLegalAccess()
    Controller->>Orch: activateLegalAccess()
    Orch->>Manager: generateLegalReKey()

    Manager->>Manager: 1. Vérifier contextId (INV-81-12)
    Manager->>Manager: 2. Vérifier TTL (ERR-81-09)
    Manager->>Manager: 3. Vérifier scopeDocumentIds
    Manager->>TSP: 4. verifyBobIdentity(bobPublicKey)
    TSP-->>Manager: tspResult

    rect rgb(255, 240, 230)
        Note over Manager: NOUVEAU PD-277
        Manager->>Manager: 5. Extraire ownerCertificateId
        Manager->>Manager: 6. Extraire recipientCertificateId
        Manager->>Manager: 7. Valider non-nullité
        alt Certificat absent/invalide
            Manager-->>Orch: PRE_CERTIFICATE_BINDING_FAILED
            Orch-->>Controller: 400
            Controller-->>Client: erreur
        end
    end

    Manager->>PRE: 8. generateReKey()
    PRE-->>Manager: reKey + kfrags
    Manager->>Manager: 9. encryptKfrags()
    Manager->>DB: 10. Persister LegalReKey<br/>+ ownerCertificateId<br/>+ recipientCertificateId<br/>+ usedNonces=[]
    DB-->>Manager: OK
    Manager->>Manager: 11. Émettre événement probatif
    Manager-->>Orch: LegalReKey
    Orch-->>Controller: résultat
    Controller-->>Client: 200 OK

Diagramme de séquence — F2 : reEncrypt avec anti-rejeu nonce

sequenceDiagram
    participant Client
    participant Controller as LegalPreController
    participant Orch as LegalPreOrchestratorService
    participant Manager as LegalReKeyManagerService
    participant PRE as PreService (PD-41)
    participant DB as PostgreSQL

    Client->>Controller: reEncrypt(nonce, capsule, kfragIndex)
    Controller->>Orch: routage existant
    Orch->>Manager: reEncryptWithNonce()

    Manager->>Manager: 1. Valider format nonce (UUID v4)
    alt Nonce absent
        Manager-->>Orch: ERR-NONCE-MISSING (400)
    end
    alt Nonce hors format
        Manager-->>Orch: ERR-NONCE-FORMAT (400)
    end

    rect rgb(230, 240, 255)
        Note over Manager,DB: Transaction SERIALIZABLE
        Manager->>DB: 2. BEGIN SERIALIZABLE
        Manager->>DB: 3. SELECT LegalReKey (verrou)
        DB-->>Manager: LegalReKey

        Manager->>Manager: 3b. Vérifier binding PKI valide<br/>(certificats non vides)
        alt Certificats vides (legacy)
            Manager->>DB: ROLLBACK
            Manager-->>Orch: PRE_CERTIFICATE_BINDING_FAILED (400)
        end

        Manager->>DB: 4. Vérifier nonce ∉ used_nonces (@>)
        alt Nonce déjà utilisé
            Manager->>DB: ROLLBACK
            Manager-->>Orch: PRE_NONCE_REPLAY_DETECTED (409)
        end

        Manager->>DB: 5. UPDATE used_nonces = used_nonces || nonce
        Manager->>Manager: 6. findOneWithKfrags()
        Manager->>Manager: 7. decryptKfrags()
        Manager->>PRE: 8. reEncrypt(reKey, capsule, kfragIndex)
        PRE-->>Manager: PreCFragArtefact
        Manager->>DB: 9. COMMIT
    end

    Manager-->>Orch: 10. PreCFragArtefact
    Orch-->>Controller: résultat
    Controller-->>Client: 200 OK

    Note over Manager,DB: Si erreur étapes 4-8 → ROLLBACK<br/>Aucun nonce persisté

3. Mapping invariants → mécanismes

Invariant ID Exigence Mécanisme Composant Observable Risque
INV-277-01-fail-closed Toute anomalie → rejet explicite Validation nonce format + vérification certificats + try/catch avec rollback sur transaction C3, C4, C6 Code erreur déterministe retourné (ERR-NONCE-, PRE_) Faible — pattern déjà utilisé dans PD-81 (INV-81-11)
INV-277-02-nonce-unique Nonce déjà utilisé → rejet Vérification JSONB @> dans transaction SERIALIZABLE avant insertion C3 2e appel même nonce → PRE_NONCE_REPLAY_DETECTED Moyen — concurrence à tester (TC-NEG-02)
INV-277-03-nonce-persist-before-success Nonce persisté avant succès API INSERT nonce dans used_nonces AVANT appel reEncrypt, le tout dans même transaction. COMMIT uniquement après reEncrypt réussi C3 Trace nonce visible en DB dès succès HTTP Faible — séquence linéaire dans transaction
INV-277-04-pki-binding-mandatory Création interdite sans certificats valides Validation non-nullité + non-vacuité + validité (non expiré, non révoqué, compatible mandat) de ownerCertificateId et recipientCertificateId. Contrôle fail-closed dans reEncryptWithNonce pour les ReKeys hérités (certificats vides → rejet PRE_CERTIFICATE_BINDING_FAILED) C4, C3, C7 generateReKey et reEncryptWithNonce échouent avec PRE_CERTIFICATE_BINDING_FAILED si certificats absents/invalides/vides Faible — guard applicatif + contrôle fail-closed legacy
INV-277-05-binding-immutability Certificats immuables post-création Mécanisme applicatif : updateStatus() ne touche jamais les champs certificats. Vérification explicite dans service si tentative de modification C5 TC-ERR-08 : tentative update → rejet, valeurs inchangées Faible — protection applicatif, accès DB restreint en contexte crypto-proof
INV-277-06-envelope-encryption Chiffrement at-rest hérité infra Pas de code nouveau. Validé par configuration PostgreSQL TDE / Vault transit Infra Preuves de configuration dans dossier de conformité Néant — hors scope code
INV-277-07-audit-traceability Faits Prolog ⟷ état réel Ajout des 3 @Column dans entity → extract-facts.py génère automatiquement les entity_column C2, C8 _generated-facts.pl contient les 3 faits, run_audit. → 24/24 Faible — extracteur existant éprouvé
INV-277-08-state-transitions Aucun nouveau StatusEnum Aucune modification de LegalReKeyStatus enum Vérification Diff enum avant/après identique Néant — pas de modification

4. Mapping critères d'acceptation → mécanismes

Critère ID Mécanisme(s) Composant Observable Risque
CA-277-01 Extraction certificateId depuis TSP result + persistance dans LegalReKey C4, C7 Lecture DB : owner_certificate_id et recipient_certificate_id non nuls après création Faible
CA-277-02 Guard applicatif : if (!ownerCertificateId || !recipientCertificateId) throw PRE_CERTIFICATE_BINDING_FAILED C4, C6 generateReKey retourne erreur explicite Faible
CA-277-03 Vérification JSONB @> + insertion atomique dans transaction SERIALIZABLE C3 1er appel succès + nonce dans used_nonces. 2e appel rejeté PRE_NONCE_REPLAY_DETECTED Moyen — sérialisation à tester sous charge
CA-277-04 Pattern try/catch + rollback systématique. Aucun succès partiel possible C3, C4, C6 Toute erreur → code erreur déterministe, pas de side-effect Faible
CA-277-05 @Column decorators dans entity → extract-facts.py → _generated-facts.pl C2, C8 Fichier contient entity_column pour les 3 champs Faible
CA-277-06 Hors scope code — preuves infra Infra Documentation configuration TDE/Vault transit Néant
CA-277-07 Régénération facts + exécution run_audit. C8 24/24 bloquants OK, checks 23+24 OK, pas de régression 1..22 Faible
CA-277-08 Aucune modification de LegalReKeyStatus/LegalMandateStatus/LegalAccessEventType Vérification Diff enums = 0 changement Néant
CA-277-09 Migration TypeORM up/down avec code succès C1 npm run typeorm migration:run et migration:revert OK Faible

5. Mapping tests (TC-*) → mécanismes + observables

Test ID Référence spec Mécanisme(s) Point(s) d'observation Niveau de test visé
TC-NOM-01 INV-277-04, INV-277-05, CA-277-01 Extraction certIds depuis TSP + persistance LegalReKey owner_certificate_id et recipient_certificate_id en DB, non modifiables Integration
TC-NOM-02 INV-277-02, INV-277-03, CA-277-03 Transaction SERIALIZABLE : vérif nonce + insert nonce + reEncrypt 1er appel OK + nonce dans used_nonces, 2e appel rejeté Integration
TC-NOM-03 INV-277-07, CA-277-05 Ajout @Column → extract-facts.py → _generated-facts.pl Présence des 3 entity_column dans le fichier Unit (vérification fichier)
TC-NOM-04 INV-277-07, CA-277-07 Exécution run_audit. sur facts régénérés 24/24 OK, checks 23+24 OK Integration (CI)
TC-NOM-05 CA-277-09 Migration TypeORM up + down Schéma correct après up, restauré après down Integration
TC-ERR-01 INV-277-01, CA-277-04 Validation nonce : if (!nonce) throw ERR_NONCE_MISSING Code erreur ERR-NONCE-MISSING, used_nonces inchangé Unit
TC-ERR-02 INV-277-01, CA-277-04 Regex UUID v4 : /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/ Code erreur ERR-NONCE-FORMAT Unit
TC-ERR-03 INV-277-02, CA-277-03 Vérification JSONB @> dans transaction PRE_NONCE_REPLAY_DETECTED Unit/Integration
TC-ERR-04 INV-277-04, CA-277-02 Guard applicatif : ownerCertificateId absent → rejet PRE_CERTIFICATE_BINDING_FAILED, pas de LegalReKey créé Unit
TC-ERR-05 INV-277-04, CA-277-02 Guard applicatif : recipientCertificateId absent → rejet PRE_CERTIFICATE_BINDING_FAILED, pas de LegalReKey créé Unit
TC-ERR-06 INV-277-04, CA-277-02 Certificats incohérents avec mandat → rejet PRE_CERTIFICATE_BINDING_FAILED Unit
TC-ERR-07 INV-277-01, INV-277-03, CA-277-04 Injection erreur persistance → rollback ERR-PERSISTENCE-CONTROL, aucun side-effect Integration
TC-ERR-08 INV-277-01, INV-277-05, CA-277-04 Guard immuabilité : rejet si tentative modification certificats Valeurs DB inchangées après tentative Unit/Integration
TC-ERR-09 INV-277-07, CA-277-05 Facts non régénérés → audit incohérent Échec audit détectable Integration (CI)
TC-ERR-10 CA-277-07 Résultat < 24/24 → verdict non conforme Échec explicite audit Integration (CI)
TC-INV-03 INV-277-02, INV-277-03 Nonce déjà dans used_nonces → rejet, pas de duplication PRE_NONCE_REPLAY_DETECTED, used_nonces inchangé Integration
TC-INV-05 INV-277-04, CA-277-02 owner_certificate_id absent → rejet total, pas de création partielle Pas de LegalReKey en DB Integration
TC-INV-06 INV-277-04, INV-277-06, CA-277-02, CA-277-06 recipient_certificate_id absent → rejet + preuves config at-rest PRE_CERTIFICATE_BINDING_FAILED + documentation TDE Integration + Infra
TC-INV-08 INV-277-08, CA-277-08 Comparaison enums avant/après PD-277 Diff = 0 Unit (snapshot)
TC-NR-01 CA-277-07 Exécution run_audit. pré vs post PD-277 Checks 1..22 identiques avant/après Integration (CI)
TC-NR-02 Périmètre Aucun import/modification hors legal-pre et migration grep/review des fichiers modifiés Review
TC-NR-03 CA-277-09 Migration up/down répétable 2x up/down sans dérive schéma Integration
TC-NR-04 ECT-04 Suppression ReKey → suppression used_nonces avec l'entité Après destroyReKey : ReKey absent de la DB Integration
TC-NEG-01 INV-277-01 Regex UUID v4 stricte : rejette casse mixte, espaces, préfixes ERR-NONCE-FORMAT pour chaque variante Unit
TC-NEG-02 INV-277-02 Transaction SERIALIZABLE + verrouillage Au plus 1 succès, autres PRE_NONCE_REPLAY_DETECTED Integration (concurrent)
TC-NEG-03 H-277-03 Même nonce sur 2 LegalReKey différents Les 2 réussissent (périmètre par LegalReKey) Integration
TC-NEG-04 INV-277-05 Tentative UPDATE direct via repository Rejet fail-closed, valeurs inchangées Integration
TC-NEG-05 INV-277-04 Certificat expiré/révoqué/non autorisé PRE_CERTIFICATE_BINDING_FAILED Unit
TC-NEG-06 INV-277-07 Facts anciens réutilisés après changement Audit incohérent détectable Integration (CI)

6. Gestion des erreurs

Nouveaux codes d'erreur PD-277

Code Constante HTTP Déclencheur Observable
ERR-NONCE-MISSING ERR_NONCE_MISSING 400 reEncryptWithNonce() : nonce null/undefined/absent Réponse JSON { errorCode: 'ERR-NONCE-MISSING', message: '...' }
ERR-NONCE-FORMAT ERR_NONCE_FORMAT 400 reEncryptWithNonce() : nonce ne match pas UUID v4 lowercase regex Réponse JSON { errorCode: 'ERR-NONCE-FORMAT' }
PRE_NONCE_REPLAY_DETECTED ERR_NONCE_REPLAY 409 reEncryptWithNonce() : nonce ∈ used_nonces du LegalReKey Réponse JSON { errorCode: 'PRE_NONCE_REPLAY_DETECTED' }
PRE_CERTIFICATE_BINDING_FAILED ERR_CERTIFICATE_BINDING 400 generateLegalReKey() : certificat owner ou recipient absent/invalide Réponse JSON { errorCode: 'PRE_CERTIFICATE_BINDING_FAILED' }
ERR-PERSISTENCE-CONTROL ERR_PERSISTENCE_CONTROL 500 Transaction échoue en persistance nonces/binding Réponse JSON { errorCode: 'ERR-PERSISTENCE-CONTROL' }

Stratégie de rollback

Toutes les erreurs dans reEncryptWithNonce() déclenchent un ROLLBACK complet de la transaction SERIALIZABLE. Aucun nonce n'est ajouté à used_nonces en cas d'erreur. L'état pré-transaction est garanti restauré.

Pour generateLegalReKey(), l'erreur PRE_CERTIFICATE_BINDING_FAILED est levée AVANT le save() du LegalReKey, donc aucune persistance partielle.

Erreurs de sérialisation PostgreSQL (SQLSTATE 40001)

En cas d'erreur de sérialisation (SerializationFailureError), le comportement est fail-closed : l'erreur est propagée comme ERR-PERSISTENCE-CONTROL. Le client reçoit un HTTP 500 et peut retenter avec un nouveau nonce. Ce n'est PAS un retry automatique — le client génère un nouveau nonce via crypto.randomUUID() et réessaie.


7. Impacts sécurité

7.1 Anti-rejeu

Le mécanisme anti-rejeu protège contre : - Replay attack : un attaquant intercepte une requête reEncrypt valide et la rejoue. - Double-spend : une même capsule re-chiffrée deux fois avec le même nonce.

Mitigation : le nonce est vérifié et persisté atomiquement dans une transaction SERIALIZABLE, éliminant toute fenêtre de rejeu.

7.2 PKI certificate binding

Le binding certificat protège contre : - Certificate substitution : remplacement du certificat destinataire après émission du ReKey. - Unauthorized access : génération d'un ReKey vers un destinataire non autorisé.

Mitigation : les IDs certificats sont validés à la création (via TSP) et rendus immuables. Toute modification ultérieure est rejetée en fail-closed.

7.3 Nonce format

Le format UUID v4 lowercase impose : - 122 bits d'entropie (via crypto.randomUUID()). - Aucune prédictibilité (CSPRNG). - Format canonique sans normalisation ambiguë.

7.4 Journalisation

Les événements probatifs existants (PD-81 audit trail) couvrent déjà la traçabilité des opérations generateReKey. Pour reEncrypt, un événement supplémentaire pourra être ajouté si nécessaire (hors scope PD-277 car le focus est sur le contrôle de nonce, pas sur la traçabilité de chaque re-chiffrement).

7.5 Comparaisons sécuritaires

La vérification du nonce utilise l'opérateur JSONB PostgreSQL @> (côté DB, pas de comparaison applicative string), ce qui évite les timing attacks sur la comparaison de nonce.


8. Hypothèses techniques

ID Hypothèse Impact si faux
H-277-01 Le module cible est ProbatioVault-backend (NestJS + TypeORM + PostgreSQL). Spec invalide, réémettre.
H-277-02 MandateValidatorService reste la source de vérité pour validation PKI côté domaine légal. Revoir le mécanisme de résolution des certificats.
H-277-03 Le périmètre anti-rejeu est le LegalReKey (et non global). Un même nonce peut être utilisé sur 2 LegalReKey différents. Revoir la table d'unicité nonce (scope global = table séparée).
H-277-04 L'ajout des champs n'impose pas de refonte API externe. Les certificats sont résolus en interne (TSP stub), pas fournis par l'appelant. Extension d'API avec paramètres certificats à ajouter.
H-277-05 Les checks Prolog 23/24 sont dérivés automatiquement par extract-facts.py à partir des @Column de l'entité. Modification manuelle de extract-facts.py ou ajout de facts statiques.
H-277-T01 Des LegalReKey pré-existants peuvent exister en base (adresse le constat majeur Gate 3). Le DEFAULT transitoire '' pour les certificats est acceptable car ces ReKeys anciens n'ont jamais eu de binding PKI. Si le DEFAULT '' n'est pas acceptable, utiliser une migration en 2 étapes (ADD COLUMN nullable → UPDATE → ALTER NOT NULL).
H-277-T02 Le TspVerificationResult actuel peut être étendu pour retourner ownerCertificateId et recipientCertificateId sans casser l'interface existante (champs optionnels). Modification de l'interface ITspVerifier et tous ses implémenteurs.
H-277-T03 La méthode reEncrypt() de PreService (PD-41) ne gère PAS le nonce — le nonce est contrôlé au niveau LegalReKeyManagerService uniquement, avant l'appel à PreService. Si PreService doit aussi vérifier le nonce, modifier l'interface ReEncryptParams.
H-277-T04 La structure interne de used_nonces est un array de strings brutes UUID (["uuid1", "uuid2"]), pas d'objets avec métadonnées. Ce choix est cohérent avec le TTL aligné sur le LegalReKey (pas besoin de timestamp par nonce). Si métadonnées requises, modifier le format JSONB et les assertions de test.

9. Points de vigilance (risques, dette, pièges)

V1 — Concurrence SERIALIZABLE

Le choix de SERIALIZABLE pour la transaction anti-rejeu peut générer des erreurs de sérialisation (40001) sous forte charge concurrente. Le code doit propager ces erreurs comme ERR-PERSISTENCE-CONTROL, pas les cacher. Le test TC-NEG-02 doit vérifier ce comportement avec des requêtes réellement concurrentes (pas séquentielles).

V2 — Performance JSONB used_nonces

L'opérateur @> sur un array JSONB croissant peut dégrader les performances si un LegalReKey accumule des centaines de nonces. En pratique, le TTL de 30 jours max limite la durée de vie du ReKey. Pas d'index GIN nécessaire à ce stade — à surveiller en production.

V3 — Certificats dans le stub TSP

Le TspVerifierStub actuel retourne un résultat hardcodé. Pour PD-277, il doit être enrichi pour retourner des certificateId cohérents. Si les certificats ne sont pas extractibles du résultat TSP actuel, un mécanisme alternatif de résolution (via MandateValidatorService) peut être nécessaire.

V4 — Scope du nonce (H-277-03)

Le nonce est unique par LegalReKey, pas globalement. Cela signifie que le même nonce UUID peut être utilisé sur 2 LegalReKey différents sans conflit. Ce design est conforme à la spec (H-277-03) mais doit être documenté pour éviter toute confusion.

V5 — DEFAULT transitoire pour les certificats

Le DEFAULT '' pour owner_certificate_id et recipient_certificate_id est un compromis de migration contractualisé dans la spec §6.1. Les LegalReKey pré-existants avec certificats vides ne peuvent PAS être utilisés pour de nouvelles opérations : reEncryptWithNonce() vérifie explicitement que ownerCertificateId et recipientCertificateId sont non vides (étape 3b du flux F2). Si les certificats sont vides → rejet PRE_CERTIFICATE_BINDING_FAILED (fail-closed démontré). Ce DEFAULT devra être supprimé si une politique de backfill est définie ultérieurement.

V6 — Pas de modification de PreService (PD-41)

PD-277 ne modifie PAS pre.service.ts. Le contrôle anti-rejeu est ajouté au niveau LegalReKeyManagerService, en amont de l'appel à preService.reEncrypt(). C'est conforme au périmètre exclu de la spec : "Toute modification du service PRE bas niveau".

V7 — Rotation/révocation de certificats post-binding

La spec ne traite pas l'invalidation de certificats après la création du LegalReKey (constat 8 de Gate 3). Ce risque résiduel est accepté — la gestion du cycle de vie des certificats relèvera d'une story séparée. L'immuabilité du binding est le choix contractuel de PD-277.


10. Hors périmètre

  • 5 checks non bloquants : cron destruction, cron expiration, ETSI trusted list, blockchain anchoring, revocation propagation.
  • Modification de pre.service.ts / umbral.provider.ts : le contrôle nonce est au niveau legal-pre, pas au niveau PRE bas niveau.
  • Évolution fonctionnelle hors module legal-pre : aucune modification d'API, de contrôleur, ou de module tiers. Le nonce est introduit au niveau LegalReKeyManagerService.reEncryptWithNonce(), appelé depuis le flux orchestrateur existant — pas de nouveau endpoint controller.
  • Rotation/révocation de certificats post-binding : story séparée.
  • Purge séparée des nonces : les nonces sont détruits avec le LegalReKey.
  • Index GIN sur used_nonces : optimisation à évaluer en production si nécessaire.
  • Détection runtime de faits Prolog obsolètes : contrôle CI/CD, pas de mécanisme runtime (cf. constat 5 Gate 3).
  • Retry automatique sur erreur de sérialisation : le client génère un nouveau nonce et retente.

11. Contraintes techniques et dépendances inter-PD

Dépendance Story Statut Impact si absent
PreService.reEncrypt() PD-41 DONE Méthode appelée depuis reEncryptWithNonce() — interface stable
LegalReKeyManagerService.generateLegalReKey() PD-81 DONE Méthode enrichie par PD-277 (ajout binding PKI) — aucune modification de signature
TspVerifierStub PD-81 DONE (stub) Stub étendu avec certificateId — story réelle TSP à planifier
extract-facts.py (EntityExtractor) PD-189 DONE Extracteur Prolog existant — aucune modification requise
pv_pre_compliance.pl (checks 23/24) PD-189 DONE Règles Prolog existantes — PD-277 fournit les faits manquants
Contrôle statut ACTIVE sur LegalReKey PD-81 DONE Hérité de PD-81, hors scope PD-277

Mécanismes cross-module

Aucune modification d'autres modules. PD-277 est entièrement contenu dans le module legal-pre et ses migrations. Les interactions avec PreService (module crypto/pre) se font via l'interface existante reEncrypt() sans modification. Les interactions avec le stub TSP sont des extensions optionnelles de l'interface existante.