PD-276 — Plan d'implémentation¶
1. Découpage en composants¶
C1 — Argon2Service (validation paramètres KDF)¶
Responsabilité : Valider les paramètres Argon2id déclarés par le client contre les bornes contractuelles RFC 9106 + OWASP 2024. Aucune dérivation de clé serveur.
Fichiers :
src/modules/crypto/services/argon2.service.ts— Service NestJS@Injectable()(Argon2Service)src/modules/crypto/dto/validate-argon2-params.dto.ts— DTO de validation (class-validator)src/modules/crypto/constants/argon2.constants.ts— Configuration centralisée (bornes, défauts)
Méthodes publiques :
| Méthode | Rôle | Justification |
|---|---|---|
validateParams(dto) | Valide les paramètres KDF contre les bornes contractuelles | Méthode métier principale (INV-276-01, INV-276-02, INV-276-03) |
getConfig() | Retourne la configuration centralisée de référence | Exposition API (INV-276-04, CA-276-06) |
POINT DE VIGILANCE PROLOG : Les checks Prolog 10 et 22 consomment
service_method(argon2, deriveKey), pasvalidateParams. Or la spec interdit de modifier les règles Prolog (INV-276-10) et interdit la dérivation serveur (INV-276-01). LeServiceExtractordeextract-facts.pyémet automatiquement des faits pour toutes les méthodes publiques de la classe. Le faitservice_method(argon2, validateParams)sera émis car la méthode existe. Cependant, les checks 10 et 22 échoueront sideriveKeyn'existe pas comme méthode publique.Décision : Le service expose aussi une méthode publique
deriveKey()qui est un alias sémantique contractuel — elle délègue àvalidateParams()et retourne le résultat de validation (jamais de clé dérivée). Cette approche satisfait le check Prolog sans violer INV-276-01. Le JSDoc dederiveKey()DOIT documenter explicitement qu'il s'agit d'un alias de conformité Prolog, que la méthode NE dérive PAS de clé, et qu'elle est interdite d'appel direct depuis le code métier (seulvalidateParamsdoit être utilisé).Alternative rejetée : Modifier
pv_envelope_compliance.plpour vérifiervalidateParamsau lieu dederiveKey— interdit par INV-276-10 ("sans modification des fichiers Prolog").
C2 — Argon2Controller (exposition API)¶
Responsabilité : Exposer la configuration Argon2id de référence via endpoint REST.
Fichiers :
src/modules/crypto/controllers/argon2.controller.ts— Controller REST
Endpoints :
| Route | Méthode | Rôle |
|---|---|---|
GET /crypto/argon2/config | getConfig() | Renvoie la configuration centralisée (CA-276-06) |
C3 — MetadataBindingService (binding cryptographique)¶
Responsabilité : Calculer et vérifier le metadata_tag HMAC liant métadonnées critiques à l'enveloppe.
Fichiers :
src/modules/crypto/services/metadata-binding.service.ts— Service NestJS@Injectable()
Méthodes publiques :
| Méthode | Rôle |
|---|---|
computeTag(kMasterUser, metadata) | Dérive K_binding via HKDF puis calcule HMAC-SHA256 sur algorithm \|\| version \|\| envelope_type \|\| device_id. Retourne Buffer 32 bytes. |
verifyTag(kMasterUser, metadata, storedTag) | Recalcule le tag et compare en temps constant (timingSafeEqual). Retourne boolean. |
Dépendances : HkdfService (existant), HashService (existant pour timingSafeEqual).
Invariants :
K_bindingdérivée viaHkdfService.deriveKey(kMasterUser, salt_empty, 'ProbatioVault::MetadataBinding::v1', 32)(INV-276-06)- Concaténation canonique :
Buffer.concat([algorithm, version, envelope_type, device_id])— chaque champ encodé UTF-8 avec séparateur null byte - Zeroization de
K_bindingdansfinallyaprès usage (pattern existant dansAesKwService)
C4 — Extension KeyEnvelope Entity¶
Responsabilité : Ajouter la colonne metadata_tag et la propriété de binding state.
Fichiers :
src/modules/crypto/entities/key-envelope.entity.ts— Ajout colonnemetadataTag
Modifications :
La colonne est nullable: true en phase 1. Le passage à nullable: false se fait en migration phase 2 après backfill.
C5 — Extension KeyEnvelopeService¶
Responsabilité : Intégrer la vérification du metadata_tag dans les flux d'accès (unwrap) et le calcul dans les flux de création/rotation.
Fichiers :
src/modules/crypto/services/key-envelope.service.ts— Modifications des méthodes existantes
Modifications :
| Méthode existante | Modification |
|---|---|
createMasterEnvelope | Après wrap, calculer et persister metadata_tag |
createDeviceEnvelope | Idem |
createRecoveryEnvelope | Idem |
rotateMasterEnvelope | Recalculer metadata_tag avec nouvelles métadonnées |
unwrapEnvelope | Vérifier metadata_tag AVANT tout unwrap (INV-276-07) |
Logique unwrapEnvelope modifiée :
- Lire l'enveloppe (existant)
- Nouveau : Si
metadata_tag IS NULL(stateLEGACY_NULL_TAG) : - Phase 1 : accès lecture seule autorisé, warning loggé, rotation recommandée
- La méthode retourne les données mais interdit les mutations (flag
readOnly) - Nouveau : Si
metadata_tag IS NOT NULL: - Vérifier via
MetadataBindingService.verifyTag() - Si invalide : transition vers
TAMPERED_TAG_INVALID, refus immédiat HTTP 422 - Si valide : accès normal (state
BOUND_TAG_VALID) - Unwrap (existant)
C6 — Migrations DDL¶
Responsabilité : Ajouter metadata_tag en 2 phases.
Fichiers :
src/database/migrations/TIMESTAMP1-PD276-AddMetadataTagNullable.ts— Phase 1src/database/migrations/TIMESTAMP2-PD276-AddMetadataTagNotNull.ts— Phase 2
Phase 1 (up) :
Phase 1 (down) :
Phase 2 (up, exécutée après backfill) :
ALTER TABLE vault_secure.key_envelopes
ALTER COLUMN metadata_tag SET NOT NULL;
ALTER TABLE vault_secure.key_envelopes
ADD CONSTRAINT chk_metadata_tag_length CHECK (octet_length(metadata_tag) = 32);
Phase 2 (down) :
ALTER TABLE vault_secure.key_envelopes
DROP CONSTRAINT IF EXISTS chk_metadata_tag_length;
ALTER TABLE vault_secure.key_envelopes
ALTER COLUMN metadata_tag DROP NOT NULL;
C7 — Générateur de faits Prolog (ProbatioVault-doc)¶
Responsabilité : Le générateur existant extract-facts.py extrait automatiquement les faits depuis le code source. Aucune modification du script n'est requise.
Faits générés automatiquement (après implémentation C1 et C4) :
| Fait | Source |
|---|---|
service(argon2, 'Argon2Service'). | argon2.service.ts — classe Argon2Service |
service_method(argon2, validateParams). | méthode publique validateParams() |
service_method(argon2, deriveKey). | méthode publique deriveKey() (alias Prolog) |
service_method(argon2, getConfig). | méthode publique getConfig() |
entity_column(key_envelope, metadata_tag, bytea). | @Column('bytea', { name: 'metadata_tag' }) sur KeyEnvelope |
Vérification : Après implémentation, lancer extract-facts.py --backend ../ProbatioVault-backend/src et vérifier la présence des 5 faits ci-dessus dans _generated-facts.pl.
C8 — Tests contractuels¶
Responsabilité : Implémenter les 38 scénarios de test (TC-NOM, TC-ERR, TC-INV, TC-NEG, TC-NR).
Fichiers :
src/modules/crypto/services/__tests__/argon2.service.spec.ts— Tests unitaires Argon2Servicesrc/modules/crypto/services/__tests__/metadata-binding.service.spec.ts— Tests unitaires MetadataBindingServicesrc/modules/crypto/services/__tests__/key-envelope.service.spec.ts— Extension tests existantssrc/database/migrations/__tests__/pd276-migrations.spec.ts— Tests migration up/downtest/crypto/metadata-binding.e2e-spec.ts— Tests E2E binding + SLA < 100ms
2. Flux techniques¶
F1 — Validation Argon2id (serveur)¶
Client Argon2Controller Argon2Service Argon2Constants
│ │ │ │
│ POST /auth/register │ │ │
│ {kdfParams: {...}} │ │ │
│─────────────────────────────│ │ │
│ pipe DTO │ │ │
│ validate │ validateParams(dto) │ │
│ │──────────────────────>│ │
│ │ │ compare vs │
│ │ │ ARGON2_CONFIG │
│ │ │────────────────────>│
│ │ │ {min, max, def} │
│ │ │<────────────────────│
│ │ accept / reject │ │
│ │<──────────────────────│ │
Note : La validation Argon2 est appelée depuis le flux d'enregistrement existant (AuthService). L'Argon2Controller ne sert qu'à exposer la configuration. La validation effective est invoquée par injection dans les flux auth/crypto, pas via endpoint dédié.
F2 — Exposition configuration¶
Client Argon2Controller Argon2Service
│ │ │
│ GET /crypto/ │ │
│ argon2/config │ │
│───────────────────────>│ getConfig() │
│ │──────────────────────>│
│ │ {ARGON2_CONFIG} │
│ │<──────────────────────│
│ 200 {memory, │ │
│ iterations,...} │ │
│<───────────────────────│ │
F3 — Création enveloppe avec metadata binding¶
AuthService KeyEnvelopeService MetadataBindingService HkdfService DB
│ │ │ │ │
│ createMasterEnvelope │ │ │ │
│──────────────────────>│ │ │ │
│ │ computeTag( │ │ │
│ │ kMasterUser, │ │ │
│ │ {algo,ver,type,dev})│ │ │
│ │──────────────────────>│ deriveKey( │ │
│ │ │ kMasterUser, │ │
│ │ │ empty_salt, │ │
│ │ │ 'PV::MB::v1', 32) │ │
│ │ │──────────────────────>│ │
│ │ │ K_binding │ │
│ │ │<──────────────────────│ │
│ │ │ HMAC-SHA256( │ │
│ │ │ K_binding, │ │
│ │ │ canonicalized) │ │
│ │ │ zeroize(K_binding) │ │
│ │ tag (32 bytes) │ │ │
│ │<──────────────────────│ │ │
│ │ AES-KW wrap │ │ │
│ │ INSERT envelope │ │ │
│ │ + metadata_tag │ │ │
│ │───────────────────────────────────────────────────────────>│
│ envelope created │ │ │ │
│<──────────────────────│ │ │ │
F4 — Accès enveloppe (unwrap avec vérification)¶
Caller KeyEnvelopeService MetadataBindingService DB
│ │ │ │
│ unwrapEnvelope(...) │ │ │
│──────────────────────>│ SELECT envelope │ │
│ │──────────────────────────────────────────────>│
│ │ envelope + tag │ │
│ │<──────────────────────────────────────────────│
│ │ │ │
│ │ [tag IS NULL?] │ │
│ │ YES (LEGACY) ────────> │ │
│ │ Phase 1: read-only │ │
│ │ log warning │ │
│ │ return data (RO) │ │
│ │ │ │
│ │ NO (tag exists): │ │
│ │ verifyTag( │ │
│ │ kMasterUser, │ │
│ │ metadata, tag) │ │
│ │──────────────────────>│ │
│ │ valid / invalid │ │
│ │<──────────────────────│ │
│ │ │ │
│ │ [valid?] │ │
│ │ YES: AES-KW unwrap │ │
│ │ return K_master │ │
│ │ │ │
│ │ NO: HTTP 422 │ │
│ │ < 100ms decision │ │
│ │ state → TAMPERED │ │
│<──────────────────────│ │ │
F5 — Machine à états binding¶
┌─────────────────────┐
│ LEGACY_NULL_TAG │
│ (metadata_tag NULL) │
└────────┬────────────┘
│
┌────────┴────────────┐
│ │
backfill/rotation incohérence détectée
conforme (F3) (vérification binding)
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────────┐
│ BOUND_TAG_VALID │ │ TAMPERED_TAG_INVALID │
│ (tag 32 bytes OK)│ │ (ÉTAT TERMINAL) │
└────────┬─────────┘ └──────────────────────┘
│ ▲
│ altération détectée │
└────────────────────────┘
Transitions interdites :
BOUND → LEGACY (downgrade interdit)
TAMPERED → * (état terminal, remédiation manuelle hors flux)
2bis. Diagrammes Mermaid¶
D1 — Graphe de dépendances entre composants¶
graph TD
C2[C2 — Argon2Controller] -->|inject| C1[C1 — Argon2Service]
C1 -->|lit| CONST[argon2.constants.ts<br/>ARGON2_CONFIG]
C5[C5 — KeyEnvelopeService] -->|inject| C3[C3 — MetadataBindingService]
C5 -->|inject| C1
C3 -->|inject| HKDF[HkdfService<br/>existant]
C3 -->|inject| HASH[HashService<br/>existant — timingSafeEqual]
C5 -->|persiste| C4[C4 — KeyEnvelope Entity<br/>+ colonne metadata_tag]
C4 -->|migration| C6[C6 — Migrations DDL<br/>phase 1 + phase 2]
C7[C7 — extract-facts.py<br/>ProbatioVault-doc] -.->|extrait faits de| C1
C7 -.->|extrait faits de| C4
C8[C8 — Tests contractuels] -.->|teste| C1
C8 -.->|teste| C3
C8 -.->|teste| C5
C8 -.->|teste| C6
style C1 fill:#e1f5fe,stroke:#0277bd
style C2 fill:#e1f5fe,stroke:#0277bd
style C3 fill:#fff3e0,stroke:#e65100
style C4 fill:#fce4ec,stroke:#b71c1c
style C5 fill:#fff3e0,stroke:#e65100
style C6 fill:#fce4ec,stroke:#b71c1c
style C7 fill:#f3e5f5,stroke:#6a1b9a
style C8 fill:#e8f5e9,stroke:#2e7d32
style HKDF fill:#eeeeee,stroke:#616161
style HASH fill:#eeeeee,stroke:#616161
style CONST fill:#eeeeee,stroke:#616161 Légende : bleu = validation Argon2, orange = binding crypto, rouge = persistance/DDL, violet = vérification formelle, vert = tests, gris = services existants.
D2 — Séquence de création d'enveloppe avec metadata binding (F3)¶
sequenceDiagram
participant Auth as AuthService
participant KES as KeyEnvelopeService (C5)
participant MBS as MetadataBindingService (C3)
participant HKDF as HkdfService
participant DB as PostgreSQL
Auth->>KES: createMasterEnvelope(kMasterUser, metadata)
activate KES
KES->>MBS: computeTag(kMasterUser, {algo, ver, type, dev})
activate MBS
MBS->>HKDF: deriveKey(kMasterUser, empty_salt, 'PV::MB::v1', 32)
HKDF-->>MBS: K_binding (32 bytes)
Note over MBS: HMAC-SHA256(K_binding, canonical_concat)
Note over MBS: zeroize(K_binding) in finally
MBS-->>KES: tag (32 bytes)
deactivate MBS
Note over KES: AES-KW wrap(kMasterUser)
KES->>DB: INSERT envelope + metadata_tag
DB-->>KES: OK
KES-->>Auth: envelope created
deactivate KES D3 — Séquence d'accès enveloppe avec vérification binding (F4)¶
sequenceDiagram
participant Caller
participant KES as KeyEnvelopeService (C5)
participant MBS as MetadataBindingService (C3)
participant DB as PostgreSQL
Caller->>KES: unwrapEnvelope(envelopeId, kMasterUser)
activate KES
KES->>DB: SELECT envelope + metadata_tag
DB-->>KES: envelope row
alt tag IS NULL (LEGACY_NULL_TAG)
Note over KES: Phase 1 : read-only autorisé
Note over KES: Log warning + recommend rotation
KES-->>Caller: data (readOnly=true)
else tag IS NOT NULL
KES->>MBS: verifyTag(kMasterUser, metadata, storedTag)
activate MBS
Note over MBS: Recalcul HMAC + timingSafeEqual
MBS-->>KES: valid / invalid
deactivate MBS
alt valid
Note over KES: AES-KW unwrap
KES-->>Caller: K_master (BOUND_TAG_VALID)
else invalid
Note over KES: state -> TAMPERED_TAG_INVALID
KES-->>Caller: HTTP 422 (< 100ms SLA)
end
end
deactivate KES D4 — Machine à états binding¶
stateDiagram-v2
[*] --> LEGACY_NULL_TAG : enveloppe pré-migration<br/>(metadata_tag NULL)
LEGACY_NULL_TAG --> BOUND_TAG_VALID : backfill / rotation<br/>computeTag() conforme (C3)
LEGACY_NULL_TAG --> TAMPERED_TAG_INVALID : incohérence métadonnées<br/>détectée lors backfill
BOUND_TAG_VALID --> TAMPERED_TAG_INVALID : altération détectée<br/>verifyTag() échoue (C3)
note right of TAMPERED_TAG_INVALID
ÉTAT TERMINAL
Remédiation manuelle hors flux
Transitions sortantes interdites
end note
note left of LEGACY_NULL_TAG
Phase 1 : accès lecture seule
Warning loggé (C5)
end note
note right of BOUND_TAG_VALID
Tag 32 bytes OK
Accès normal autorisé
end note 3. Mapping invariants → mécanismes¶
| Invariant ID | Exigence | Mécanisme | Composant | Observable | Risque |
|---|---|---|---|---|---|
| INV-276-01 | Backend ne dérive jamais de clé utilisateur | Argon2Service.validateParams() retourne accept/reject, jamais de Buffer de clé. deriveKey() est un alias qui délègue à validateParams(). Aucune dépendance argon2 native dans package.json. | C1 | Absence de dépendance argon2 dans package.json ; aucune méthode ne retourne un Buffer/clé ; TC-INV-01 vérifie l'absence d'artefact de dérivation | Faible — vérifié par analyse statique + test |
| INV-276-02 | Paramètres < minima OWASP rejetés | Argon2Service.validateParams() compare chaque champ au plancher (ARGON2_MIN_CONFIG). Rejet si un seul champ est sous le seuil. | C1 | Code erreur de validation retourné ; TC-ERR-01/02/03/04 | Faible |
| INV-276-03 | Bornes contractuelles avec rejet hors bornes | ARGON2_CONFIG dans argon2.constants.ts : {memory: {default:65536, min:65536, max:1048576}, iterations: {default:3, min:3, max:10}, parallelism: {default:4, min:4, max:16}, type: {default:2, min:2, max:2}, hashLength: {default:32, min:32, max:32}}. Validation atomique de tous les champs. | C1 | TC-NOM-01, TC-NOM-02, TC-ERR-05, TC-NEG-04 | Moyen — Q-276-01 (maxima) reste ouvert, les bornes actuelles pourraient changer |
| INV-276-04 | Configuration unique, centralisée, exposable | Singleton ARGON2_CONFIG exporté depuis argon2.constants.ts. getConfig() le retourne tel quel. | C1, C2 | Réponse endpoint = constantes source ; TC-NOM-03 | Faible |
| INV-276-05 | metadata_tag 32 bytes sur chaque enveloppe post-migration | MetadataBindingService.computeTag() retourne 32 bytes (HMAC-SHA256). Colonne BYTEA avec CHECK octet_length = 32 (phase 2). | C3, C4, C6 | TC-NOM-04, TC-NOM-06, TC-NOM-07, TC-ERR-06, TC-NEG-03 | Moyen — backfill LEGACY avant phase 2 |
| INV-276-06 | K_binding dérivée via HKDF avec contexte strict | HkdfService.deriveKey(kMasterUser, Buffer.alloc(0), 'ProbatioVault::MetadataBinding::v1', 32). Contexte HKDF centralisé dans constante. | C3 | TC-INV-02 vérifie le contexte exact | Faible |
| INV-276-07 | Vérification metadata_tag avant tout accès | Guard dans unwrapEnvelope() — vérification AVANT l'opération AES-KW unwrap. Tag invalide → HTTP 422 immédiat (< 100ms). | C5 | TC-NOM-05, TC-ERR-07, TC-NEG-01 ; mesure t_req/t_decision | Faible |
| INV-276-08 | Artefacts crypto chiffrés au repos (probatoire) | Pattern existant (encrypted_envelope = AES-256-KW). K_binding zeroized dans finally. Scan probatoire post-opération. | C3, C5 | TC-INV-03 (scans sur échantillons) | Mineur — exigence probatoire, pas exhaustive |
| INV-276-09 | Faits Prolog requis présents | Extraction automatique par extract-facts.py. Fichier nommé argon2.service.ts → slug argon2. Méthode deriveKey() publique → service_method(argon2, deriveKey). Colonne metadata_tag TypeORM → entity_column(key_envelope, metadata_tag, bytea). | C1, C4, C7 | TC-NOM-08, TC-NOM-09, TC-INV-04 | Moyen — dépend du nommage exact du fichier et de la classe |
| INV-276-10 | Non-régression des 21 checks existants | Aucune modification de pv_envelope_compliance.pl. Migration additive (ADD COLUMN). Pas de modification de trigger existant. | C4, C6 | TC-NR-01, TC-NR-02 — comparatif baseline avant/après | Faible |
| INV-276-11 | Machine à états exhaustive, terminal explicite | Enum interne BindingState (LEGACY_NULL_TAG, BOUND_TAG_VALID, TAMPERED_TAG_INVALID). Transitions autorisées/interdites implémentées dans KeyEnvelopeService. État TAMPERED → aucune transition sortante. | C5 | TC-NOM-10, TC-NOM-13, TC-ERR-10, TC-ERR-11, TC-NEG-02 | Moyen — logique LEGACY→TAMPERED requiert condition de détection claire |
4. Mapping critères d'acceptation → mécanismes¶
| Critère ID | Mécanisme(s) | Composant | Observable | Risque |
|---|---|---|---|---|
| CA-276-01 | Extraction faits + audit Prolog | C7 | swipl retourne 24/24 OK | Moyen — dépend de C1, C4 |
| CA-276-02 | Argon2Service injectable, fait service(argon2, 'Argon2Service') | C1 | Fait présent dans _generated-facts.pl | Faible |
| CA-276-03 | Méthode publique validateParams(), fait service_method(argon2, validateParams) | C1 | Fait présent dans _generated-facts.pl | Faible |
| CA-276-04 | validateParams() avec bornes min | C1 | Rejet sur paramètres < min | Faible |
| CA-276-05 | validateParams() avec bornes max | C1 | Rejet sur paramètres > max | Faible |
| CA-276-06 | GET /crypto/argon2/config retourne ARGON2_CONFIG | C1, C2 | Payload JSON = constantes source | Faible |
| CA-276-07 | Colonne metadata_tag sur entity + fait Prolog | C4 | entity_column(key_envelope, metadata_tag, bytea) | Faible |
| CA-276-08 | MetadataBindingService.verifyTag() + guard dans unwrapEnvelope() | C3, C5 | HTTP 422 sur tag invalide, < 100ms | Moyen — SLA timing |
| CA-276-09 | Migrations phase 1 (nullable) + phase 2 (NOT NULL) | C6 | Schema information_schema | Faible |
| CA-276-10 | down() dans les deux migrations | C6 | Schéma restauré après down | Faible |
| CA-276-11 | Aucune modification de pv_envelope_compliance.pl | C7 | Diff vide sur fichier Prolog + comparatif baseline | Faible |
| CA-276-12 | Scans probatoires post-opération | C3, C5 | Aucun secret en clair détecté dans échantillons | Mineur — probatoire |
| CA-276-13 | Logique LEGACY dans unwrapEnvelope() | C5 | Lecture seule autorisée, warning loggé | Faible |
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-01/02/03, CA-04 | validateParams({65536,3,4,2,32}) → accept | Retour validation sans erreur, pas d'artefact de dérivation | Unit |
| TC-NOM-02 | INV-03, CA-05 | validateParams({1048576,10,16,2,32}) → accept | Bornes max acceptées | Unit |
| TC-NOM-03 | INV-04, CA-06 | GET /crypto/argon2/config | Payload JSON exact au contrat | Integration |
| TC-NOM-04 | INV-05/06, CA-08 | computeTag(kMaster, metadata) | Tag retourné, Buffer.byteLength === 32 | Unit |
| TC-NOM-05 | INV-07, CA-08 | unwrapEnvelope() avec tag valide | Vérification précède unwrap, accès autorisé | Unit |
| TC-NOM-06 | INV-05, CA-09 | Migration phase 1 up | Colonne metadata_tag BYTEA NULL existe | Integration |
| TC-NOM-07 | INV-05, CA-09 | Migration phase 2 up | metadata_tag NOT NULL, INSERT sans tag rejeté | Integration |
| TC-NOM-08 | INV-09, CA-02/03/07 | extract-facts.py sur code candidat | 3 faits requis présents | Integration |
| TC-NOM-09 | INV-09/10, CA-01/11 | swipl audit complet | 24/24 OK, 21 checks historiques stables | Integration |
| TC-NOM-10 | INV-11, CA-09 | computeTag sur enveloppe LEGACY puis persist | Transition LEGACY → BOUND observée | Unit |
| TC-NOM-11 | CA-10 | Migration down() séquentiel | Schéma restauré à l'état pré-migration | Integration |
| TC-NOM-12 | CA-13 | unwrapEnvelope() sur enveloppe tag IS NULL en phase 1 | Accès lecture seule, warning dans logs | Unit |
| TC-NOM-13 | INV-11 | unwrapEnvelope() sur LEGACY avec incohérence binding | Transition LEGACY → TAMPERED, refus HTTP 422 | Unit |
| TC-ERR-01 | E-01, INV-02 | validateParams({memory:65535,...}) | Rejet, erreur validation | Unit |
| TC-ERR-02 | E-01, INV-02 | validateParams({iterations:2,...}) | Rejet | Unit |
| TC-ERR-03 | E-03, INV-02 | validateParams({hashLength: undefined}) | Rejet paramètre manquant | Unit |
| TC-ERR-04 | E-04, INV-02 | validateParams({type:1,...}) | Rejet type != 2 | Unit |
| TC-ERR-05 | E-02, INV-03 | validateParams({memory:1048577,...}) | Rejet hors bornes max | Unit |
| TC-ERR-06 | E-05, INV-05 | INSERT enveloppe sans tag en phase 2 | Rejet NOT NULL constraint | Integration |
| TC-ERR-07 | E-06, INV-07 | unwrapEnvelope() avec tag altéré | HTTP 422, < 100ms, aucune donnée restituée | Unit + Perf |
| TC-ERR-08 | E-07, INV-09 | extract-facts.py avec fait omis (mock) | Audit < 24/24 | Integration |
| TC-ERR-09 | E-08, INV-10 | Audit comparatif avec check régressé | Régression détectée, livraison refusée | Integration |
| TC-ERR-10 | INV-11 | Tentative transition BOUND → LEGACY | Rejet, état inchangé | Unit |
| TC-ERR-11 | INV-11 | Tentative transition TAMPERED → * | Interdite, état terminal | Unit |
| TC-INV-01 | INV-01, CA-06 | Audit du flux complet de validation | Aucun artefact de dérivation serveur produit | Unit |
| TC-INV-02 | INV-06, CA-08 | Inspection du contexte HKDF | Contexte exact ProbatioVault::MetadataBinding::v1 | Unit |
| TC-INV-03 | INV-08, CA-12 | Scan artefacts crypto post-opération | Aucun secret clair dans échantillons contrôlés | Sec |
| TC-INV-04 | H-02, INV-09 | Cartographie check → fait | check_10↔service(argon2,_)+service_method(argon2,deriveKey), check_19↔entity_column(key_envelope,metadata_tag,_), check_22↔service(argon2,_)+service_method(argon2,deriveKey) | Integration |
| TC-NEG-01 | INV-07 | unwrapEnvelope() avec device_id substitué | HTTP 422, < 100ms | Unit + Perf |
| TC-NEG-02 | INV-11 | Transition BOUND → LEGACY programmatique | Rejet | Unit |
| TC-NEG-03 | INV-05 | computeTag avec résultat tronqué | Rejet persistance | Unit |
| TC-NEG-04 | INV-02/03 | Paramètres mixtes (1 ok + 1 hors borne) | Rejet atomique | Unit |
| TC-NEG-05 | INV-04 | Mock endpoint retournant config altérée | Détection écart contrat | Integration |
| TC-NEG-06 | INV-10 | Audit avec Prolog modifié | Campagne rejetée (checksum) | Integration |
| TC-NR-01 | INV-10 | Comparatif baseline/candidate 21 checks | Même statut OK/KO | Integration |
| TC-NR-02 | INV-10 | Trigger updated_at stable après migration | Même comportement avant/après | Integration |
| TC-NR-03 | CA-13 | LEGACY en phase 1 : lecture ok, écriture ko | Politique transitoire respectée | Integration |
| TC-NR-04 | CA-10 | Séquence up/down/up | Schéma identique | Integration |
6. Gestion des erreurs¶
| Code erreur | Cas | HTTP | Réponse | Composant |
|---|---|---|---|---|
ARGON2_PARAMS_BELOW_MINIMUM | E-01 : Paramètre < plancher OWASP | 400 | { error: 'ARGON2_PARAMS_BELOW_MINIMUM', field: '<field>', value: <val>, minimum: <min> } | C1 |
ARGON2_PARAMS_ABOVE_MAXIMUM | E-02 : Paramètre > plafond contractuel | 400 | { error: 'ARGON2_PARAMS_ABOVE_MAXIMUM', field: '<field>', value: <val>, maximum: <max> } | C1 |
ARGON2_PARAMS_MISSING | E-03 : Paramètre absent | 400 | { error: 'ARGON2_PARAMS_MISSING', field: '<field>' } | C1 |
ARGON2_INVALID_TYPE | E-04 : type != 2 | 400 | { error: 'ARGON2_INVALID_TYPE', expected: 2, received: <val> } | C1 |
METADATA_TAG_MISSING | E-05 : Tag absent en phase finale | 500 | Erreur interne (contrainte DB NOT NULL) | C6 |
METADATA_TAG_INVALID | E-06 : Tag invalide (tampering) | 422 | { error: 'METADATA_TAG_INVALID', envelope_id: '<id>' } — aucun champ sensible exposé | C5 |
PROLOG_AUDIT_FAILED | E-07 : Faits manquants | — | CI bloqué (pas HTTP, erreur CI/CD) | C7 |
PROLOG_REGRESSION | E-08 : Régression check | — | CI bloqué | C7 |
SLA : La décision de rejet sur METADATA_TAG_INVALID doit intervenir en < 100ms (t_decision - t_req). Le périmètre de mesure : de la réception de la requête dans unwrapEnvelope() jusqu'au throw de l'exception HTTP 422.
7. Impacts sécurité¶
7.1 Risques et mitigations¶
| Risque | Impact | Mitigation |
|---|---|---|
Exposition de K_binding en mémoire | Compromission du binding | Zeroization dans finally après usage (pattern AesKwService) |
| Timing attack sur comparaison de tag | Leak d'information sur le tag | crypto.timingSafeEqual() exclusivement (via HashService.constantTimeEqual) |
| Tag vide accepté par erreur | Bypass binding | CHECK constraint octet_length(metadata_tag) = 32 + validation applicative |
| Dérivation accidentelle de clé serveur | Violation Zero-Knowledge | Aucune dépendance argon2 dans package.json ; deriveKey() est un alias strict de validateParams() ; review statique |
| Race condition sur transition d'état | Double unwrap concurrent | Transactions SERIALIZABLE sur vérification + transition TAMPERED |
7.2 Journalisation sécurité¶
| Événement | Niveau | Contenu |
|---|---|---|
| Validation Argon2 acceptée | INFO | { action: 'argon2_validate', result: 'accepted', userId } |
| Validation Argon2 rejetée | WARN | { action: 'argon2_validate', result: 'rejected', field, reason } |
| Metadata tag vérifié OK | DEBUG | { action: 'metadata_verify', result: 'valid', envelopeId } |
| Metadata tag invalide (tampering) | ERROR | { action: 'metadata_verify', result: 'TAMPERED', envelopeId, state: 'TAMPERED_TAG_INVALID' } |
| Accès LEGACY avec warning | WARN | { action: 'legacy_access', envelopeId, phase: 1, recommendation: 'rotate_to_bound' } |
| Migration phase ½ appliquée | INFO | { action: 'migration', phase, direction: 'up'/'down' } |
7.3 Conformité¶
- RFC 9106 : Validation des paramètres Argon2id (sans dérivation serveur)
- OWASP 2024 : Plancher mémoire 65536 KiB, iterations >= 3, parallelism >= 4
- Zero-Knowledge : Aucun secret utilisateur n'atteint le serveur en clair
- Anti-substitution : HMAC binding empêche la substitution d'enveloppe inter-device
8. Hypothèses techniques¶
| ID | Hypothèse | Impact si faux |
|---|---|---|
| H-276-01 | Projet cible = ProbatioVault-backend (NestJS/TypeORM/PostgreSQL 16+) | Mauvais contrat technique |
| H-276-02 | Les checks Prolog 10 et 22 consomment service_method(argon2, deriveKey) (vérifié dans pv_envelope_compliance.pl l. 168-169, l. 346-348). Le service doit exposer deriveKey() comme alias public. | Si le Prolog vérifie un autre nom → check KO |
| H-276-03 | extract-facts.py extrait les méthodes publiques par regex sur les fichiers *.service.ts. Le fichier DOIT être nommé argon2.service.ts et la classe Argon2Service. | Slug incorrect → fait absent → audit échoue |
| H-276-04 | HkdfService.deriveKey() accepte un salt vide (Buffer.alloc(0)) — vérifié dans le code source (l. 18 : crypto.hkdfSync('sha256', masterKey, salt, info, length), Node.js accepte un buffer vide). | Si salt vide rejeté → K_binding non dérivable |
| H-276-05 | Les enregistrements LEGACY existants peuvent être backfillés en batch hors downtime. Le backfill requiert K_master_user pour chaque enveloppe → nécessite un flux applicatif (pas un simple UPDATE SQL). | Backfill impossible en simple DDL, nécessite un script applicatif dédié |
| H-276-06 | Le code erreur contractuel pour tag invalide est HTTP 422 (spec E-06). | Alignement API confirmé |
| H-276-07 | Le trigger existant trg_key_envelopes_updated_at (BEFORE UPDATE → updated_at = NOW()) n'est pas affecté par l'ajout de metadata_tag. | Si trigger a une clause restrictive → risque de non-déclenchement |
| H-276-08 | Q-276-01 (bornes max Argon2) : les valeurs memory<=1048576, iterations<=10, parallelism<=16 sont considérées stables pour cette story. Si elles changent après implémentation, seule la constante ARGON2_CONFIG est à modifier. | Nécessite un ticket de suivi si décision produit change |
9. Points de vigilance (risques, dette, pièges)¶
9.1 Contradiction spec vs Prolog (CRITIQUE)¶
La spec INV-276-09 déclare que le fait requis est service_method(argon2, validateParams). Les checks Prolog 10 et 22 vérifient service_method(argon2, deriveKey). La spec interdit de modifier le Prolog (INV-276-10).
Résolution : Le service expose deriveKey() comme alias public de validateParams(). Les deux faits sont émis. Le check Prolog passe via deriveKey. Le test TC-INV-04 doit vérifier que check_10 et check_22 consomment service_method(argon2, deriveKey) (et non validateParams).
Action : Signaler l'écart à l'auteur de la spec pour correction en prochaine itération (aligner INV-276-09 sur la réalité Prolog).
9.2 Backfill LEGACY (COMPLEXITÉ)¶
Le backfill des enveloppes existantes requiert K_master_user pour dériver K_binding. Or K_master_user n'est jamais stockée en clair — elle est wrappée dans l'enveloppe elle-même. Le backfill nécessite donc un flux applicatif déclenché lors du prochain accès utilisateur (lazy migration), et non un script SQL batch.
Impact : La phase 2 (NOT NULL) ne peut être activée qu'après que tous les utilisateurs actifs aient accédé au moins une fois à leur enveloppe post-migration. La migration phase 2 doit être déployée séparément, avec un monitoring du nombre d'enveloppes LEGACY restantes.
9.3 Transition LEGACY → TAMPERED (AMBIGUÏTÉ)¶
Le scénario TC-NOM-13 teste la transition LEGACY_NULL_TAG → TAMPERED_TAG_INVALID sur "incohérence détectée". Pour une enveloppe LEGACY (tag NULL), l'incohérence ne peut être détectée que si un contrôle d'intégrité est tenté (backfill ou rotation). Le cas se produit quand : - Un calcul de tag est tenté sur une enveloppe LEGACY - Le tag calculé est incompatible avec les métadonnées en base (ex: device_id corrompu)
Observable : Lors d'une tentative de rotation/backfill, si le tag calculé échoue à la validation (métadonnées incohérentes), l'enveloppe est marquée TAMPERED.
9.4 SLA < 100ms (RISQUE OPÉRATIONNEL)¶
Le SLA de décision < 100ms sur rejet de binding est mesuré dans le périmètre unwrapEnvelope() (de l'entrée de la méthode au throw). Les facteurs de risque : - Latence DB pour la lecture de l'enveloppe (réseau) - Calcul HKDF + HMAC (CPU, < 1ms attendu)
Mitigation : Mesure performance.now() avant/après dans les tests E2E. En production, métriques OpenTelemetry sur le span de vérification.
9.5 Contexte HKDF hardcodé¶
Le contexte 'ProbatioVault::MetadataBinding::v1' est un littéral contractuel. Il DOIT être centralisé dans argon2.constants.ts (ou un fichier de constantes crypto dédié) et JAMAIS dupliqué en inline.
10. Hors périmètre¶
- Dérivation Argon2id côté serveur : Le serveur valide les paramètres mais ne dérive jamais de clé (INV-276-01).
- Modification des règles Prolog :
pv_envelope_compliance.plreste inchangé. - Refonte des 21 checks existants : Aucun check vert ne peut devenir rouge.
- Librairie native Argon2 backend : Aucune dépendance
argon2,@phc/argon2, ou similaire. - Paramétrage adaptatif client par device : Hors scope de cette story.
- Preuve exhaustive d'absence de secrets : Seuls des contrôles probatoires sont attendus (CA-276-12).
- Backfill automatique des enveloppes LEGACY : Le mécanisme de lazy migration est documenté mais l'exécution en production est hors scope du code livré. Le code fournit les primitives (
computeTag, migration phase ½) ; le déclenchement du backfill est opérationnel. - Mécanismes cross-module : Aucune modification d'autres modules identifiée. Le guard de binding est interne au module
crypto.
11. Écarts spec identifiés (review inputs)¶
| # | Type | Description | Traitement dans le plan |
|---|---|---|---|
| 1 | Contradiction | INV-276-09 déclare validateParams comme fait requis mais checks Prolog 10/22 vérifient deriveKey | Résolu : alias deriveKey() → validateParams() dans Argon2Service |
| 2 | Contradiction | Vérification préalable metadata_tag (INV-07) vs accès lecture LEGACY autorisé (CA-13) | Résolu : branche conditionnelle tag IS NULL → read-only (phase 1) |
| 3 | Ambiguïté | SLA < 100ms — périmètre de mesure non borné | Borné dans le plan : entrée unwrapEnvelope() → throw exception |
| 4 | Incohérence Spec↔Tests | TC-NOM-04 rattaché à CA-276-08 alors qu'il teste la création (pas le contrôle d'accès) | Documenté, non bloquant — traçabilité fragilisée |
| 5 | Hypothèse dangereuse | H-276-07 — journaux d'audit prérequis externe | Plan assume que le Logger NestJS existant suffit pour les observables PD-276 ; les journaux signés sont hors scope |
| 6 | Ambiguïté | Q-276-01 — bornes max Argon2 ouvertes | Plan utilise les bornes contractuelles INV-276-03 ; constante centralisée facilite le changement futur |