Aller au contenu

PD-81 - Plan d'implementation Legal PRE

Statut: PLAN D'IMPLEMENTATION Version: 1.2.0 Date: 2026-02-23 Source normative: PD-81-specification.md v1.0.0 Perimetre: EPIC PD-189 (Cryptographie, preuves et acces legaux)


1. Hypotheses

Les hypotheses suivantes sont faites en l'absence d'information explicite dans la specification ou le codebase existant. Chacune doit etre validee avant Gate 5.

ID Hypothese Impact si invalide
H-01 Le TSP reel n'est pas disponible en backend actuel. Une interface ITspVerifier + stub est utilisee comme dette technique tracee (pas comme suffisance). Le stub valide le format et la structure du mandat mais NE verifie PAS la cryptographie eIDAS reelle. Conformite AC-81-01 partielle: la conformite eIDAS complete requiert un TSP reel, delivree via story de dependance dediee. TODO trace obligatoire sur chaque methode du stub avec reference AC-81-01-PARTIAL. [CORRECTION v2 — ecart BLOQUANT TSP] Si TSP deja present, adapter l'import au lieu de creer l'interface. Story de dependance TSP reel a creer si non existante.
H-02 Le mapping PD-82 DPO -> PARENT, LegalOfficer -> AUTHORITY est un mapping logique au sein de PD-81; les enums PD-82 (ValidatorType.PARENT/AUTHORITY) ne sont pas modifies. Les etats intermediaires PENDING_DPO/PENDING_LEGAL_OFFICER sont des alias semantiques presentes en DTO/API, mappes vers les etats PD-82 reels en couche service. Si la spec exige des enums dedies PD-82, il faut un refactoring de PD-82 (hors scope).
H-03 La resolution documentId mandat -> documentId interne est effectuee par un appel a DocumentsModule existant (methode de lookup par identifiant externe). Si aucune methode de lookup n'existe, il faut la creer dans le DocumentsModule.
H-04 La cle publique de l'autorite judiciaire (bobPublicKey) est extraite du payload du mandat eIDAS, verifiee comme controle BLOQUANT dans la sequence generateLegalReKey: (1) verification TSP (signature/chaine/CRL/OCSP) via ITspVerifier, (2) rapprochement IAM via ILegalIdentityResolver.resolveJudicialAuthority(bobPublicKey). Si l'une des deux verifications echoue, la generation de ReKey est REFUSEE (fail-closed, nouveau code erreur ERR_BOB_IDENTITY_UNVERIFIED). Ce n'est PAS une hypothese optionnelle — c'est un controle de securite obligatoire (spec §9.2 CORRECTION v2). [CORRECTION v2 — ecart MAJEUR bobPublicKey] Si l'IAM est deja integre, adapter l'interface.
H-05 L'espace de stockage isole pour les ReKey legales est un schema PostgreSQL dedie (vault_legal) ou une colonne discriminante storageDomain='LEGAL' sur les tables existantes. L'implementation retient la colonne discriminante avec un repository guard (custom repository TypeORM appliquant systematiquement le filtre storageDomain='LEGAL') pour eviter tout oubli de discriminant. Si l'isolation par schema est exigee, refactoring des entites.
H-06 Le HashService existant (PD-38) produit du SHA3-256; c'est bien l'algorithme utilise pour eventPayloadHashSha3 et mandateHashSha3. Verification du code hash.service.ts confirmee: utilise @noble/hashes SHA3-256.
H-07 Le BatchService (PD-39) est utilise tel quel pour l'ancrage probatoire; les evenements Legal PRE sont ajoutes comme items de batch existants. Le delai max 24h est garanti par: (1) la cadence de batch existante, (2) un scheduler de monitoring (LegalAnchoringMonitor) qui verifie toutes les 6h que tous les evenements de plus de 18h sont ancres, et declenche une alerte + ancrage force si depassement. Si la cadence actuelle est > 24h, le monitor detecte et force l'ancrage.
H-08 Les identites userId IAM sont disponibles dans le contexte de requete via le decorator @CurrentUser() existant et le JWT token. Si l'identite juridique requiert un enrichissement supplementaire, adapter le guard.
H-09 La destruction cryptographique (DESTROYED) consiste en la zeroisation de la colonne kfrags (overwrite avec zeros puis DELETE) dans la meme transaction, tracee probatoirement. Si une destruction physique differente est exigee (ex: HSM-backed key destroy), adapter le mecanisme.
H-10 Le module legal-pre est enregistre dans app.module.ts au meme titre que les autres modules. Standard NestJS, pas de risque.

2. Architecture et decoupage en composants

2.1 Vue d'ensemble du module

src/modules/legal-pre/
├── legal-pre.module.ts                    # Module NestJS principal
├── constants/
│   └── legal-pre.constants.ts             # Constantes contractuelles
├── enums/
│   ├── legal-access-event-type.enum.ts    # Types d'evenements probatoires
│   ├── legal-rekey-status.enum.ts         # Etats du cycle de vie ReKey
│   └── legal-mandate-status.enum.ts       # Etats de validation du mandat
├── interfaces/
│   ├── legal-pre.interfaces.ts            # Interfaces du service orchestrateur
│   ├── tsp-verifier.interface.ts          # Interface TSP (stub)
│   └── legal-identity-resolver.interface.ts # Interface resolution identite
├── dto/
│   ├── register-legal-mandate.dto.ts      # DTO enregistrement mandat
│   ├── submit-internal-validation.dto.ts  # DTO validation interne
│   ├── activate-legal-access.dto.ts       # DTO activation acces
│   ├── close-legal-access.dto.ts          # DTO cloture acces
│   ├── get-legal-audit-proof.dto.ts       # DTO preuve composite
│   ├── legal-consultation.dto.ts          # DTO consultation document
│   └── legal-access-status-response.dto.ts # DTO reponse statut
├── entities/
│   ├── legal-mandate.entity.ts            # Entite mandat judiciaire
│   ├── legal-validation-case.entity.ts    # Entite dossier validation
│   ├── legal-rekey.entity.ts              # Entite ReKey legale
│   ├── legal-access-event.entity.ts       # Entite evenement probatoire
│   └── legal-composite-proof.entity.ts    # Entite preuve composite
├── services/
│   ├── legal-pre-orchestrator.service.ts  # Orchestrateur principal (§9.1)
│   ├── mandate-validator.service.ts       # Validation eIDAS du mandat
│   ├── legal-validation-adapter.service.ts # Adaptation PD-82
│   ├── legal-rekey-manager.service.ts     # Gestion cycle de vie ReKey
│   ├── legal-audit-trail.service.ts       # Tracabilite probatoire
│   ├── legal-composite-proof.service.ts   # Assemblage preuve composite
│   └── legal-destruction.service.ts       # Destruction cryptographique
├── providers/
│   ├── tsp-verifier.stub.ts               # Stub TSP (TODO: reel)
│   └── legal-identity-resolver.stub.ts    # Stub resolution identite
├── repositories/
│   └── legal-rekey.repository.ts          # Custom repository avec filtre storageDomain automatique (ECT-07)
├── guards/
│   ├── legal-pre-access.guard.ts          # Controle d'acces Legal PRE
│   ├── legal-write-block.guard.ts         # Interception write/delete (ERR-81-11) [v1.2.0]
│   └── legal-rate-limit.guard.ts          # Rate limiting par ReKey
├── schedulers/
│   ├── legal-rekey-expiration.scheduler.ts     # Expiration TTL ReKey
│   ├── legal-rekey-destruction.scheduler.ts    # Destruction planifiee
│   └── legal-anchoring-monitor.scheduler.ts    # Monitoring ancrage 24h (ECT-08)
├── controllers/
│   └── legal-pre.controller.ts            # Endpoints REST
└── __tests__/
    ├── fixtures/
    │   ├── legal-mandate.fixture.ts       # Mandats de test
    │   ├── legal-rekey.fixture.ts         # ReKeys de test
    │   └── legal-identities.fixture.ts    # Identites de test
    ├── legal-pre-orchestrator.service.spec.ts
    ├── mandate-validator.service.spec.ts
    ├── legal-validation-adapter.service.spec.ts
    ├── legal-rekey-manager.service.spec.ts
    ├── legal-audit-trail.service.spec.ts
    ├── legal-composite-proof.service.spec.ts
    ├── legal-destruction.service.spec.ts
    ├── legal-write-block.guard.spec.ts
    ├── legal-rate-limit.guard.spec.ts
    ├── legal-rekey-expiration.scheduler.spec.ts
    ├── legal-rekey-destruction.scheduler.spec.ts
    ├── legal-anchoring-monitor.scheduler.spec.ts
    ├── legal-rekey.repository.spec.ts
    └── legal-pre.integration.spec.ts

2.2 Dependances entre composants (ordre de construction)

Phase 1 - Fondations (pas de dependance inter-PD-81)
  ├── constants/legal-pre.constants.ts
  ├── enums/*
  ├── interfaces/*
  └── entities/*

Phase 2 - Providers et services de base (depend Phase 1)
  ├── providers/tsp-verifier.stub.ts
  ├── providers/legal-identity-resolver.stub.ts
  ├── services/mandate-validator.service.ts    (← ITspVerifier, HashService, entities)
  ├── services/legal-audit-trail.service.ts    (← HsmAuditLoggerService, HashService, BatchService, entities)
  └── services/legal-destruction.service.ts    (← entities, legal-audit-trail)

Phase 3 - Services metier (depend Phase 2 + PD existants)
  ├── services/legal-validation-adapter.service.ts (← DualValidationService, entities)
  ├── services/legal-rekey-manager.service.ts      (← PreService, entities, legal-audit-trail)
  └── services/legal-composite-proof.service.ts    (← InclusionProofService, entities)

Phase 4 - Orchestrateur et controles (depend Phase 3)
  ├── services/legal-pre-orchestrator.service.ts   (← tous les services Phase 2-3)
  ├── guards/legal-pre-access.guard.ts
  ├── guards/legal-rate-limit.guard.ts
  ├── schedulers/legal-rekey-expiration.scheduler.ts
  └── schedulers/legal-rekey-destruction.scheduler.ts

Phase 5 - Controller et module (depend Phase 4)
  ├── controllers/legal-pre.controller.ts
  └── legal-pre.module.ts

3. Composants detailles

// Contexte contractuel obligatoire (INV-81-12)
export const LEGAL_PRE_CONTEXT_ID = 'LEGAL_PRE_MANDATE';

// Bornes TTL ReKey (INV-81-01)
export const LEGAL_REKEY_MAX_TTL_DAYS = 30;
export const LEGAL_REKEY_MAX_TTL_SECONDS = 30 * 24 * 60 * 60; // 2_592_000

// Bornes TTL validation (spec §3)
export const LEGAL_VALIDATION_DEFAULT_TTL_DAYS = 7;
export const LEGAL_VALIDATION_MIN_TTL_SECONDS = 3600;       // 1h
export const LEGAL_VALIDATION_MAX_TTL_SECONDS = 2_592_000;   // 30j

// Destruction deadline (INV-81-07, spec §7.3)
export const LEGAL_DESTRUCTION_DEFAULT_DEADLINE_SECONDS = 3600;  // 1h
export const LEGAL_DESTRUCTION_MIN_DEADLINE_SECONDS = 60;        // 1min
export const LEGAL_DESTRUCTION_MAX_DEADLINE_SECONDS = 86_400;    // 24h

// Rate limiting (spec §10.2)
export const LEGAL_RATE_LIMIT_DEFAULT_PER_MINUTE = 10;
export const LEGAL_RATE_LIMIT_MIN = 1;
export const LEGAL_RATE_LIMIT_MAX = 60;

// Revocation invalidation delay (spec §10.2)
export const LEGAL_REVOCATION_MAX_DELAY_MS = 5000; // 5 secondes

// Ancrage max delay (INV-81-08)
export const LEGAL_ANCHORING_MAX_DELAY_HOURS = 24;
export const LEGAL_ANCHORING_MONITOR_INTERVAL_HOURS = 6;     // Frequence check (ECT-08)
export const LEGAL_ANCHORING_ALERT_THRESHOLD_HOURS = 18;     // Seuil alerte (ECT-08)

// Storage domain isolation (INV-81-10, ECT-07)
export const LEGAL_STORAGE_DOMAIN = 'LEGAL';
export const STANDARD_STORAGE_DOMAIN = 'STANDARD';

// Scheduler intervals
export const LEGAL_EXPIRATION_CHECK_INTERVAL_MS = 30_000;     // 30s
export const LEGAL_DESTRUCTION_CHECK_INTERVAL_MS = 60_000;    // 60s

Observables: Constantes importees dans tous les composants; toute deviation est une erreur de compilation TypeScript.

3.2 Enums

export enum LegalReKeyStatus {
  ACTIVE = 'ACTIVE',
  REVOKED = 'REVOKED',
  EXPIRED = 'EXPIRED',
  COMPLETED = 'COMPLETED',
  DESTROYED = 'DESTROYED',
}

Transitions contractuelles (spec §7.3): - ACTIVE -> REVOKED (via revokeReKey()) - ACTIVE -> EXPIRED (echeance expiresAt) - ACTIVE -> COMPLETED (via closeLegalAccess + END_OF_CONSULTATION) - REVOKED -> DESTROYED (destruction cryptographique, delai max destructionDeadline) - EXPIRED -> DESTROYED (idem) - COMPLETED -> DESTROYED (idem)

Observable: Le guard de consultation refuse tout statut != ACTIVE. Le scheduler de destruction traite les statuts REVOKED|EXPIRED|COMPLETED.

export enum LegalAccessEventType {
  // Evenements probatoires (TSA obligatoire)
  MANDATE_QUALIFIED = 'LEGAL_MANDATE_QUALIFIED',
  VALIDATION_SUBMITTED = 'LEGAL_VALIDATION_SUBMITTED',
  VALIDATION_ACTIVATED = 'LEGAL_VALIDATION_ACTIVATED',
  LEGAL_ACCESS_ACTIVATED = 'LEGAL_ACCESS_ACTIVATED',
  LEGAL_DOCUMENT_ACCESSED = 'LEGAL_DOCUMENT_ACCESSED',
  LEGAL_ACCESS_CLOSED = 'LEGAL_ACCESS_CLOSED',
  LEGAL_REKEY_REVOKED = 'LEGAL_REKEY_REVOKED',
  LEGAL_REKEY_DESTROYED = 'LEGAL_REKEY_DESTROYED',

  // Evenements informatifs (pas de TSA obligatoire)
  MANDATE_REJECTED = 'LEGAL_MANDATE_REJECTED',
  VALIDATION_REJECTED = 'LEGAL_VALIDATION_REJECTED',
  ACCESS_DENIED_OUT_OF_SCOPE = 'LEGAL_ACCESS_DENIED_OUT_OF_SCOPE',
  ACCESS_DENIED_WRITE_ATTEMPT = 'LEGAL_ACCESS_DENIED_WRITE_ATTEMPT',
  ACCESS_DENIED_EXPIRED = 'LEGAL_ACCESS_DENIED_EXPIRED',
  ACCESS_DENIED_RATE_LIMIT = 'LEGAL_ACCESS_DENIED_RATE_LIMIT',
  REUSE_AFTER_DESTRUCTION = 'LEGAL_REUSE_AFTER_DESTRUCTION',
  SECURITY_ALERT = 'LEGAL_SECURITY_ALERT',
}

export const PROBATIVE_EVENT_TYPES: LegalAccessEventType[] = [
  LegalAccessEventType.MANDATE_QUALIFIED,
  LegalAccessEventType.VALIDATION_SUBMITTED,
  LegalAccessEventType.VALIDATION_ACTIVATED,
  LegalAccessEventType.LEGAL_ACCESS_ACTIVATED,
  LegalAccessEventType.LEGAL_DOCUMENT_ACCESSED,
  LegalAccessEventType.LEGAL_ACCESS_CLOSED,
  LegalAccessEventType.LEGAL_REKEY_REVOKED,
  LegalAccessEventType.LEGAL_REKEY_DESTROYED,
];

Observable: Seuls les PROBATIVE_EVENT_TYPES declenchent l'horodatage TSA obligatoire. Les evenements informatifs sont journalises avec HSM+SHA3 mais sans blocage fail-closed sur echec TSA.

export enum LegalMandateStatus {
  VALID = 'VALID',
  INVALID = 'INVALID',
}

3.3 Interfaces

Interfaces du service orchestrateur, strictement conformes aux §9.1 et §9.2 de la spec:

// --- §9.1 Orchestrateur PD-81 ---

export interface RegisterLegalMandateInput {
  mandatePayload: Buffer;           // Mandat signe complet
  contextId: string;                // Doit etre LEGAL_PRE_MANDATE
  metadata?: Record<string, string>;
}

export interface LegalMandateResult {
  mandateId: string;
  legalCaseId: string;
  validationStatus: LegalMandateStatus;
  state: string;                    // PENDING_BOTH
  scopeDocumentIds: string[];
  receivedAt: Date;
}

export interface SubmitInternalValidationInput {
  legalCaseId: string;
  validatorRole: 'DPO' | 'LegalOfficer';
  validatorIdentity: string;        // userId IAM
  signature: Buffer;
  signatureAlgorithm: string;
  certificateChain: string;
  dataToSign: Buffer;
}

export interface ValidationStateResult {
  legalCaseId: string;
  newState: string;
  isActivation: boolean;
  validationId: string;
}

export interface ActivateLegalAccessInput {
  legalCaseId: string;
  ttlSeconds?: number;              // Defaut selon mandat, max 30j
}

export interface LegalAccessActivationResult {
  legalReKeyId: string;
  mandateId: string;
  scopeDocumentIds: string[];
  expiresAt: Date;
  status: LegalReKeyStatus;
}

export interface CloseLegalAccessInput {
  legalReKeyId: string;
  reason: 'REVOKE' | 'EXPIRE' | 'END_OF_CONSULTATION';
  actorIdentity: string;
}

export interface LegalAccessClosureResult {
  legalReKeyId: string;
  newStatus: LegalReKeyStatus;
  closedAt: Date;
}

export interface GetLegalAuditProofInput {
  mandateId?: string;
  legalCaseId?: string;
  requesterId: string;              // userId du demandeur
}

// --- §9.2 Extension PD-41 ---

export interface LegalConstraints {
  mandateId: string;
  isLegal: true;
  ttl: number;                      // en secondes, <= 2_592_000
  scopeDocumentIds: string[];
  destructionDeadline?: number;     // en secondes, defaut 3600
}

// [CORRECTION v2 — ecart BLOQUANT §9.2] Interface strictement conforme a la signature
// contractuelle generateLegalReKey(alicePublicKey, bobPublicKey, contextId, legalConstraints).
// ownerSecretKey et ownerSigningKey sont EXCLUS car non prevus par le contrat §9.2.
// La delegation a PD-41 PreService.generateReKey() gere les secrets en interne.
export interface GenerateLegalReKeyInput {
  alicePublicKey: string;           // ownerPublicKey
  bobPublicKey: string;             // recipientPublicKey (autorite judiciaire)
  contextId: string;                // LEGAL_PRE_MANDATE
  legalConstraints: LegalConstraints;
}

export interface RevokeLegalReKeyInput {
  legalReKeyId: string;
  reason: string;
  actorIdentity: string;
}

export interface RevocationResult {
  legalReKeyId: string;
  newStatus: LegalReKeyStatus;
  revokedAt: Date;
}

export interface LegalAccessStatus {
  legalReKeyId: string;
  status: LegalReKeyStatus | 'UNKNOWN';
  mandateId?: string;
  scopeDocumentIds?: string[];
  expiresAt?: Date;
  issuedAt?: Date;
}

Observable: Les types sont exports et utilises comme contrats entre services. Toute rupture de contrat est une erreur de compilation.

tsp-verifier.interface.ts

export interface TspVerificationResult {
  valid: boolean;
  issuerIdentity?: string;
  issuerRole?: string;
  signatureProfile?: string;
  certificateChainValid?: boolean;
  revocationStatus?: 'good' | 'revoked' | 'unknown';
  validFrom?: Date;
  validUntil?: Date;
  bobPublicKey?: string;           // Cle publique autorite judiciaire
  scopeDocumentIds?: string[];      // Identifiants de documents du mandat
  reason?: string;                  // Motif de rejet
}

export interface ITspVerifier {
  verifyMandateSignature(mandatePayload: Buffer): Promise<TspVerificationResult>;
}

export const TSP_VERIFIER_TOKEN = 'TSP_VERIFIER';

Observable: Le token d'injection permet de substituer le stub par une implementation reelle sans modifier les consommateurs. Le stub est marque avec un TODO trace.

export interface LegalIdentityInfo {
  userId: string;
  legalName: string;
  isAuthorizedJudicialAuthority: boolean;
  eidasLevel: 'substantial' | 'high';
}

export interface ILegalIdentityResolver {
  resolveJudicialAuthority(bobPublicKey: string): Promise<LegalIdentityInfo | null>;
  resolveInternalValidator(userId: string, role: string): Promise<LegalIdentityInfo | null>;
}

export const LEGAL_IDENTITY_RESOLVER_TOKEN = 'LEGAL_IDENTITY_RESOLVER';

3.4 Entites TypeORM

@Entity({ schema: 'vault_secure', name: 'legal_mandate' })
export class LegalMandate {
  @PrimaryGeneratedColumn('uuid')
  mandateId: string;

  @Column({ type: 'varchar', length: 512 })
  issuerIdentity: string;

  @Column({ type: 'varchar', length: 128 })
  issuerRole: string;

  @Column({ type: 'varchar', length: 128 })
  signatureProfile: string;

  @Column({ type: 'text' })
  certificateChainRef: string;

  @Column({ type: 'varchar', length: 32, default: 'good' })
  revocationStatus: string;  // 'good' | 'revoked' | 'unknown'

  @Column({ type: 'timestamptz' })
  validFrom: Date;

  @Column({ type: 'timestamptz' })
  validUntil: Date;

  @Column({ type: 'jsonb' })
  scopeDocumentIds: string[];

  @Column({ type: 'char', length: 64 })
  mandateHashSha3: string;

  @Column({ type: 'timestamptz', default: () => 'NOW()' })
  receivedAt: Date;

  @Column({ type: 'varchar', length: 16 })
  validationStatus: LegalMandateStatus;

  @Column({ type: 'bytea', select: false })
  mandatePayloadRaw: Buffer;        // Payload brut (non expose en API)
}

Index: idx_legal_mandate_status sur validationStatus.

@Entity({ schema: 'vault_secure', name: 'legal_validation_case' })
export class LegalValidationCase {
  @PrimaryGeneratedColumn('uuid')
  legalCaseId: string;

  @Column({ type: 'uuid' })
  @Index('idx_legal_case_mandate')
  mandateId: string;

  @Column({ type: 'varchar', length: 128, default: LEGAL_PRE_CONTEXT_ID })
  contextId: string;

  @Column({ type: 'varchar', length: 32 })
  state: string;  // Etats PD-82 adaptes

  @Column({ type: 'int', default: LEGAL_VALIDATION_DEFAULT_TTL_DAYS * 86400 })
  validationTtlSeconds: number;

  @Column({ type: 'uuid', nullable: true })
  validator1Identity: string | null;

  @Column({ type: 'varchar', length: 32, nullable: true })
  validator1Role: string | null;  // 'DPO'

  @Column({ type: 'bytea', nullable: true, select: false })
  validator1Signature: Buffer | null;

  @Column({ type: 'timestamptz', nullable: true })
  validator1At: Date | null;

  @Column({ type: 'uuid', nullable: true })
  validator2Identity: string | null;

  @Column({ type: 'varchar', length: 32, nullable: true })
  validator2Role: string | null;  // 'LegalOfficer'

  @Column({ type: 'bytea', nullable: true, select: false })
  validator2Signature: Buffer | null;

  @Column({ type: 'timestamptz', nullable: true })
  validator2At: Date | null;

  @Column({ type: 'timestamptz', default: () => 'NOW()' })
  createdAt: Date;

  @Column({ type: 'timestamptz', nullable: true })
  expiresAt: Date | null;

  @ManyToOne(() => LegalMandate)
  @JoinColumn({ name: 'mandateId' })
  mandate: LegalMandate;
}

Observable: validator1Identity et validator2Identity sont compares (INV-81-03): si egaux, la 2e validation est refusee (ERR-81-08).

@Entity({ schema: 'vault_secure', name: 'legal_rekey' })
export class LegalReKey {
  @PrimaryGeneratedColumn('uuid')
  legalReKeyId: string;

  @Column({ type: 'uuid' })
  @Index('idx_legal_rekey_mandate')
  mandateId: string;

  @Column({ type: 'uuid' })
  legalCaseId: string;

  @Column({ type: 'boolean', default: true })
  isLegal: boolean;

  @Column({ type: 'varchar', length: 128, default: LEGAL_PRE_CONTEXT_ID })
  contextId: string;

  @Column({ type: 'jsonb' })
  scopeDocumentIds: string[];

  @Column({ type: 'int' })
  ttlSeconds: number;

  @Column({ type: 'int', default: LEGAL_DESTRUCTION_DEFAULT_DEADLINE_SECONDS })
  destructionDeadline: number;

  @Column({ type: 'timestamptz' })
  issuedAt: Date;

  @Column({ type: 'timestamptz' })
  expiresAt: Date;

  @Column({ type: 'varchar', length: 16, default: LegalReKeyStatus.ACTIVE })
  @Index('idx_legal_rekey_status')
  status: LegalReKeyStatus;

  @Column({ type: 'varchar', length: 16, default: LEGAL_STORAGE_DOMAIN })
  storageDomain: string;

  // Artefact PRE temporaire (chiffre, zeroisation a destruction)
  @Column({ type: 'bytea', select: false, nullable: true })
  encryptedKfrags: Buffer | null;

  // Metadata PRE pour reconstruction
  @Column({ type: 'varchar', length: 64 })
  ownerPublicKeyHash: string;

  @Column({ type: 'varchar', length: 64 })
  recipientPublicKeyHash: string;

  @Column({ type: 'timestamptz', nullable: true })
  terminalStateAt: Date | null;      // Quand le statut est devenu terminal

  @ManyToOne(() => LegalMandate)
  @JoinColumn({ name: 'mandateId' })
  mandate: LegalMandate;

  @ManyToOne(() => LegalValidationCase)
  @JoinColumn({ name: 'legalCaseId' })
  validationCase: LegalValidationCase;
}

Observables: - encryptedKfrags est select: false (jamais dans les queries par defaut) et nullable (zeroisation = set null apres overwrite). - storageDomain discrimine les ReKey legales des ReKey standard (INV-81-10). - terminalStateAt permet au scheduler de destruction de calculer le deadline.

@Entity({ schema: 'vault_secure', name: 'legal_access_event' })
export class LegalAccessEvent {
  @PrimaryGeneratedColumn('uuid')
  eventId: string;

  @Column({ type: 'varchar', length: 64 })
  @Index('idx_legal_event_type')
  eventType: LegalAccessEventType;

  @Column({ type: 'uuid' })
  @Index('idx_legal_event_mandate')
  mandateId: string;

  @Column({ type: 'uuid' })
  legalCaseId: string;

  @Column({ type: 'uuid', nullable: true })
  legalReKeyId: string | null;

  @Column({ type: 'varchar', length: 512 })
  actorIdentity: string;

  @Column({ type: 'varchar', length: 64 })
  actorRole: string;

  @Column({ type: 'timestamptz', default: () => 'NOW()' })
  eventAt: Date;

  @Column({ type: 'char', length: 64 })
  eventPayloadHashSha3: string;

  @Column({ type: 'bytea' })
  hsmSignature: Buffer;

  @Column({ type: 'varchar', length: 256, nullable: true })
  tsaTokenRef: string | null;

  @Column({ type: 'varchar', length: 256, nullable: true })
  merkleLeafRef: string | null;

  @Column({ type: 'varchar', length: 256, nullable: true })
  anchoringBatchRef: string | null;
}

Observable: La table est append-only (aucune operation UPDATE/DELETE autorisee; enforcement par trigger PostgreSQL comme pour ValidationRecord PD-82).

@Entity({ schema: 'vault_secure', name: 'legal_composite_proof' })
export class LegalCompositeProof {
  @PrimaryGeneratedColumn('uuid')
  proofId: string;

  @Column({ type: 'uuid' })
  @Index('idx_legal_proof_mandate')
  mandateId: string;

  @Column({ type: 'varchar', length: 16, default: '1.0.0' })
  proofVersion: string;

  @Column({ type: 'jsonb' })
  mandateEvidenceRef: object;

  @Column({ type: 'jsonb' })
  doubleValidationEvidenceRef: object;

  @Column({ type: 'jsonb' })
  rekeyLifecycleEvidenceRef: object;

  @Column({ type: 'jsonb' })
  auditLogEvidenceRef: object;

  @Column({ type: 'jsonb' })
  anchoringEvidenceRef: object;

  @Column({ type: 'timestamptz', default: () => 'NOW()' })
  generatedAt: Date;

  @Column({ type: 'jsonb' })
  verificationMaterial: object;     // Suffisant pour verification hors plateforme
}

3.5 Services

3.5.1 mandate-validator.service.ts

Responsabilite: Validation eIDAS du mandat (flux N1, INV-81-02).

Dependances: ITspVerifier (inject via token), HashService (PD-38), DocumentsModule (lookup).

Methodes:

Methode Description INV/ERR couverts
validateMandate(payload: Buffer) Orchestre la validation complete du mandat INV-81-02, ERR-81-01..06
verifySignature(payload) Delegation au TSP ERR-81-02
verifyCertificateChain(payload) Delegation au TSP ERR-81-03
verifyRevocationStatus(payload) CRL/OCSP via TSP ERR-81-04
verifyTemporalWindow(validFrom, validUntil) Controle fenetre temporelle vs now() ERR-81-05
extractAndResolveScope(payload) Extraction documentIds + resolution interne ERR-81-06
computeMandateHash(payload) SHA3-256 du payload brut INV-81-08

Observable: Chaque etape emet un log structure. Si une etape echoue, la methode validateMandate leve une exception typee et aucun dossier n'est cree (fail-closed).

Mecanisme fail-closed (INV-81-11): - Toute exception dans verifySignature ou verifyCertificateChain provoque un rejet immediat. - L'indisponibilite TSP (catch sur appel TSP) leve LegalPreException avec code TSP_UNAVAILABLE (ERR-81-15). - Aucun fallback: si le TSP ne repond pas, la validation echoue.

Responsabilite: Adaptation du workflow PD-82 au contexte Legal PRE (flux N2, INV-81-03).

Dependances: DualValidationService (PD-82 import), LegalValidationCase repository.

Mapping des roles:

DPO         -> ValidatorType.PARENT
LegalOfficer -> ValidatorType.AUTHORITY

Mapping des etats (presentation API):

PENDING_BOTH       -> PENDING_BOTH (identique)
PENDING_AUTHORITY   -> PENDING_LEGAL_OFFICER (alias)
PENDING_PARENT      -> PENDING_DPO (alias)
ACTIVATED           -> ACTIVATED (identique)
REJECTED            -> REJECTED (identique)
EXPIRED             -> EXPIRED (identique)

Methodes:

Methode Description INV/ERR couverts
createLegalValidationCase(mandateId, validationTtl) Cree dossier + delegation a PD-82 INV-81-03
submitValidation(input: SubmitInternalValidationInput) Mappe role puis delegue a PD-82 INV-81-03, ERR-81-08
getValidationState(legalCaseId) Lit l'etat du dossier avec alias -

Controle d'identite juridique (INV-81-03, ERR-81-08): Avant de deleguer submitValidation a PD-82: 1. Recuperer le LegalValidationCase par legalCaseId. 2. Si validation 1 deja faite ET input.validatorIdentity === case.validator1Identity → rejet ERR_SAME_IDENTITY (ERR-81-08). 3. Sinon deleguer a DualValidationService.submitValidation() avec le mapping de role.

Observable: Le controle d'identite est effectue AVANT la delegation, donc PD-82 n'est jamais appele si les userId sont identiques.

Responsabilite: Gestion du cycle de vie des ReKey legales (flux N3, INV-81-04, INV-81-05, INV-81-07, INV-81-10).

Dependances: PreService (PD-41), LegalReKey repository, LegalAuditTrailService, HashService.

Methodes:

Methode Description INV/ERR couverts
generateLegalReKey(input) Generation ReKey legale temporaire INV-81-04, INV-81-05, INV-81-10, INV-81-12, ERR-81-09
revokeReKey(input) Revocation immediate INV-81-01, INV-81-07, ERR-81-12
expireReKey(legalReKeyId) Expiration TTL (appele par scheduler) INV-81-01
closeByEndOfConsultation(legalReKeyId, actor) Cloture sur fin consultation INV-81-07
getLegalAccessStatus(legalReKeyId) Statut courant -
validateConsultationAccess(legalReKeyId, documentId) Controle acces consultation INV-81-05, INV-81-06, ERR-81-10, ERR-81-11, ERR-81-12, ERR-81-16, ERR-81-17

generateLegalReKey - detail: 1. Verifier contextId === LEGAL_PRE_CONTEXT_ID (INV-81-12). Sinon → ERR (S2). 2. Verifier ttl <= LEGAL_REKEY_MAX_TTL_SECONDS (ERR-81-09). 3. Verifier scopeDocumentIds non vide (INV-81-05). 4. [CORRECTION v2 — controle bloquant bobPublicKey] Verifier l'authenticite de bobPublicKey: a. Appeler ITspVerifier.verifyMandateSignature() pour valider la chaine de certificats associee a bobPublicKey. Si echec → ERR ERR_BOB_IDENTITY_UNVERIFIED (fail-closed). b. Appeler ILegalIdentityResolver.resolveJudicialAuthority(bobPublicKey) pour rapprocher l'identite IAM. Si retourne null ou isAuthorizedJudicialAuthority=false → ERR ERR_BOB_IDENTITY_UNVERIFIED (fail-closed). 5. Appeler PreService.generateReKey() avec contextId=LEGAL_PRE_MANDATE et scope=mandateId. 5. Stocker le PreReKeyArtefact.kfrags chiffre dans encryptedKfrags (domain LEGAL). 6. Valider l'artefact via PreService.validate(). 7. Creer l'entite LegalReKey avec tous les champs obligatoires. 8. Emettre evenement probatoire LEGAL_ACCESS_ACTIVATED.

revokeReKey - detail: 1. Charger la ReKey. Si inexistante → UNKNOWN. Si terminale → idempotent (R5). 2. Dans une transaction SERIALIZABLE: a. Update status = REVOKED, terminalStateAt = NOW(). b. L'invalidation est effective dans cette transaction (< 5s garanti par DB). 3. Emettre evenement probatoire LEGAL_REKEY_REVOKED.

validateConsultationAccess - detail: 1. Charger la ReKey avec legalReKeyId. Si inexistante → ERR-81-12 (UNKNOWN). 2. Si status !== ACTIVE → ERR-81-12 (expiree/revoquee/detruite) ou ERR-81-17 (post-destruction). 3. Si NOW() >= expiresAt → declencher expireReKey() inline, puis ERR-81-12. 4. Verifier mandateId coherent avec le dossier legal associe (ERR-81-16). 5. Verifier documentId in scopeDocumentIds (ERR-81-10, INV-81-05). 6. Mode lecture seule enforce par: (a) l'absence de methodes d'ecriture sur l'API legal, ET (b) le LegalWriteBlockGuard qui intercepte toute tentative PUT/PATCH/DELETE, emet evenement ACCESS_DENIED_WRITE_ATTEMPT, et retourne 403 (ERR-81-11, INV-81-06) [CORRECTION v2].

Observable: Chaque methode retourne un resultat type. Les rejets levent LegalPreException avec un code specifique mappe sur les ERR-81-*.

Responsabilite: Tracabilite probatoire complete (INV-81-08, INV-81-09).

Dependances: HsmAuditLoggerService (PD-37), HashService (PD-38), BatchService + InclusionProofService (PD-39), LegalAccessEvent repository.

Methodes:

Methode Description INV/ERR couverts
emitProbativeEvent(params) Emission d'un evenement probatoire complet INV-81-08, ERR-81-13, ERR-81-14
emitInformativeEvent(params) Emission d'un evenement informatif INV-81-08 (partiel)
addToAnchoringBatch(event) Ajout au batch d'ancrage PD-39 INV-81-08
getEventsForMandate(mandateId) Recuperation des evenements INV-81-09

emitProbativeEvent - detail (chemin critique, fail-closed): 1. Construire le payload canonique de l'evenement (JSON deterministe). 2. Calculer eventPayloadHashSha3 via HashService.hash() (PD-38). 3. Signer le hash via HsmService.sign() (PD-37). Si echec → fail-closed: lever LegalPreException(ERR_HSM_SIGNING_FAILED) (ERR-81-13). L'operation appelante DOIT etre annulee. 4. Demander horodatage TSA via BatchService ou appel direct RFC 3161. Si echec → fail-closed: lever LegalPreException(ERR_TSA_TIMESTAMP_FAILED) (ERR-81-14). L'operation appelante DOIT etre annulee. 5. Persister LegalAccessEvent avec tous les champs obligatoires. 6. Ajouter l'evenement au batch d'ancrage periodique (PD-39) de facon asynchrone (non bloquant, cadence max 24h garanti par batch scheduler existant).

Observable: Le fail-closed est implement par des exceptions propagees; l'appelant encapsule l'emission dans la meme transaction que l'operation metier, donc un rollback annule tout.

Responsabilite: Assemblage de la preuve composite (INV-81-09, AC-81-12).

Dependances: LegalAccessEvent repository, InclusionProofService (PD-39), LegalCompositeProof repository.

Methodes:

Methode Description INV/ERR couverts
generateProof(mandateId) Assemble la preuve composite complete INV-81-09, AC-81-12
verifyProofIntegrity(proofId) Verifie la coherence interne INV-81-09

generateProof - detail: 1. Recuperer tous les LegalAccessEvent pour le mandateId. 2. Recuperer le LegalMandate (evidence du mandat). 3. Recuperer le LegalValidationCase (evidence de double validation). 4. Recuperer les LegalReKey (evidence du cycle de vie). 5. Pour chaque evenement probatoire, recuperer InclusionProof via PD-39. 6. Assembler LegalCompositeProof avec: - mandateEvidenceRef: hash mandat, certificats, timestamps. - doubleValidationEvidenceRef: identites, signatures, timestamps. - rekeyLifecycleEvidenceRef: creation, revocation/expiration, destruction. - auditLogEvidenceRef: chainage hash evenements, signatures HSM. - anchoringEvidenceRef: Merkle roots, inclusion proofs, batch IDs. - verificationMaterial: cles publiques HSM, racines de confiance TSA, algorithmes.

Observable: La preuve est verifiable hors plateforme car elle contient tout le materiel cryptographique necessaire (cles publiques, algorithmes, preuves d'inclusion).

Responsabilite: Destruction cryptographique verifiable (INV-81-07, flux N4).

Dependances: LegalReKey repository, LegalAuditTrailService.

Methodes:

Methode Description INV/ERR couverts
destroyReKey(legalReKeyId) Zeroisation + passage a DESTROYED INV-81-07
processDestructionQueue() Traitement batch des destructions en attente INV-81-07, INV-81-01

destroyReKey - detail (transaction SERIALIZABLE): 1. Charger la ReKey. Si deja DESTROYED → idempotent (retour success). 2. Si status not in [REVOKED, EXPIRED, COMPLETED] → erreur interne (ne devrait pas arriver). 3. Overwrite encryptedKfrags avec un buffer de zeros de meme taille (Buffer.alloc(originalSize, 0)). 4. Set encryptedKfrags = null. 5. Update status = DESTROYED. 6. Emettre evenement probatoire LEGAL_REKEY_DESTROYED.

Observable: La zeroisation est une operation en 2 temps (overwrite zeros puis set null) pour minimiser la fenetre de recuperation en memoire/WAL.

Responsabilite: Point d'entree principal, orchestration des 5 etapes du flux (§9.1).

Dependances: Tous les services ci-dessus.

Methodes (mappage direct §9.1):

Methode Flux Description
registerLegalMandate(input) N1 Validation mandat + creation dossier
submitInternalValidation(input) N2 Double validation adaptee
activateLegalAccess(input) N3 Activation PRE legale
closeLegalAccess(input) N4 Cloture + destruction
getLegalAuditProof(input) - Export preuve composite
consultDocument(legalReKeyId, documentId, actorIdentity) N3.3-4 Consultation bornee

registerLegalMandate - orchestration: 1. Verifier input.contextId === LEGAL_PRE_CONTEXT_ID (INV-81-12). 2. Appeler mandateValidator.validateMandate(input.mandatePayload) (N1.1-5). 3. Si invalide → emettre evenement MANDATE_REJECTED + lever exception. 4. Persister LegalMandate avec tous champs extraits. 5. Appeler legalValidationAdapter.createLegalValidationCase(mandateId, ttl) (N1.7). 6. Emettre evenement probatoire MANDATE_QUALIFIED (N1.6). 7. Retourner LegalMandateResult.

activateLegalAccess - orchestration: 1. Charger le LegalValidationCase par legalCaseId. 2. Verifier state === ACTIVATED (ERR-81-07). 3. Verifier validite temporelle du mandat associe (validUntil >= now). 4. Charger le LegalMandate associe. 5. Appeler legalReKeyManager.generateLegalReKey(...) (N3.1-2). 6. Retourner LegalAccessActivationResult.

closeLegalAccess - orchestration: 1. Charger la ReKey par legalReKeyId. 2. Selon input.reason: - REVOKElegalReKeyManager.revokeReKey(...). - END_OF_CONSULTATIONlegalReKeyManager.closeByEndOfConsultation(...). - EXPIRElegalReKeyManager.expireReKey(...). 3. La destruction est geree par le scheduler de destruction (asynchrone, deadline respecte). 4. Retourner LegalAccessClosureResult.

getLegalAuditProof - orchestration [CORRECTION v2 — ecart MAJEUR AC-81-14]: 1. Verifier l'autorisation du demandeur: requesterId doit etre le ownerUserId du coffre cible OU posseder un role explicitement delegue dans IAM pour l'audit legal (spec §9.1 post-condition). La delegation est verifiee via ILegalIdentityResolver.resolveInternalValidator(requesterId, 'audit_delegate'). 2. Si non autorise → ERR + journal ACCESS_DENIED + evenement informatif. 3. Appeler legalCompositeProof.generateProof(mandateId).

consultDocument - orchestration: 1. Appeler legalReKeyManager.validateConsultationAccess(legalReKeyId, documentId). 2. Si autorise, effectuer la re-encryption via PreService.reEncrypt(). 3. Emettre evenement probatoire LEGAL_DOCUMENT_ACCESSED. 4. Retourner le document re-chiffre.

3.6 Guards

Responsabilite: Controle d'acces aux endpoints Legal PRE.

Roles autorises: 'dpo', 'legal_officer', 'vault_owner' (pour audit). [CORRECTION v2 — ecart MAJEUR: role admin retire car non rattache explicitement dans la spec contractuelle Legal PRE (spec §5 N2, §9.1-§9.2). Seuls les roles explicitement cadres par la spec sont autorises.]

Observable: Refuse toute requete d'un utilisateur sans role legal explicitement prevu par la spec.

Responsabilite: Interception explicite de toute tentative d'ecriture/suppression sur les endpoints Legal PRE (ERR-81-11, INV-81-06, AC-81-08).

Implementation: Guard NestJS applique globalement sur le controller LegalPreController qui: 1. Detecte les methodes HTTP PUT, PATCH, DELETE sur toute route /legal-pre/**. 2. Emet un evenement probatoire ACCESS_DENIED_WRITE_ATTEMPT via LegalAuditTrailService.emitInformativeEvent() avec l'identite de l'acteur, le document cible, et la methode HTTP tentee. 3. Retourne HTTP 403 avec code WRITE_OPERATION_FORBIDDEN.

Observable: Le test E11 verifie que: - Toute requete PUT/PATCH/DELETE sur /legal-pre/access/:id/consult est rejetee 403. - Un evenement ACCESS_DENIED_WRITE_ATTEMPT est emis dans legal_access_event. - Le document WORM est non altere.

Pourquoi un guard et pas juste l'absence de routes: L'absence de routes d'ecriture est un invariant structurel, mais un guard explicite fournit: 1. Un point d'observabilite journalise probatoirement (exige par le test E11). 2. Une protection defense-en-profondeur si un endpoint est accidentellement ajoute. 3. Un chemin de code testable par E11.

Responsabilite: Rate limiting par legalReKeyId (ERR-81-18, spec §10.2).

Implementation: Compteur Redis avec cle legal-pre:rate:{legalReKeyId}, TTL 60s, increment atomique.

Configuration: - Default: 10 consultations/minute. - Configurable via ConfigService dans [1..60].

Observable: Retourne HTTP 429 avec Retry-After header. Emet evenement ACCESS_DENIED_RATE_LIMIT.

3.7 Schedulers

Responsabilite: Detection et traitement des ReKey expirees (INV-81-01).

Cron: Toutes les 30 secondes (LEGAL_EXPIRATION_CHECK_INTERVAL_MS).

Logique: 1. Query: SELECT * FROM legal_rekey WHERE status = 'ACTIVE' AND expiresAt <= NOW(). 2. Pour chaque ReKey trouvee: appeler legalReKeyManager.expireReKey(legalReKeyId).

Observable: Nombre de ReKey expirees loggue a chaque execution. La granularite de 30s garantit que le delai d'expiration effectif est < 30s apres expiresAt (bien en dessous du seuil de 5s pour la revocation, car ici c'est une expiration naturelle).

Responsabilite: Destruction cryptographique dans le delai contractuel (INV-81-07).

Cron: Toutes les 60 secondes (LEGAL_DESTRUCTION_CHECK_INTERVAL_MS).

Logique: 1. Query: SELECT * FROM legal_rekey WHERE status IN ('REVOKED','EXPIRED','COMPLETED') AND terminalStateAt IS NOT NULL. 2. Pour chaque ReKey: appeler legalDestruction.destroyReKey(legalReKeyId). 3. Si NOW() - terminalStateAt > destructionDeadline → alerte critique (deadline depassee).

Observable: La destruction intervient au plus tard 60s apres l'entree dans l'etat terminal, bien en dessous du deadline par defaut de 1h. L'alerte sur deadline depasse est un garde-fou supplementaire.

3.8 Controller

Base path: /legal-pre

Authentication: @ApiBearerAuth() + @UseGuards(JwtAuthGuard, LegalPreAccessGuard)

Methode Route Guard supplementaire Service
POST /legal-pre/mandates registerLegalMandate - orchestrator.registerLegalMandate()
POST /legal-pre/validations submitInternalValidation - orchestrator.submitInternalValidation()
POST /legal-pre/access/activate activateLegalAccess - orchestrator.activateLegalAccess()
POST /legal-pre/access/close closeLegalAccess - orchestrator.closeLegalAccess()
GET /legal-pre/access/:legalReKeyId/status getLegalAccessStatus - legalReKeyManager.getLegalAccessStatus()
POST /legal-pre/access/:legalReKeyId/consult consultDocument LegalRateLimitGuard orchestrator.consultDocument()
GET /legal-pre/proofs/:mandateId getLegalAuditProof - orchestrator.getLegalAuditProof()
POST /legal-pre/rekeys/:legalReKeyId/revoke revokeReKey - orchestrator.closeLegalAccess({reason:'REVOKE'})

4. Flux de donnees et sequences

4.1 Sequence N1 - Enregistrement mandat

Client -> Controller.registerLegalMandate(input)
  -> Orchestrator.registerLegalMandate(input)
    -> [1] MandateValidator.validateMandate(payload)
      -> [1.1] ITspVerifier.verifyMandateSignature(payload)  // Fail-closed INV-81-11
      -> [1.2] HashService.hash(payload)                      // mandateHashSha3
      -> [1.3] DocumentsModule.resolveDocumentIds(externalIds) // ERR-81-06
    -> [2] LegalMandate.save()                                // Persistance
    -> [3] LegalValidationAdapter.createLegalValidationCase()
      -> [3.1] DualValidationService.createRequest()          // PD-82
      -> [3.2] LegalValidationCase.save()
    -> [4] LegalAuditTrail.emitProbativeEvent(MANDATE_QUALIFIED)
      -> [4.1] HashService.hash(eventPayload)                 // SHA3-256
      -> [4.2] HsmService.sign(hash)                          // HSM sig, fail-closed ERR-81-13
      -> [4.3] TSA.requestTimestamp(hash)                     // RFC 3161, fail-closed ERR-81-14
      -> [4.4] LegalAccessEvent.save()
      -> [4.5] BatchService.addItem(eventHash)                // Ancrage async
  -> Return LegalMandateResult

4.2 Sequence N2 - Double validation

Client -> Controller.submitInternalValidation(input)
  -> Orchestrator.submitInternalValidation(input)
    -> [1] LegalValidationAdapter.submitValidation(input)
      -> [1.1] Load LegalValidationCase
      -> [1.2] Check userId != validator1Identity        // INV-81-03, ERR-81-08
      -> [1.3] Map role: DPO->PARENT, LegalOfficer->AUTHORITY
      -> [1.4] DualValidationService.submitValidation()  // PD-82 state machine
      -> [1.5] Update LegalValidationCase (validator, state)
    -> [2] LegalAuditTrail.emitProbativeEvent(VALIDATION_SUBMITTED)
    -> [3] Si isActivation:
      -> [3.1] LegalAuditTrail.emitProbativeEvent(VALIDATION_ACTIVATED)
  -> Return ValidationStateResult

4.3 Sequence N3 - Activation et consultation

Client -> Controller.activateLegalAccess(input)
  -> Orchestrator.activateLegalAccess(input)
    -> [1] Load LegalValidationCase, assert state=ACTIVATED  // ERR-81-07
    -> [2] Load LegalMandate, assert validUntil >= now        // fenetre
    -> [3] LegalReKeyManager.generateLegalReKey(...)
      -> [3.1] Assert contextId=LEGAL_PRE_MANDATE            // INV-81-12
      -> [3.2] Assert ttl <= MAX_TTL                          // ERR-81-09
      -> [3.3] PreService.generateReKey({                     // PD-41
               contextId: LEGAL_PRE_MANDATE,
               scope: mandateId })
      -> [3.4] PreService.validate(reKeyArtefact)
      -> [3.5] LegalReKey.save(encrypted kfrags, domain=LEGAL)
      -> [3.6] LegalAuditTrail.emitProbativeEvent(LEGAL_ACCESS_ACTIVATED)
  -> Return LegalAccessActivationResult

Client -> Controller.consultDocument(legalReKeyId, documentId)
  -> [rate limit guard: LegalRateLimitGuard]                  // ERR-81-18
  -> Orchestrator.consultDocument(legalReKeyId, documentId, actor)
    -> [1] LegalReKeyManager.validateConsultationAccess(...)
      -> [1.1] Load LegalReKey
      -> [1.2] Assert status=ACTIVE                           // ERR-81-12, ERR-81-17
      -> [1.3] Assert expiresAt > now                         // INV-81-01
      -> [1.4] Assert documentId in scopeDocumentIds          // INV-81-05, ERR-81-10
      -> [1.5] Assert mandateId coherent                      // ERR-81-16
    -> [2] PreService.reEncrypt({reKey, capsule, ...})        // PD-41
    -> [3] LegalAuditTrail.emitProbativeEvent(LEGAL_DOCUMENT_ACCESSED)
  -> Return re-encrypted document

4.4 Sequence N4 - Cloture et destruction

Client -> Controller.closeLegalAccess(input) ou revokeReKey(input)
  -> Orchestrator.closeLegalAccess(input)
    -> [1] Selon reason:
      REVOKE -> LegalReKeyManager.revokeReKey()
        -> [1.1] Transaction SERIALIZABLE: status=REVOKED, terminalStateAt=NOW()
        -> [1.2] LegalAuditTrail.emitProbativeEvent(LEGAL_REKEY_REVOKED)
      END_OF_CONSULTATION -> LegalReKeyManager.closeByEndOfConsultation()
        -> [1.1] Transaction SERIALIZABLE: status=COMPLETED, terminalStateAt=NOW()
        -> [1.2] LegalAuditTrail.emitProbativeEvent(LEGAL_ACCESS_CLOSED)
  -> Return LegalAccessClosureResult

[Scheduler async, < destructionDeadline]
  LegalReKeyDestructionScheduler.processDestructionQueue()
    -> LegalDestruction.destroyReKey(legalReKeyId)
      -> [1] Overwrite encryptedKfrags with zeros
      -> [2] Set encryptedKfrags = null
      -> [3] status = DESTROYED
      -> [4] LegalAuditTrail.emitProbativeEvent(LEGAL_REKEY_DESTROYED)

[Post-destruction]
  LegalCompositeProof.generateProof(mandateId)
    -> Assemble toutes les evidences
    -> Return LegalCompositeProof

4.5 Diagrammes Mermaid

4.5.1 Graphe de dependances entre composants

graph TD
    subgraph "Phase 1 — Fondations"
        CONST[legal-pre.constants.ts]
        ENUMS[enums/*]
        IFACES[interfaces/*]
        ENTITIES[entities/*]
    end

    subgraph "Phase 2 — Providers et services de base"
        TSP_STUB[tsp-verifier.stub.ts]
        ID_STUB[legal-identity-resolver.stub.ts]
        MANDATE_VAL[MandateValidatorService]
        AUDIT_TRAIL[LegalAuditTrailService]
        DESTRUCTION[LegalDestructionService]
    end

    subgraph "Phase 3 — Services metier"
        VAL_ADAPTER[LegalValidationAdapterService]
        REKEY_MGR[LegalReKeyManagerService]
        COMPOSITE[LegalCompositeProofService]
    end

    subgraph "Phase 4 — Orchestration et controles"
        ORCHESTRATOR[LegalPreOrchestratorService]
        ACCESS_GUARD[LegalPreAccessGuard]
        WRITE_GUARD[LegalWriteBlockGuard]
        RATE_GUARD[LegalRateLimitGuard]
        EXP_SCHED[LegalReKeyExpirationScheduler]
        DESTR_SCHED[LegalReKeyDestructionScheduler]
        ANCHOR_MON[LegalAnchoringMonitorScheduler]
    end

    subgraph "Phase 5 — Controller et module"
        CONTROLLER[LegalPreController]
        MODULE[LegalPreModule]
    end

    subgraph "Dependances externes PD"
        HASH_SVC[HashService PD-38]
        HSM_SVC[HsmAuditLoggerService PD-37]
        BATCH_SVC[BatchService PD-39]
        PRE_SVC[PreService PD-41]
        DUAL_VAL[DualValidationService PD-82]
        INCL_PROOF[InclusionProofService]
        DOC_MOD[DocumentsModule]
    end

    %% Phase 2 depends on Phase 1 + external
    TSP_STUB --> IFACES
    ID_STUB --> IFACES
    MANDATE_VAL --> TSP_STUB
    MANDATE_VAL --> HASH_SVC
    MANDATE_VAL --> ENTITIES
    AUDIT_TRAIL --> HSM_SVC
    AUDIT_TRAIL --> HASH_SVC
    AUDIT_TRAIL --> BATCH_SVC
    AUDIT_TRAIL --> ENTITIES
    DESTRUCTION --> ENTITIES
    DESTRUCTION --> AUDIT_TRAIL

    %% Phase 3 depends on Phase 2 + external
    VAL_ADAPTER --> DUAL_VAL
    VAL_ADAPTER --> ENTITIES
    REKEY_MGR --> PRE_SVC
    REKEY_MGR --> ENTITIES
    REKEY_MGR --> AUDIT_TRAIL
    COMPOSITE --> INCL_PROOF
    COMPOSITE --> ENTITIES

    %% Phase 4 depends on Phase 3
    ORCHESTRATOR --> MANDATE_VAL
    ORCHESTRATOR --> VAL_ADAPTER
    ORCHESTRATOR --> REKEY_MGR
    ORCHESTRATOR --> AUDIT_TRAIL
    ORCHESTRATOR --> COMPOSITE
    ORCHESTRATOR --> DESTRUCTION
    ORCHESTRATOR --> DOC_MOD
    ORCHESTRATOR --> ID_STUB
    EXP_SCHED --> REKEY_MGR
    DESTR_SCHED --> DESTRUCTION
    ANCHOR_MON --> AUDIT_TRAIL
    ANCHOR_MON --> BATCH_SVC

    %% Phase 5 depends on Phase 4
    CONTROLLER --> ORCHESTRATOR
    CONTROLLER --> ACCESS_GUARD
    CONTROLLER --> WRITE_GUARD
    CONTROLLER --> RATE_GUARD
    MODULE --> CONTROLLER

4.5.2 Sequence — Enregistrement mandat et activation acces (N1 + N3)

sequenceDiagram
    participant C as Client
    participant CTRL as LegalPreController
    participant ORCH as LegalPreOrchestratorService
    participant MV as MandateValidatorService
    participant TSP as ITspVerifier (stub)
    participant HASH as HashService (PD-38)
    participant DOC as DocumentsModule
    participant VA as LegalValidationAdapterService
    participant DV as DualValidationService (PD-82)
    participant AT as LegalAuditTrailService
    participant HSM as HsmAuditLoggerService (PD-37)
    participant TSA as TSA (RFC 3161)
    participant BATCH as BatchService (PD-39)

    Note over C,BATCH: Sequence N1 — Enregistrement mandat

    C->>CTRL: registerLegalMandate(input)
    CTRL->>ORCH: registerLegalMandate(input)

    ORCH->>MV: validateMandate(payload)
    MV->>TSP: verifyMandateSignature(payload)
    TSP-->>MV: result (fail-closed INV-81-11)
    MV->>HASH: hash(payload) → mandateHashSha3
    HASH-->>MV: hash
    MV->>DOC: resolveDocumentIds(externalIds)
    DOC-->>MV: documentIds
    MV-->>ORCH: validated mandate

    ORCH->>ORCH: LegalMandate.save()

    ORCH->>VA: createLegalValidationCase()
    VA->>DV: createRequest()
    DV-->>VA: validationCase
    VA->>VA: LegalValidationCase.save()
    VA-->>ORCH: case created

    ORCH->>AT: emitProbativeEvent(MANDATE_QUALIFIED)
    AT->>HASH: hash(eventPayload) → SHA3-256
    AT->>HSM: sign(hash)
    HSM-->>AT: signature (fail-closed ERR-81-13)
    AT->>TSA: requestTimestamp(hash)
    TSA-->>AT: tsaToken (fail-closed ERR-81-14)
    AT->>AT: LegalAccessEvent.save()
    AT->>BATCH: addItem(eventHash)
    AT-->>ORCH: event recorded

    ORCH-->>CTRL: LegalMandateResult
    CTRL-->>C: 201 Created

4.5.3 Sequence — Consultation document (N3 — sous-flux consultation)

sequenceDiagram
    participant C as Client
    participant RG as LegalRateLimitGuard
    participant CTRL as LegalPreController
    participant ORCH as LegalPreOrchestratorService
    participant RKM as LegalReKeyManagerService
    participant PRE as PreService (PD-41)
    participant AT as LegalAuditTrailService

    C->>CTRL: consultDocument(legalReKeyId, documentId)
    CTRL->>RG: check rate limit
    RG-->>CTRL: pass (or ERR-81-18)
    CTRL->>ORCH: consultDocument(legalReKeyId, documentId, actor)

    ORCH->>RKM: validateConsultationAccess(...)
    RKM->>RKM: Load LegalReKey
    RKM->>RKM: Assert status=ACTIVE (ERR-81-12/17)
    RKM->>RKM: Assert expiresAt > now (INV-81-01)
    RKM->>RKM: Assert documentId in scopeDocumentIds (INV-81-05)
    RKM->>RKM: Assert mandateId coherent (ERR-81-16)
    RKM-->>ORCH: access validated

    ORCH->>PRE: reEncrypt(reKey, capsule, ...)
    PRE-->>ORCH: re-encrypted fragment

    ORCH->>AT: emitProbativeEvent(LEGAL_DOCUMENT_ACCESSED)
    AT-->>ORCH: event recorded

    ORCH-->>CTRL: re-encrypted document
    CTRL-->>C: 200 OK

4.5.4 Sequence — Cloture, destruction et preuve composite (N4)

sequenceDiagram
    participant C as Client
    participant CTRL as LegalPreController
    participant ORCH as LegalPreOrchestratorService
    participant RKM as LegalReKeyManagerService
    participant DESTR as LegalDestructionService
    participant AT as LegalAuditTrailService
    participant CP as LegalCompositeProofService
    participant SCHED as LegalReKeyDestructionScheduler

    C->>CTRL: closeLegalAccess(input)
    CTRL->>ORCH: closeLegalAccess(input)

    alt reason = REVOKE
        ORCH->>RKM: revokeReKey()
        RKM->>RKM: TX SERIALIZABLE: status=REVOKED
        RKM->>AT: emitProbativeEvent(LEGAL_REKEY_REVOKED)
    else reason = END_OF_CONSULTATION
        ORCH->>RKM: closeByEndOfConsultation()
        RKM->>RKM: TX SERIALIZABLE: status=COMPLETED
        RKM->>AT: emitProbativeEvent(LEGAL_ACCESS_CLOSED)
    end

    ORCH-->>CTRL: LegalAccessClosureResult
    CTRL-->>C: 200 OK

    Note over SCHED,AT: Async — Scheduler destruction (< destructionDeadline)

    SCHED->>DESTR: destroyReKey(legalReKeyId)
    DESTR->>DESTR: Overwrite encryptedKfrags with zeros
    DESTR->>DESTR: Set encryptedKfrags = null
    DESTR->>DESTR: status = DESTROYED
    DESTR->>AT: emitProbativeEvent(LEGAL_REKEY_DESTROYED)

    Note over CP,AT: Post-destruction — Preuve composite

    CP->>CP: generateProof(mandateId)
    CP->>CP: Assemble toutes les evidences
    CP-->>CP: LegalCompositeProof

5. Mapping INV -> Mecanisme technique

INV Mecanisme technique Composant Observable
INV-81-01 TTL strict sur LegalReKey.expiresAt; scheduler expiration 30s; revocation dans transaction SERIALIZABLE; transition obligatoire vers DESTROYED LegalReKeyManager, ExpirationScheduler, DestructionScheduler Toute consultation post-TTL/revocation retourne erreur. Pas de ReKey ACTIVE avec expiresAt < NOW() en DB (verifiable par query).
INV-81-02 Appel obligatoire a ITspVerifier.verifyMandateSignature() avant creation de dossier; aucun chemin de code qui bypasse la verification MandateValidator Aucun LegalMandate avec validationStatus=VALID sans trace TSP. Test E02-E05 couvrent les rejets.
INV-81-03 Controle validator1Identity !== validator2Identity dans LegalValidationAdapter.submitValidation() AVANT delegation PD-82; mapping DPO->PARENT, LegalOfficer->AUTHORITY LegalValidationAdapter La 2e validation est rejetee si meme userId. Test E08 couvre specifiquement.
INV-81-04 encryptedKfrags est select: false et stocke chiffre; aucune cle de document n'est exposee; seuls les artefacts PRE temporaires transitent; AUDIT_FORBIDDEN_FIELDS du PD-41 est respecte LegalReKey entity, LegalReKeyManager Les API ne retournent jamais de kfrags. Les logs ne contiennent aucun champ interdit.
INV-81-05 Controle documentId in scopeDocumentIds dans validateConsultationAccess(); scope fixe a la creation, immutable apres LegalReKeyManager Toute tentative hors scope retourne ERR-81-10. Test E10.
INV-81-06 L'API legal ne propose que consultDocument (lecture); aucune methode d'ecriture/suppression n'existe sur le controller legal. De plus, LegalWriteBlockGuard intercepte explicitement toute tentative PUT/PATCH/DELETE, emet ACCESS_DENIED_WRITE_ATTEMPT et retourne 403 [CORRECTION v2] LegalPreController, LegalWriteBlockGuard Absence de endpoint d'ecriture + guard de protection defense-en-profondeur. Test E11 verifie l'interception et la journalisation.
INV-81-07 Overwrite zeros + nullification de encryptedKfrags; transition DESTROYED; scheduler destruction avec deadline; idempotence LegalDestruction, DestructionScheduler encryptedKfrags IS NULL AND status='DESTROYED' post-destruction. Tests L2, L3, L5, R5.
INV-81-08 Chaque etape critique appelle LegalAuditTrail.emitProbativeEvent() qui enchaine: SHA3-256 → HSM sign → TSA timestamp → persist → batch add; fail-closed sur echec HSM/TSA; LegalAnchoringMonitorScheduler (§8.6) verifie toutes les 6h que tous les evenements sont ancres dans les 24h, avec alerte et ancrage force si depassement LegalAuditTrail, LegalAnchoringMonitorScheduler Chaque LegalAccessEvent a hash, signature, et tsaTokenRef non null. Tests E13, E14. Le monitoring ancrage est teste dans legal-anchoring-monitor.scheduler.spec.ts (scenario: evenement non ancre apres 18h → alerte + ajout force au batch). [CORRECTION v2 — reference R-ANCHOR clarifiee: le test est dans le fichier spec du scheduler, pas dans le referentiel PD-81-tests.md car il s'agit d'un test d'implementation interne, pas d'un test fonctionnel contractuel.]
INV-81-09 LegalCompositeProof.generateProof() assemble toutes les evidences avec materiel de verification LegalCompositeProof service Preuve exportable et verifiable offline. Tests N4, L4.
INV-81-10 Colonne storageDomain='LEGAL' sur LegalReKey; queries filtrees par domain via LegalReKeyRepository (custom repository — §8.2.1); interdiction @InjectRepository(LegalReKey) directe dans les services; table et schema separes de PRE standard LegalReKey entity, LegalReKeyRepository, LegalReKeyManager WHERE storageDomain='LEGAL' applique systematiquement par le repository. Test S4 + lint CI grep.
INV-81-11 Exceptions propagees depuis TSP/HSM/TSA; aucun catch silencieux sur les chemins critiques; toute erreur declenche un rejet MandateValidator, LegalAuditTrail Operation annulee si TSP/HSM/TSA indisponible. Tests E13, E14, E15, R1-R3.
INV-81-12 Assertion contextId === 'LEGAL_PRE_MANDATE' dans generateLegalReKey() et registerLegalMandate(); le champ est non nullable en entite LegalReKeyManager, Orchestrator Rejet si contextId absent ou different. Test S2.

6. Mapping AC -> Mecanisme + Test

AC Mecanisme technique Composant(s) Scenario(s) de test
AC-81-01 MandateValidator.validateMandate() → TSP verification complete (signature, chaine, temporalite, CRL/OCSP) MandateValidator N1
AC-81-02 Echec de validateMandate() → exception + evenement MANDATE_REJECTED + aucune ReKey MandateValidator, Orchestrator E01, E02, E03, E04, E05
AC-81-03 LegalValidationAdapter requiert 2 validations avec userId distincts; PD-82 state machine refuse activation sur 1 seule LegalValidationAdapter, DualValidationStateMachine E07
AC-81-04 PD-82 state machine: PENDING_BOTH → PENDING_* → ACTIVATED avec 2 validations dans validationTtl LegalValidationAdapter N2
AC-81-05 generateLegalReKey() verifie state===ACTIVATED du dossier; produit ReKey avec isLegal=true et mandateId Orchestrator.activateLegalAccess(), LegalReKeyManager N3, E07
AC-81-06 generateLegalReKey() verifie ttl <= LEGAL_REKEY_MAX_TTL_SECONDS LegalReKeyManager E09
AC-81-07 validateConsultationAccess() verifie documentId in scopeDocumentIds LegalReKeyManager E10
AC-81-08 Pas de endpoint d'ecriture dans LegalPreController; validateConsultationAccess est read-only LegalPreController, LegalReKeyManager E11
AC-81-09 ExpirationScheduler + DestructionScheduler: ACTIVE → EXPIRED → DESTROYED Schedulers N4, L1
AC-81-10 revokeReKey() dans transaction SERIALIZABLE; invalidation immediate (<5s); scheduler destruction LegalReKeyManager N4, L2
AC-81-11 emitProbativeEvent() enchaine SHA3 + HSM + TSA + persist pour chaque evenement critique LegalAuditTrail N1, N2, N3, N4
AC-81-12 generateProof() assemble preuve composite complete LegalCompositeProof service N4, L4
AC-81-13 storageDomain='LEGAL' + queries filtrees LegalReKey entity N3, S4
AC-81-14 getLegalAuditProof() verifie requesterId === ownerUserId OU role IAM delegue (audit_delegate) via ILegalIdentityResolver [CORRECTION v2] Orchestrator L4, S6

7. Mapping ERR -> Handler technique

ERR Condition Handler Code exception HTTP
ERR-81-01 Mandat absent ou format invalide MandateValidator.validateMandate() detection schema/presence INVALID_MANDATE_FORMAT 400
ERR-81-02 Signature eIDAS invalide ITspVerifier.verifyMandateSignature() retourne valid=false INVALID_EIDAS_SIGNATURE 400
ERR-81-03 Chaine certificats invalide ITspVerifier retourne certificateChainValid=false INVALID_CERTIFICATE_CHAIN 400
ERR-81-04 Certificat revoque ITspVerifier retourne revocationStatus='revoked' CERTIFICATE_REVOKED 400
ERR-81-05 Fenetre temporelle invalide MandateValidator.verifyTemporalWindow() MANDATE_TEMPORAL_INVALID 400
ERR-81-06 Scope vide/incoherent/non resoluble MandateValidator.extractAndResolveScope() MANDATE_SCOPE_INVALID 400
ERR-81-07 Activation sans ACTIVATED Orchestrator.activateLegalAccess() verifie state ACTIVATION_STATE_INVALID 409
ERR-81-08 Meme userId pour 2 validations LegalValidationAdapter.submitValidation() compare userId SAME_VALIDATOR_IDENTITY 403
ERR-81-09 TTL > 30 jours LegalReKeyManager.generateLegalReKey() verifie borne TTL_EXCEEDS_MAXIMUM 400
ERR-81-10 DocumentId hors scope LegalReKeyManager.validateConsultationAccess() DOCUMENT_OUT_OF_SCOPE 403
ERR-81-11 Tentative ecriture/suppression LegalWriteBlockGuard intercepte toute methode HTTP PUT/PATCH/DELETE sur /legal-pre/**, emet evenement ACCESS_DENIED_WRITE_ATTEMPT avec journalisation probatoire, et retourne 403 WRITE_OPERATION_FORBIDDEN 403
ERR-81-12 ReKey expiree/revoquee/inconnue LegalReKeyManager.validateConsultationAccess() verifie status REKEY_NOT_ACTIVE 403
ERR-81-13 Echec signature HSM LegalAuditTrail.emitProbativeEvent() catch sur HsmService.sign() HSM_SIGNING_FAILED 503
ERR-81-14 Echec TSA LegalAuditTrail.emitProbativeEvent() catch sur TSA TSA_TIMESTAMP_FAILED 503
ERR-81-15 TSP indisponible MandateValidator.validateMandate() catch sur TSP TSP_UNAVAILABLE 503
ERR-81-16 Incoherence mandateId LegalReKeyManager.validateConsultationAccess() compare mandateId MANDATE_MISMATCH 403
ERR-81-17 Reutilisation post-destruction LegalReKeyManager.validateConsultationAccess() verifie status!==DESTROYED REKEY_DESTROYED 403
ERR-81-18 Depassement rate limiting LegalRateLimitGuard compteur Redis RATE_LIMIT_EXCEEDED 429

Tous les handlers emettent un evenement informatif (MANDATE_REJECTED, ACCESS_DENIED_*, SECURITY_ALERT selon gravite).


8. Securite et fail-closed

8.1 Principes fail-closed

Le module applique systematiquement le principe fail-closed:

  1. Aucun catch silencieux sur les chemins critiques (TSP, HSM, TSA).
  2. Exceptions propagees via LegalPreException avec code specifique.
  3. Transactions SERIALIZABLE pour les operations d'etat (generation, revocation, destruction).
  4. Pas de fallback en cas d'indisponibilite d'un composant de confiance: l'operation echoue.

8.2 Isolation

  1. Stockage: storageDomain='LEGAL' discrimine les ReKey legales.
  2. Requetes: toutes les queries du module filtrent sur storageDomain='LEGAL' via le LegalReKeyRepository (custom repository).
  3. API: endpoints separes sous /legal-pre/, pas de routes partagees avec PRE standard.
  4. Journaux: LegalAccessEvent est une table dediee, non partagee avec PD-41.

8.2.1 Mecanisme de protection storageDomain (ECT-07)

Probleme: Le filtre storageDomain='LEGAL' pourrait etre oublie dans une requete, causant un melange Legal/Standard.

Solution: LegalReKeyRepository — custom TypeORM repository qui applique systematiquement le filtre storageDomain='LEGAL' sur toutes les operations :

@Injectable()
export class LegalReKeyRepository {
  constructor(
    @InjectRepository(LegalReKey)
    private readonly repo: Repository<LegalReKey>,
  ) {}

  // Toutes les methodes appliquent automatiquement le filtre storageDomain
  private baseWhere(): FindOptionsWhere<LegalReKey> {
    return { storageDomain: LEGAL_STORAGE_DOMAIN };
  }

  async findOneByMandateId(mandateId: string): Promise<LegalReKey | null> {
    return this.repo.findOne({ where: { ...this.baseWhere(), mandateId } });
  }

  async findActiveByMandateId(mandateId: string): Promise<LegalReKey | null> {
    return this.repo.findOne({
      where: { ...this.baseWhere(), mandateId, status: LegalReKeyStatus.ACTIVE },
    });
  }

  async findExpired(): Promise<LegalReKey[]> {
    return this.repo
      .createQueryBuilder('rk')
      .where('rk.storageDomain = :domain', { domain: LEGAL_STORAGE_DOMAIN })
      .andWhere('rk.status = :status', { status: LegalReKeyStatus.ACTIVE })
      .andWhere('rk.expiresAt < NOW()')
      .getMany();
  }

  // ... toutes les methodes suivent le meme pattern
}

Invariant structurel : Aucun service du module n'accede directement a Repository<LegalReKey>. Seul LegalReKeyRepository est injecte. Cela est verifie par: - Code contract (Module 4): forbidden_patterns: ["@InjectRepository(LegalReKey)"] — interdit l'injection directe du repository TypeORM dans les services. - Test S4 : verifie que toutes les queries passent par le custom repository. - Lint rule : un test grep dans le CI verifie l'absence de getRepository(LegalReKey) ou @InjectRepository(LegalReKey) en dehors de legal-rekey.repository.ts.

8.3 Protection des secrets

  1. encryptedKfrags est select: false dans TypeORM.
  2. Aucune cle privee ne transite dans les DTOs de reponse.
  3. Les logs respectent AUDIT_FORBIDDEN_FIELDS de PD-41.
  4. La zeroisation de encryptedKfrags est une operation atomique dans la transaction de destruction.

8.4 Rate limiting

  • Compteur Redis atomique par legalReKeyId.
  • Fenetre glissante de 60 secondes.
  • Configurable via ConfigService.
  • Evenement emis a chaque depassement.

8.5 Concurrence

  • Transactions SERIALIZABLE pour toutes les mutations d'etat.
  • Le scheduler d'expiration et le scheduler de destruction utilisent des locks applicatifs (SELECT FOR UPDATE) pour eviter les traitements concurrents.
  • La revocation dans la fenetre de 5s: la revocation pose status=REVOKED dans une transaction; les consultations concurrentes qui verifient status dans leur propre transaction voient REVOKED immediatement (isolation SERIALIZABLE).

8.6 Monitoring ancrage 24h (ECT-08)

Probleme: Le respect du delai contractuel 24h d'ancrage (INV-81-08, spec §10.3) repose sur l'assertion que la cadence batch PD-39 le respecte, sans mecanisme de verification ni alerte.

Solution: LegalAnchoringMonitorScheduler — scheduler dedie qui verifie proactivement l'ancrage :

@Injectable()
export class LegalAnchoringMonitorScheduler {
  private readonly logger = new Logger(LegalAnchoringMonitorScheduler.name);

  constructor(
    @InjectRepository(LegalAccessEvent)
    private readonly eventRepo: Repository<LegalAccessEvent>,
    private readonly batchService: BatchService,
  ) {}

  // Toutes les 6 heures
  @Cron(CronExpression.EVERY_6_HOURS)
  async checkAnchoringDeadline(): Promise<void> {
    const THRESHOLD_HOURS = 18; // alerte a 18h pour laisser 6h de marge
    const threshold = new Date(Date.now() - THRESHOLD_HOURS * 3600 * 1000);

    // [CORRECTION v2 — alignement champs entite] Trouver les evenements non ancres de plus de 18h
    // Champs alignes avec legal-access-event.entity.ts: eventAt (pas createdAt), anchoringBatchRef (pas anchoringBatchId), eventId (pas id)
    const unanchored = await this.eventRepo
      .createQueryBuilder('e')
      .where('e.eventAt < :threshold', { threshold })
      .andWhere('e.anchoringBatchRef IS NULL')
      .getMany();

    if (unanchored.length === 0) return;

    this.logger.warn(
      `[ANCHORING_DELAY] ${unanchored.length} events unanchored after ${THRESHOLD_HOURS}h`,
    );

    // Forcer l'ajout au batch courant
    for (const event of unanchored) {
      await this.batchService.addItem({
        type: 'LEGAL_ACCESS_EVENT',
        hash: event.eventPayloadHashSha3,
        referenceId: event.eventId,  // [CORRECTION v2 — alignement avec entite eventId]
      });
    }

    this.logger.warn(
      `[ANCHORING_FORCED] ${unanchored.length} events force-added to current batch`,
    );
  }
}

Garanties : 1. Detection proactive : verification toutes les 6h (seuil 18h = marge de 6h avant deadline 24h). 2. Action corrective : force l'ajout au batch courant si evenements non ancres. 3. Alerte : log WARN pour monitoring externe (collecte par stack de logs existante). 4. Test : scenario R-ANCHOR verifie qu'un evenement non ancre apres 18h declenche l'alerte et l'ajout force.

Constantes ajoutees :

export const LEGAL_ANCHORING_MONITOR_INTERVAL_HOURS = 6;
export const LEGAL_ANCHORING_ALERT_THRESHOLD_HOURS = 18;

9. Migration de base de donnees

9.1 Tables a creer

Table Schema Description
legal_mandate vault_secure Mandats judiciaires
legal_validation_case vault_secure Dossiers de validation
legal_rekey vault_secure ReKeys legales
legal_access_event vault_secure Evenements probatoires (append-only)
legal_composite_proof vault_secure Preuves composites

9.2 Triggers

  • legal_access_event: trigger BEFORE UPDATE OR DELETERAISE EXCEPTION (append-only, comme validation_record PD-82).
  • legal_rekey.encryptedKfrags: pas de trigger special; la zeroisation est geree au niveau applicatif dans une transaction.

9.3 Index

Table Index Colonnes
legal_mandate idx_legal_mandate_status validationStatus
legal_validation_case idx_legal_case_mandate mandateId
legal_rekey idx_legal_rekey_mandate mandateId
legal_rekey idx_legal_rekey_status status
legal_rekey idx_legal_rekey_expiration expiresAt (partiel: WHERE status='ACTIVE')
legal_rekey idx_legal_rekey_destruction terminalStateAt (partiel: WHERE status IN ('REVOKED','EXPIRED','COMPLETED'))
legal_access_event idx_legal_event_type eventType
legal_access_event idx_legal_event_mandate mandateId
legal_composite_proof idx_legal_proof_mandate mandateId

10. Module NestJS

@Module({
  imports: [
    TypeOrmModule.forFeature([
      LegalMandate,
      LegalValidationCase,
      LegalReKey,
      LegalAccessEvent,
      LegalCompositeProof,
    ]),
    CryptoModule,        // PreService, HashService, HsmService
    TsaModule,           // BatchService, InclusionProofService
    DualValidationModule, // DualValidationService
    ScheduleModule,       // @nestjs/schedule
  ],
  controllers: [LegalPreController],
  providers: [
    // Services
    LegalPreOrchestratorService,
    MandateValidatorService,
    LegalValidationAdapterService,
    LegalReKeyManagerService,
    LegalAuditTrailService,
    LegalCompositeProofService,
    LegalDestructionService,
    // Guards
    LegalPreAccessGuard,
    LegalWriteBlockGuard,
    LegalRateLimitGuard,
    // Repositories
    LegalReKeyRepository,
    // Schedulers
    LegalReKeyExpirationScheduler,
    LegalReKeyDestructionScheduler,
    LegalAnchoringMonitorScheduler,
    // Providers (stubs)
    { provide: TSP_VERIFIER_TOKEN, useClass: TspVerifierStub },
    { provide: LEGAL_IDENTITY_RESOLVER_TOKEN, useClass: LegalIdentityResolverStub },
  ],
  exports: [LegalPreOrchestratorService],
})
export class LegalPreModule {}

10.2 Registration dans app.module.ts

Ajout de LegalPreModule dans le tableau imports de AppModule.


11. Strategie de test

11.1 Organisation des tests

Type Fichier(s) Couverture
Unitaire *.spec.ts par service Chaque service isole avec mocks
Integration legal-pre.integration.spec.ts Flux N1-N4 end-to-end avec DB et mocks HSM/TSP/TSA
Securite legal-pre.security.spec.ts S1-S6
Robustesse legal-pre.robustness.spec.ts R1-R7
Cycle de vie legal-pre.lifecycle.spec.ts L1-L5

11.2 Mapping tests spec -> fichiers

Scenario Test file Service(s) testes
N1 legal-pre-orchestrator.service.spec.ts, mandate-validator.service.spec.ts Orchestrator, MandateValidator
N2 legal-validation-adapter.service.spec.ts LegalValidationAdapter
N3 legal-rekey-manager.service.spec.ts, legal-pre-orchestrator.service.spec.ts LegalReKeyManager, Orchestrator
N4 legal-destruction.service.spec.ts, legal-composite-proof.service.spec.ts Destruction, CompositeProof
E01-E06 mandate-validator.service.spec.ts MandateValidator
E07 legal-pre-orchestrator.service.spec.ts Orchestrator (verifie state)
E08 legal-validation-adapter.service.spec.ts LegalValidationAdapter
E09 legal-rekey-manager.service.spec.ts LegalReKeyManager
E10-E12, E16-E17 legal-rekey-manager.service.spec.ts LegalReKeyManager
E13-E14 legal-audit-trail.service.spec.ts LegalAuditTrail
E15 mandate-validator.service.spec.ts MandateValidator
E11 legal-write-block.guard.spec.ts LegalWriteBlockGuard [CORRECTION v2]
E18 legal-rate-limit.guard.spec.ts LegalRateLimitGuard
S1-S6 legal-pre.integration.spec.ts ou fichier dedie Integration
R1-R7 legal-pre.integration.spec.ts ou fichier dedie Integration
L1-L5 legal-pre.integration.spec.ts ou fichier dedie Integration

11.3 Fixtures de test

  • mandate-valid-001: Mandat eIDAS valide, scope ['DOC-1001', 'DOC-1002'].
  • mandate-scope-empty-001: Mandat sans documentIds.
  • mandate-expired-001: Mandat avec validUntil dans le passe.
  • mandate-notyet-001: Mandat avec validFrom dans le futur.
  • validator-dpo-001: Identite DPO, userId unique.
  • validator-legal-001: Identite LegalOfficer, userId unique.
  • validator-dual-001: Meme userId IAM (pour test E08).
  • Certificats: valide, non qualifie, chaine incomplete, revoque.

11.4 Mocking strategy

Composant externe Mock
TSP TspVerifierStub (configurable pour chaque scenario)
HSM HsmService mock (succes/echec configurable)
TSA BatchService mock ou TsaClientService mock
IAM LegalIdentityResolverStub
PRE PreService mock (retourne artefacts de test)
PostgreSQL Base de test dediee (reset entre scenarios)
Redis Instance de test (pour rate limiting)

11.5 Contraintes techniques de test (ECT-13, ECT-14, ECT-15)

Framework de test : Jest (choisi pour coherence avec le codebase ProbatioVault-backend existant qui utilise Jest exclusivement via @nestjs/testing).

Compatibilite ESM/CJS : Le projet utilise CJS (CommonJS), impose par NestJS (configuration TypeScript module: "commonjs" dans tsconfig.json). Aucune dependance ESM-only n'est introduite par PD-81. Les modules tiers utilises (@noble/hashes, @nucypher/nucypher-ts) sont compatibles CJS.

Variables d'environnement CI requises :

Variable Usage Valeur CI
DATABASE_URL PostgreSQL de test postgresql://test:test@localhost:5432/pv_test
REDIS_URL Redis de test (rate limiting) redis://localhost:6379/1
CI Detection environnement CI true
NODE_ENV Environnement d'execution test
HSM_MOCK Desactiver appels HSM reels true
TSP_MOCK Desactiver appels TSP reels true

11.6 Dependances inter-PD (ECT-12)

Story Statut Nature de la dependance
PD-41 (PRE NuCypher/Umbral) DONE PreService.generateReKey(), PreService.reEncrypt() [CORRECTION v2 — alignement: le flux Legal PRE utilise reEncrypt conformement au plan §4.3 N3]
PD-37 (HSM audit signature) DONE HsmService.sign()
PD-38 (SHA3 hash) DONE HashService.sha3()
PD-39 (TSA RFC3161) DONE BatchService.addItem(), InclusionProofService
PD-82 (Double validation) DONE DualValidationService, DualValidationStateMachine

12. Vigilances et risques

ID Risque Mitigation
V-01 La zeroisation en PostgreSQL laisse des traces dans le WAL/replicas. La zeroisation applicative est un best-effort; documenter la limitation. Pour une destruction niveau HSM, l'architecture devrait utiliser des cles HSM pour les kfrags (hors scope actuel).
V-02 Le scheduler d'expiration a une granularite de 30s; une consultation pourrait passer dans la fenetre post-TTL. Le validateConsultationAccess() verifie expiresAt en temps reel, donc la fenetre est fermee cote applicatif meme si le scheduler n'a pas encore tourne.
V-03 Le stub TSP ne verifie rien reellement. TODO trace explicite; le stub valide le format mais pas la cryptographie. L'interface ITspVerifier garantit la substitution future.
V-04 La resolution documentId externe -> interne depend d'un service non specifie. Interface ILegalIdentityResolver + stub; le service concret est a implementer dans DocumentsModule.
V-05 Concurrence entre revocation et consultation dans la fenetre de 5s. Transaction SERIALIZABLE; la revocation est visible immediatement pour les nouvelles consultations (pas de cache intermediaire).
V-06 Le rate limiting Redis pourrait etre contourne si Redis tombe. En cas d'indisponibilite Redis, le guard refuse toute consultation (fail-closed).
V-07 L'ancrage probatoire dans les 24h depend du batch scheduler PD-39. Mitigation implementee (§8.6): LegalAnchoringMonitorScheduler verifie toutes les 6h que les evenements de plus de 18h sont ancres; force l'ajout au batch courant si depassement; alerte via log WARN pour monitoring externe.

13. Plan de livraison

Phase 1 - Fondations (pas de dependance externe au module)

  1. Creer src/modules/legal-pre/ et tous les sous-dossiers.
  2. Implementer constants, enums, interfaces.
  3. Implementer les 5 entites TypeORM.
  4. Creer la migration de base de donnees.
  5. Implementer les stubs (TspVerifierStub, LegalIdentityResolverStub).

Phase 2 - Services de base

  1. Implementer MandateValidatorService + tests unitaires.
  2. Implementer LegalAuditTrailService + tests unitaires.
  3. Implementer LegalDestructionService + tests unitaires.

Phase 3 - Services metier

  1. Implementer LegalValidationAdapterService + tests unitaires.
  2. Implementer LegalReKeyManagerService + tests unitaires.
  3. Implementer LegalCompositeProofService + tests unitaires.

Phase 4 - Orchestration et controles

  1. Implementer LegalPreOrchestratorService + tests unitaires.
  2. Implementer LegalPreAccessGuard.
  3. Implementer LegalRateLimitGuard + tests unitaires.
  4. Implementer les 2 schedulers + tests unitaires.

Phase 5 - Controller et integration

  1. Implementer LegalPreController.
  2. Implementer LegalPreModule et l'enregistrer dans AppModule.
  3. Implementer les tests d'integration (N1-N4, E01-E18, S1-S6, R1-R7, L1-L5).
  4. Verification couverture de test >= 85%.