Sécurisation (traçabilité)
**************************

Présentation
------------

|	*Package parent :* **fr.gouv.vitam.logbook**
|	*Packages concernés :* **fr.gouv.vitam.logbook.common.traceability**, **fr.gouv.vitam.logbook.administration.core**, **fr.gouv.vitam.worker.core.plugin.lfc_traceability**

La sécurisation fige périodiquement le contenu des journaux dans un fichier horodaté, stocké sur
les offres. Elle permet de démontrer a posteriori qu'aucune entrée n'a été modifiée ni
supprimée. Trois périmètres sont sécurisés indépendamment :

- le journal des opérations (``TraceabilityType.OPERATION``) ;
- le journal du cycle de vie des unités archivistiques (``TraceabilityType.UNIT_LIFECYCLE``) ;
- le journal du cycle de vie des groupes d'objets (``TraceabilityType.OBJECTGROUP_LIFECYCLE``).

Le journal des écritures de stockage (``TraceabilityType.STORAGE``) suit le même algorithme
générique, mais il est porté par le module *storage-engine-server*
(**StorageTraceabilityAdministration**, **LogbookStorageTraceabilityHelper**). Il n'est pas
décrit ici : se référer à la documentation du module *storage*.

Modules impliqués :

- **common-private** : briques cryptographiques transverses, arbre de Merkle
  (``security.merkletree``) et production du jeton d'horodatage (``timestamp``) ;
- **logbook-common** : algorithme générique (**TraceabilityService**), contrat
  **LogbookTraceabilityHelper**, écriture du zip (**TraceabilityFile**), modèle
  **TraceabilityEvent** ;
- **logbook-administration** : sécurisation du journal des opérations
  (**LogbookAdministration**, **LogbookOperationTraceabilityHelper**) ;
- **logbook-administration-lfc** : déclenchement de la sécurisation des cycles de vie
  (**LogbookLFCAdministration**, **LfcTraceabilityType**) ;
- **worker-core** (``plugin.lfc_traceability``) : plugins PREPARE / BUILD / GENERATE /
  FINALIZE et croisement d'empreintes (**DigestValidator**) ;
- **processing-management** : définition des workflows
  ``DefaultUnitLifecycleTraceability.json`` et ``DefaultObjectGroupLifecycleTraceability.json`` ;
- **scheduler-server** : jobs Quartz **TraceabilityJob** et **TraceabilityLFCJob**.

La documentation métier (finalité, valeur probante, exploitation fonctionnelle) n'est pas reprise
ici : se référer à la documentation *vitam-doc*.

Vue d'ensemble
==============

Deux déclencheurs et deux chemins d'exécution, mais un seul algorithme et un seul format
d'archive :

.. code-block:: text

                               scheduler-server
                                      |
                     +----------------+-----------------+
                     |                                  |
              TraceabilityJob                   TraceabilityLFCJob
                     |                                  |
                     v                                  v
           LogbookAdministration            LogbookLFCAdministration
        (dans le serveur logbook,                       |
             sans workflow)               workflow LOGBOOK_*_LFC_TRACEABILITY
                     |                    PREPARE > BUILD > GENERATE > FINALIZE
                     |                       (distribué sur les workers)
                     +----------------+-----------------+
                                      v
                              TraceabilityService
                                      |
                           LogbookTraceabilityHelper
                            (spécifique au périmètre)
                                      |
                 +--------------------+--------------------+
                 v                    v                    v
             sélection          MerkleTreeAlgo       TimeStampService
       (_lastPersistedDate)            |            + TimestampGenerator
                 |                     |                    |
                 v                     v                    v
             data.txt           merkleTree.json          token.tsp
                                      |
                                      v
                    archive zip sur les offres + TraceabilityEvent
                              (evDetData de l'opération)

Le journal des opérations est sécurisé directement dans le serveur logbook ; les cycles de vie
passent par un workflow, leur volumétrie et les accès aux métadonnées et aux offres imposant de
distribuer le travail. À partir de **TraceabilityService**, le traitement est strictement le
même.

Algorithme générique
--------------------

L'algorithme est unique, porté par **TraceabilityService**. Il est paramétré par une
implémentation de **LogbookTraceabilityHelper** propre à chaque périmètre. Le point d'entrée est
``secureData(strategyId)``.

Déroulement
===========

Vue synthétique de l'enchaînement, du point d'entrée jusqu'à la journalisation :

.. code-block:: text

    secureData(strategyId)
        |
        +-- helper.startTraceability()
        |
        +-- création du TraceabilityFile (zip temporaire, sans compression)
        |
        +-- helper.saveDataInZip(merkleAlgo, traceabilityFile)
        |       |
        |       +-- sélection des entrées (par _lastPersistedDate)
        |       +-- sérialisation CanonicalJsonFormatter
        |       +-- écriture d'une ligne dans data.txt
        |       +-- merkleAlgo.addLeaf(...)        <-- feuille ajoutée au vol
        |
        +-- merkleAlgo.generateMerkle()            --> merkleTree.json
        |       (si aucune entrée : suppression du zip, helper.saveEmpty(), arrêt)
        |
        +-- timeStampService.getDigestFrom(racine, jetons précédent / M-1 / A-1)
        |
        +-- TimestampGenerator (tampon RFC 3161)   --> token.tsp
        |
        +-- écriture de computing_information.txt et additional_information.txt
        |
        +-- helper.storeAndDeleteZip(...)
        |
        +-- helper.createLogbookOperationEvent(...) --> TraceabilityEvent en evDetData

Le détail de chaque étape :

1. ``helper.startTraceability()`` : ouverture ou rattachement de l'opération de traçabilité dans
   le journal des opérations.
2. Création du zip temporaire dans le dossier de travail, nommé
   ``<tenant>_<zipName>_<yyyyMMdd_HHmmss>.zip``, où ``zipName`` est fourni par le helper et la
   date est la date de fin de traçabilité. **TraceabilityFile** écrit l'archive sans compression
   (``setLevel(0)``), le fichier étant destiné à être re-parcouru ligne à ligne lors des
   vérifications.
3. ``helper.saveDataInZip(merkleAlgo, traceabilityFile)`` : écriture de ``data.txt``, une ligne
   par entrée sécurisée sérialisée en JSON canonique (**CanonicalJsonFormatter**). Chaque ligne
   est simultanément ajoutée comme feuille de l'arbre de Merkle.
4. ``merkleAlgo.generateMerkle()`` : construction de l'arbre, sérialisé dans ``merkleTree.json``.
   Si l'arbre est ``null``, c'est-à-dire s'il n'y a aucune entrée à sécuriser, le zip est
   supprimé, ``helper.saveEmpty(tenantId)`` est appelé et le traitement s'arrête.
5. Horodatage : l'empreinte à horodater est calculée par **TimeStampService** sur la
   concaténation de la racine de Merkle et des tampons des traçabilités précédente, M-1 et A-1,
   les valeurs nulles étant ignorées. Le tampon RFC 3161 est produit par **TimestampGenerator**
   puis écrit en base 64 dans ``token.tsp``. Les valeurs d'entrée du calcul sont conservées en
   clair dans ``computing_information.txt``.
6. ``additional_information.txt`` : ``numberOfElements``, ``startDate``, ``endDate`` et
   ``securisationVersion``.
7. ``helper.storeAndDeleteZip(...)`` : dépôt du zip, directement sur les offres pour le journal
   des opérations, via le workspace pour les cycles de vie, puis suppression du fichier
   temporaire.
8. ``helper.createLogbookOperationEvent(...)`` : journalisation de l'événement final, portant le
   **TraceabilityEvent** en ``evDetData``.

Contenu du zip
==============

.. code-block:: text

    data.txt                     # une ligne JSON canonique par entrée sécurisée
    merkleTree.json              # arbre de Merkle sérialisé
    token.tsp                    # tampon RFC 3161 (base 64)
    computing_information.txt    # racine courante + tampons J-1 / M-1 / A-1
    additional_information.txt   # nombre d'entrées, bornes, SecurisationVersion

Le détail de chaque fichier est donné dans `Contenu de l'archive sécurisée`_.

Le **TraceabilityEvent** enregistré en ``evDetData`` reprend ces informations sous forme
exploitable : ``LogType``, ``StartDate`` et ``EndDate``, ``Hash`` (racine de Merkle),
``TimeStampToken``, les identifiants et dates des traçabilités précédentes
(``PreviousLogbookOperationId``, ``MinusOneMonthLogbookOperationId``,
``MinusOneYearLogbookOperationId``, ...), ``NumberOfElements``, ``FileName``, ``Size``,
``DigestAlgorithm``, ``MaxEntriesReached``, ``SecurisationVersion`` et ``Statistics``.

Rôle de LogbookTraceabilityHelper
=================================

L'interface concentre tout ce qui dépend du périmètre sécurisé :

- sélection et écriture des données : ``saveDataInZip``, ``getDataSize`` ;
- bornes temporelles : ``getTraceabilityStartDate``, ``getTraceabilityEndDate`` ;
- chaînage : ``getPrevious*OperationId``, ``getPrevious*StartDate``,
  ``getPrevious*TimestampToken``, pour les trois profondeurs (précédente, M-1, A-1) ;
- journalisation : ``startTraceability``, ``createLogbookOperationEvent``, ``saveEmpty``,
  ``getStepName``, ``getTimestampStepName`` ;
- stockage : ``storeAndDeleteZip``, ``getZipName`` ;
- métadonnées de l'événement : ``getTraceabilityType``, ``getMaxEntriesReached``,
  ``getTraceabilityStatistics``, ``getSecurisationVersion``.

La méthode par défaut ``decorateZipName(zipName, operationId)`` construit le nom de base de
l'archive. Le nom historique est conservé pour la version de sécurisation par défaut ; il est
suffixé par ``_<version>_<GUID>`` pour toute autre version. La date étant ajoutée ensuite par
l'appelant, l'analyse de date de **StorageFileNameHelper** n'est pas impactée.

Périmètre sécurisé
------------------

Le périmètre sécurisé est l'ensemble des entrées prises en compte par une opération de
traçabilité. Il n'est pas défini par une liste d'identifiants, mais par un intervalle temporel
évalué sur la date de dernière persistance de chaque entrée (``_lastPersistedDate``), et non sur
sa date d'événement (``evDateTime``).

``_lastPersistedDate`` est la date d'écriture effective en base, mise à jour à chaque
modification du document. Une entrée ne peut donc pas apparaître dans un intervalle déjà traité
après le passage de la traçabilité, ce que la date d'événement ne garantirait pas : une opération
peut être journalisée avec un ``evDateTime`` antérieur à son écriture.

Bornes de l'intervalle
======================

*Borne inférieure*
    Date de fin (``EndDate``) de la dernière traçabilité OK du même périmètre et de la même
    version de sécurisation, ce qui assure la continuité du chaînage. Pour la première
    traçabilité, la borne est l'époque (``LogbookTraceabilityHelper.INITIAL_START_DATE``).

*Borne supérieure*
    ``now`` diminué du délai de temporisation, 300 s par défaut. Les documents du journal sont
    enrichis au fil des événements. Sécuriser une entrée trop récente reviendrait à figer un
    document encore en cours d'écriture, dont l'empreinte évoluerait ensuite. La temporisation
    limite la sécurisation aux documents stabilisés.

Les deux bornes sont incluses (``>=`` et ``<=``). Une entrée dont la date de persistance tombe
exactement sur la borne peut donc être sécurisée par deux opérations consécutives. Ce
recouvrement est volontaire, un doublon étant préférable à un trou dans la couverture.

Continuité de la couverture
===========================

La borne supérieure d'une opération devient la borne inférieure de la suivante. Bout à bout, les
périmètres couvrent la totalité du journal sans discontinuité.

Une traçabilité vide, sans aucune entrée dans l'intervalle, ne produit ni archive ni empreinte,
et ne peut donc pas servir de borne. La recherche de la borne inférieure des cycles de vie porte
pour cette raison sur la dernière traçabilité disposant d'un zip (paramètre
``traceabilityWithZipOnly``), et non sur la dernière traçabilité OK.

Réduction du périmètre par le plafond
=====================================

Le nombre d'entrées retenues par opération est plafonné : ``operationTraceabilityMaxEntries``
pour le journal des opérations, ``lifecycleTraceabilityMaxEntries`` pour les cycles de vie,
100 000 par défaut dans les deux cas. Lorsque le plafond est atteint, la borne supérieure est
ramenée au ``_lastPersistedDate`` de la dernière entrée traitée et ``MaxEntriesReached`` est
positionné dans le ``TraceabilityEvent``. La traçabilité suivante repart de cette borne.

Le plafond est une limite souple. Une fois atteint, la sélection continue de renvoyer les entrées
dont la date de persistance est identique à celle de la dernière entrée retenue, dans la limite
d'une réserve de 10 000 entrées (``OVERFLOW_LIMIT``). Un groupe d'entrées partageant la même date
de persistance n'est ainsi jamais coupé en deux, la borne de reprise étant cette même date.

La sélection est réalisée par lecture directe des collections MongoDB
(``selectRawByLastPersistenceDateInterval``), sans passer par l'index de recherche ni par le DSL,
dont la limite de résultats par défaut est de 10 000 documents.

Cloisonnement et limites
========================

La sélection est filtrée sur le tenant. Il y a donc un périmètre, un chaînage et une version de
sécurisation par tenant et par périmètre : journal des opérations, cycles de vie des unités,
cycles de vie des groupes d'objets.

Le périmètre ne couvre pas :

- les objets binaires, qui ne sont jamais intégrés à l'archive ; seules leurs empreintes le sont,
  via la traçabilité des cycles de vie de groupes d'objets ;
- les métadonnées pour elles-mêmes ; elles sont couvertes indirectement, par l'empreinte portée
  dans la ligne du cycle de vie correspondant ;
- le journal des écritures de stockage, qui relève d'un périmètre distinct porté par le module
  *storage*.

Contenu de l'archive sécurisée
------------------------------

L'archive est un zip non compressé de cinq fichiers. Les fichiers ``traceabilityEvent.json`` et
``traceabilityStats.json``, présents dans les workflows de cycle de vie, n'en font pas partie :
ce sont des fichiers de travail du workspace, dont le contenu est repris dans l'``evDetData`` de
l'opération.

data.txt
========

``data.txt`` est le seul fichier porteur de données ; les quatre autres portent la preuve qui s'y
rapporte. Il contient une ligne par entrée sécurisée, séparée par ``\n``, chaque ligne étant
sérialisée par **CanonicalJsonFormatter**.

La sérialisation canonique rend l'empreinte reproductible : clés d'objet triées, aucune
indentation ni espacement, échappement restreint à un jeu fixe de caractères, caractères non
imprimables en ``\uXXXX``, entiers écrits tels quels. Cet encodage ne doit pas évoluer, sous
peine de rendre non reproductibles, donc invérifiables, les empreintes des archives antérieures.

L'ordre des lignes est celui de la sélection, par date de persistance croissante. Il est
signifiant, chaque ligne étant une feuille de l'arbre de Merkle dans cet ordre.

Le contenu d'une ligne dépend du périmètre.

Cas du journal des opérations
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

La ligne est le document logbook brut et complet, tel qu'il est stocké en base : aucun champ
n'est retiré, aucune valeur n'est transformée. On y trouve :

- les identifiants et le contexte d'appel : ``_id``, ``evId``, ``evParentId``, ``evIdProc``
  (identifiant du processus), ``evIdReq`` (identifiant de requête) ;
- le typage : ``evType``, ``evTypeProc`` ;
- la date de l'événement : ``evDateTime`` ;
- le résultat : ``outcome``, ``outDetail``, ``outMessg`` ;
- les agents : ``agId``, ``agIdApp``, ``agIdPers``, ``agIdExt``, ``evIdAppSession`` ;
- les objets concernés : ``obId``, ``obIdReq``, ``obIdIn`` ;
- les données de détail : ``evDetData``, ``rightsStatementIdentifier`` ;
- le tableau ``events``, contenant tous les sous-événements de l'opération, chacun portant les
  mêmes champs ;
- les champs techniques : ``_tenant``, ``_v`` et ``_lastPersistedDate``.

``_lastPersistedDate`` étant le champ qui délimite le périmètre, son inclusion dans l'empreinte
interdit de déplacer une entrée d'un périmètre à l'autre après sécurisation.

Pour la liste exhaustive des champs et leur correspondance métier (PREMIS), se référer au
*Modèle de données* de l'architecture fonctionnelle du logbook.

Cas des cycles de vie
^^^^^^^^^^^^^^^^^^^^^

La ligne n'est pas le document de cycle de vie, mais un condensé structuré
(**LifeCycleTraceabilitySecureFileObject**). La volumétrie des cycles de vie interdit de les
recopier intégralement, et le périmètre doit également couvrir la métadonnée associée ainsi que,
pour les groupes d'objets, les objets binaires.

.. list-table::
   :header-rows: 1
   :widths: 20 80

   * - Champ
     - Contenu
   * - ``lEvtIdProc``
     - identifiant de l'opération (``evIdProc``) portée par le cycle de vie
   * - ``lEvTypeProc``
     - type de processus du dernier événement du cycle de vie
   * - ``lEvDTime``
     - date du dernier événement du cycle de vie
   * - ``lfcId``
     - identifiant du cycle de vie, égal à celui de l'unité ou du groupe d'objets
   * - ``mdType``
     - ``UNIT`` ou ``OBJECTGROUP``
   * - ``version``
     - version (``_v``) du document de cycle de vie
   * - ``ltEvtOutcome``
     - ``outcome`` du dernier événement du cycle de vie
   * - ``hLFC``
     - empreinte du document de cycle de vie complet
   * - ``hLFCEvts``
     - empreinte du seul tableau ``events`` du cycle de vie
   * - ``hMetadata``
     - empreinte de la métadonnée, après retrait des champs calculés
   * - ``hGlobalFStorage``
     - empreinte de la métadonnée retenue après croisement avec les offres
   * - ``hGlobalDetails``
     - ``strategyId`` et ``offerIds`` interrogées ; en cas d'incohérence seulement, ``dbHash``
       et ``offerHash`` (empreinte par offre)
   * - ``hOGDocsStorage``
     - groupes d'objets uniquement : par objet binaire, ``id``, ``hObject`` et ``hDetails``
       (même structure que ``hGlobalDetails``). Les objets physiques sont ignorés, n'ayant pas
       de contenu stocké
   * - ``up``
     - identifiants des parents directs (``_up``) de l'unité
   * - ``idOG``
     - unités uniquement : identifiant du groupe d'objets rattaché (``_og``)

.. code-block:: json

    {
      "lEvtIdProc": "aeeaaaaaa...", "lEvTypeProc": "INGEST", "lEvDTime": "2026-05-12T09:41:07.213",
      "lfcId": "aeaqaaaaa...", "mdType": "UNIT", "version": 3, "ltEvtOutcome": "OK",
      "hLFC": "...", "hLFCEvts": "...", "hMetadata": "...",
      "hGlobalFStorage": "...",
      "hGlobalDetails": { "strategyId": "default", "offerIds": ["offer-fs-1"] },
      "up": ["aeaqaaaaa..."], "idOG": "aebaaaaaa..."
    }

``hLFC`` couvre le document de cycle de vie entier, ``hLFCEvts`` uniquement ses événements. La
présence des deux empreintes permet, lors d'un contrôle, de distinguer une altération du corps du
cycle de vie d'une altération de sa liste d'événements.

Les champs calculés sont retirés de la métadonnée avant calcul de ``hMetadata``. Il s'agit des
champs recalculables par la plateforme, dont la valeur peut changer sans que l'archive ait été
modifiée : champs de graphe (``_us``, ``_sps``, ``_graph``, ``_glpd``, ``_min``, ``_max``,
``_uds``, ``_computedInheritedRules``, ``_validComputedInheritedRules``, ``_vups``) et champs
temporaires ou techniques (``_elimination``, ``_av``, ``_acd``, ``_aud``, ``_opts``,
``_batchId``, ``_uploadPath``, ``_errors``, ``_ogInfo``) pour les unités ; ``_sps``, ``_us``,
``_glpd``, ``_av``, ``_acd``, ``_aud``, ``_batchId`` et ``_errors`` pour les groupes d'objets.

.. note::

   **Pourquoi retirer ces champs ?** Ils sont recalculés par la plateforme et peuvent donc
   changer sans qu'aucune donnée métier n'ait été modifiée. S'ils étaient inclus dans
   ``hMetadata``, un simple recalcul de graphe suffirait à faire diverger l'empreinte de la
   valeur scellée, et donc à faire échouer une vérification sur une archive pourtant intacte.

``hGlobalFStorage`` vaut l'empreinte calculée en base lorsque toutes les offres de la stratégie
renvoient la même valeur, et la chaîne littérale ``INVALID_HASH`` dès qu'une offre diverge, une
empreinte absente étant traitée comme divergente. Une incohérence constatée lors de la
sécurisation est ainsi scellée dans l'archive, et pas seulement signalée dans les journaux.

merkleTree.json
===============

Arbre de Merkle complet des lignes de ``data.txt``, sérialisé en JSON. Voir `Arbre de Merkle`_
pour sa construction et son format.

token.tsp
=========

Jeton d'horodatage RFC 3161 de la traçabilité courante, encodé en base 64. Il contient la date
d'horodatage, l'empreinte horodatée et la chaîne de certificats du signataire. Voir
`Horodatage et signature`_.

computing_information.txt
=========================

Fichier de propriétés (``clé=valeur``) donnant les quatre valeurs en entrée du calcul de
l'empreinte horodatée. Il permet de refaire ce calcul sans accès à la base :

.. code-block:: text

    currentHash=<racine de Merkle de la traçabilité courante, base 64>
    previousTimestampToken=<jeton de la traçabilité précédente, base 64>
    previousTimestampTokenMinusOneMonth=<jeton de la traçabilité de M-1, base 64>
    previousTimestampTokenMinusOneYear=<jeton de la traçabilité de A-1, base 64>

Les trois dernières valeurs sont sérialisées à ``null`` lorsqu'il n'existe pas de traçabilité
correspondante, cas des premières sécurisations d'une chaîne.

additional_information.txt
==========================

Fichier de propriétés décrivant le périmètre couvert par l'archive :

.. code-block:: text

    numberOfElements=<nombre de lignes de data.txt>
    startDate=<borne inférieure du périmètre>
    endDate=<borne supérieure du périmètre>
    securisationVersion=<version de sécurisation, ex. V1>

Ces informations sont redondantes avec le ``TraceabilityEvent`` journalisé. L'archive reste ainsi
exploitable indépendamment de la base.

Arbre de Merkle
---------------

|	*Package :* **fr.gouv.vitam.common.security.merkletree**

L'arbre de Merkle est un arbre binaire d'empreintes qui condense l'ensemble des entrées
sécurisées en une valeur unique, sa racine, tout en permettant de localiser les entrées
responsables d'une divergence. C'est cette racine, et non les données, qui est horodatée : la
preuve porte sur un condensé de taille constante, quelle que soit la volumétrie sécurisée.

Construction
============

**MerkleTreeAlgo** est alimenté en flux, pendant l'écriture de ``data.txt``. Chaque ligne, une
entrée sérialisée en JSON canonique, est ajoutée par ``addLeaf(byte[])``, qui stocke non pas la
donnée mais son empreinte. L'algorithme de hachage est celui de la plateforme
(``VitamConfiguration.getDefaultDigestType()``, ``SHA512`` par défaut).

``generateMerkle()`` construit ensuite l'arbre :

1. Complétion (``addPadding``) : le nombre de feuilles est complété jusqu'à la puissance de deux
   immédiatement supérieure, avec la feuille vide ``MerkleTree.EMPTY_LEAF``, de racine de
   longueur nulle. Un nombre de feuilles déjà égal à une puissance de deux n'est pas modifié.
2. Remontée par paires : à chaque niveau, les feuilles sont consommées deux par deux et
   l'empreinte du nœud parent est calculée sur la concaténation des racines gauche puis droite.
   L'opération est répétée jusqu'à ne conserver qu'un seul nœud, dont la racine est la racine de
   l'arbre.

.. code-block:: text

               racine = H(H12 || H34)
                 /                  \
        H12 = H(h1 || h2)     H34 = H(h3 || h4)
         /          \           /          \
    h1=H(l1)     h2=H(l2)   h3=H(l3)    h4=H(l4)   # l1..l4 : lignes de data.txt

L'ordre des feuilles est celui des lignes de ``data.txt``. Toute modification, suppression,
insertion ou permutation de ligne change la racine.

Propagation d'une altération
============================

L'intérêt de la structure tient à cette propagation : une modification, même d'un seul caractère
d'une seule ligne, remonte mécaniquement jusqu'à la racine. En reprenant l'arbre ci-dessus, si la
ligne ``l2`` est altérée en ``l2'`` :

1. ``h2' = H(l2') != h2`` : l'empreinte de la feuille change ;
2. ``H12' = H(h1 || h2') != H12`` : l'empreinte du nœud parent change ;
3. ``racine' = H(H12' || H34) != racine`` : la racine change.

Le contrôle ``CHECK_MERKLE_TREE`` recalcule la racine à partir du ``data.txt`` de l'archive et
constate l'écart avec ``merkleTree.json`` et avec le ``Hash`` journalisé : l'action est ``KO``
avec l'erreur ``INCORRECT_MERKLE_TREE``. Comme l'arbre entier est conservé, la comparaison
branche par branche localise la ou les lignes en cause.

Sérialisation
=============

L'arbre est sérialisé en entier dans ``merkleTree.json``, et non seulement sa racine, ce qui
permet à un contrôle ultérieur de descendre dans l'arbre pour isoler la branche fautive. Les
nœuds fils portent les noms courts ``l`` et ``r`` afin de limiter la taille du fichier, la racine
étant sérialisée en base 64 :

.. code-block:: json

    {
      "Root": "eqA0h...=",
      "l": { "Root": "...", "l": { "Root": "..." }, "r": { "Root": "..." } },
      "r": { "Root": "...", "l": { "Root": "..." }, "r": { "Root": "..." } }
    }

La racine est également reprise, en base 64, dans ``computing_information.txt``
(``currentHash``) et dans le ``TraceabilityEvent`` (``Hash``). Elle est donc présente à la fois
dans l'archive sécurisée et dans le journal indexé, ce qui rend les deux comparables.

Vérification
============

**VerifyMerkleTreeActionHandler** (action ``CHECK_MERKLE_TREE`` du workflow de contrôle)
recalcule l'arbre à partir du ``data.txt`` extrait de l'archive, avec l'algorithme d'empreinte
déclaré dans le ``TraceabilityEvent`` (``DigestAlgorithm``) et non avec celui configuré au moment
du contrôle, de façon à ce que les anciennes archives restent vérifiables. Le découpage en
feuilles se fait sur le séparateur de ligne ; un reliquat sans saut de ligne final est traité
comme une dernière feuille.

La racine recalculée est confrontée à deux références, chacune donnant lieu à une sous-action :

- ``COMPARE_MERKLE_HASH_WITH_SAVED_HASH`` : comparaison avec la racine de ``merkleTree.json``,
  donc avec l'archive elle-même ;
- ``COMPARE_MERKLE_HASH_WITH_INDEXED_HASH`` : comparaison avec le champ ``Hash`` du
  ``TraceabilityEvent`` journalisé.

Un écart sur l'une des deux comparaisons place l'action en ``KO``, avec l'erreur
``INCORRECT_MERKLE_TREE`` dans le rapport, et pose un drapeau d'erreur qui court-circuite les
plugins suivants pour cette archive.

Horodatage et signature
-----------------------

|	*Packages :* **fr.gouv.vitam.common.timestamp**, **fr.gouv.vitam.logbook.common.traceability**

L'horodatage produit un jeton RFC 3161 signé, attestant qu'une empreinte donnée existait à une
date donnée. VITAM ne s'appuie pas sur une autorité d'horodatage externe : il tient le rôle de
TSA avec un couple clé privée et certificat, stocké dans un keystore PKCS#12 local.

Empreinte horodatée et chaînage
===============================

L'empreinte soumise à l'horodatage n'est pas la seule racine de Merkle. **TimeStampService**
calcule un condensé sur la concaténation de quatre valeurs, dans cet ordre :

.. code-block:: java

    byte[] hashDigest = timeStampService.getDigestFrom(
        merkleRootHash,          // racine de l'arbre de la traçabilité courante
        previousTimestampToken,  // jeton de la traçabilité précédente
        previousMonthToken,      // jeton de la traçabilité de M-1
        previousYearToken);      // jeton de la traçabilité de A-1

Soit :

.. code-block:: text

    empreinte horodatée = H( racine de Merkle || jeton J-1 || jeton M-1 || jeton A-1 )

Les valeurs nulles sont ignorées, ce qui couvre le cas des premières traçabilités d'une chaîne.
L'intégration des jetons antérieurs dans l'empreinte fait que chaque jeton scelle la chaîne qui
le précède : une traçabilité passée ne peut pas être réécrite sans invalider les jetons
postérieurs. Les trois profondeurs (précédente, M-1, A-1) rendent le chaînage robuste à la perte
d'un maillon intermédiaire.

Génération du jeton
===================

**TimestampGenerator** construit la requête d'horodatage et délègue la signature à une
implémentation de l'interface **TimeStampSignature** :

- la requête porte l'OID de l'algorithme d'empreinte
  (``VitamConfiguration.getDefaultTimestampDigestType()``, ``SHA512`` par défaut) et l'empreinte
  à horodater ;
- ``setCertReq(true)`` demande l'inclusion du certificat signataire dans le jeton, nécessaire à
  une vérification autonome ultérieure ;
- le *nonce* n'est pas utilisé ; sa mise en place impliquerait d'adapter la vérification
  (**VerifyTimeStampActionHandler**) ;
- la réponse est validée contre la requête (``TimeStampResponse.validate``) avant retour du jeton
  encodé.

**TimeStampSignatureWithKeystore** est l'implémentation utilisée en production :

- chargement d'un keystore PKCS#12, qui doit contenir un seul alias ; dans le cas contraire
  l'initialisation échoue, la clé de signature à employer étant ambiguë ;
- récupération de la clé privée et de la chaîne de certificats associée ;
- l'algorithme de signature est déduit du couple algorithme d'empreinte et algorithme de clé,
  sous la forme ``<digest>With<algorithmeDeClé>``, par exemple ``SHA512WithRSA`` ;
- la chaîne de certificats complète est ajoutée au jeton, la politique d'horodatage est portée
  par l'OID ``1.1`` et la date d'horodatage (``genTime``) est l'heure courante du serveur.

Le jeton est écrit en base 64 dans ``token.tsp`` et repris dans le ``TraceabilityEvent`` (champ
``TimeStampToken``). L'événement ``OP_SECURISATION_TIMESTAMP``, ou son équivalent pour les cycles
de vie, porte le statut de cette étape. Un échec d'horodatage est ``FATAL`` et interrompt la
sécurisation.

Certificats et configuration
============================

La signature est réalisée là où la traçabilité est calculée, donc dans deux composants :

- le logbook, pour le journal des opérations : ``p12LogbookFile`` et ``p12LogbookPassword`` dans
  ``logbook.conf`` ;
- le worker, pour les cycles de vie (plugin ``GENERATE_*_LFC_TRACEABILITY``) : les mêmes clés,
  lues dans ``verify-timestamp.conf``.

Les certificats de timestamping sont produits par la PKI de déploiement à partir de
``environments/certs/timestamping/``, qui porte des CA racine et intermédiaire dédiées et les
certificats sous ``vitam/``. Ils sont convertis en keystores
``keystores/timestamping/keystore_<usage>.p12`` par ``generate_stores.sh``. Le fichier déployé
auprès des composants est ``keystore_secure-logbook.p12``, son mot de passe étant porté par le
*vault* Ansible.

Vérification du jeton
=====================

**VerifyTimeStampActionHandler** (action ``VERIFY_TIMESTAMP``) procède en trois temps, chacun
matérialisé par une sous-action et un code d'erreur dans le rapport :

``COMPARE_TOKEN_TIMESTAMP``
    Le jeton contenu dans ``token.tsp`` est comparé à celui journalisé dans l'``evDetData`` de
    l'opération. Une divergence signale une incohérence entre l'archive et le journal
    (``INEQUAL_TIMESTAMP``).

``VALIDATE_TOKEN_TIMESTAMP``
    Validation cryptographique du jeton :

    - extraction du certificat signataire embarqué dans le jeton, sélectionné par l'identifiant
      du signataire ; son absence donne ``MISSING_SIGNING_CERTIFICATE`` ;
    - vérification de la signature du jeton avec ce certificat ;
    - contrôle de l'attribut signé ``SigningCertificate`` ou ``SigningCertificateV2`` :
      l'empreinte du certificat qu'il référence doit correspondre au certificat effectivement
      utilisé, ce qui interdit la substitution d'un certificat dans le jeton ;
    - lorsqu'un truststore est configuré, validation de la confiance accordée au certificat.

``VERIFY_TOKEN_TIMESTAMP``
    Vérification que le jeton horodate les données de l'archive : la racine de
    ``merkleTree.json`` doit être égale au ``currentHash`` de ``computing_information.txt``, puis
    l'empreinte est recalculée sur les quatre valeurs de chaînage lues dans
    ``computing_information.txt`` et comparée au ``messageImprintDigest`` extrait du jeton. Tout
    écart donne ``INVALID_TIMESTAMP``.

Validation de la confiance
==========================

**TimestampCertificateValidator** évalue le certificat signataire vis-à-vis d'un truststore
fourni par l'exploitant. La date de référence est la date de génération du jeton (``genTime``) et
non la date du contrôle : un certificat expiré depuis la sécurisation reste valide pour les
traçabilités qu'il a signées. Deux contrôles sont enchaînés :

1. validité temporelle du certificat à la date ``genTime`` ; un certificat non encore valide ou
   déjà expiré à cette date donne ``CERTIFICATE_NOT_VALID_AT_SIGNATURE_DATE`` ;
2. confiance : soit le certificat signataire est lui-même présent dans le truststore (confiance
   directe, cas d'un certificat feuille non auto-signé), soit sa chaîne est validée par PKIX
   contre les ancres du truststore, avec la révocation désactivée et ``genTime`` comme date de
   validation. Un échec donne ``UNSUPPORTED_CERTIFICATE``.

Le truststore est optionnel et désactivé par défaut. Il est déclaré par
``timestampTrustStoreFile`` et ``timestampTrustStorePassword`` dans ``verify-timestamp.conf``,
généré par ``generate_stores.sh`` à partir des certificats déposés dans
``environments/certs/timestamping/trusted/``, et activé par la variable Ansible
``vitam.worker.timestamp_truststore_enabled``. En l'absence de truststore, la signature du jeton
est vérifiée sans ancrage de confiance : un jeton signé par un certificat arbitraire passe le
contrôle. Lorsqu'un truststore est configuré, tout échec de chargement est bloquant au démarrage
du worker, afin de ne pas dégrader le contrôle sans que ce soit visible.

Journal des opérations
----------------------

La sécurisation du journal des opérations est exécutée directement dans le serveur logbook, sans
workflow : le job **TraceabilityJob** appelle l'API du logbook, qui invoque
**LogbookAdministration**.

Déclenchement et sélection
==========================

``generateSecureLogbook(tenantId)`` génère le GUID de l'opération, le pose comme identifiant de
requête de la session, résout la version de sécurisation du tenant, puis instancie
**LogbookOperationTraceabilityHelper**. ``generateSecureLogbooks(tenants)`` parallélise le
traitement sur les tenants, avec un pool dimensionné par
``operationTraceabilityThreadPoolSize``, et lève une alerte pour chaque tenant en échec.

``initialize()`` détermine les bornes :

- ``startDate`` = date de fin (``EndDate``) de la dernière traçabilité OK de la même version, ou
  l'époque s'il n'en existe aucune ;
- ``endDate`` = ``now`` diminué du délai de temporisation
  (``operationTraceabilityTemporizationDelay``, 300 s par défaut).

``isTraceabilityOperationRequired()`` décide de l'exécution. La traçabilité est requise si la
dernière est plus ancienne que le délai de renouvellement maximal
(``operationTraceabilityMaxRenewalDelay``, 690 minutes par défaut), ou s'il existe de nouvelles
opérations éligibles dans l'intervalle. Sinon la sécurisation est ignorée et aucune opération
n'est produite.

Les entrées sont sélectionnées par date de persistance
(``selectOperationsByLastPersistenceDateInterval``) et sécurisées en tant qu'opérations
complètes : le document logbook entier, avec l'ensemble de ses événements, est écrit dans
``data.txt``.

Plafonnement
============

La sélection est bornée par ``operationTraceabilityMaxEntries``, 100 000 par défaut. Le mécanisme
de réduction de la borne haute et de reprise est décrit dans
`Réduction du périmètre par le plafond`_. À la différence des cycles de vie, aucun enchaînement
automatique n'est déclenché : le reliquat est traité à l'exécution suivante du job.

Événements produits
===================

- ``STP_OP_SECURISATION`` : événement maître, ``STARTED`` à l'ouverture, ``OK`` en fin de
  traitement, ``WARNING`` lorsqu'il n'y avait rien à sécuriser ;
- ``OP_SECURISATION_TIMESTAMP`` : horodatage ;
- ``OP_SECURISATION_STORAGE`` : dépôt du zip. Le fichier transite par un conteneur workspace
  nommé d'après l'identifiant de requête, puis est copié en ``DataCategory.LOGBOOK`` sur la
  stratégie de stockage par défaut, avant suppression du conteneur.

LFC Unit et LFC GOT
-------------------

À la différence du journal des opérations, la sécurisation des cycles de vie passe par un
workflow. La volumétrie et les accès aux métadonnées et aux offres nécessitent la distribution du
travail sur les workers.

Déclenchement
=============

Le job **TraceabilityLFCJob** (``@DisallowConcurrentExecution``, paramétré par un item ``UNIT``
ou ``OBJECTGROUP``) traite tous les tenants en parallèle et appelle, pour chacun,
``traceabilityLfcUnit()`` ou ``traceabilityLfcObjectGroup()`` sur le client logbook, ce qui
aboutit à ``LogbookLFCAdministration.generateSecureLogbookLFC(guid, lfcTraceabilityType)``.

Cette méthode ne démarre pas si un processus du même workflow est encore en cours. Elle évalue
ensuite ``isNewTraceabilityRequired`` à partir de la dernière traçabilité de la version courante
et de la dernière traçabilité disposant d'un zip, une traçabilité vide n'ayant pas de
``FileName`` et ne pouvant donc pas servir de borne. La traçabilité est requise si :

- c'est la première traçabilité de ce type ; ou
- la dernière est plus ancienne que ``lifecycleTraceabilityMaxRenewalDelay`` ; ou
- la dernière avait atteint son plafond (``MaxEntriesReached``) ; ou
- de nouveaux cycles de vie existent depuis la borne, à la temporisation près
  (``lifecycleTraceabilityTemporizationDelay``).

Le workflow est alors démarré : création du conteneur workspace, événement ``STARTED``,
``initVitamProcess`` puis ``RESUME``. Les délais de temporisation et le plafond d'entrées sont
transmis en paramètres d'exécution.

Le job attend la fin de l'opération en interrogeant le logbook
(``checkLifecycleTraceabilityWorkflowStatus``), avec une temporisation doublée à chaque
itération, de 1 s à 60 s au maximum. L'attente est abandonnée au bout de 30 minutes, ou dès que
le processus est en ``PAUSE``. Si le statut retourné indique ``maxEntriesReached``, le job relance
un tour complet, jusqu'à épuisement du reliquat.

Workflows
=========

Les workflows ``LOGBOOK_UNIT_LFC_TRACEABILITY`` et ``LOGBOOK_OBJECTGROUP_LFC_TRACEABILITY``
comportent deux étapes, toutes deux en distribution ``REF`` :

.. code-block:: text

    STP_UNIT_LFC_TRACEABILITY (resp. STP_OG_LFC_TRACEABILITY)
      CHECK_CONCURRENT_WORKFLOW_LOCK   # verrou sur l'identifiant de workflow
      PREPARE_UNIT_LFC_TRACEABILITY    # -> lastOperation.json, traceabilityInformation.json,
                                       #    lfcWithMetadata.jsonl
      BUILD_UNIT_LFC_TRACEABILITY      # -> traceabilityData.jsonl, traceabilityStats.json
      GENERATE_UNIT_LFC_TRACEABILITY   # -> traceabilityEvent.json, traceabilityFile.zip

    STP_UNIT_LFC_TRACEABILITY_FINALIZATION (non annulable)
      FINALIZE_UNIT_LFC_TRACEABILITY   # dépôt du zip en DataCategory.LOGBOOK

``CHECK_CONCURRENT_WORKFLOW_LOCK`` garantit qu'une seule sécurisation d'un même type de cycle de
vie s'exécute à un instant donné. ``PREPARE`` sélectionne les cycles de vie à sécuriser et les
apparie avec leurs métadonnées. ``BUILD`` calcule les empreintes et produit une ligne par entrée.
``GENERATE`` exécute l'algorithme générique, **TraceabilityService** avec
**LogbookUnitLifeCycleTraceabilityHelper** ou **LogbookObjectGroupLifeCycleTraceabilityHelper**,
et dépose le zip et l'événement dans le workspace. ``FINALIZE``, non annulable, transfère le zip
du workspace vers les offres et clôt l'opération. Les fichiers intermédiaires transitent en
``MEMORY``, sauf ``lastOperation.json``, ``traceabilityEvent.json`` et ``traceabilityFile.zip``,
portés par le workspace.

Données produites
=================

``PREPARE`` produit ``lfcWithMetadata.jsonl``, où chaque ligne apparie un cycle de vie brut et sa
métadonnée (**LfcMetadataPair**), ``traceabilityInformation.json``, qui porte le périmètre retenu
(``startDate``, ``endDate``, ``nbEntries``, ``maxEntriesReached``), et ``lastOperation.json``,
copie de la dernière traçabilité utilisée comme borne.

``BUILD`` transforme cet appariement en ``traceabilityData.jsonl``, dont chaque ligne est un
**LifeCycleTraceabilitySecureFileObject**. C'est ce fichier qui devient le ``data.txt`` de
l'archive. Le détail de ses champs est donné dans `Cas des cycles de vie`_.

Croisement d'empreintes
=======================

**DigestValidator** compare, pour chaque métadonnée et chaque objet, l'empreinte recalculée en
base, par sérialisation canonique de la métadonnée accompagnée de son cycle de vie, aux
empreintes retournées par chacune des offres de la stratégie :

- toutes les empreintes concordent : comptabilisé ``OK`` ;
- une partie des offres diverge ou ne renvoie pas d'empreinte : ``WARNING``, une alerte est émise
  et le détail des empreintes (``dbHash``, ``offerHashes``) est conservé dans la ligne sécurisée,
  où il n'est écrit qu'en cas d'incohérence ;
- toutes les offres divergent de la base : l'étape est ``KO`` et la liste des identifiants
  fautifs est reportée en ``evDetData``.

Le bilan est agrégé dans ``traceabilityStats.json`` (**TraceabilityStatistics**), puis repris dans
le ``TraceabilityEvent``.

Plafonnement des cycles de vie
==============================

La sécurisation est bornée par ``lifecycleTraceabilityMaxEntries``, 100 000 par défaut, transmis
au workflow en paramètre d'exécution et appliqué dès l'étape ``PREPARE``. Le dépassement remonte
jusqu'au job via ``MaxEntriesReached``, qui enchaîne alors les exécutions tant que du reliquat
subsiste.

SecurisationVersion
-------------------

Définition
==========

La version de sécurisation est un marqueur porté par chaque opération de traçabilité, défini par
tenant et par périmètre. **VitamConfiguration** conserve trois tables, pour le journal des
opérations, les cycles de vie d'unités et les cycles de vie de groupes d'objets, et retourne
``DEFAULT_TRACEABILITY_VERSION`` (``V1``) pour tout tenant non configuré :

.. code-block:: java

    String version = VitamConfiguration.getLogbookOperationTraceabilityVersion(tenantId);
    String unitVersion = VitamConfiguration.getLfcUnitTraceabilityVersion(tenantId);
    String gotVersion = VitamConfiguration.getLfcGotTraceabilityVersion(tenantId);

La configuration est portée par **TenantTraceabilityVersionConfiguration**, qui valide le format
attendu (``V1``, ``V2``, ...).

Stockage
========

La valeur est écrite dans le journal et dans l'archive sécurisée :

- ``evDetData.SecurisationVersion`` de l'événement de traçabilité (constante
  ``TraceabilityEvent.SECURISATION_VERSION``), y compris pour les traçabilités vides, où
  l'``evDetData`` ne contient que ce champ ;
- ``securisationVersion=<version>`` dans ``additional_information.txt``.

Le champ est déclaré dans le *mapping* Elasticsearch du logbook et dans la description DSL de la
collection ``LogbookOperation``, ce qui le rend interrogeable.

Critère de chaînage
===================

La recherche de la traçabilité précédente est filtrée sur la version. Un changement de version
ouvre donc une nouvelle chaîne au lieu de se raccrocher à la précédente. Sont concernés
``findLastTraceabilityOperationOK(version)``,
``findLastTraceabilityOperationOKBeforeDate(date, version)``, utilisés pour la borne de départ et
pour les tampons M-1 et A-1,
``findLastLifecycleTraceabilityOperation(eventType, version, traceabilityWithZipOnly)`` et la
requête équivalente de **LogbookLifeCycleTraceabilityHelper**.

Une clause de compatibilité est prévue pour ``V1`` dans
``findLastLifecycleTraceabilityOperation`` : le filtre accepte alors aussi les opérations
dépourvues du champ ``events.evDetData.SecurisationVersion``, les anciennes traçabilités vides,
sans zip, ne le portant pas. Le même principe est appliqué côté audit de chaîne de traçabilité
(**LogbookInternalResourceImpl**), où ``V1`` et l'absence du champ sont traités comme
équivalents.

Usage côté vérification
=======================

Les traitements de vérification résolvent la version attendue du tenant et ne considèrent que
les traçabilités correspondantes :

- **EvidenceService** : recherche de la traçabilité couvrant une date donnée et détermination de
  la dernière traçabilité de cycle de vie ;
- **ProbativeCreateReportEntry** : recherche de la traçabilité de référence et de la plus proche
  traçabilité antérieure ;
- **TraceabilityLinkedCheckPreparePlugin** : les opérations dont la version diffère de celle
  attendue sont écartées de la distribution et signalées comme ignorées.

Configuration
=============

Le déploiement Ansible génère la section ``traceabilityVersionConfiguration`` de ``vitam.conf``
pour chaque tenant, avec un défaut à ``V1`` :

.. code-block:: yaml

    traceabilityVersionConfiguration:
      - tenant: 0
        logbookOperation: V1
        lfcUnit: V1
        lfcGot: V1

Les valeurs sont surchargeables par tenant via
``extendedConfiguration.custom[<tenant>].traceabilityVersion.*``, et globalement via
``extendedConfiguration.default.traceabilityVersion.*``.

Portée du champ
===============

Aucune branche de l'algorithme de sécurisation ne dépend de la valeur de la version : quelle
qu'elle soit, les mêmes données sont sélectionnées, le même arbre de Merkle est construit et le
même horodatage est appliqué. La version intervient comme critère de filtrage et de chaînage
décrit ci-dessus, et comme discriminant dans le nom de l'archive (``decorateZipName``, qui
préserve le nom historique pour ``V1`` et rend le nom unique par opération au-delà, ce qui évite
un conflit sur l'offre). Le champ est donc conservatoire : il permet une évolution ultérieure du
format ou de l'algorithme sans rompre les chaînes existantes.

Synthèse de la chaîne de preuve
-------------------------------

Chaîne de dépendances
=====================

Chaque maillon est vérifiable indépendamment, et chacun dépend du précédent : c'est cet
enchaînement qui rend l'archive opposable.

.. code-block:: text

    data.txt (une ligne par entrée, ordre significatif)
        |
        v
    CanonicalJsonFormatter --> empreinte par ligne --> MerkleTreeAlgo
                                                            |
                                                            v
                                                    racine de Merkle
                                                            |
             +----------------------------------------------+
             v                                              v
      merkleTree.json                        computing_information.txt
      (archive)                              (currentHash + jetons J-1 / M-1 / A-1)
                                                            |
                                                            v
                                            TimeStampService.getDigestFrom(...)
                                                            |
                                                            v
                                       TimestampGenerator (keystore PKCS#12)
                                                            |
                                                            v
                                                        token.tsp
                                                            |
                                                            v
                                     TraceabilityEvent (evDetData, indexé)

La vérification remonte cette chaîne en sens inverse : recalcul des empreintes de lignes, de
l'arbre et de la racine, comparaison à ``merkleTree.json`` et au ``Hash`` journalisé
(`Vérification`_), puis recalcul de l'empreinte horodatée et validation du jeton
(`Vérification du jeton`_).

Récapitulatif pas à pas
=======================

1. Résoudre la version de sécurisation du tenant et déterminer les bornes ``startDate`` et
   ``endDate`` du périmètre.
2. Sélectionner les entrées stabilisées par ``_lastPersistedDate``, dans la limite du plafond et
   de sa réserve de débordement.
3. Cycles de vie uniquement : apparier les cycles de vie et leurs métadonnées (``PREPARE``), puis
   croiser les empreintes de la base avec celles de chaque offre via **DigestValidator**
   (``BUILD``).
4. Sérialiser chaque entrée avec **CanonicalJsonFormatter**.
5. Écrire chaque ligne dans ``data.txt`` et l'ajouter comme feuille de **MerkleTreeAlgo**.
6. Appeler ``generateMerkle()`` : complétion des feuilles puis remontée par paires jusqu'à la
   racine, sérialisée dans ``merkleTree.json``.
7. Récupérer les jetons des traçabilités précédente, M-1 et A-1 de la même version, pour le
   chaînage.
8. Calculer l'empreinte à horodater via **TimeStampService** et conserver ses quatre valeurs
   d'entrée dans ``computing_information.txt``.
9. Produire le jeton RFC 3161 avec **TimestampGenerator** et le keystore PKCS#12, puis l'écrire
   en base 64 dans ``token.tsp``.
10. Écrire ``additional_information.txt`` (nombre d'entrées, bornes, version de sécurisation).
11. Déposer l'archive en ``DataCategory.LOGBOOK`` sur les offres, directement pour le journal des
    opérations, via le workspace et l'étape ``FINALIZE`` pour les cycles de vie, puis supprimer
    le fichier temporaire.
12. Journaliser le **TraceabilityEvent** en ``evDetData`` de l'opération, ce qui la clôt et rend
    l'archive retrouvable et comparable depuis le journal indexé.

Vérifications aval
------------------

Les contrôles portant sur l'arbre de Merkle et sur le jeton d'horodatage sont décrits plus haut
(`Vérification`_ et `Vérification du jeton`_). Les traitements qui les orchestrent ne sont pas
détaillés ici :

- le contrôle de cohérence de la chaîne de traçabilité (workflow ``LINKED_CHECK_SECURISATION``,
  ``LinkedCheckTraceability.json``), qui enchaîne récupération et extraction du zip, contrôle des
  empreintes, ``CHECK_MERKLE_TREE`` et ``VERIFY_TIMESTAMP`` ;
- l'audit de cohérence (**EvidenceService**) ;
- le relevé de valeur probante (**ProbativeCreateReportEntry**).

Se référer à la documentation de ces traitements et à *vitam-doc* pour leur description
fonctionnelle.
