Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions ee/psso/Bridge/SysdBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down
78 changes: 72 additions & 6 deletions ee/psso/PSSO/API.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -59,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 {
Expand Down
38 changes: 34 additions & 4 deletions ee/psso/PSSO/PlatformSSO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}
Expand All @@ -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
Expand Down Expand Up @@ -88,10 +95,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
}
}
}
Loading