Aller au contenu

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, et notification utilisateur à l’état SEALED.

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-256 avant tout upload.
  • OCR optionnel local uniquement (Apple Vision), sans valeur probatoire.
  • Chiffrement local AES-256-GCM file-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.

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).

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.
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.
dek_wrapped_b64 DEK wrappé avec KEK backend, encodé Base64, transmis dans POST /documents/capture.
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.
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.

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, ReKey) 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.

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-] insensitive hex ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$ 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 longueur max trunc interdit, rejet > max
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
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ègle de référence unique : toute section ultérieure (6, 7, 8, 10) réutilise ces formats sans redéfinition.

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 0 900 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

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, notification de retard, réconciliation active
deferredUploadTtl (UPLOAD_DEFERRED) 24h 1h 24h oui (config upload) transition forcée CANCELLED + purge locale chiffrée

5.4 Flux nominal A — Capture et préparation locale

  1. L’utilisateur ouvre l’outil Capture probatoire.
  2. L’utilisateur déclenche la capture applicative.
  3. Le système produit image_original_bytes (PNG) et timestamp_device.
  4. Le système calcule hash_sha3_256 = SHA3-256(image_original_bytes).
  5. Le système construit les métadonnées (capture_id, device_id, app_version, mime_type, size_bytes).
  6. Si OCR activé, extraction locale ocr_text, ocr_confidence, ocr_language; en cas d’échec OCR, continuer sans OCR.
  7. Le système génère un DEK aléatoire 256 bits (K_doc) via CSPRNG.
  8. Le système génère un nonce 96 bits unique pour la capture.
  9. Le système chiffre le fichier en local (AES-256-GCM file-level) avec DEK et nonce, puis obtient ciphertext + tag.
  10. Le système wrappe DEK avec la clé publique backend (KEK, RSA-OAEP-SHA256) et produit dek_wrapped_b64.
  11. Le système prépare les artefacts ciphertext + nonce + tag + dek_wrapped_b64 et efface DEK en clair de la mémoire applicative après succès.

5.5 Flux nominal B — Upload et reprise différée

  1. Le système demande une URL pré-signée d’upload.
  2. Le système envoie uniquement le payload chiffré (ciphertext), jamais le clair.
  3. L’upload S3 inclut un contrôle d’intégrité du ciphertext (Content-MD5 ou x-amz-content-sha256).
  4. Le système appelle POST /documents/capture avec 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.
  5. Le backend unwrappe DEK via sa clé privée (usage mémoire volatile uniquement), initialise capture_events.signature_status='PENDING_SIGNATURE', puis persiste l’événement.
  6. Sur succès réseau + ACK backend, transition UPLOADING -> UPLOADED.
  7. En cas d’échec retryable ou réseau indisponible, transition UPLOADING -> UPLOAD_DEFERRED.
  8. Le payload différé reste stocké localement chiffré jusqu’à reprise.
  9. À retour réseau, la reprise exige une session valide (JWT auth valide) et la redemande d’une nouvelle URL pré-signée avant UPLOAD_DEFERRED -> UPLOADING.
  10. Si session JWT invalide/expirée, pas de reprise; réauthentification/refresh requis.
  11. À expiration TTL différé, transition UPLOAD_DEFERRED -> CANCELLED puis purge.
  12. En cas de replay même capture_id, le backend répond 200 idempotent (payload canonique identique) ou 409 Conflict (payload divergent/politique stricte), sans doublon d’attestation.

5.5bis Flux nominal B2 — Upload multipart

  1. Si size_bytes > 10_000_000, le système bascule en upload multipart.
  2. Le backend fournit upload_id + URLs pré-signées par part.
  3. Le ciphertext est découpé en chunks (chunk_size contractuel, défaut 5_242_880 bytes).
  4. Chaque part est uploadée avec contrôle d’intégrité (Content-MD5 ou x-amz-content-sha256) et retries bornés.
  5. 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.
  6. À 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_id expiré/invalide).
  7. UPLOADING -> UPLOADED n’est autorisée qu’après CompleteMultipartUpload confirmé S3 + ACK backend.
  8. Sur annulation utilisateur explicite en UPLOADING, l’application annule la session multipart S3 (AbortMultipartUpload) puis purge le ciphertext local/in-transit.

5.6 Flux nominal C — Scellement et notification

  1. Le backend persiste l’événement probatoire (capture_events + journal probatoire append-only); ce commit déclenche UPLOADED -> PENDING_SEAL et le fichier acquiert le statut fichier probatoire.
  2. Pipeline de scellement exécute Merkle, signature HSM, timestamp TSA et ancrage blockchain.
  3. Garde de transition : signature_status='SIGNED' ET hsm_signature_ref != NULL.
  4. Si garde satisfaite, transition PENDING_SEAL -> SEALED.
  5. Après confirmation d’ancrage, transition SEALED -> ANCHOR_CONFIRMED.
  6. À 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] Le modèle d’états est couvert par les invariants INV-103-07 et INV-103-20..30.

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
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
Crash pré-commit rollback, aucun artefact persistant
Crash post-commit DB valide, reprise asynchrone par réconciliation

5.9 Mécanismes de protection distribuée

Mécanisme Contrat PD-103
Lock distribué Scope capture_id (ou batch d’ancrage). TTL défaut 300s, min 60s, max 900s. Si lock non acquis: pas de transition d’état, retry planifié après 30s.
Idempotence Clé capture_id. Rejeu même clé + payload canonique identique => 200 idempotent sans doublon d’attestation. Payload divergent (ou politique stricte backend) => 409 Conflict. Fenêtre de dédup défaut 24h, min 1h, max 168h.
Réconciliation Cron défaut 10min, min 8min, max 12min; seuil orphelin défaut 15min, min 5min, max 60min; stratégie scan non-terminaux puis re-enqueue.
Rate-limiting Granularité user_id + IP; quota défaut 60 req/min, min 1, max 300; dépassement => 429.
Clearing conditionnel Flag SEAL_DELAYED levé après 3 cycles conformes consécutifs (min 1, max 10) via compteur persistant.

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. - [x] Réconciliation: interval cron + seuil orphelin + rattrapage définis. - [x] Rate-limit: granularité + quota + comportement dépassement définis. - [x] Clearing: nombre de cycles conformes + mécanisme de comptage définis.

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 contrainte inter-module applicable (aucune route d’un autre module n’est bloquée ou modifiée par PD-103).

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, 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 + payload canonique identique (aucun doublon d’attestation)
Sémantique conflit 409 Conflict si même capture_id avec payload divergent ou politique backend stricte de duplication
Erreurs contrat 400 validation/format/skew timestamp, 401/403 auth, 429 rate-limit
Effet backend Initialisation capture_events.signature_status='PENDING_SIGNATURE' à l’ingestion, puis pipeline de scellement

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 : 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 CAPTURED
      Autorisees: UPLOADING, CANCELLED
      Interdites: UPLOADED, UPLOAD_DEFERRED, PENDING_SEAL, SEALED, ANCHOR_CONFIRMED
    end note

    note right of UPLOAD_DEFERRED
      Reprise autorisee seulement si:
      reseau restaure + JWT valide + nouvelle URL pre-signee
    end note

    note right of PENDING_SEAL
      Garde obligatoire: signature_status='SIGNED'
      ET hsm_signature_ref != NULL
    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 M as Worker Merkle
    participant H as HSM
    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)
    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

    A->>S3: PUT(ciphertext, Content-MD5|x-amz-content-sha256)
    S3-->>A: 200 OK

    A->>B: POST /documents/capture {capture_id, hash_sha3_256, metadata, ocr?, nonce_b64, tag_b64, dek_wrapped_b64}
    B-->>A: 202 Accepted (ACK ingestion)
    A->>A: Transition UPLOADING -> UPLOADED

    B->>B: unwrap(DEK) via cle privee backend (memoire volatile)
    B->>B: insert capture_events(signature_status='PENDING_SIGNATURE')
    B->>B: append journal probatoire
    B->>B: Transition UPLOADED -> PENDING_SEAL

    B->>M: append leaf = hash(payload_canonique)
    M-->>B: merkle_root

    B->>H: sign(merkle_root)
    H-->>B: signature_status=SIGNED + hsm_signature_ref

    B->>T: timestamp(hash(merkle_root))
    T-->>B: tsa_token_ref

    B->>BC: anchor(hash(merkle_root || tsa_token_ref))
    BC-->>B: tx_hash confirmee

    B-->>A: status=PENDING_SEAL puis status=SEALED puis status=ANCHOR_CONFIRMED
    B->>N: push("Votre capture a ete scellee avec succes")
    N-->>U: Notification recue

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; 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 canonique identique; 409 Conflict si payload divergent/politique stricte, 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.

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) et unwrappé backend via clé privée. Présence de dek_wrapped_b64 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 politique. Codes HTTP et 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.

8. Scénarios de test (Given / When / Then)

  1. SCN-103-01 Capture nominale
    Given OCR activé et réseau disponible; When l’utilisateur déclenche puis valide; Then CAPTURED -> UPLOADING -> UPLOADED avec hash local avant upload et dek_wrapped_b64 présent dans POST.

  2. SCN-103-02 OCR désactivé
    Given OCR désactivé; When capture validée; Then upload réussi sans champs OCR.

  3. 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.

  4. SCN-103-04 Réseau indisponible
    Given réseau coupé en UPLOADING; When retries échouent; Then transition vers UPLOAD_DEFERRED avec artefact local chiffré.

  5. SCN-103-05 Reprise différée
    Given état UPLOAD_DEFERRED; When réseau revient avant TTL avec JWT valide; Then redemande URL pré-signée et reprise automatique UPLOAD_DEFERRED -> UPLOADING -> UPLOADED.

  6. SCN-103-06 Expiration différé
    Given UPLOAD_DEFERRED au-delà TTL; When watchdog TTL s’exécute; Then transition CANCELLED et purge locale.

  7. SCN-103-07 Annulation utilisateur pré-upload
    Given état CAPTURED; When l’utilisateur annule; Then CANCELLED et aucune donnée probatoire persistée.

  8. SCN-103-08 Garde SEALED négative
    Given PENDING_SEAL avec signature_status='PENDING_SIGNATURE'; When tentative transition; Then refus explicite vers SEALED.

  9. SCN-103-09 Garde SEALED positive
    Given PENDING_SEAL avec signature_status='SIGNED' et hsm_signature_ref renseigné; When worker valide; Then transition SEALED.

  10. SCN-103-10 Notification de scellement
    Given passage à SEALED; When événement backend émis; Then push iOS reçue et écran détail accessible.

  11. SCN-103-11 Intégrité bit-à-bit
    Given image capturée; When comparaison hash(original) vs hash(submitted); Then égalité stricte.

  12. SCN-103-12 Rate-limit
    Given dépassement quota minute; When nouvel appel POST /documents/capture; Then réponse 429 et aucun nouvel enregistrement probatoire.

  13. SCN-103-13 Upload multipart nominal + reprise partielle
    Given size_bytes > 10MB; When un chunk échoue puis reprise; Then seules les parts manquantes sont rejouées, puis UPLOADING -> UPLOADED.

  14. SCN-103-14 Annulation en upload
    Given état UPLOADING (single ou multipart); When annulation explicite utilisateur; Then UPLOADING -> CANCELLED + purge + annulation upload S3.

  15. SCN-103-15 Validation skew timestamp
    Given timestamp_device hors tolérance ±5 min; When POST /documents/capture; Then HTTP 400 sans création d’événement.

  16. SCN-103-16 Idempotence replay identique
    Given un capture_id déjà accepté; When même payload canonique est rejoué; Then 200 idempotent sans doublon d’attestation.

  17. SCN-103-17 Conflit duplicate capture_id
    Given un capture_id existant; When payload divergent est posté; Then 409 Conflict sans modification d’attestation existante.

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/429). 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. Données locales potentiellement conservées trop longtemps/pas assez.
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) est distribuée 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.

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)

Règle explicite : aucune mention Swift/SwiftUI dans cette story; la couche mobile est contractuellement React Native/Expo.

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-07 Politique légale de conservation locale en mode différé au-delà du TTL. Risque conformité rétention locale.
Q-103-08 Politique de rotation/révocation de la clé publique backend (KEK) côté mobile. Risque indisponibilité ou compromission crypto si non cadré.

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

É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

PD-103-tests.md (v2)

PD-103 — Scénarios de tests contractuels

1. Références

  • Spécification : PD-103-specification.md
  • Epic : EPIC-XX (MOBILE-IOS / PD-195 à confirmer PO)

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 TC-INV-05, TC-INV-06, TC-INV-07, TC-INV-08, TC-ERR-10, TC-ERR-13 Oui Lock/idempotence/réconciliation/rate-limit/clearing.
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.
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-18 TC-NOM-15, TC-ERR-13 Oui Contrat 200 idempotent ou 409 Conflict.
N/A CA-103-19 TC-NOM-13 Oui Multipart + reprise partielle.

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, 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) 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 DEK est wrappé en `dek_wrapped_b64` via RSA-OAEP-SHA256
  - Le PUT S3 transporte uniquement le `ciphertext` avec `Content-MD5` ou `x-amz-content-sha256`
  - Le POST `/documents/capture` contient `nonce_b64`, `tag_b64`, `dek_wrapped_b64`
  - 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, CA-103-18

GIVEN
  - Une capture `capture_id=X` déjà acceptée
  - Rejeu du même `capture_id=X` avec payload canonique identique
WHEN
  - `POST /documents/capture` est rejoué dans la fenêtre de déduplication
THEN
  - Réponse `200 OK` idempotente
  - Aucun doublon d’attestation probatoire
AND
  - L’état métier retourné est cohérent avec l’existant

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` ou `dek_wrapped_b64` 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, §5.3

GIVEN
  - Capture en `PENDING_SEAL` depuis plus de 10 minutes
WHEN
  - Le contrôle SLA de scellement s’exécute
THEN
  - L’état reste `PENDING_SEAL`
  - Une notification "scellement en cours" est émise
  - La réconciliation est planifiée/active
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, §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, §5.12, CA-103-18

GIVEN
  - Une capture `capture_id=X` déjà enregistrée
  - Nouveau POST avec `capture_id=X` mais payload canonique divergent
WHEN
  - `POST /documents/capture` est exécuté
THEN
  - HTTP 409 Conflict (ou politique stricte équivalente)
  - 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

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

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 DEK est wrappé en RSA-OAEP-SHA256 puis transmis via `dek_wrapped_b64`
  - 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-11 (idempotence), §5.12, INV-103-24

GIVEN
  - Un `capture_id` déjà traité
WHEN
  - Cas A: replay du payload canonique identique
  - Cas B: replay divergent
THEN
  - Cas A: réponse idempotente (`200`) sans doublon
  - Cas B: `409 Conflict` sans corruption d’état
  - La reprise différée requiert toujours JWT valide + nouvelle URL pré-signée
TEST-ID: TC-INV-07
Référence spec: INV-103-11 (réconciliation)

GIVEN
  - Captures non terminales orphelines injectées
WHEN
  - Le job de réconciliation périodique s’exécute
THEN
  - Les éléments orphelins 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-11 (clearing conditionnel)

GIVEN
  - Flag `SEAL_DELAYED` actif
WHEN
  - Des cycles de contrôle conformes consécutifs sont exécutés
THEN
  - Le flag est levé uniquement après le nombre contractuel de cycles conformes
  - Le compteur persistant reflète strictement les cycles exécutés
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

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 + ECT-03.
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 toujours présent, DEK jamais en clair en base Cible régressions crypto ECT-01/02.
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.

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 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 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.

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/429, corps de réponse minimalement contractuel, corrélation capture_id.
  • Journal d’audit : traces immuables pour capture, upload, garde de scellement, transitions refusées, purge.
  • É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 (DEK uniquement wrappé).
  • Observabilité reprise différée : trace de redemande URL pré-signée et validation JWT avant reprise.
  • Observabilité backend : capture_events.signature_status initialisé PENDING_SIGNATURE à l’ingestion.

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
Politique légale de conservation locale au-delà TTL (Q-103-07) Exigence conformité juridique non arrêtée Bloquant

10. Verdict QA

  • ✅ Testable contractuellement (avec réserves documentaires non fonctionnelles listées en §9)

Annexe A — Traçabilité Gate 3 v1 → v2 (tests)

Écart Gate 3 Couverture tests v2
ECT-01 TC-NOM-01, TC-INV-03, TC-NR-07
ECT-02 TC-NOM-01, TC-ERR-06, TC-NEG-11
ECT-03 TC-NOM-06, TC-ERR-15, TC-NR-03
DIV-01 TC-ERR-06, TC-NEG-09
DIV-02 TC-NOM-13, TC-NR-08
DIV-03 TC-NOM-14, TC-INV-09
DIV-04 Couvert côté spec (diagramme séquence), impacts indirects via TC-NOM-07/08
DIV-05 TC-INV-03, TC-NR-07
DIV-06 TC-NOM-07
DIV-07 TC-NOM-07, TC-INV-03, observabilité §8
DIV-08 TC-INV-01..10 tous en GIVEN/WHEN/THEN explicite
DIV-09 TC-NOM-01, TC-ERR-14, TC-NEG-12
DIV-10 TC-ERR-12, TC-NEG-10, TC-NR-09
DIV-11 TC-NOM-15, TC-ERR-13, matrice §2 (200/409)

codex

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-256 avant tout upload.
  • OCR optionnel local uniquement (Apple Vision), sans valeur probatoire.
  • Chiffrement local AES-256-GCM file-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 (KEK courante + historiques) et champ kek_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

  1. L’utilisateur ouvre l’outil Capture probatoire.
  2. L’utilisateur déclenche la capture applicative.
  3. Le système produit image_original_bytes (PNG) et timestamp_device.
  4. Le système calcule hash_sha3_256 = SHA3-256(image_original_bytes).
  5. Le système construit les métadonnées (capture_id, device_id, app_version, mime_type, size_bytes) en normalisant capture_id en lowercase.
  6. Si OCR activé, extraction locale ocr_text, ocr_confidence, ocr_language; en cas d’échec OCR, continuer sans OCR.
  7. Le système génère un DEK aléatoire 256 bits (K_doc) via CSPRNG.
  8. Le système génère un nonce 96 bits unique via CSPRNG pour la capture.
  9. Le système chiffre le fichier en local (AES-256-GCM file-level) avec DEK et nonce, puis obtient ciphertext + tag.
  10. Le système wrappe DEK avec la clé publique backend (KEK, RSA-OAEP-SHA256), récupère le kek_id actif correspondant, et produit dek_wrapped_b64.
  11. 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).
  12. Le système prépare les artefacts ciphertext + nonce + tag + dek_wrapped_b64 + kek_id pour upload ou différé.

[v3]

5.5 Flux nominal B — Upload et reprise différée

  1. Le système demande une URL pré-signée d’upload.
  2. Le système envoie uniquement le payload chiffré (ciphertext), jamais le clair.
  3. L’upload S3 inclut un contrôle d’intégrité du ciphertext (Content-MD5 ou x-amz-content-sha256).
  4. Le système appelle POST /documents/capture avec 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.
  5. Le backend tente l’unwrap DEK via keyring KEK (KEK courante puis anciennes, avec priorité de recherche sur kek_id si présent), initialise capture_events.signature_status='PENDING_SIGNATURE', puis persiste l’événement.
  6. Si dek_wrapped_b64 est invalide/incompatible (clé non retrouvée ou échec cryptographique) : HTTP 422 (UNWRAP_DEK_FAILED), aucune persistance d’événement.
  7. Si service de clé (HSM/KMS/clé privée) indisponible : HTTP 503 (KEY_SERVICE_UNAVAILABLE), aucune persistance d’événement.
  8. Sur succès réseau + ACK backend, transition UPLOADING -> UPLOADED.
  9. Immédiatement après UPLOADED, suppression locale du payload chiffré (non conservation post-upload).
  10. En cas d’échec retryable ou réseau indisponible, transition UPLOADING -> UPLOAD_DEFERRED.
  11. En UPLOAD_DEFERRED, le payload reste stocké localement chiffré jusqu’à reprise ou expiration TTL.
  12. À retour réseau, la reprise exige une session valide (JWT auth valide) et la redemande d’une nouvelle URL pré-signée avant UPLOAD_DEFERRED -> UPLOADING.
  13. Si session JWT invalide/expirée, pas de reprise; réauthentification/refresh requis.
  14. À expiration TTL différé, transition UPLOAD_DEFERRED -> CANCELLED puis purge locale chiffrée.
  15. En cas de replay même capture_id normalisé, le backend répond 200 idempotent si payload_canonical_sha256 identique, sinon 409 Conflict si fingerprint divergent.
  16. 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

  1. Si size_bytes > 10_000_000, le système bascule en upload multipart.
  2. Le backend fournit upload_id + URLs pré-signées par part.
  3. Le ciphertext est découpé en chunks (chunk_size contractuel, défaut 5_242_880 bytes).
  4. Chaque part est uploadée avec contrôle d’intégrité (Content-MD5 ou x-amz-content-sha256) et retries bornés.
  5. 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.
  6. À 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_id expiré/invalide).
  7. UPLOADING -> UPLOADED n’est autorisée qu’après CompleteMultipartUpload confirmé S3 + ACK backend.
  8. Sur annulation utilisateur explicite en UPLOADING, l’application annule la session multipart S3 (AbortMultipartUpload) puis purge le ciphertext local/in-transit.
  9. 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

  1. Le backend persiste l’événement probatoire (capture_events + journal probatoire append-only); ce commit déclenche UPLOADED -> PENDING_SEAL et le fichier acquiert le statut fichier probatoire.
  2. Pipeline de scellement exécute Merkle, signature HSM, timestamp TSA et ancrage blockchain.
  3. Garde de transition : signature_status='SIGNED' ET hsm_signature_ref != NULL.
  4. Si garde satisfaite, transition PENDING_SEAL -> SEALED.
  5. Après confirmation d’ancrage, transition SEALED -> ANCHOR_CONFIRMED.
  6. À 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)

  1. Le backend expose la clé publique active et son kek_id signé/versionné.
  2. Le mobile wrappe le DEK avec cette clé active et transmet obligatoirement kek_id.
  3. Le backend maintient un keyring : KEK courante + N anciennes KEK (configurée en §5.2).
  4. À l’ingestion, l’unwrap tente la KEK courante puis les anciennes, avec priorisation de la clé correspondant à kek_id quand disponible.
  5. Si aucune clé ne permet l’unwrap: 422 UNWRAP_DEK_FAILED.
  6. Si la clé privée n’est pas accessible (HSM/KMS indisponible): 503 KEY_SERVICE_UNAVAILABLE.
  7. Les anciennes KEK sont conservées au minimum pendant deferredUploadTtl + dedupWindow pour couvrir les captures en UPLOAD_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)

  1. SCN-103-01 Capture nominale
    Given OCR activé et réseau disponible; When l’utilisateur déclenche puis valide; Then CAPTURED -> UPLOADING -> UPLOADED avec hash local avant upload, dek_wrapped_b64 + kek_id présents dans POST.

  2. SCN-103-02 OCR désactivé
    Given OCR désactivé; When capture validée; Then upload réussi sans champs OCR.

  3. 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.

  4. SCN-103-04 Réseau indisponible
    Given réseau coupé en UPLOADING; When retries échouent; Then transition vers UPLOAD_DEFERRED avec artefact local chiffré.

  5. SCN-103-05 Reprise différée
    Given état UPLOAD_DEFERRED; When réseau revient avant TTL avec JWT valide; Then redemande URL pré-signée et reprise automatique UPLOAD_DEFERRED -> UPLOADING -> UPLOADED.

  6. SCN-103-06 Expiration différé
    Given UPLOAD_DEFERRED au-delà TTL; When watchdog TTL s’exécute; Then transition CANCELLED et purge locale.

  7. SCN-103-07 Annulation utilisateur pré-upload
    Given état CAPTURED; When l’utilisateur annule; Then CANCELLED et aucune donnée probatoire persistée.

  8. SCN-103-08 Garde SEALED négative
    Given PENDING_SEAL avec signature_status='PENDING_SIGNATURE'; When tentative transition; Then refus explicite vers SEALED.

  9. SCN-103-09 Garde SEALED positive
    Given PENDING_SEAL avec signature_status='SIGNED' et hsm_signature_ref renseigné; When worker valide; Then transition SEALED.

  10. SCN-103-10 Notification de scellement
    Given passage à SEALED; When événement backend émis; Then push iOS reçue et écran détail accessible.

  11. SCN-103-11 Intégrité bit-à-bit
    Given image capturée; When comparaison hash(original) vs hash(submitted); Then égalité stricte.

  12. SCN-103-12 Rate-limit
    Given dépassement quota minute; When nouvel appel POST /documents/capture; Then réponse 429 et aucun nouvel enregistrement probatoire.

  13. SCN-103-13 Upload multipart nominal + reprise partielle
    Given size_bytes > 10MB; When un chunk échoue puis reprise; Then seules les parts manquantes sont rejouées, puis UPLOADING -> UPLOADED.

  14. SCN-103-14 Annulation en upload
    Given état UPLOADING (single ou multipart); When annulation explicite utilisateur; Then UPLOADING -> CANCELLED + purge + annulation upload S3.

  15. SCN-103-15 Validation skew timestamp
    Given timestamp_device hors tolérance ±5 min; When POST /documents/capture; Then HTTP 400 sans création d’événement.

  16. SCN-103-16 Idempotence replay identique
    Given un capture_id déjà accepté; When même payload canonique est rejoué; Then 200 idempotent sans doublon d’attestation.

  17. SCN-103-17 Conflit duplicate capture_id
    Given un capture_id existant; When payload canonique divergent est posté; Then 409 Conflict sans modification d’attestation existante.

  18. SCN-103-18 Normalisation capture_id
    Given un capture_id initial lowercase; When replay avec même UUID en uppercase; Then backend normalise et applique la même décision idempotence.

  19. SCN-103-19 Échec unwrap DEK
    Given dek_wrapped_b64 corrompu/incompatible; When POST /documents/capture; Then HTTP 422 UNWRAP_DEK_FAILED.

  20. SCN-103-20 Clé privée indisponible
    Given HSM/KMS indisponible; When POST /documents/capture; Then HTTP 503 KEY_SERVICE_UNAVAILABLE.

  21. SCN-103-21 Trigger SEAL_DELAYED
    Given capture PENDING_SEAL au-delà sealSla; When job de réconciliation passe; Then SEAL_DELAYED est positionné.

  22. SCN-103-22 Clearing SEAL_DELAYED
    Given SEAL_DELAYED actif; When 3 cycles conformes consécutifs sont observés; Then le flag est levé.

  23. 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é.

  24. SCN-103-24 Rotation KEK
    Given capture chiffrée avec kek_id historique; When backend a roté vers une nouvelle KEK; Then unwrap réussi via keyring historique.

  25. SCN-103-25 Purge locale post-upload
    Given capture passée UPLOADED; 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]