MangoVM Engineering-Leitfaden

iOS Keychain-Migration auf einem Cloud-Mac regressionstesten

iOS Keychain-Migration auf einem Cloud-Mac regressionstesten

Ein gewöhnliches iOS-Update kann dazu führen, dass ein Anmeldetoken plötzlich nicht mehr auffindbar ist. Die neue Version stürzt nicht ab, und auch die Schnittstelle funktioniert einwandfrei. Die eigentliche Änderung verbirgt sich häufig im Keychain-service, im account, in der Access Group oder in den Schutzattributen. Um solche Probleme auf einem Cloud-Mac zuverlässig zu erkennen, müssen Tests mit dem Ablauf „alte Version schreibt, neue Version liest“ beginnen und dürfen sich nicht auf Neuinstallationen beschränken.

Migrationsvertrag zuerst definieren

Keychain-Einträge sollten zunächst als Datenstrukturen betrachtet werden, deren Kompatibilität gewahrt werden muss. Mindestens fünf Eigenschaften sind zu dokumentieren: Eintragsklasse, service, account, Access Group und kSecAttrAccessible. Wenn die neue Version Bezeichner ändern soll, dürfen nicht einfach die Abfragebedingungen ersetzt werden. Stattdessen wird der Eintrag zuerst mit den alten Bedingungen gelesen, anschließend in der neuen Struktur gespeichert und erst nach erfolgreicher Prüfung gelöscht.

Für jede Migration empfiehlt sich eine ganzzahlige Version, beispielsweise keychainSchemaVersion = 2. Der Abschlussmarker wird in den normalen App-Einstellungen gespeichert. Er dient lediglich dazu, eine erneute Migration zu vermeiden, und ist kein Nachweis dafür, dass die Keychain-Daten tatsächlich vorhanden sind. Beim Start muss die App den Zieleintrag weiterhin prüfen und auch den Fall behandeln, dass der Marker vorhanden ist, die Daten jedoch gelöscht wurden.

Eine Migration gilt nicht schon dann als erfolgreich, wenn die Funktion Erfolg zurückgibt. Die neue Version muss die alten Daten lesen können, wiederholte Ausführungen dürfen keine Daten beschädigen, und bei einem Fehler muss der ursprüngliche Eintrag wiederherstellbar bleiben.

Zunächst wird eine minimale Abnahmematrix festgelegt:

Szenario Ausgangszustand Erwartetes Ergebnis
Neuinstallation Kein alter Eintrag Eintrag im neuen Format wird erstellt
Update-Installation Alter Eintrag vollständig vorhanden Eintrag ist nach der Migration lesbar
Wiederholter Start Migration bereits abgeschlossen Kein erneutes Schreiben
Beschädigter alter Eintrag Ungültiges Datenformat Ausgangszustand bleibt erhalten und ein eindeutiger Fehler wird zurückgegeben
Geänderte Access Group Neue Version kann nicht auf die alte Gruppe zugreifen Veröffentlichung wird blockiert und die Signaturkonfiguration geprüft

Test-Fixtures für alte und neue Version erstellen

Es werden zwei installierbare .app-Pakete für den Simulator aufbewahrt: Die alte Version legt die Testdaten an, die neue Version migriert und überprüft sie. Zwischen den beiden Builds darf weder erase ausgeführt noch die App deinstalliert werden, da dies die Testvoraussetzungen verändern würde. Der Test-Build erhält Startargumente, die ausschließlich in der Automatisierungsumgebung aktiv sind, etwa -UITestSeedLegacyKeychain und -UITestVerifyKeychainV2. Produktions-Builds reagieren nicht auf diese Argumente.

set -euo pipefail

UDID="${SIMULATOR_UDID:?missing simulator id}"
OLD_APP="${OLD_APP_PATH:?missing old app path}"
NEW_APP="${NEW_APP_PATH:?missing new app path}"
BUNDLE_ID="com.example.product"

xcrun simctl bootstatus "$UDID" -b
xcrun simctl install "$UDID" "$OLD_APP"
xcrun simctl launch --terminate-running-process \
  "$UDID" "$BUNDLE_ID" -UITestSeedLegacyKeychain

xcrun simctl install "$UDID" "$NEW_APP"
xcrun simctl launch --terminate-running-process \
  "$UDID" "$BUNDLE_ID" -UITestVerifyKeychainV2

Das Anlegen der Testdaten muss überprüfbar sein. Nachdem die alte Version den Eintrag geschrieben hat, kann die Testoberfläche eine Statusdatei ohne vertrauliche Werte ausgeben. Darin werden lediglich die Eintragsversion, das Schreibergebnis und eine zufällige Testfallnummer erfasst. Nach Abschluss der Prüfung schreibt auch die neue Version eine Ergebnisdatei. Die CI vergleicht ausschließlich diese Statusinformationen und gibt keine Token-Inhalte aus.

Verunreinigungen durch Parallelität vermeiden

Auf demselben Simulator dürfen nicht mehrere Gruppen von Keychain-Tests parallel ausgeführt werden. Für jeden Auftrag wird entweder ein separater Simulator erstellt oder die Testsuiten werden nacheinander ausgeführt. service und account sollten ein zufälliges Testfall-Suffix erhalten, damit nach einem abgebrochenen vorherigen Lauf zurückgebliebene Daten im nächsten Test nicht fälschlich als Erfolg gewertet werden.

Diagnostizierbare Ergebnisse im Code beibehalten

Nicht jeder Fehler sollte in nil umgewandelt werden. Nur wenn OSStatus erhalten bleibt, lassen sich ein nicht gefundener Eintrag, unzureichende Berechtigungen und ungültige Parameter voneinander unterscheiden. Die Abfragefunktion kann einen eindeutigen Ergebnistyp zurückgeben:

enum KeychainReadResult {
    case found(Data)
    case missing
    case failed(OSStatus)
}

func readLegacyToken(service: String, account: String) -> KeychainReadResult {
    let query: [CFString: Any] = [
        kSecClass: kSecClassGenericPassword,
        kSecAttrService: service,
        kSecAttrAccount: account,
        kSecReturnData: true,
        kSecMatchLimit: kSecMatchLimitOne
    ]

    var item: CFTypeRef?
    let status = SecItemCopyMatching(query as CFDictionary, &item)

    if status == errSecItemNotFound {
        return .missing
    }
    guard status == errSecSuccess, let data = item as? Data else {
        return .failed(status)
    }
    return .found(data)
}

Bei der Migration wird zuerst der neue Eintrag angelegt. Anschließend wird er erneut gelesen und sein Inhalts-Hash verglichen; erst danach wird der alte Eintrag gelöscht. Der alte Eintrag darf nicht vor dem Schreibvorgang entfernt werden. Schlägt das Schreiben wegen fehlender Berechtigungen oder ungültiger Parameter fehl, wären die Benutzerdaten sonst nicht wiederherstellbar. Protokolliert werden ausschließlich Statuscode, Migrationsversion und Testfallnummer, nicht die ursprünglichen Zugangsdaten.

Signatur und Access Groups prüfen

Wenn sich der Code nicht geändert hat, eine Abfrage aber plötzlich errSecItemNotFound zurückgibt, sollten die Entitlements der alten und neuen Artefakte verglichen werden. Jede Änderung am Präfix der Access Group, an der App-Kennung oder an den Keychain Access Groups kann dazu führen, dass der ursprüngliche Eintrag für die neue Version nicht mehr sichtbar ist.

codesign -d --entitlements :- "$OLD_APP_PATH" > old-entitlements.plist
codesign -d --entitlements :- "$NEW_APP_PATH" > new-entitlements.plist
diff -u old-entitlements.plist new-entitlements.plist

Die Differenzdatei wird als CI-Anhang gespeichert, Signaturmaterial darf jedoch nicht in das Build-Protokoll kopiert werden. Falls die Access Group tatsächlich geändert werden muss, ist eine Übergangsversion vorzusehen, die sowohl auf die alte als auch auf die neue Gruppe zugreifen und den Eintrag übertragen kann. Es darf nicht davon ausgegangen werden, dass die neue Version beliebige alte Gruppen direkt lesen kann.

Der Simulator eignet sich zum Prüfen des Migrationsalgorithmus und der Fehlerpfade, bildet jedoch nicht jedes Signatur- und Zugriffskontrollverhalten ab. Vor der Veröffentlichung muss derselbe Ablauf daher einmal auf einem kontrollierten physischen Gerät ausgeführt werden: alte Version installieren, Daten anlegen und anschließend die neue Version als Update installieren. Die Ergebnisse dieses Tests werden getrennt von den Simulatorergebnissen erfasst.

Bereinigung, Fehleranalyse und Release-Gate

Nach einem erfolgreichen Test werden die Testeinträge über eine interne Testschnittstelle der App anhand des exakten service und account gelöscht. Allgemeine Löschabfragen sind zu vermeiden, da sie andere Daten auf demselben Testgerät entfernen könnten. Bei einem fehlgeschlagenen Test bleibt der Simulator zunächst erhalten. Vor einer möglichen Löschung werden die Entitlements der neuen und alten Version, OSStatus, die einzelnen Migrationsschritte und die App-Protokolle gesichert.

Das Release-Gate muss drei Punkte prüfen: Die neue Version kann die Daten der alten Version lesen; zwei unmittelbar aufeinanderfolgende Migrationen liefern dasselbe Ergebnis; fehlerhafte Eingaben führen nicht zum Löschen des alten Eintrags. Schlägt eine dieser Prüfungen fehl, darf das Artefakt nicht an die nächsten Verteilungsstufen weitergegeben werden.

Für das Migrationsskript werden abschließend feste Eingaben beibehalten: Build-Kennung der alten Version, Build-Kennung der neuen Version, Systemversion des Simulators und Testfallnummer. So lässt sich derselbe Upgrade-Pfad aus archivierten Artefakten wiederherstellen, selbst wenn der Cloud-Mac neu initialisiert wurde. Das schwierigste Problem bei Keychain-Fehlern ist nicht das Schreiben, sondern der fehlende alte Zustand. Solange sich das Fixture der alten Version, die Signaturunterschiede und die Fehlercodes reproduzieren lassen, wird aus einem Produktionsrisiko ein gewöhnlicher Regressionstest.

Häufig gestellte Fragen

Warum reicht ein Keychain-Test nach einer Neuinstallation nicht aus?

Er zeigt nur, dass der aktuelle Build neue Einträge schreiben kann. Ob er Service, Account, Access Group und Schutzattribute eines älteren Builds lesen kann, bleibt dabei ungeprüft.

Was sollte bei einer fehlgeschlagenen Keychain-Migration zuerst geprüft werden?

Sichern Sie den OSStatus von SecItemCopyMatching und vergleichen Sie danach Entitlements, Access Groups, Service, Account und kSecAttrAccessible beider Builds, bevor alte Einträge gelöscht werden.

Kann der Simulator die Geräteprüfung vollständig ersetzen?

Nein. Er eignet sich für wiederholbare Abfrage- und Migrationstests, doch Signierung, Zugriffskontrolle und einzelne Sicherheitsmechanismen müssen zusätzlich auf einem kontrollierten Gerät geprüft werden.

Exklusiver physischer Knoten

Einen dedizierten Cloud-Mac für Ihre Entwicklungs-Pipeline konfigurieren

MangoVM bietet dedizierte physische Knoten mit Apple Silicon in den Varianten M4 und M4 Pro. Wählen Sie Laufzeit, Region und zusätzliche Speicheroptionen, um die vollständigen Bestelldetails zu prüfen.

Konfiguration auswählen und bestellen