Lorsqu’une ressource hors ligne est téléchargée par blocs sous iOS, chaque bloc peut passer les contrôles sans que la ressource reconstituée corresponde à la bonne version : le client a pu accepter des blocs associés à un autre manifeste, ou le générateur et le vérificateur ne traitent pas de la même manière les niveaux comportant un nombre impair de nœuds. Installer le vérificateur dans un environnement Swift figé sur un Mac cloud permet d’abord de valider le protocole sur de petits arbres, puis de l’intégrer au téléchargement réel. Cette démarche facilite le diagnostic par rapport à une simple comparaison du hachage global une fois le téléchargement terminé.
Définir le protocole de l’arbre et les entrées de confiance
Cet article utilise SHA-256. Une feuille se calcule par H(0x00 || octets bruts du bloc) et un nœud interne par H(0x01 || hachage gauche || hachage droit). Les préfixes distinguent les deux types d’entrées. La concaténation porte sur les 32 octets bruts de chaque hachage, et non sur leur représentation hexadécimale.
Lorsqu’un nœud reste seul à un niveau, il est directement remonté au niveau suivant, sans être dupliqué. La preuve est ordonnée de la feuille vers la racine et ne contient que les nœuds frères réellement présents. Leur position à gauche ou à droite se déduit de l’indice de la feuille et de la largeur du niveau courant.
Le manifeste doit lier la version de la ressource, la taille des blocs, le nombre total d’octets, le nombre de feuilles et la racine. Le dernier bloc peut être plus court que la taille standard. Le cas d’une ressource vide doit être défini séparément, sans laisser le vérificateur le deviner.
Une preuve de Merkle répond à la question « Ce bloc appartient-il à la racine indiquée ? », et non à la question « Qui a publié cette racine ? ». La racine et le nombre de feuilles doivent provenir d’un canal de confiance ou d’un manifeste dont la signature a été vérifiée.
Implémenter un vérificateur de preuves strict
Sur le Mac cloud, enregistrez le contenu suivant dans MerkleCheck.swift. Exécutez d’abord xcrun swift --version pour consigner la chaîne d’outils, puis xcrun swift MerkleCheck.swift. Le code ne dépend que des frameworks système : aucun paquet tiers n’est à télécharger.
import Foundation
import CryptoKit
func leaf(_ bytes: Data) -> Data {
Data(SHA256.hash(data: Data([0]) + bytes))
}
func parent(_ left: Data, _ right: Data) -> Data {
Data(SHA256.hash(data: Data([1]) + left + right))
}
func verify(
chunk: Data,
index: Int,
count: Int,
siblings: [Data],
root: Data
) -> Bool {
guard count > 0, index >= 0, index < count,
root.count == 32,
siblings.allSatisfy({ $0.count == 32 })
else { return false }
var value = leaf(chunk)
var position = index
var width = count
var consumed = 0
while width > 1 {
let hasSibling = position % 2 == 1 || position < width - 1
if hasSibling {
guard consumed < siblings.count else { return false }
let sibling = siblings[consumed]
value = position % 2 == 0
? parent(value, sibling)
: parent(sibling, value)
consumed += 1
}
position /= 2
width = width / 2 + width % 2
}
return consumed == siblings.count && value == root
}
func require(_ condition: Bool, _ name: String) {
guard condition else {
fatalError("Check failed: \(name)")
}
}
let a = Data("A".utf8)
let b = Data("B".utf8)
let c = Data("C".utf8)
let ab = parent(leaf(a), leaf(b))
let root = parent(ab, leaf(c))
require(verify(chunk: c, index: 2, count: 3,
siblings: [ab], root: root), "odd promotion")
require(verify(chunk: a, index: 0, count: 3,
siblings: [leaf(b), leaf(c)], root: root), "left branch")
require(!verify(chunk: Data("X".utf8), index: 2, count: 3,
siblings: [ab], root: root), "tampered chunk")
require(!verify(chunk: c, index: 2, count: 3,
siblings: [ab, ab], root: root), "excess proof")
require(!verify(chunk: c, index: 3, count: 3,
siblings: [ab], root: root), "invalid index")
En cas de succès, le script se termine sans rien afficher ; en cas d’échec, il renvoie un code de sortie non nul. Archivez le statut d’exécution. Ne remplacez pas le contrôle du code de sortie par la recherche d’un message de réussite particulier.
Couvrir les cas limites de structure avec de petits arbres
Dans un arbre à trois feuilles, la troisième feuille est directement remontée au premier niveau : sa preuve ne nécessite donc qu’un seul hachage frère. Si le générateur duplique le dernier nœud, la racine obtenue sera différente. Il ne s’agit pas d’une erreur réseau, et relancer le téléchargement n’est pas la solution.
Après avoir intégré le script à une cible de tests, ajoutez au minimum les vérifications suivantes :
- Arbre à une feuille : une preuve vide est acceptée ; l’ajout de n’importe quel nœud entraîne un rejet.
- Arbre à deux feuilles : les feuilles gauche et droite sont acceptées à leur position respective ; l’inversion des positions entraîne un rejet.
- Arbres à trois et à cinq feuilles : la dernière feuille est correctement remontée ; un nœud de preuve en moins ou en trop entraîne un rejet.
- Entrées invalides : les indices négatifs, un nombre de feuilles nul, les hachages trop courts et les indices hors limites sont tous rejetés.
L’exemple construit la racine avec les mêmes fonctions de hachage que le vérificateur : il ne valide donc que le flux de contrôle. Faites également produire des vecteurs de test fixes par une implémentation indépendante, puis versionnez les valeurs hexadécimales des octets des blocs, de la racine et des preuves. Vous éviterez ainsi qu’une même erreur soit partagée par le générateur et le vérificateur. Si deux positions contiennent exactement les mêmes données, les seuls octets du bloc ne permettent pas de les distinguer. Si l’identité doit être liée à la position, le protocole doit intégrer l’indice ainsi qu’un encodage explicite des entiers.
Vérifier avant d’écrire
La couche de téléchargement commence par contrôler la taille et l’indice du bloc à partir du manifeste, puis vérifie la preuve. Une fois ces contrôles réussis, elle écrit le bloc dans le répertoire temporaire propre à cette version de la ressource. Lorsque tous les blocs sont présents, elle vérifie la taille totale et publie un marqueur de fin. L’interface ne doit pas assimiler « le fichier existe » à « la ressource est disponible ».
Une preuve est une entrée non fiable. Avant de l’analyser, limitez la taille du corps de réponse, le nombre de nœuds et la longueur des hachages. La taille d’une preuve valide croît tout au plus avec le nombre de niveaux de l’arbre : les tableaux de taille illimitée ne doivent pas être acceptés. Limitez aussi la taille des blocs bruts pour éviter que leur chargement intégral n’épuise la mémoire avant même la vérification.
Les nouvelles tentatives doivent conserver le même instantané du manifeste. Si une nouvelle version est détectée pendant le téléchargement, il faut arrêter l’ancienne tâche ou poursuivre avec l’ancienne version, mais jamais vérifier d’anciens blocs avec la nouvelle racine. Les journaux de validation peuvent se limiter à la version de la ressource, à l’indice du bloc et à la catégorie d’échec. Ils ne doivent contenir ni le contenu des blocs ni les identifiants d’accès.
Obtenir des résultats de non-régression explicables
Avec la chaîne d’outils figée, exécutez séparément les vecteurs valides et les vecteurs malveillants. Si une entrée correcte est rejetée, examinez en priorité la règle appliquée aux niveaux impairs, l’ordre des nœuds de la preuve et l’encodage des octets. Si une entrée incorrecte est acceptée, cherchez d’abord un contrôle manquant sur l’indice, les nœuds de preuve restants ou la provenance de confiance de la racine.
Terminez par une injection de faute de bout en bout : modifiez un octet d’un bloc temporaire, puis vérifiez que ce bloc est rejeté et qu’aucun marqueur de fin n’est créé. Rétablissez ensuite le bloc correct et confirmez que la tâche peut reprendre. Le critère d’acceptation n’est pas « le code de hachage s’est exécuté jusqu’au bout », mais bien l’impossibilité pour toute donnée dont la preuve n’a pas été validée d’intégrer l’ensemble des ressources accessibles en lecture.
Questions fréquentes
Peut-on télécharger la racine avec les ressources ?
Oui, mais la racine et le nombre de feuilles doivent être authentifiés par un canal de confiance ou un manifeste signé dont la signature a été vérifiée. Sinon, les deux peuvent être remplacés.
Faut-il dupliquer le dernier nœud d'un niveau impair ?
Le protocole présenté le fait remonter sans modification et sans consommer de hachage voisin. La duplication constitue une autre convention, à ne pas mélanger avec celle-ci.
Confiez votre machine de build à notre centre de données
Louez un Mac mini physique dédié et exécutez vos builds Xcode et votre CI macOS sur une machine disponible toute l’année. Choisissez la configuration selon vos besoins et le nœud selon l’emplacement de votre équipe.
Choisir un Mac cloud