From 0cf3ab7b4f5fed154630cacafb6897c7fef3e357 Mon Sep 17 00:00:00 2001 From: Daniel Arantes Loverde Date: Fri, 31 Jul 2026 13:41:31 -0300 Subject: [PATCH 1/4] [app-attest-env] Fix App Attest environment mismatch for distribution builds com.apple.developer.devicecheck.appattest-environment was hardcoded to "development" for every build, including the App Store/TestFlight distribution build. Apple's App Attest servers validate this claim against how the app was actually signed/distributed, so a "development" claim on a real distribution build fails - guest session handshake never gets past the challenge step, no store/city data ever loads. Parameterized per configuration: development for Debug, production for Release, via an APP_ATTEST_ENVIRONMENT build setting. --- Darwin/Entitlements.plist | 2 +- Darwin/PediFoods.xcodeproj/project.pbxproj | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Darwin/Entitlements.plist b/Darwin/Entitlements.plist index 70084b4..3376aa1 100644 --- a/Darwin/Entitlements.plist +++ b/Darwin/Entitlements.plist @@ -3,6 +3,6 @@ com.apple.developer.devicecheck.appattest-environment - development + $(APP_ATTEST_ENVIRONMENT) diff --git a/Darwin/PediFoods.xcodeproj/project.pbxproj b/Darwin/PediFoods.xcodeproj/project.pbxproj index 9a9e402..580bc88 100644 --- a/Darwin/PediFoods.xcodeproj/project.pbxproj +++ b/Darwin/PediFoods.xcodeproj/project.pbxproj @@ -178,6 +178,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 496EB72F2A6AE4DE00C1253B /* PediFoods.xcconfig */; buildSettings = { + APP_ATTEST_ENVIRONMENT = development; DEVELOPMENT_TEAM = K4E5BZMM4V; ENABLE_PREVIEWS = YES; INFOPLIST_KEY_CFBundleDisplayName = "Pedi Foods"; @@ -196,6 +197,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 496EB72F2A6AE4DE00C1253B /* PediFoods.xcconfig */; buildSettings = { + APP_ATTEST_ENVIRONMENT = production; CODE_SIGN_IDENTITY = "Apple Distribution"; CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = K4E5BZMM4V; From e9e3e9da1f88c06348f6e4ce798d9da688445a39 Mon Sep 17 00:00:00 2001 From: Daniel Arantes Loverde Date: Fri, 31 Jul 2026 14:05:29 -0300 Subject: [PATCH 2/4] [app-attest-env] Log the actual error behind guest-session App Attest failures Guest session handshake fails silently after the challenge step - no console output, just a generic "could not load" message in the UI. DCAppAttestService errors (generateKey/attestKey/generateAssertion) propagate up uncaught by anything that logs them. Add explicit logging at each step so the real thrown error is visible instead of debugging blind. --- .../Services/GuestSessionService.swift | 26 ++++++++++++++++--- .../Views/Main/PublicLocationPickerView.swift | 1 + 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Sources/PediFoods/Services/GuestSessionService.swift b/Sources/PediFoods/Services/GuestSessionService.swift index 869c3ed..9206983 100644 --- a/Sources/PediFoods/Services/GuestSessionService.swift +++ b/Sources/PediFoods/Services/GuestSessionService.swift @@ -93,6 +93,7 @@ actor GuestSessionService { #if os(iOS) private func refreshTokenWithAppAttest() async throws -> String { + print("[GuestSessionService] DCAppAttestService.isSupported = \(DCAppAttestService.shared.isSupported), existingKeyId = \(store.appAttestKeyId ?? "nil")") guard DCAppAttestService.shared.isSupported else { // Simulator can never support App Attest (hardware limitation, // not environment-specific) — server has its own documented @@ -141,9 +142,22 @@ actor GuestSessionService { } private func handshakeWithFreshAttestation(challenge: String) async throws -> String { - let keyId = try await DCAppAttestService.shared.generateKey() + let keyId: String + do { + keyId = try await DCAppAttestService.shared.generateKey() + } catch { + print("[GuestSessionService] generateKey failed: \(error)") + throw error + } + let clientDataHash = Data(SHA256.hash(data: Data(challenge.utf8))) - let attestationObject = try await DCAppAttestService.shared.attestKey(keyId, clientDataHash: clientDataHash) + let attestationObject: Data + do { + attestationObject = try await DCAppAttestService.shared.attestKey(keyId, clientDataHash: clientDataHash) + } catch { + print("[GuestSessionService] attestKey failed: \(error)") + throw error + } let payload = GuestSessionAttestPayload( platform: "ios", @@ -175,7 +189,13 @@ actor GuestSessionService { private func handshakeWithAssertion(keyId: String, challenge: String) async throws -> String { let clientDataHash = Data(SHA256.hash(data: Data(challenge.utf8))) - let assertionObject = try await DCAppAttestService.shared.generateAssertion(keyId, clientDataHash: clientDataHash) + let assertionObject: Data + do { + assertionObject = try await DCAppAttestService.shared.generateAssertion(keyId, clientDataHash: clientDataHash) + } catch { + print("[GuestSessionService] generateAssertion failed: \(error)") + throw error + } let payload = GuestSessionAttestPayload( platform: "ios", diff --git a/Sources/PediFoods/Views/Main/PublicLocationPickerView.swift b/Sources/PediFoods/Views/Main/PublicLocationPickerView.swift index 085b911..f5263e1 100644 --- a/Sources/PediFoods/Views/Main/PublicLocationPickerView.swift +++ b/Sources/PediFoods/Views/Main/PublicLocationPickerView.swift @@ -161,6 +161,7 @@ struct PublicLocationPickerView: View { errorMessage = "Nenhum estado disponível no momento." } } catch { + print("[PublicLocationPickerView] loadLocations failed: \(error)") errorMessage = "Não foi possível carregar. Tente novamente." } isLoading = false From 6187dc1907ffb55e7d5aecac1ba0a6d69b464323 Mon Sep 17 00:00:00 2001 From: Daniel Arantes Loverde Date: Fri, 31 Jul 2026 14:09:41 -0300 Subject: [PATCH 3/4] [app-attest-env] Clear stale App Attest key on local DCError, not just server 403 Root cause of the permanent stuck-at-challenge symptom: a stale appAttestKeyId in Keychain (Secure Enclave key invalidated by an app reinstall or signing change) makes generateAssertion fail every time with DCError code 2 (invalidInput). Only NetworkError 403 was clearing the stored key, so this local rejection was never recovered from - every guest-authed call kept retrying the same broken key forever. Catch DCError here too and fall through to fresh attestation. --- Sources/PediFoods/Services/GuestSessionService.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Sources/PediFoods/Services/GuestSessionService.swift b/Sources/PediFoods/Services/GuestSessionService.swift index 9206983..411bace 100644 --- a/Sources/PediFoods/Services/GuestSessionService.swift +++ b/Sources/PediFoods/Services/GuestSessionService.swift @@ -113,6 +113,15 @@ actor GuestSessionService { // other error (network blip, timeout, decode issue) must // NOT wipe a perfectly valid registered key. store.appAttestKeyId = nil + } catch let error as DCError { + // DeviceCheck itself rejects the key locally (e.g. the app + // was reinstalled and the Secure Enclave key backing this + // keyId no longer exists) - distinct from the server + // rejecting it, but equally unrecoverable without a fresh + // key. Without this, generateAssertion fails the same way + // forever since appAttestKeyId is never cleared. + print("[GuestSessionService] existing key rejected locally, re-attesting with a fresh key: \(error)") + store.appAttestKeyId = nil } } From 0bfdbb5c31c7b3d01948d5a868a3006260c6b32a Mon Sep 17 00:00:00 2001 From: Daniel Arantes Loverde Date: Fri, 31 Jul 2026 14:20:55 -0300 Subject: [PATCH 4/4] [app-attest-env] Opt-in app attest via CDhash and switch local signing to automatic --- Darwin/Entitlements.plist | 4 ++++ Darwin/PediFoods.xcodeproj/project.pbxproj | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Darwin/Entitlements.plist b/Darwin/Entitlements.plist index 3376aa1..1b0cdf5 100644 --- a/Darwin/Entitlements.plist +++ b/Darwin/Entitlements.plist @@ -2,6 +2,10 @@ + com.apple.developer.devicecheck.app-attest-opt-in + + CDhash + com.apple.developer.devicecheck.appattest-environment $(APP_ATTEST_ENVIRONMENT) diff --git a/Darwin/PediFoods.xcodeproj/project.pbxproj b/Darwin/PediFoods.xcodeproj/project.pbxproj index 580bc88..5fdba32 100644 --- a/Darwin/PediFoods.xcodeproj/project.pbxproj +++ b/Darwin/PediFoods.xcodeproj/project.pbxproj @@ -198,8 +198,8 @@ baseConfigurationReference = 496EB72F2A6AE4DE00C1253B /* PediFoods.xcconfig */; buildSettings = { APP_ATTEST_ENVIRONMENT = production; - CODE_SIGN_IDENTITY = "Apple Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = K4E5BZMM4V; ENABLE_PREVIEWS = YES; INFOPLIST_KEY_CFBundleDisplayName = "Pedi Foods"; @@ -208,7 +208,7 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; - PROVISIONING_PROFILE_SPECIFIER = "com.br.pedifoods.app AppStore"; + PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; TARGETED_DEVICE_FAMILY = 1;