Aller au contenu

PD-37 — Plan d'implémentation


📚 Navigation User Story | Document | | | ---------- | -- | | 📋 [Spécification](PD-37-specification.md) | | | 🛠️ **Plan d'implémentation** | *(ce document)* | | ✅ [Critères d'acceptation](PD-37-acceptability.md) | | | 📝 [Retour d'expérience](PD-37-rex.md) | | [← Retour à crypto-proof](../PD-189-epic.md) · [↑ Index User Story](index.md)

Objectif

Implémenter la signature HSM des événements d'audit pour garantir leur non-répudiation et intégrité.

Choix techniques retenus

  • Algorithme : ECDSA P-256 (courbe secp256r1)
  • Hash : SHA3-256 avant signature
  • Clé HSM : probatiovault-audit-ecdsa-p256 (non-exportable, Sign=true, Decrypt=false, Derive=false)
  • Format : JSON canonicalisé (RFC 8785)
  • Stockage : Table append-only dans vault_secure schema
  • Résilience : Queue BullMQ avec retry exponentiel (3 tentatives)

Composants disponibles

  • HsmService (src/modules/crypto/hsm/hsm.service.ts) : Client PKCS#11 complet avec signature ECDSA P-256
  • HashService (src/modules/crypto/hash.service.ts) : SHA3-256 déjà implémenté
  • AuditService (src/modules/audit/audit.service.ts) : Service basique sans signature
  • JobsModule (src/modules/jobs/jobs.module.ts) : BullMQ configuré (Redis)

Architecture cible

AuditSignatureService
        |
        +---> canonicalize(entry) [RFC 8785]
        |
        +---> HashService.hash(canonical) [SHA3-256]
        |
        +---> HsmService.sign(hash, keyLabel) [ECDSA P-256]
        |
        +---> AuditLogRepository.insert() [append-only]
        |
        +---> AuditSignatureQueue [retry sur erreur HSM]

Structure fichiers

src/modules/audit/
├── audit.module.ts
├── audit.service.ts              # Service existant
├── audit.constants.ts            # Constantes HSM et queue
├── entities/
│   ├── audit-event.entity.ts     # Entité existante
│   └── audit-log.entity.ts       # Nouvelle: log signé
├── services/
│   ├── audit-log.service.ts      # Facade pour autres modules
│   ├── audit-signature.service.ts # Signature HSM
│   └── json-canonicalize.service.ts # RFC 8785
├── processors/
│   └── audit-signature.processor.ts # Queue BullMQ
└── types/
    └── audit-action.types.ts     # Enums et interfaces

Diagrammes Mermaid

Graphe de dépendances des composants

graph TD
    subgraph "Module Audit"
        ALS[AuditLogService<br/><i>Facade</i>]
        ASS[AuditSignatureService]
        JCS[JsonCanonicalizeService<br/><i>RFC 8785</i>]
        ASP[AuditSignatureProcessor<br/><i>BullMQ</i>]
        ALE[AuditLog Entity<br/><i>vault_secure.audit_log</i>]
        AAT[AuditActionTypes<br/><i>Enums & Interfaces</i>]
        AC[audit.constants.ts<br/><i>HSM labels & queue name</i>]
    end

    subgraph "Module Crypto (existant)"
        HSM[HsmService<br/><i>PKCS#11 / CloudHSM</i>]
        HASH[HashService<br/><i>SHA3-256</i>]
    end

    subgraph "Module Jobs (existant)"
        BULL[BullMQ / Redis]
    end

    subgraph "PostgreSQL"
        DB[(vault_secure.audit_log<br/><i>append-only</i>)]
    end

    ALS -->|"delègue"| ASS
    ALS -->|"ou enqueue"| ASP
    ASP -->|"traite job"| ASS
    ASS --> JCS
    ASS --> HASH
    ASS --> HSM
    ASS -->|"Repository.save"| ALE
    ALE -->|"mapped to"| DB
    ASS -.->|"utilise"| AC
    ASS -.->|"utilise"| AAT
    ASP -->|"enregistré dans"| BULL

    style HSM fill:#f9d71c,stroke:#333
    style DB fill:#336791,stroke:#fff,color:#fff
    style BULL fill:#dc382c,stroke:#fff,color:#fff

Diagramme de séquence — Signature d'un événement d'audit

sequenceDiagram
    participant Caller as Module appelant
    participant ALS as AuditLogService
    participant ASS as AuditSignatureService
    participant JCS as JsonCanonicalizeService
    participant HASH as HashService
    participant HSM as HsmService (CloudHSM)
    participant DB as PostgreSQL (vault_secure)

    Caller->>ALS: emitAudit(entry)
    ALS->>ASS: signAuditEntry(entry)

    Note over ASS: Pipeline sign

    ASS->>JCS: canonicalize(entry)
    JCS-->>ASS: entryCanonical (RFC 8785)

    ASS->>HASH: hash(entryCanonical)
    HASH-->>ASS: entryHash (SHA3-256, 32 bytes)

    ASS->>HSM: sign(entryHash, 'probatiovault-audit-ecdsa-p256', ECDSA_SHA256)
    HSM-->>ASS: { signature (DER), keyLabel }

    ASS->>DB: INSERT INTO vault_secure.audit_log (append-only)
    DB-->>ASS: saved (id, createdAt)

    ASS-->>ALS: SignedAuditResult { id, entryHash, signature, keyId, timestamp }
    ALS-->>Caller: SignedAuditResult

Diagramme de séquence — Retry via BullMQ (erreur HSM)

sequenceDiagram
    participant Caller as Module appelant
    participant ALS as AuditLogService
    participant Queue as BullMQ (audit-signature)
    participant ASP as AuditSignatureProcessor
    participant ASS as AuditSignatureService
    participant HSM as HsmService (CloudHSM)

    Caller->>ALS: emitAudit(entry)
    ALS->>Queue: add(entry, { attempts: 3, backoff: exponential })
    Queue-->>ALS: jobId

    Note over Queue: Tentative 1
    Queue->>ASP: handleSignature(job)
    ASP->>ASS: signAuditEntry(entry)
    ASS->>HSM: sign(entryHash, keyLabel)
    HSM--xASS: HSM Timeout / Unavailable
    ASS--xASP: throw error
    ASP--xQueue: failed (retry in 1s)

    Note over Queue: Tentative 2 (backoff 2s)
    Queue->>ASP: handleSignature(job)
    ASP->>ASS: signAuditEntry(entry)
    ASS->>HSM: sign(entryHash, keyLabel)
    HSM-->>ASS: { signature, keyLabel }
    ASS-->>ASP: SignedAuditResult
    ASP-->>Queue: completed

Découpage technique

Phase 1 : Canonicalisation JSON (RFC 8785)

Fichier : src/modules/audit/services/json-canonicalize.service.ts

Spécification RFC 8785 :

  • Tri lexicographique des clés (Unicode code points)
  • Pas d'espaces blancs
  • Nombres : pas de notation scientifique pour les entiers, pas de -0
  • Strings : échappement minimal (\n, \r, \t, \\, \", et \uXXXX pour < 0x20)
  • UTF-8 strict

API :

@Injectable()
export class JsonCanonicalizeService {
  canonicalize(obj: unknown): string;
  // Retourne JSON canonique RFC 8785
}

Phase 2 : Entité AuditLog

Fichier : src/modules/audit/entities/audit-log.entity.ts

Schéma :

@Entity({ name: 'audit_log', schema: 'vault_secure' })
export class AuditLog {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ type: 'text', name: 'entry_canonical' })
  entryCanonical: string;  // JSON RFC 8785

  @Column({ type: 'bytea', name: 'entry_hash' })
  entryHash: Buffer;  // SHA3-256 (32 bytes)

  @Column({ type: 'bytea', name: 'hsm_signature' })
  hsmSignature: Buffer;  // ECDSA DER

  @Column({ name: 'hsm_key_id' })
  hsmKeyId: string;  // Label clé HSM

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

  @Column({ type: 'uuid', name: 'actor_id', nullable: true })
  actorId?: string;

  @Column({ name: 'action_type' })
  actionType: string;

  @Column({ type: 'uuid', name: 'entity_id', nullable: true })
  entityId?: string;
}

Phase 3 : Migration PostgreSQL append-only

Fichier : src/database/migrations/XXXXXXXXXX-CreateAuditLogTable.ts

-- Table audit_log dans vault_secure
CREATE TABLE vault_secure.audit_log (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  entry_canonical TEXT NOT NULL,
  entry_hash BYTEA NOT NULL,
  hsm_signature BYTEA NOT NULL,
  hsm_key_id VARCHAR(255) NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  actor_id UUID,
  action_type VARCHAR(100) NOT NULL,
  entity_id UUID
);

-- Index pour requêtes
CREATE INDEX idx_audit_log_created ON vault_secure.audit_log(created_at DESC);
CREATE INDEX idx_audit_log_actor ON vault_secure.audit_log(actor_id) WHERE actor_id IS NOT NULL;
CREATE INDEX idx_audit_log_entity ON vault_secure.audit_log(entity_id) WHERE entity_id IS NOT NULL;
CREATE INDEX idx_audit_log_action ON vault_secure.audit_log(action_type);

-- Contrainte append-only : bloquer UPDATE/DELETE
CREATE OR REPLACE FUNCTION vault_secure.audit_log_immutable()
RETURNS TRIGGER AS $$
BEGIN
  RAISE EXCEPTION 'audit_log is append-only: UPDATE and DELETE are forbidden';
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_audit_log_immutable
BEFORE UPDATE OR DELETE ON vault_secure.audit_log
FOR EACH ROW
EXECUTE FUNCTION vault_secure.audit_log_immutable();

COMMENT ON TABLE vault_secure.audit_log IS 'Journal probatoire append-only avec signature HSM (PD-37)';

Phase 4 : Service de signature

Fichier : src/modules/audit/services/audit-signature.service.ts

@Injectable()
export class AuditSignatureService {
  constructor(
    private readonly canonicalizer: JsonCanonicalizeService,
    private readonly hashService: HashService,
    private readonly hsmService: HsmService,
    private readonly auditLogRepository: Repository<AuditLog>,
    @InjectQueue('audit-signature') private readonly signatureQueue: Queue,
  ) {}

  async signAuditEntry(entry: AuditEntry): Promise<SignedAuditResult> {
    // 1. Canonicalisation RFC 8785
    const entryCanonical = this.canonicalizer.canonicalize(entry);

    // 2. Hash SHA3-256
    const entryHash = Buffer.from(this.hashService.hash(entryCanonical), 'hex');

    // 3. Signature HSM ECDSA P-256
    const signResult = await this.hsmService.sign(
      entryHash,
      HSM_AUDIT_KEY_LABEL,  // 'probatiovault-audit-ecdsa-p256'
      SignatureAlgorithm.ECDSA_SHA256,
    );

    // 4. Stockage append-only
    const auditLog = this.auditLogRepository.create({
      entryCanonical,
      entryHash,
      hsmSignature: signResult.signature,
      hsmKeyId: signResult.keyLabel,
      actorId: entry.actorId,
      actionType: entry.actionType,
      entityId: entry.entityId,
    });

    const saved = await this.auditLogRepository.save(auditLog);

    return {
      id: saved.id,
      entryHash: entryHash.toString('hex'),
      signature: signResult.signature.toString('base64'),
      keyId: signResult.keyLabel,
      timestamp: saved.createdAt,
    };
  }

  async verifyAuditEntry(id: string): Promise<VerificationResult> {
    const auditLog = await this.auditLogRepository.findOneOrFail({ where: { id } });

    // Recalcul du hash
    const recomputedHash = Buffer.from(
      this.hashService.hash(auditLog.entryCanonical),
      'hex'
    );

    // Vérification hash
    if (!recomputedHash.equals(auditLog.entryHash)) {
      return { valid: false, error: 'Hash mismatch - entry may have been tampered' };
    }

    // Vérification signature HSM
    const verifyResult = await this.hsmService.verify(
      auditLog.entryHash,
      auditLog.hsmSignature,
      auditLog.hsmKeyId,
      SignatureAlgorithm.ECDSA_SHA256,
    );

    return verifyResult;
  }
}

Phase 5 : Queue BullMQ

Fichier : src/modules/audit/processors/audit-signature.processor.ts

@Processor(AUDIT_SIGNATURE_QUEUE)
export class AuditSignatureProcessor {
  private readonly logger = new Logger(AuditSignatureProcessor.name);

  constructor(
    private readonly auditSignatureService: AuditSignatureService,
  ) {}

  @Process()
  async handleSignature(job: Job<AuditSignatureJob>): Promise<SignedAuditResult> {
    const { entry, attempt, correlationId } = job.data;

    this.logger.log(`Processing audit signature job ${correlationId} (attempt ${attempt})`);

    try {
      return await this.auditSignatureService.signAuditEntry(entry);
    } catch (error) {
      this.logger.error(`Audit signature failed: ${error.message}`);
      throw error; // BullMQ will retry
    }
  }
}

Configuration Queue :

BullModule.registerQueue({
  name: AUDIT_SIGNATURE_QUEUE,
  defaultJobOptions: {
    attempts: 3,
    backoff: {
      type: 'exponential',
      delay: 1000, // 1s, 2s, 4s
    },
    removeOnComplete: 100,
    removeOnFail: 1000,
  },
});

Phase 6 : Types d'actions audit

Fichier : src/modules/audit/types/audit-action.types.ts

export enum AuditActionType {
  // Documents
  DOCUMENT_UPLOAD = 'document.upload',
  DOCUMENT_DOWNLOAD = 'document.download',
  DOCUMENT_DELETE = 'document.delete',
  DOCUMENT_SEAL = 'document.seal',

  // PRE (Proxy Re-Encryption)
  PRE_SHARE_CREATE = 'pre.share.create',
  PRE_SHARE_REVOKE = 'pre.share.revoke',
  PRE_REENCRYPT = 'pre.reencrypt',

  // Clés
  KEY_ROTATION = 'key.rotation',
  KEY_ENVELOPE_CREATE = 'key.envelope.create',
  KEY_ENVELOPE_REVOKE = 'key.envelope.revoke',

  // Dossiers
  FOLDER_CREATE = 'folder.create',
  FOLDER_ACCESS = 'folder.access',
  FOLDER_DELETE = 'folder.delete',

  // Auth
  USER_LOGIN = 'user.login',
  USER_LOGOUT = 'user.logout',
  USER_MFA_ENABLE = 'user.mfa.enable',

  // Admin
  ADMIN_USER_CREATE = 'admin.user.create',
  ADMIN_USER_DELETE = 'admin.user.delete',
}

export interface AuditEntry {
  actorId?: string;
  actionType: AuditActionType;
  entityId?: string;
  metadata: Record<string, unknown>;
  timestamp: Date;
}

Phase 7 : Constantes HSM

Fichier : src/modules/audit/audit.constants.ts

/**
 * Label de la clé HSM dédiée aux signatures audit
 * Cette clé doit être créée dans le HSM avec:
 * - Type: ECDSA P-256
 * - Non-exportable
 * - Sign = true, Decrypt = false, Derive = false
 */
export const HSM_AUDIT_KEY_LABEL = 'probatiovault-audit-ecdsa-p256';

export const HSM_AUDIT_SIGNATURE_ALGORITHM = SignatureAlgorithm.ECDSA_SHA256;

export const AUDIT_SIGNATURE_QUEUE = 'audit-signature';

Phase 8 : Tests

Tests unitaires canonicalisation

  • Vecteurs RFC 8785 officiels
  • Tri lexicographique Unicode
  • Gestion des nombres spéciaux (NaN, Infinity rejetés)
  • Strings avec caractères spéciaux
  • Objets imbriqués profonds

Tests unitaires signature

  • Mock HsmService
  • Pipeline complet (canonicalize → hash → sign → store)
  • Vérification signature valide
  • Détection tampering (hash modifié)
  • Gestion erreur HSM (retry queue)

Tests intégration HSM

  • Signature réelle via HSM (CloudHSM/SoftHSM)
  • Vérification signature ECDSA P-256
  • Test de charge (1000 signatures)
  • Test indisponibilité HSM

Tests append-only

  • INSERT réussit
  • UPDATE échoue (trigger)
  • DELETE échoue (trigger)

Fichiers à créer

Fichier Description
src/modules/audit/services/json-canonicalize.service.ts Canonicalisation RFC 8785
src/modules/audit/services/json-canonicalize.service.spec.ts Tests RFC 8785
src/modules/audit/services/audit-signature.service.ts Service signature HSM
src/modules/audit/services/audit-signature.service.spec.ts Tests signature
src/modules/audit/services/audit-log.service.ts Facade pour autres modules
src/modules/audit/entities/audit-log.entity.ts Entité TypeORM
src/modules/audit/types/audit-action.types.ts Types et enums
src/modules/audit/audit.constants.ts Constantes HSM
src/modules/audit/processors/audit-signature.processor.ts Processor BullMQ
src/database/migrations/XXXXXXX-CreateAuditLogTable.ts Migration SQL

Fichiers à modifier

Fichier Modification
src/modules/audit/audit.module.ts Ajout nouveaux providers et imports

Ordre d'implémentation

  1. JsonCanonicalizeService + tests (indépendant)
  2. Types et constantes (audit-action.types.ts, audit.constants.ts)
  3. AuditLog entity (TypeORM)
  4. Migration PostgreSQL append-only
  5. AuditSignatureService + tests unitaires
  6. Queue BullMQ + processor
  7. AuditLogService (facade)
  8. Update AuditModule
  9. Tests intégration HSM
  10. Tests append-only

Critères de validation

  • signAuditEntry() retourne signature ECDSA valide
  • Canonicalisation RFC 8785 exacte (tests vecteurs)
  • SHA3-256 correct
  • Table append-only (UPDATE/DELETE bloqués)
  • Retry automatique via BullMQ (3 tentatives)
  • Vérification indépendante possible
  • Performance < 20ms par signature

Risques et mitigations

Risque Mitigation
HSM indisponible Queue BullMQ avec retry exponentiel
Clé audit non créée dans HSM Script d'initialisation + health check
Performance signature Benchmark < 10ms (CloudHSM typique)
Tampering table Trigger PostgreSQL bloquant

Points de vigilance

  • Ordre clés : Critique pour canonicalisation
  • HSM availability : Queue permet retry
  • Timestamp : Utiliser horloge serveur synchronisée (NTP)
  • Append-only : Jamais modifier événement signé

Hors périmètre

  • Horodatage TSA (→ future US)
  • Ancrage blockchain (→ PD-60+)
  • Archivage long terme (→ future US)
  • Dead Letter Queue pour entrées définitivement en échec