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