iOS-Anfragesignaturen auf dem Cloud-Mac prüfen: HMAC und Replay-Schutz
Eine unter Windows entwickelte plattformübergreifende App wird an eine API mit Anfragesignaturen angebunden. Einfache Anfragen funktionieren, doch Anfragen mit chinesischen Zeichen im Query-String oder einem JSON-Body schlagen fehl. Wer das Signaturmodul zunächst isoliert auf einem Cloud-Mac ausführt, kann die Byte-Konventionen prüfen und es erst danach wieder in das iOS-Projekt einbinden. So lassen sich Fehler leichter eingrenzen als durch wiederholte API-Aufrufe über einen Remote-Desktop.
Im Folgenden entsteht mit Swift und CryptoKit ein offline ausführbares Prüfskript. Dafür sind weder eine echte API noch Produktionsschlüssel oder Signierzertifikate erforderlich. Client- und Server-Team sollten diese Test-Fixtures gemeinsam vorhalten.
Zuerst den Vertrag festlegen
HMAC-SHA256 verarbeitet Bytes, keine Anfragen, die nur „gleich aussehen“. Zunächst müssen sechs Felder vereinbart werden: Protokollversion, Methode, Anfrageziel, Zeitstempel, Nonce und SHA-256-Hash des Bodys.
In diesem Beispiel wird die Methode immer großgeschrieben. Das Anfrageziel enthält den Pfad und den unveränderten Query-String, aber kein Fragment. Der Zeitstempel wird als ganzzahlige Sekundenangabe in Dezimalschreibweise übergeben; für jede Anfrage wird eine neue Nonce erzeugt. Der Body-Hash wird hexadezimal mit Kleinbuchstaben ausgegeben.
Query-Parameter dürfen nicht eigenmächtig sortiert werden. Ob doppelte Schlüssel, leere Werte, Prozentkodierungen und unterschiedliche Parameterreihenfolgen als gleichwertig gelten, muss das API-Protokoll festlegen. Der Client sollte erst nach der endgültigen Kodierung signieren. Der Server muss die vereinbarte Darstellung erfassen, bevor das Framework Pfad oder Query-Parameter umschreibt.
HMAC weist nach, dass Nachricht und Schlüssel zusammenpassen. Es verschlüsselt weder den Body noch verhindert es automatisch Replay-Angriffe. Es ersetzt keine Transportverschlüsselung und macht einen im App-Bundle hinterlegten gemeinsamen Schlüssel auch nicht zu einem verlässlichen Identitätsnachweis.
Die Bytes verbindlich festlegen
Felder sollten nicht einfach durch Zeilenumbrüche verbunden werden: Enthält ein Feld selbst einen Zeilenumbruch, kann die Darstellung mehrdeutig werden. In diesem Beispiel erhält jedes UTF-8-Feld ein Längenpräfix als vorzeichenlose Acht-Byte-Ganzzahl in Big-Endian-Reihenfolge, gefolgt vom Feldinhalt. Der Server muss exakt dieselben Regeln umsetzen.
Speichern Sie den folgenden Inhalt als request-signing.swift. Prüfen Sie auf dem Cloud-Mac zunächst mit xcrun swift --version die Toolchain und führen Sie anschließend xcrun swift request-signing.swift aus. Bei Erfolg endet das Skript regulär; schlägt eine Assertion fehl, liefert es einen Status ungleich null zurück.
import Foundation
import CryptoKit
func hex<S: Sequence>(_ bytes: S) -> String
where S.Element == UInt8 {
bytes.map { String(format: "%02x", $0) }.joined()
}
func frame(_ fields: [String]) -> Data {
var output = Data()
for field in fields {
let bytes = Data(field.utf8)
var length = UInt64(bytes.count).bigEndian
withUnsafeBytes(of: &length) {
output.append(contentsOf: $0)
}
output.append(bytes)
}
return output
}
func canonical(
target: String,
body: Data,
timestamp: String,
nonce: String
) -> Data {
frame([
"request-v1",
"POST",
target,
timestamp,
nonce,
hex(SHA256.hash(data: body))
])
}
let vectorKey = SymmetricKey(
data: Data(repeating: 0x0b, count: 20)
)
let vector = HMAC<SHA256>.authenticationCode(
for: Data("Hi There".utf8),
using: vectorKey
)
precondition(
hex(vector) ==
"b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
)
let key = SymmetricKey(data: Data(repeating: 0x42, count: 32))
let body = Data(#"{"count":1}"#.utf8)
let target = "/v1/orders?a=1&b=2"
let timestamp = "1700000000"
let nonce = "fixture-nonce-001"
let message = canonical(
target: target, body: body,
timestamp: timestamp, nonce: nonce
)
let tag = Data(HMAC<SHA256>.authenticationCode(
for: message, using: key
))
func accepts(_ candidate: Data) -> Bool {
HMAC<SHA256>.isValidAuthenticationCode(
tag, authenticating: candidate, using: key
)
}
precondition(accepts(message))
precondition(!accepts(canonical(
target: target, body: Data(#"{"count":2}"#.utf8),
timestamp: timestamp, nonce: nonce
)))
precondition(!accepts(canonical(
target: "/v1/orders?b=2&a=1", body: body,
timestamp: timestamp, nonce: nonce
)))
precondition(!accepts(canonical(
target: target, body: body,
timestamp: timestamp, nonce: "fixture-nonce-002"
)))
Der feste Schlüssel, der Zeitstempel und die Nonce sind ausschließlich für die Test-Fixtures bestimmt. Der Standardtestvektor prüft den Aufruf der kryptografischen Funktion. Die anschließenden Assertions prüfen, ob die Protokollfelder tatsächlich in die Signatur einfließen. Keine der beiden Testarten kann die andere ersetzen.
Darstellungsunterschiede abdecken
Erzeugen Sie bei der Integration in die App den endgültigen Body als Data nur einmal und verwenden Sie ihn sowohl für die Hash-Berechnung als auch für die Netzwerkanfrage. Kodieren Sie das JSON nach dem Signieren nicht erneut, und lassen Sie nicht zu, dass Middleware den Body unbemerkt ersetzt.
Kodierung prüfen
Ergänzen Sie drei Gruppen von Test-Fixtures: Query-Strings mit chinesischen Zeichen und Emojis, doppelte Query-Schlüssel sowie einen leeren Body im Vergleich zu einem leeren JSON-Objekt. Speichern Sie für jede Gruppe die Feldinhalte, die kanonischen Bytes in Hexadezimaldarstellung und die erwartete Signatur. Beide Seiten müssen diese Daten unabhängig voneinander einlesen. Die erwarteten Ergebnisse dürfen nicht zur Laufzeit von der zu prüfenden Funktion erzeugt werden.
Unterscheiden Sie insbesondere zwischen einer leeren Zeichenfolge, einem fehlenden Feld und der Zeichenfolge "null". Erlaubt das Protokoll eine bestimmte Eingabe nicht, muss sie vor der HMAC-Verarbeitung zurückgewiesen werden, statt stillschweigend einen Standardwert einzusetzen.
Transportverhalten prüfen
Ein Reverse-Proxy kann Pfadpräfixe umschreiben; ein Framework kann sowohl %20 als auch ein Pluszeichen als dasselbe Leerzeichen dekodieren. Legen Sie fest, ob sich die Signatur auf das externe Anfrageziel oder die interne Route bezieht, und erfassen Sie die Bytes an der entsprechenden Systemgrenze.
Protokollieren Sie bei der Fehlersuche nur Test-Fixtures oder bereinigte Hash-Werte. Bodys aus dem Produktivbetrieb, Authentifizierungsheader und Schlüssel gehören nicht in Build-Logs. Übergeben Sie Schlüssel auch nicht als Kommandozeilenargumente.
Replay-Anfragen abweisen
Auch nach erfolgreicher Signaturprüfung muss der Server das Format des Zeitstempels und die zulässige Zeitabweichung prüfen. Die Länge des Zeitfensters richtet sich nach den fachlichen Anforderungen. Die Tests müssen mindestens vier Fälle abdecken: innerhalb des Fensters, exakt an der Grenze, knapp außerhalb und mit einem Zeitstempel in der Zukunft.
Führen Sie anschließend einen atomaren Schreibvorgang mit „Kennung des Berechtigungsnachweises + Nonce“ als Deduplizierungsschlüssel aus. Bewahren Sie den Eintrag so lange auf, bis die Anfrage die Zeitprüfung nicht mehr bestehen kann. Erst nachzusehen und danach einzufügen führt zu einer Race Condition: Zwei parallele Anfragen könnten beide akzeptiert werden. Erforderlich ist daher ein atomarer bedingter Schreibvorgang oder ein Unique-Constraint.
Der alte Zeitstempel aus dem Offline-Skript eignet sich nicht für die Online-Abnahme. Online-Anfragen müssen die aktuelle Zeit auslesen und eine neue Nonce erzeugen. Ob Wiederholungsversuche denselben fachlichen Idempotenzschlüssel verwenden, muss gesondert vereinbart werden. Nonce-Deduplizierung ist keine Bestellidempotenz.
Die Prüfung als Freigabebedingung einbinden
Nehmen Sie das Skript zunächst in die Commit-Prüfungen auf und verwenden Sie anschließend dieselben Test-Fixtures in den Server-Tests. Ergänzen Sie neben Änderungen am Body auch Negativtests für einen falschen Schlüssel, eine gekürzte Signatur und eine ungültige Signaturkodierung. Die empfangende Seite sollte die Verifikationsfunktion der Kryptografiebibliothek verwenden, keinen gewöhnlichen Zeichenfolgenvergleich.
Senden Sie abschließend zwei vollständig identische gültige Anfragen gleichzeitig und prüfen Sie, dass nur eine davon die fachliche Verarbeitung erreicht. Simulieren Sie danach einen zulässigen Wiederholungsversuch innerhalb des gültigen Zeitfensters und kontrollieren Sie, ob er das vereinbarte idempotente Ergebnis liefert.
Es genügt, Toolchain-Version, Fixture-Version und die Nummer der fehlgeschlagenen Assertion festzuhalten. Die Abnahme ist nicht schon dann abgeschlossen, wenn „eine Anfrage funktioniert“. Entscheidend ist, dass beide Seiten für dieselben Bytes dieselbe Signatur berechnen und ungültige Eingaben sowie doppelte Anfragen konsistent und reproduzierbar zurückweisen.
Häufig gestellte Fragen
Verhindert ein gültiger HMAC bereits Replay-Angriffe?
Nein. Eine unveränderte signierte Anfrage lässt sich erneut senden. Der Server muss das Zeitfenster prüfen und die Kombination aus Zugangsdatenkennung und Nonce atomar deduplizieren. Fachliche Wiederholungen benötigen einen eigenen Idempotenzvertrag.
Soll JSON vor dem Signieren erneut kodiert werden?
In diesem Verfahren nicht. Der Client hasht die tatsächlich gesendeten Body-Bytes, der Server die empfangenen Rohdaten. Erneutes Kodieren kann Schlüsselreihenfolge, Leerzeichen und Zahlenformate verändern.
Cloud-Mac mieten
Mieten Sie einen exklusiven physischen Mac tage-, wochen-, monats- oder quartalsweise. Planen Sie Builds nach Bedarf – nach Ablauf endet die Miete automatisch.
Mac auswählen