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_secureschema - 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\uXXXXpour < 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¶
- JsonCanonicalizeService + tests (indépendant)
- Types et constantes (audit-action.types.ts, audit.constants.ts)
- AuditLog entity (TypeORM)
- Migration PostgreSQL append-only
- AuditSignatureService + tests unitaires
- Queue BullMQ + processor
- AuditLogService (facade)
- Update AuditModule
- Tests intégration HSM
- 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