Aller au contenu

PD-287 — Plan d'implémentation

Stack cible : ProbatioVault-backend (NestJS + TypeORM + PostgreSQL). Aucune autre pile n'est visée.

Hypothèse contractuelle retenue : le backend expose déjà vault_secure.proofs (service Proofs), un module Crypto (HSM + KMS envelope), un service Mail transactionnel, un journal d'audit plateforme, un rate-limiter distribué (Redis) et un orchestrateur de workers. Le présent plan s'insère comme module SharingModule + workers dédiés, sans fork de modules existants.

1. Découpage en composants

# Composant Responsabilité Owner (agent step 6b) Routes/endpoints exposés
C1 SharingModule (API partage) Create/list/revoke/read liens côté propriétaire, exposition destinataire du lien. Applique guards partage, lock distribué, idempotence. agent-sharing-api POST /shares, GET /shares, GET /shares/:id, POST /shares/:id/revoke, GET /sharing/:token, POST /sharing/:token/activate, POST /sharing/:token/reauth/request-otp, POST /sharing/:token/reauth/confirm, GET /sharing/:token/proof, POST /sharing/:token/export
C2 OtpService Émission OTP email 6 digits, validation, compteurs session/cumulé, cooldown, rate-limit renvois, hook notification propriétaire. agent-otp interne (consommé par C1)
C3 RecipientSessionService Gestion sessions éphémères destinataire (création post-OTP, timeout d'inactivité, réauth, invalidation forcée sur révocation/expiration). agent-recipient-session interne (consommé par C1)
C4 PreCryptoService Génération identité éphémère destinataire, PRE_ReKeyGen, PRE_ReEncrypt, envelope chiffrée de la DEK. Appels HSM via wrapper existant. agent-crypto-pre interne (consommé par C1, C10)
C5 AuditJournalService Append-only synchrone bloquant, hash-chain (prev_hash), séquenceur d'événements, extraction cohérente par lien. agent-audit-journal interne (consommé par C1, C3, C6, C7, C10) + GET /shares/:id/events (owner)
C6 MerkleAnchorWorker Worker périodique : batch N événements, calcul racine Merkle, ancrage blockchain, persistence audit_batch_merkle_root, rattrapage post-crash ordonné. agent-audit-journal (workers) cron interne
C7 ReconciliationWorker Détection orphelins (lien non terminal inactif > seuil), rattrapage ancrage, clearing HEALTHY après N cycles conformes. agent-reconciliation-worker cron interne
C8 RetentionWorker Purge RGPD J+90 post-terminal : suppression email/IP/UA/session/clés éphémères/compteurs ; préservation du journal d'audit. Event technique de purge. agent-retention-worker cron interne
C9 TrustStoreService Chargement du trust-store plateforme (racines X.509 + politique CRL/OCSP), vérification chaîne certificat destinataire éphémère, fail-closed explicite. agent-trust-store interne (consommé par C4)
C10 ExportComposerService Construction ZIP (document chiffré + artefacts + manifeste JCS + signature plateforme + certificat), calcul SHA-256, contrôle taille. Feature-flag export_zip_enabled. agent-export interne (consommé par C1)
C11 NotificationDispatcher Notifications propriétaire (3 blocages OTP, révocation anti-abus) via canaux mail/push existants. Journalisation canal+corrélation. agent-notifications interne (consommé par C2, C3)
C12 ShareProofGuard (cross-module) Guard NestJS enregistré dans le scope ProofsModule pour les routes listées §5.8 de la spec ; vérifie share_state=ACTIVE, session valide, TTL, non-révocation, ownership indirect. agent-sharing-api (livré avec C1, intégré dans ProofsModule) injecté sur GET /proofs/:id/document, GET /proofs/:id/probative-metadata, POST /proofs/:id/export-composite
C13 Migrations DDL + repositories Schéma vault_secure.share_links, share_otp_state, share_sessions, share_audit_events, share_audit_batches, share_idempotency_keys. Contraintes FK, index anti-scan sur share_url_token (hash). agent-sharing-api n/a
C14 Suite de tests Unit + Integration + E2E + Perf (k6/Artillery) + Sécurité (fuzz token, scan logs/dumps). agent-tests n/a

2. Flux techniques

2.1 F-287-01 — Création du lien (propriétaire)

  1. POST /shares avec proof_id, recipient_email, ttl_seconds?, header Idempotency-Key.
  2. SharingController valide le payload via DTO + class-validator (regex §5.1).
  3. Acquisition lock Redis share:create:{proof_id}:{recipient_email} TTL 600s → 409 SHARE_BUSY sinon.
  4. Vérification idempotence : share_idempotency_keys(key=sha256(key+canonicalJson(payload))) → 409 IDEMPOTENCY_CONFLICT si payload différent, 409 IDEMPOTENCY_WINDOW_EXPIRED si hors fenêtre 24h.
  5. Vérification ownership preuve via ProofsService.assertOwner(proof_id, userId).
  6. Vérification quotas (C7 expose QuotaCheckService interrogé via repository share_quota_counters) : create_links_per_proof_per_24h, create_links_per_owner_per_24h, active_links_per_owner_max.
  7. PreCryptoService.generateEphemeralIdentity()recipient_ephemeral_pubkey_pem, cert signé par CA plateforme (DER b64). Aucun stockage du secret privé côté backend (la clé privée n'est jamais générée côté serveur ; seuls la pubkey et la capsule PRE transitent).
  8. PreCryptoService.reKeyGen(owner_key_ref, recipient_ephemeral_pubkey)pre_rekey_envelope_b64 (chiffré KMS).
  9. Génération token : 32 octets CSPRNG crypto.randomBytes(32) → base64url 43 chars. Persistence de sha256(token) comme PK indexée ; le token clair n'est jamais stocké.
  10. Transaction SQL unique :
    • INSERT share_links (id, proof_id, owner_user_id, recipient_email_lower, token_sha256, state='PENDING_ACTIVATION', created_at, expires_at=created_at+ttl, pre_rekey_envelope_b64, recipient_ephemeral_cert_der_b64, encrypted_dek_envelope_b64)
    • AuditJournalService.append(LINK_CREATED, share_link_id, prev_hash) bloquant dans la même transaction (pattern outbox pas utilisé ici — l'audit est synchrone §5.7).
    • Commit.
  11. Libération lock. Réponse 201 { share_url_token, share_link_id, state, expires_at_utc } au propriétaire uniquement.

2.2 F-287-02 — Activation destinataire OTP

  1. GET /sharing/:token : résolution token_sha256, check état PENDING_ACTIVATION + non expiré. Si KO → 404 générique (§INV-287-12).
  2. OtpService.emitOtp(share_link_id, recipient_email) :
  3. Contrôle otp_resend_limit_per_hour (Redis counter fenêtre glissante 1h).
  4. Génère code 6 digits via crypto.randomInt(0, 1_000_000) (pas Math.random() — cf. learning crypto.randomUUID() 2026-02-21, même justification S2245).
  5. Stocke sha256(otp_code) + emitted_at_utc + valid_until_utc = emitted_at_utc + OTP_CODE_TTL_SECONDS (nouvelle constante OTP_CODE_TTL_SECONDS=300s, cf. Point de vigilance #10).
  6. Envoi email via MailService.sendOtp().
  7. Retour 200 (pas de différenciation succès/rate-limit vers destinataire autre que 429 rate-limit).
  8. POST /sharing/:token/activate { otp_code } :
  9. Lock Redis share:otp:{share_link_id} TTL 30s (empêche concurrence session).
  10. Chargement share_otp_state (compteur session otp_attempt_count, cumulé otp_failed_attempts_total).
  11. Comparaison timing-safe crypto.timingSafeEqual(sha256(input), stored_sha256) + check non péremption OTP.
  12. Si OK : transition PENDING_ACTIVATION → ACTIVE, création session recipient_session_id, reset otp_attempt_count=0, append audit LINK_ACTIVATED.
  13. Si KO et otp_attempt_count < 4 : 404 générique, incrément session + cumulé.
  14. Si KO et otp_attempt_count == 4 (5e échec) : transition → OTP_BLOCKED, otp_blocked_until_utc = now + 900s, incrément blocages → hook NotificationDispatcher si 3e blocage, append audit. Réponse 429 (suit §5.4 F-287-02, cf. Point de vigilance #1).
  15. Si otp_failed_attempts_total >= 50 après incrément : transition → REVOKED, notification propriétaire, append audit LINK_REVOKED. Réponse 429.
  16. Déverrouillage automatique OTP_BLOCKED → ACTIVE exécuté par ReconciliationWorker (tick 60s) OU à la prochaine tentative OTP valide si cooldown expiré.

2.3 F-287-03 — Consultation de la preuve

  1. GET /sharing/:token/proof avec header X-Recipient-Session: <recipient_session_id>.
  2. ShareProofGuard (C12) : valide token, charge share_link, vérifie state=ACTIVE, expiration, session non expirée (last_activity + 1800s).
  3. Si session invalide → 404 générique + indication UX de réauth via /reauth/request-otp (enveloppe standard { reauth_required: true } sans différencier existence lien).
  4. Mise à jour session last_activity = now.
  5. PreCryptoService.reEncrypt(encrypted_dek_envelope, pre_rekey_envelope)recipient_capsule_encrypted_b64.
  6. ProofsService.getEncryptedDocument(proof_id) retourne le document chiffré (interprétation plan : le document chiffré est streamé inline en base64 ou via flux binaire dans la réponse — cf. Point de vigilance #2 et ADR-287-01).
  7. AuditJournalService.append(LINK_VIEWED, ...) avant émission de la réponse (synchrone bloquant). Si append échoue → 503 fail-closed, réponse vidée.
  8. Réponse 200 { encrypted_document_b64, recipient_capsule_encrypted_b64, proof_hash_sha256_hex, merkle_proof_canonical_json, tsa_token_der_b64, blockchain_anchor_txid_hex }.

2.4 F-287-04 — Export composite (optionnel)

  1. Feature flag export_zip_enabled=false par défaut. Si false → 404 générique sur POST /sharing/:token/export (pas de différenciation de capability).
  2. Si true et session valide : ExportComposerService construit :
  3. /document.bin (copie chiffrée, capsule PRE en métadonnée)
  4. /proof.json (hash+Merkle+TSA+anchor)
  5. /audit-subset.jsonl (événements LINK_* strictement liés à ce lien)
  6. /manifest.json (JCS canonical, clés minimales §D-287-26)
  7. /manifest.sig (signature ECDSA P-384 par clé plateforme HSM)
  8. /platform-cert.pem (cert plateforme + chaîne racine)
  9. Check size <= export_zip_max_size_mb → 413 sinon (avant streaming).
  10. AuditJournalService.append(LINK_EXPORTED, export_zip_sha256_hex) synchrone.
  11. Streaming ZIP vers destinataire.

2.5 F-287-05 — Révocation

  1. POST /shares/:id/revoke (owner) → lock share:lifecycle:{id}.
  2. Transaction : UPDATE state→REVOKED, revoked_at_utc=now, invalidation share_sessions actives (table flag), append audit LINK_REVOKED bloquant.
  3. Publication event Redis share.revokedRecipientSessionService purge cache session local (coupure au round-trip suivant, cf. CA-287-08).
  4. RetentionWorker rescheduled pour J+90.

2.6 F-287-06 — Expiration TTL

  • Worker cron 30s scanne share_links WHERE state IN ('PENDING_ACTIVATION','ACTIVE','OTP_BLOCKED') AND expires_at < now().
  • Transition batch → EXPIRED, append LINK_EXPIRED + invalidation sessions + purge artefacts temporaires.
  • Réponses destinataire post-expiration : 404 générique.

2.7 F-287-07 — Vue propriétaire

  • GET /shares?state=active|all paginé. GET /shares/:id/events renvoie événements liés au lien (depuis share_audit_events scopé share_link_id).
  • Premier affichage UI propriétaire : flag user_preferences.noDrmWarningAcknowledged (scope compte utilisateur serveur — cf. Point de vigilance #16 et ADR-287-02).

2bis. Diagramme de dépendances agents

graph LR
    subgraph Wave1[Wave 1 - fondations]
        C13[agent-sharing-api/migrations<br/>C13 DDL]
        C9[agent-trust-store<br/>C9]
        C5[agent-audit-journal<br/>C5 append + hash-chain]
    end
    subgraph Wave2[Wave 2 - services crypto et session]
        C4[agent-crypto-pre<br/>C4]
        C2[agent-otp<br/>C2]
        C3[agent-recipient-session<br/>C3]
        C11[agent-notifications<br/>C11]
    end
    subgraph Wave3[Wave 3 - API + guard + workers]
        C1[agent-sharing-api<br/>C1 + C12 guard]
        C6[agent-audit-journal<br/>C6 Merkle worker]
        C7[agent-reconciliation-worker<br/>C7]
        C8[agent-retention-worker<br/>C8]
        C10[agent-export<br/>C10]
    end
    subgraph Wave4[Wave 4 - tests]
        C14[agent-tests<br/>Unit+Int+E2E+Perf+Sec]
    end

    C13 --> C4
    C13 --> C2
    C13 --> C3
    C13 --> C5
    C9 --> C4
    C5 --> C1
    C5 --> C6
    C5 --> C7
    C5 --> C8
    C5 --> C10
    C4 --> C1
    C4 --> C10
    C2 --> C1
    C3 --> C1
    C11 --> C2
    C11 --> C3
    C1 --> C14
    C6 --> C14
    C7 --> C14
    C8 --> C14
    C10 --> C14

Lecture : 4 waves. Wave 1 livre les fondations (DDL + trust-store + audit service). Wave 2 livre les services middleware (crypto PRE, OTP, sessions, notifications). Wave 3 livre l'API, le guard cross-module et les workers. Wave 4 produit la suite de tests. Les agents d'une même wave sont parallélisables.

2ter. Mécanismes cross-module (obligatoire)

Élément Détail
Routes protégées GET /proofs/:id/document, GET /proofs/:id/probative-metadata, POST /proofs/:id/export-composite
Controllers impactés ProofsController (module ProofsModule)
Effet du guard HTTP 404 générique pour TOUT cas invalide côté destinataire : share_link.state ∉ {ACTIVE}, session invalide, TTL dépassé, otp_failed_attempts_total >= 50, token inconnu. Code uniforme 404 (anti-enumeration INV-287-12). Seul le 429 rate-limit est distinct.
Mécanisme de jointure cross-schéma vault_secure.share_links.proof_id → vault_secure.proofs.id via FK + view matérialisée read-only share_access_view(share_link_id, proof_id, state, expires_at, session_id, last_activity) indexée sur share_link_id et session_id.
Scope d'enregistrement { provide: APP_GUARD, useClass: ShareProofGuard } dans ProofsModule uniquement (pas global). Ordre : après JwtAuthGuard si présent, avant RolesGuard. Le guard détecte via metadata décorateur @ShareContext() que la requête est dans un contexte partage (sinon bypass, fallback au guard owner standard).
Exceptions d'accès Aucune. Aucun rôle (y compris admin) ne peut contourner une révocation/expiration sur un accès destinataire. Les opérateurs plateforme accèdent aux métadonnées via leurs propres routes (hors périmètre story).

3. Mapping invariants → mécanismes

Invariant Exigence Mécanisme Composant Observable Risque
INV-287-01 Zero-knowledge serveur (document jamais en clair) Document chiffré au repos (AES-256-GCM envelope KMS) ; backend ne manipule que encrypted_dek_envelope_b64 et pre_rekey_envelope_b64 ; PreCryptoService opère sur capsules, jamais sur la DEK en clair. Logs filtrés (middleware SensitiveFieldScrubber). C4, C1, C5, infra log Test TC-INV-11 : capture traffic + grep logs + dump mémoire avant GC forcé. Scan automatisé sur release candidate. Timing de GC → mémoire transitoire du binaire chiffré. Mitigation : pas de déchiffrement serveur.
INV-287-02 PRE obligatoire, pas de clé en clair PreCryptoService.reKeyGen/reEncrypt wrapper HSM. Interdiction lint : pattern Buffer.*dek hors C4 flaggé par ESLint rule custom. C4 Audit code + test roundtrip TC-INV-09. Alternance PRE vs KMS direct tentante à des fins de perf. Interdit par invariant YAML forbidden.
INV-287-03 WORM original inchangé Accès read-only à proofs.document_blob_ref ; aucun service du périmètre n'ouvre le blob en écriture. C1, C4, C10 TC-NOM-13, TC-NR-02 : SHA-256 avant/après cycle complet. Export ZIP pourrait copier le blob chiffré — toléré (pas mutation).
INV-287-04 OTP requis à chaque session RecipientSessionService exige recipient_session_id sur toutes les routes post-activation ; session invalide → réauth obligatoire. C3 TC-NOM-02, TC-ERR-09. Bearer-link implicite si cookie session non bridgé au token. Mitigation : session liée au share_link_id + IP+UA fingerprint faible (log only).
INV-287-05 Identité éphémère unique par lien PreCryptoService.generateEphemeralIdentity() appelé à chaque création de lien, recipient_ephemeral_cert_der_b64 stocké par share_link_id avec contrainte UNIQUE. C4, C13 TC-NOM-12. Collision HSM clé — probabilité cryptographique négligeable.
INV-287-06 États fermés (5 états) Enum PostgreSQL share_state_enum + validation TypeScript discriminated union. Aucun string libre. C13, C1 TC-INV-01. Ajout futur d'état : migration + bump enum.
INV-287-07 Transitions non listées interdites ShareStateMachine classe pure (pattern FSM table) : canTransition(from, to). Toute UPDATE state passe par cette fonction. C1 TC-INV-02, TC-NEG-12. Contournement via SQL direct — bloqué par trigger PG prevent_forbidden_state_transition.
INV-287-08 Révocation effective <= 2s P95 Publication Redis pub/sub share.revoked + invalidation cache session local (TTL cache 500ms). C1, C3 TC-NOM-09, mesure perf k6 (N>=200 échantillons, cf. Point de vigilance #5). Charge sortant du nominal (1 lien, 10 RPS, staging mono) non couverte — documenté.
INV-287-09 Expiration TTL coupe les accès Worker scan 30s + check inline expires_at < now() dans ShareProofGuard. C1, C7 TC-NOM-10, TC-ERR-11. Drift horloge entre instances. Mitigation : now() PostgreSQL side, pas Node.js.
INV-287-10 REVOKED/EXPIRED terminaux ShareStateMachine refuse toute transition sortante depuis ces états. Trigger PG défense en profondeur. C1, C13 TC-INV-02, TC-NEG-12. Réactivation demandée business — refusée par design.
INV-287-11 Destinataire ne peut créer de partage SharingController.create() vérifie ownership via JwtAuthGuard ; session destinataire n'a pas de JWT owner. C1 TC-ERR-18, TC-NEG-05. Confusion de contexte d'auth — tests d'isolation de rôles.
INV-287-12 Anti-enumeration 404 générique Middleware GenericErrorMapper dans SharingController scope destinataire : mappe NotFoundException, ForbiddenException, InvalidTokenException, SessionExpiredException{ error: 'NOT_FOUND' } avec message statique. C1 TC-INV-06, TC-ERR-05, TC-NEG-02. Point de vigilance #1 : 429 sur OTP_BLOCKED/REVOKED introduit un signal différenciant. Documenté comme écart accepté (spec impose 429) ; mitigation : appliquer jitter sur temps de réponse 429 pour homogénéiser latence avec 404.
INV-287-13 Journal append-only + hash-chain + ancrage Merkle Table share_audit_events sans DELETE/UPDATE (permissions PG + trigger prevent_audit_mutation). Chaque row : prev_hash, event_hash=sha256(canonical(payload)+prev_hash). Worker Merkle batch 100 entries → racine → ancrage blockchain (service existant). C5, C6, C13 TC-NOM-14, TC-ERR-14, TC-INV-14. Ordre multi-batchs post-crash (Point de vigilance #14) — sérialisation par audit_sequence_number BIGSERIAL + worker mono-instance via advisory lock PG.
INV-287-15 Rétention RGPD J+90 + audit conservé RetentionWorker cron quotidien + hook opportuniste sur transition terminale programmant un job J+90. Suppression/anonymisation colonnes destinataire ; audit préservé (colonnes dédiées recipient_email_anonymized post-purge). C8 TC-NOM-19, TC-INV-12. Journal contient déjà recipient_email → stocké en version hashée (recipient_email_sha256) dans share_audit_events, pas en clair (cf. Point de vigilance #15).
INV-287-16 Secrets temporaires chiffrés au repos Colonnes *_envelope_b64 chiffrées via KMS envelope ; DEK en mémoire uniquement dans HSM ; lint CI no-plaintext-key sur migrations. C4, C13 TC-INV-08, TC-NEG-06. Backup PostgreSQL — hors périmètre story, assumée sécurisée par infra.
INV-287-17 Trust-store obligatoire TrustStoreService chargé au bootstrap NestJS ; exception bloquante si trust-store absent/corrompu → app ne démarre pas. Fail-closed documenté. C9 TC-INV-07, TC-ERR-16, TC-NEG-08. Définition de "incomplet" (Point de vigilance #25) : check présence racine plateforme + OCSP stapling (si configuré) ; absence racine = fail, absence OCSP = warning + rejet si TRUST_STORE_REQUIRE_OCSP=true.
INV-287-18 Tests roundtrip crypto Suite test/crypto-roundtrip/ : pre.reKeyGen → reEncrypt → décapsulation simulée client avec vecteurs fixes. Obligatoire avant merge (CI gate). C4, C14 TC-INV-09. Stub cryptographique — interdit, lint CI.
INV-287-19 Purge proactive artefacts temporaires RetentionWorker.purgeOnBoot() au démarrage + hook ShareStateMachine.onTerminal() planifie purge artefacts crypto temporaires (IV jetables, capsules mises en cache côté worker). C8, C1 TC-NOM-17, TC-NEG-04. Crash du worker → next boot purge.
INV-287-20 Lock + idempotence + rate-limit + réconciliation + clearing Redis (ioredis) pour locks (TTL 600s scope share:*) et rate-limit (token bucket par IP/lien/preuve/compte). Table share_idempotency_keys avec contrainte UNIQUE. Worker réconciliation cron 300s. C1, C7 TC-NOM-05/06/15/16/18, TC-ERR-04/08/12/13/19/20, TC-NEG-01/09/10/13. Redis down → fail-closed (retour 503) ; pas de fallback local (cohérence distribuée).
INV-287-21 Atomicité DB + audit synchrone Audit append-only dans la même transaction SQL que l'UPDATE d'état. Si AuditJournalService.append() throw → rollback transaction → 503 vers client. Ancrage Merkle async (worker séparé). C5, C1 TC-INV-04, TC-INV-05, TC-NOM-16, TC-ERR-14. Outbox pattern rejeté ici (trop permissif vs fail-closed).
INV-287-22 Contrôle inter-module ShareProofGuard (C12) enregistré dans ProofsModule scope. Aucune route Proofs destinataire ne bypasse. C12 TC-INV-03, TC-NEG-05. Ajout futur de route dans ProofsModule sans décorateur @ShareContext() → bypass. Mitigation : test de non-régression checklist sur ProofsController.
INV-287-23 Révocation non rétroactive Journal immuable (trigger PG). Révocation ajoute un nouvel événement, ne modifie jamais les précédents. C5 TC-NOM-14.
INV-287-24 Mentions UX explicites API owner renvoie { warnings: ['no-drm-ack-required' (si non ack), 'revocation-future-only'] } sur GET /shares. Persistance user_preferences.no_drm_ack_at_utc. C1 TC-NOM-11. Scope ack = compte utilisateur (ADR-287-02).
INV-287-25 Règles testables ou hors périmètre Matrice de tests §2 spec exhaustive ; toute règle non couverte → documentée §10 (Hors périmètre). C14 Audit Gate 5

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

Critère Mécanisme(s) Composant Observable Risque
CA-287-01 DTO CreateShareDto + pipeline validation ; default ttl_seconds=604800 ; bornes @Min(900) @Max(2592000) C1 HTTP 400 sur hors-bornes, réponse 201 avec expires_at = created_at + 604800s si omis
CA-287-02 INSERT … state='PENDING_ACTIVATION' C1, C13 Row DB
CA-287-03 ShareProofGuard refuse si pas de recipient_session_id valide C12, C3 404 générique sur route proof sans session
CA-287-04 OtpService incrément + transition état + otp_blocked_until_utc C2 Row share_otp_state, row share_audit_events(LINK_OTP_BLOCKED) (événement interne, pas dans enum D-287-23) Nouvel événement audit interne — cf. Point #26
CA-287-05 OtpService.recordBlock() incrémente blocks_count, trigger NotificationDispatcher à 3 C2, C11 Row notification + log canal
CA-287-06 Redis token bucket otp-resend:{share_link_id} C2 429 + Retry-After
CA-287-07 Transition ACTIVE + session + audit LINK_ACTIVATED C2, C3, C5 Row + événement audit
CA-287-08 Pub/sub share.revoked + invalidation cache session + mesure P95 C1, C3 Metric Prometheus share_revocation_latency_p95_ms Mesure requiert perf test dédié (k6, N≥200)
CA-287-09 Worker expiration + check inline guard C7, C12 404 générique post-TTL
CA-287-10 ShareStateMachine.canTransition() + trigger PG C1, C13 Tentative transition rejetée (exception)
CA-287-11 ShareProofGuard refuse sans session C12 404
CA-287-12 Payload réponse conforme §5.4 F-287-03 C1, C4 Réponse contient 6 champs listés Format encrypted_document : inline base64 streamé (ADR-287-01)
CA-287-13 ExportComposerService + feature flag C10 ZIP vérifiable offline Désactivé par défaut
CA-287-14 Audit zero-knowledge (test sécurité) C14 Logs+traffic+dump+DB scrubbed Point #11 — protocole déterministe
CA-287-15 Contrôle hash WORM avant/après C14 SHA-256 identique
CA-287-16 Journal append-only + worker Merkle + ancrage C5, C6 Rows audit + batch row + TXID blockchain
CA-287-17 Immuabilité DB (trigger) C5 Rows inchangées post-revoke
CA-287-18 GenericErrorMapper + jitter 429 C1 Enveloppe homogène 404 Point #1
CA-287-19 PreCryptoService.generateEphemeralIdentity() par lien C4 Certs distincts
CA-287-20 SharingController.create gated par JWT owner C1 404 générique si session destinataire (anti-enum)
CA-287-21 Redis token bucket par scope C1 429
CA-287-22 Redis SET NX locks C1 409 SHARE_BUSY
CA-287-23 Table idempotence + JCS hash payload C1 409 conflict / replay identique Point #17 — JCS RFC8785
CA-287-24 ReconciliationWorker C7 État cohérent post-run
CA-287-25 RetentionWorker.purgeOnBoot + hook terminal C8 Absence artefacts
CA-287-26 API warnings[] + flag user_preferences.no_drm_ack_at_utc C1 Ack row Scope compte (ADR-287-02)
CA-287-27 Page activation destinataire : bandeau RGPD obligatoire (front) + event audit LINK_VIEWED avec flag rgpd_info_shown=true C1 + front (hors périmètre backend, stubbé PD-287-FRONT) Event audit Stub front tracé PD-287-FRONT
CA-287-28 Suite roundtrip CI gate C14 Tests verts
CA-287-29 TrustStoreService fail-closed C9 503 si trust-store incomplet Point #25
CA-287-30 RetentionWorker J+90 C8 Rows purgées, audit conservé
CA-287-31 OtpService transition → REVOKED sur otp_failed_attempts_total >= 50 C2 État final + notification Point #4 — DoS accepté par spec

5. Mapping tests → mécanismes + observables

Test ID Réf spec Mécanisme(s) Point(s) d'observation Niveau
TC-NOM-01 F-287-01, CA-287-01/02 Création flow complet Row DB, event audit, réponse HTTP Integration
TC-NOM-02 F-287-02, CA-287-03/07/27 Activation OTP Transition état, session, event, bandeau RGPD audit Integration
TC-NOM-03 F-287-02, CA-287-04 5 échecs OTP → OTP_BLOCKED Row share_otp_state.otp_blocked_until_utc Integration
TC-NOM-04 5.5 FSM OTP_BLOCKED → ACTIVE, reset compteur Row session + OTP state Integration
TC-NOM-05 CA-287-05 3 blocages → notif Row notification Integration
TC-NOM-06 CA-287-06 Rate-limit renvoi OTP Redis counter + 429 Integration
TC-NOM-07 F-287-03, CA-287-12 Consultation nominale Payload complet 6 champs + audit LINK_VIEWED Integration
TC-NOM-08 F-287-04, CA-287-13 Export ZIP (feature-flag ON) ZIP valide, signature vérifiée offline Integration
TC-NOM-09 INV-287-08, CA-287-08 Révocation → round-trip Pub/sub + 404 suivant Integration + Perf
TC-NOM-10 F-287-06, CA-287-09 Expiration TTL Transition EXPIRED + event Integration
TC-NOM-11 CA-287-26, INV-287-24 Warnings UX warnings[] puis flag ack Integration (API)
TC-NOM-12 INV-287-05 Identités éphémères distinctes Certs distincts en base Unit
TC-NOM-13 INV-287-03 WORM inchangé SHA-256 original identique Integration
TC-NOM-14 INV-287-13/23 Journal + hash-chain + ancrage Rows audit + TXID Integration
TC-NOM-15 INV-287-20, CA-287-22/23 Lock + idempotence Redis keys + 409 Integration
TC-NOM-16 INV-287-21, CA-287-24 Atomicité + réconciliation Transaction + orphelins rattrapés Integration
TC-NOM-17 INV-287-19, CA-287-25 Purge artefacts Filesystem/DB sans résiduels Integration
TC-NOM-18 INV-287-20, CA-287-31 Seuil OTP cumulé Transition REVOKED + notif Integration
TC-NOM-19 INV-287-15, CA-287-30 Rétention J+90 Rows purgées Integration
TC-ERR-01..03 D-287-01/02/07 Validation DTO HTTP 400 Unit
TC-ERR-04 CA-287-21 Rate-limit création 429 Integration
TC-ERR-05 CA-287-18 Anti-enum homogène 4 cas → même 404 Integration
TC-ERR-06 ERR-287-06 OTP invalide < seuil 404 + compteur+1 Integration
TC-ERR-07 ERR-287-07 Seuil session atteint 429 + OTP_BLOCKED Integration
TC-ERR-08 ERR-287-08 Renvoi OTP > limite 429 Integration
TC-ERR-09 ERR-287-09 Session inactive 404 + reauth_required Integration
TC-ERR-10 INV-287-08 Révocation en session Refus round-trip suivant Integration
TC-ERR-11 INV-287-09 Expiration en session Refus Integration
TC-ERR-12 CA-287-22 Lock non acquis 409 SHARE_BUSY Integration
TC-ERR-13 CA-287-23 Idempotence conflit 409 IDEMPOTENCY_CONFLICT Integration
TC-ERR-14 INV-287-13/21 Journal indispo 503 fail-closed Integration (fault injection)
TC-ERR-15 INV-287-02 Crypto PRE échec 503 Integration (fault injection)
TC-ERR-16 INV-287-17 Trust-store absent 503 au bootstrap Integration
TC-ERR-17 ERR-287-17 ZIP trop gros 413 Integration
TC-ERR-18 CA-287-20 Re-partage destinataire 404 Integration
TC-ERR-19 ERR-287-19 Idempotence hors fenêtre 409 Integration
TC-ERR-20 CA-287-31 Compteur=49 + échec 429 + REVOKED Integration
TC-INV-01..14 Invariants Contrôles dédiés Rows + logs + metrics Unit+Integration
TC-INV-11 INV-287-01 Zero-knowledge bornée Capture tcpdump + grep mémoire + scan logs Sécurité (protocole déterministe)
TC-NR-01..05 Non-régression Snapshot API Golden files Integration
TC-NEG-01..14 Adversarial Fuzz + brute-force + race Logs sécurité Sécurité
Perf P95 CA-287-08, INV-287-08 k6 scenario N≥200 req Histogramme latence Perf

5bis. Périmètre de test

Niveau In scope Hors scope (justification)
Unitaire Tous composants C1–C12 : DTOs, ShareStateMachine, OtpService, PreCryptoService mocks HSM, parseurs JCS, GenericErrorMapper, TrustStoreService
Intégration Interactions SharingModule ↔ ProofsModule ↔ Redis ↔ PostgreSQL ↔ stub HSM ; workers (réconciliation, retention, Merkle) contre DB réelle Intégration Mail SMTP réelle → stubbée (stub trace PD-287-MAIL-STUB, exploité via MailService existant)
E2E Flux complets owner → recipient → revoke/expire via API gateway staging Front destinataire (hors périmètre backend, tracé PD-287-FRONT)
Perf P95 révocation (CA-287-08), P95 création (§5.2), N≥200 échantillons staging mono-instance charge nominale (1 lien, 10 RPS) Scénarios multi-instances / charge sur-nominale → PD-287-PERF-EXT
Sécurité Fuzz token (length/charset), brute-force OTP multi-session, grep mémoire process, scan logs, scan DB secrets clairs, trust-store fail-closed, replay idempotence Pen-test manuel tiers → hors périmètre MVP (PD-287-PENTEST)

Couverture minimale attendue : 80 % lignes + 80 % branches sur le périmètre in-scope (C1–C12). Seuils mesurés via Jest coverage + --coverageThreshold.

6. Gestion des erreurs

Palette HTTP et mapping :

Cas Code Corps Effet bord
Validation DTO (ERR-287-01/02/03) 400 { error: 'VALIDATION_ERROR', details: [...] } Aucun
Quota/rate-limit création (ERR-287-04) 429 { error: 'RATE_LIMITED' }, Retry-After: <s> Aucun
Destinataire : toute erreur invalidité/inexistence (ERR-287-05/06/09/10/11) 404 { error: 'NOT_FOUND' } (jitter latence) Incrément compteur OTP si applicable
OTP session bloqué (ERR-287-07) 429 { error: 'RATE_LIMITED' }, Retry-After: 900 Transition OTP_BLOCKED
Resend OTP rate-limit (ERR-287-08) 429 Retry-After Aucun
Lock non acquis (ERR-287-12) 409 { error: 'SHARE_BUSY' } Aucun
Idempotence conflit (ERR-287-13) 409 { error: 'IDEMPOTENCY_CONFLICT' } Aucun
Idempotence hors fenêtre (ERR-287-19) 409 { error: 'IDEMPOTENCY_WINDOW_EXPIRED' } Aucun
Journal indispo (ERR-287-14) 503 { error: 'SERVICE_UNAVAILABLE' } Rollback transaction
Crypto échec (ERR-287-15) 503 idem Aucun
Trust-store (ERR-287-16) 503 idem Aucun (ou crash boot si fatal)
ZIP trop gros (ERR-287-17) 413 { error: 'PAYLOAD_TOO_LARGE' } Aucun
Re-partage destinataire (ERR-287-18) 404 { error: 'NOT_FOUND' } Event audit sécurité (anti-enum INV-287-12)
Cumul OTP = 50 (ERR-287-20) 429 Retry-After: 0 Transition REVOKED + notif owner

Anti-catch-absorb (learning 2026-03-08) : tout catch qui appelle auditLog DOIT re-throw OU utiliser finally. Règle ESLint custom audit-must-throw déployée.

7. Impacts sécurité

Axe Risque Mitigation
Zero-knowledge Fuite document en mémoire/log Backend ne déchiffre jamais. Scrubber logs. Scan CI secrets clairs (detect-secrets).
Anti-enumeration 429 différenciant (écart #1) Jitter latence 429 aligné P95 404, masquage header X-OTP-Blocked-Until. Risque résiduel documenté.
DoS via token volé Seuil 50 OTP → REVOKED terminal (écart #4) Rate-limit IP open_requests_per_ip_per_min=30 en amont. Owner peut recréer un lien. Dette : mécanisme de "freeze" intermédiaire non implémenté (hors périmètre MVP, suivi PD-287-EXT-1).
OTP TTL Spec absente (écart #10) Ajout plan : OTP_CODE_TTL_SECONDS=300s (constante, non configurable MVP), appliquée à la validation. Documenté en Point de vigilance #10 et Hypothèse H-plan-03.
Chiffrement données destinataire (écart #15) Email/IP/UA en clair en DB Ajout plan : colonnes recipient_email, client_ip, user_agent stockées via pgcrypto pgp_sym_encrypt clé dérivée KMS. recipient_email_sha256 indexé pour recherche.
Trust-store incomplet (écart #25) Vérification faible Définition : racine plateforme obligatoire + CRL téléchargeable + OCSP stapling si TRUST_STORE_REQUIRE_OCSP=true. Check explicite au bootstrap.
Ordre Merkle multi-batchs (écart #14) Tree poisoning Worker mono-instance via pg_advisory_lock(MERKLE_WORKER_LOCK_ID) + séquenceur audit_sequence_number.
Idempotence hashing (écart #17) Faux positif conflit JCS RFC 8785 appliqué avant hashing (@transmute/json-canon-rs ou équivalent).
Session hijacking Session id deviné recipient_session_id = UUIDv4 + cookie HttpOnly; Secure; SameSite=Strict + fingerprint IP log-only.
Replay URL token Token propagé par email forwarding OTP à chaque session (INV-287-04) bride l'impact ; log IP+UA pour détection anomale ; risque accepté (UX vs sécurité).
Timing attack OTP Comparaison naïve crypto.timingSafeEqual obligatoire.

Conformité RGPD : - Consentement RGPD affiché avant activation (CA-287-27, côté front stubbé). - Droit à l'oubli : RetentionWorker J+90 ; request DELETE /owner/shares/:id/recipient-data possible en avance sur purge automatique (hors spec MVP — tracé PD-287-EXT-2). - Audit conservation légale : justifiée par valeur probatoire (art. 6.1.c RGPD obligation légale / 6.1.f intérêt légitime).

8. Hypothèses techniques

ID Hypothèse Impact si faux
H-plan-01 ProofsModule existant expose ProofsService.getEncryptedDocument(proof_id) et ProofsService.assertOwner(proof_id, userId). Refactor ProofsModule requis (hors périmètre).
H-plan-02 Service HSM existant fournit PRE (re-key-gen, re-encrypt) via wrapper ; sinon, intégration lib externe (Umbral PRE ou NuCypher ReCrypt). Lot additionnel C4 ×2 charge ; décision architecture formelle Gate 5.
H-plan-03 OTP TTL = 300s retenu (écart Gate 3 #10, non décidé par PO). Valeur à confirmer Gate 5 ; ajustable par constante sans migration.
H-plan-04 Ancrage blockchain : service existant BlockchainAnchorClient fournissant TXID sur envoi racine Merkle. Intégration externe si absent.
H-plan-05 Trust-store plateforme = fichier PEM + politique JSON chargés via env TRUST_STORE_PATH. Alternative Vault dynamic secrets si spec infra impose.
H-plan-06 Scope mémorisation warning "pas de DRM" = compte utilisateur serveur (ADR-287-02, écart #16). Refactor UX côté propriétaire si scope device.
H-plan-07 Front destinataire stubbé côté backend (stub PD-287-FRONT) — l'API renvoie toutes les données nécessaires mais le rendu est livré par une autre story.
H-plan-08 Idempotence canonique via JCS RFC 8785 (écart #17). Tous clients doivent soit envoyer JCS soit accepter 409 sur divergence.
H-plan-09 Reauth endpoints /reauth/request-otp et /reauth/confirm partagent le même rate-limiter que l'activation initiale (écart #3). Surface attaque reauth = activation.

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

  1. Anti-enumeration vs 429 (écart Gate 3 #1 — Bloquant) : spec impose 429 sur OTP_BLOCKED/REVOKED, INV-287-12 impose 404 homogène. Plan suit spec (429) + jitter latence. Risque résiduel de fingerprinting par latence ; documenté et accepté ; à reconfronter Gate 8 via métriques.
  2. Format payload consultation (écart #2 — Bloquant) : ADR-287-01 retient le document chiffré inline (base64) dans la réponse JSON vs URL présignée, car INV-287-01 interdit l'exposition d'un flux clair et une URL présignée impliquerait un service Proofs servant du chiffré — acceptable mais plus de surface. Taille borne : max_inline_document_bytes = 50 MB, au-delà → 413 ou streaming binaire application/octet-stream multipart (tracé PD-287-LARGE).
  3. Endpoints reauth (écart #3 — Bloquant) : plan les formalise (POST /sharing/:token/reauth/request-otp, POST /sharing/:token/reauth/confirm) avec le même rate-limit et la même anti-enum que l'activation initiale. Audit event LINK_REAUTH_* ajouté (hors enum D-287-23 — extension spec mineure à tracer dans YAML index).
  4. DoS via seuil OTP cumulé (écart #4 — Bloquant) : plan suit spec (REVOKED terminal). Mitigation : rate-limit IP en amont + possibilité pour owner de recréer un lien. Piste d'évolution tracée PD-287-EXT-1 (seuil intermédiaire non-terminal).
  5. SLA P95 non déterministe en test unitaire (écart #5 — Bloquant) : exige suite perf dédiée (k6 N≥200). TC-NOM-09 split en TC-NOM-09a (fonctionnel single-request) et TC-NOM-09b (perf). Budget CI : job nightly perf-staging.
  6. FSM no-op (écart #6 — Majeur) : ShareStateMachine.transition(from, to)from==tothrow ForbiddenTransitionError côté API, mapping 409 owner / 404 destinataire. Plan explicite.
  7. INV-287-08 configurabilité (écart #7 — Majeur) : plan fixe valeur non configurable MVP (constante REVOCATION_P95_SLA_MS=2000), conforme §5.3.
  8. Reset compteur OTP session (écart #8 — Majeur) : plan documente reset sur transition OTP_BLOCKED→ACTIVE + sur ACTIVE frais (post-activation valide). Règle explicitée dans OtpService.resetSessionCounter().
  9. Formats owner_key_ref / encrypted_doc_ref (écart #9 — Majeur) : plan définit owner_key_ref = HSM KeyHandle (opaque string ≤ 128 chars) et encrypted_document = base64 inline (cf. point 2). À proposer comme extension au §5.1 spec.
  10. OTP TTL (écart #10 — Majeur) : OTP_CODE_TTL_SECONDS=300s (5 min) par défaut. Cohérent avec pratique OTP mail (Stripe 15min, GitHub 10min — 5min plus strict). Ajuster Gate 5 si PO challenge.
  11. Grep mémoire process non déterministe (écart #11 — Majeur) : TC-INV-11 formalisé en protocole reproductible : (1) lancer test sous Node --expose-gc, (2) déclencher cycle complet, (3) global.gc() ×3, (4) process.memoryUsage().heapSnapshot, (5) grep heap dump (node --heapsnapshot-signal=SIGUSR2) pour motifs document clair. Tolérance : heap dump post-3×GC sans motif. Script scripts/security/memory-scan.sh.
  12. max_consultations_per_link (écart #12 — Majeur) : paramètre §5.2 sans contrat → plan traite comme feature optionnelle désactivée par défaut (max_consultations_per_link=0 → illimité). Si activé par config : incrément compteur sur LINK_VIEWED, transition → EXPIRED quand atteint + LINK_EXPIRED(reason: consultation_limit). Pas de test obligatoire MVP (feature dormante).
  13. 429 sur états FSM (écart #13 — Majeur) : plan applique 429 + Retry-After non informatif (Retry-After: 0 pour REVOKED), conformité spec. Risque de monitoring ambigu mitigé par label métrique retry_reason.
  14. Ordre Merkle multi-batchs (écart #14 — Majeur) : plan impose worker Merkle mono-instance via pg_advisory_lock + audit_sequence_number strictement monotone (BIGSERIAL) + batchs ordonnés par séquence. Post-crash : reprise depuis le dernier batch ancré confirmé.
  15. Chiffrement données destinataire (écart #15 — Majeur) : plan ajoute chiffrement pgp_sym_encrypt clé KMS sur recipient_email/client_ip/user_agent. Migration + index hashé pour recherche.
  16. Scope mémorisation warning DRM (écart #16 — Majeur) : ADR-287-02 retient compte utilisateur serveur.
  17. Idempotence canonique (écart #17 — Majeur) : JCS RFC 8785. Documenté dans l'API (Idempotency-Key doc page).
  18. Borne D-287-31 vs seuil REVOKED (écart #18 — Mineur) : plan définit check count >= 50 avant incrément (sémantique : la 50e tentative échouée déclenche REVOKED, stockage max 49 ou 50 selon transaction). Garde-fou race via lock OTP.
  19. H-287-07 manquante (écart #19 — Mineur) : coquille spec. Plan non impacté.
  20. Export skip (écart #20 — Mineur) : tests export describe.skip conditionnel sur process.env.EXPORT_ZIP_ENABLED !== 'true'. Documenté CI.
  21. D-287-09 formule vs regex (écart #21 — Mineur) : plan traite comme règle de cohérence, validée par trigger PG CHECK (expires_at = created_at + make_interval(secs => ttl_seconds)).
  22. Ownership mutable (écart #22 — Mineur) : plan documente hypothèse ownership immuable durant la vie du lien. Si futur transfert : REVOKED automatique (trigger PG ON UPDATE proofs.owner_user_id). Hors périmètre MVP, tracé PD-287-EXT-3.
  23. Diagramme d'état incomplet (écart #23 — Mineur) : plan ne modifie pas la spec, documente en annotation.
  24. Périmètre INV-287-01 (écart #24 — Mineur) : plan énumère le périmètre d'audit : services C1, C2, C3, C4, C5, C6, C7, C8, C10, C12 + logs applicatifs + DB secondaires (replicas). Exclu : systèmes externes (HSM, KMS, Blockchain client).
  25. Trust-store incomplet (écart #25 — Mineur) : définition formelle dans TrustStoreService.
  26. Événements audit hors enum D-287-23 : plan étend l'enum contractuel audit_event_type avec 3 événements additionnels : LINK_OTP_BLOCKED, LINK_REAUTH_REQUESTED, LINK_REAUTH_CONFIRMED. Ces événements sont inclus dans la même chaîne hash et le même journal append-only (pas de table séparée). Cette extension est une correction spec proposée alignée sur les flux réauth documentés dans la spec §5.5. Les tests TC correspondants doivent couvrir ces événements.
  27. Charge Redis : single-instance staging MVP. Multi-instance → tracé PD-287-HA.
  28. HSM rate-limit : PreCryptoService peut saturer HSM en cas de pic. Circuit breaker (opossum) + file d'attente bornée.

10. Hors périmètre

  • Front destinataire (web/mobile) : livré par story dédiée (stub PD-287-FRONT). Les tests contractuels TC-NOM-02 (info RGPD avant activation) et TC-NOM-11 (warning DRM propriétaire) sont vérifiés côté backend via les réponses API (champ rgpd_notice_url dans la réponse activation, champ no_drm_ack_required dans la réponse création). La vérification UI complète est déléguée à PD-287-FRONT.
  • Pentest manuel tiers : PD-287-PENTEST.
  • Multi-instance HA Redis/Postgres : PD-287-HA.
  • Pages OAuth / SSO destinataire (invitation remplace compte) : hors scope par spec.
  • Mobile offline consultation : exclu par spec §2.
  • Partage inter-comptes ProbatioVault : exclu par spec.
  • Décision judiciaire admissibilité : non technique.
  • Révocation partielle (consultation vs export) : exclue par spec.
  • Heartbeats signés (preuve d'absence d'accès) : hors MVP.
  • Transfert ownership pendant vie du lien : PD-287-EXT-3.
  • Seuil intermédiaire non-terminal anti-DoS OTP : PD-287-EXT-1.
  • API DELETE /owner/shares/:id/recipient-data on-demand : PD-287-EXT-2.
  • Scénarios perf sur-nominaux : PD-287-PERF-EXT.
  • Performance multi-instance / failover : hors périmètre MVP.

Annexe A — ADR courts

ADR-287-01 — Livraison du document chiffré destinataire - Contexte : spec §5.4 F-287-03 dit "document chiffré" ; diagramme dit encrypted_doc_ref. - Décision : livraison inline base64 dans la réponse JSON (jusqu'à 50 MB), sinon stream binaire. - Conséquences : simplicité de l'atomicité zero-knowledge, pas de second service exposé. Charge réseau accrue vs URL présignée ; acceptable pour documents probatoires (<= 50 MB typique).

ADR-287-02 — Scope mémorisation warning "pas de DRM" - Contexte : écart Gate 3 #16. - Décision : persistance serveur au niveau compte utilisateur (user_preferences.no_drm_ack_at_utc). - Conséquences : cohérence multi-device, testable server-side, aligné privacy (pas de cookie).

ADR-287-03 — Worker Merkle mono-instance - Contexte : écart Gate 3 #14. - Décision : pg_advisory_lock garantit un seul worker actif simultané ; séquenceur audit_sequence_number strict. - Conséquences : SPOF worker (mitigé par failover rapide via advisory lock release sur crash TCP). Alternative multi-instance avec coordination (ZooKeeper/etcd) hors périmètre MVP.