PD-54 — Plan d'implementation¶
1. Objectif¶
Ce document decrit comment implementer le mecanisme de construction d'arbres de Merkle periodiques a feuilles partagees tel que specifie dans PD-54-specification.md. L'implementation produit, pour chaque fenetre temporelle close, une racine de Merkle deterministe engageant l'integralite des evenements valides, utilisable comme unique entree d'horodatage et/ou d'ancrage externe.
2. Choix techniques retenus¶
| Decision | Choix | Justification |
|---|---|---|
| Algorithme de hash | SHA-256 | Standard industriel, resistance aux collisions |
| Encodage des hashes | Hexadecimal lowercase | Interoperabilite, lisibilite |
| Ordonnancement feuilles | Lexicographique sur hash hex | Deterministe, simple |
| Canonicalisation | JSON Canonicalization Scheme (JCS - RFC 8785) | Standard, deterministe |
| Fuseau horaire | IANA TZ database | Standard industriel |
| Arbre binaire | Duplication du dernier noeud si impair | Conforme RFC 6962 (Certificate Transparency) |
3. Identifiants normatifs versions et registre¶
3.1 Identifiants¶
| Identifiant | Valeur | Description |
|---|---|---|
canonicalization_id | JCS-RFC8785-V1 | JSON Canonicalization Scheme |
leaf_ordering_id | LEX-HEX-ASC-V1 | Tri lexicographique ascendant sur hash hex |
hash_algorithm_id | SHA256-V1 | SHA-256, sortie 32 octets |
proof_format_id | MERKLE-PROOF-V1 | Format de preuve d'inclusion |
Ces identifiants DOIVENT etre presents dans les metadonnees de chaque arbre et preuve.
3.2 Registre normatif public (R18, TC-NOM-12)¶
Conformement a §5.0 de la specification, les definitions normatives associees aux identifiants DOIVENT etre : - soit publiques et accessibles independamment, - soit fournies avec la preuve elle-meme.
Registre normatif : Un document JSON public (normative-registry.json) contient les definitions completes :
{
"registry_version": "1.0",
"definitions": {
"JCS-RFC8785-V1": {
"type": "canonicalization",
"reference": "RFC 8785 - JSON Canonicalization Scheme",
"url": "https://datatracker.ietf.org/doc/html/rfc8785",
"rules": [
"Cles triees lexicographiquement (Unicode code point)",
"Pas d'espaces superflus",
"Nombres sans exposant si possible",
"Encodage UTF-8 strict"
]
},
"LEX-HEX-ASC-V1": {
"type": "leaf_ordering",
"description": "Tri lexicographique ascendant sur representation hexadecimale lowercase des hashes",
"algorithm": "leaves.sort((a, b) => a.hash.localeCompare(b.hash))"
},
"SHA256-V1": {
"type": "hash_algorithm",
"reference": "FIPS 180-4",
"name": "SHA-256",
"output_size_bytes": 32,
"output_encoding": "hexadecimal lowercase"
},
"MERKLE-PROOF-V1": {
"type": "proof_format",
"concatenation": "binary (bytes decoded from hex)",
"hash_input": "left_bytes || right_bytes",
"path_position": "L = sibling a gauche, R = sibling a droite"
}
}
}
Inclusion dans les preuves : Chaque preuve DOIT inclure soit : - une URL vers le registre normatif public, soit - les definitions normatives inline dans le champ normative_definitions
Ceci permet a un auditeur externe de verifier la preuve sans acces au systeme emetteur (R18).
4. Architecture ciblee¶
┌─────────────────────────────────────────────────────────────────────────┐
│ MERKLE TREE ENGINE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ EventValidator │───►│ Canonicalizer │───►│ LeafBuilder │ │
│ │ (R1-R3, 5.0) │ │ (R4-R5) │ │ (R6-R7) │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ WindowManager │───►│ LeafSorter │───►│ TreeBuilder │ │
│ │ (R15-R16) │ │ (R8-R9) │ │ (R10-R12) │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ HashProvider │ │ ProofGenerator │◄───│ RootFinalizer │ │
│ │ (R20) │ │ (R17-R19) │ │ (R13-R14) │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────┐ │
│ │ MerkleDAGManager (R21-R24) │ │
│ │ Gestion feuilles partagees │ │
│ └────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ STORAGE LAYER │
├─────────────────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Trees │ │ Leaves │ │ Proofs │ │ Metadata │ │
│ │ (roots) │ │ (shared) │ │ (inclusion) │ │ (windows) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
5. Decoupage technique¶
5.1 Module EventValidator¶
Responsabilite : Valider les evenements selon les criteres de la section 5.0 de la spec.
Interface :
interface EventValidator {
validate(event: Event, window: TimeWindow): ValidationResult;
filterValid(events: Event[], window: TimeWindow): Event[];
}
interface Event {
id: string;
event_time: unknown; // Format non contraint par la spec ; doit permettre attribution univoque a une fenetre
payload: unknown;
}
interface TimeWindow {
window_start: unknown; // Format non contraint par la spec ; interprete dans window_timezone
window_end: unknown; // Format non contraint par la spec ; interprete dans window_timezone
window_timezone: string; // IANA TZ (ex. "Europe/Paris")
}
type ValidationResult =
| { valid: true }
| { valid: false; reason: 'NO_ID' | 'NO_TIME' | 'OUT_OF_WINDOW' | 'CANONICALIZATION_FAILED' };
Regles de validation (§5.0) : 1. Identifiant present et non vide → sinon invalide 2. event_time present et parsable → sinon invalide 3. window_start <= event_time < window_end (intervalle semi-ouvert) → sinon hors fenetre 4. Canonicalisation possible → sinon invalide
Observable : Evenements invalides exclus, traces avec raison
5.2 Module Canonicalizer¶
Responsabilite : Transformer un evenement en representation canonique deterministe.
Interface :
interface Canonicalizer {
canonicalize(event: Event): Result<Uint8Array, CanonicalizationError>;
}
type CanonicalizationError = { code: 'E3'; message: string };
Algorithme (JCS-RFC8785-V1) : 1. Serialiser l'evenement en JSON 2. Appliquer RFC 8785 : - Cles triees lexicographiquement (Unicode code point) - Pas d'espaces superflus - Nombres sans exposant si possible - Encodage UTF-8 strict 3. Retourner les octets UTF-8 du JSON canonique
Contrainte : Deux executions avec le meme evenement DOIVENT produire des octets identiques.
Observable : Representation canonique identique pour evenements identiques (TC-NOM-02)
5.3 Module LeafBuilder¶
Responsabilite : Construire une feuille a partir d'un evenement canonique.
Interface :
interface LeafBuilder {
buildLeaf(canonicalEvent: Uint8Array): Leaf;
}
interface Leaf {
hash: string; // Hex lowercase, 64 chars
eventId: string;
treeId?: string; // Optionnel, pour tracer l'appartenance
}
Algorithme :
Contrainte R7 : Une meme valeur de leaf.hash PEUT etre reutilisee dans plusieurs arbres.
Observable : leaf = HASH(canonical_event) deterministe (TC-NOM-03)
5.4 Module LeafSorter¶
Responsabilite : Trier les feuilles selon un ordre canonique deterministe.
Interface :
Algorithme (LEX-HEX-ASC-V1) :
Contrainte : L'ordre est strictement identique a chaque execution pour le meme ensemble.
Observable : Ordre final identique sur executions repetees (TC-NOM-04)
5.5 Module TreeBuilder¶
Responsabilite : Construire l'arbre de Merkle binaire a partir des feuilles triees.
Interface :
interface TreeBuilder {
build(sortedLeaves: Leaf[]): MerkleTree;
}
interface MerkleTree {
root: string; // Hash racine
levels: string[][]; // Niveaux de l'arbre (0 = feuilles)
leafCount: number;
}
Algorithme :
function build(leaves):
if leaves.length == 0:
return null // E1: pas d'arbre
currentLevel = leaves.map(l => l.hash) // hashes en hex lowercase
levels = [currentLevel]
while currentLevel.length > 1:
nextLevel = []
for i = 0; i < currentLevel.length; i += 2:
leftHex = currentLevel[i]
rightHex = currentLevel[i + 1] ?? currentLevel[i] // R12: duplication si impair
// IMPORTANT (R11): Concatenation sur les BYTES, pas sur les chaines hex
leftBytes = bytes.fromhex(leftHex) // 32 bytes
rightBytes = bytes.fromhex(rightHex) // 32 bytes
parentBytes = SHA256(leftBytes || rightBytes) // 64 bytes en entree
parentHex = lowercase_hex(parentBytes)
nextLevel.push(parentHex)
currentLevel = nextLevel
levels.push(currentLevel)
return {
root: currentLevel[0],
levels: levels,
leafCount: leaves.length
}
Contraintes : - R10 : Arbre strictement binaire - R11 : parent = HASH(left_bytes || right_bytes) — concatenation sur les bytes bruts (32+32=64 octets), PAS sur les chaines hex (64+64=128 caracteres) - R12 : Duplication du dernier noeud si nombre impair
IMPORTANT : L'encodage de la concatenation est explicitement defini comme binaire (bytes decodes de hex). Cette regle est documentee dans le registre normatif sous MERKLE-PROOF-V1.concatenation.
Observable : Structure binaire valide, duplication correcte (TC-NOM-05)
5.6 Module WindowManager¶
Responsabilite : Gerer les fenetres temporelles et l'attribution des evenements.
Interface :
interface WindowManager {
createWindow(start: string, end: string, timezone: string): TimeWindow;
getWindowForEvent(event: Event, windows: TimeWindow[]): TimeWindow | null;
isWindowClosed(window: TimeWindow): boolean;
}
Regles : - Intervalle semi-ouvert : [window_start, window_end) - window_timezone DOIT etre un identifiant IANA TZ valide - Un evenement appartient a une unique fenetre
Mecanismes d'enforcement (R1, R15-R16, H-01, H-03) :
interface WindowManager {
// ... existant ...
// Enforcement de la cloture (R1, H-01)
closeWindow(window: TimeWindow): ClosedWindow;
isWindowClosed(window: TimeWindow): boolean;
// Enforcement du non-chevauchement (R15-R16, H-03)
validateNoOverlap(windows: TimeWindow[]): ValidationResult;
registerWindow(window: TimeWindow): Result<void, OverlapError>;
}
interface ClosedWindow extends TimeWindow {
closedAt: unknown;
eventCount: number;
readonly: true; // Immutable apres cloture
}
type OverlapError = { code: 'WINDOW_OVERLAP'; windows: [TimeWindow, TimeWindow] };
Algorithme de validation non-chevauchement :
function validateNoOverlap(windows):
sorted = windows.sortBy(w => w.window_start)
for i = 0; i < sorted.length - 1; i++:
if sorted[i].window_end > sorted[i+1].window_start:
return { valid: false, overlap: [sorted[i], sorted[i+1]] }
return { valid: true }
Algorithme de cloture :
function closeWindow(window):
if isWindowClosed(window):
throw "Window already closed"
// Marquer comme close - aucun evenement supplementaire accepte
window.readonly = true
window.closedAt = now()
// Persister l'etat de cloture
persist(window)
return window as ClosedWindow
Observable : Attribution correcte des evenements (TC-NOM-13), rejet si fenetre close ou chevauchement
5.7 Module RootFinalizer¶
Responsabilite : Finaliser et verrouiller la racine apres construction.
Interface :
interface RootFinalizer {
finalize(tree: MerkleTree, window: TimeWindow): FinalizedTree;
isFinalized(treeId: string): boolean;
}
interface FinalizedTree {
treeId: string;
root: string;
window: TimeWindow;
finalizedAt: unknown; // Format non contraint ; horodatage de finalisation
metadata: TreeMetadata;
locked: true;
}
interface TreeMetadata {
canonicalization_id: string;
leaf_ordering_id: string;
hash_algorithm_id: string;
proof_format_id: string;
leaf_count: number;
}
Contraintes : - R13 : La racine engage l'integralite des evenements - R14 : Aucun ajout apres finalisation
Observable : Ajout rejete apres cloture (TC-NOM-06)
5.8 Module ProofGenerator¶
Responsabilite : Generer et verifier les preuves d'inclusion.
Interface :
interface ProofGenerator {
generateProof(tree: MerkleTree, leafHash: string): InclusionProof;
verifyProof(proof: InclusionProof, root: string): boolean;
}
interface InclusionProof {
leaf: string;
root: string;
path: ProofStep[];
metadata: ProofMetadata;
}
interface ProofStep {
hash: string;
position: 'L' | 'R'; // Position du sibling
}
interface ProofMetadata {
tree_id: string;
window_start: unknown; // Format non contraint
window_end: unknown; // Format non contraint
window_timezone: string;
canonicalization_id: string;
leaf_ordering_id: string;
hash_algorithm_id: string;
proof_format_id: string;
// TC-NOM-12, R18: Au moins une des deux options DOIT etre presente
normative_registry_url?: string; // URL vers le registre normatif public
normative_definitions?: NormativeDefinitions; // Definitions inline
}
interface NormativeDefinitions {
[id: string]: {
type?: string;
reference?: string;
description?: string;
[key: string]: unknown;
};
}
Algorithme de generation :
function generateProof(tree, leafHash):
leafIndex = tree.levels[0].indexOf(leafHash)
if leafIndex == -1:
throw "Leaf not found"
path = []
index = leafIndex
for level = 0; level < tree.levels.length - 1; level++:
siblingIndex = index % 2 == 0 ? index + 1 : index - 1
sibling = tree.levels[level][siblingIndex] ?? tree.levels[level][index]
position = index % 2 == 0 ? 'R' : 'L'
path.push({ hash: sibling, position: position })
index = Math.floor(index / 2)
return { leaf: leafHash, root: tree.root, path: path, metadata: ... }
Algorithme de verification :
function verifyProof(proof, expectedRoot):
currentHex = proof.leaf
for step in proof.path:
// Decoder les hashes hex en bytes avant concatenation
currentBytes = bytes.fromhex(currentHex)
siblingBytes = bytes.fromhex(step.hash)
if step.position == 'L':
// Sibling a gauche : sibling || current
parentBytes = SHA256(siblingBytes || currentBytes)
else:
// Sibling a droite : current || sibling
parentBytes = SHA256(currentBytes || siblingBytes)
currentHex = lowercase_hex(parentBytes)
return currentHex == expectedRoot
IMPORTANT : La concatenation || opere sur les bytes bruts (32 octets chacun), conformement a R11 et au registre normatif MERKLE-PROOF-V1.concatenation.
Contraintes : - R18 : Verification possible hors systeme emetteur - R19 : La racine est l'unique entree pour usage aval
Observable : Preuve verifiable independamment (TC-NOM-09)
5.9 Module HashProvider¶
Responsabilite : Fournir l'algorithme de hash versionne.
Interface :
interface HashProvider {
getAlgorithm(): HashAlgorithm;
hash(data: Uint8Array): string;
}
interface HashAlgorithm {
id: string; // "SHA256-V1"
name: string; // "SHA-256"
outputSize: number; // 32 bytes
}
Contrainte R20 : L'algorithme DOIT etre nomme, versionne et appartenir a une liste autorisee.
Algorithme utilise dans cette implementation : - SHA256-V1 : SHA-256 (FIPS 180-4), sortie 32 octets
Note sur la liste autorisee : La specification (R20) exige que l'algorithme appartienne a "une liste autorisee" sans definir cette liste. Cette implementation utilise exclusivement SHA256-V1. L'extension a d'autres algorithmes (SHA-384, SHA-512) est hors perimetre de cette implementation et necessite une evolution du registre normatif.
Observable : hash_algorithm_id = "SHA256-V1" dans les metadonnees (TC-NOM-10)
5.10 Module MerkleDAGManager¶
Responsabilite : Gerer le partage de feuilles entre arbres sans reference croisee.
Interface :
interface MerkleDAGManager {
registerLeaf(leaf: Leaf, treeId: string): void;
getTreesForLeaf(leafHash: string): string[];
validateNoCircularReference(trees: FinalizedTree[]): boolean;
}
Contraintes : - R21 : Une feuille PEUT appartenir a plusieurs arbres - R22 : Aucun arbre NE DOIT referencer un autre arbre - R23 : Le graphe DOIT etre acyclique (DAG) - R24 : Pas de dependance temporelle, logique ou juridique
Observable : Racines independantes, pas de reference croisee (TC-NOM-11)
6. Schema de stockage¶
-- Arbres finalises
CREATE TABLE merkle_trees (
tree_id UUID PRIMARY KEY,
root VARCHAR(64) NOT NULL,
window_start TIMESTAMP WITH TIME ZONE NOT NULL,
window_end TIMESTAMP WITH TIME ZONE NOT NULL,
window_timezone VARCHAR(50) NOT NULL,
leaf_count INTEGER NOT NULL,
finalized_at TIMESTAMP WITH TIME ZONE NOT NULL,
canonicalization_id VARCHAR(30) NOT NULL,
leaf_ordering_id VARCHAR(30) NOT NULL,
hash_algorithm_id VARCHAR(30) NOT NULL,
proof_format_id VARCHAR(30) NOT NULL,
locked BOOLEAN NOT NULL DEFAULT TRUE,
CONSTRAINT chk_root_format CHECK (root ~ '^[a-f0-9]{64}$'),
CONSTRAINT chk_window_order CHECK (window_start < window_end),
CONSTRAINT chk_locked CHECK (locked = TRUE)
);
-- Feuilles (partagees potentiellement)
CREATE TABLE merkle_leaves (
leaf_hash VARCHAR(64) NOT NULL,
event_id UUID NOT NULL,
tree_id UUID NOT NULL REFERENCES merkle_trees(tree_id),
leaf_index INTEGER NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
PRIMARY KEY (tree_id, leaf_hash),
CONSTRAINT chk_leaf_format CHECK (leaf_hash ~ '^[a-f0-9]{64}$')
);
-- Index pour partage de feuilles
CREATE INDEX idx_leaves_by_hash ON merkle_leaves(leaf_hash);
-- Preuves d'inclusion
CREATE TABLE inclusion_proofs (
proof_id UUID PRIMARY KEY,
tree_id UUID NOT NULL REFERENCES merkle_trees(tree_id),
leaf_hash VARCHAR(64) NOT NULL,
proof_path JSONB NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
CONSTRAINT fk_leaf FOREIGN KEY (tree_id, leaf_hash)
REFERENCES merkle_leaves(tree_id, leaf_hash)
);
-- Niveaux de l'arbre (optionnel, pour reconstruction)
CREATE TABLE tree_levels (
tree_id UUID NOT NULL REFERENCES merkle_trees(tree_id),
level_index INTEGER NOT NULL,
node_index INTEGER NOT NULL,
node_hash VARCHAR(64) NOT NULL,
PRIMARY KEY (tree_id, level_index, node_index)
);
7. Flux detailles¶
7.1 Flux de construction d'arbre¶
events[]
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 1. VALIDATION (EventValidator) │
│ - Filtrer events[] pour la fenetre [start, end) │
│ - Exclure evenements invalides (E2) │
│ - Si vide → E1, pas d'arbre │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. CANONICALISATION (Canonicalizer) │
│ - Pour chaque event: canonical = JCS(event) │
│ - Si echec → E3, exclure │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. CONSTRUCTION FEUILLES (LeafBuilder) │
│ - Pour chaque canonical: leaf = SHA256(canonical) │
│ - Encoder en hex lowercase │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. TRI (LeafSorter) │
│ - Trier feuilles par ordre lexicographique ascendant │
│ - Ordre deterministe garanti │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 5. CONSTRUCTION ARBRE (TreeBuilder) │
│ - Niveau 0 = feuilles triees │
│ - Pour chaque niveau: │
│ - Grouper par paires │
│ - Si impair: dupliquer dernier │
│ - parent = SHA256(left || right) │
│ - Repeter jusqu'a racine unique │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 6. FINALISATION (RootFinalizer) │
│ - Associer metadonnees (window, algorithms) │
│ - Verrouiller l'arbre (locked = true) │
│ - Persister racine et structure │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 7. GENERATION PREUVES (ProofGenerator) │
│ - Pour chaque feuille: generer chemin d'inclusion │
│ - Persister preuves avec metadonnees │
└─────────────────────────────────────────────────────────────┘
│
▼
FinalizedTree
(root, proofs, metadata)
7.2 Flux de verification independante¶
┌─────────────────────────────────────────────────────────────┐
│ VERIFICATEUR EXTERNE │
└─────────────────────────────────────────────────────────────┘
│
│ Recoit: InclusionProof
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 1. EXTRAIRE METADONNEES │
│ - hash_algorithm_id → determiner fonction hash │
│ - leaf, root, path │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. RECALCULER LA RACINE │
│ current = leaf │
│ for step in path: │
│ if step.position == 'L': │
│ current = HASH(step.hash || current) │
│ else: │
│ current = HASH(current || step.hash) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. COMPARER │
│ return current == proof.root │
└─────────────────────────────────────────────────────────────┘
│
▼
VALIDE / INVALIDE
7.3 Diagrammes Mermaid¶
7.3.1 Graphe de dependances des modules¶
graph TD
EV[EventValidator<br/><i>R1-R3, 5.0</i>] --> CA[Canonicalizer<br/><i>R4-R5</i>]
CA --> LB[LeafBuilder<br/><i>R6-R7</i>]
LB --> LS[LeafSorter<br/><i>R8-R9</i>]
LS --> TB[TreeBuilder<br/><i>R10-R12</i>]
TB --> RF[RootFinalizer<br/><i>R13-R14</i>]
RF --> PG[ProofGenerator<br/><i>R17-R19</i>]
PG --> DAG[MerkleDAGManager<br/><i>R21-R24</i>]
HP[HashProvider<br/><i>R20</i>] -.->|SHA-256| LB
HP -.->|SHA-256| TB
HP -.->|SHA-256| PG
WM[WindowManager<br/><i>R15-R16</i>] -->|fenetre close| EV
DAG --> SL[(Storage Layer)]
RF --> SL
classDef engine fill:#e8f4fd,stroke:#2196f3
classDef infra fill:#fff3e0,stroke:#ff9800
classDef storage fill:#e8f5e9,stroke:#4caf50
class EV,CA,LB,LS,TB,RF,PG,DAG engine
class HP,WM infra
class SL storage 7.3.2 Sequence de construction d'arbre (flux principal)¶
sequenceDiagram
participant WM as WindowManager
participant EV as EventValidator
participant CA as Canonicalizer
participant LB as LeafBuilder
participant HP as HashProvider
participant LS as LeafSorter
participant TB as TreeBuilder
participant RF as RootFinalizer
participant PG as ProofGenerator
participant DAG as MerkleDAGManager
participant DB as Storage Layer
WM->>EV: fenetre close [start, end)
EV->>EV: filtrer events valides (R1-R3)
alt aucun event valide
EV-->>WM: E1 — pas d'arbre
end
EV->>CA: events[] valides
CA->>CA: JCS(event) pour chaque event (R4-R5)
alt echec canonicalisation
CA-->>EV: E3 — event exclu
end
CA->>LB: canonical[]
LB->>HP: demande SHA-256
HP-->>LB: hash hex lowercase
LB->>LS: leaves[]
LS->>LS: tri lexicographique ascendant (R8-R9)
LS->>TB: sorted_leaves[]
loop chaque niveau
TB->>HP: SHA-256(left || right)
HP-->>TB: parent hash
Note over TB: si impair, dupliquer dernier noeud
end
TB->>RF: arbre complet
RF->>RF: associer metadonnees + locked=true (R13-R14)
RF->>DB: persister racine + structure
RF->>PG: arbre finalise
loop chaque feuille
PG->>PG: calculer chemin d'inclusion (R17-R19)
end
PG->>DAG: preuves generees
DAG->>DAG: resoudre feuilles partagees (R21-R24)
DAG->>DB: persister preuves + metadonnees DAG 7.3.3 Sequence de verification independante¶
sequenceDiagram
participant V as Verificateur externe
participant P as InclusionProof
participant H as Fonction hash
V->>P: extraire leaf, root, path, hash_algorithm_id
V->>V: current = leaf
loop chaque step in path
alt step.position == 'L'
V->>H: SHA-256(step.hash || current)
else step.position == 'R'
V->>H: SHA-256(current || step.hash)
end
H-->>V: nouveau current
end
V->>V: comparer current == proof.root
alt match
V-->>V: VALIDE
else mismatch
V-->>V: INVALIDE
end 8. Mapping Invariants → Mecanismes techniques¶
| ID | Invariant | Mecanisme technique | Observable |
|---|---|---|---|
| I1 | Determinisme strict | Canonicalisation JCS + tri lexicographique + SHA-256 deterministe | Racine identique sur executions repetees (TC-INV-01) |
| I2 | Sensibilite aux modifications | Hash cryptographique : tout changement propage jusqu'a la racine | Racine differente si 1 bit change (TC-INV-02) |
| I3 | Cloture post-racine | Flag locked=true + contrainte DB + validation en ecriture | Rejet de tout ajout post-finalisation (TC-INV-03) |
| I4 | Independance inter-arbres | Pas de FK entre arbres + racine sans reference externe + validation DAG | Aucune reference croisee observable (TC-INV-04) |
| I5 | Partage sans impact | Feuilles identifiees par hash (content-addressed) + arbres construits independamment | Racines inchangees malgre partage (TC-INV-05) |
9. Mapping Regles normatives → Tests¶
| Regles | Description | Mecanisme | Test(s) | Observable |
|---|---|---|---|---|
| R1-R3 | Entree/validation | EventValidator filtre et valide | TC-NOM-01 | Arbre produit avec tous events valides |
| R4-R5 | Canonicalisation | JCS-RFC8785-V1 deterministe | TC-NOM-02 | Canoniques identiques pour memes events |
| R6-R7 | Construction feuilles | SHA256(canonical) + reutilisation possible | TC-NOM-03 | Feuille deterministe, partageable |
| R8-R9 | Ordonnancement | Tri lexicographique LEX-HEX-ASC-V1 | TC-NOM-04 | Ordre identique a chaque execution |
| R10-R12 | Construction arbre | Arbre binaire + HASH(L||R) + duplication | TC-NOM-05 | Structure binaire valide |
| R13-R14 | Racine/cloture | RootFinalizer + locked=true | TC-NOM-06 | Rejet apres cloture |
| R15-R16 | Periodicite | WindowManager + arbres independants | TC-NOM-07 | Arbres sans lien |
| R17 | Persistance | Schema DB complet | TC-NOM-08 | Racine, preuves, metadonnees presentes |
| R18-R19 | Verifiabilite | ProofGenerator + algo de verif | TC-NOM-09 | Preuve valide hors systeme |
| R20 | Hash versionne | HashProvider + liste autorisee | TC-NOM-10 | Algo versionne dans metadonnees |
| R21-R24 | Merkle DAG | MerkleDAGManager + validation acyclique | TC-NOM-11 | Pas de reference croisee |
| §5.0 | Identifiants normatifs | Metadonnees versionnees completes | TC-NOM-12 | Identifiants presents et valides |
| §5.0 | Attribution fenetres | Intervalle [start, end) semi-ouvert | TC-NOM-13 | Attribution correcte |
10. Gestion des erreurs¶
| Code | Situation | Point d'interception | Comportement | Trace |
|---|---|---|---|---|
| E1 | events[] vide | EventValidator.filterValid() | Aucun arbre produit | { "error": "E1", "window": "...", "message": "Empty batch" } |
| E2 | Evenement invalide (§5.0) | EventValidator.validate() | Exclusion de l'evenement | { "error": "E2", "event_id": "...", "reason": "NO_ID|NO_TIME|OUT_OF_WINDOW" } |
| E3 | Canonicalisation non deterministe | Canonicalizer.canonicalize() | Non-conformite, echec construction | { "error": "E3", "event_id": "...", "message": "Canonicalization failed" } |
| E4 | Algorithme non conforme | HashProvider.getAlgorithm() | Rejet de la construction | { "error": "E4", "algorithm": "...", "message": "Unknown or unversioned algorithm" } |
Observabilite : - Chaque erreur genere une trace structuree - Les erreurs E2 n'empechent pas la construction (exclusion) - Les erreurs E1, E3, E4 empechent la construction
11. Structure des preuves d'inclusion (MERKLE-PROOF-V1)¶
11.1 Format JSON¶
{
"version": "MERKLE-PROOF-V1",
"leaf": "a1b2c3d4...",
"root": "e5f6g7h8...",
"path": [
{ "hash": "1234abcd...", "position": "R" },
{ "hash": "5678efgh...", "position": "L" },
{ "hash": "9012ijkl...", "position": "R" }
],
"metadata": {
"tree_id": "uuid-xxx",
"window_start": "...",
"window_end": "...",
"window_timezone": "Europe/Paris",
"canonicalization_id": "JCS-RFC8785-V1",
"leaf_ordering_id": "LEX-HEX-ASC-V1",
"hash_algorithm_id": "SHA256-V1",
"proof_format_id": "MERKLE-PROOF-V1",
// Option 1: URL vers le registre normatif public (TC-NOM-12, R18)
"normative_registry_url": "https://example.com/normative-registry.json",
// Option 2: Definitions normatives inline (alternative si URL non disponible)
"normative_definitions": {
"SHA256-V1": { "reference": "FIPS 180-4", "output_size_bytes": 32 },
"MERKLE-PROOF-V1": { "concatenation": "binary", "hash_input": "left_bytes || right_bytes" }
// ... autres definitions
}
}
}
Note TC-NOM-12 : Au moins une des deux options (normative_registry_url ou normative_definitions) DOIT etre presente pour permettre la verification independante (R18).
11.2 Algorithme de verification¶
def verify_proof(proof, expected_root):
"""
Verification independante - aucune dependance au systeme emetteur.
IMPORTANT: La concatenation s'effectue sur les BYTES bruts (32 octets chacun),
conformement a R11 et au registre normatif MERKLE-PROOF-V1.concatenation.
"""
# 1. Charger les definitions normatives (depuis URL ou inline)
definitions = load_normative_definitions(proof)
# 2. Determiner l'algorithme de hash
algo = proof['metadata']['hash_algorithm_id']
if algo not in definitions:
raise UnsupportedAlgorithm(algo)
hash_fn = get_hash_function(definitions[algo]) # ex: sha256
# 3. Partir de la feuille (decoder hex -> bytes)
current = bytes.fromhex(proof['leaf']) # 32 bytes
# 4. Remonter le chemin
for step in proof['path']:
sibling = bytes.fromhex(step['hash']) # 32 bytes
# Concatenation sur les BYTES (32 + 32 = 64 octets)
if step['position'] == 'L':
# Sibling a gauche : sibling || current
current = hash_fn(sibling + current)
else:
# Sibling a droite : current || sibling
current = hash_fn(current + sibling)
# 5. Comparer avec la racine attendue
computed_root = current.hex() # Reencode en hex lowercase
return computed_root == expected_root
def load_normative_definitions(proof):
"""Charge les definitions normatives depuis l'URL ou inline."""
if 'normative_registry_url' in proof['metadata']:
return fetch_json(proof['metadata']['normative_registry_url'])
elif 'normative_definitions' in proof['metadata']:
return proof['metadata']['normative_definitions']
else:
raise MissingNormativeDefinitions()
12. Securite et contraintes¶
12.1 Independance des arbres¶
- Chaque arbre est auto-suffisant (racine + preuves + metadonnees)
- Pas de FK entre tables
merkle_trees - Validation DAG : parcours du graphe pour detecter cycles
12.2 Confidentialite (objectif de conception, non testable - Spec §9)¶
Note : Conformement a §9 de la specification, la confidentialite est un objectif de conception et non une exigence testable. Les points ci-dessous sont des proprietes visees, pas des contraintes contractuelles :
- Seuls les hashes sont exposes (pas les evenements en clair)
- La racine ne revele pas le contenu des evenements
- Les preuves minimisent l'information divulguee
Ces proprietes ne font pas l'objet de tests d'acceptation.
12.3 Integrite¶
- Verrouillage post-finalisation (
locked=true) - Contraintes DB pour empecher modification
- Hash cryptographique pour detecter alteration
12.4 Retrocompatibilite¶
- Toute evolution de version des identifiants DOIT etre retrocompatible
- Meme ensemble d'evenements → meme racine (invariant contractuel)
13. Hypotheses techniques¶
13.1 Hypotheses heritees de la spec¶
| ID | Hypothese | Mecanisme d'enforcement | Impact si faux |
|---|---|---|---|
| H-01 | events[] est fini et clos avant construction | WindowManager.closeWindow() verrouille la fenetre ; EventValidator rejette tout ajout post-cloture | Construction non deterministe |
| H-02 | Evenements validés avant inclusion | EventValidator.validate() filtre les evenements invalides | Evenements invalides dans l'arbre |
| H-03 | Fenetres non chevauchantes | WindowManager.validateNoOverlap() + registerWindow() rejette les chevauchements | Attribution ambigue |
13.2 Hypotheses d'implementation¶
| ID | Hypothese | Impact si faux |
|---|---|---|
| H-04 | Implementation JCS conforme RFC 8785 | Canonicalisation non deterministe |
| H-05 | Implementation SHA-256 conforme FIPS 180-4 | Hashes incorrects |
| H-06 | Base IANA TZ disponible et a jour | Attribution fenetre incorrecte |
| H-07 | Stockage garantit l'atomicite | Arbres partiellement ecrits |
| H-08 | Horloge systeme fiable pour finalized_at | Metadonnees incorrectes |
| H-09 | Le format temporel choisi pour event_time, window_start, window_end permet une comparaison deterministe et une attribution univoque a une fenetre | Attribution incorrecte des evenements |
| H-10 | Le registre normatif est accessible (URL publique ou inline) lors de la verification independante | TC-NOM-12 non realisable |
14. Points de vigilance¶
14.1 Risques¶
| Risque | Probabilite | Impact | Mitigation |
|---|---|---|---|
| Implementation JCS non conforme | Moyenne | Racines differentes entre systemes | Tests de conformite RFC 8785 |
| Ordre de tri non deterministe | Faible | I1 viole | Tests TC-NOM-04 exhaustifs |
| Fuite d'evenements en clair | Faible | Confidentialite compromise | Revue de code + tests negatifs |
| Modification post-cloture | Faible | I3 viole | Contraintes DB + tests TC-NOM-06 |
14.2 Tests de non-regression¶
- TC-NR-01 : Meme entree + nouvelle version → racine inchangee
- TC-NR-02 : Ajout d'un arbre → arbres existants inchanges
14.3 Tests adversariaux¶
- TC-NEG-01 : Injection evenement post-cloture → rejet
- TC-NEG-02 : Collision volontaire entree → racine differente
14.4 Cas limites¶
- 1 evenement : arbre a 1 feuille, racine = feuille
- 2 evenements : arbre minimal
- Nombre impair : verification duplication correcte
- Feuilles identiques dans le meme arbre : tri stable requis
15. Hors perimetre (rappel)¶
Explicitement exclus de cette implementation :
- Horodatage eIDAS (RFC 3161)
- Ancrage blockchain
- Signature cryptographique des racines
- Transport, stockage physique ou replication
- Validation metier ou semantique des evenements
16. Vecteurs de test de reference¶
VEC-01 : Arbre minimal (2 feuilles)¶
Events:
- { id: "evt-1", event_time: "2024-01-01T10:00:00+01:00", payload: { type: "A" } }
- { id: "evt-2", event_time: "2024-01-01T11:00:00+01:00", payload: { type: "B" } }
Window: [2024-01-01T00:00:00+01:00, 2024-01-02T00:00:00+01:00), Europe/Paris
Canonicals (JCS):
- evt-1: {"event_time":"2024-01-01T10:00:00+01:00","id":"evt-1","payload":{"type":"A"}}
- evt-2: {"event_time":"2024-01-01T11:00:00+01:00","id":"evt-2","payload":{"type":"B"}}
Leaves (SHA256, hex):
- leaf_1 = SHA256(canonical_1) = <hash_1>
- leaf_2 = SHA256(canonical_2) = <hash_2>
Sorted (lexicographic):
- [min(hash_1, hash_2), max(hash_1, hash_2)]
Root:
- root = SHA256(sorted[0] || sorted[1])
VEC-02 : Arbre impair (3 feuilles)¶
Leaves: [L1, L2, L3] (sorted)
Level 0: [L1, L2, L3]
Level 1: [H(L1||L2), H(L3||L3)] // L3 duplique
Level 2: [H(Level1[0]||Level1[1])] = Root
VEC-03 : Feuille partagee¶
Tree A: events [E1, E2]
Tree B: events [E2, E3]
E2 produit la meme feuille dans les deux arbres.
Root_A != Root_B (car ensembles differents)
Proof pour E2 dans A valide contre Root_A
Proof pour E2 dans B valide contre Root_B
17. Checklist de livraison¶
- Module EventValidator implemente et teste (TC-NOM-01, TC-ERR-02)
- Module Canonicalizer implemente (JCS-RFC8785-V1) et teste (TC-NOM-02, TC-ERR-03)
- Module LeafBuilder implemente et teste (TC-NOM-03)
- Module LeafSorter implemente et teste (TC-NOM-04)
- Module TreeBuilder implemente et teste (TC-NOM-05)
- Module WindowManager implemente et teste (TC-NOM-07, TC-NOM-13)
- Module RootFinalizer implemente et teste (TC-NOM-06)
- Module ProofGenerator implemente et teste (TC-NOM-09)
- Module HashProvider implemente et teste (TC-NOM-10, TC-ERR-04)
- Module MerkleDAGManager implemente et teste (TC-NOM-11)
- Schema de stockage deploye (TC-NOM-08)
- Metadonnees normatives completes (TC-NOM-12)
- Tests d'invariants passes (TC-INV-01 a TC-INV-05)
- Tests de non-regression passes (TC-NR-01, TC-NR-02)
- Tests adversariaux passes (TC-NEG-01, TC-NEG-02)
- Verificateur independant fonctionnel
- Documentation API
References¶
- Specification : PD-54-specification.md
- Tests : PD-54-tests.md
- Epic : PD-189 Crypto
- JIRA : PD-54
- Standards : RFC 8785 (JCS), RFC 6962 (Certificate Transparency), FIPS 180-4 (SHA-256)