PD-85 — Plan d'implémentation¶
1. Découpage en composants¶
C1 — export-dto : DTOs de requête et de réponse¶
Responsabilité : Validation d'entrée (ComplaintFileRequestDto) et sérialisation de sortie (ComplaintFileResponseDto, ManifestDto, ChronologyDto, RejectedProofDto).
ComplaintFileRequestDto:proofIds: UUID[](1..500), validé parclass-validator(regex UUID v4,@ArrayMinSize(1),@ArrayMaxSize(500),@ArrayUnique()).ComplaintFileResponseDto: payload 200 complet (exportId,manifest,chronology,signedUrls,guideUrl,readmeVerification,rejectedProofs).ManifestDto:version,exportId,exportedAt,exportedBy,proofCount,proofs[],integrityHash.ChronologyDto:events[]triés partimestampnon décroissant.RejectedProofDto:proofId,reason.
C2 — export-exceptions : Codes d'erreur métier PD-85¶
Responsabilité : ExportException étendant HttpException avec codes machine-readable.
| Code | HTTP | Condition |
|---|---|---|
PAYLOAD_INVALID | 400 | proofIds absent/vide/format/doublons |
PREMIUM_REQUIRED | 403 | Plan FREE (réutilise FreemiumException) |
PROOF_NOT_FOUND | 404 | proofId inexistant ou non possédé (fail-fast) |
EXPORT_SIZE_EXCEEDED | 413 | Taille > 1 GB |
ALL_PROOFS_REJECTED | 422 | Toutes preuves invalides |
EXPORT_INTERNAL_ERROR | 500 | Incohérence interne |
C3 — export-validators : Validations invariants RFC par preuve¶
Responsabilité : Implémentation des 5 validations RFC (INV-02, INV-03, INV-09, INV-11, INV-12) sur chaque ProofEnvelope. Retourne {valid: boolean, reason?: string} par preuve.
FiveSectionsCompleteValidator: vérifie les 5 sections RFC présentes et non vides.ReKeyActiveValidator: vérifie qu'aucun ReKey actif n'existe sur la preuve.SecretExposureValidator: scan déterministe des patterns sensibles (DEK, ReKey fragments, mots de passe).EnvelopeSealValidator: vérifie la validité cryptographique du sceau HSM.OfflineMaterialValidator: vérifie la présence du matériel de vérification offline.
C4 — export-service : Service d'orchestration export¶
Responsabilité : Logique métier principale. Orchestre la chaîne complète : validation requête → contrôle plan → RLS ownership → validation invariants → calcul taille → génération manifest/chronology → URLs signées → audit WORM.
- Traitement synchrone (pas de queue/worker).
- Rejet partiel : les preuves invalides sont exclues du manifest et collectées dans
rejectedProofs. - Fail-fast sur 404 : la première preuve non possédée/inexistante interrompt le traitement.
- Audit : événement WORM émis pour TOUT résultat (200, 400, 403, 404, 413, 422).
C5 — export-manifest-builder : Construction manifest + chronology¶
Responsabilité : Assemblage du manifest.json (avec integrityHash SHA3-256 JCS) et du chronology.json (événements triés). Construction du README_VERIFICATION.txt.
- Canonicalisation via
json-canonicalize(RFC 8785) — réutilisation du service existanttsa/utils/canonical-json.utils.ts. - Hash SHA3-256 via
node:crypto(createHash('sha3-256')), pas SHA-256 (différent du TSA existant qui utilise SHA-256). - Champ
integrityHashexclu de la canonicalisation avant calcul du hash.
C6 — export-controller : Endpoint REST¶
Responsabilité : Remplacement du stub PD-84 ExportController par l'implémentation réelle. Endpoint POST /exports/complaint-file (nouvelle route, distincte des stubs folders/:id/exports/*).
- Guards :
OidcJwtAuthGuard(auth) +PremiumGuard(plan). - Rate limiting :
EXPORT_RATE_LIMIT_PER_HOUR(défaut 10, configurable). - Injection du
ExportService.
C7 — export-config : Configuration et constantes¶
Responsabilité : Variables d'environnement et constantes PD-85.
| Variable | Défaut | Validation |
|---|---|---|
EXPORT_SIGNED_URL_TTL_MIN | 15 | Joi.number().min(1).max(30) |
EXPORT_RATE_LIMIT_PER_HOUR | 10 | Joi.number().min(1).max(100) |
MAX_BACKEND_EXPORT_BYTES | 1073741824 (1 GB) | Joi.number().positive() |
MAX_PROOF_IDS | 500 | Joi.number().min(1).max(10000) |
C8 — export-audit : Extension audit WORM¶
Responsabilité : Ajout du type EXPORT_GENERATED à AuditActionType et émission des événements audit pour chaque appel (succès ET erreurs). Champs obligatoires : actorId, exportId, proofCount, result (état terminal), timestamp_utc, correlationId.
C9 — export-minor-evidence : Évidences B2C-mineurs¶
Responsabilité : Enrichissement conditionnel du payload export avec les évidences mineur/représentant légal (Mandate, Dual Validation, ReKey Lifecycle). Consultation des données existantes du module dual-validation et freemium.
C10 — export-tests : Tests unitaires et d'intégration¶
Responsabilité : Couverture complète des TC-* du cahier de tests.
2. Flux techniques¶
F1 — Export nominal complet (200, rejectedProofs=[])¶
Client → POST /exports/complaint-file {proofIds}
→ OidcJwtAuthGuard (auth)
→ PremiumGuard (plan != FREE → 403 sinon)
→ ExportController.createComplaintFile()
→ ExportService.execute(userId, proofIds)
1. Validation DTO (proofIds format, doublons → 400)
2. RLS ownership check (fail-fast → 404)
3. Chargement ProofEnvelopes depuis DB
4. Validation invariants RFC par preuve (5 validators)
→ Si toutes invalides → 422
→ Sinon → partition valides/rejetées
5. Calcul taille totale chiffrée → si > 1 GB → 413
6. ExportManifestBuilder.build(validProofs, userId, role)
→ manifest.json (JCS canonicalisé + SHA3-256 integrityHash)
→ chronology.json (events triés)
→ README_VERIFICATION.txt (asset statique)
7. S3PresignService.getSignedUrl() pour chaque document chiffré
→ TTL configurable (défaut 15 min, max 30 min)
8. Guide statique guideUrl (asset serveur)
9. Audit WORM (EXPORT_GENERATED, fail-closed)
→ Response 200 {exportId, manifest, chronology, signedUrls, guideUrl, readmeVerification, rejectedProofs=[]}
F2 — Export partiel avec rejets (200, rejectedProofs non vide)¶
Identique à F1, étapes 1-3, puis à l'étape 4 certaines preuves sont rejetées. Les preuves rejetées sont collectées avec motif, les valides continuent le flux F1 (étapes 5-9). proofCount = nombre de valides uniquement.
F3 — Cas mineur/représentant légal¶
À l'étape 6 du flux F1, ExportMinorEvidenceService est invoqué conditionnellement : - Si user.accountRole == LEGAL_GUARDIAN → exportedBy.role = LEGAL_GUARDIAN - Si dossier mineur actif → enrichissement avec évidences (Mandate si présent, Dual Validation si présent, ReKey Lifecycle si présent) - Aucune évidence non applicable n'est ajoutée (test TC-NOM-03)
2bis. Diagrammes Mermaid¶
Graphe de dépendances entre composants¶
graph TD
C6["C6 — export-controller<br/>(endpoint REST)"]
C4["C4 — export-service<br/>(orchestration)"]
C1["C1 — export-dto<br/>(DTOs)"]
C2["C2 — export-exceptions<br/>(erreurs métier)"]
C3["C3 — export-validators<br/>(validations RFC)"]
C5["C5 — export-manifest-builder<br/>(manifest + chronology)"]
C7["C7 — export-config<br/>(configuration)"]
C8["C8 — export-audit<br/>(audit WORM)"]
C9["C9 — export-minor-evidence<br/>(évidences B2C-mineurs)"]
C10["C10 — export-tests<br/>(tests)"]
C6 --> C4
C6 --> C1
C6 --> C7
C4 --> C1
C4 --> C2
C4 --> C3
C4 --> C5
C4 --> C7
C4 --> C8
C4 --> C9
C5 --> C7
C10 -.->|teste| C1
C10 -.->|teste| C2
C10 -.->|teste| C3
C10 -.->|teste| C4
C10 -.->|teste| C5
C10 -.->|teste| C6
C10 -.->|teste| C8
C10 -.->|teste| C9
EXT_S3["S3PresignService<br/>(PD-46)"]
EXT_AUDIT["AuditLogService<br/>(existant)"]
EXT_FREEMIUM["FreemiumModule<br/>(PD-84)"]
EXT_PROOF["ProofEnvelopeRepo<br/>(legal-pre)"]
EXT_DUAL["DualValidationRepo<br/>(dual-validation)"]
C4 --> EXT_S3
C8 --> EXT_AUDIT
C6 --> EXT_FREEMIUM
C4 --> EXT_PROOF
C9 --> EXT_DUAL
style C6 fill:#4a90d9,color:#fff
style C4 fill:#d94a4a,color:#fff
style C3 fill:#d9a44a,color:#fff
style C5 fill:#4ad98a,color:#fff
style EXT_S3 fill:#888,color:#fff
style EXT_AUDIT fill:#888,color:#fff
style EXT_FREEMIUM fill:#888,color:#fff
style EXT_PROOF fill:#888,color:#fff
style EXT_DUAL fill:#888,color:#fff Diagramme de séquence — Flux nominal F1 (export complet)¶
sequenceDiagram
participant Client
participant C6 as C6 — ExportController
participant Auth as OidcJwtAuthGuard
participant Plan as PremiumGuard (C6/PD-84)
participant C4 as C4 — ExportService
participant DB as ProofEnvelopeRepo (legal-pre)
participant C3 as C3 — Validators (x5)
participant C5 as C5 — ManifestBuilder
participant C9 as C9 — MinorEvidence
participant S3 as S3PresignService (PD-46)
participant C8 as C8 — ExportAudit
participant Audit as AuditLogService (existant)
Client->>C6: POST /exports/complaint-file {proofIds}
C6->>Auth: JWT validation
Auth-->>C6: userId
C6->>Plan: plan != FREE ?
Plan-->>C6: OK (ou 403)
C6->>C4: execute(userId, proofIds)
Note over C4: 1. Validation DTO (C1)
C4->>DB: findByIds(proofIds) WHERE user_id = userId
DB-->>C4: ProofEnvelopes (ou 404 fail-fast)
Note over C4: 4. Validation invariants RFC
loop Pour chaque preuve
C4->>C3: validate(envelope)
C3-->>C4: {valid, reason?}
end
Note over C4: Partition valides / rejetées
Note over C4: 5. Calcul taille (> 1 GB -> 413)
C4->>C9: enrichIfMinor(userId, validProofs)
C9-->>C4: évidences conditionnelles
C4->>C5: build(validProofs, userId, role, evidences)
C5-->>C4: manifest + chronology + README
loop Pour chaque document chiffré
C4->>S3: getSignedUrl(s3Key, ttl)
S3-->>C4: signedUrl
end
C4->>C8: emitExportAudit(exportId, result=SUCCESS)
C8->>Audit: logAsync(EXPORT_GENERATED, ...)
Audit-->>C8: OK (ou 500 fail-closed)
C8-->>C4: OK
C4-->>C6: {exportId, manifest, chronology, signedUrls, ...}
C6-->>Client: 200 ComplaintFileResponseDto 3. Mapping invariants → mécanismes¶
| Invariant ID | Exigence | Mécanisme | Composant | Observable | Risque |
|---|---|---|---|---|---|
| INV-02 | FiveSectionsComplete | FiveSectionsCompleteValidator vérifie 5 sections RFC non vides | C3 | Preuve rejetée avec motif FIVE_SECTIONS_INCOMPLETE | Faible — validation déterministe |
| INV-03 | Pas de ReKey actif | ReKeyActiveValidator requête DB rekey_state != ACTIVE | C3 | Preuve rejetée avec motif REKEY_ACTIVE | Faible — requête DB simple |
| INV-09 | Aucun secret exposé | SecretExposureValidator scan patterns sensibles (regex) sur payload sérialisé | C3 | Preuve rejetée avec motif SECRET_EXPOSED | Moyen — patterns à maintenir exhaustifs |
| INV-11 | envelopeSeal valide | EnvelopeSealValidator vérifie signature ECDSA-P384-SHA3-384 via crypto.verify(null, ...) | C3 | Preuve rejetée avec motif SEAL_INVALID | Moyen — dépend HSM/PKI. Attention : crypto.verify(null, hash, key, sig) (raw ECDSA, pas createVerify) |
| INV-12 | Matériel offline suffisant | OfflineMaterialValidator vérifie présence verificationMaterial complet | C3 | Preuve rejetée avec motif OFFLINE_MATERIAL_MISSING | Faible — vérification structurelle |
| INV-85-01 | Backend ne déchiffre jamais | Architecture Zero-Knowledge : S3PresignService génère URL sans lecture. Aucun appel GetObject / déchiffrement dans ExportService | C4, C6 | Absence d'appel S3 GetObject dans traces/audit. Test : grep code pour opérations decrypt | Faible — pattern déjà prouvé PD-63 |
| INV-85-02 | exportId UUID v4 unique | crypto.randomUUID() à chaque requête | C4 | UUID v4 format + non-collision sur série | Faible |
| INV-85-03 | integrityHash = SHA3-256 JCS | ExportManifestBuilder : canonicalise manifest sans integrityHash, hash SHA3-256, injecte | C5 | Recalcul externe identique | Moyen — attention SHA3-256 ≠ SHA-256 |
| INV-85-04 | URLs signées <= 30 min | S3PresignService.getSignedUrl({expiresIn}) avec TTL configurable borné Joi max(30) | C7, C4 | expiresAt - now ∈ [1, 30] min | Faible — borne Joi + test |
| INV-85-05 | Audit WORM immuable | AuditLogService.logAsync() fail-closed pour TOUT résultat HTTP | C8 | Entrée audit WORM présente pour chaque requête | Moyen — fail-closed si audit indisponible → 500 |
| INV-85-06 | Export n'altère pas ProofEnvelope | Lecture seule des ProofEnvelopes (find, pas save/update) | C4 | Hash metadata avant/après identiques | Faible — code review statique |
| INV-85-07 | Taille <= 1 GB | Sommation encrypted_size des documents, comparaison à MAX_BACKEND_EXPORT_BYTES | C4 | 413 si dépassement | Faible |
| INV-85-08-transitions | États terminaux sans transition | Machine d'états : 6 états terminaux, aucune transition sortante. Enum ExportResultState sans méthode de transition | C2 | Test unitaire vérifiant absence de transition | Faible |
| INV-85-09-envelope-encryption | Secrets crypto chiffrés au repos | Aucun artefact crypto temporaire créé par PD-85 (lecture seule). Vérification que les ProofEnvelopes existantes respectent la contrainte | C3 | Scan DB : aucun champ DEK/fragment en clair | Faible — PD-85 ne crée pas de secrets |
4. Mapping critères d'acceptation → mécanismes¶
| Critère ID | Mécanisme(s) | Composant | Observable | Risque |
|---|---|---|---|---|
| CA-01 | Chaîne complète F1 : auth → plan → RLS → validation → manifest → URLs → audit | C4, C5, C6 | 200 + exportId UUID + manifest + signedUrls non vides | Faible |
| CA-02 | class-validator sur ComplaintFileRequestDto : @IsArray, @ArrayMinSize(1), @IsUUID('4', {each: true}) | C1 | 400 + message validation | Faible |
| CA-03 | PremiumGuard vérifie user.plan != FREE | C6 | 403 + motif PREMIUM_REQUIRED | Faible — réutilise pattern PD-84 |
| CA-04 | RLS ownership check fail-fast dans ExportService : WHERE proof.user_id = :userId | C4 | 404 corrélé au premier proofId fautif | Faible |
| CA-05 | Sommation encrypted_size vs MAX_BACKEND_EXPORT_BYTES | C4 | 413 + taille estimée + limite 1 GB | Faible |
| CA-06 | Si partition valides/rejetées → toutes rejetées → 422 | C4 | 422 + rejectedProofs exhaustif (3 entrées dans ST-05) | Faible |
| CA-07 | Partition valides/rejetées → export partiel | C4, C5 | 200 + proofCount = valides + rejectedProofs non vide | Faible |
| CA-08 | ExportManifestBuilder : JCS canonicalisation + SHA3-256 | C5 | Recalcul externe == manifest.integrityHash | Moyen — JCS doit être byte-identical |
| CA-09 | ChronologyBuilder : events.sort((a, b) => a.timestamp.localeCompare(b.timestamp)) | C5 | Ordre non décroissant vérifié | Faible |
| CA-10 | S3PresignService.getSignedUrl({expiresIn: ttlMinutes * 60}) + calculateExpiresAt() | C4 | Accès OK avant expiration, refus après | Moyen — test d'intégration S3 requis |
| CA-11 | Zero-Knowledge : aucun GetObject/decrypt dans code PD-85 | C4 | Absence de trace/appel déchiffrement | Faible |
| CA-12 | AuditLogService.logAsync() pour TOUT résultat (200 + 4xx + 422) | C8 | Entrée audit immuable pour chaque réponse HTTP | Moyen — nécessite fail-closed |
| CA-13 | ExportMinorEvidenceService enrichissement conditionnel | C9 | Évidences présentes si applicables, absentes sinon | Moyen — dépend données B2C |
| CA-14 | manifest.exportedBy.userId = JWT sub + role = USER | LEGAL_GUARDIAN | C5 | Champs présents et valides |
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 | CA-01, INV-02/09/11/12, INV-85-02/06 | Chaîne F1 complète | 200 + payload complet + immutabilité source | Integration |
| TC-NOM-02 | CA-07, INV-03 | Partition valides/rejetées | 200 + proofCount=8 + 2 rejets | Integration |
| TC-NOM-03 | CA-13 | ExportMinorEvidenceService | 200 + LEGAL_GUARDIAN + évidences conditionnelles | Integration |
| TC-NOM-04 | CA-09, CA-14 | ChronologyBuilder + exportedBy | Ordre chrono + userId/role valides | Unit |
| TC-NOM-05 | CA-08, INV-85-03 | ExportManifestBuilder.computeIntegrityHash() | Recalcul SHA3-256 == integrityHash | Unit |
| TC-NOM-06 | CA-10, INV-85-04 | S3PresignService.getSignedUrl() + TTL | Accès OK avant expiration + HTTPS + TTL ≤ 30 min | Integration |
| TC-NOM-07 | CA-10, INV-85-04 | URL expirée → refus | Accès refusé post-expiration | Integration |
| TC-ERR-01 | CA-02 | class-validator rejet | 400 + message validation | Unit |
| TC-ERR-02 | CA-03 | PremiumGuard | 403 + motif plan | Unit |
| TC-ERR-03 | CA-04 | RLS fail-fast | 404 corrélé proofId | Integration |
| TC-ERR-04 | CA-05, INV-85-07 | Sommation taille | 413 + estimée + limite | Unit |
| TC-ERR-05 | CA-06 | Toutes invalides | 422 + 3 rejets + motifs | Integration |
| TC-ERR-06 | CA-02 | UUID format invalide | 400 + format invalide | Unit |
| TC-INV-02 | INV-02 | FiveSectionsCompleteValidator | Preuve rejetée si section manquante | Unit |
| TC-INV-03 | INV-03 | ReKeyActiveValidator | Preuve rejetée si ReKey actif | Unit |
| TC-INV-09 | INV-09 | SecretExposureValidator | Aucun pattern sensible dans output | Unit |
| TC-INV-11 | INV-11 | EnvelopeSealValidator | Signature ECDSA valide (roundtrip sign-verify) | Unit |
| TC-INV-12 | INV-12 | OfflineMaterialValidator | Matériel complet et cohérent | Unit |
| TC-INV-8501 | INV-85-01 | Absence opération déchiffrement | Scan code + audit technique | Unit (statique) |
| TC-INV-8502 | INV-85-02 | crypto.randomUUID() | UUID v4 unique × N requêtes | Unit |
| TC-INV-8503 | INV-85-03 | JCS + SHA3-256 | Hash recalculé identique | Unit |
| TC-INV-8504 | INV-85-04 | TTL borné Joi + calcul | expiresAt - now ∈ [1, 30] min | Unit + Integration |
| TC-INV-8505 | INV-85-05 | AuditLogService.logAsync() fail-closed | Entrée WORM pour chaque requête | Integration |
| TC-INV-8506 | INV-85-06 | Lecture seule ProofEnvelope | Checksum avant/après identiques | Integration |
| TC-INV-8507 | INV-85-07 | Sommation > 1 GB → 413 | 413 + taille | Unit |
| TC-INV-8508 | INV-85-08 | Enum ExportResultState sans transition | Pas de méthode next()/transition() | Unit |
| TC-INV-8509 | INV-85-09 | Aucun secret temporaire PD-85 | Scan : pas de DEK/fragment en clair | Unit (statique) |
| TC-NR-01 | Stabilité contrat 200 | Schéma JSON snapshot | Champs obligatoires présents | Unit |
| TC-NR-02 | Stabilité codes erreur | Jeu de données → mêmes codes | Pas de régression mapping | Integration |
| TC-NR-03 | Déterminisme proofCount | proofCount == proofs.length | Cohérence interne | Unit |
| TC-NR-04 | Intégrité hash inter-exécutions | Mêmes données → même hash | Déterminisme JCS | Unit |
| TC-NR-05 | Non-régression URL signées | TTL ≤ 30 min à chaque release | Borne respectée | Unit |
| TC-NR-06 | Non-régression WORM | Entrée audit sur succès | Traçabilité minimale | Integration |
| TC-NEG-01 | Doublons proofIds | 400 (doublons détectés) | Pas de déduplication | Unit |
| TC-NEG-02 | Mélange valides + format invalide | 400 prioritaire | Aucun export | Unit |
| TC-NEG-03 | URL non-HTTPS (robustesse) | Rejet interne | HTTPS strict | Unit |
| TC-NEG-04 | Horodatage non UTC | Rejet preuve | UTC Z strict | Unit |
| TC-NEG-05 | URL expirée répétée | Refus systématique | Pas de réactivation | Integration |
| TC-NEG-06 | Secrets en clair stockage temp | Aucun détecté | Conformité INV-85-09 | Unit (statique) |
6. Gestion des erreurs¶
| Code | Condition | Traitement | Observable |
|---|---|---|---|
| 400 | proofIds absent, vide, format UUID invalide, doublons | class-validator + ExportException(PAYLOAD_INVALID). Aucun export créé, aucune URL signée | Message validation explicite + audit WORM |
| 403 | Plan FREE | PremiumGuard → FreemiumException(PREMIUM_REQUIRED) (réutilise PD-84) | Motif plan + audit WORM |
| 404 | proofId inexistant OU non possédé | Fail-fast : la première erreur interrompt, ExportException(PROOF_NOT_FOUND, {proofId}) | proofId corrélé + audit WORM |
| 413 | Taille totale > 1 GB | ExportException(EXPORT_SIZE_EXCEEDED, {estimatedBytes, limitBytes}) | Taille estimée + limite + audit WORM |
| 422 | Toutes preuves rejetées par validateurs | ExportException(ALL_PROOFS_REJECTED, {rejectedProofs}) | Détail par preuve/motif + audit WORM |
| 429 | Rate limit dépassé | Throttler NestJS (@nestjs/throttler) avec EXPORT_RATE_LIMIT_PER_HOUR | Message retry-after |
| 500 | Incohérence interne (exportedBy.role absent, proofCount != proofs.length, etc.) | Exception interne capturée par filtre global. Audit WORM avec result: ERROR | Logs internes, pas de fuite d'info |
Fail-closed audit : si AuditLogService.logAsync() échoue, l'export DOIT échouer (500). L'opération métier et l'audit sont atomiques au niveau applicatif — un export sans trace audit violerait INV-85-05.
7. Impacts sécurité¶
Zero-Knowledge (INV-85-01)¶
L'endpoint POST /exports/complaint-file ne lit JAMAIS le contenu des documents. Il : - Lit les métadonnées des ProofEnvelopes depuis PostgreSQL (structures, hashes, seals) - Génère des URLs signées S3 via S3PresignService (opération de signature, pas de GetObject) - Assemble le manifest/chronology à partir des métadonnées uniquement
Mécanisme de vérification : Revue statique du code + test TC-INV-8501 (grep absence GetObject, decrypt, decipher).
Authentification et autorisation¶
OidcJwtAuthGuard: auth JWT Keycloak (existant)PremiumGuard: vérifieuser.plan != FREE(nouveau, inspiré dePlanStubGuard)- RLS ownership :
WHERE proof.user_id = :userId(PostgreSQL RLS + vérification applicative, défense en profondeur) - Rate limiting :
@nestjs/throttlerpar utilisateur (EXPORT_RATE_LIMIT_PER_HOUR)
Données sensibles¶
- Aucun secret (DEK, K_master, fragments ReKey) dans le payload de réponse (INV-09)
SecretExposureValidatorscanne le payload sérialisé avant émission- Les URLs signées contiennent des credentials temporaires (S3 HMAC) — TTL borné ≤ 30 min
Journalisation¶
- Audit WORM pour TOUT résultat (succès + erreurs), fail-closed
- Champs :
actorId,exportId,proofIds,proofCount,result,rejectedCount,timestamp_utc,correlationId - Aucune donnée sensible dans les logs (pas de contenu document, pas de clés)
Conformité¶
- Art. 1366 Code civil : auto-vérifiabilité via
integrityHash+ matériel offline - NF Z42-013 / ISO 14641 : audit WORM immuable
- eIDAS : matériel de vérification embarqué (PD-282)
8. Hypothèses techniques¶
| ID | Hypothèse | Impact si faux |
|---|---|---|
| HT-01 | La bibliothèque json-canonicalize (npm) implémente RFC 8785 de manière strictement conforme | Hash non reproductible inter-systèmes. Mitigation : utiliser la même lib que tsa/utils/canonical-json.utils.ts (déjà validée) |
| HT-02 | node:crypto supporte sha3-256 via createHash('sha3-256') (Node.js >= 18 avec OpenSSL 3) | Fallback : utiliser @noble/hashes/sha3 si indisponible |
| HT-03 | La table des ProofEnvelopes contient un champ encrypted_size ou permet de calculer la taille | Si absent : requête S3 HeadObject par document (latence additionnelle). À vérifier en implémentation |
| HT-04 | Le service AuditLogService.logAsync() accepte les nouveaux AuditActionType sans modification de schéma | Sinon : migration DDL pour la table audit |
| HT-05 | Le S3PresignService existant (PD-46) supporte un TTL paramétrable et peut servir plusieurs URLs par requête | Sinon : extension mineure du service avec boucle |
| HT-06 | L'asset guide_plainte_france.pdf est un fichier statique servi depuis le serveur (pas S3) | Si S3 : URL signée supplémentaire à générer |
| HT-07 | Les données B2C-mineurs (Mandate, Dual Validation, ReKey Lifecycle) sont accessibles via les repositories TypeORM existants | Si non : création de méthodes de requête dans les services existants |
| HT-08 | La route POST /exports/complaint-file est distincte des stubs PD-84 (/folders/:id/exports/*) — pas de collision | Si collision : refactoring des routes PD-84 |
9. Points de vigilance (risques, dette, pièges)¶
SHA3-256 vs SHA-256¶
Le codebase existant (tsa/utils/canonical-json.utils.ts) utilise SHA-256 (sha256Hash). PD-85 requiert SHA3-256 pour integrityHash. Ne PAS réutiliser canonicalizeAndHash() directement — créer une fonction sha3_256Hash() dédiée ou utiliser createHash('sha3-256') directement.
Fail-fast 404 vs rejets partiels¶
Le 404 (proof non possédée/inexistante) est fail-fast : le traitement s'arrête à la première erreur. Les rejets partiels (422/200) ne s'appliquent qu'aux validations d'invariants RFC (INV-02/03/09/11/12), PAS à l'ownership. L'ordre de traitement est critique : 1. Validation payload (400) 2. Contrôle plan (403) 3. RLS ownership — fail-fast (404) 4. Validation taille (413) 5. Validation invariants RFC — rejet partiel (200 ou 422)
Signature ECDSA raw pour INV-11¶
Attention au piège double-hash (REX PD-282) : CloudHSM signe en mode CKM_ECDSA (raw). Utiliser crypto.verify(null, hash, publicKey, signature), jamais createVerify('SHA3-384').update(hash).
Rate limiting¶
Le rate limit (10/h par défaut) doit être par utilisateur, pas global. Utiliser @nestjs/throttler avec la clé userId extraite du JWT.
Atomicité audit¶
L'audit est fail-closed : si l'écriture WORM échoue, l'export doit échouer (500). Cela implique que AuditLogService.logAsync() doit être attendu (await) malgré son nom async — il s'agit d'un fire-and-await, pas d'un fire-and-forget pour cet usage.
Doublons proofIds¶
La spec contractualise le rejet 400 pour les doublons (pas de déduplication). Le DTO doit utiliser @ArrayUnique() de class-validator.
10. Hors périmètre¶
- Assemblage ZIP : PD-283 (côté app).
- Déchiffrement documents : interdit (Zero-Knowledge).
- Script de vérification automatisé : V2.
- Timeline PDF riche : V2.
- Internationalisation du guide : V2.
- Notification automatique : V2.
- Tests de performance P95 : contexte matériel non spécifié (cf. H-06 spec). Les seuils 5s/50 preuves et 10s/100 preuves sont documentés mais non testables sans environnement de référence.
- Migration DDL : aucun changement de schéma contractualisé. Si
encrypted_sizemanque sur les entités, c'est une adaptation d'implémentation (HT-03), pas un changement de spec.
Périmètre de test¶
| Niveau de test | In scope | Hors scope (justification) |
|---|---|---|
| Unitaire | Tous les composants C1-C9 : DTOs, validators, manifest builder, service, exceptions, config | — |
| Intégration | Interactions entre ExportService ↔ Validators ↔ ManifestBuilder ↔ S3PresignService ↔ AuditLogService | — |
| E2E | Flux complet POST /exports/complaint-file avec DB PostgreSQL + S3 mock | S3 réel (OVH) hors périmètre E2E local — testé en CI/CD |
Tous les niveaux de test sont couverts, aucune exclusion. Les tests d'intégration S3 (TC-NOM-06, TC-NOM-07) utilisent un mock S3 local (localstack ou mock S3PresignService). Les tests E2E vérifient le flux HTTP complet avec base de données réelle.
Mécanismes cross-module¶
| Élément | Détail |
|---|---|
| Routes d'autres modules à protéger | Aucune — PD-85 ne modifie pas de routes existantes. Le stub PD-84 ExportController est remplacé, pas les routes folders/:id/exports/* (celles-ci restent en 403/501 pour FREE/PREMIUM respectivement) |
| Modules consommés en lecture | freemium (User.plan, accountRole), legal-pre (ProofEnvelope, EnvelopeSeal), dual-validation (données Mandate, Dual Validation), documents (DocumentSecure metadata), storage (S3PresignService), audit (AuditLogService) |
| Modules modifiés | audit (ajout EXPORT_GENERATED à AuditActionType), config (ajout variables EXPORT_*) |
| FK/résolution cross-module | Via repositories TypeORM existants : ProofEnvelope.userId, DocumentSecure.s3Key, User.plan, DualValidation.proofId |
Aucune modification de routes d'autres modules.