PD-103 — Scénarios de tests contractuels¶
1. Références¶
- Spécification : PD-103-specification.md (v3)
- Epic : EPIC-XX (
MOBILE-IOS/ PD-195 à confirmer PO)
[v3]
2. Matrice de couverture¶
| ID Invariant | ID Critère | ID Test | Couverture | Commentaire |
|---|---|---|---|---|
| INV-103-01-fidelity | CA-103-03 | TC-INV-01, TC-NOM-01 | Oui | Égalité bit-à-bit original/submitted/probatory. |
| INV-103-02-no-transform | CA-103-04 | TC-INV-01, TC-NOM-01 | Oui | Aucune recompression/redimensionnement/altération. |
| INV-103-03-hash-local-first | CA-103-02 | TC-NOM-01, TC-INV-02 | Oui | Hash local horodaté avant premier appel upload. |
| INV-103-04-ocr-local-only | CA-103-05, CA-103-06 | TC-NOM-02, TC-NOM-03 | Oui | OCR local uniquement, désactivable et non bloquant. |
| INV-103-05-ocr-non-probative | CA-103-06 | TC-NOM-03 | Oui | Échec OCR ne bloque pas la preuve image. |
| INV-103-06-encryption-file-level | CA-103-07, CA-103-13 | TC-NOM-01, TC-ERR-07, TC-ERR-14, TC-INV-03 | Oui | Ciphertext-only + contrôle d’intégrité S3 obligatoire. |
| INV-103-07-purge-startup | CA-103-12 | TC-NOM-04 | Oui | purgeStale() exécuté avant capture/chiffrement. |
| INV-103-08-sealed-guard | CA-103-09 | TC-NOM-08, TC-ERR-08 | Oui | Cas positif et négatif sous garde. |
| INV-103-09-envelope-encryption | CA-103-13, CA-103-15 | TC-INV-03, TC-NOM-01 | Oui | DEK wrappé RSA-OAEP, jamais en clair en base. |
| INV-103-10-atomicity-scope | N/A | TC-INV-04 | Oui | Pré/post-commit cohérent, rattrapage async idempotent. |
| INV-103-11-distributed-protection | CA-103-18, CA-103-24, CA-103-26 | TC-INV-05, TC-INV-06, TC-INV-07, TC-INV-08, TC-INV-11, TC-INV-14, TC-ERR-10, TC-ERR-13 | Oui | Lock/idempotence/réconciliation/rate-limit/clearing/orphelins. |
| INV-103-12-notify-sealed | CA-103-10 | TC-NOM-08 | Oui | Push obligatoire à l’entrée en SEALED. |
| INV-103-20-transition-captured-uploading | CA-103-02 | TC-INV-02 | Oui | CAPTURED->UPLOADING refusé sans hash+chiffrement. |
| INV-103-21-transition-captured-cancelled | N/A | TC-ERR-02 | Oui | Annulation explicite autorisée. |
| INV-103-22-transition-uploading-uploaded | CA-103-07 | TC-NOM-01, TC-NOM-13 | Oui | UPLOADED seulement sur S3 OK + ACK backend. |
| INV-103-23-transition-uploading-deferred | CA-103-08 | TC-NOM-05, TC-ERR-01 | Oui | Bascule différée en indisponibilité/retries épuisés. |
| INV-103-24-transition-deferred-uploading | CA-103-16 | TC-NOM-06, TC-ERR-15 | Oui | Reprise avec JWT valide + nouvelle URL pré-signée. |
| INV-103-25-transition-uploaded-pending-seal | N/A | TC-NOM-07 | Oui | Passage après persistance événement backend/journal. |
| INV-103-26-transition-pending-seal-sealed | CA-103-09 | TC-NOM-08, TC-ERR-08 | Oui | Transition conditionnée par garde INV-103-08. |
| INV-103-27-transition-sealed-anchor | CA-103-11 | TC-NOM-09 | Oui | ANCHOR_CONFIRMED après confirmation blockchain. |
| INV-103-28-terminal-states | CA-103-14 | TC-NOM-12, TC-INV-10 | Oui | États terminaux stricts. |
| INV-103-29-transition-uploading-cancelled | CA-103-20 | TC-NOM-14, TC-INV-09 | Oui | Annulation explicite + purge + abort upload S3. |
| INV-103-30-key-exchange | CA-103-15 | TC-NOM-01, TC-INV-03 | Oui | Génération DEK, wrapping, transmission, unwrapping backend. |
| INV-103-31-capture-id-normalization | CA-103-22 | TC-NOM-15, TC-INV-06 | Oui | capture_id lowercased côté client+backend. |
| INV-103-32-dek-zeroization | CA-103-23 | TC-INV-12 | Oui | Vérification buffer DEK rempli 0x00 après wrapping. |
| INV-103-33-s3-orphan-gc | CA-103-24 | TC-INV-11 | Oui | Orphelins S3 détectés et supprimés > orphanTtl. |
| INV-103-34-kek-keyring-rotation | CA-103-25 | TC-NOM-16, TC-INV-13, TC-ERR-16 | Oui | Keyring courante+anciennes et kek_id obligatoire. |
| INV-103-35-seal-delayed-trigger | CA-103-26 | TC-INV-14, TC-ERR-09 | Oui | Trigger automatique SEAL_DELAYED sur dépassement SLA. |
| INV-103-36-seal-delayed-clearing | CA-103-26 | TC-INV-08 | Oui | Clearing après 3 cycles conformes consécutifs. |
| INV-103-37-idempotency-canonical-fingerprint | CA-103-18, CA-103-21 | TC-NOM-15, TC-INV-06, TC-ERR-13 | Oui | Fingerprint canonique déterministe pour 200/409. |
| INV-103-38-transition-deferred-cancelled | CA-103-27 | TC-ERR-11, TC-NOM-17 | Oui | TTL différé + purge locale; pas de rétention post-UPLOADED. |
| INV-103-39-nonce-csprng | CA-103-07 | TC-INV-03 | Oui | Nonce 96 bits via CSPRNG, unique par capture. |
| N/A | CA-103-01 | TC-NOM-10 | Oui | P95 trigger->CAPTURED <= 3s. |
| N/A | CA-103-08 | TC-NOM-11 | Oui | P95 trigger->UPLOADED <= 5s. |
| N/A | CA-103-17 | TC-ERR-12 | Oui | Rejet skew timestamp > ±5 min. |
| N/A | CA-103-19 | TC-NOM-13 | Oui | Multipart + reprise partielle. |
| N/A | CA-103-27 | TC-NOM-17 | Oui | Suppression locale immédiate après ACK UPLOADED. |
| N/A | CA-103-28 | TC-ERR-16, TC-ERR-17 | Oui | Distinction 422 vs 503 sur unwrapping. |
[v3]
3. Scénarios de test – Flux nominaux¶
TEST-ID: TC-NOM-01
Référence spec: INV-103-01, INV-103-02, INV-103-03, INV-103-06, INV-103-22, INV-103-30, INV-103-31, CA-103-02, CA-103-03, CA-103-04, CA-103-07, CA-103-15
GIVEN
- Device de référence iPhone 12 (A14), iOS 16+, horloge synchronisée UTC
- Utilisateur authentifié (JWT valide), capture validée explicitement
- OCR activé
- Réseau disponible avec S3 et backend joignables
- Clé publique backend (KEK) active + `kek_id` disponible côté mobile
WHEN
- L’utilisateur déclenche puis valide une capture probatoire
THEN
- Le hash `SHA3-256(image_original_bytes)` est calculé localement avant tout appel upload
- Un DEK aléatoire 256 bits est généré localement puis utilisé pour AES-256-GCM
- Le nonce 96 bits est généré via CSPRNG
- Le DEK est wrappé en `dek_wrapped_b64` via RSA-OAEP-SHA256 avec `kek_id`
- Le PUT S3 transporte uniquement le `ciphertext` avec `Content-MD5` ou `x-amz-content-sha256`
- Le POST `/documents/capture` contient `aes_gcm_nonce_b64`, `aes_gcm_tag_b64`, `dek_wrapped_b64`, `kek_id`
- La transition d’état est strictement `CAPTURED -> UPLOADING -> UPLOADED`
AND
- `hash(original) == hash(submitted) == hash(probatory)`
- Les métadonnées image restent invariantes (aucune transformation destructrice)
TEST-ID: TC-NOM-02
Référence spec: INV-103-04, INV-103-05, CA-103-05
GIVEN
- `ocr_enabled=false` défini avant capture
- Réseau disponible
WHEN
- L’utilisateur valide la capture
THEN
- Le flux atteint `UPLOADED` sans blocage
- Les champs OCR optionnels (`ocr_text`, `ocr_confidence`, `ocr_language`) sont absents
AND
- L’image probatoire et son hash restent inchangés
TEST-ID: TC-NOM-03
Référence spec: INV-103-04, INV-103-05, CA-103-06
GIVEN
- `ocr_enabled=true`
- Injection d’erreur OCR locale (Vision indisponible)
WHEN
- L’utilisateur valide la capture
THEN
- Le flux continue sans OCR jusqu’à `UPLOADED` (ou `UPLOAD_DEFERRED` selon réseau)
- Aucun appel réseau vers un service IA externe n’est émis
AND
- L’image et son hash ne sont pas modifiés par l’échec OCR
TEST-ID: TC-NOM-04
Référence spec: INV-103-07, CA-103-12
GIVEN
- Artefacts temporaires sensibles préexistants (session interrompue précédente)
- Traces d’exécution activées
WHEN
- Un nouveau flux capture démarre
THEN
- `purgeStale()` est exécuté avant toute étape capture/hash/chiffrement
- Les artefacts obsolètes sont supprimés
AND
- Une trace horodatée de purge est présente et corrélée au `capture_id`
TEST-ID: TC-NOM-05
Référence spec: INV-103-23, Flux 5.5, CA-103-13
GIVEN
- État `UPLOADING`
- Réseau indisponible et retries upload configurés à 3
WHEN
- Le flux tente l’upload chiffré
THEN
- Après épuisement des retries, transition `UPLOADING -> UPLOAD_DEFERRED`
- Aucun passage à `UPLOADED`
AND
- Le payload différé est stocké localement chiffré
TEST-ID: TC-NOM-06
Référence spec: INV-103-24, Flux 5.5, CA-103-16
GIVEN
- Capture en `UPLOAD_DEFERRED`
- Réseau restauré avant expiration TTL
- Session JWT valide
- URL pré-signée initiale expirée
WHEN
- Le mécanisme de reprise automatique s’exécute
THEN
- Une nouvelle URL pré-signée est redemandée avant tout PUT
- Transition `UPLOAD_DEFERRED -> UPLOADING -> UPLOADED`
AND
- Le `capture_id` reste inchangé et sans duplication d’attestation
TEST-ID: TC-NOM-07
Référence spec: INV-103-25, Flux 5.6
GIVEN
- Capture en `UPLOADED`
- Backend disponible pour persister l’événement probatoire
WHEN
- L’événement backend est persisté dans `capture_events` et journal probatoire append-only
THEN
- Transition `UPLOADED -> PENDING_SEAL`
- Le fichier acquiert le statut probatoire à cet instant
AND
- L’entrée de persistance est unique et traçable par `capture_id`
TEST-ID: TC-NOM-08
Référence spec: INV-103-08, INV-103-12, INV-103-26, CA-103-09, CA-103-10
GIVEN
- Capture en `PENDING_SEAL`
- `signature_status='SIGNED'`
- `hsm_signature_ref` non nul
WHEN
- Le worker de scellement évalue la garde puis valide la transition
THEN
- Transition `PENDING_SEAL -> SEALED`
- Notification push iOS envoyée à l’utilisateur
AND
- Le message push est exactement: "Votre capture a été scellée avec succès. Preuve vérifiable disponible."
TEST-ID: TC-NOM-09
Référence spec: INV-103-27, CA-103-11
GIVEN
- Capture en `SEALED`
- Confirmation d’ancrage blockchain reçue
WHEN
- L’événement de confirmation est traité
THEN
- Transition `SEALED -> ANCHOR_CONFIRMED`
AND
- `merkle_proof_ref`, `tsa_token_ref`, `tx_hash` sont présents et non vides
TEST-ID: TC-NOM-10
Référence spec: CA-103-01, §5.2
GIVEN
- Campagne instrumentée de 200 captures sur iPhone 12 (A14), iOS 16+
- Conditions contrôlées identiques pour tous les runs
WHEN
- Mesure de `trigger -> CAPTURED` sur l’échantillon
THEN
- P95 <= 3000 ms
AND
- Toute valeur P95 > 3000 ms est classée non-conforme contractuelle
TEST-ID: TC-NOM-11
Référence spec: CA-103-08, INV-103-23, §5.2, §5.3
GIVEN
- Campagne instrumentée de 200 captures sur device de référence
- SLA `captureUploadSla=5s` (non configurable)
WHEN
- Mesure de `trigger -> UPLOADED` sur l’échantillon
THEN
- P95 <= 5000 ms
AND
- Les cas hors SLA ne doivent pas forcer `UPLOADED` et basculent vers `UPLOAD_DEFERRED`
TEST-ID: TC-NOM-12
Référence spec: INV-103-28, CA-103-14, §5.7
GIVEN
- Une capture en `CANCELLED`
- Une capture en `ANCHOR_CONFIRMED`
WHEN
- Une transition sortante est demandée depuis ces états
THEN
- La transition est rejetée dans 100% des cas
- L’état reste inchangé
AND
- La journalisation indique "état terminal, résolution manuelle uniquement"
TEST-ID: TC-NOM-13
Référence spec: Flux 5.5bis, INV-103-22, CA-103-19
GIVEN
- `size_bytes > 10_000_000` (multipart obligatoire)
- Upload multipart initialisé avec `upload_id`
- Échec injecté sur une part après au moins une part validée
WHEN
- Le flux entre en reprise différée puis reprend avec URLs pré-signées fraîches
THEN
- Les parts déjà validées sont conservées (ETags)
- Seules les parts manquantes sont rejouées (ou session multipart recréée si `upload_id` expiré)
- `CompleteMultipartUpload` est exécuté avec succès
AND
- Transition finale `UPLOADING -> UPLOADED`
TEST-ID: TC-NOM-14
Référence spec: INV-103-29, CA-103-20, §5.7
GIVEN
- Capture en `UPLOADING` (single ou multipart)
- Données ciphertext en transit
WHEN
- L’utilisateur annule explicitement
THEN
- Transition `UPLOADING -> CANCELLED`
- Upload S3 est annulé (`AbortMultipartUpload` ou annulation PUT)
- Artefacts ciphertext locaux/in-transit sont purgés
AND
- Aucune reprise automatique ultérieure n’est autorisée
TEST-ID: TC-NOM-15
Référence spec: §5.12, INV-103-31, INV-103-37, CA-103-18, CA-103-21, CA-103-22
GIVEN
- Une capture `capture_id=x-lower` déjà acceptée
- Rejeu avec `capture_id=x-upper` (même UUID en uppercase)
- Les champs canoniques non optionnels sont identiques
- `ocr_text` diffère volontairement entre les deux requêtes
WHEN
- `POST /documents/capture` est rejoué dans la fenêtre de déduplication
THEN
- Le backend normalise `capture_id` en lowercase
- Le fingerprint `payload_canonical_sha256` recalculé est identique
- Réponse `200 OK` idempotente
AND
- Aucun doublon d’attestation probatoire
TEST-ID: TC-NOM-16
Référence spec: INV-103-34, §5.11, CA-103-25
GIVEN
- Une capture différée chiffrée avec `kek_id=K1` (ancienne KEK)
- Le backend a roté vers `kek_id=K2` (KEK courante), K1 conservée au keyring
WHEN
- La reprise upload envoie le payload avec `dek_wrapped_b64` et `kek_id=K1`
THEN
- L’unwrap backend réussit via keyring
- L’événement probatoire est créé normalement
AND
- Le flux poursuit `UPLOADING -> UPLOADED -> PENDING_SEAL`
TEST-ID: TC-NOM-17
Référence spec: §5.3, §5.5, §5.8, INV-103-38, CA-103-27
GIVEN
- Capture en `UPLOADING` avec artefact local chiffré présent
WHEN
- Upload S3 et ACK backend réussissent
THEN
- Transition `UPLOADING -> UPLOADED`
- L’artefact local chiffré est supprimé immédiatement
AND
- Aucun artefact local de cette capture n’est conservé jusqu’au TTL différé
[v3]
4. Scénarios de test – Cas d’erreur¶
TEST-ID: TC-ERR-01
Référence spec: ER-103-01, INV-103-23
GIVEN
- Upload en cours
- Erreurs réseau retryables injectées sur chaque tentative
WHEN
- Le mécanisme d’upload exécute les retries automatiques
THEN
- Les retries sont effectués puis épuisés
- Transition finale `UPLOADING -> UPLOAD_DEFERRED`
- Aucun passage à `UPLOADED`
TEST-ID: TC-ERR-02
Référence spec: ER-103-02, INV-103-21
GIVEN
- Capture en `CAPTURED` avec écran de prévisualisation actif
WHEN
- L’utilisateur annule explicitement
THEN
- Transition `CAPTURED -> CANCELLED`
- Aucune persistance probatoire backend
- Aucune transition ultérieure automatique
TEST-ID: TC-ERR-03
Référence spec: ER-103-03, INV-103-04, INV-103-05
GIVEN
- OCR activé
- Erreur OCR locale forcée
WHEN
- Le flux de capture continue
THEN
- L’upload probatoire n’est pas bloqué par l’échec OCR
- Les champs OCR restent absents/incomplets selon l’échec
- L’intégrité image/hash reste inchangée
TEST-ID: TC-ERR-04
Référence spec: ER-103-04, INV-103-23
GIVEN
- Réseau indisponible dès l’entrée en `UPLOADING`
WHEN
- Le flux tente l’upload
THEN
- Transition vers `UPLOAD_DEFERRED`
- Donnée différée conservée localement chiffrée
- Aucune fuite de contenu en clair
TEST-ID: TC-ERR-05
Référence spec: ER-103-05, INV-103-03
GIVEN
- Mismatch injecté entre bytes image et hash déclaré
WHEN
- Le contrôle d’intégrité pré-upload est exécuté
THEN
- Le flux est rejeté
- Les artefacts temporaires sont purgés
- Aucun upload réseau n’est déclenché
TEST-ID: TC-ERR-06
Référence spec: ER-103-06, §5.1
GIVEN
- Jeu de cas invalides (au moins):
- `capture_id` non UUIDv4
- `hash_sha3_256` hors regex
- `timestamp_device` hors regex RFC3339 UTC
- `mime_type != image/png`
- `size_bytes` hors bornes
- `aes_gcm_nonce_b64`, `aes_gcm_tag_b64`, `dek_wrapped_b64` ou `kek_id` invalides
WHEN
- `POST /documents/capture` est appelé avec un seul champ invalide par cas
THEN
- HTTP 400 pour chaque cas
- Aucune transition vers `PENDING_SEAL`
- Aucun artefact probatoire créé
TEST-ID: TC-ERR-07
Référence spec: ER-103-07, INV-103-06, CA-103-13
GIVEN
- Échec de chiffrement local (erreur crypto injectée)
WHEN
- Le flux lance le chiffrement file-level
THEN
- Rejet immédiat
- Transition vers `CANCELLED`
- Purge des artefacts sensibles temporaires
TEST-ID: TC-ERR-08
Référence spec: ER-103-08, INV-103-08, INV-103-26, CA-103-09
GIVEN
- Capture en `PENDING_SEAL`
- `signature_status='PENDING_SIGNATURE'` ou `hsm_signature_ref=NULL`
WHEN
- Une transition vers `SEALED` est demandée
THEN
- Transition refusée
- État maintenu à `PENDING_SEAL`
- Aucune notification `SEALED`
TEST-ID: TC-ERR-09
Référence spec: ER-103-09, INV-103-35, §5.3
GIVEN
- Capture en `PENDING_SEAL` depuis plus de 10 minutes
WHEN
- Le contrôle SLA de scellement s’exécute via réconciliation
THEN
- L’état reste `PENDING_SEAL`
- Le flag `SEAL_DELAYED` est positionné
- Une notification "scellement en cours" est émise
TEST-ID: TC-ERR-10
Référence spec: ER-103-10, INV-103-11
GIVEN
- Limite atteinte pour `user_id + IP` sur fenêtre 60s
WHEN
- Nouvel appel `POST /documents/capture`
THEN
- HTTP 429
- Aucun nouvel enregistrement probatoire
- Aucun changement d’état sur captures existantes
TEST-ID: TC-ERR-11
Référence spec: ER-103-11, INV-103-38, §5.3
GIVEN
- Capture en `UPLOAD_DEFERRED`
- TTL différé expiré
WHEN
- Le watchdog TTL s’exécute
THEN
- Transition forcée `UPLOAD_DEFERRED -> CANCELLED`
- Purge locale chiffrée obligatoire exécutée
- Aucune reprise d’upload ultérieure
TEST-ID: TC-ERR-12
Référence spec: ER-103-12, CA-103-17, §5.2
GIVEN
- Payload valide sauf `timestamp_device` avec skew > ±300s vs horloge UTC backend
WHEN
- `POST /documents/capture` est soumis
THEN
- HTTP 400 avec code métier `TIMESTAMP_SKEW_EXCEEDED`
- Aucun événement probatoire créé
- Aucun changement d’état de capture
TEST-ID: TC-ERR-13
Référence spec: ER-103-13, INV-103-37, §5.12, CA-103-18
GIVEN
- Une capture `capture_id=X` déjà enregistrée
- Nouveau POST avec `capture_id=X` mais fingerprint canonique divergent
WHEN
- `POST /documents/capture` est exécuté
THEN
- HTTP 409 Conflict
- Aucun doublon d’attestation
- Aucune altération de l’enregistrement initial
TEST-ID: TC-ERR-14
Référence spec: ER-103-14, INV-103-06, §5.5
GIVEN
- Upload S3 ciphertext en cours
- Header d’intégrité absent ou checksum incorrect injecté
WHEN
- Le PUT S3 est traité
THEN
- Le PUT est rejeté
- Retries automatiques sont appliqués dans les bornes
- En échec persistant, transition `UPLOADING -> UPLOAD_DEFERRED`
TEST-ID: TC-ERR-15
Référence spec: ER-103-15, INV-103-24, CA-103-16
GIVEN
- Capture en `UPLOAD_DEFERRED`
- Session JWT expirée ou invalide
- URL pré-signée initiale expirée
WHEN
- Le mécanisme de reprise automatique se déclenche
THEN
- Aucun upload n’est tenté
- Le système exige refresh/réauth + nouvelle URL pré-signée
- L’état reste `UPLOAD_DEFERRED` jusqu’à restauration des prérequis ou expiration TTL
TEST-ID: TC-ERR-16
Référence spec: ER-103-16, INV-103-34, CA-103-28
GIVEN
- `dek_wrapped_b64` corrompu ou incompatible avec le keyring KEK
- `kek_id` fourni mais non exploitable cryptographiquement
WHEN
- `POST /documents/capture` est soumis
THEN
- HTTP 422 avec code métier `UNWRAP_DEK_FAILED`
- Aucune création de `capture_events`
- Aucune transition vers `PENDING_SEAL`
TEST-ID: TC-ERR-17
Référence spec: ER-103-17, CA-103-28
GIVEN
- HSM/KMS indisponible (clé privée backend inaccessible)
- Payload autrement valide
WHEN
- `POST /documents/capture` est soumis
THEN
- HTTP 503 avec code métier `KEY_SERVICE_UNAVAILABLE`
- Aucune création de `capture_events`
- Le client peut retenter ultérieurement
[v3]
5. Tests d’invariants (non négociables) — format GIVEN/WHEN/THEN¶
TEST-ID: TC-INV-01
Référence spec: INV-103-01, INV-103-02
GIVEN
- Corpus d’images PNG de référence
WHEN
- Le pipeline complet capture->upload->probatoire est exécuté
THEN
- `hash(original)=hash(submitted)=hash(probatory)`
- Aucun écart de métadonnées image indiquant une transformation destructive
TEST-ID: TC-INV-02
Référence spec: INV-103-03, INV-103-20
GIVEN
- Une capture en `CAPTURED`
- Deux variantes d’injection: hash absent, chiffrement absent
WHEN
- Une transition `CAPTURED -> UPLOADING` est demandée
THEN
- La transition est refusée dans chaque variante
- La transition est autorisée uniquement quand hash local et chiffrement local sont validés
TEST-ID: TC-INV-03
Référence spec: INV-103-06, INV-103-09, INV-103-30, INV-103-39
GIVEN
- Clé publique backend active côté mobile
- Capture prête à être chiffrée
WHEN
- Le pipeline crypto est exécuté jusqu’au POST backend
THEN
- Un DEK 256 bits aléatoire par capture est généré
- Le nonce 96 bits est généré via CSPRNG et unique par capture
- Le DEK est wrappé en RSA-OAEP-SHA256 puis transmis via `dek_wrapped_b64` + `kek_id`
- Le backend n’enregistre jamais de DEK en clair en base
- Le stockage au repos contient uniquement des artefacts chiffrés/wrappés
TEST-ID: TC-INV-04
Référence spec: INV-103-10
GIVEN
- Injection de crash avant commit DB puis après commit DB
WHEN
- Le flux de persistance/scellement est relancé
THEN
- Pré-commit: rollback sans artefact persistant incohérent
- Post-commit: état DB cohérent et rattrapage async idempotent
TEST-ID: TC-INV-05
Référence spec: INV-103-11 (lock)
GIVEN
- Deux workers concurrents sur le même `capture_id`
WHEN
- Les workers tentent une transition d’état simultanée
THEN
- Un seul lock est acquis
- Le worker sans lock n’effectue aucune transition et planifie un retry
TEST-ID: TC-INV-06
Référence spec: INV-103-31, INV-103-37, §5.12
GIVEN
- Un `capture_id` déjà traité
WHEN
- Cas A: replay du payload canonique identique
- Cas B: replay divergent sur un champ canonique requis
- Cas C: replay avec seule variation de `ocr_text` (champ optionnel exclu)
- Cas D: replay avec même UUID mais casse différente (`UPPER/lower`)
THEN
- Cas A: réponse idempotente (`200`) sans doublon
- Cas B: `409 Conflict` sans corruption d’état
- Cas C: `200` idempotent (OCR optionnel exclu du canonique)
- Cas D: même décision qu’en lowercase (normalisation appliquée)
- `payload_canonical_sha256` stocké est stable et déterministe
TEST-ID: TC-INV-07
Référence spec: INV-103-11 (réconciliation)
GIVEN
- Captures non terminales bloquées injectées
WHEN
- Le job de réconciliation périodique s’exécute
THEN
- Les éléments bloqués sont détectés puis re-enqueue
- La convergence vers l’état attendu est observée sans doublon
TEST-ID: TC-INV-08
Référence spec: INV-103-36, §5.9 (cycle conforme)
GIVEN
- Flag `SEAL_DELAYED` actif
- Compteur persistant des cycles conformes initialisé
WHEN
- Des cycles de réconciliation sont exécutés
THEN
- Un cycle est compté conforme uniquement si toutes les captures `PENDING_SEAL` du cycle sont scellées avec succès
- Le flag n’est pas levé après 1 ou 2 cycles conformes
- Le flag est levé uniquement après 3 cycles conformes consécutifs
TEST-ID: TC-INV-09
Référence spec: INV-103-29
GIVEN
- Capture en `UPLOADING` avec ciphertext en transit
WHEN
- L’utilisateur annule explicitement
THEN
- Transition `UPLOADING -> CANCELLED`
- Upload S3 single/multipart est annulé
- Artefacts ciphertext temporaires sont purgés
TEST-ID: TC-INV-10
Référence spec: INV-103-28
GIVEN
- Une capture en `CANCELLED`
- Une capture en `ANCHOR_CONFIRMED`
WHEN
- Une transition sortante est tentée
THEN
- Toute transition sortante est refusée
- L’état reste inchangé et journalisé comme terminal
TEST-ID: TC-INV-11
Référence spec: INV-103-33, §5.9
GIVEN
- Objet S3 présent sans enregistrement backend correspondant
- Âge de l’objet > `orphanTtl`
WHEN
- Un cycle complet de réconciliation est exécuté
THEN
- L’objet est détecté comme orphelin
- L’objet est supprimé automatiquement
- Un log d’audit de suppression est produit avec `capture_id`/clé objet
TEST-ID: TC-INV-12
Référence spec: INV-103-32, §5.4, CA-103-23
GIVEN
- DEK alloué dans un buffer `TypedArray` mobile
WHEN
- Le wrapping RSA-OAEP-SHA256 se termine avec succès
THEN
- Chaque octet du buffer DEK vaut `0x00` avant déréférencement
- Le test valide le buffer applicatif uniquement (pas la mémoire physique GC)
TEST-ID: TC-INV-13
Référence spec: INV-103-34, §5.11
GIVEN
- Keyring backend: KEK courante + au moins une ancienne KEK
- Payload chiffré avec l’ancienne KEK et `kek_id` correspondant
WHEN
- `POST /documents/capture` est exécuté
THEN
- L’unwrap aboutit via l’ancienne KEK autorisée
- L’ingestion poursuit le flux nominal sans perte
TEST-ID: TC-INV-14
Référence spec: INV-103-35, §5.9, CA-103-26
GIVEN
- Capture en `PENDING_SEAL` avec âge strictement supérieur à `sealSla`
WHEN
- Le job de réconciliation exécute son contrôle SLA
THEN
- Le flag `SEAL_DELAYED` est positionné automatiquement
- Le compteur de cycles conformes est réinitialisé pour cette capture
[v3]
6. Tests de non-régression¶
| Test ID | Objet | Observable | Commentaire |
|---|---|---|---|
| TC-NR-01 | Fidélité image après MAJ SDK mobile | Hash triplet inchangé sur corpus de référence | Cible les régressions de transcodage implicite. |
| TC-NR-02 | Ordonnancement hash/chiffrement/upload | Traces temporelles toujours hash -> encrypt -> upload | Cible régressions pipeline client. |
| TC-NR-03 | Reprise différée après redémarrage app | UPLOAD_DEFERRED reprend sans perte ni doublon, avec URL fraîche | Couvre crash/restart mobile. |
| TC-NR-04 | Garde PENDING_SEAL -> SEALED | Cas négatif toujours bloqué | Évite réouverture fenêtre PENDING_SIGNATURE. |
| TC-NR-05 | États terminaux | CANCELLED et ANCHOR_CONFIRMED restent sans sortie | Évite régression machine à états. |
| TC-NR-06 | Références probatoires de scellement | merkle_proof_ref, tsa_token_ref, tx_hash toujours présents à l’ancrage | Cible régressions worker backend. |
| TC-NR-07 | Key exchange envelope | dek_wrapped_b64 et kek_id toujours présents, DEK jamais en clair en base | Cible régressions crypto. |
| TC-NR-08 | Upload multipart | Reprise partielle chunks reste opérationnelle >10MB | Cible régressions flux 5.5bis. |
| TC-NR-09 | Validation skew timestamp | Rejet stable au-delà ±5 min | Cible régressions anti-rejeu temporel. |
| TC-NR-10 | Fingerprint canonique idempotence | payload_canonical_sha256 stable pour payload logique identique | Cible régressions 200/409. |
| TC-NR-11 | Rotation KEK | Upload différé ancien kek_id toujours déchiffrable pendant fenêtre de rétention keyring | Cible régressions de rotation. |
| TC-NR-12 | Purge locale post-upload | Aucun artefact local conservé après UPLOADED | Cible régression de rétention locale. |
| TC-NR-13 | GC orphelins S3 | Orphelins > orphanTtl supprimés de manière répétable | Cible régression coût/sécurité stockage. |
| TC-NR-14 | Trigger/Clearing SEAL_DELAYED | Positionnement au dépassement SLA et clearing après 3 cycles conformes | Cible régression supervision scellement. |
[v3]
7. Tests négatifs et adversariaux¶
| Test ID | Entrée invalide / abus | Résultat attendu | Observable |
|---|---|---|---|
| TC-NEG-01 | capture_id non UUID v4 | HTTP 400 | Réponse API + absence d’événement probatoire. |
| TC-NEG-02 | hash_sha3_256 non lowercase hex / longueur != 64 | HTTP 400 | Validation regex rejetée. |
| TC-NEG-03 | mime_type=image/jpeg | HTTP 400 | Rejet format contractuel PNG. |
| TC-NEG-04 | size_bytes=0 ou >524288000 | HTTP 400 | Rejet bornes taille. |
| TC-NEG-05 | Fichier non-PNG déguisé en PNG | Rejet flux | Signature PNG invalide en trace de validation. |
| TC-NEG-06 | aes_gcm_nonce_b64/aes_gcm_tag_b64 mal formés | HTTP 400 | Rejet validation champs crypto. |
| TC-NEG-07 | ocr_text > 20000 caractères après sanitisation | HTTP 400 sans troncature | Rejet explicite, aucun fallback silencieux. |
| TC-NEG-08 | ocr_confidence <0 ou >1 | HTTP 400 | Validation stricte intervalle [0,1]. |
| TC-NEG-09 | timestamp_device format non UTC RFC3339 (+02:00, format invalide) | HTTP 400 | Validation regex rejetée. |
| TC-NEG-10 | timestamp_device valide syntaxiquement mais skew > ±5 min | HTTP 400 TIMESTAMP_SKEW_EXCEEDED | Contrôle horloge backend. |
| TC-NEG-11 | dek_wrapped_b64 absent ou base64 invalide | HTTP 400 | Rejet de key exchange incomplet. |
| TC-NEG-12 | Upload S3 sans Content-MD5 ni x-amz-content-sha256 | Rejet upload | Trace échec intégrité transport. |
| TC-NEG-13 | Tentative CAPTURED -> SEALED directe | Transition interdite | Journal de refus transition. |
| TC-NEG-14 | Rejeu même capture_id avec payload canonique divergent | 409 Conflict | Aucune duplication/altération d’attestation. |
| TC-NEG-15 | Concurrence de workers sur même capture_id | Un seul worker transitionne | Lock non acquis => retry planifié 30s. |
| TC-NEG-16 | Config retry/backoff/chunk hors bornes contractuelles | Clamp/rejet selon §5.2 | Valeurs appliquées dans bornes observables. |
| TC-NEG-17 | kek_id absent ou mal formé | HTTP 400 | Validation format/présence rejetée. |
| TC-NEG-18 | dek_wrapped_b64 valide Base64 mais incompatible keyring (kek_id inconnu/non retenu) | HTTP 422 UNWRAP_DEK_FAILED | Rejet unwrap sans création d’événement. |
[v3]
8. Observabilité requise pour les tests¶
- État système : historique des transitions par
capture_id(état source, état cible, timestamp UTC, motif). - Réponse API : codes
200/202/400/409/422/429/503, corps de réponse minimalement contractuel, corrélationcapture_id. - Journal d’audit : traces immuables pour capture, upload, garde de scellement, transitions refusées, purge locale, GC orphelins S3.
- Événement signé / horodaté : présence de
signature_status,hsm_signature_ref,tsa_token_ref,tx_hash, timestamp RFC3339 UTC. - Export probatoire : hash SHA3-256, référence preuve Merkle, référence TSA, hash transaction blockchain.
- Traçabilité locale mobile : timestamps ordonnés
capture -> hash -> encrypt -> upload. - Observabilité réseau : preuve d’absence de payload clair en transit + présence header intégrité S3.
- Audit stockage : preuve d’absence de secrets crypto temporaires en clair au repos (
DEKuniquement wrappé). - Observabilité reprise différée : trace de redemande URL pré-signée et validation JWT avant reprise.
- Observabilité backend :
capture_events.signature_statusinitialiséPENDING_SIGNATUREà l’ingestion. - Idempotence : stockage et exposition interne de
payload_canonical_sha256, mapping explicite200/409. - Rotation KEK : logs d’unwrap avec
kek_idutilisé et clé effectivement retenue (courante ou historique). SEAL_DELAYED: événements de trigger (dépassement SLA) + compteur de cycles conformes + événement de clearing.- Mobile sécurité mémoire : trace test de zéroïsage buffer DEK (
0x00) après wrapping.
[v3]
9. Règles non testables¶
| Règle | Raison | Impact |
|---|---|---|
| Protection contre photo externe prise par un autre appareil | Hors périmètre applicatif explicite (impossible à garantir par le logiciel testé) | Mineur |
| Valeur officielle de l’epic (Q-103-01) | Métadonnée documentaire manquante, pas une règle comportementale exécutable | Mineur |
| Validation P95 sur “devices tiers” (Q-103-02) | Liste officielle des devices non fixée, comparabilité inter-environnements indéterminée | Majeur |
Politique notification à ANCHOR_CONFIRMED (Q-103-04) | Exigence produit non définie (oui/non/contenu) | Mineur |
| Whitelist finale des langues OCR (Q-103-05) | Règle d’acceptation/rejet langues non contractualisée | Majeur |
| Quota rate-limit final pour usages massifs légitimes (Q-103-06) | Valeur métier cible non figée, tests de charge finaux non opposables | Majeur |
[v3]
10. Verdict QA¶
- ✅ Testable contractuellement (aucun bloquant ouvert; réserves documentaires non fonctionnelles listées en §9).
[v3]
Annexe A — Traçabilité Gate 3 v1 → v2 → v3 (tests)¶
| Écart Gate 3 | Couverture tests |
|---|---|
| ECT-01 | v2: TC-NOM-01, TC-INV-03, TC-NR-07 |
| ECT-02 | v2: TC-NOM-01, TC-ERR-06, TC-NEG-11 |
| ECT-03 | v2: TC-NOM-06, TC-ERR-15, TC-NR-03 |
| DIV-01 | v2: TC-ERR-06, TC-NEG-09 |
| DIV-02 | v2: TC-NOM-13, TC-NR-08 |
| DIV-03 | v2: TC-NOM-14, TC-INV-09 |
| DIV-04 | v2: Couvert côté spec; impacts indirects via TC-NOM-07/08 |
| DIV-05 | v2: TC-INV-03, TC-NR-07 |
| DIV-06 | v2: TC-NOM-07 |
| DIV-07 | v2: TC-NOM-07, TC-INV-03, observabilité §8 |
| DIV-08 | v2: TC-INV-01..10 en GIVEN/WHEN/THEN explicite |
| DIV-09 | v2: TC-NOM-01, TC-ERR-14, TC-NEG-12 |
| DIV-10 | v2: TC-ERR-12, TC-NEG-10, TC-NR-09 |
| DIV-11 | v2: TC-NOM-15, TC-ERR-13, matrice §2 (200/409) |
| ECT-01-v2 | v3: TC-NOM-15, TC-INV-06, TC-NR-10 (payload canonique défini + fingerprint SHA-256). |
| ECT-11-v2 | v3: TC-ERR-16 (422), TC-ERR-17 (503). |
| ECT-19-v2 | v3: TC-NOM-17, TC-ERR-11 (rétention locale post-upload résolue). |
| ECT-07-v2 | v3: TC-NR-07 + contrôles prérequis stack crypto en campagne CI mobile. |
| ECT-05-v2 | v3: TC-INV-12 (zéroïsage DEK buffer). |
| ECT-06-v2 | v3: TC-INV-11, TC-NR-13 (GC orphelins S3). |
| ECT-08-v2 | v3: TC-INV-08 (définition cycle conforme + clearing). |
| ECT-02-v2 | v3: TC-INV-03 (artefacts crypto sans “ReKey”). |
| ECT-03-v2 | v3: TC-NOM-15, TC-INV-06 (normalisation lowercase capture_id). |
| ECT-13-v2 | v3: TC-INV-12 (test explicite effacement DEK). |
| ECT-17-v2 | v3: TC-NOM-16, TC-INV-13 (rotation KEK/keyring). |
| ECT-09-v2 | v3: TC-INV-14, TC-ERR-09 (trigger automatique SEAL_DELAYED). |
[v3] tokens used 71 196
PD-103 — Capture probatoire d’écran et scellement automatique¶
1. Objectif¶
Définir le contrat fonctionnel, sécurité et conformité permettant de transformer une capture d’écran applicative iOS en preuve probatoire scellée, avec hash local préalable, chiffrement local, upload automatique, intégration pipeline Merkle/TSA/HSM/blockchain, notification utilisateur à l’état SEALED, idempotence déterministe basée sur payload canonique, et gestion explicite des erreurs d’unwrapping DEK.
[v3]
2. Périmètre / Hors périmètre¶
Inclus¶
- Capture d’écran du contenu applicatif ProbatioVault (iOS).
- Prévisualisation puis validation ou annulation explicite.
- Distinction contractuelle
fichier original/fichier soumis/fichier probatoire. - Calcul local du hash
SHA3-256avant tout upload. - OCR optionnel local uniquement (Apple Vision), sans valeur probatoire.
- Chiffrement local
AES-256-GCMfile-level avant envoi réseau. - Upload automatique via URL pré-signée.
- Création événement probatoire backend puis scellement (Merkle + TSA + HSM + blockchain).
- Notification utilisateur à l’atteinte de
SEALED. - Gestion des erreurs réseau avec file locale chiffrée et reprise.
- Purge proactive des artefacts temporaires sensibles au démarrage du flux.
- Normalisation canonique et comparaison idempotente côté backend via empreinte
SHA-256. - Rotation de KEK avec keyring backend (
KEKcourante + historiques) et champkek_id. - Suppression locale immédiate du payload chiffré après confirmation
UPLOADED. - Garbage collection des objets S3 orphelins.
Exclu¶
- Capture d’écran globale système iOS hors application (sandbox Apple).
- Capture vidéo.
- Annotation d’image avancée.
- Analyse IA distante du contenu.
- Import galerie (story séparée).
- Protection contre photo externe prise par un autre appareil (hors périmètre, non testable applicativement).
[v3]
3. Définitions¶
| Terme | Définition contractuelle |
|---|---|
fichier original | Fichier PNG brut issu de la capture applicative. |
fichier soumis | Fichier effectivement envoyé (doit être bit-à-bit identique à l’original). |
fichier probatoire | Fichier référencé par hash et inclus dans la chaîne probatoire à partir de la transition UPLOADED -> PENDING_SEAL après persistance de l’événement dans le journal probatoire. |
capture_id | Identifiant unique de la capture, UUID v4, normalisé en lowercase avant stockage, lookup et comparaison. |
content_hash | Alias canonique de hash_sha3_256 (hex lowercase). |
OCR | Extraction textuelle locale (indexation), non substitutive du contenu image. |
K_doc / DEK | Data Encryption Key unique par capture (256 bits aléatoires), générée localement via CSPRNG, utilisée pour chiffrer image_original_bytes en AES-256-GCM. |
KEK backend | Clé publique backend (RSA-OAEP-SHA256) utilisée côté mobile pour wrapper le DEK. La clé privée associée est conservée côté backend pour unwrapping. |
kek_id | Identifiant de la KEK publique utilisée au wrapping mobile, transmis au backend dans POST /documents/capture. |
keyring KEK | Ensemble backend composé de la KEK courante + N anciennes KEK autorisées pour unwrapping des uploads différés. |
dek_wrapped_b64 | DEK wrappé avec KEK backend, encodé Base64, transmis dans POST /documents/capture. |
payload canonique d’idempotence | Représentation JSON déterministe construite depuis les champs contractuels non optionnels (minimum capture_id + content_hash), clés triées alphabétiquement, valeurs normalisées, sérialisation UTF-8 stable. |
payload_canonical_sha256 | Empreinte SHA-256 du payload canonique; stockée en base et utilisée pour distinguer 200 idempotent vs 409 Conflict. |
session valide | Session d’authentification utilisateur active (JWT non expiré, signature valide, scopes requis). En reprise différée, une nouvelle URL pré-signée S3 doit être redemandée même si la session reste valide. |
upload différé | État de reprise où la capture chiffrée attend un réseau disponible et une session d’authentification valide. |
SEAL_DELAYED | Flag de retard de scellement posé quand une capture reste en PENDING_SEAL au-delà du sealSla. |
cycle de réconciliation | Une exécution complète du job de réconciliation (scan états, rattrapage, contrôles SLA, GC orphelins). |
cycle conforme | Cycle de réconciliation où toutes les captures PENDING_SEAL présentes en début de cycle sont scellées avec succès avant fin de cycle. |
SEALED | État de scellement confirmé côté backend (garde signature obligatoire). |
ANCHOR_CONFIRMED | État terminal de confirmation d’ancrage blockchain. |
état terminal | État sans transition sortante autorisée. |
[v3]
4. Invariants (non négociables)¶
| ID | Règle | Justification |
|---|---|---|
| INV-103-01-fidelity | fichier soumis = fichier original bit-à-bit. | Évite transcodage silencieux invalidant la preuve. |
| INV-103-02-no-transform | Recompression, redimensionnement, altération colorimétrique interdites. | Intégrité probatoire. |
| INV-103-03-hash-local-first | SHA3-256(image_bytes) est calculé localement avant tout upload. | Preuve d’intégrité en amont réseau. |
| INV-103-04-ocr-local-only | OCR optionnel, local, non bloquant, sans appel IA externe. | Vie privée et conformité besoin. |
| INV-103-05-ocr-non-probative | L’OCR enrichit les métadonnées mais ne remplace jamais l’image probatoire. | Valeur probante portée par image + hash. |
| INV-103-06-encryption-file-level | Chiffrement AES-256-GCM file-level obligatoire avant upload; chaque upload S3 doit fournir un contrôle d’intégrité du ciphertext (Content-MD5 ou x-amz-content-sha256). | Empêche fuite clair + évite nonce-reuse chunk-level + garantit l’intégrité du ciphertext en transit. |
| INV-103-07-purge-startup | purgeStale() exécuté au démarrage de chaque flux. | Résilience crash et suppression artefacts sensibles. |
| INV-103-08-sealed-guard | Transition PENDING_SEAL -> SEALED autorisée uniquement si signature_status='SIGNED' ET hsm_signature_ref != NULL. | Fermeture fenêtre PENDING_SIGNATURE. |
| INV-103-09-envelope-encryption | Tout artefact crypto temporaire (DEK, nonce, fragment) est chiffré au repos; DEK est wrappé avec la clé publique backend RSA-OAEP-SHA256, stocké en base uniquement sous forme wrappée, jamais en clair. | Invariant sécurité crypto testable. |
| INV-103-10-atomicity-scope | DB sync ACID; journal/Merkle/TSA/blockchain async post-commit idempotent et rattrapable. | Cohérence multi-composant. |
| INV-103-11-distributed-protection | Locks, idempotence, réconciliation, rate-limit et clearing conditionnel sont contractuels. | Robustesse en concurrence et retries. |
| INV-103-12-notify-sealed | Notification utilisateur obligatoire à l’entrée en SEALED. | Exigence fonctionnelle EF-6. |
| INV-103-20-transition-captured-uploading | CAPTURED -> UPLOADING seulement si hash local + chiffrement local validés. | Préconditions sécurité. |
| INV-103-21-transition-captured-cancelled | CAPTURED -> CANCELLED autorisée sur annulation utilisateur explicite. | Contrôle utilisateur. |
| INV-103-22-transition-uploading-uploaded | UPLOADING -> UPLOADED seulement sur upload S3 confirmé + ACK backend. | Cohérence état transport. |
| INV-103-23-transition-uploading-deferred | UPLOADING -> UPLOAD_DEFERRED sur réseau indisponible ou retries épuisés. | Continuité sans perte. |
| INV-103-24-transition-deferred-uploading | UPLOAD_DEFERRED -> UPLOADING uniquement sur restauration réseau + session JWT valide + redemande d’une nouvelle URL pré-signée. | Reprise contrôlée malgré expiration naturelle des URL S3. |
| INV-103-25-transition-uploaded-pending-seal | UPLOADED -> PENDING_SEAL après persistance événement probatoire backend dans capture_events et journal probatoire append-only. | Début pipeline scellement. |
| INV-103-26-transition-pending-seal-sealed | PENDING_SEAL -> SEALED respecte INV-103-08. | Sécurité de transition. |
| INV-103-27-transition-sealed-anchor | SEALED -> ANCHOR_CONFIRMED uniquement après confirmation d’ancrage blockchain. | Complétude chaîne probatoire. |
| INV-103-28-terminal-states | CANCELLED et ANCHOR_CONFIRMED : -> * : INTERDITE (état terminal, résolution manuelle uniquement). | Machine à états fermée. |
| INV-103-29-transition-uploading-cancelled | UPLOADING -> CANCELLED autorisée uniquement sur annulation utilisateur explicite; obligation d’annuler l’upload S3 (single/multipart) et de purger le ciphertext en transit/local temporaire. | Annulation sûre sans résidu sensible. |
| INV-103-30-key-exchange | Échange de clé obligatoire: DEK aléatoire 256 bits par capture, chiffrement image avec DEK, wrapping DEK avec KEK backend, transmission dek_wrapped_b64, unwrapping backend via clé privée en mémoire volatile. | Backend déchiffre sans jamais persister le DEK en clair. |
| INV-103-31-capture-id-normalization | capture_id DOIT être normalisé en lowercase côté mobile (avant émission) et côté backend (à réception) avant stockage/comparaison/idempotence. | Évite collisions logiques et ambiguïtés de casse. |
| INV-103-32-dek-zeroization | Le buffer mémoire applicatif contenant le DEK DOIT être écrasé (0x00) immédiatement après wrapping réussi, puis déréférencé. | Réduction de fenêtre d’exposition en environnement JS. |
| INV-103-33-s3-orphan-gc | Tout objet S3 sans enregistrement backend correspondant et âgé de plus que orphanTtl DOIT être supprimé par réconciliation. | Évite accumulation d’orphelins et fuite de coûts/données. |
| INV-103-34-kek-keyring-rotation | Le backend DOIT maintenir un keyring (KEK courante + historiques) et tenter l’unwrap avec ces clés selon politique de rotation; kek_id DOIT être présent dans le POST. | Assure déchiffrement des uploads différés après rotation. |
| INV-103-35-seal-delayed-trigger | Une capture PENDING_SEAL dont l’âge dépasse sealSla DOIT être marquée SEAL_DELAYED par le job de réconciliation. | Détection automatique du retard de scellement. |
| INV-103-36-seal-delayed-clearing | Le flag SEAL_DELAYED ne peut être levé qu’après 3 cycles conformes consécutifs. | Évite clearing prématuré des alertes. |
| INV-103-37-idempotency-canonical-fingerprint | Le backend DOIT comparer payload_canonical_sha256 pour décider 200 idempotent vs 409 Conflict; les champs optionnels OCR sont exclus de ce fingerprint. | Contrat idempotence déterministe et testable. |
| INV-103-38-transition-deferred-cancelled | UPLOAD_DEFERRED -> CANCELLED est autorisée uniquement à expiration de deferredUploadTtl, avec purge locale chiffrée obligatoire. | Contrat explicite de fin de vie du différé. |
| INV-103-39-nonce-csprng | Le nonce AES-GCM 96 bits DOIT provenir d’un CSPRNG et être unique par capture. | Prévention de réutilisation nonce en GCM. |
[v3]
5. Flux nominaux¶
5.1 Modèle de données (formats et contraintes contractuelles)¶
| Donnée | Format / encodage | Taille / longueur | Jeu caractères | Casse | Validation | Si invalide |
|---|---|---|---|---|---|---|
capture_id | UUID v4 texte | 36 | [0-9a-f-] | canonical lower | regex UUID v4 (accept upper/lower en entrée puis normalisation lowercase) | 400 |
image_original_bytes | PNG binaire | 1..524288000 bytes | binaire | n/a | signature PNG valide | rejet flux |
image_submitted_bytes | PNG binaire | 1..524288000 bytes | binaire | n/a | égalité bit-à-bit avec original | rejet flux |
image_probatory_bytes | PNG binaire | 1..524288000 bytes | binaire | n/a | égalité bit-à-bit avec soumis | rejet flux |
hash_sha3_256 | hex | 64 caractères (32 bytes) | [a-f0-9] | case-sensitive lower | ^[a-f0-9]{64}$ | 400 |
timestamp_device | RFC3339 UTC | 20..32 | UTF-8 | n/a | ^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z$ + skew backend <= ±300s | 400 |
device_id | UUID v4 texte | 36 | [0-9a-f-] | insensitive hex | regex UUID v4 | 400 |
app_version | SemVer | 5..32 | [0-9A-Za-z.+-] | case-sensitive | ^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$ | 400 |
mime_type | string | 9 | image/png | case-sensitive | valeur exacte image/png | 400 |
size_bytes | entier non signé | 1..524288000 | chiffres | n/a | >=1 && <=524288000 | 400 |
ocr_enabled | booléen JSON | 1 booléen | true|false | n/a | type strict | 400 |
ocr_text | UTF-8 | 0..20000 caractères | UTF-8 imprimable | case-sensitive | normalisation NFC + suppression des control chars C0/C1 (hors , ` | |
, | ||||||
| `) + longueur max | rejet > max après sanitisation | |||||
ocr_confidence | décimal | [0.00,1.00] | chiffres + . | n/a | 0<=x<=1 | 400 |
ocr_language | tag BCP-47 | 2..35 | [A-Za-z0-9-] | case-insensitive | ^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$ | 400 |
aes_gcm_nonce_b64 | base64 | 16 chars (12 bytes) | [A-Za-z0-9+/] | case-sensitive | ^[A-Za-z0-9+/]{16}$ | 400 |
aes_gcm_tag_b64 | base64 | 24 chars (16 bytes) | [A-Za-z0-9+/=] | case-sensitive | ^[A-Za-z0-9+/]{22}==$ | 400 |
dek_wrapped_b64 | base64 | 128..4096 chars (ex: 344 chars pour RSA-2048) | [A-Za-z0-9+/=] | case-sensitive | ^[A-Za-z0-9+/]+={0,2}$ + decode Base64 valide | 400 |
kek_id | texte opaque | 1..64 | [A-Za-z0-9._-] | case-sensitive | ^[A-Za-z0-9._-]{1,64}$ | 400 |
payload_canonical_sha256 (interne backend) | hex | 64 | [a-f0-9] | case-sensitive lower | ^[a-f0-9]{64}$ | erreur interne si absent/incohérent |
signature_status | enum | 1 valeur | A-Z_ | case-sensitive | colonne backend capture_events.signature_status, initialisée à PENDING_SIGNATURE lors du POST; valeurs PENDING_SIGNATURE ou SIGNED | transition refusée |
hsm_signature_ref | texte opaque | 1..256 | ASCII printable | case-sensitive | non nul pour SEALED | transition refusée |
Règles de normalisation contractuelle : 1. capture_id est normalisé en lowercase avant stockage et comparaison. 2. content_hash canonique = lower(hash_sha3_256). 3. Les champs optionnels OCR (ocr_text, ocr_confidence, ocr_language) sont exclus du fingerprint canonique d’idempotence. 4. La sérialisation canonique utilise des clés JSON triées alphabétiquement (cf. §5.12). 5. Toute section ultérieure (6, 7, 8, 10) réutilise ces formats sans redéfinition.
[v3]
5.2 Bornes numériques contractuelles¶
| Paramètre | Défaut | Min | Max | Unité | Référence / Percentile | Hors bornes |
|---|---|---|---|---|---|---|
Latence capture (trigger -> CAPTURED) | 3000 | 0 | 3000 | ms | iPhone 12 (A14), iOS 16+, P95 | non-conformité CA + erreur perf |
Latence capture->upload (trigger -> UPLOADED) | 5000 | 0 | 5000 | ms | iPhone 12 (A14), iOS 16+, P95 | transition vers UPLOAD_DEFERRED |
Latence scellement (UPLOADED -> SEALED) | 600000 | 60000 | 600000 | ms | Backend prod + worker PD-55, P95 | reste PENDING_SEAL, alerte retard |
| Taille max image | 524288000 | 1 | 524288000 | bytes | Contrat upload mobile | rejet 400 |
| Seuil multipart | 10000000 | 10000000 | 10000000 | bytes | règle fixe produit | non applicable (fixe) |
| Taille chunk multipart | 5242880 | 5242880 | 52428800 | bytes | Contrat upload backend/mobile | clamp puis upload |
| Retries upload auto | 3 | 0 | 5 | tentatives | réseau mobile/Wi-Fi | clamp config, sinon rejet |
| Backoff base retry | 1 | 1 | 10 | secondes | réseau mobile/Wi-Fi | clamp config |
| TTL URL pré-signée S3 | 900 | 60 | 900 | secondes | Contrat S3 | URL expirée => redemande URL |
| TTL session upload différé | 86400 | 3600 | 86400 | secondes | contrat upload backend | UPLOAD_SESSION_EXPIRED + purge |
Tolérance skew timestamp_device | 300 | 300 | 300 | secondes | Contrat backend anti-rejeu | 400 TIMESTAMP_SKEW_EXCEEDED |
Rate-limit POST /documents/capture | 60 | 1 | 300 | req/min/utilisateur | backend API, fenêtre 60s | HTTP 429 |
TTL objet S3 orphelin (orphanTtl) | 900 | 300 | 3600 | secondes | réconciliation backend | suppression automatique |
| Profondeur keyring KEK (anciennes clés) | 3 | 1 | 10 | clés | politique rotation crypto | rejet config non conforme |
[v3]
5.3 SLA temporels (transitions avec dimension temps)¶
| SLA | Défaut | Min | Max | Configurabilité | Comportement à expiration |
|---|---|---|---|---|---|
captureUploadSla (trigger -> UPLOADED) | 5s | 0s | 5s | non (contrat produit) | UPLOADING -> UPLOAD_DEFERRED, reprise automatique |
sealSla (UPLOADED -> SEALED) | 10min | 1min | 10min | oui (config backend) | état reste PENDING_SEAL, SEAL_DELAYED positionné par réconciliation, notification de retard |
deferredUploadTtl (UPLOAD_DEFERRED) | 24h | 1h | 24h | oui (config upload) | transition forcée CANCELLED + purge locale chiffrée |
postUploadLocalPurgeDelay (UPLOADED) | immédiat | immédiat | immédiat | non (contrat sécurité) | suppression locale immédiate du payload chiffré après ACK backend |
Note contractuelle rétention locale : le TTL local ne s’applique qu’aux captures en UPLOAD_DEFERRED. Une capture confirmée UPLOADED est supprimée localement sans attendre le TTL.
[v3]
5.4 Flux nominal A — Capture et préparation locale¶
- L’utilisateur ouvre l’outil
Capture probatoire. - L’utilisateur déclenche la capture applicative.
- Le système produit
image_original_bytes(PNG) ettimestamp_device. - Le système calcule
hash_sha3_256 = SHA3-256(image_original_bytes). - Le système construit les métadonnées (
capture_id,device_id,app_version,mime_type,size_bytes) en normalisantcapture_iden lowercase. - Si OCR activé, extraction locale
ocr_text,ocr_confidence,ocr_language; en cas d’échec OCR, continuer sans OCR. - Le système génère un
DEKaléatoire 256 bits (K_doc) via CSPRNG. - Le système génère un nonce 96 bits unique via CSPRNG pour la capture.
- Le système chiffre le fichier en local (
AES-256-GCMfile-level) avecDEKet nonce, puis obtientciphertext + tag. - Le système wrappe
DEKavec la clé publique backend (KEK,RSA-OAEP-SHA256), récupère lekek_idactif correspondant, et produitdek_wrapped_b64. - Le système écrase le buffer
DEK(TypedArray.fill(0x00)) puis déréférence immédiatement l’objet; limitation reconnue: l’effacement physique complet n’est pas garanti en runtime JS (GC non déterministe). - Le système prépare les artefacts
ciphertext + nonce + tag + dek_wrapped_b64 + kek_idpour upload ou différé.
[v3]
5.5 Flux nominal B — Upload et reprise différée¶
- Le système demande une URL pré-signée d’upload.
- Le système envoie uniquement le payload chiffré (
ciphertext), jamais le clair. - L’upload S3 inclut un contrôle d’intégrité du ciphertext (
Content-MD5oux-amz-content-sha256). - Le système appelle
POST /documents/captureavec les métadonnées et crypto-matériaux:capture_id,hash_sha3_256,timestamp_device,metadata,ocr?,aes_gcm_nonce_b64,aes_gcm_tag_b64,dek_wrapped_b64,kek_id. - Le backend tente l’unwrap
DEKvia keyring KEK (KEK courante puis anciennes, avec priorité de recherche surkek_idsi présent), initialisecapture_events.signature_status='PENDING_SIGNATURE', puis persiste l’événement. - Si
dek_wrapped_b64est invalide/incompatible (clé non retrouvée ou échec cryptographique) : HTTP422(UNWRAP_DEK_FAILED), aucune persistance d’événement. - Si service de clé (HSM/KMS/clé privée) indisponible : HTTP
503(KEY_SERVICE_UNAVAILABLE), aucune persistance d’événement. - Sur succès réseau + ACK backend, transition
UPLOADING -> UPLOADED. - Immédiatement après
UPLOADED, suppression locale du payload chiffré (non conservation post-upload). - En cas d’échec retryable ou réseau indisponible, transition
UPLOADING -> UPLOAD_DEFERRED. - En
UPLOAD_DEFERRED, le payload reste stocké localement chiffré jusqu’à reprise ou expiration TTL. - À retour réseau, la reprise exige une
session valide(JWT auth valide) et la redemande d’une nouvelle URL pré-signée avantUPLOAD_DEFERRED -> UPLOADING. - Si session JWT invalide/expirée, pas de reprise; réauthentification/refresh requis.
- À expiration TTL différé, transition
UPLOAD_DEFERRED -> CANCELLEDpuis purge locale chiffrée. - En cas de replay même
capture_idnormalisé, le backend répond200idempotent sipayload_canonical_sha256identique, sinon409 Conflictsi fingerprint divergent. - Les URL pré-signées S3 sont traitées comme bearer token: protection contractuelle par TLS obligatoire (
HTTPS) en transit.
[v3]
5.5bis Flux nominal B2 — Upload multipart¶
- Si
size_bytes > 10_000_000, le système bascule en upload multipart. - Le backend fournit
upload_id+ URLs pré-signées par part. - Le ciphertext est découpé en chunks (
chunk_sizecontractuel, défaut 5_242_880 bytes). - Chaque part est uploadée avec contrôle d’intégrité (
Content-MD5oux-amz-content-sha256) et retries bornés. - En échec d’une part après retries, la capture passe en
UPLOAD_DEFERRED; les parts réussies (ETags) sont conservées pour reprise partielle. - À la reprise, le client redemande des URLs pré-signées fraîches et reprend les parts manquantes (ou redémarre la session multipart si
upload_idexpiré/invalide). UPLOADING -> UPLOADEDn’est autorisée qu’aprèsCompleteMultipartUploadconfirmé S3 + ACK backend.- Sur annulation utilisateur explicite en
UPLOADING, l’application annule la session multipart S3 (AbortMultipartUpload) puis purge le ciphertext local/in-transit. - Après ACK backend menant à
UPLOADED, les artefacts locaux chiffrés multipart sont supprimés immédiatement.
[v3]
5.6 Flux nominal C — Scellement et notification¶
- Le backend persiste l’événement probatoire (
capture_events+ journal probatoire append-only); ce commit déclencheUPLOADED -> PENDING_SEALet le fichier acquiert le statutfichier probatoire. - Pipeline de scellement exécute Merkle, signature HSM, timestamp TSA et ancrage blockchain.
- Garde de transition :
signature_status='SIGNED'EThsm_signature_ref != NULL. - Si garde satisfaite, transition
PENDING_SEAL -> SEALED. - Après confirmation d’ancrage, transition
SEALED -> ANCHOR_CONFIRMED. - À
SEALED, push iOS obligatoire : “Votre capture a été scellée avec succès. Preuve vérifiable disponible.”
5.7 Machine à états (transitions autorisées/interdites + retours)¶
| État | Transitions sortantes autorisées | Transitions interdites | Comportement downgrade/retour |
|---|---|---|---|
CAPTURED | UPLOADING, CANCELLED | UPLOADED, UPLOAD_DEFERRED, PENDING_SEAL, SEALED, ANCHOR_CONFIRMED | n/a |
UPLOADING | UPLOADED, UPLOAD_DEFERRED, CANCELLED | CAPTURED, PENDING_SEAL, SEALED, ANCHOR_CONFIRMED | UPLOADING -> CAPTURED interdit; UPLOADING -> CANCELLED uniquement sur annulation explicite (purge ciphertext + annulation upload S3) |
UPLOAD_DEFERRED | UPLOADING, CANCELLED (TTL expiré) | UPLOADED, PENDING_SEAL, SEALED, ANCHOR_CONFIRMED | Retour autorisé UPLOAD_DEFERRED -> UPLOADING avec JWT valide + nouvelle URL pré-signée |
UPLOADED | PENDING_SEAL | CAPTURED, UPLOADING, UPLOAD_DEFERRED, SEALED, ANCHOR_CONFIRMED, CANCELLED | UPLOADED -> UPLOADING interdit |
PENDING_SEAL | SEALED (garde INV-103-08) | CAPTURED, UPLOADING, UPLOAD_DEFERRED, UPLOADED, CANCELLED, ANCHOR_CONFIRMED | PENDING_SEAL -> UPLOADED interdit |
SEALED | ANCHOR_CONFIRMED | CAPTURED, UPLOADING, UPLOAD_DEFERRED, UPLOADED, PENDING_SEAL, CANCELLED | SEALED -> PENDING_SEAL interdit |
ANCHOR_CONFIRMED | aucune | -> * : INTERDITE (état terminal, résolution manuelle uniquement) | terminal |
CANCELLED | aucune | -> * : INTERDITE (état terminal, résolution manuelle uniquement) | terminal |
Transitions retour applicables : uniquement UPLOAD_DEFERRED -> UPLOADING.
Toutes les autres transitions inverses sont explicitement interdites.
Checklist machine à états : - [x] Chaque état liste ses transitions sortantes autorisées et interdites. - [x] Les états terminaux portent la mention explicite -> * : INTERDITE (état terminal, résolution manuelle uniquement). - [x] UPLOADING -> CANCELLED est contractualisée avec condition d’annulation explicite et purge/abort. - [x] UPLOAD_DEFERRED -> CANCELLED est explicitement bornée par deferredUploadTtl (INV-103-38). - [x] Le modèle d’états est couvert par les invariants INV-103-07, INV-103-20..30 et INV-103-38.
[v3]
5.8 Atomicité multi-composant (DB + async)¶
| Scope | Synchrone/Async | Garantie |
|---|---|---|
| Calcul hash + chiffrement local | Synchrone | Atomicité locale du flux avant réseau |
| Upload S3 ciphertext + contrôle intégrité | Synchrone | Intégrité transport vérifiable par checksum |
Persistance événement capture backend | Synchrone (transaction DB) | ACID |
| Unwrap DEK backend | Synchrone (ingest API) | DEK en mémoire volatile uniquement, jamais persisté en clair; erreurs mappées 422/503 sans création d’événement |
Purge locale après ACK UPLOADED | Synchrone côté mobile | Suppression immédiate des artefacts locaux chiffrés post-upload |
| Journal append-only | Async post-commit | Idempotent, retry-safe |
| Agrégation Merkle | Async post-commit | Rattrapage par réconciliation |
| Signature HSM / TSA / blockchain | Async post-commit | Retry-safe et ordonnancement déterministe |
| GC objets S3 orphelins | Async (réconciliation) | Détection puis suppression > orphanTtl |
| Crash pré-commit | — | rollback, aucun artefact persistant |
| Crash post-commit | — | DB valide, reprise asynchrone par réconciliation |
[v3]
5.9 Mécanismes de protection distribuée¶
| Mécanisme | Contrat PD-103 |
|---|---|
| Lock distribué | Scope capture_id uniquement. TTL défaut 300s, min 60s, max 900s. Si lock non acquis: pas de transition d’état, retry planifié après 30s. |
| Idempotence | Clé logique capture_id normalisé + payload_canonical_sha256. Rejeu même clé + fingerprint identique => 200 idempotent sans doublon d’attestation. Fingerprint divergent => 409 Conflict. Fenêtre de dédup défaut 24h, min 1h, max 168h. |
| Réconciliation | Cron défaut 10min, min 8min, max 12min; scan états non-terminaux, contrôle dépassement sealSla, re-enqueue, et scan S3 des orphelins. Suppression des orphelins âgés de plus que orphanTtl. |
| Rate-limiting | Granularité user_id + IP; quota défaut 60 req/min, min 1, max 300; dépassement => 429. |
| Clearing conditionnel | SEAL_DELAYED levé après 3 cycles conformes consécutifs (min 1, max 10) via compteur persistant par capture. |
Définitions opérationnelles : 1. Un cycle = une exécution complète du job de réconciliation. 2. Un cycle est conforme si toutes les captures en PENDING_SEAL au début du cycle sont scellées avec succès avant la fin du cycle. 3. Déclenchement SEAL_DELAYED : une capture en PENDING_SEAL depuis plus de sealSla est détectée pendant un cycle et marquée automatiquement. 4. Levée SEAL_DELAYED : uniquement après 3 cycles conformes consécutifs.
Checklist protection distribuée : - [x] Lock: scope + TTL + comportement lock non acquis définis. - [x] Idempotence: clé + fenêtre de dédup + comportement 200/409 définis avec fingerprint canonique. - [x] Réconciliation: interval cron + seuil orphelin + rattrapage + GC orphelins définis. - [x] Rate-limit: granularité + quota + comportement dépassement définis. - [x] Clearing: nombre de cycles conformes, définition de cycle conforme, trigger SEAL_DELAYED et mécanisme de comptage définis.
[v3]
5.10 Stratégie de migration DDL¶
Aucune modification de colonne existante identifiée dans PD-103.
Stratégie de migration DDL : non applicable à cette story.
5.11 Contraintes inter-modules¶
Aucune route d’un autre module n’est bloquée ou modifiée par PD-103.
Rotation KEK (contrat backend/mobile)¶
- Le backend expose la clé publique active et son
kek_idsigné/versionné. - Le mobile wrappe le
DEKavec cette clé active et transmet obligatoirementkek_id. - Le backend maintient un keyring : KEK courante +
Nanciennes KEK (configurée en §5.2). - À l’ingestion, l’unwrap tente la KEK courante puis les anciennes, avec priorisation de la clé correspondant à
kek_idquand disponible. - Si aucune clé ne permet l’unwrap:
422 UNWRAP_DEK_FAILED. - Si la clé privée n’est pas accessible (HSM/KMS indisponible):
503 KEY_SERVICE_UNAVAILABLE. - Les anciennes KEK sont conservées au minimum pendant
deferredUploadTtl + dedupWindowpour couvrir les captures enUPLOAD_DEFERRED.
[v3]
5.12 Contrat API — POST /documents/capture¶
| Élément | Contrat |
|---|---|
| Méthode / route | POST /documents/capture |
| Authentification | Authorization: Bearer <JWT> obligatoire |
| Corps requis | capture_id, hash_sha3_256, timestamp_device, device_id, app_version, mime_type, size_bytes, aes_gcm_nonce_b64, aes_gcm_tag_b64, dek_wrapped_b64, kek_id, références upload S3, ocr_* optionnels |
| Sémantique succès (nouveau) | 202 Accepted (ingestion acceptée, statut courant exposé) |
| Sémantique idempotente | 200 OK si même capture_id normalisé + même payload_canonical_sha256 (aucun doublon d’attestation) |
| Sémantique conflit | 409 Conflict si même capture_id normalisé avec fingerprint canonique divergent |
| Erreurs contrat | 400 validation/format/skew timestamp, 401/403 auth, 422 unwrap DEK invalide/incompatible, 429 rate-limit, 503 service de clé indisponible |
| Effet backend | Initialisation capture_events.signature_status='PENDING_SIGNATURE' à l’ingestion réussie, puis pipeline de scellement |
Définition normative du payload canonique d’idempotence
Objet canonique utilisé pour calculer payload_canonical_sha256 :
{
"aes_gcm_nonce_b64": "<base64>",
"aes_gcm_tag_b64": "<base64>",
"capture_id": "<uuid-v4-lowercase>",
"content_hash": "<hash_sha3_256-lowercase>",
"dek_wrapped_b64": "<base64>",
"kek_id": "<opaque-key-id>",
"mime_type": "image/png",
"size_bytes": 12345,
"upload_object_key": "<s3-object-reference>"
}
Règles : 1. capture_id est lowercased avant sérialisation. 2. content_hash est lower(hash_sha3_256). 3. Clés JSON triées alphabétiquement, sérialisation UTF-8 déterministe sans espace superflu. 4. Les champs optionnels (ocr_text, ocr_confidence, ocr_language) sont exclus. 5. Le backend calcule payload_canonical_sha256 = SHA-256(canonical_json_bytes) et le stocke en base. 6. Comparaison idempotence backend : - même capture_id + même payload_canonical_sha256 => 200 - même capture_id + hash différent => 409
[v3]
5bis. Diagrammes¶
Diagramme d’état (stateDiagram-v2)¶
stateDiagram-v2
[*] --> CAPTURED : capture applicative validee
CAPTURED --> UPLOADING : INV-103-20
CAPTURED --> CANCELLED : INV-103-21
UPLOADING --> UPLOADED : INV-103-22
UPLOADING --> UPLOAD_DEFERRED : INV-103-23
UPLOADING --> CANCELLED : INV-103-29
UPLOAD_DEFERRED --> UPLOADING : INV-103-24
UPLOAD_DEFERRED --> CANCELLED : INV-103-38 (TTL expire)
UPLOADED --> PENDING_SEAL : INV-103-25
PENDING_SEAL --> SEALED : INV-103-26
SEALED --> ANCHOR_CONFIRMED : INV-103-27
ANCHOR_CONFIRMED --> [*]
CANCELLED --> [*]
note right of PENDING_SEAL
Si age(PENDING_SEAL) > sealSla:
flag SEAL_DELAYED positionne (INV-103-35)
clearing apres 3 cycles conformes (INV-103-36)
end note
note right of ANCHOR_CONFIRMED
Etat terminal:
-> * INTERDITE (resolution manuelle uniquement)
end note
note right of CANCELLED
Etat terminal:
-> * INTERDITE (resolution manuelle uniquement)
end note Diagramme de séquence (sequenceDiagram)¶
sequenceDiagram
actor U as Utilisateur
participant A as App Mobile (React Native/Expo)
participant C as Crypto Local
participant O as OCR Local (Vision)
participant B as Backend API
participant S3 as S3 (URL pre-signee)
participant R as Reconciliation Job
participant M as Worker Merkle
participant H as HSM/KMS
participant T as TSA
participant BC as Blockchain
participant N as Notification Push
U->>A: Declenche capture probatoire
A->>A: image_original_bytes (PNG)
A->>C: SHA3-256(image_original_bytes)
C-->>A: hash_sha3_256_hex
opt OCR active
A->>O: OCR(image_original_bytes)
O-->>A: ocr_text + ocr_confidence + ocr_language
end
A->>C: DEK = random(256 bits) (CSPRNG)
A->>C: nonce = random(96 bits) (CSPRNG)
A->>C: AES-256-GCM(image_original_bytes, DEK, nonce)
C-->>A: ciphertext + nonce_b64 + tag_b64
A->>C: RSA-OAEP-SHA256 wrap(DEK, KEK_pub_backend)
C-->>A: dek_wrapped_b64 + kek_id
A->>A: zeroize(DEK_buffer)
A->>S3: PUT(ciphertext, Content-MD5|x-amz-content-sha256)
S3-->>A: 200 OK
A->>B: POST /documents/capture {..., dek_wrapped_b64, kek_id}
B->>H: unwrap(DEK) via keyring (courante + anciennes)
alt unwrap OK
H-->>B: DEK volatil
B->>B: insert capture_events(signature_status='PENDING_SIGNATURE')
B->>B: persist payload_canonical_sha256
B-->>A: 202 Accepted
A->>A: UPLOADING -> UPLOADED + purge locale immediate
else dek_wrapped invalide/incompatible
H-->>B: unwrap error
B-->>A: 422 UNWRAP_DEK_FAILED
else HSM/KMS indisponible
H-->>B: service unavailable
B-->>A: 503 KEY_SERVICE_UNAVAILABLE
end
opt reseau KO avant ACK backend
A->>A: UPLOADING -> UPLOAD_DEFERRED
A->>A: stockage local chiffre (TTL differe)
end
B->>M: append leaf = SHA-256(payload_canonique_json)
M-->>B: merkle_root
B->>H: sign(merkle_root)
H-->>B: signature_status=SIGNED + hsm_signature_ref
B->>T: timestamp(SHA-256(merkle_root))
T-->>B: tsa_token_ref
B->>BC: anchor(SHA-256(merkle_root || tsa_token_ref))
BC-->>B: tx_hash confirmee
R->>B: controle SLA PENDING_SEAL
alt age > sealSla
R->>B: set SEAL_DELAYED
end
B->>N: push("Votre capture a ete scellee avec succes")
N-->>U: Notification recue [v3]
6. Cas d’erreur¶
| ID | Situation | Comportement attendu |
|---|---|---|
| ER-103-01 | Upload échoue (retryable) | Retry automatique; si échec final, UPLOADING -> UPLOAD_DEFERRED. |
| ER-103-02 | Capture annulée par l’utilisateur | CAPTURED -> CANCELLED; aucune persistance probatoire. |
| ER-103-03 | OCR échoue | Continuer le flux sans OCR; image et hash inchangés. |
| ER-103-04 | Réseau indisponible | Stockage local temporaire chiffré; UPLOAD_DEFERRED; reprise automatique. |
| ER-103-05 | Hash invalide ou mismatch | Rejet du flux, purge artefacts, pas d’upload. |
| ER-103-06 | Format payload invalide (§5.1) | HTTP 400, aucune transition vers PENDING_SEAL. |
| ER-103-07 | Échec chiffrement local | Rejet immédiat, purge, CANCELLED. |
| ER-103-08 | Garde PENDING_SEAL -> SEALED non satisfaite | État reste PENDING_SEAL; pas de passage à SEALED. |
| ER-103-09 | SLA scellement dépassé (>10min) | État reste PENDING_SEAL; SEAL_DELAYED positionné; notification “scellement en cours”; réconciliation. |
| ER-103-10 | Rate-limit dépassé | HTTP 429; aucun artefact probatoire créé. |
| ER-103-11 | Session upload différé expirée (TTL) | UPLOAD_DEFERRED -> CANCELLED + purge locale chiffrée obligatoire. |
| ER-103-12 | timestamp_device hors tolérance skew (> ±5 min) | HTTP 400 TIMESTAMP_SKEW_EXCEEDED; rejet sans création d’événement. |
| ER-103-13 | Rejeu capture_id | 200 idempotent si payload_canonical_sha256 identique; 409 Conflict si fingerprint divergent, sans doublon. |
| ER-103-14 | Intégrité ciphertext S3 invalide (checksum mismatch) | Rejet upload part/objet; retries; puis UPLOAD_DEFERRED si échec persistant. |
| ER-103-15 | Reprise différée sans session JWT valide | Pas de reprise; demande de refresh/réauth; état conservé UPLOAD_DEFERRED jusqu’à TTL. |
| ER-103-16 | Échec unwrap DEK (dek_wrapped_b64 corrompu/incompatible KEK keyring) | HTTP 422 UNWRAP_DEK_FAILED; aucune persistance capture_events; objet S3 traité ensuite par GC orphelins si présent. |
| ER-103-17 | Clé privée backend indisponible (HSM/KMS down) | HTTP 503 KEY_SERVICE_UNAVAILABLE; aucune persistance capture_events; retry backend/client autorisé. |
[v3]
7. Critères d’acceptation (testables)¶
| ID | Critère | Observable |
|---|---|---|
| CA-103-01 | Capture réalisée en < 3s (P95). | Mesure instrumentée trigger -> CAPTURED. |
| CA-103-02 | hash_sha3_256 calculé localement avant upload. | Traces locales horodatées + absence d’appel upload avant hash. |
| CA-103-03 | fichier soumis bit-à-bit identique au fichier original. | Comparaison hash local original vs soumis identique. |
| CA-103-04 | Aucune transformation destructrice appliquée. | Métadonnées image invariantes + hash stable. |
| CA-103-05 | OCR désactivable par l’utilisateur. | Toggle OCR modifie présence/absence payload OCR. |
| CA-103-06 | OCR reste local et non bloquant. | Aucun appel externe IA; échec OCR n’interrompt pas upload. |
| CA-103-07 | Upload automatique après validation utilisateur. | Aucun input manuel supplémentaire requis post-validation. |
| CA-103-08 | Flux upload respecte < 5s (P95) sur device de référence. | Mesure trigger -> UPLOADED. |
| CA-103-09 | Transition PENDING_SEAL -> SEALED requiert SIGNED + hsm_signature_ref. | Cas négatif bloque, cas nominal passe. |
| CA-103-10 | Notification push envoyée à SEALED. | Réception push iOS + statut app SEALED. |
| CA-103-11 | Inclusion preuve dans batch Merkle puis ancrage confirmé. | Présence merkle_proof_ref, tsa_token_ref, tx_hash. |
| CA-103-12 | Purge proactive purgeStale() au démarrage du flux. | Trace d’exécution avant capture/chiffrement. |
| CA-103-13 | Aucune donnée sensible en clair au repos serveur/local temporaire. | Audit stockage: seulement données chiffrées/wrappées. |
| CA-103-14 | ANCHOR_CONFIRMED et CANCELLED sont terminaux stricts. | Toute tentative de transition sortante est rejetée. |
| CA-103-15 | Le key exchange est complet: DEK généré localement, wrappé (dek_wrapped_b64) avec kek_id, et unwrappé backend via clé privée/keyring. | Présence de dek_wrapped_b64 + kek_id dans POST + trace backend d’unwrap sans persistance DEK clair. |
| CA-103-16 | Reprise UPLOAD_DEFERRED nécessite JWT valide + nouvelle URL pré-signée. | Traces montrent refresh URL avant reprise; ancienne URL expirée non réutilisée. |
| CA-103-17 | Le backend rejette les captures avec skew timestamp_device > ±5 min. | HTTP 400 TIMESTAMP_SKEW_EXCEEDED; aucune transition probatoire. |
| CA-103-18 | Contrat idempotence API appliqué: replay même capture_id => 200 idempotent ou 409 Conflict conforme fingerprint canonique. | Codes HTTP, payload_canonical_sha256, absence de doublon d’attestation. |
| CA-103-19 | Upload multipart (>10MB) supporte reprise partielle des chunks. | ETags partiels conservés + complétion réussie après reprise. |
| CA-103-20 | Annulation explicite en UPLOADING purge les artefacts en transit et annule l’upload S3. | Trace AbortMultipartUpload/annulation PUT + transition CANCELLED. |
| CA-103-21 | La forme canonique d’idempotence est déterministe (tri clés JSON, normalisation lowercase). | Recalcul backend stable payload_canonical_sha256 pour un même payload logique. |
| CA-103-22 | capture_id est toujours normalisé lowercase avant stockage/comparaison. | Base et logs n’exposent que des capture_id lowercase. |
| CA-103-23 | Le DEK est zéroïsé côté mobile juste après wrapping. | Vérification buffer 0x00 après wrapping (limitation GC documentée). |
| CA-103-24 | Les orphelins S3 sont détectés et supprimés après orphanTtl. | Logs de réconciliation + suppression effective des objets sans enregistrement backend. |
| CA-103-25 | Rotation KEK couverte: un payload ancien (kek_id historique) reste déchiffrable. | Unwrap réussi via keyring avec KEK historique. |
| CA-103-26 | SEAL_DELAYED est posé automatiquement après dépassement sealSla et levé après 3 cycles conformes. | Logs de flag + compteur persistant de cycles conformes. |
| CA-103-27 | Les captures locales chiffrées sont supprimées immédiatement après UPLOADED. | Absence d’artefact local après ACK backend. |
| CA-103-28 | Les erreurs d’unwrapping et d’indisponibilité clé sont distinguées (422 vs 503). | Codes HTTP et absence d’événement probatoire dans les deux cas. |
[v3]
8. Scénarios de test (Given / When / Then)¶
-
SCN-103-01 Capture nominale
Given OCR activé et réseau disponible; When l’utilisateur déclenche puis valide; ThenCAPTURED -> UPLOADING -> UPLOADEDavec hash local avant upload,dek_wrapped_b64+kek_idprésents dans POST. -
SCN-103-02 OCR désactivé
Given OCR désactivé; When capture validée; Then upload réussi sans champs OCR. -
SCN-103-03 OCR en erreur
Given OCR activé mais extraction échoue; When flux continue; Then preuve image est uploadée et statut final non bloqué par OCR. -
SCN-103-04 Réseau indisponible
Given réseau coupé enUPLOADING; When retries échouent; Then transition versUPLOAD_DEFERREDavec artefact local chiffré. -
SCN-103-05 Reprise différée
Given étatUPLOAD_DEFERRED; When réseau revient avant TTL avec JWT valide; Then redemande URL pré-signée et reprise automatiqueUPLOAD_DEFERRED -> UPLOADING -> UPLOADED. -
SCN-103-06 Expiration différé
GivenUPLOAD_DEFERREDau-delà TTL; When watchdog TTL s’exécute; Then transitionCANCELLEDet purge locale. -
SCN-103-07 Annulation utilisateur pré-upload
Given étatCAPTURED; When l’utilisateur annule; ThenCANCELLEDet aucune donnée probatoire persistée. -
SCN-103-08 Garde SEALED négative
GivenPENDING_SEALavecsignature_status='PENDING_SIGNATURE'; When tentative transition; Then refus explicite versSEALED. -
SCN-103-09 Garde SEALED positive
GivenPENDING_SEALavecsignature_status='SIGNED'ethsm_signature_refrenseigné; When worker valide; Then transitionSEALED. -
SCN-103-10 Notification de scellement
Given passage àSEALED; When événement backend émis; Then push iOS reçue et écran détail accessible. -
SCN-103-11 Intégrité bit-à-bit
Given image capturée; When comparaisonhash(original)vshash(submitted); Then égalité stricte. -
SCN-103-12 Rate-limit
Given dépassement quota minute; When nouvel appelPOST /documents/capture; Then réponse429et aucun nouvel enregistrement probatoire. -
SCN-103-13 Upload multipart nominal + reprise partielle
Givensize_bytes > 10MB; When un chunk échoue puis reprise; Then seules les parts manquantes sont rejouées, puisUPLOADING -> UPLOADED. -
SCN-103-14 Annulation en upload
Given étatUPLOADING(single ou multipart); When annulation explicite utilisateur; ThenUPLOADING -> CANCELLED+ purge + annulation upload S3. -
SCN-103-15 Validation skew timestamp
Giventimestamp_devicehors tolérance ±5 min; WhenPOST /documents/capture; Then HTTP 400 sans création d’événement. -
SCN-103-16 Idempotence replay identique
Given uncapture_iddéjà accepté; When même payload canonique est rejoué; Then200idempotent sans doublon d’attestation. -
SCN-103-17 Conflit duplicate capture_id
Given uncapture_idexistant; When payload canonique divergent est posté; Then409 Conflictsans modification d’attestation existante. -
SCN-103-18 Normalisation
capture_id
Given uncapture_idinitial lowercase; When replay avec même UUID en uppercase; Then backend normalise et applique la même décision idempotence. -
SCN-103-19 Échec unwrap DEK
Givendek_wrapped_b64corrompu/incompatible; WhenPOST /documents/capture; Then HTTP422 UNWRAP_DEK_FAILED. -
SCN-103-20 Clé privée indisponible
Given HSM/KMS indisponible; WhenPOST /documents/capture; Then HTTP503 KEY_SERVICE_UNAVAILABLE. -
SCN-103-21 Trigger
SEAL_DELAYED
Given capturePENDING_SEALau-delàsealSla; When job de réconciliation passe; ThenSEAL_DELAYEDest positionné. -
SCN-103-22 Clearing
SEAL_DELAYED
GivenSEAL_DELAYEDactif; When 3 cycles conformes consécutifs sont observés; Then le flag est levé. -
SCN-103-23 Orphelin S3
Given objet S3 sans événement backend et âge >orphanTtl; When réconciliation s’exécute; Then objet supprimé et audit journalisé. -
SCN-103-24 Rotation KEK
Given capture chiffrée aveckek_idhistorique; When backend a roté vers une nouvelle KEK; Then unwrap réussi via keyring historique. -
SCN-103-25 Purge locale post-upload
Given capture passéeUPLOADED; When ACK backend reçu; Then payload local chiffré supprimé immédiatement, hors TTL différé.
[v3]
9. Hypothèses explicites¶
| ID | Hypothèse | Impact si faux |
|---|---|---|
| H-103-01 | Epic de rattachement: MOBILE-IOS (PD-195). | Référence documentaire à corriger. |
| H-103-02 | Device de référence perf: iPhone 12 (A14), iOS 16+. | Les seuils P95 doivent être recalibrés. |
| H-103-03 | Endpoint POST /documents/capture disponible avec contrat §5.12 (202/200/409/400/422/429/503). | Flux backend bloqué/ambigu. |
| H-103-04 | Pipeline de scellement backend (Merkle/TSA/HSM/blockchain) est opérationnel via dépendances PD-56/PD-55/PD-41. | Impossible d’atteindre SEALED/ANCHOR_CONFIRMED. |
| H-103-05 | Rate-limit initial 60 req/min/utilisateur acceptable métier. | Ajuster quota et scénarios de charge. |
| H-103-06 | Fenêtre de dédup idempotence 24h acceptable. | Risque doublons ou rejets légitimes à recalibrer. |
| H-103-07 | TTL upload différé 24h conforme politique produit. | Ajuster SLA rétention locale en UPLOAD_DEFERRED. |
| H-103-08 | OCR confidence normalisée dans [0,1] par moteur local. | Adapter validation format §5.1. |
| H-103-09 | La clé publique backend (KEK) et son kek_id sont distribués au mobile de manière authentique et rotatable. | Risque crypto et indisponibilité wrapping DEK. |
| H-103-10 | TTL URL pré-signée S3 est court (1 à 15 min) et inférieur au TTL différé. | Reprise impossible sans redemande URL. |
| H-103-11 | Les dépendances crypto React Native requises (§10.1) sont déployables en build mobile cible (EAS/dev client). | Impossibilité de respecter SHA3-256 et RSA-OAEP-SHA256 côté mobile. |
[v3]
10. Contraintes techniques et points à clarifier¶
10.1 Contraintes techniques (stack réelle, obligatoire)¶
| Composant | Stack contractuelle |
|---|---|
Projet cible principal ProbatioVault-app | React Native + Expo SDK 54 + TypeScript |
Backend intégré ProbatioVault-backend | NestJS + TypeORM + PostgreSQL |
| Infra dépendante (stockage/ops) | Terraform + Ansible + Shell (ProbatioVault-infra) |
| Crypto mobile obligatoire | react-native-quick-crypto (SHA3-256, AES-256-GCM, RSA-OAEP-SHA256) + react-native-get-random-values (CSPRNG) |
| Environnement Expo | Dev Client/EAS Build requis (fonctionnalités crypto non garanties dans Expo Go standard) |
| Transport upload | HTTPS/TLS obligatoire pour URL pré-signées S3 (token bearer protégé par TLS) |
Règle explicite : aucune mention Swift/SwiftUI dans cette story; la couche mobile est contractuellement React Native/Expo.
[v3]
10.2 Points à clarifier¶
| ID | Point à clarifier | Impact |
|---|---|---|
| Q-103-01 | Valeur officielle de l’epic (Référence épique fournie vide). | Traçabilité documentaire incomplète. |
| Q-103-02 | Liste officielle des devices/perf tiers pour validation P95. | Résultats perf non comparables inter-environnements. |
| Q-103-03 | LEVÉ en v2: contrat réponse POST /documents/capture fixé en §5.12 (202/200/409/400/429). | Ambiguïté levée. |
| Q-103-04 | Politique de notification à ANCHOR_CONFIRMED (oui/non, contenu). | Comportement UX final non figé. |
| Q-103-05 | Whitelist OCR langues et limite finale ocr_text validées PO/juridique. | Risque de non-conformité privacy/indexation. |
| Q-103-06 | Quota rate-limit définitif pour captures massives légitimes. | Risque blocage usage intensif ou protection insuffisante. |
| Q-103-08 | LEVÉ en v3: rotation/révocation KEK cadrée par kek_id + keyring + codes 422/503 (§5.11, §5.12). | Ambiguïté crypto levée. |
[v3]
Références¶
- Epic :
MOBILE-IOS(PD-195, à confirmer PO) - JIRA :
PD-103 - Repos concernés :
ProbatioVault-app,ProbatioVault-backend,ProbatioVault-infra - Documents associés : PD-101, PD-248, PD-251, PD-55, PD-56, PD-105, PD-283, PD-262
Annexe A — Traçabilité Gate 3 v1 → v2 → v3¶
| Écart Gate 3 | Correctif spécification |
|---|---|
| ECT-01 | §3 (définitions K_doc/DEK, KEK, dek_wrapped_b64), §5.4 (génération/chiffrement/wrapping), INV-103-30, §5.12 |
| ECT-02 | §5.1 (dek_wrapped_b64), §5.5 (payload POST enrichi), §5.12 (contrat API + unwrapping backend) |
| ECT-03 | §3 (session valide = JWT), INV-103-24, §5.5 (redemande URL pré-signée), §5.2 (TTL URL S3) |
| DIV-01 | §5.1 regex timestamp_device corrigée (?:\.\d{1,6})? |
| DIV-02 | §5.5bis nouveau sous-flux multipart complet |
| DIV-03 | INV-103-29 ajouté + §5.5bis + §5.7 comportement annulation upload |
| DIV-04 | §5bis diagramme de séquence corrigé (étapes UPLOADED puis PENDING_SEAL) |
| DIV-05 | INV-103-09 précisé (RSA-OAEP, stockage DEK wrappé, jamais clair en base) |
| DIV-06 | §5.6 (moment exact statut probatoire à UPLOADED -> PENDING_SEAL) |
| DIV-07 | §5.1 (signature_status localisé en colonne capture_events, init PENDING_SIGNATURE) |
| DIV-08 | Tests v2 section invariants convertie en GIVEN/WHEN/THEN (cf. document tests) |
| DIV-09 | INV-103-06 + §5.5 + §5.5bis (checksum Content-MD5/x-amz-content-sha256) |
| DIV-10 | §5.1 + §5.2 + §6 (skew ±5 min, rejet 400) |
| DIV-11 | §5.12 contrat API (409 Conflict + 200 idempotent), §5.5, §6 ER-103-13 |
| ECT-01-v2 | Définition normative du payload canonique d’idempotence en §3 + §5.12; fingerprint payload_canonical_sha256 stocké en base; normalisation lowercase + tri clés JSON. |
| ECT-11-v2 | Gestion explicite échecs unwrap en §5.5 + §6 (422 UNWRAP_DEK_FAILED, 503 KEY_SERVICE_UNAVAILABLE). |
| ECT-19-v2 | Politique locale post-upload résolue en Option A: purge immédiate après UPLOADED; TTL réservé à UPLOAD_DEFERRED (§5.3, §5.5, §5.8). |
| ECT-07-v2 | Contraintes crypto RN/Expo explicitées en §10.1 (dépendances obligatoires). |
| ECT-05-v2 | Limitation effacement mémoire JS documentée + zéroïsage buffer obligatoire (§5.4, INV-103-32). |
| ECT-06-v2 | GC orphelins S3 ajouté en §5.9 + INV-103-33. |
| ECT-08-v2 | Définition formelle de “cycle conforme” en §3 et §5.9 + clearing après 3 cycles. |
| ECT-02-v2 | ReKey retiré de INV-103-09. |
| ECT-03-v2 | capture_id normalisé lowercase en §5.1 + INV-103-31 + idempotence §5.12. |
| ECT-13-v2 | Exigence test zéroïsage DEK ajoutée via CA-103-23 (tests dédiés). |
| ECT-17-v2 | Rotation KEK contractualisée (§5.11), kek_id ajouté au payload (§5.1, §5.12), invariant INV-103-34. |
| ECT-09-v2 | Déclenchement SEAL_DELAYED après dépassement sealSla défini en §5.9 + INV-103-35. |
[v3]