Aller au contenu

PD-60 — Plan d'implémentation : Dépôt probatoire d'un document (Upload)

Référence spec : PD-60-specification.md (v2 — amendée 3 février 2026, incluant INV-60-15..17 et CA-60-19..21). Toutes les règles référencées ci-dessous (INV-60-01..17, CA-60-01..21, ERR-60-01..12) sont présentes dans la spécification canonique amendée.

1. Découpage en composants

Composants impactés

Composant Responsabilité PD-60 Fichiers principaux
DepositController (nouveau) Endpoints POST /documents/upload et GET /documents/deposits/:id/proof, validation DTO, orchestration src/modules/documents/controllers/deposit.controller.ts
DepositService (nouveau) Logique métier : validation, idempotence, staging, confirmation probatoire, récupération preuve src/modules/documents/services/deposit.service.ts
ProofReceiptService (nouveau) Génération et vérification du reçu JWS (proof_receipt) src/modules/documents/services/proof-receipt.service.ts
DepositAuditService (nouveau) Écriture traces d'audit spécifiques au dépôt probatoire src/modules/documents/services/deposit-audit.service.ts
NtpHealthService (nouveau) Vérification dérive NTP, exposition statut horodatage src/modules/documents/services/ntp-health.service.ts
AuditModule (existant) Journalisation append-only, signature HSM src/modules/audit/
CryptoModule (existant) HashService (SHA-256 pour fingerprint), HSM signing src/modules/crypto/
UploadModule (existant) Réception contenu opaque (multipart streaming) src/modules/upload/
StorageModule (existant) Stockage S3 du contenu opaque src/modules/storage/
AuthModule (existant) JWT guards, RLS context src/modules/auth/
Database (migration) Table deposits, table deposit_stagings src/database/migrations/

Endpoints

Méthode Path Responsabilité Référence spec
POST /documents/upload Dépôt probatoire (FN-60-01..04), staging dégradé Spec §5
GET /documents/deposits/:id/proof Récupération du reçu probatoire d'un dépôt existant ERR-60-09 (refus si staging)

Nouveaux fichiers

src/modules/documents/
├── controllers/
│   └── deposit.controller.ts
├── dto/
│   ├── create-deposit.dto.ts
│   └── deposit-response.dto.ts
├── entities/
│   ├── deposit.entity.ts
│   └── deposit-staging.entity.ts
├── services/
│   ├── deposit.service.ts
│   ├── proof-receipt.service.ts
│   ├── deposit-audit.service.ts
│   └── ntp-health.service.ts
└── __tests__/
    ├── deposit.controller.spec.ts
    ├── deposit.service.spec.ts
    ├── proof-receipt.service.spec.ts
    └── deposit-audit.service.spec.ts

2. Flux techniques

FN-60-01 — Confirmation probatoire nominale

Client                    DepositController      DepositService       ProofReceiptService    AuditModule        S3
  │                             │                      │                     │                  │              │
  │ POST /documents/upload      │                      │                     │                  │              │
  │ [JWT + body multipart]      │                      │                     │                  │              │
  │────────────────────────────>│                      │                     │                  │              │
  │                             │ validate DTO         │                     │                  │              │
  │                             │ (class-validator)    │                     │                  │              │
  │                             │─────────────────────>│                     │                  │              │
  │                             │                      │ 1. Check auth+RLS   │                  │              │
  │                             │                      │ 2. Check notice_ack │                  │              │
  │                             │                      │    + notice_text    │                  │              │
  │                             │                      │ 3. SHA-256(content) │                  │              │
  │                             │                      │    == fingerprint?  │                  │              │
  │                             │                      │ 4. Check idempotence│                  │              │
  │                             │                      │    (client_request_id)                 │              │
  │                             │                      │ 5. Check NTP health │                  │              │
  │                             │                      │                     │                  │              │
  │                             │                      │ 6. Store content ───────────────────────────────────>│
  │                             │                      │                     │                  │              │
  │                             │                      │ 7. Write audit ─────────────────────>│              │
  │                             │                      │    (append-only +   │                  │ HSM sign     │
  │                             │                      │     HSM signature)  │                  │              │
  │                             │                      │    ← audit_result   │                  │              │
  │                             │                      │                     │                  │              │
  │                             │                      │ 8. Generate JWS ───>│  (tenté INDÉPENDAMMENT         │
  │                             │                      │                     │   du résultat audit)            │
  │                             │                      │                     │ sign(payload,    │              │
  │                             │                      │                     │  HSM private key)│              │
  │                             │                      │                     │<── receipt_result │              │
  │                             │                      │                     │                  │              │
  │                             │                      │ 9. Check double     │                  │              │
  │                             │                      │    condition :      │                  │              │
  │                             │                      │    audit OK AND     │                  │              │
  │                             │                      │    receipt OK ?     │                  │              │
  │                             │                      │    → OUI: INSERT deposit (PROBATIVE_DEPOSIT)          │
  │                             │                      │    → NON: INSERT staging (NON_PROBATIVE_STAGING)       │
  │                             │                      │                     │                  │              │
  │                             │<─────────────────────│                     │                  │              │
  │ 201 Created                 │                      │                     │                  │              │
  │ { deposit_id, timestamp,    │                      │                     │                  │              │
  │   proof_receipt, ... }      │                      │                     │                  │              │
  │<────────────────────────────│                      │                     │                  │              │

FN-60-03 — Contexte dégradé (staging)

  DepositService tente audit ET receipt indépendamment.
  Si l'un ou l'autre échoue (ou les deux) :
  → INSERT deposit_stagings (NON_PROBATIVE_STAGING, degraded_reason indique le(s) composant(s) en échec)
  → Retourne staging_id, act_type=NON_PROBATIVE_STAGING
  → PAS de deposit_id, PAS de proof_receipt, PAS de timestamp

FN-60-04 — Confirmation après staging

  Client rejoue POST /documents/upload avec même client_request_id
  → DepositService détecte staging existant + conditions OK
  → Applique FN-60-01 (promotion staging → deposit)
  → DELETE staging, INSERT deposit

ERR-60-09 — Demande de preuve sur staging

  Client appelle GET /documents/deposits/:staging_id/proof
  → DepositService cherche par id dans deposits (non trouvé)
  → DepositService cherche par id dans deposit_stagings (trouvé, act_type=NON_PROBATIVE_STAGING)
  → Refus explicite : HTTP 400 { error: "PROOF_NOT_AVAILABLE", message: "staging has no probative status" }
  → Audit entry : DEPOSIT_PROOF_REFUSED_STAGING

2b. Diagrammes Mermaid

Graphe de dépendances des modules

graph TD
    Client([Client HTTP])

    subgraph DocumentsModule
        DC[DepositController]
        DS[DepositService]
        PRS[ProofReceiptService]
        DAS[DepositAuditService]
        NTHS[NtpHealthService]
    end

    subgraph Modules existants
        AM[AuditModule]
        CM[CryptoModule / HashService]
        UM[UploadModule]
        SM[StorageModule / S3]
        AuthM[AuthModule / JWT + RLS]
    end

    subgraph Persistance
        DB[(PostgreSQL<br/>deposits + deposit_stagings)]
    end

    Client -->|POST /documents/upload<br/>GET /deposits/:id/proof| DC
    DC --> AuthM
    DC --> DS
    DS --> CM
    DS --> NTHS
    DS --> UM
    DS --> SM
    DS --> DAS
    DS --> PRS
    DS --> DB
    DAS --> AM
    PRS --> CM
    AM -->|HSM sign| CM

Séquence — Dépôt probatoire nominal (FN-60-01)

sequenceDiagram
    actor C as Client
    participant DC as DepositController
    participant Auth as AuthModule
    participant DS as DepositService
    participant CM as CryptoModule
    participant NTHS as NtpHealthService
    participant SM as StorageModule (S3)
    participant DAS as DepositAuditService
    participant PRS as ProofReceiptService
    participant DB as PostgreSQL

    C->>DC: POST /documents/upload [JWT + multipart]
    DC->>Auth: Validate JWT + extract RLS context
    Auth-->>DC: userId, vaultId
    DC->>DS: createDeposit(dto, user)

    DS->>DS: Validate notice_ack + notice_text
    DS->>CM: SHA-256(content)
    CM-->>DS: fingerprint
    DS->>DS: Compare fingerprint vs dto.fingerprint
    DS->>DS: Check idempotence (client_request_id)
    DS->>NTHS: checkDrift()
    NTHS-->>DS: NTP OK

    DS->>SM: store(content, key)
    SM-->>DS: s3_key

    par Audit + Receipt (indépendants)
        DS->>DAS: writeAudit(DEPOSIT_CREATED, metadata)
        DAS-->>DS: audit OK
    and
        DS->>PRS: generateReceipt(payload)
        PRS->>CM: HSM sign(payload)
        CM-->>PRS: JWS
        PRS-->>DS: proof_receipt
    end

    alt audit OK AND receipt OK
        DS->>DB: INSERT deposits (PROBATIVE_DEPOSIT)
    else un ou les deux en échec
        DS->>DB: INSERT deposit_stagings (NON_PROBATIVE_STAGING)
    end

    DS-->>DC: depositResult
    DC-->>C: 201 Created {deposit_id, timestamp, proof_receipt}

Séquence — Confirmation après staging (FN-60-04)

sequenceDiagram
    actor C as Client
    participant DS as DepositService
    participant DAS as DepositAuditService
    participant PRS as ProofReceiptService
    participant DB as PostgreSQL

    C->>DS: POST /documents/upload (même client_request_id)
    DS->>DB: Lookup staging par client_request_id
    DB-->>DS: staging trouvé

    DS->>DAS: writeAudit(DEPOSIT_CREATED, metadata)
    DAS-->>DS: audit OK
    DS->>PRS: generateReceipt(payload)
    PRS-->>DS: proof_receipt

    alt double condition OK
        DS->>DB: DELETE deposit_stagings
        DS->>DB: INSERT deposits (PROBATIVE_DEPOSIT)
        DS-->>C: 201 Created {deposit_id, proof_receipt}
    else échec persistant
        DS->>DB: UPDATE deposit_stagings (degraded_reason)
        DS-->>C: 201 Created {staging_id, NON_PROBATIVE_STAGING}
    end

3. Mapping invariants → mécanismes

Invariant ID Exigence Mécanisme Composant Observable Risque
INV-60-01 Auth + autorisation obligatoire @UseGuards(JwtAuthGuard, AuthorizationGuard) + @Roles('user') AuthModule / DepositController 401/403 si non autorisé Faible — patterns existants
INV-60-02 Atomicité confirmation Transaction unique englobante avec sérialisation inter-table : L'ensemble du traitement s'exécute dans une unique transaction PostgreSQL explicite (BEGIN … COMMIT/ROLLBACK). Les trois phases ci-dessous sont toutes à l'intérieur de cette transaction. Phase 0 (sérialisation, dans la TX) — SELECT pg_advisory_xact_lock(hashtext(user_id::text || '/' || client_request_id)) acquiert un verrou advisory lié à la transaction courante. Ce verrou est maintenu jusqu'au COMMIT ou ROLLBACK de la TX englobante, garantissant l'exclusion mutuelle entre les tables deposits et deposit_stagings pour le même (user_id, client_request_id). Phase A (opérations externes, dans la TX) — audit write et receipt generation sont tentés indépendamment (voir INV-60-10/INV-60-16). Ces opérations appellent des services externes (HSM) mais le verrou advisory reste acquis car la TX est toujours ouverte. Ces opérations sont idempotentes ou annulables côté externe. Phase B (INSERT final, dans la TX) — selon les résultats de Phase A : si les deux ont réussi → INSERT deposit (PROBATIVE_DEPOSIT) ; si l'un a échoué → INSERT staging (NON_PROBATIVE_STAGING). Puis COMMIT. L'atomicité d'INV-60-02 porte sur la TX entière : l'INSERT final est tout-ou-rien, et le verrou empêche toute interleaving concurrente. Implémentation NestJS : DataSource.transaction(async (manager) => { /* Phase 0 + A + B */ }) DepositService Pas d'état partiel observable en DB ; pas de doublon inter-table Moyen — latence HSM maintient la TX ouverte en Phase A
INV-60-03 deposit_id unique + timestamp UTC deposit_id = UUIDv7, existence_timestamp_utc = horodatage UTC tronqué à la seconde (format YYYY-MM-DDTHH:mm:ssZ, sans millisecondes). Implémentation : new Date().toISOString().replace(/\.\d{3}Z$/, 'Z') DepositService Champs présents dans response + DB, format sans millisecondes Faible
INV-60-04 Pas de contenu intelligible Le contenu est traité comme Buffer opaque. Aucun log ne contient le contenu. Response ne retourne que des références DepositController / AuditModule Grep logs/response pour marqueur de test Moyen — vigilance logs
INV-60-05 Reçu JWS vérifiable hors plateforme ProofReceiptService.sign() → JWS compact (RS256/ES256). Clé publique publiée à GET /documents/.well-known/jwks.json (endpoint public, sans JWT). Dépendance explicite : la vérification tiers nécessite que le tiers accède à cet endpoint OU dispose d'une copie de la clé publique obtenue par un canal externe (documentation, export). Le plan prévoit l'endpoint JWKS comme seul mécanisme de distribution. ProofReceiptService JWS décodable + signature valide avec clé publique Moyen — gestion clé publique
INV-60-06 Journalisation append-only Utilise AuditModule existant (PD-37) : table audit.audit_log avec trigger PostgreSQL BEFORE UPDATE OR DELETE → RAISE EXCEPTION. Signature HSM ECDSA P-384. Note : la résistance à un acteur disposant d'un accès administrateur au stockage est hors périmètre de cette US (PC-60-13, spec §4 INV-60-06). AuditModule Entrée audit_log corrélée à client_request_id Faible — infrastructure existante
INV-60-07 Lien depositor + scope depositor_identity_ref = JWT.sub, responsibility_scope_ref = JWT.tenant_id. Le champ tenant_id DOIT être présent dans le JWT ; requête rejetée sinon (HTTP 400). DepositService Champs dans response, receipt, audit Faible
INV-60-08 Détection altération Reçu JWS contient document_fingerprint (SHA-256 du contenu). Tiers compare hash(document) vs fingerprint du reçu ProofReceiptService Vérification tiers échoue si document modifié Faible
INV-60-09 Statut explicite Enum TypeORM : PROBATIVE_DEPOSIT / NON_PROBATIVE_STAGING. Champ act_type dans response DepositService / Entities Champ act_type dans chaque réponse Faible
INV-60-10 Double condition existence juridique L'audit et le receipt sont tentés indépendamment (le receipt n'est pas conditionné au succès de l'audit). legal_existence_status=ESTABLISHED uniquement si les deux ont réussi (audit_trace_ref IS NOT NULL AND proof_receipt IS NOT NULL). Sinon → staging DepositService Vérification en base + response Moyen
INV-60-11 Staging en dégradé Si l'audit OU le receipt échoue (l'autre peut réussir) → INSERT staging. Les deux opérations sont tentées indépendamment ; le staging enregistre laquelle a échoué (degraded_reason) DepositService staging_id retourné, pas de deposit_id Moyen
INV-60-12 Notice ack obligatoire + texte exact DTO validation : probative_notice_ack (@IsBoolean() @Equals(true)) ET probative_notice_text (@IsString() @Equals(PROBATIVE_NOTICE_TEXT)). Décision d'implémentation (tracée) : La spec §3 impose probative_notice_ack=true comme condition d'acceptation système. CA-60-16 impose en complément que la « valeur exacte de notice acceptée » soit "Vous réalisez un acte probatoire daté.". Le champ probative_notice_text est la matérialisation côté requête de CA-60-16 : sans ce champ, il est impossible de vérifier que le client a soumis la valeur exacte (et non une valeur arbitraire), et les tests TC-INV-12 (correspondance textuelle exacte) et TC-NEG-03 (rejet de variations textuelles) deviennent irréalisables — constat confirmé comme BLOQUANT lors de la review v3 du plan. Ce champ est donc une conséquence directe de CA-60-16, pas un ajout de contrainte. Les deux conditions (ack=true + text=valeur exacte) sont complémentaires et non redondantes : ack matérialise le consentement (spec §3), text matérialise la vérification d'exactitude (CA-60-16) CreateDepositDto 400 si ack absent/false OU texte inexact Faible
INV-60-13 Répartition responsabilités Champ responsibility_declaration dans le payload JWS du reçu : { depositor: "document_submitted", organization: "authorizations_mandates", probatiovault: "traceability_date_integrity" } ProofReceiptService Présence dans JWS payload Faible
INV-60-14 Obligation limitée Notice contractuelle + absence de clauses supplémentaires dans le reçu ProofReceiptService Vérification syntaxique Faible
INV-60-15 Vérification SHA-256 serveur crypto.createHash('sha256').update(content).digest('hex') comparé au document_fingerprint du DTO DepositService ERR-60-11 si mismatch Faible
INV-60-16 Injection de faute pour tests Interface AuditWriter et ReceiptSigner injectables. En test : mocks qui lèvent des exceptions sélectivement DepositService (DI) Tests TC-NOM-09 reproductibles Moyen — design DI
INV-60-17 Limite testabilité juridique Vérification syntaxique dans tests (présence des 3 volets) Tests contractuels Couverture partielle documentée Faible

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

Critère ID Mécanisme(s) Composant Observable Risque
CA-60-01 JwtAuthGuard rejette requêtes sans token AuthModule HTTP 401 Faible
CA-60-02 AuthorizationGuard vérifie @Roles('user') AuthModule HTTP 403 Faible
CA-60-03 UUIDv7 pour deposit_id + horodatage UTC tronqué à la seconde (YYYY-MM-DDTHH:mm:ssZ) DepositService Champs dans response, format sans millisecondes Faible
CA-60-04 JWS payload contient tous les champs contractuels ProofReceiptService Décodage JWS Faible
CA-60-05 Transaction unique explicite englobant Phase 0 + A + B (DataSource.transaction()). Phase 0 : pg_advisory_xact_lock acquis dans la TX, maintenu jusqu'au COMMIT. Phase A : appels HSM (audit/receipt) exécutés pendant que la TX est ouverte (verrou maintenu). Phase B : INSERT final (deposit ou staging). COMMIT libère le verrou et finalise l'écriture. ROLLBACK annule tout. Aucune phase n'est hors TX DepositService Pas de row incomplète ; pas de doublon inter-table ; verrou effectif pendant toute la durée Moyen — TX longue si HSM lent
CA-60-06 Verrou advisory inter-table (Phase 0) + contrainte UNIQUE (user_id, client_request_id) par table. Logique applicative : SELECT par (user_id, client_request_id) dans deposits puis deposit_stagings (sous verrou) → si existe avec même document_fingerprint → retourner le deposit existant (idempotence) DepositService Même deposit_id retourné Faible
CA-60-07 Verrou advisory inter-table (Phase 0) + contrainte UNIQUE (user_id, client_request_id) par table. Logique applicative : SELECT par (user_id, client_request_id) → si existe avec document_fingerprint différent → HTTP 409 Conflict DepositService HTTP 409 Conflict Faible
CA-60-08 Aucun content dans logs/response. Log audit ne contient que des refs DepositService / AuditModule Grep marqueur absent Moyen
CA-60-09 Audit entry pour chaque tentative (succès/erreur/staging) DepositAuditService Entrée corrélée à client_request_id Faible
CA-60-10 JWS vérifiable avec clé publique obtenue via GET /documents/.well-known/jwks.json (endpoint public). Vérification tiers : (1) récupérer JWKS, (2) vérifier signature JWS, (3) comparer fingerprint vs hash document ProofReceiptService Test de vérification hors plateforme Moyen
CA-60-11 SHA-256(doc modifié) ≠ fingerprint du reçu ProofReceiptService Échec vérification Faible
CA-60-12 JWT.sub → depositor_identity_ref, JWT.tenant_id → responsibility_scope_ref DepositService Champs dans response + audit Faible
CA-60-13 @Equals(true) sur probative_notice_ack dans DTO CreateDepositDto HTTP 400 si ack absent ou false Faible
CA-60-14 Audit et receipt tentés indépendamment. Check après les deux : audit_trace_ref != null && proof_receipt != null → ESTABLISHED ; sinon → staging DepositService Response legal_existence_status Moyen
CA-60-15 Audit et receipt tentés indépendamment. Si l'un échoue → INSERT staging avec degraded_reason DepositService act_type=NON_PROBATIVE_STAGING Moyen
CA-60-16 @Equals("Vous réalisez un acte probatoire daté.") sur probative_notice_text dans DTO (matérialisation de CA-60-16, voir justification INV-60-12). Toute variation textuelle (casse, espace, ponctuation) est rejetée HTTP 400. La valeur exacte est embarquée dans le reçu JWS et l'entrée d'audit. Ce mécanisme complète ack=true (spec §3) sans le remplacer CreateDepositDto / ProofReceiptService HTTP 400 si texte inexact ; valeur exacte dans artefacts Faible
CA-60-17 responsibility_declaration dans JWS payload ProofReceiptService Présence dans payload Faible
CA-60-18 JWS signature invalide si audit_trace_ref altéré ProofReceiptService Échec vérif signature Faible
CA-60-19 SHA-256 serveur recalculé et comparé. Couverture test : TC-ERR-04 couvre « empreinte invalide/incohérente » (incluant ERR-60-11 hash mismatch). Observable : HTTP 422 FINGERPRINT_MISMATCH + audit DEPOSIT_FINGERPRINT_MISMATCH DepositService HTTP 422 si mismatch (TC-ERR-04) Faible
CA-60-20 JWS format compact, algo RS256/ES256 ProofReceiptService jose library vérifie Faible
CA-60-21 NtpHealthService vérifie offset NTP < 1s. Couverture test : TC-NOM-03 couvre le contexte dégradé (dont NTP drift > 1s via mock NtpHealthService). Observable : NON_PROBATIVE_STAGING avec degraded_reason indiquant NTP NtpHealthService Contexte dégradé si > 1s (TC-NOM-03) Moyen

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

Tests nominaux

Test ID Référence spec Mécanisme(s) Point(s) d'observation Niveau
TC-NOM-01 FN-60-01, INV-60-02/03/09/10 TX complète : audit + receipt + deposit Response 201 + tous champs contractuels Integration
TC-NOM-02 FN-60-02, CA-60-06 SELECT existant par (user_id, client_request_id), compare fingerprint → idempotent Même deposit_id, pas de nouvelle row Integration
TC-NOM-03 FN-60-03, INV-60-09/11, CA-60-21 Cas 1 : Mock AuditWriter → exception → staging. Cas 2 : Mock NtpHealthService → drift > 1s → staging (couvre CA-60-21). Dans tous les cas : degraded_reason indique le composant en échec Response NON_PROBATIVE_STAGING + degraded_reason Integration
TC-NOM-04 FN-60-04, INV-60-02/10/11 Staging exists + retry → promotion deposit créé, staging supprimé Integration
TC-NOM-05 FN-60-05, INV-60-05 JWS verify avec clé publique obtenue via GET /documents/.well-known/jwks.json Signature valide, payload cohérent E2E
TC-NOM-06 INV-60-04, CA-60-08 Marqueur dans content → grep response/logs Marqueur absent partout Integration
TC-NOM-07 INV-60-07, CA-60-12 JWT.sub → depositor_identity_ref, JWT.tenant_id → responsibility_scope_ref Champs cohérents dans response + audit Integration
TC-NOM-08 INV-60-08, CA-60-11 SHA-256(doc_modifié) vs fingerprint reçu Mismatch détecté Unit
TC-NOM-09 INV-60-10, CA-60-14 Cas A : mock audit OK + mock receipt OK → ESTABLISHED. Cas B : mock audit OK + mock receipt FAIL → NOT_ESTABLISHED (staging). Cas C : mock audit FAIL + mock receipt OK → NOT_ESTABLISHED (staging). L'audit et le receipt sont tentés indépendamment (receipt n'est PAS conditionné au succès de l'audit). La double condition est vérifiée après les deux opérations. Injection via interfaces AuditWriter et ReceiptSigner (INV-60-16) legal_existence_status correct par cas Integration
TC-NOM-10 INV-60-13, CA-60-17 JWS payload.responsibility_declaration 3 volets présents : depositor, organization, probatiovault Unit

Tests d'erreur

Test ID Référence spec Mécanisme(s) Point(s) d'observation Niveau
TC-ERR-01 ERR-60-01, CA-60-01 Requête sans JWT HTTP 401 + audit entry DEPOSIT_AUTH_FAILED Integration
TC-ERR-02 ERR-60-02, CA-60-02 JWT sans rôle 'user' HTTP 403 + audit entry DEPOSIT_AUTHZ_FAILED Integration
TC-ERR-03 ERR-60-03 DTO incomplet (champ manquant) HTTP 400 + audit entry DEPOSIT_VALIDATION_FAILED Integration
TC-ERR-04 ERR-60-04, ERR-60-11, CA-60-19 Cas 1 : fingerprint mal formaté (non hex, longueur ≠ 64) → HTTP 422. Cas 2 : fingerprint bien formaté mais hash serveur ≠ fingerprint client (contenu altéré) → HTTP 422 FINGERPRINT_MISMATCH HTTP 422 + audit entry DEPOSIT_FINGERPRINT_MISMATCH Integration
TC-ERR-05 ERR-60-05, CA-60-07 SELECT par (user_id, client_request_id) → existe avec fingerprint différent HTTP 409 + audit entry DEPOSIT_CONFLICT Integration
TC-ERR-06 ERR-60-06, INV-60-02 Exception injectée avant TX (mock service technique) HTTP 500 + pas de deposit ni staging Integration
TC-ERR-07 ERR-60-07, CA-60-13/16 probative_notice_ack absent ou false, OU probative_notice_text absent HTTP 400 Integration
TC-ERR-08 ERR-60-08 Rejeu identique (même client_request_id + même fingerprint) après succès Même response que TC-NOM-01 (idempotent) Integration
TC-ERR-09 ERR-60-09 GET /documents/deposits/:staging_id/proof sur un NON_PROBATIVE_STAGING HTTP 400 PROOF_NOT_AVAILABLE + audit entry DEPOSIT_PROOF_REFUSED_STAGING Integration
TC-ERR-10 ERR-60-10, CA-60-18 Altérer audit_trace_ref dans le JWS manuellement → vérifier signature Signature JWS invalide Unit

Tests d'invariants

Test ID Référence spec Mécanisme(s) Point(s) d'observation Niveau
TC-INV-06 INV-60-06, CA-60-09 3 tentatives contrôlées (succès, refus auth, staging) 3 entrées audit_log corrélées à client_request_id ; tentative UPDATE/DELETE sur audit_log → rejeté par trigger Integration
TC-INV-12 INV-60-12, CA-60-16 Soumission avec probative_notice_text = "Vous réalisez un acte probatoire daté." + probative_notice_ack = true → accepté. Soumission avec texte modifié (casse, espace, ponctuation) → rejeté HTTP 400. Vérification que la valeur exacte est présente dans le reçu JWS et l'entrée d'audit Acceptation/rejet basé sur correspondance exacte du texte ; présence dans artefacts Unit + Integration
TC-INV-13 INV-60-13, CA-60-17 Décodage JWS payload → vérification structure responsibility_declaration 3 volets depositor, organization, probatiovault présents avec valeurs non vides Unit
TC-INV-14 INV-60-14 Décodage JWS payload → énumération des clés Pas de champs d'obligation au-delà de ceux définis dans la spec Unit

Tests de non-régression

Test ID Référence spec Mécanisme(s) Point(s) d'observation Niveau
TC-NR-01 FN-60-01 Re-run TC-NOM-01 Même ensemble de champs contractuels obligatoires dans la réponse 201 Integration
TC-NR-02 FN-60-02, CA-60-06 Re-run TC-NOM-02 Même deposit_id, aucun doublon en DB (COUNT = 1) Integration
TC-NR-03 INV-60-04 Re-run TC-NOM-06 Marqueur de contenu absent de response + logs + audit Integration
TC-NR-04 INV-60-10 Re-run TC-NOM-09 (cas A uniquement) legal_existence_status=ESTABLISHED si et seulement si audit + receipt OK Integration
TC-NR-05 INV-60-05/08, CA-60-10/18 Re-run TC-NOM-05 + TC-NOM-08 Vérification tiers pass/fail cohérente E2E + Unit
TC-NR-06 INV-60-11, CA-60-15 Re-run TC-NOM-03 NON_PROBATIVE_STAGING sans deposit_id ni proof_receipt Integration

Tests adversariaux

Test ID Référence spec Mécanisme(s) Point(s) d'observation Niveau
TC-NEG-01 CA-60-06/07 Deux requêtes concurrentes avec même client_request_id + documents différents. Mécanisme : verrou advisory pg_advisory_xact_lock (Phase 0) sérialise les requêtes inter-table + contrainte UNIQUE par table. La seconde requête attend le verrou, puis constate le deposit existant avec fingerprint différent Exactement un deposit créé ; la seconde requête reçoit HTTP 409. COUNT deposits + stagings par client_request_id = 1 Integration
TC-NEG-02 INV-60-05, CA-60-18 Altération manuelle de audit_trace_ref dans le JWS compact → vérification signature Signature JWS invalide car payload altéré. Rapport d'incohérence reçu/audit Unit
TC-NEG-03 INV-60-12, CA-60-13/16 Soumission avec probative_notice_ack=true mais probative_notice_text différent de la valeur contractuelle (ex: texte en anglais, texte tronqué, texte avec espace supplémentaire). Puis soumission avec probative_notice_ack absent/false HTTP 400 pour chaque variation. Aucun deposit ni staging créé. Audit entry DEPOSIT_VALIDATION_FAILED Integration
TC-NEG-04 ERR-60-09, INV-60-11 Boucle de 10 appels GET /documents/deposits/:staging_id/proof sur un staging Refus constant HTTP 400 à chaque appel. act_type reste NON_PROBATIVE_STAGING. legal_existence_status reste NOT_ESTABLISHED Integration
TC-NEG-05 INV-60-04 Document contenant 5 marqueurs sensibles distincts (UUID, chaîne identifiable, etc.) → dépôt succès puis staging Grep des 5 marqueurs dans : response body, stdout logs, stderr logs, audit_log.payload → 0 occurrence Integration

6. Gestion des erreurs

Code HTTP Condition Corps réponse Audit
400 DTO invalide : champ manquant, probative_notice_ack absent/false, probative_notice_text inexact, tenant_id manquant dans JWT { error: "VALIDATION_ERROR", message: "...", fields: [...] } Oui — DEPOSIT_VALIDATION_FAILED
400 GET /documents/deposits/:id/proof sur un staging (ERR-60-09) { error: "PROOF_NOT_AVAILABLE", message: "staging has no probative status" } Oui — DEPOSIT_PROOF_REFUSED_STAGING
401 Pas de JWT ou JWT invalide { error: "UNAUTHORIZED" } Oui — DEPOSIT_AUTH_FAILED
403 JWT valide mais pas de permission de dépôt { error: "FORBIDDEN" } Oui — DEPOSIT_AUTHZ_FAILED
404 GET /documents/deposits/:id/proof : id non trouvé (ni deposit ni staging) { error: "NOT_FOUND" } Oui — DEPOSIT_NOT_FOUND
409 client_request_id déjà utilisé avec document différent (ERR-60-05) { error: "CONFLICT", message: "client_request_id already used with different document" } Oui — DEPOSIT_CONFLICT
409 Rejeu staging avec document_fingerprint différent (ERR-60-12) { error: "CONFLICT", message: "staging exists with different document fingerprint" } Oui — DEPOSIT_STAGING_CONFLICT
422 Hash serveur ≠ fingerprint client (INV-60-15, ERR-60-11) { error: "FINGERPRINT_MISMATCH" } Oui — DEPOSIT_FINGERPRINT_MISMATCH
500 Erreur technique (DB, S3, HSM) avant confirmation { error: "INTERNAL_ERROR" } Oui — DEPOSIT_TECHNICAL_ERROR
503 Contexte dégradé (NTP drift > 1s, audit indisponible) → staging { staging_id, act_type: "NON_PROBATIVE_STAGING", ... } Oui — DEPOSIT_DEGRADED_STAGING

7. Impacts sécurité

Risque Mitigation Composant
Fuite contenu intelligible dans logs Aucun content ou Buffer dans les arguments de logger. Intercepteur de log sanitize DepositService, LoggerModule
Falsification d'empreinte Vérification serveur SHA-256 (INV-60-15) DepositService
Rejeu cross-tenant RLS PostgreSQL isole par user_id. Verrou advisory + client_request_id UNIQUE par (user_id, client_request_id) par table, avec sérialisation inter-table RlsMiddleware, DB constraints, pg_advisory_xact_lock
Compromission clé de signature JWS Clé HSM non-extractable (FIPS 140-2 L3 — PD-7). Rotation via KeyRotationModule CryptoModule / HSM
Timing attack sur idempotence Réponse en temps constant (même durée pour hit vs miss) DepositService
Injection dans audit Paramètres liés (prepared statements), pas de concaténation AuditModule / TypeORM
RGPD — données personnelles depositor_identity_ref = UUID opaque (pas d'email, pas de nom). Contenu chiffré côté client Architecture zero-knowledge

8. Hypothèses techniques

ID Hypothèse Impact si faux
HT-01 Le HashService existant peut être étendu pour SHA-256 (actuellement SHA3-256) Si non → ajouter méthode hashSha256() dans HashService. Impact faible
HT-02 La bibliothèque jose (npm) est disponible ou ajoutée pour la génération JWS Si non → utiliser jsonwebtoken ou implémentation manuelle. Impact faible
HT-03 La clé HSM PD-7 peut signer des JWS avec un algorithme contractuellement autorisé : ES256 (ECDSA P-256) ou RS256 (RSA ≥ 2048 bits), conformément à la spec §3 (« algorithme RS256 ou ES256 »). Note : la clé HSM ECDSA P-384 existante (PD-7) est utilisée pour la signature d'audit (INV-60-06) mais pas pour le reçu JWS. Le reçu JWS nécessite une clé P-256 (ES256) ou RSA (RS256) dédiée sur le HSM Si la clé HSM ne supporte pas P-256 ou RSA → générer une paire de clés dédiée pour le reçu probatoire (ES256 recommandé). Impact moyen
HT-04 Le module ntp-client ou équivalent existe en npm pour vérifier la dérive NTP Si non → appel ntpdate -q via child_process ou vérification via API externe. Impact faible
HT-05 Le champ tenant_id est présent dans le JWT pour responsibility_scope_ref Si non → la requête est rejetée HTTP 400 (pas de fallback). Le JWT DOIT contenir tenant_id. Impact moyen — nécessite coordination avec l'AuthModule.
HT-06 L'upload multipart existant (UploadModule) peut être réutilisé pour recevoir le contenu opaque + JSON metadata dans la même requête Si non → adapter l'UploadModule pour supporter ce format. Aucun comportement alternatif « deux appels séparés » n'est prévu (la spec impose un flux unique POST /documents/upload). Impact moyen
HT-07 Le trigger PostgreSQL BEFORE UPDATE OR DELETE sur audit_log existe déjà (PD-37) Si non → ajouter migration pour le créer. Impact faible (confirmé existant)
HT-08 L'endpoint JWKS (GET /documents/.well-known/jwks.json) est accessible publiquement (sans JWT) et est le mécanisme de distribution de la clé publique pour la vérification tiers Si non → nécessite un canal alternatif de distribution. Impact moyen — affecte INV-60-05, CA-60-10

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

  1. Latence HSM en Phase A (TX ouverte) : Les signatures HSM (audit + receipt) sont exécutées en Phase A, à l'intérieur de la transaction englobante (voir INV-60-02). La TX reste ouverte pendant les appels HSM, ce qui maintient le verrou advisory mais allonge la durée de la TX. Si la latence HSM > 500ms, la TX est plus longue mais l'atomicité et la sérialisation restent garanties. Mitigation : timeout configurable par opération HSM (avec ROLLBACK en cas de timeout), monitoring latence HSM, alerte si durée TX > seuil.

  2. Taille du contenu opaque : La spec ne définit pas de limite de taille. L'UploadModule existant gère le multipart streaming mais la vérification SHA-256 serveur nécessite de hasher tout le contenu. Pour des fichiers > 1 Go, utiliser un hash streaming (déjà disponible via HashStreamService).

  3. Migration des documents existants : Les DocumentSecure existants n'ont pas de deposit_id ni de proof_receipt. PD-60 crée de nouvelles tables (deposits, deposit_stagings), pas de migration des données existantes.

  4. Clé publique JWKS : L'endpoint /documents/.well-known/jwks.json expose la clé publique. Il doit être public (pas de JWT requis) mais protégé contre les abus (rate-limiting infra, cache CDN). C'est le mécanisme unique de distribution de la clé publique pour la vérification tiers (HT-08).

  5. Concurrence sur client_request_id (sérialisation inter-table) : Deux requêtes simultanées avec le même (user_id, client_request_id) sont sérialisées par pg_advisory_xact_lock(hashtext(user_id::text || '/' || client_request_id)) (Phase 0). Ce verrou couvre les deux tables (deposits et deposit_stagings), empêchant qu'un même (user_id, client_request_id) produise un deposit dans une table et un staging dans l'autre par interleaving concurrent. La deuxième requête attend la libération du verrou puis applique la logique idempotence/conflit sur l'état résultant.

  6. NTP monitoring : Le NtpHealthService doit être un health check périodique (cron toutes les 30s), pas un appel NTP par requête. Le statut est mis en cache.

10. Hors périmètre

  • Consultation, modification, partage, suppression, recherche, versioning (PD-61 à PD-69)
  • Rate-limiting sur l'endpoint upload (relève de l'infra — PC-60-17)
  • Résistance admin-level de l'append-only audit — explicitement hors périmètre de cette US par décision contractuelle PC-60-13 (spec §4 INV-60-06 : « La non-altérabilité absolue (résistance à un acteur disposant d'un accès administrateur au stockage) est hors périmètre de cette US et relève de l'infrastructure »). Cette exclusion est actée dans la spec canonique, pas une hypothèse du plan.
  • Qualification juridictionnelle de la valeur probatoire
  • Définition des performances (latence, débit, volumétrie)
  • Gestion des clés de signature (rotation, publication) — relève du module crypto existant

Schéma base de données

Table vault_secure.deposits

CREATE TABLE vault_secure.deposits (
  id             UUID PRIMARY KEY DEFAULT gen_random_uuid(),  -- deposit_id
  user_id        UUID NOT NULL REFERENCES vault_secure.users(id),
  client_request_id  VARCHAR(255) NOT NULL,
  document_fingerprint VARCHAR(64) NOT NULL,  -- SHA-256 hex lowercase
  existence_timestamp_utc TIMESTAMPTZ NOT NULL,
  depositor_identity_ref UUID NOT NULL,  -- == user_id (JWT.sub)
  responsibility_scope_ref VARCHAR(255) NOT NULL,  -- JWT.tenant_id (obligatoire)
  proof_receipt   TEXT NOT NULL,  -- JWS compact
  audit_trace_ref UUID NOT NULL,  -- FK audit_log.id
  act_type        VARCHAR(30) NOT NULL DEFAULT 'PROBATIVE_DEPOSIT',
  legal_existence_status VARCHAR(30) NOT NULL DEFAULT 'ESTABLISHED',
  storage_path    VARCHAR(512) NOT NULL,  -- S3 path
  created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),

  CONSTRAINT uq_deposits_client_request UNIQUE (user_id, client_request_id)
);

-- RLS
ALTER TABLE vault_secure.deposits ENABLE ROW LEVEL SECURITY;
CREATE POLICY deposits_user_isolation ON vault_secure.deposits
  USING (user_id = current_setting('app.current_user_id')::uuid);

Sérialisation inter-table (applicative, Phase 0 — dans la TX englobante) :

-- Exécuté en Phase 0, DANS la transaction explicite (BEGIN déjà émis)
-- Le verrou est maintenu jusqu'au COMMIT ou ROLLBACK de cette TX
SELECT pg_advisory_xact_lock(hashtext(user_id::text || '/' || client_request_id));
Ce verrou advisory lié à la transaction (pg_advisory_xact_lock, pas pg_advisory_lock) garantit qu'un seul processus à la fois traite un (user_id, client_request_id) donné tant que la TX est ouverte. Il est libéré automatiquement au COMMIT/ROLLBACK, empêchant toute race condition entre les tables deposits et deposit_stagings.

Logique d'idempotence/conflit (applicative, sous verrou advisory) : - SELECT par (user_id, client_request_id) dans deposits → si existe : - document_fingerprint identique → retourner le deposit existant (CA-60-06, ERR-60-08) - document_fingerprint différent → HTTP 409 (CA-60-07, ERR-60-05) - Si n'existe pas → SELECT dans deposit_stagings : - Staging trouvé avec même fingerprint + conditions OK → promouvoir (FN-60-04) - Staging trouvé avec fingerprint différent → HTTP 409 (ERR-60-12) - Rien trouvé → nouveau dépôt (FN-60-01)

Table vault_secure.deposit_stagings

CREATE TABLE vault_secure.deposit_stagings (
  id             UUID PRIMARY KEY DEFAULT gen_random_uuid(),  -- staging_id
  user_id        UUID NOT NULL REFERENCES vault_secure.users(id),
  client_request_id  VARCHAR(255) NOT NULL,
  document_fingerprint VARCHAR(64) NOT NULL,
  storage_path    VARCHAR(512) NOT NULL,
  act_type        VARCHAR(30) NOT NULL DEFAULT 'NON_PROBATIVE_STAGING',
  legal_existence_status VARCHAR(30) NOT NULL DEFAULT 'NOT_ESTABLISHED',
  degraded_reason VARCHAR(255),  -- Reason for degraded context
  created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),

  CONSTRAINT uq_stagings_client_request UNIQUE (user_id, client_request_id)
);

-- RLS
ALTER TABLE vault_secure.deposit_stagings ENABLE ROW LEVEL SECURITY;
CREATE POLICY stagings_user_isolation ON vault_secure.deposit_stagings
  USING (user_id = current_setting('app.current_user_id')::uuid);

Ordre d'implémentation

Phase Tâche Dépendances Estimation relative
1 Migration DB : tables deposits + deposit_stagings + RLS S
2 Entities TypeORM : Deposit, DepositStaging Phase 1 S
3 DTO : CreateDepositDto (avec probative_notice_ack + probative_notice_text), DepositResponseDto S
4 NtpHealthService : vérification dérive NTP S
5 ProofReceiptService : génération JWS + endpoint JWKS public CryptoModule/HSM M
6 DepositAuditService : wrapper audit spécialisé dépôt AuditModule S
7 DepositService : logique métier complète (dépôt, idempotence, staging, promotion, preuve) Phases 2-6 L
8 DepositController : endpoints POST /documents/upload + GET /documents/deposits/:id/proof + guards + DTO binding Phase 7 M
9 Tests unitaires : ProofReceiptService, NtpHealthService, DTO (notice ack + text validation), TC-NOM-08/10, TC-ERR-10, TC-INV-12/13/14 Phases 3-5 M
10 Tests d'intégration : TC-NOM-01..09, TC-ERR-01..09, TC-INV-06 Phase 8 L
11 Tests non-régression : TC-NR-01..06 Phase 10 M
12 Tests adversariaux : TC-NEG-01..05 Phase 10 M

Tailles : S = petite, M = moyenne, L = grande

Références

  • Spécification : PD-60-specification.md (v2 — amendée 3 février 2026, incluant INV-60-15..17 et CA-60-19..21)
  • Tests contractuels : PD-60-tests.md
  • Review spécification : PD-60-specification-review.md
  • Epic : PD-192 — API Documents & preuves probatoires
  • Architecture existante : NestJS 10.3 + TypeORM 0.3 + PostgreSQL + CloudHSM (PD-7)