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¶
3.1 Constants (legal-pre.constants.ts)¶
// 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¶
legal-rekey-status.enum.ts¶
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.
legal-access-event-type.enum.ts¶
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.
legal-mandate-status.enum.ts¶
3.3 Interfaces¶
legal-pre.interfaces.ts¶
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.
legal-identity-resolver.interface.ts¶
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¶
legal-mandate.entity.ts¶
@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.
legal-validation-case.entity.ts¶
@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).
legal-rekey.entity.ts¶
@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.
legal-access-event.entity.ts¶
@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).
legal-composite-proof.entity.ts¶
@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.
3.5.2 legal-validation-adapter.service.ts¶
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:
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.
3.5.3 legal-rekey-manager.service.ts¶
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-*.
3.5.4 legal-audit-trail.service.ts¶
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.
3.5.5 legal-composite-proof.service.ts¶
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).
3.5.6 legal-destruction.service.ts¶
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.
3.5.7 legal-pre-orchestrator.service.ts¶
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: - REVOKE → legalReKeyManager.revokeReKey(...). - END_OF_CONSULTATION → legalReKeyManager.closeByEndOfConsultation(...). - EXPIRE → legalReKeyManager.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¶
legal-pre-access.guard.ts¶
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.
legal-write-block.guard.ts [CORRECTION v2 — ecart BLOQUANT ERR-81-11]¶
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.
legal-rate-limit.guard.ts¶
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¶
legal-rekey-expiration.scheduler.ts¶
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).
legal-rekey-destruction.scheduler.ts¶
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¶
legal-pre.controller.ts¶
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:
- Aucun catch silencieux sur les chemins critiques (TSP, HSM, TSA).
- Exceptions propagees via
LegalPreExceptionavec code specifique. - Transactions SERIALIZABLE pour les operations d'etat (generation, revocation, destruction).
- Pas de fallback en cas d'indisponibilite d'un composant de confiance: l'operation echoue.
8.2 Isolation¶
- Stockage:
storageDomain='LEGAL'discrimine les ReKey legales. - Requetes: toutes les queries du module filtrent sur
storageDomain='LEGAL'via leLegalReKeyRepository(custom repository). - API: endpoints separes sous
/legal-pre/, pas de routes partagees avec PRE standard. - Journaux:
LegalAccessEventest 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¶
encryptedKfragsestselect: falsedans TypeORM.- Aucune cle privee ne transite dans les DTOs de reponse.
- Les logs respectent
AUDIT_FORBIDDEN_FIELDSde PD-41. - La zeroisation de
encryptedKfragsest 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=REVOKEDdans une transaction; les consultations concurrentes qui verifientstatusdans leur propre transaction voientREVOKEDimmediatement (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: triggerBEFORE UPDATE OR DELETE→RAISE EXCEPTION(append-only, commevalidation_recordPD-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¶
10.1 legal-pre.module.ts¶
@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 avecvalidUntildans le passe.mandate-notyet-001: Mandat avecvalidFromdans 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)¶
- Creer
src/modules/legal-pre/et tous les sous-dossiers. - Implementer
constants,enums,interfaces. - Implementer les 5 entites TypeORM.
- Creer la migration de base de donnees.
- Implementer les stubs (
TspVerifierStub,LegalIdentityResolverStub).
Phase 2 - Services de base¶
- Implementer
MandateValidatorService+ tests unitaires. - Implementer
LegalAuditTrailService+ tests unitaires. - Implementer
LegalDestructionService+ tests unitaires.
Phase 3 - Services metier¶
- Implementer
LegalValidationAdapterService+ tests unitaires. - Implementer
LegalReKeyManagerService+ tests unitaires. - Implementer
LegalCompositeProofService+ tests unitaires.
Phase 4 - Orchestration et controles¶
- Implementer
LegalPreOrchestratorService+ tests unitaires. - Implementer
LegalPreAccessGuard. - Implementer
LegalRateLimitGuard+ tests unitaires. - Implementer les 2 schedulers + tests unitaires.
Phase 5 - Controller et integration¶
- Implementer
LegalPreController. - Implementer
LegalPreModuleet l'enregistrer dansAppModule. - Implementer les tests d'integration (N1-N4, E01-E18, S1-S6, R1-R7, L1-L5).
- Verification couverture de test >= 85%.