From a76778702f4592b331e482aa6c2aed2cc81807a5 Mon Sep 17 00:00:00 2001 From: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:23:31 +0200 Subject: [PATCH 1/4] fix(psso): guard SE-key unwraps in RegisterDevice/RegisterUser RegisterDevice and RegisterUser no longer force-unwrap loginManager.key(for:) or getPublicKeyString(...). A missing or invalid key now logs an error and returns nil/.failed instead of crashing the extension mid-(re)registration. --- ee/psso/PSSO/API.swift | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/ee/psso/PSSO/API.swift b/ee/psso/PSSO/API.swift index b16ea431a..8e2d242a9 100644 --- a/ee/psso/PSSO/API.swift +++ b/ee/psso/PSSO/API.swift @@ -15,10 +15,25 @@ class API { -> ASAuthorizationProviderExtensionLoginConfiguration? { do { - let (SignKeyID, DeviceSigningKey, _) = try getPublicKeyString( - from: loginManager.key(for: .currentDeviceSigning)!)! - let (EncKeyID, DeviceEncryptionKey, _) = try getPublicKeyString( - from: loginManager.key(for: .currentDeviceEncryption)!)! + guard let signingKey = loginManager.key(for: .currentDeviceSigning) else { + self.logger.error("device signing key unavailable") + return nil + } + guard let encryptionKey = loginManager.key(for: .currentDeviceEncryption) else { + self.logger.error("device encryption key unavailable") + return nil + } + guard let (SignKeyID, DeviceSigningKey, _) = try getPublicKeyString(from: signingKey) + else { + self.logger.error("failed to derive device signing public key") + return nil + } + guard + let (EncKeyID, DeviceEncryptionKey, _) = try getPublicKeyString(from: encryptionKey) + else { + self.logger.error("failed to derive device encryption public key") + return nil + } self.logger.debug("registering device with sysd...") let config = try await SysdBridge.shared.pssoRegisterDevice( deviceSigningKey: DeviceSigningKey, @@ -40,8 +55,17 @@ class API { userToken: String, ) async -> ASAuthorizationProviderExtensionRegistrationResult { do { - let (EnclaveKeyID, UserSecureEnclaveKey, _) = try getPublicKeyString( - from: loginManger.key(for: .userSecureEnclaveKey)!)! + guard let enclaveKey = loginManger.key(for: .userSecureEnclaveKey) else { + self.logger.error("user secure enclave key unavailable") + return .failed + } + guard + let (EnclaveKeyID, UserSecureEnclaveKey, _) = try getPublicKeyString( + from: enclaveKey) + else { + self.logger.error("failed to derive user secure enclave public key") + return .failed + } self.logger.debug("registering user with sysd...") let loginConfig = try await SysdBridge.shared .pssoRegisterUser( From 1f503a64604f0e26077e22b30813af9b7e4a920c Mon Sep 17 00:00:00 2001 From: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:24:10 +0200 Subject: [PATCH 2/4] feat(psso): add keyWillRotate support with RotateDeviceKey re-registration --- ee/psso/PSSO/API.swift | 42 ++++++++++++++++++++++++++++++++++ ee/psso/PSSO/PlatformSSO.swift | 29 ++++++++++++++++++++--- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/ee/psso/PSSO/API.swift b/ee/psso/PSSO/API.swift index 8e2d242a9..98d3c69e8 100644 --- a/ee/psso/PSSO/API.swift +++ b/ee/psso/PSSO/API.swift @@ -83,6 +83,48 @@ class API { } } + /// Re-register a rotated device key with the backend so the IdP keeps a matching public + /// key. Without this, the OS begins signing login assertions with a key the IdP was never + /// told about, which the token endpoint rejects → eventual `permanentLoginFailure`. + /// Device registration needs only the device keys + domain token, so it runs without user + /// interaction. Returns false on any failure so the caller can reject the rotation. + func RotateDeviceKey( + loginManager: ASAuthorizationProviderExtensionLoginManager, + keyType: ASAuthorizationProviderExtensionKeyType, + newKey: SecKey, + ) async -> Bool { + do { + // The rotation hasn't happened yet, so `loginManager.key(for:)` still returns the + // current keys. Substitute the incoming `newKey` for the type being rotated. + guard let currentSigning = loginManager.key(for: .currentDeviceSigning), + let currentEncryption = loginManager.key(for: .currentDeviceEncryption) + else { + self.logger.error("device keys unavailable during rotation") + return false + } + let signingKey = keyType == .currentDeviceSigning ? newKey : currentSigning + let encryptionKey = keyType == .currentDeviceEncryption ? newKey : currentEncryption + + guard let (signKeyID, deviceSigningKey, _) = try getPublicKeyString(from: signingKey), + let (encKeyID, deviceEncryptionKey, _) = try getPublicKeyString(from: encryptionKey) + else { + self.logger.error("failed to derive public keys during rotation") + return false + } + _ = try await SysdBridge.shared.pssoRegisterDevice( + deviceSigningKey: deviceSigningKey, + deviceEncryptionKey: deviceEncryptionKey, + encKeyID: encKeyID, + signKeyID: signKeyID + ) + self.logger.debug("re-registered rotated device key with backend") + return true + } catch { + self.logger.error("failed to re-register rotated device key: \(error)") + return false + } + } + func getPublicKey(from privateKey: SecKey) -> SecKey? { // Use SecKeyCopyPublicKey to get the public key from the private key guard let publicKey = SecKeyCopyPublicKey(privateKey) else { diff --git a/ee/psso/PSSO/PlatformSSO.swift b/ee/psso/PSSO/PlatformSSO.swift index 62e48b1fb..5964a7bb0 100644 --- a/ee/psso/PSSO/PlatformSSO.swift +++ b/ee/psso/PSSO/PlatformSSO.swift @@ -88,10 +88,33 @@ extension AuthenticationViewController: ASAuthorizationProviderExtensionRegistra func keyWillRotate( for keyType: ASAuthorizationProviderExtensionKeyType, - newKey _: SecKey, - loginManager _: ASAuthorizationProviderExtensionLoginManager, + newKey: SecKey, + loginManager: ASAuthorizationProviderExtensionLoginManager, ) async -> Bool { self.logger.debug("keyWillRotate \(String(describing: keyType))") - return false + switch keyType { + case .currentDeviceSigning, .currentDeviceEncryption: + // Re-register the rotated device key so the IdP keeps a matching public key. + // Rejecting the rotation here would leave the server with the old key and break + // subsequent login assertions. + let ok = await API.shared.RotateDeviceKey( + loginManager: loginManager, keyType: keyType, newKey: newKey) + if !ok { + self.logger.warning( + "failed to re-register rotated device key; rejecting rotation") + } + return ok + case .userSecureEnclaveKey: + // Re-registering the user SE key requires fresh user auth, which isn't available in + // this callback. Reject so the OS falls back to interactive user re-registration via + // beginUserRegistration. See plan: user-key rotation needs a server-coordinated change. + self.logger.warning( + "user SE key rotation requested; rejecting to force interactive re-registration") + return false + default: + self.logger.warning( + "unhandled key rotation for \(String(describing: keyType)); rejecting") + return false + } } } From 2cb68c775502a751e9cf50052bee7e25d44be9ed Mon Sep 17 00:00:00 2001 From: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:24:13 +0200 Subject: [PATCH 3/4] fix(psso): validate endpoints in SysdBridge instead of force-unwrapping URLs --- ee/psso/Bridge/SysdBridge.swift | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/ee/psso/Bridge/SysdBridge.swift b/ee/psso/Bridge/SysdBridge.swift index 24e264698..b5ca79409 100644 --- a/ee/psso/Bridge/SysdBridge.swift +++ b/ee/psso/Bridge/SysdBridge.swift @@ -43,6 +43,11 @@ public enum SocketID: String { case ctrlSocket = "ctrl" } +public enum SysdBridgeError: Error { + /// A login-configuration endpoint returned by the server was empty or not a valid URL. + case invalidEndpoint(name: String, value: String) +} + public class SysdBridge { public static let shared: SysdBridge = SysdBridge() @@ -193,6 +198,7 @@ public class SysdBridge { encKeyID: String, signKeyID: String, ) async throws -> ASAuthorizationProviderExtensionLoginConfiguration { + let logger = self.logger return try await self.withClient { client in let c = SystemAuthApple.Client(wrapping: client) let res = try await c.registerDevice( @@ -204,14 +210,29 @@ public class SysdBridge { $0.signKeyID = signKeyID } )) + guard let tokenEndpointURL = URL(string: res.tokenEndpoint) else { + logger.error("invalid token endpoint: '\(res.tokenEndpoint)'") + throw SysdBridgeError.invalidEndpoint( + name: "tokenEndpoint", value: res.tokenEndpoint) + } + guard let jwksEndpointURL = URL(string: res.jwksEndpoint) else { + logger.error("invalid jwks endpoint: '\(res.jwksEndpoint)'") + throw SysdBridgeError.invalidEndpoint( + name: "jwksEndpoint", value: res.jwksEndpoint) + } + guard let nonceEndpointURL = URL(string: res.nonceEndpoint) else { + logger.error("invalid nonce endpoint: '\(res.nonceEndpoint)'") + throw SysdBridgeError.invalidEndpoint( + name: "nonceEndpoint", value: res.nonceEndpoint) + } let cfg = ASAuthorizationProviderExtensionLoginConfiguration( clientID: res.clientID, issuer: res.issuer, - tokenEndpointURL: URL(string: res.tokenEndpoint)!, - jwksEndpointURL: URL(string: res.jwksEndpoint)!, + tokenEndpointURL: tokenEndpointURL, + jwksEndpointURL: jwksEndpointURL, audience: res.audience ) - cfg.nonceEndpointURL = URL(string: res.nonceEndpoint)! + cfg.nonceEndpointURL = nonceEndpointURL cfg.customNonceRequestValues .append( URLQueryItem( From 38cbbff11db26d6918d70578cf25249f50a13c93 Mon Sep 17 00:00:00 2001 From: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:24:20 +0200 Subject: [PATCH 4/4] fix(psso): gate includePreviousRefreshTokenInLoginRequest behind static flag --- ee/psso/PSSO/PlatformSSO.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ee/psso/PSSO/PlatformSSO.swift b/ee/psso/PSSO/PlatformSSO.swift index 5964a7bb0..c406f81bb 100644 --- a/ee/psso/PSSO/PlatformSSO.swift +++ b/ee/psso/PSSO/PlatformSSO.swift @@ -3,6 +3,12 @@ import Bridge extension AuthenticationViewController: ASAuthorizationProviderExtensionRegistrationHandler { + /// Whether to embed the previous refresh token in the login request. Disabled by default for + /// the `UserSecureEnclaveKey` method: after a password reset or session revocation a stale + /// refresh token can cause the login request to be rejected. Flip to `true` to test against a + /// server that expects it. + static let includePreviousRefreshTokenInLoginRequest = false + var supportedDeviceEncryptionAlgorithms: [ASAuthorizationProviderExtensionEncryptionAlgorithm] { return [.ecdhe_A256GCM] } @@ -27,7 +33,8 @@ extension AuthenticationViewController: ASAuthorizationProviderExtensionRegistra ) if let registration = registration { registration.accountDisplayName = "authentik" - registration.includePreviousRefreshTokenInLoginRequest = true + registration.includePreviousRefreshTokenInLoginRequest = + AuthenticationViewController.includePreviousRefreshTokenInLoginRequest do { try loginManager.saveLoginConfiguration(registration) return .success