7.10.2.13. Sécurisation (traçabilité)¶
7.10.2.13.1. Présentation¶
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.jsonetDefaultObjectGroupLifecycleTraceability.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.
7.10.2.13.1.1. Vue d’ensemble¶
Deux déclencheurs et deux chemins d’exécution, mais un seul algorithme et un seul format d’archive :
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.
7.10.2.13.2. 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).
7.10.2.13.2.1. Déroulement¶
Vue synthétique de l’enchaînement, du point d’entrée jusqu’à la journalisation :
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 :
helper.startTraceability(): ouverture ou rattachement de l’opération de traçabilité dans le journal des opérations.- Création du zip temporaire dans le dossier de travail, nommé
<tenant>_<zipName>_<yyyyMMdd_HHmmss>.zip, oùzipNameest 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. helper.saveDataInZip(merkleAlgo, traceabilityFile): écriture dedata.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.merkleAlgo.generateMerkle(): construction de l’arbre, sérialisé dansmerkleTree.json. Si l’arbre estnull, 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.- 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 danscomputing_information.txt. additional_information.txt:numberOfElements,startDate,endDateetsecurisationVersion.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.helper.createLogbookOperationEvent(...): journalisation de l’événement final, portant le TraceabilityEvent enevDetData.
7.10.2.13.2.2. Contenu du zip¶
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.
7.10.2.13.2.3. 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.
7.10.2.13.3. 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.
7.10.2.13.3.1. 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
nowdiminué 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.
7.10.2.13.3.2. 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.
7.10.2.13.3.3. 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.
7.10.2.13.3.4. 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.
7.10.2.13.4. 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.
7.10.2.13.4.1. 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.
7.10.2.13.4.1.1. 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,_vet_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.
7.10.2.13.4.1.2. 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.
| 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) |
{
"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.
7.10.2.13.4.2. 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.
7.10.2.13.4.3. 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.
7.10.2.13.4.4. 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 :
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.
7.10.2.13.4.5. additional_information.txt¶
Fichier de propriétés décrivant le périmètre couvert par l’archive :
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.
7.10.2.13.5. Arbre de Merkle¶
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.
7.10.2.13.5.1. 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 :
- Complétion (
addPadding) : le nombre de feuilles est complété jusqu’à la puissance de deux immédiatement supérieure, avec la feuille videMerkleTree.EMPTY_LEAF, de racine de longueur nulle. Un nombre de feuilles déjà égal à une puissance de deux n’est pas modifié. - 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.
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.
7.10.2.13.5.2. 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' :
h2' = H(l2') != h2: l’empreinte de la feuille change ;H12' = H(h1 || h2') != H12: l’empreinte du nœud parent change ;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.
7.10.2.13.5.3. 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 :
{
"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.
7.10.2.13.5.4. 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 demerkleTree.json, donc avec l’archive elle-même ;COMPARE_MERKLE_HASH_WITH_INDEXED_HASH: comparaison avec le champHashduTraceabilityEventjournalisé.
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.
7.10.2.13.6. Horodatage et signature¶
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.
7.10.2.13.6.1. 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 :
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 :
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.
7.10.2.13.6.2. 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(),SHA512par 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 exempleSHA512WithRSA; - la chaîne de certificats complète est ajoutée au jeton, la politique d’horodatage est portée
par l’OID
1.1et 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.
7.10.2.13.6.3. 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 :
p12LogbookFileetp12LogbookPassworddanslogbook.conf; - le worker, pour les cycles de vie (plugin
GENERATE_*_LFC_TRACEABILITY) : les mêmes clés, lues dansverify-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.
7.10.2.13.6.4. 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.tspest comparé à celui journalisé dans l”evDetDatade l’opération. Une divergence signale une incohérence entre l’archive et le journal (INEQUAL_TIMESTAMP). VALIDATE_TOKEN_TIMESTAMPValidation 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é
SigningCertificateouSigningCertificateV2: 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.
- extraction du certificat signataire embarqué dans le jeton, sélectionné par l’identifiant
du signataire ; son absence donne
VERIFY_TOKEN_TIMESTAMP- Vérification que le jeton horodate les données de l’archive : la racine de
merkleTree.jsondoit être égale aucurrentHashdecomputing_information.txt, puis l’empreinte est recalculée sur les quatre valeurs de chaînage lues danscomputing_information.txtet comparée aumessageImprintDigestextrait du jeton. Tout écart donneINVALID_TIMESTAMP.
7.10.2.13.6.5. 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 :
- validité temporelle du certificat à la date
genTime; un certificat non encore valide ou déjà expiré à cette date donneCERTIFICATE_NOT_VALID_AT_SIGNATURE_DATE; - 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
genTimecomme date de validation. Un échec donneUNSUPPORTED_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.
7.10.2.13.7. 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.
7.10.2.13.7.1. 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=nowdiminué 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.
7.10.2.13.7.2. 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.
7.10.2.13.7.3. Événements produits¶
STP_OP_SECURISATION: événement maître,STARTEDà l’ouverture,OKen fin de traitement,WARNINGlorsqu’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é enDataCategory.LOGBOOKsur la stratégie de stockage par défaut, avant suppression du conteneur.
7.10.2.13.8. 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.
7.10.2.13.8.1. 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.
7.10.2.13.8.2. Workflows¶
Les workflows LOGBOOK_UNIT_LFC_TRACEABILITY et LOGBOOK_OBJECTGROUP_LFC_TRACEABILITY
comportent deux étapes, toutes deux en distribution REF :
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.
7.10.2.13.8.3. 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.
7.10.2.13.8.4. 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
KOet la liste des identifiants fautifs est reportée enevDetData.
Le bilan est agrégé dans traceabilityStats.json (TraceabilityStatistics), puis repris dans
le TraceabilityEvent.
7.10.2.13.8.5. 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.
7.10.2.13.9. SecurisationVersion¶
7.10.2.13.9.1. 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é :
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, …).
7.10.2.13.9.2. Stockage¶
La valeur est écrite dans le journal et dans l’archive sécurisée :
evDetData.SecurisationVersionde l’événement de traçabilité (constanteTraceabilityEvent.SECURISATION_VERSION), y compris pour les traçabilités vides, où l”evDetDatane contient que ce champ ;securisationVersion=<version>dansadditional_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.
7.10.2.13.9.3. 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.
7.10.2.13.9.4. 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.
7.10.2.13.9.5. Configuration¶
Le déploiement Ansible génère la section traceabilityVersionConfiguration de vitam.conf
pour chaque tenant, avec un défaut à V1 :
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.*.
7.10.2.13.9.6. 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.
7.10.2.13.10. Synthèse de la chaîne de preuve¶
7.10.2.13.10.1. 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.
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).
7.10.2.13.10.2. Récapitulatif pas à pas¶
- Résoudre la version de sécurisation du tenant et déterminer les bornes
startDateetendDatedu périmètre. - Sélectionner les entrées stabilisées par
_lastPersistedDate, dans la limite du plafond et de sa réserve de débordement. - 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). - Sérialiser chaque entrée avec CanonicalJsonFormatter.
- Écrire chaque ligne dans
data.txtet l’ajouter comme feuille de MerkleTreeAlgo. - Appeler
generateMerkle(): complétion des feuilles puis remontée par paires jusqu’à la racine, sérialisée dansmerkleTree.json. - 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.
- Calculer l’empreinte à horodater via TimeStampService et conserver ses quatre valeurs
d’entrée dans
computing_information.txt. - Produire le jeton RFC 3161 avec TimestampGenerator et le keystore PKCS#12, puis l’écrire
en base 64 dans
token.tsp. - Écrire
additional_information.txt(nombre d’entrées, bornes, version de sécurisation). - Déposer l’archive en
DataCategory.LOGBOOKsur les offres, directement pour le journal des opérations, via le workspace et l’étapeFINALIZEpour les cycles de vie, puis supprimer le fichier temporaire. - Journaliser le TraceabilityEvent en
evDetDatade l’opération, ce qui la clôt et rend l’archive retrouvable et comparable depuis le journal indexé.
7.10.2.13.11. 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_TREEetVERIFY_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.