Просмотр исходного кода

Handle stale attest and registry keys by dropping them client-sided

Deniz Cengiz 1 день назад
Родитель
Сommit
1bb3632cf3

+ 30 - 0
Trio/Sources/Services/Telemetry/TelemetryAttestor.swift

@@ -85,6 +85,24 @@ final class TelemetryAttestor: Injectable {
         do {
             attestationCBOR = try await service.attestKey(keyID, clientDataHash: clientDataHash)
         } catch {
+            // `attestKey` is one-shot per key per device, but only on success.
+            // Branch on the DCError code so logs distinguish the recoverable
+            // cases from real failures:
+            //   .invalidKey         — keyID is permanently burnt; drop it.
+            //   .serverUnavailable  — Apple's App Attest backend is down or
+            //                         throttling. Key is still valid; the
+            //                         next cycle retries with the same keyID.
+            if let dcError = error as? DCError {
+                switch dcError.code {
+                case .invalidKey:
+                    keychain.removeObject(forKey: Self.keyIDStorageKey)
+                    debug(.telemetry, "attestKey invalidKey: discarded dead keyID; will regenerate next cycle")
+                case .serverUnavailable:
+                    debug(.telemetry, "attestKey serverUnavailable: Apple App Attest backend transient — will retry next cycle")
+                default:
+                    break
+                }
+            }
             debug(.telemetry, "attestKey failed: \(error.localizedDescription)")
             throw AttestError.attestationFailed(error)
         }
@@ -130,6 +148,18 @@ final class TelemetryAttestor: Injectable {
         }
     }
 
+    /// Clears the local App Attest state so the next `registerIfNeeded`
+    /// generates a fresh key and re-runs the handshake from scratch. Both the
+    /// keyID and the "registered" flag are dropped: `attestKey` may be called
+    /// at most once per key per device, so reusing the old keyID would throw
+    /// `DCError.invalidKey`. Use when `/checkin` returns 401 (server lost our
+    /// registration).
+    func invalidateRegistration() {
+        injectIfNeeded()
+        keychain.removeObject(forKey: Self.keyIDStorageKey)
+        keychain.removeObject(forKey: Self.registeredStorageKey)
+    }
+
     // MARK: - Per-ping assertion
 
     /// Builds the App Attest assertion for a single `/checkin` send.

+ 10 - 2
Trio/Sources/Services/Telemetry/TelemetryClient.swift

@@ -284,11 +284,19 @@ final class TelemetryClient: Injectable {
                 debug(.telemetry, "send: non-HTTP response")
                 return
             }
-            if (200 ..< 300).contains(http.statusCode) {
+            switch http.statusCode {
+            case 200 ..< 300:
                 PropertyPersistentFlags.shared.telemetryLastSentAt = Date()
                 PropertyPersistentFlags.shared.telemetryLastSentSha = BuildDetails.shared.trioCommitSHA
                 debug(.telemetry, "send ok status=\(http.statusCode)")
-            } else {
+            case 401:
+                // Server doesn't recognize our registration (e.g. its registry
+                // was wiped). Drop the local keyID + registered flag so the
+                // next cycle generates a fresh key and re-attests — `attestKey`
+                // can't be re-run on the existing keyID (one-shot per Apple).
+                attestor.invalidateRegistration()
+                debug(.telemetry, "send 401: stale registration, will re-register next cycle")
+            default:
                 debug(.telemetry, "send non-2xx status=\(http.statusCode)")
             }
         } catch {