PD-294 — Plan d'implémentation¶
Prompt :
4-plan-implementation v1.4.0— 2026-04-19 Source spec :PD-294-specification.md(v2, §1-§10) Source tests :PD-294-tests.md
1. Découpage en composants¶
Cible : ProbatioVault-backend, module existant src/modules/merkle/. Tous les composants PD-294 sont ajoutés en extension non intrusive du module merkle : le service legacy MerkleProofService (PD-56) n'est pas modifié, une couche v2 est superposée en lecture.
| # | Composant | Chemin (proposé) | Responsabilité | Remplace / étend | Owner agent |
|---|---|---|---|---|---|
| C1 | merkle-v2-dto | src/modules/merkle/v2/dto/ | DTOs v2 (MerkleProofV2Dto, ProofVersionEnum), constantes §5.1, regex, branded types HexHash32, validateurs format | — (nouveau) | agent-typescript-dto |
| C2 | merkle-v2-classifier | src/modules/merkle/v2/services/proof-format-classifier.service.ts | Classification initiale (§5.4 table 1) + machine d'états (transitions autorisées / interdites) | — (nouveau) | agent-typescript-service |
| C3 | merkle-v2-normalizer | src/modules/merkle/v2/services/proof-normalizer.service.ts | Mapping v1→v2 (§5.5), forçage hash_algorithm='sha3-256', forçage proof_version=2, suppression treeId/hashAlgorithmVersion, conservation d'ordre merklePath→inclusion_path | — (nouveau) | agent-typescript-service |
| C4 | merkle-v2-validator | src/modules/merkle/v2/services/proof-format-validator.service.ts | Validation §5.1 post-normalisation (regex, bornes, cohérence length=0 ⇔ tree_size=1, mapping erreur → ERR-294-04/05/06/07) | — (nouveau) | agent-typescript-service |
| C5 | merkle-v2-verifier | src/modules/merkle/v2/crypto/rfc9162-verifier.service.ts | Vérification RFC 9162 §2.1.3.2 adaptée SHA3-256 : HASH, NODE_HASH(0x01‖l‖r), boucle principale + inner while-loop (§5.8 spec) | Coexiste avec MerkleProofVerifier PD-56 | agent-typescript-crypto |
| C6 | merkle-v2-orchestrator | src/modules/merkle/v2/services/merkle-proof-v2.service.ts | Façade orchestratrice : lecture DB (via repos existants) → classifier → normalizer → validator → retour DTO v2. Aucune écriture. Appel verifier en option (endpoint /verify) | Facade au-dessus de MerkleProofService (ne modifie pas ce dernier) | agent-typescript-service |
| C7 | merkle-v2-controller | src/modules/merkle/v2/controllers/merkle-proof-v2.controller.ts | Endpoint GET /v2/merkle-proof/:eventId → DTO v2 ; mapping exception → HTTP (400/404/409/422/500) | Nouveau endpoint versionné, endpoint v1 laissé intact | agent-typescript-service |
| C8 | merkle-v2-exceptions | src/modules/merkle/v2/exceptions/ | Exceptions typées Pd294Exception + codes ERR-294-01..11, filter NestJS Pd294ExceptionFilter mappant vers HTTP contractuel | — (nouveau) | agent-typescript-service |
| C9 | merkle-v2-envelope-guard | src/modules/merkle/v2/interceptors/proof-envelope-leak.interceptor.ts | Interceptor NestJS post-sérialisation : rejet ERR-294-08 si treeId ou hashAlgorithmVersion dans merkle_proof exposé (enforcement runtime INV-294-07) | — (nouveau) | agent-typescript-service |
| C10 | merkle-v2-audit | Extension MerkleProofAuditService (PD-56) | Hook d'audit lecture/normalisation/transition/rejet (fail-closed) | Extension services/merkle-proof-audit.service.ts | agent-typescript-service |
| C11 | merkle-v2-empirical-check | scripts/pd294-empirical-hash-check.ts | Script CLI de vérification H-294-02 (CA-294-12) : replay d'échantillon DB avec recalcul SHA3-256 vs SHA-256 | — (nouveau, outillage) | agent-typescript-service |
| C12 | merkle-v2-tests | src/modules/merkle/v2/__tests__/ + test/integration/merkle-v2-*.e2e-spec.ts | TC-NOM-01..10, TC-ERR-01..11, TC-NR-01..04, TC-NEG-01..06 + vecteurs RFC 9162 figés | — (nouveau) | agent-tests |
Justification du versionnement /v2 dans le path : - Le contrat v2 est rupture (noms de champs, algorithme). Cohabitation v1/v2 transitoire en lecture seule. - Les dépendances Nest (AnchorBatch, MerkleLeaf, MerkleTree) restent non modifiées. - INV-294-04 (no-retrowrite) est structurellement garanti : les services v2 n'ont aucun repository d'écriture injecté.
2. Flux techniques¶
2.1 Flux nominal A — Preuve v2 native¶
Client GET /v2/merkle-proof/:eventId
↓
Controller → V2OrchestratorService.getProof(eventId)
↓
Repository (MerkleLeaf + MerkleTree + AnchorBatchEvent) — lecture seule
↓ (record v1 ou v2 selon colonne proof_version)
Classifier.classify(record) → état CANONICAL_V2_READY
↓
Validator.validate(record) → ok / throw Pd294Exception(ERR-294-*)
↓
LeakInterceptor (post-sérialisation) → 200 JSON { merkle_proof: {...7 champs} }
2.2 Flux nominal B — Preuve legacy v1¶
Repository → record { proof_version: null | 1, merklePath[], merkleRoot, ... }
↓
Classifier → état LEGACY_V1_DETECTED (table §5.4)
↓
Normalizer.normalize(record) :
- proof_version := 2 (forçage)
- inclusion_path := merklePath (SANS inversion — INV-294-10)
- merkle_root := merkleRoot
- leaf_index := leafIndex
- tree_size := treeSize
- event_hash := eventHash
- hash_algorithm := 'sha3-256' (forçage)
- drop(treeId, hashAlgorithmVersion)
↓ Transition LEGACY_V1_DETECTED → CANONICAL_V2_READY
Validator.validate(normalized)
↓
Réponse 200 JSON v2
— AUCUN appel à repository.save() / update() / insert() —
2.3 Flux nominal C — Vérification crypto (endpoint /verify optionnel)¶
V2OrchestratorService.verify(dto) :
1. validate(dto) → throw ERR-294-* si format invalide
2. rfc9162Verifier.verifyInclusion(dto) → algo §5.8 implémenté strictement
- r ← hexToBytes(event_hash) (§5.8 step 3 — "r = hexToBytes(event_hash)")
- boucle principale (LSB(fn)==1 OR fn==sn) vs else
- inner while-loop (sous-cas fn==sn AND LSB(fn)==0) — cf. §9 vigilance V-01
- shift final fn >>= 1 ; sn >>= 1
3. check sn==0 AND r == hexToBytes(merkle_root)
→ valid=true OU throw Pd294Exception(ERR-294-09)
2.4 Flux de rejet (état REJECTED_INVALID)¶
Classifier → REJECTED_INVALID (state terminal)
↓
Audit.logRejection(cause, eventId) — fail-closed (propage l'erreur d'audit)
↓
throw Pd294Exception(ERR-294-*) → Pd294ExceptionFilter → HTTP contractuel
2bis. Diagramme de dépendances agents¶
5 agents step 6b (>= 3 → diagramme obligatoire).
graph LR
subgraph Wave1[Wave 1 — fondations parallèles]
DTO[C1 merkle-v2-dto<br/>agent-typescript-dto]
EXC[C8 merkle-v2-exceptions<br/>agent-typescript-service]
end
subgraph Wave2[Wave 2 — logique métier parallèle]
CLS[C2 classifier<br/>agent-typescript-service]
NRM[C3 normalizer<br/>agent-typescript-service]
VLD[C4 validator<br/>agent-typescript-service]
VRF[C5 rfc9162-verifier<br/>agent-typescript-crypto]
end
subgraph Wave3[Wave 3 — intégration]
ORC[C6 orchestrator<br/>agent-typescript-service]
GRD[C9 envelope-guard<br/>agent-typescript-service]
AUD[C10 audit-hook<br/>agent-typescript-service]
end
subgraph Wave4[Wave 4 — exposition + outillage]
CTR[C7 controller<br/>agent-typescript-service]
EMP[C11 empirical-check<br/>agent-typescript-service]
end
subgraph Wave5[Wave 5 — qualification]
TST[C12 tests<br/>agent-tests]
end
DTO --> CLS
DTO --> NRM
DTO --> VLD
DTO --> VRF
EXC --> CLS
EXC --> NRM
EXC --> VLD
EXC --> VRF
CLS --> ORC
NRM --> ORC
VLD --> ORC
VRF --> ORC
ORC --> CTR
DTO --> GRD
EXC --> GRD
GRD --> CTR
ORC --> AUD
DTO --> EMP
VRF --> EMP
CTR --> TST
EMP --> TST Règle : vérification TypeScript (npx tsc --noEmit) entre chaque agent (procédure PD-283/PD-282/PD-265).
2ter. Diagramme de séquence enrichi (repris de spec §5bis)¶
sequenceDiagram
autonumber
participant Client
participant Ctl as MerkleProofV2Controller
participant Orc as MerkleProofV2Service
participant Repo as TypeORM repos (MerkleLeaf/Tree/AnchorBatchEvent)
participant Cls as ProofFormatClassifier
participant Nrm as ProofNormalizer
participant Val as ProofFormatValidator
participant Vrf as Rfc9162Verifier
participant Itc as EnvelopeLeakInterceptor
participant Aud as MerkleProofAuditService
Client->>Ctl: GET /v2/merkle-proof/:eventId
Ctl->>Orc: getProof(eventId)
Orc->>Repo: SELECT leaf + tree + anchor_batch_event (lecture seule)
Repo-->>Orc: record (proof_version null|1|2)
Orc->>Cls: classify(record)
alt record v1
Cls-->>Orc: LEGACY_V1_DETECTED
Orc->>Nrm: normalize(record)
Nrm-->>Orc: dto v2 { proof_version=2, inclusion_path=merklePath, hash_algorithm='sha3-256' }
else record v2
Cls-->>Orc: CANONICAL_V2_READY
Orc->>Nrm: passthrough(record)
Nrm-->>Orc: dto v2 inchangé
else autre
Cls-->>Orc: REJECTED_INVALID
Orc->>Aud: logRejection(ERR-294-01)
Orc-->>Ctl: throw Pd294Exception(ERR-294-01)
end
Orc->>Val: validate(dto)
Val-->>Orc: ok / throw Pd294Exception(ERR-294-*)
Orc-->>Ctl: dto v2
Ctl->>Itc: serialize(response)
Itc->>Itc: scan merkle_proof for treeId/hashAlgorithmVersion
alt fuite détectée
Itc-->>Client: 500 ERR-294-08
else propre
Itc-->>Client: 200 JSON { merkle_proof: {...7 champs} }
end
rect rgb(240,240,240)
note over Vrf: Endpoint optionnel POST /v2/merkle-proof/verify
Ctl->>Vrf: verify(dto)
Vrf->>Vrf: fn=leaf_index, sn=tree_size-1, r=hexToBytes(event_hash)
loop inclusion_path[i]
Vrf->>Vrf: if sn==0 FAIL(ERR-294-09)
alt LSB(fn)==1 OR fn==sn
Vrf->>Vrf: r = SHA3-256(0x01 || p || r)
loop while LSB(fn)==0 AND fn!=0
Vrf->>Vrf: fn >>= 1 ; sn >>= 1
end
else
Vrf->>Vrf: r = SHA3-256(0x01 || r || p)
end
Vrf->>Vrf: fn >>= 1 ; sn >>= 1
end
Vrf-->>Ctl: valid=true / throw ERR-294-09
end Note cruciale : l'ordre inner-while / else dans le diagramme implémente la spec §5.8 telle qu'écrite (la règle while LSB(fn)==0 est imbriquée dans la branche LSB(fn)==1 OR fn==sn). Écart RFC 9162 strict vs spec §5.8 tracé en V-01.
3. Mapping invariants → mécanismes¶
| Invariant ID | Exigence | Mécanisme | Composant | Observable | Risque |
|---|---|---|---|---|---|
| INV-294-01-format-v2 | 7 champs v2 exacts émis | DTO MerkleProofV2Dto avec whitelist strict + class-validator @ValidateNested + class-transformer excludeExtraneousValues: true | C1 | Schéma JSON PASS sur toute réponse v2 ; assertion test Object.keys(dto).sort() === AVAILABLE_KEYS_V2.sort() | Divergence schéma si nouveau champ amont non filtré |
| INV-294-02-hash-algorithm | hash_algorithm === 'sha3-256' | Constante HASH_ALGORITHM_V2 = 'sha3-256' as const + regex ^sha3-256$ dans validator (ERR-294-07) + forçage normalizer | C1, C3, C4 | Assert runtime sur 100% des payloads v2 ; test négatif SHA-256 → 422 | H-294-02 : amont réellement SHA3-256 ? Voir V-02 |
| INV-294-03-dual-read-single-write | Lecture v1 et v2, émission v2 seule | Classifier 3-états + absence totale de repository d'écriture dans module v2 (readonly typage + audit TypeScript) | C2, C6 | Grep statique CI : aucun save()/update()/insert() dans v2/ ; test snapshot DB avant/après lecture | Oubli d'un repo injecté en écriture |
| INV-294-04-no-retrowrite | Aucune réécriture des preuves v1 | Aucun @InjectRepository mutable dans v2/, uniquement readonly Repository<T> typé + audit CI bloquant | C6 | Checksum row-level avant/après lecture = identique (TC-NOM-02) ; transactions d'écriture absentes des logs pgsql | Migration accidentelle d'une autre story |
| INV-294-05-discriminant | proof_version absent/1 → v1, 2 → v2, autre → rejet | Classifier table de routage switch(proof_version) avec default → REJECTED_INVALID(ERR-294-01) | C2 | TC-NOM-03 + TC-ERR-01 ; couverture branch 100% | Valeur float (ex 2.0) — cf. validation Number.isInteger |
| INV-294-06-rfc-verify-applicable | Vérif RFC 9162 §2.1.3.2 SHA3-256 OK | Rfc9162Verifier.verifyInclusion() avec primitives SHA3-256 (node:crypto createHash('sha3-256')) et NODE_HASH(0x01‖l‖r) | C5 | TC-NOM-06 sur vecteur figé VEC-RFC9162-SHA3-256-01 (à produire dans le dossier de tests) ; TC-NOM-10 campagne empirique | E-01 review : inner while-loop spec divergente du RFC — cf. V-01 ; E-12 leaf-hash LH(m) non contractualisé — cf. V-05 |
| INV-294-07-no-treeid-exposed | treeId/hashAlgorithmVersion jamais en sortie v2 | DTO whitelist (C1) + Pd294EnvelopeLeakInterceptor (C9) scan post-sérialisation défensif : ['treeId','hashAlgorithmVersion'].some(k => k in payload.merkle_proof) → 500 ERR-294-08 | C1, C9 | TC-ERR-08 + snapshot JSON de toutes les réponses test ; alerte prod Sentry si déclenché | Couplage interceptor / format de réponse wrapping (ex: data.merkle_proof vs merkle_proof) — documenté dans l'interceptor |
| INV-294-08-format-state-transitions | Machine d'états §5.4 respectée | ProofFormatClassifier implémenté en table explicite + méthode assertTransition(from, to) qui throw Pd294Exception(ERR-294-11) sur transitions interdites | C2 | TC-NOM-07 + TC-ERR-11 ; journal de transitions exporté par audit | Downgrade interne non prévu |
| INV-294-09-format-single-source | Formats définis en §5.1 seulement | Documentaire : toutes les constantes format v2 dans C1/dto/v2-constants.ts ; CI check grep ^sha3-256 autorisé uniquement dans v2/dto/ | C1 | Script scripts/pd294-check-single-source.sh en CI ; TC-NOM-08 | Copié/collé pattern (PD-250 clarity oscillante) |
| INV-294-10-v1-path-order | merklePath bottom-to-top → inclusion_path sans inversion | Normalizer utilise Array.from(record.merklePath) sans .reverse() + test contractuel TC-NOM-09 sur vecteur témoin | C3 | Diff byte-à-byte entre merklePath entrant et inclusion_path sortant ; TC-NEG-05 rejette l'ordre inversé via ERR-294-09 | Fausse "lisibilité" côté dev : personne ne pense à inverser, justement c'est la règle — commentaire explicite // INV-294-10: do NOT reverse obligatoire |
4. Mapping critères d'acceptation → mécanismes¶
| Critère ID | Mécanisme(s) | Composant | Observable | Risque |
|---|---|---|---|---|
| CA-294-01 | DTO whitelist v2 (C1) + Interceptor (C9) | C1, C9 | Schéma JSON PASS sur TC-NOM-01 | — |
| CA-294-02 | Constante HASH_ALGORITHM_V2 + validator strict | C1, C4 | TC-NOM-02 / TC-ERR-07 | H-294-02 |
| CA-294-03 | Classifier + Normalizer read-only | C2, C3, C6 | Snapshot DB inchangé (TC-NOM-02) | — |
| CA-294-04 | Classifier table de routage exhaustive | C2 | TC-NOM-03 / TC-ERR-01 | Valeurs float/string |
| CA-294-05 | Interceptor C9 anti-leak | C1, C9 | TC-ERR-08 | Wrapping réponse |
| CA-294-06 | Validator §5.1 (regex + bornes) | C4 | TC-ERR-01..11 matrice | — |
| CA-294-07 | Validator bornes tree_size int32 / leaf_index < tree_size | C4 | TC-NOM-04 / TC-ERR-02 / TC-ERR-03 | int32 vs uint64 RFC — V-06 |
| CA-294-08 | Validator length ∈ [0,31] + cohérence length=0 ⇔ tree_size=1 | C4 | TC-NOM-05 / TC-ERR-04 | — |
| CA-294-09 | Rfc9162Verifier (algo §5.8) | C5 | TC-NOM-06 vecteur figé | V-01 (inner while-loop) |
| CA-294-10 | Rfc9162Verifier — cas altéré → ERR-294-09 | C5 | TC-ERR-09 | — |
| CA-294-11 | Aucun repo d'écriture dans v2/ | C6 | TC-NR-01 diff DB | — |
| CA-294-12 | Script C11 pd294-empirical-hash-check.ts sur échantillon prod/staging | C11 | Rapport reports/pd294-empirical-hash-check-YYYYMMDD.json ; TC-NOM-10 | Si l'amont est SHA-256 → blocage déploiement — V-02 |
| CA-294-13 | assertTransition throw ERR-294-11 | C2 | TC-ERR-11 | — |
| CA-294-14 | Normalizer sans .reverse() + commentaire INV-294-10 | C3 | TC-NOM-09 / TC-NEG-05 | — |
5. Mapping tests (TC-*) → mécanismes + observables¶
| Test ID | Référence spec | Mécanisme(s) | Point(s) d'observation | Niveau |
|---|---|---|---|---|
| TC-NOM-01 | INV-294-01, -07 / CA-294-01, -05 | DTO whitelist + Interceptor | Response JSON keys ; absence treeId | Integration |
| TC-NOM-02 | INV-294-02, -03, -04 / CA-294-02, -03, -11 | Classifier + Normalizer + snapshot DB | Payload v2 ; checksum row + updated_at | Integration |
| TC-NOM-03 | INV-294-05 / CA-294-04 | Classifier table routage | État classifié ; payload résultant | Unit + Integration |
| TC-NOM-04 | §5.2 / CA-294-07 | Validator bornes | HTTP 200 ; valeurs renvoyées identiques | Unit |
| TC-NOM-05 | §5.2 / CA-294-08 | Validator longueur + cohérence | inclusion_path.length = 0 ou 31 | Unit |
| TC-NOM-06 | INV-294-06 / CA-294-09 | Rfc9162Verifier + vecteur VEC-RFC9162-SHA3-256-01 | valid=true ; r == merkle_root | Unit |
| TC-NOM-07 | INV-294-08 / CA-294-03 | Classifier + journal transitions | Traces uniquement parmi les 3 transitions autorisées | Unit |
| TC-NOM-08 | INV-294-09 | Script pd294-check-single-source.sh | Rapport PASS ; 0 redéfinition | Documentaire CI |
| TC-NOM-09 | INV-294-10 / CA-294-14 | Normalizer sans reverse | Diff merklePath vs inclusion_path = 0 | Unit |
| TC-NOM-10 | H-294-02 / CA-294-12 | Script C11 | Rapport comparatif SHA3-256 vs SHA-256 | Integration (campagne) |
| TC-ERR-01 | ERR-294-01 | Classifier default → rejet | HTTP 400 ; code erreur | Integration |
| TC-ERR-02 | ERR-294-02 | Validator bornes leaf_index | HTTP 422 ; code erreur | Unit + Integration |
| TC-ERR-03 | ERR-294-03 | Validator bornes tree_size | HTTP 422 ; code erreur | Unit |
| TC-ERR-04 | ERR-294-04 | Validator format + longueur inclusion_path | HTTP 400 ; code erreur | Unit |
| TC-ERR-05 | ERR-294-05 | Validator regex merkle_root | HTTP 400 ; code erreur | Unit |
| TC-ERR-06 | ERR-294-06 | Validator regex event_hash | HTTP 400 ; code erreur | Unit |
| TC-ERR-07 | ERR-294-07 | Validator hash_algorithm === 'sha3-256' | HTTP 422 ; code erreur | Unit |
| TC-ERR-08 | ERR-294-08 | Interceptor anti-leak | HTTP 500 ; code erreur | Integration |
| TC-ERR-09 | ERR-294-09 | Rfc9162Verifier — cas altéré | HTTP 422 ; valid=false | Unit + Integration |
| TC-ERR-10 | ERR-294-10 | Orchestrator handler leaf not found | HTTP 404 ; code erreur | Integration |
| TC-ERR-11 | ERR-294-11 | assertTransition throw | HTTP 409 ; code erreur | Unit |
| TC-NR-01 | CA-294-11 | Snapshot DB | 0 row update/insert/delete sur leaves v1 | Integration |
| TC-NR-02 | CA-294-01 | Snapshot JSON canonique | Diff byte-à-byte vs fixture | Integration |
| TC-NR-03 | Hors périmètre ProofEnvelope | Fixture tsa_token, hsm_signature | Sous-objets inchangés | Integration |
| TC-NR-04 | §5.9 | Diff schéma DB | pg_dump --schema-only identique avant/après | CI |
| TC-NEG-01 | §5.1 case | Validator regex lowercase | HTTP 400 | Unit |
| TC-NEG-02 | §5.1 types | Validator @IsString() element | HTTP 400 ERR-294-04 | Unit |
| TC-NEG-03 | Concurrence | Test parallèle N lectures | 0 write, payloads identiques | Integration |
| TC-NEG-04 | §5.4 downgrade | assertTransition | HTTP 409 ERR-294-11 | Unit |
| TC-NEG-05 | INV-294-10 | Normalizer + Verifier | valid=false sur ordre inversé | Integration |
| TC-NEG-06 | Intégrité | Verifier altération merkle_root | valid=false ; ERR-294-09 | Unit |
6. Gestion des erreurs¶
| Code | Classe exception | HTTP | Composant source | Comportement audit |
|---|---|---|---|---|
ERR-294-01 | InvalidProofVersionException | 400 | Classifier (default) | Audit format_rejected |
ERR-294-02 | LeafIndexOutOfRangeException | 422 | Validator | Audit format_rejected |
ERR-294-03 | TreeSizeOutOfRangeException | 422 | Validator | Audit format_rejected |
ERR-294-04 | InclusionPathInvalidException | 400 | Validator | Audit format_rejected |
ERR-294-05 | MerkleRootInvalidException | 400 | Validator | Audit format_rejected |
ERR-294-06 | EventHashInvalidException | 400 | Validator | Audit format_rejected |
ERR-294-07 | HashAlgorithmInvalidException | 422 | Validator | Audit format_rejected |
ERR-294-08 | ProofLeakDetectedException | 500 | Interceptor | Audit leak_detected + Sentry/alert prod |
ERR-294-09 | InclusionVerificationFailedException | 422 | Verifier | Audit verification_failed |
ERR-294-10 | ProofNotFoundException | 404 | Orchestrator | Audit proof_not_found |
ERR-294-11 | InvalidStateTransitionException | 409 | Classifier | Audit transition_rejected |
Mécanisme :
- Toutes les exceptions dérivent d'une classe
Pd294Exceptioncontenantcode: stringethttpStatus: HttpStatus. - Un
Pd294ExceptionFilter(NestJS) intercepte, logge, appelle l'audit fail-closed, puis retourne : - Fail-closed audit (learning universel catch-absorb) : le filter
awaitl'audit SANS.catch(() => ...). Pattern correct : → toute erreur d'audit remonte en 500 (la visibilité rejet reste garantie). - Anti-énumération (risque V-07) :
ERR-294-10(404) distinguable deERR-294-06(400) — documenté comme dette connue (E-19 review step 3) ; pas de correction dans PD-294.
7. Impacts sécurité¶
| Risque | Mitigation | Traçabilité |
|---|---|---|
Fuite champs internes (treeId, hashAlgorithmVersion) | DTO whitelist (C1) + Interceptor runtime défensif (C9) + alerte prod si ERR-294-08 | INV-294-07 / TC-ERR-08 |
| Normalisation de preuve forgée (review E-10) | Documenté en dette : PD-294 n'introduit pas de vérification hsm_signature avant normalisation (spec §2 hors périmètre). Mitigation compensatoire : la normalisation v2 n'écrit rien et reste en lecture ; l'émission v2 d'une preuve forgée impose tout de même de passer la vérification RFC 9162 côté client | V-04 |
| Divergence amont SHA-256 réel vs label SHA3-256 (review E-09 / H-294-02) | Script C11 bloquant avant déploiement : pd294-empirical-hash-check.ts doit rapporter 100% PASS SHA3-256 sur échantillon production. Si FAIL → gate 8 bloquée, réévaluation spec | V-02, CA-294-12 |
| Divergence algo §5.8 vs RFC 9162 (review E-01, bug inner while-loop) | Documenté en V-01 : implémentation stricte de la spec §5.8. Vecteur figé VEC-RFC9162-SHA3-256-01 à produire avec deux branches de chemin (fn pair strict fn != sn) pour détecter le bug avant Gate 5. Si le vecteur échoue → escalade spec | V-01 |
Leaf-hash RFC 9162 LH(m)=HASH(0x00‖m) (review E-12) | Documenté en V-05 : event_hash amont est présumé "leaf-hash déjà préfixé". Vérification à mener en C11 si possible (comparer avec/sans préfixe 0x00) | V-05 |
| Types hexadécimaux interchangeables (apprentissage universel branded types) | Types branded HexHash32 = string & {__brand}, constructeurs toHexHash32(x: string): HexHash32 avec validation regex à la construction | Anti-swap event_hash / merkle_root |
| Concurrence / rejeu massif | Aucun état muté → pas de race condition. Test TC-NEG-03 parallèle | — |
Injection via proof_version non numérique | Validator @IsInt() @In([1,2]) + réception JSON → parse strict | ERR-294-01 |
Journalisation obligatoire (via MerkleProofAuditService étendu) :
proof_read(sortie OK) :eventId,proof_version_source,proof_version_emitted,state_final, timestamp UTCproof_normalized:eventId,state: LEGACY_V1_DETECTED → CANONICAL_V2_READYproof_rejected:eventId,error_code,causeproof_transition_forbidden:eventId,from,to,ERR-294-11proof_leak_detected:eventId,fields_leaked,ERR-294-08(alerte prod bloquante)
Corrélation : tous les logs portent un correlationId = \pd294-${randomUUID()}`(learning universelcrypto.randomUUID()`).
Conformité :
- INV-294-04 garantit que les preuves eIDAS historiques ne sont pas modifiées (valeur probatoire préservée).
hash_algorithm='sha3-256'forcé mais H-294-02 non validée empiriquement tant que C11 n'a pas tourné → Gate 8 conditionné au rapport C11.
8. Hypothèses techniques¶
| ID | Hypothèse | Impact si faux |
|---|---|---|
| HT-01 | Les colonnes event_hash, merkle_root, leaf_index, tree_size, inclusion_proof, proof_version existent dans vault_merkle.merkle_leaves / merkle_trees (ou un champ JSON contenant la preuve) | Si proof_version absent du schéma DB, Classifier lit null → branche v1 systématique (acceptable). Si les autres colonnes manquent → bloquant avant Gate 5, escalade obligatoire |
| HT-02 | node:crypto.createHash('sha3-256') disponible (Node.js >= 10.9) | Node ancien → fallback sha3 npm package. Backend actuel : vérifier node --version (a priori Node 20) |
| HT-03 | L'amont produit réellement des hashes SHA3-256 (H-294-02 spec) | Si faux : V-02 bloquant. Rollback = maintenir label SHA-256 et rejeter PD-294 |
| HT-04 | L'algorithme §5.8 tel que rédigé dans la spec est la source de vérité, même si le review step 3 v2 l'identifie comme divergent du RFC 9162 (E-01) | Si faux : V-01 — TC-NOM-06 avec vecteur dual-branch FAIL → boucle correction spec step 1 avant Gate 5 |
| HT-05 | event_hash stocké est déjà un leaf-hash au sens RFC 9162 (HASH(0x00‖data)) | V-05 — si faux : divergence racine systématique, invalidation crypto massive |
| HT-06 | L'endpoint /v2/merkle-proof/:eventId peut coexister avec l'endpoint v1 existant sans collision de route NestJS | Aucun — préfixe /v2 explicite |
| HT-07 | Les services MerkleProofService / MerkleProofVerifier PD-56 ne sont pas invoqués par PD-294 (aucune dépendance amont bidirectionnelle) | Aucun — PD-294 crée ses propres services, lit les mêmes repositories |
| HT-08 | Les repositories TypeORM peuvent être typés Readonly pour garantir statiquement INV-294-04 (via wrapper ReadOnlyRepository<T> ou convention d'injection) | Fallback : audit CI statique + revue humaine à chaque PR touchant v2/ |
| HT-09 | Le Pd294ExceptionFilter peut être scoped au module MerkleV2Module (APP_FILTER module-level) sans contaminer le filter global des autres modules | Fallback : filter global avec discrimination par route /v2/merkle-proof |
| HT-10 | tree_size int32 signé accepté par la spec (§5.2) reste cohérent avec le volume métier actuel (H-294-03) | V-06 — si très grands arbres (> 2^31-1) apparaissent : ERR-294-03 en cascade, escalade BIGINT |
9. Points de vigilance (risques, dette, pièges)¶
| ID | Sujet | Description | Origine |
|---|---|---|---|
| V-01 | Algo §5.8 divergent RFC 9162 — inner while-loop placée hors du if (LSB(fn)==1 OR fn==sn) | Le review step 3 v2 (E-01) démontre par contre-exemple leaf_index=4, tree_size=8 que la transcription §5.8 n'est pas RFC-conforme. Action : le vecteur figé VEC-RFC9162-SHA3-256-01 DOIT inclure un cas fn pair strict (fn != sn) pour que TC-NOM-06 détecte la divergence. Si TC-NOM-06 FAIL → escalade spec (boucle correction) avant Gate 5 | Review step 3 v2 E-01 (Bloquant) |
| V-02 | H-294-02 hash_algorithm amont | Le code PD-56 actuel label SHA-256. PD-294 force sha3-256. Si l'amont calcule réellement SHA-256 → TC-NOM-10 FAIL, invalidation complète. C11 (script empirique) doit tourner AVANT Gate 8, rapport archivé comme preuve CA-294-12 | Spec §9 H-294-02 + Review E-09 |
| V-03 | Desync CA-294-12 spec ↔ tests (Bloquant review step 3 v2 E-03) | Spec §7 : CA-294-12 = vérif empirique H-294-02. Tests §2 matrice : CA-294-12 = chiffrement. Action plan : C11 + TC-NOM-10 implémentent la version spec. Le document PD-294-tests.md reste à resynchroniser en amont Gate 5 par le PO | Review step 3 v2 E-02, E-03 |
| V-04 | Normalisation sans vérification d'intégrité amont | Aucune vérification hsm_signature/tsa_token avant normalisation. Dette eIDAS documentée (hors périmètre spec §2). Mitigation : le client DOIT vérifier la signature envelope avant de faire confiance à la preuve | Review E-10 (persistant v1) |
| V-05 | Leaf-hash LH(m)=HASH(0x00‖m) non contractualisé | La spec §5.8 définit NODE_HASH(0x01‖l‖r) mais pas LEAF_HASH. Hypothèse "event_hash est un leaf-hash déjà préfixé" non testée. Impact : divergence racine silencieuse | Review step 3 v2 E-12 |
| V-06 | tree_size int32 vs uint64 RFC | Borne <= 2^31-1 contractuelle. Les preuves RFC natives avec tree_size >= 2^31 seraient rejetées ERR-294-03. Interopérabilité RFC partielle, dette documentée | Review E-08 |
| V-07 | Anti-énumération ERR-294-10 vs ERR-294-06 (404 vs 400) | Permet à un attaquant de distinguer "preuve absente" de "ID mal formé". Dette connue, hors périmètre PD-294 (learning universel PD-85 non appliqué ici par choix contractuel) | Review E-19 |
| V-08 | Code legacy MerkleProofService PD-56 | Le service v1 ne doit pas être dépendance de PD-294 pour éviter un couplage qui forcerait la mise à jour v1. Contrôle Code Contracts forbidden : v2/ NE DOIT PAS importer MerkleProofService | INV-294-03 dual-read-single-write |
| V-09 | Interceptor post-sérialisation et wrapping NestJS | Le scan anti-leak doit fonctionner selon le format exact de réponse. Vérifier au step 6 : response.data.merkle_proof vs response.merkle_proof. Test d'intégration TC-ERR-08 OBLIGATOIRE | INV-294-07 |
| V-10 | Audit fail-closed (apprentissage universel) | MerkleProofAuditService.logXxx() jamais enrobé par .catch(() => log). Pattern correct : finally { await audit } ou catch { await audit; throw } | Learning universel anti-catch-absorb (PD-85, PD-63, PD-250, PD-262, PD-265) |
| V-11 | Stubs inter-PD | Aucun stub identifié. Tous les composants PD-294 sont auto-contenus | — |
| V-12 | Branded types HexHash32 | Learning universel (2026-03-04) : refined types obligatoires pour UUID/hex sémantiquement distincts. Application : HexHash32 pour event_hash, merkle_root, éléments inclusion_path. Constructeur validant avec SHA3_HEX_LOWERCASE_REGEX à la construction | Learning universel types branded |
| V-13 | crypto.randomUUID() pour correlationId | Aucun Math.random() dans PD-294 (learning universel S2245) | Learning universel |
| V-14 | Purge stale | Aucun fichier temporaire sensible dans PD-294 (module lecture seule) | Learning universel PD-283 — N/A ici |
| V-15 | Validation format ≠ fonctionnelle (learning universel PD-283) | hash_algorithm='sha3-256' validé en format (regex) par C4 ET vérifié fonctionnellement via Rfc9162Verifier (C5) qui utilise réellement SHA3-256. Les deux vérifications DOIVENT exister | Learning universel PD-283/PD-282/PD-265 |
Mécanismes cross-module¶
Aucune modification d'autres modules.
Le plan PD-294 est strictement contenu dans src/modules/merkle/v2/. Il n'ajoute ni guard, ni middleware, ni intercepteur affectant les routes d'autres modules (documents, anchor, audit externe). Les repositories TypeORM utilisés (MerkleLeaf, MerkleTree, AnchorBatch, AnchorBatchEvent) sont partagés en lecture, ce qui ne constitue pas une modification cross-module. L'interceptor EnvelopeLeakInterceptor (C9) est scoped au module MerkleV2Module uniquement.
Périmètre de test¶
| Niveau de test | In scope | Hors scope (justification) |
|---|---|---|
| Unitaire | C1 (validateurs format), C2 (classifier + machine d'états), C3 (normalizer), C4 (validator §5.1), C5 (Rfc9162Verifier + vecteurs figés), C8 (exceptions mapping), C9 (interceptor logique de scan) | — |
| Intégration | C6 orchestrator + repositories réels (TypeORM SQLite in-memory ou PostgreSQL test container), C7 controller + interceptors + filters, C10 audit hook, flux complet TC-NOM-01..02, TC-ERR-*, TC-NR-01..04 | — |
| E2E | Endpoint /v2/merkle-proof/:eventId — flux réponse complet via Supertest, snapshot JSON v2, test de coexistence avec endpoint v1 | — |
| Campagne empirique | C11 pd294-empirical-hash-check.ts sur échantillon DB staging | Sur prod : hors scope Gate 5, à exécuter pré-Gate 8 |
| Performance | Hors scope | Module lecture seule sans transformation coûteuse ; RFC verifier = O(log tree_size) SHA3-256 → négligeable ; performance testing reportée si incident opérationnel |
| Sécurité (fuzzing entrées JSON) | Hors scope PD-294 | Le fuzzing complet des entrées API est couvert par le pipeline sécurité Sonar + tests négatifs TC-NEG-01..06 (cas représentatifs). Fuzzing exhaustif = backlog plate-forme |
Couverture minimale attendue : 80% lignes sur src/modules/merkle/v2/ (hors __tests__, *.dto.ts, fichiers de barrel export).
10. Hors périmètre¶
- Modification des sous-objets
ProofEnvelopeautres quemerkle_proof(tsa_token,hsm_signature,blockchain_anchor,event_metadata) - Consistency proofs RFC 9162 §2.1.4
- Migration rétroactive des preuves v1 en base (INV-294-04)
- Changement d'algorithme cryptographique amont (si H-294-02 est fausse, PD-294 bloque — pas de correction amont ici)
- Chiffrement d'artefacts cryptographiques temporaires
- Modification de
MerkleProofService(PD-56), ses DTOs, son verifier - Modification DDL / schéma PostgreSQL (§5.9)
- Correction du code d'algorithme §5.8 vs RFC 9162 strict (V-01) — si nécessaire, boucle correction spec step 1, pas step 4
- Resynchronisation du document
PD-294-tests.mdavec la spec v2 (V-03) — responsabilité step 2 / PO
Code contracts similaires¶
Aucune injection effectuée (placeholder {{SIMILAR_CONTRACTS}} non renseigné par l'orchestrateur). À enrichir si une injection automatique est disponible (cf. /contracts "merkle proof normalisation").
Références¶
- Spec : PD-294-specification.md (v2)
- Tests : PD-294-tests.md
- Review step 3 v2 : PD-294-review-step3-v2.md (3 Bloquants, 10 Majeurs, 9 Mineurs)
- RFC 9162 §2.1.1 (LH), §2.1.3, §2.1.3.2 (inclusion path verification)
- Epic :
ProbatioVault-backend/docs/epics/probatoire/PD-294-merkle-rfc9162 - Module existant :
src/modules/merkle/(PD-56 / PD-237) - Learnings universels : branded types, crypto.randomUUID, anti-catch-absorb, validation format ≠ fonctionnelle, anti-énumération