Ed25519 für iOS auf dem Cloud-Mac prüfen: Testvektoren und Bytes

Ed25519 für iOS auf dem Cloud-Mac prüfen: Testvektoren und Bytes

Ed25519 für iOS auf dem Cloud-Mac prüfen: Testvektoren und Bytes

Bei der Übergabe von Signaturkonfigurationen zwischen Teams in verschiedenen Regionen tritt häufig dasselbe Problem auf: „Der Server signiert erfolgreich, aber die Prüfung unter iOS schlägt fehl.“ Beide Seiten zeigen identisches JSON an, und auch neu erzeugte Schlüssel helfen nicht weiter. Um Fehler im Kryptografieaufruf, bei der Transportdekodierung und beim Rekonstruieren der Nachricht auseinanderzuhalten, braucht es zunächst eine von den Anwendungsschnittstellen unabhängige Referenzprüfung auf dem Cloud-Mac. Danach werden die Rohbytes verglichen. Die folgenden Schritte verwenden Swift und CryptoKit und arbeiten ausschließlich mit öffentlichen Testdaten. Private Produktionsschlüssel sind nicht erforderlich.

Zuerst Algorithmus und Eingabeformat festlegen

Als Algorithmus wird gewöhnliches Ed25519 vereinbart. Der öffentliche Schlüssel liegt als 32 Byte lange Rohdarstellung vor, die Signatur als 64 Byte lange Rohdarstellung. In CryptoKit ist dafür Curve25519.Signing.PublicKey vorgesehen. Trotz ähnlicher Namen darf dieser Typ nicht durch einen Typ für den Schlüsselaustausch ersetzt werden.

Die signierende Seite muss angeben, ob sie die unveränderten Bytes des Antwortbodys oder das Ergebnis eines Kanonisierungsverfahrens signiert. Werden die Rohbytes des Bodys signiert, muss der Client zuerst die Signatur prüfen und anschließend den Inhalt parsen. Er darf nicht erst das JSON parsen, neu kodieren und dann prüfen. Ist eine Kanonisierung vereinbart, müssen deren Regelversion und die zugehörigen Testfälle feststehen.

Eine Ed25519-Signatur bestätigt, dass die Nachricht zum vertrauenswürdigen öffentlichen Schlüssel passt. Sie bietet weder Vertraulichkeit noch automatisch Schutz vor Replay-Angriffen. Stammt der öffentliche Schlüssel aus derselben austauschbaren Antwort, reicht auch eine erfolgreiche Signaturprüfung nicht aus, um Vertrauen herzustellen.

Außerdem muss zwischen gewöhnlichem Ed25519 und Ed25519ph unterschieden werden. Eine Nachricht zunächst zu hashen und den Hash anschließend an die gewöhnliche Schnittstelle zu übergeben, ist nicht dasselbe wie die Verwendung der Prehash-Variante. Das Protokoll darf diese Bedingungen nicht mit dem Hinweis auslassen, es sei „alles Ed25519“.

Die minimale Prüfung mit einem öffentlichen Standardvektor durchführen

Auf einem Cloud-Mac mit bereits installierten Entwicklungswerkzeugen zunächst xcrun swift --version ausführen und die Toolchain-Version sowie die Ausgabe von sw_vers festhalten. Den folgenden Inhalt als verify.swift speichern und anschließend mit xcrun swift verify.swift ausführen. Der Code verwendet den Testvektor für eine leere Nachricht aus RFC 8032. Die öffentlichen Hexadezimaldaten sind keine Schlüssel aus dem Anwendungsbetrieb.

import Foundation
import CryptoKit

enum FixtureError: Error {
    case invalidHex
    case checkFailed
}

func hex(_ text: String) throws -> Data {
    let chars = Array(text.utf8)
    guard chars.count % 2 == 0 else {
        throw FixtureError.invalidHex
    }
    var bytes = [UInt8]()
    for offset in stride(from: 0, to: chars.count, by: 2) {
        let pair = String(
            decoding: chars[offset..<(offset + 2)],
            as: UTF8.self
        )
        guard let value = UInt8(pair, radix: 16) else {
            throw FixtureError.invalidHex
        }
        bytes.append(value)
    }
    return Data(bytes)
}

func require(_ value: Bool) throws {
    guard value else {
        throw FixtureError.checkFailed
    }
}

let publicBytes = try hex(
    "d75a980182b10ab7d54bfed3c964073a0" +
    "ee172f3daa62325af021a68f707511a"
)
let signature = try hex(
    "e5564300c360ac729086e2cc806e828a8" +
    "4877f1eb8e5d974d873e0652249015555" +
    "5fb8821590a33bacc61e39701cf9b46bd" +
    "25bf5f0595bbe24655141438e7a100b"
)
try require(publicBytes.count == 32)
try require(signature.count == 64)

let key = try Curve25519.Signing.PublicKey(
    rawRepresentation: publicBytes
)
try require(key.isValidSignature(signature, for: Data()))
try require(!key.isValidSignature(
    signature, for: Data([0x00])
))

var altered = signature
altered[altered.startIndex] ^= 1
try require(!key.isValidSignature(altered, for: Data()))
try require(!key.isValidSignature(
    Data(signature.dropLast()), for: Data()
))

var rejectedKey = false
do {
    _ = try Curve25519.Signing.PublicKey(
        rawRepresentation: Data(publicBytes.dropLast())
    )
} catch {
    rejectedKey = true
}
try require(rejectedKey)

Bei erfolgreichem Durchlauf gibt das Skript nichts aus. Schlägt eine Prüfung fehl, wird ein Fehler ausgelöst und das Skript mit einem Exit-Status ungleich null beendet. Hier werden bewusst Prüfungen verwendet, die Fehler auslösen, statt assert: So können die Prüfungen bei optimierter Kompilierung nicht entfernt werden. Erst wenn dieser feste Datensatz erfolgreich geprüft wurde, sollten Testdaten aus der Anwendung hinzukommen. Andernfalls vergrößert die gleichzeitige Fehlersuche in Netzwerk- und Kryptografieschnittstellen die Zahl der möglichen Ursachen.

Textunterschiede auf Byteunterschiede eingrenzen

Eine Datei mit der Nachricht vor dem Signieren und eine weitere mit den tatsächlich beim Client angekommenen Daten aufbewahren. Öffentliche Testdateien lassen sich mit cmp vergleichen; mit xxd -g 1 kann anschließend die erste Abweichung lokalisiert werden. Bodies aus dem Produktivbetrieb können sensible Informationen enthalten und gehören nicht ungeprüft in gemeinsam genutzte Logs.

Die Prüfung beginnt bei Inhalten, die Editoren leicht verändern: Gibt es einen zusätzlichen Zeilenumbruch am Dateiende? Werden LF oder CRLF verwendet? Beginnt die Datei mit einer UTF-8-BOM? Wurden Zeichen einer Unicode-Normalisierung unterzogen? Auch optisch identische Zeichen mit Akzent können aus unterschiedlichen Bytefolgen bestehen.

Dateien nicht beiläufig mit echo erzeugen, da der Befehl einen Zeilenumbruch anhängen kann. Die Nachricht direkt als Rohdaten aus der Datei lesen, statt sie zunächst in eine Zeichenkette und danach wieder in Daten umzuwandeln. Signiert das Protokoll tatsächlich Zeichenketten, muss UTF-8 ausdrücklich festgelegt werden. Für abschließende Zeilenumbrüche, leere Nachrichten und kombinierende Zeichen sind jeweils eigene Testdaten anzulegen.

Das Transportformat separat prüfen

Überträgt die Schnittstelle Base64, wird zuerst dekodiert und danach die Länge geprüft. 128 Hexadezimalzeichen sind keine 128 Byte lange Signatur. Auch ein öffentlicher Schlüssel in Rohdarstellung ist nicht mit einer Schlüsseldatei samt Header-Informationen gleichzusetzen. Die vollständigen Daten eines solchen Containerformats dürfen nicht direkt an rawRepresentation übergeben werden.

Schlägt die Dekodierung fehl, darf die Prüfung nicht mit leeren Daten als Ersatz fortgesetzt werden. Intern sollten „ungültiges Transportformat“, „falsche Länge“ und „Signatur stimmt nicht überein“ getrennte Fehlertypen sein. Nach außen sollten dagegen keine unnötigen Details zur Verarbeitung offengelegt werden.

Gegenproben erstellen, die Fehler bei der Einbindung aufdecken

Der minimale Testsatz muss mindestens Folgendes enthalten: die korrekte Nachricht mit der korrekten Signatur, eine Nachricht mit einem zusätzlich angehängten Nullbyte, eine Signatur mit einem umgekippten Bit, eine gekürzte Signatur, einen gekürzten öffentlichen Schlüssel sowie die Prüfung der ursprünglichen Signatur mit einem anderen gültigen öffentlichen Schlüssel. Für den letzten Fall muss tatsächlich ein anderer gültiger öffentlicher Schlüssel verwendet werden. Es reicht nicht, durch einen beschädigten Schlüssel lediglich einen Fehler bei der Initialisierung auszulösen.

Auf Anwendungsebene kommt ein weiterer Fall hinzu: Der Body ist korrekt, die Signatur gehört jedoch zur vorherigen Nachricht. Damit wird geprüft, ob der Code fälschlicherweise zwischengespeicherte Ergebnisse wiederverwendet. Enthält der signierte Inhalt eine Version, eine Gültigkeitsdauer oder eine Zweckkennung, muss außerdem sichergestellt werden, dass diese Felder tatsächlich Bestandteil der signierten Bytes sind und nicht nur im unsignierten äußeren Objekt stehen.

Diese Tests prüfen die Ablehnung fehlerhafter Eingaben. Zufällige Beschädigungen sind kein Ersatz für feste Testdaten. Nur mit festen Fällen können Teammitglieder auf unterschiedlichen Knoten und in unterschiedlichen Schichten denselben Fehler reproduzieren.

Die regionsübergreifende Übergabe mit einer Nachweisliste abschließen

Bei der Übergabe werden Commit-ID, Betriebssystem- und Toolchain-Version, Algorithmusvariante, Kennung des öffentlichen Schlüssels, Nachrichtenlänge in Byte, Signaturlänge in Byte und Testergebnisse festgehalten. Öffentliche Testdaten können gemeinsam mit diesen Angaben gespeichert werden. Echte Anwendungsnachrichten müssen entsprechend ihrer Sensibilität unter kontrollierten Zugriffsbedingungen aufbewahrt werden. Auch ein Hash kann die ursprüngliche Nachricht bei der Signaturprüfung nicht ersetzen.

Sobald die Referenzprüfung auf der Kommandozeile erfolgreich ist, werden dieselben Testdaten in das iOS-Test-Target übernommen. Dort muss die von der App tatsächlich verwendete Kapselung der Signaturprüfung aufgerufen werden. Nur so werden auch die Base64-Dekodierung, die Auswahl des öffentlichen Schlüssels und der Lesepfad für die Nachricht erfasst, statt lediglich die Funktionsfähigkeit von CryptoKit selbst nachzuweisen.

Abgeschlossen ist die Prüfung nicht, wenn sie „einmal funktioniert hat“. Korrekte Eingaben müssen zuverlässig akzeptiert, sämtliche Gegenbeispiele eindeutig abgelehnt werden. Beide Seiten müssen sich über die signierten Bytes einig sein, und die nächste Schicht muss alle Prüfungen ohne privaten Schlüssel erneut ausführen können.

Häufig gestellte Fragen

Braucht die Prüfung auf dem Client den privaten Schlüssel?

Nein. Ein vertrauenswürdiger öffentlicher Schlüssel, die ursprünglichen Nachrichtenbytes und die Signatur reichen aus. Private Produktionsschlüssel gehören nicht in die Prüfumgebung.

Warum scheitert die Prüfung bei inhaltlich gleichen JSON-Objekten?

Ed25519 prüft Bytes statt JSON-Semantik. Schlüsselreihenfolge, Leerzeichen und Unicode-Darstellung verändern die Nachricht. Prüfen Sie die empfangenen Originalbytes oder vereinbaren Sie ein gemeinsames kanonisches Format.

Den passenden Cloud-Mac für Ihre nächste Aufgabe wählen

Wählen Sie einen Standort in der Nähe des Hauptstandorts Ihres Teams und nutzen Sie einen exklusiven physischen Mac mini. Für den Remote-Desktop im Browser benötigen Sie einen aktuellen Browser und eine stabile Internetverbindung. Prüfen Sie Konfiguration, Abrechnungszeitraum und Verfügbarkeit vor der Bestellung.

Die nächste Verbindung in Ihrer Nähe planen

Prüfen Sie zunächst Konfiguration und Laufzeit. Wählen Sie anschließend einen Standort in der Nähe der Region, in der Ihr Team hauptsächlich arbeitet.