iOS-Merkle-Beweise für Datenblöcke auf dem Cloud-Mac prüfen

iOS-Merkle-Beweise für Datenblöcke auf dem Cloud-Mac prüfen

Werden Offline-Ressourcen unter iOS blockweise heruntergeladen, kann trotz erfolgreicher Prüfung jedes einzelnen Blocks am Ende die falsche Version entstehen: Der Client hat möglicherweise Blöcke aus einem anderen Manifest akzeptiert, oder Erzeuger und Prüfer behandeln ungerade Knotenzahlen unterschiedlich. Ein Prüfer in einer festgelegten Swift-Umgebung auf einem Cloud-Mac hilft, solche Fehler einzugrenzen: Zuerst wird das Protokoll anhand kleiner Bäume geprüft, dann folgt die Einbindung in den tatsächlichen Downloadablauf. So lassen sich Ursachen leichter finden als mit einem einzigen Gesamthash nach Abschluss des Downloads.

Baumprotokoll und vertrauenswürdige Eingaben festlegen

Dieser Beitrag verwendet SHA-256. Blätter werden als H(0x00 || ursprüngliche Blockbytes) berechnet, innere Knoten als H(0x01 || linker Hash || rechter Hash). Die Präfixe unterscheiden die beiden Eingabetypen. Verkettet werden die unveränderten 32 Hashbytes, nicht deren hexadezimale Zeichenfolgen.

Bleibt auf einer Ebene ein einzelner Knoten übrig, wird er unverändert auf die nächste Ebene übernommen und nicht mit sich selbst dupliziert. Der Beweis ist vom Blatt zur Wurzel geordnet und enthält nur tatsächlich vorhandene Geschwisterknoten. Ob ein Geschwister links oder rechts steht, ergibt sich aus dem Blattindex und der Breite der aktuellen Ebene.

Das Manifest muss Ressourcenversion, Blocklänge, Gesamtbytezahl, Blattanzahl und Wurzel verbindlich miteinander verknüpfen. Der letzte Block darf kürzer als die reguläre Blocklänge sein. Der Fall einer leeren Ressource muss gesondert definiert werden; der Prüfer darf hier keine Annahmen treffen.

Ein Merkle-Beweis beantwortet die Frage „Gehört dieser Block zur angegebenen Wurzel?“, nicht aber „Wer hat diese Wurzel veröffentlicht?“. Wurzel und Blattanzahl müssen aus einem vertrauenswürdigen Kanal oder einem Manifest mit verifizierter Signatur stammen.

Einen strikten Beweisprüfer implementieren

Speichern Sie den folgenden Inhalt auf dem Cloud-Mac als MerkleCheck.swift. Führen Sie zunächst xcrun swift --version aus, um die Toolchain zu dokumentieren, und anschließend xcrun swift MerkleCheck.swift. Der Code verwendet ausschließlich System-Frameworks; Pakete von Drittanbietern müssen nicht heruntergeladen werden.

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

Bei Erfolg endet das Skript ohne Ausgabe, bei einem Fehler mit einem Exitcode ungleich null. Archivieren Sie den Ausführungsstatus. Suchen Sie nicht ersatzweise nach einer bestimmten Erfolgsmeldung, statt den Exitcode auszuwerten.

Strukturelle Grenzfälle mit kleinen Bäumen abdecken

In einem Baum mit drei Blättern wird das dritte Blatt auf der ersten Ebene unverändert weitergereicht. Deshalb benötigt sein Beweis nur einen Geschwisterhash. Dupliziert der Erzeuger stattdessen den letzten Knoten, entsteht eine andere Wurzel. Das ist kein Netzwerkfehler und sollte nicht durch erneute Downloadversuche behoben werden.

Nach der Übernahme des Skripts in ein Test-Target sollten Sie mindestens die folgenden Prüfungen ergänzen:

Das Beispiel erzeugt die Wurzel mit denselben Hashfunktionen wie der Prüfer und prüft damit lediglich den Kontrollfluss. Lassen Sie deshalb zusätzlich feste Testvektoren von einer unabhängigen Implementierung erzeugen und nehmen Sie die Blockbytes, die Wurzel und den Beweis als Hexadezimalwerte in die Versionsverwaltung auf. So vermeiden Sie, dass Erzeuger und Prüfer denselben Fehler teilen. Ist der Inhalt an verschiedenen Positionen identisch, lässt sich die Identität allein anhand der Blockbytes nicht unterscheiden. Wenn eine Bindung an die Position erforderlich ist, muss das Protokoll den Index und eine eindeutig definierte Ganzzahlkodierung einbeziehen.

Vor dem Schreiben prüfen

Die Downloadschicht gleicht zunächst Blocklänge und Index mit dem Manifest ab und prüft anschließend den Beweis. Erst nach erfolgreicher Prüfung wird der Block in das temporäre Verzeichnis der jeweiligen Ressourcenversion geschrieben. Sobald alle Blöcke vorliegen, werden die Gesamtlänge geprüft und eine Abschlussmarkierung gesetzt. Die Benutzeroberfläche darf „Datei vorhanden“ nicht mit „Ressource verfügbar“ gleichsetzen.

Beweise sind nicht vertrauenswürdige Eingaben. Begrenzen Sie vor dem Parsen die Größe des Antwortinhalts, die Knotenanzahl und die Hashlänge. Die Länge eines gültigen Baumbeweises wächst höchstens mit der Anzahl der Ebenen; unbegrenzt große Arrays dürfen nicht akzeptiert werden. Begrenzen Sie außerdem die Größe der ursprünglichen Datenblöcke, damit nicht schon das vollständige Laden eines Blocks vor der Prüfung den Arbeitsspeicher erschöpft.

Wiederholungsversuche müssen denselben Manifest-Snapshot verwenden. Wird während des Downloads eine neue Version erkannt, muss der alte Auftrag beendet oder mit der alten Version fortgesetzt werden. Alte Blöcke dürfen nicht gegen die neue Wurzel geprüft werden. Für die Abnahmeprotokolle genügen Ressourcenversion, Blockindex und Fehlerkategorie. Blockinhalte und Zugangsdaten gehören nicht in die Protokolle.

Nachvollziehbare Regressionsergebnisse schaffen

Führen Sie mit der festgelegten Toolchain sowohl gültige als auch bösartig konstruierte Testvektoren aus. Werden korrekte Eingaben abgelehnt, prüfen Sie zuerst die Regeln für Ebenen mit ungerader Knotenzahl, die Reihenfolge der Beweisknoten und die Bytekodierung. Werden fehlerhafte Eingaben akzeptiert, prüfen Sie zunächst, ob die Kontrolle des Index, übrig gebliebener Beweisknoten oder der vertrauenswürdigen Herkunft der Wurzel fehlt.

Injizieren Sie abschließend einen Fehler für einen Ende-zu-Ende-Test: Ändern Sie ein Byte eines zwischengespeicherten Blocks und stellen Sie sicher, dass dieser Block abgelehnt und keine Abschlussmarkierung erzeugt wird. Stellen Sie danach den korrekten Block wieder her und prüfen Sie, ob der Auftrag fortgesetzt werden kann. Das Abnahmekriterium lautet nicht „Der Hashcode ist durchgelaufen“, sondern: Daten ohne erfolgreich geprüften Beweis dürfen unter keinen Umständen in den lesbaren Ressourcenbestand gelangen.

Häufig gestellte Fragen

Darf die Merkle-Wurzel zusammen mit den Daten geladen werden?

Ja, aber Wurzel und Blattanzahl müssen über einen vertrauenswürdigen Kanal oder ein geprüftes signiertes Manifest authentifiziert werden. Sonst können Daten und Wurzel gemeinsam ersetzt werden.

Wird der letzte Knoten einer ungeraden Ebene verdoppelt?

In diesem Protokoll wird er unverändert hochgereicht und benötigt keinen Geschwisterhash. Verdopplung ist eine andere Regel, die beide Seiten ausdrücklich vereinbaren müssten.

Build-Maschine ans Rechenzentrum auslagern

Mieten Sie einen exklusiven physischen Mac mini und betreiben Sie Xcode-Builds und macOS-CI auf einer rund um die Uhr verfügbaren Maschine. Wählen Sie die Ausstattung passend zu Ihren Aufgaben und den Standort passend zu Ihrem Team.

Cloud-Mac auswählen

Build-Maschine ans Rechenzentrum auslagern

Exklusiver physischer Mac mini – 365 Tage im Jahr online.