Aller au contenu

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 merklePathinclusion_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 defaultREJECTED_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 Pd294Exception contenant code: string et httpStatus: HttpStatus.
  • Un Pd294ExceptionFilter (NestJS) intercepte, logge, appelle l'audit fail-closed, puis retourne :
    { "error_code": "ERR-294-XX", "message": "..." }
    
  • Fail-closed audit (learning universel catch-absorb) : le filter await l'audit SANS .catch(() => ...). Pattern correct :
    try { await this.audit.logRejection(code, ctx); }
    finally { response.status(httpStatus).json(body); }
    
    → toute erreur d'audit remonte en 500 (la visibilité rejet reste garantie).
  • Anti-énumération (risque V-07) : ERR-294-10 (404) distinguable de ERR-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 UTC
  • proof_normalized : eventId, state: LEGACY_V1_DETECTED → CANONICAL_V2_READY
  • proof_rejected : eventId, error_code, cause
  • proof_transition_forbidden : eventId, from, to, ERR-294-11
  • proof_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 ProofEnvelope autres que merkle_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.md avec 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