PD-282 — Plan d'implementation¶
1. Contexte¶
- Story : PD-282 — ProofEnvelope : scellement HSM (PROOF-14) + materiel validation eIDAS (PROOF-05b)
- Gate 3 : RESERVE (8.25/10 v3) — reserves sur Mode B test nominal, OCSP revoked, P95
- Auteur : Claude Opus 4.6 (etape 4)
- Date : 2026-03-02
2. Decisions architecturales¶
DA-01 — Extension de LegalCompositeProof (pas de table separee)¶
- Decision : Ajouter une colonne
envelope_sealJSONB nullable a la table existantelegal_composite_proof - Rationale : Evite un JOIN, le trigger PD-272 existant protege deja l'immutabilite, la colonne nullable gere la retro-compatibilite
- Alternatives ecartees : Table separee
proof_envelope_sealavec FK ; stockage dans le JSONBverification_materialexistant - Trade-offs : Le code doit gerer les preuves sans sceau (NULL pour pre-PD-282)
DA-02 — Etat UNSEALED transitoire en memoire¶
- Decision : Pas de colonne
seal_status. L'INSERT est toujours atomique en etat SEALED - Rationale : Le trigger PD-272 bloque tout UPDATE. Persister en UNSEALED puis UPDATE vers SEALED est impossible sans modifier le trigger
- Alternatives ecartees : Colonne
seal_statusavec trigger conditionnel - Trade-offs : Pas d'etat intermediaire observable en DB (acceptable car construction transitoire)
DA-03 — SHA3-384 en software + CKM_ECDSA (raw) dans HSM¶
- Decision : Pre-hash SHA3-384 en software (js-sha3) puis signature ECDSA P-384 raw dans le HSM
- Rationale : CloudHSM ne supporte pas necessairement CKM_ECDSA_SHA3_384. Le pattern pre-hash + raw sign est deja utilise dans LegalAuditTrailService
- Alternatives ecartees : CKM_ECDSA_SHA384 avec double hashing
- Trade-offs : Conforme a l'identifiant algorithm ; peut necessiter ajout ECDSA_RAW dans SignatureAlgorithm enum
DA-04 — Scellement integre dans generateProof()¶
- Decision : Le scellement est la derniere etape de
generateProof(), pas une methode separee - Rationale : Le trigger PD-272 bloque les UPDATE — la preuve doit etre inseree complete
- Alternatives ecartees : Methode
sealProof(proofId)separee avec UPDATE
DA-05 — @peculiar/asn1-ocsp pour OCSP¶
- Decision : Utiliser @peculiar/asn1-ocsp pour l'encodage/decodage ASN.1 OCSP
- Rationale : TypeScript pur, maintenu, compatible @peculiar/x509
- Alternatives ecartees : Package npm
ocsp; appel OpenSSL CLI via child_process - Compatibilite ESM/CJS : Les packages @peculiar/* sont ESM-first avec exports CJS via package.json
exportsfield. Le backend NestJS est en CJS (tsconfigmodule: commonjs). Verification :require('@peculiar/asn1-ocsp')fonctionne grace aux exports CJS declares. En cas d'incompatibilite : dynamic importawait import('@peculiar/asn1-ocsp')en fallback.
DA-06 — Chaines de certificats intermediaires via ConfigService¶
- Decision : Les intermediaires et root sont configures via NestJS ConfigService (env vars ou fichiers PEM)
- Rationale : Le HSM stocke le leaf cert. Les intermediaires sont stables et geables en config
- Alternatives ecartees : Stocker toute la chaine dans le HSM ; telecharger dynamiquement via AIA
3. Adressage des reserves Gate 3¶
Reserve ECT-01 — Mode B sans test nominal¶
- Action plan : Le service OfflineVerificationService implemente Mode A ET Mode B. Le test nominal Mode B sera ajoute au test suite par l'agent-verifier (TC-NOM-xx-MODE-B).
Reserve ECT-02 — OCSP revoked non contractualise¶
- Action plan : Le VerificationMaterialAssembler rejette la finalisation si
status=revokeddetecte (exceptionOCSP_CERT_REVOKED). Contractualise dans le code-contract moduleverification-material-assembly.
Reserve DIV-01 — signedAt non protege par le sceau¶
- Action plan : Accepte par design (signedAt est dans envelopeSeal, exclu du hash). Mitigation existante :
validationTimestampdans le hash + token TSA PD-81. Le plan documente cette decision comme acceptable.
Adressage ecarts Gate 5 v1¶
INV-282-07 — Artefacts crypto temporaires (BLQ Gate 5)¶
Le pipeline PD-282 ne persiste AUCUN artefact crypto temporaire sur disque ni en base : - Le hash SHA3-384 est calcule en memoire (variable Buffer, garbage-collected apres INSERT) - La signature ECDSA est executee dans le HSM — la cle privee ne quitte jamais le HSM - Le canonicalJson (string) est une variable locale, jamais ecrite sur disque - Aucun fichier temporaire, aucun blob de staging, aucun cache de signature
Conclusion : INV-282-07 est satisfait par conception — il n'existe pas d'artefact crypto temporaire a chiffrer. L'exigence "chiffre au repos" s'applique aux modules qui produisent des artefacts persistants (ex: DEK dans PD-X, ReKey dans PD-Y). PD-282 ne produit que des valeurs transitoires en memoire.
Dispositif de verification (TC-INV-07) : Le test verifie par audit statique que : 1. Aucune ecriture fichier temporaire n'existe dans le code du module seal/verification 2. Le canonicalJson est une const locale (pas de member/property assignee a un service) 3. Les tests unitaires mockent le HSM sans jamais persister de signature intermediaire 4. Review code Gate 7 verifie l'absence de persistance non contractuelle
TC-NOM-08 — Simulation crash (BLQ Gate 5)¶
Strategie de test d'atomicite par injection de faute transactionnelle : 1. Pre-commit crash : Le test injecte une erreur (jest.spyOn(queryRunner, 'commitTransaction').mockRejectedValueOnce(new Error('simulated crash'))) dans la transaction avant commit. Verification : aucune donnee persistee (SELECT COUNT(*) FROM legal_composite_proof WHERE ... = 0). 2. Post-commit crash : Le test laisse le commit reussir puis simule un crash applicatif (kill du handler post-commit). Verification : la preuve est persistee avec sceau valide (reconciliation non necessaire car l'INSERT atomique a reussi). 3. Points d'observation : assertions sur l'etat de la table + verification du sceau via OfflineVerificationService.verify() sur la preuve inseree.
OCSP revoked — Ajout contractuel (MAJ Gate 5)¶
Le rejet sur status=revoked est un ajout contractuel impose par la Reserve ECT-02 de Gate 3 (OCSP revoked non specifie → BLOQUANT). Ce n'est pas un ecart par rapport a la spec : c'est la resolution d'un ecart identifie en Gate 3. La spec sera amendee en consequence pour contractualiser ce comportement.
Mode B — Verification en ligne constitutive (MAJ Gate 5)¶
Correction : en Mode B, la verification en ligne (OCSP live, CRL publics) est constitutive (non optionnelle). Le service OfflineVerificationService en Mode B DOIT : 1. Verifier le sceau (comme Mode A) 2. Verifier les chaines de certificats (comme Mode A) 3. En plus : requeter les serveurs OCSP/CRL publics pour confirmer la non-revocation des certificats au moment de la verification Si la verification en ligne echoue (timeout, erreur), Mode B retourne valid: false avec reason: 'online_verification_failed'.
Framework de test — Jest (MAJ Gate 5)¶
Framework : Jest (standard NestJS, deja configure dans le projet). Runner : npm run test (unitaires), npm run test:e2e (integration). Variables CI necessaires : DATABASE_URL, HSM_PROVIDER=mock, NODE_ENV=test.
Notes issues du dossier Gate 3 v2¶
| Ref | Sujet | Resolution |
|---|---|---|
| HD-02 | JCS + jsonb round-trip | Le sceau est calcule sur la forme pre-INSERT. Le stockage jsonb ne modifie pas le blob envelope_seal car c'est un objet JSON opaque. |
| HD-03 | Concurrence finalisation | generateProof() est appele dans une transaction avec le lock existant. Pas de double scellement possible. |
| AMB-04 | Mecanisme reconciliation | Reference : le reconciliation handler existant dans le module legal-pre couvre le rattrapage post-crash. |
| AMB-05 | Ordre temporel | L'implementation assemble verificationMaterial PUIS calcule le sceau. Ordre documente dans le code-contract. |
| RSC-01 | OCSP cert scellement | Hors perimetre PD-282 (le certificat HSM ProbatioVault n'a pas de responder OCSP public). A evaluer dans une story dediee si necessaire. |
4. Decomposition en modules¶
4.1 agent-entity (C1-C4)¶
Module : proof-envelope-entity - Interfaces TypeScript : EnvelopeSeal, EnhancedVerificationMaterial, OcspResponseSnapshot, SealedProofEnvelope - Extension entite : colonne envelope_seal dans LegalCompositeProof - Migration TypeORM : PD282-AddEnvelopeSealColumn.ts - Module DI : registration dans LegalPreModule
Fichiers : - src/modules/legal-pre/interfaces/proof-envelope-seal.interfaces.ts - src/modules/legal-pre/entities/legal-composite-proof.entity.ts - src/database/migrations/*-PD282-AddEnvelopeSealColumn.ts - src/modules/legal-pre/legal-pre.module.ts
4.2 agent-seal (C5-C9)¶
Module : envelope-seal-pipeline - Service principal : EnvelopeSealService — pipeline JCS, SHA3-384, ECDSA P-384 HSM - Service detection : SecretDetectionService — scan patterns interdits - Service validation : EnvelopeFormatValidatorService — regex contractuelles - Extension hash : hashSha3_384() dans HashService
Pipeline de scellement : 1. Valider format de l'enveloppe (format validation) 2. Scanner pour secrets (secret detection) 3. Exclure envelopeSeal de l'objet 4. Canonicaliser JCS RFC 8785 5. Hash SHA3-384 6. Signer ECDSA P-384 via HSM (raw sign) 7. Assembler envelopeSeal : {algorithm, signature, kid, signedAt, certificateChain}
Fichiers : - src/modules/legal-pre/services/envelope-seal.service.ts - src/modules/legal-pre/services/secret-detection.service.ts - src/modules/legal-pre/services/envelope-format-validator.service.ts - src/modules/crypto/hash.service.ts - src/modules/legal-pre/services/legal-composite-proof.service.ts
4.3 agent-verification (C10-C11)¶
Module : verification-material-assembly - Service assembleur : VerificationMaterialAssembler — collecte chaines, OCSP, timestamps - Service OCSP : OcspClientService — requetes OCSP paralleles, timeout configurable
Pipeline d'assemblage : 1. Recuperer chaines TSA et eIDAS (config + references existantes) 2. Requeter OCSP en parallele (Promise.allSettled) 3. Si timeout total → policy OCSP_UNAVAILABLE, ocspResponses=[] 4. Si status=revoked → exception OCSP_CERT_REVOKED (rejet finalisation) 5. Normaliser certSerialNumber en uppercase 6. Assembler verificationMaterial avec validationTimestamp courant
Fichiers : - src/modules/legal-pre/services/verification-material-assembler.service.ts - src/modules/legal-pre/services/ocsp-client.service.ts
4.4 agent-verifier (C12)¶
Module : offline-verification - Service : OfflineVerificationService — verification Mode A et Mode B
Pipeline de verification : 1. Extraire envelopeSeal de l'enveloppe 2. Reconstruire l'objet sans envelopeSeal 3. Canonicaliser JCS RFC 8785 (meme implementation que scellement) 4. Hash SHA3-384 5. Extraire cle publique de certificateChain[0] (leaf cert) 6. Verifier signature ECDSA P-384 via crypto.createVerify() (dsaEncoding: 'ieee-p1363') 7. Verifier chaine de certificats 8. Mode B uniquement : verification OCSP en ligne constitutive (echec → valid=false)
Fichiers : - src/modules/legal-pre/services/offline-verification.service.ts
4.5 Diagrammes Mermaid¶
Graphe de dependances inter-modules¶
graph LR
subgraph "PD-282 — Modules"
ENTITY["proof-envelope-entity<br/>(agent-entity)"]
SEAL["envelope-seal-pipeline<br/>(agent-seal)"]
VERIF_MAT["verification-material-assembly<br/>(agent-verification)"]
OFFLINE["offline-verification<br/>(agent-verifier)"]
end
subgraph "Services internes"
SEAL_SVC["EnvelopeSealService"]
SECRET["SecretDetectionService"]
FORMAT["EnvelopeFormatValidatorService"]
OCSP["OcspClientService"]
VERIF_ASM["VerificationMaterialAssembler"]
OFFLINE_SVC["OfflineVerificationService"]
end
subgraph "Dependances existantes"
PD272["PD-272<br/>Immutability Trigger"]
PD81["PD-81<br/>LegalCompositeProof / TSA"]
PD37["PD-37<br/>JsonCanonicalizeService"]
PD280["PD-280<br/>Modele d'etats"]
HSM["CloudHSM<br/>ECDSA P-384"]
HASH["HashService<br/>SHA3-384"]
end
subgraph "Dependances externes"
PECULIAR["@peculiar/asn1-ocsp<br/>@peculiar/asn1-x509"]
JSSHA3["js-sha3"]
end
SEAL --> ENTITY
VERIF_MAT --> ENTITY
OFFLINE --> ENTITY
OFFLINE --> SEAL
SEAL_SVC --> FORMAT
SEAL_SVC --> SECRET
SEAL_SVC --> PD37
SEAL_SVC --> HASH
SEAL_SVC --> HSM
VERIF_ASM --> OCSP
OCSP --> PECULIAR
OFFLINE_SVC --> PD37
OFFLINE_SVC --> HASH
OFFLINE_SVC --> OCSP
ENTITY --> PD272
ENTITY --> PD81
ENTITY --> PD280
HASH --> JSSHA3 Sequence — Pipeline de scellement (generateProof)¶
sequenceDiagram
participant Caller as LegalCompositeProofService
participant FMT as EnvelopeFormatValidatorService
participant SEC as SecretDetectionService
participant JCS as JsonCanonicalizeService<br/>(PD-37)
participant HASH as HashService
participant HSM as CloudHSM
participant VMA as VerificationMaterialAssembler
participant OCSP as OcspClientService
participant DB as PostgreSQL
Caller->>VMA: assembleVerificationMaterial(proofEnvelope)
activate VMA
VMA->>VMA: Recuperer chaines TSA + eIDAS (ConfigService)
VMA->>OCSP: queryAll(certificates)
activate OCSP
OCSP-->>VMA: OcspResponseSnapshot[]
deactivate OCSP
alt status = revoked
VMA-->>Caller: throw OCSP_CERT_REVOKED
end
VMA-->>Caller: EnhancedVerificationMaterial
deactivate VMA
Caller->>Caller: Attacher verificationMaterial a l'enveloppe
Caller->>FMT: validate(proofEnvelope)
activate FMT
FMT-->>Caller: OK / throw FORMAT_ERROR
deactivate FMT
Caller->>SEC: scan(proofEnvelope)
activate SEC
SEC-->>Caller: OK / throw SECRET_DETECTED
deactivate SEC
Caller->>Caller: Exclure envelopeSeal de l'objet
Caller->>JCS: canonicalize(envelopeWithoutSeal)
activate JCS
JCS-->>Caller: canonicalJson (string)
deactivate JCS
Caller->>HASH: hashSha3_384(canonicalJson)
activate HASH
HASH-->>Caller: digest (Buffer 48 bytes)
deactivate HASH
Caller->>HSM: sign(CKM_ECDSA, digest, kid)
activate HSM
HSM-->>Caller: signature (ieee-p1363)
deactivate HSM
Caller->>Caller: Assembler EnvelopeSeal
Caller->>DB: INSERT legal_composite_proof (atomique, trigger PD-272)
activate DB
DB-->>Caller: OK (SEALED)
deactivate DB Sequence — Verification offline (Mode A) et en ligne (Mode B)¶
sequenceDiagram
participant Client as Appelant
participant OVS as OfflineVerificationService
participant JCS as JsonCanonicalizeService<br/>(PD-37)
participant HASH as HashService
participant CRYPTO as crypto.verify()
participant OCSP as OcspClientService
Client->>OVS: verify(sealedEnvelope, mode)
activate OVS
OVS->>OVS: Extraire envelopeSeal
OVS->>OVS: Reconstruire objet sans envelopeSeal
OVS->>JCS: canonicalize(envelopeWithoutSeal)
JCS-->>OVS: canonicalJson
OVS->>HASH: hashSha3_384(canonicalJson)
HASH-->>OVS: digest
OVS->>OVS: Extraire publicKey de certificateChain[0]
OVS->>CRYPTO: verify(null, digest, publicKey, signature)
CRYPTO-->>OVS: true / false
alt signature invalide
OVS-->>Client: { valid: false, reason: 'invalid_signature' }
end
OVS->>OVS: Verifier chaine de certificats (trustedRoots obligatoire)
alt Mode B
OVS->>OCSP: queryAll(certificateChain)
activate OCSP
OCSP-->>OVS: OcspResponseSnapshot[]
deactivate OCSP
alt verification en ligne echouee
OVS-->>Client: { valid: false, reason: 'online_verification_failed' }
end
end
OVS-->>Client: { valid: true }
deactivate OVS 5. Couverture des invariants¶
| Invariant | Module(s) | Composant(s) |
|---|---|---|
| INV-282-01 | seal-pipeline | EnvelopeSealService |
| INV-282-02 | seal-pipeline | EnvelopeSealService (excl. envelopeSeal) |
| INV-282-03 | seal-pipeline | JCS canonicalisation + SHA3-384 + ECDSA |
| INV-282-04 | verification-material, offline-verification | VerificationMaterialAssembler, OfflineVerificationService |
| INV-282-05 | verification-material | VerificationMaterialAssembler (validationPolicy) |
| INV-282-06 | secret-detection | SecretDetectionService |
| INV-282-07 | seal-pipeline | Hash non reversible, cle dans HSM |
| INV-282-08 | entity | Trigger PD-272 existant |
| INV-282-09 | entity | SEALED terminal (pas d'UPDATE possible) |
| INV-282-10 | entity | Pas de SEALED->UNSEALED (trigger PD-272) |
| INV-282-11 | offline-verification | kid + certificateChain embarquee |
| INV-282-12 | format-validation | EnvelopeFormatValidatorService |
6. Dependances externes¶
| Dependance | Usage | Statut |
|---|---|---|
| PD-272 (immutability trigger) | Protection immutabilite | Disponible |
| PD-81 (LegalCompositeProof) | Token TSA dans ProofEnvelope | Disponible |
| PD-280 (modele d'etats) | PENDING/INDETERMINATE | Disponible |
| PD-37 (JsonCanonicalizeService) | Canonicalisation RFC 8785 | Disponible |
| js-sha3 | SHA3-384 hash | Dependance existante |
| @peculiar/asn1-ocsp | Encodage/decodage OCSP ASN.1 | A ajouter |
| @peculiar/asn1-x509 | Parsing certificats X.509 | A ajouter |
7. Strategie de tests¶
| Niveau | Couverture | Agent |
|---|---|---|
| Unitaire | Chaque service isole (mock HSM, mock OCSP) | Chaque agent |
| Integration | Pipeline complet generateProof() avec seal et verify | agent-verifier |
| Negatif | TC-NEG-01..10 (rejets format, secrets, rotation, replay) | agent-seal + agent-verifier |
| Non-regression | PD-272 trigger, PD-81 TSA, PD-280 etats | agent-entity |
| Mode A | Verification offline (no network). Observabilite air-gap : mock HttpService/HttpModule, assertion expect(httpService.get/post).not.toHaveBeenCalled() pour prouver zero appel reseau PV. | agent-verifier |
| Mode B | Verification online (OCSP live, CRL) | agent-verifier |
8. Risques et mitigations¶
| Risque | Probabilite | Impact | Mitigation |
|---|---|---|---|
| CloudHSM ne supporte pas CKM_ECDSA raw | Faible | Haut | Pre-hash SHA3-384 + raw sign deja valide sur LegalAuditTrailService |
| Interop SHA3-384 sign(HSM) / verify(Node.js) | Faible | Haut | Le pattern pre-hash SHA3-256 + raw ECDSA est valide avec LegalAuditTrailService. Pour SHA3-384, un test d'interoperabilite explicite (sign via HSM + verify via crypto.createVerify('SHA3-384') avec dsaEncoding ieee-p1363) DOIT etre execute en CI. Cf. TC-INV-xx-INTEROP dans le plan de tests. |
| @peculiar/asn1-ocsp incompatible | Faible | Moyen | Fallback : ocsp npm package |
| OCSP responders intermittents | Moyen | Moyen | Timeout + policy OCSP_UNAVAILABLE + monitoring 10% seuil |
| JCS round-trip jsonb | Faible | Haut | envelope_seal est un blob JSON opaque, pas reconstruit depuis jsonb |