Different PRF output when using platform or cross-platform authentication attachement

Hello,

I am using the prf extension for passkeys that is available since ios 18 and macos15. I am using a fixed, hardcoded prf input when creating or geting the credentials. After creating a passkey, i try to get the credentials and retrieve the prf output, which works great, but i am getting different prf outputs for the same credential and same prf input used in the following scenarios:

  1. Logging in directly (platform authenticator) on my macbook/iphone/ipad i get "prf output X" consistently for the 3 devices

  2. When i use my iphone/ipad to scan the qr code on my macbook (cross-platform authenticator) i get "prf output Y" consistently with both my ipad and iphone.

Is this intended? Is there a way to get deterministic prf output for both platform and cross-platform auth attachements while using the same credential and prf input?

Answered by Systems Engineer in 826557022

Yes this was a bug. The PRF values returned over hybrid should match the ones returned locally for the same input. This issue should be fixed in the current iOS 18.4 and macOS 15.4 betas.

These values should match, assuming your use of UV matches in both cases. The PRF extension specifies to use different seeds depending on whether UV (passcode/biometrics) was performed or not. But assuming you're either always performing or always not performing UV in both cases, the same inputs should produce the same outputs.

If you're seeing this not working, please let us know through Feedback Assistant! Logs from all the devices involved and any info you can provide about how you're testing would be very helpful.

Consider the following scenario. I am logging in using the platform authenticator as shown in img below

The following function was used to authenticate:

func authenticate() {
        let _challenge: String = call.getString("challenge")!
        let _rp: String = call.getString("rpId")!
        let _prf = call.getObject("extensions")?["prf"] as? [String: Any]
        let _eval = _prf?["eval"] as? [String: Any]
        
        let challengeData = Base64URLUtils.decode(_challenge)!
        

        
        let platformProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: _rp)
        let platformKeyRequest = platformProvider.createCredentialAssertionRequest(challenge: challengeData)


        if let eval = _eval,
           let first = eval["first"] as? String,
           let saltData = Base64URLUtils.decode(first) {
            let inputValues = ASAuthorizationPublicKeyCredentialPRFAssertionInput.InputValues(saltInput1: saltData)
            let prfInput = ASAuthorizationPublicKeyCredentialPRFAssertionInput.inputValues(inputValues)
            platformKeyRequest.prf = prfInput
        } else {
            NSLog("[PasskeyWallet] Failed to extract PRF data. eval: %@, first: %@", String(describing: _eval), String(describing: _eval?["first"]))
        }

        // Log input parameters for debugging
        NSLog("[AuthenticationInput] Starting authentication with parameters")
        NSLog("[AuthenticationInput] Challenge: %@", _challenge)
        NSLog("[AuthenticationInput] Relying Party ID: %@", _rp)
        NSLog("[AuthenticationInput] PRF Extension: %@", String(describing: _prf))
        NSLog("[AuthenticationInput] PRF Eval Data: %@", String(describing: _eval))

        let authController = ASAuthorizationController(authorizationRequests: [platformKeyRequest])
        authController.delegate = self
        authController.presentationContextProvider = self
        authController.performRequests()
    }

Logs:

Feb  5 11:26:27 App(Foundation)[32685] <Notice>: [AuthenticationInput] Starting authentication with parameters
Feb  5 11:26:27 App(Foundation)[32685] <Notice>: [AuthenticationInput] Challenge: randomChallenge123
Feb  5 11:26:27 App(Foundation)[32685] <Notice>: [AuthenticationInput] Relying Party ID: umbrellasoftware.ro
Feb  5 11:26:27 App(Foundation)[32685] <Notice>: [AuthenticationInput] PRF Extension: Optional(["eval": ["first": "MTI1LDMxLDUwLDYsMjQyLDE5Niw0NCw5OSwyMTIsMTQwLDEzLDEzNSwxNjUsNzYsMTM5LDIzNCwxMzAsMjM1LDE4OSwyNDYsMTMxLDM4LDIxNywyMzYsMTcyLDE3NCw2Nyw4MiwxODAsNzksMTM3LDE1MA=="]])
Feb  5 11:26:27 App(Foundation)[32685] <Notice>: [AuthenticationInput] PRF Eval Data: Optional(["first": **"MTI1LDMxLDUwLDYsMjQyLDE5Niw0NCw5OSwyMTIsMTQwLDEzLDEzNSwxNjUsNzYsMTM5LDIzNCwxMzAsMjM1LDE4OSwyNDYsMTMxLDM4LDIxNywyMzYsMTcyLDE3NCw2Nyw4MiwxODAsNzksMTM3LDE1MA=="])**

After user authenticates i process the credential assertion like this:


func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {

        switch authorization.credential {

        case let credentialRegistration as ASAuthorizationPlatformPublicKeyCredentialRegistration:

            let id = Base64URLUtils.encode(credentialRegistration.credentialID)

            let rawId = Base64URLUtils.encode(credentialRegistration.credentialID)

            let type = "public-key"

            let authenticatorAttachment = getAuthenticatorAttachment(attachment: credentialRegistration.attachment)

            let attestationObject = credentialRegistration.rawAttestationObject != nil ? Base64URLUtils.encode(credentialRegistration.rawAttestationObject!) : ""

            let clientDataJSON = Base64URLUtils.encode(credentialRegistration.rawClientDataJSON)

            

            var clientExtensionResults: [String: Any] = [:]            

            if let prf = credentialRegistration.prf,

               let symmetricKey = prf.first {

                let prfData = symmetricKey.withUnsafeBytes { Data($0) }

                let prfResult = Base64URLUtils.encode(prfData)

                clientExtensionResults["prf"] = ["results": ["first": prfResult]]

            }

            

            call.resolve([

                "rawId": rawId,

                "authenticatorAttachment": authenticatorAttachment,

                "type": type,

                "id": id,

                "response": [

                    "attestationObject": attestationObject,

                    "clientDataJSON": clientDataJSON

                ],

                "clientExtensionResults": clientExtensionResults

            ])

        case let credentialAssertion as ASAuthorizationPlatformPublicKeyCredentialAssertion:

            NSLog("[CredentialAssertion] Starting to process credential assertion")

            NSLog("[CredentialAssertion] Credential ID: %@", Base64URLUtils.encode(credentialAssertion.credentialID))

            NSLog("[CredentialAssertion] Raw Client Data JSON: %@", Base64URLUtils.encode(credentialAssertion.rawClientDataJSON))

            NSLog("[CredentialAssertion] Raw Authenticator Data: %@", Base64URLUtils.encode(credentialAssertion.rawAuthenticatorData))

            NSLog("[CredentialAssertion] Signature: %@", Base64URLUtils.encode(credentialAssertion.signature))

            NSLog("[CredentialAssertion] User ID: %@", Base64URLUtils.encode(credentialAssertion.userID))

            

            let id = Base64URLUtils.encode(credentialAssertion.credentialID)

            let rawId = Base64URLUtils.encode(credentialAssertion.credentialID)

            let type = "public-key"

            let authenticatorAttachment = getAuthenticatorAttachment(attachment: credentialAssertion.attachment)

            let clientDataJSON = Base64URLUtils.encode(credentialAssertion.rawClientDataJSON)

            

            // Initialize empty PRF result string that will store the Base64URL encoded PRF data

            var prfResult = ""

            

            // Check if PRF (Pseudorandom Function) data is available from the credential

            var clientExtensionResults: [String: Any] = [:]

            if let prfCredentialData = credentialAssertion.prf,

               let prfSymmetricKey = prfCredentialData.first as? SymmetricKey {

                let prfRawBytes = prfSymmetricKey.withUnsafeBytes { Data($0) }

                let prfResult = Base64URLUtils.encode(prfRawBytes)

                clientExtensionResults["prf"] = ["results": ["first": prfResult]]

                NSLog("[CredentialAssertion] PRF Result: %@", prfResult)

            } else {

                NSLog("[CredentialAssertion] No PRF data available")

            }

            

            

            let authenticatorData = Base64URLUtils.encode(credentialAssertion.rawAuthenticatorData)

            let signature = Base64URLUtils.encode(credentialAssertion.signature)

            let userHandle = Base64URLUtils.encode(credentialAssertion.userID)

            call.resolve([

                "rawId": rawId,

                "authenticatorAttachment": authenticatorAttachment,

                "type": type,

                "id": id,

                "response": [

                    "clientDataJSON": clientDataJSON,

                    "authenticatorData": authenticatorData,

                    "signature": signature,

                    "userHandle": userHandle

                ],

                "clientExtensionResults": clientExtensionResults

            ])

        default:

            call.reject(PasskeyError.UNKNOWN.rawValue)

        }

    }

Logs:


Feb  5 11:38:13 App(AuthenticationServices)[32793] <Notice>: Successfully completed authorization: <ASAuthorizationPlatformPublicKeyCredentialAssertion: 0x3002f80e0>

Feb  5 11:38:13 App(Foundation)[32793] <Notice>: [CredentialAssertion] Starting to process credential assertion

Feb  5 11:38:13 App(Foundation)[32793] <Notice>: [CredentialAssertion] Credential ID: NxlQaXK64UAY0EsehesfFy9rt-0

Feb  5 11:38:13 App(Foundation)[32793] <Notice>: [CredentialAssertion] Raw Client Data JSON: eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoicmFuZG9tQ2hhbGxlbmdlMTJ3Iiwib3JpZ2luIjoiaHR0cHM6Ly91bWJyZWxsYXNvZnR3YXJlLnJvIn0

Feb  5 11:38:13 App(Foundation)[32793] <Notice>: [CredentialAssertion] Raw Authenticator Data: vldv6g0NnZfh2nEqmP9eXTqSD1qTiN4v-J9WG1xlVsYdAAAAAA

Feb  5 11:38:13 App(Foundation)[32793] <Notice>: [CredentialAssertion] Signature: MEQCH0XTR7IyewTV8J3Iv3pZII4ohTKdKRtreRhagUZjG8cCIQDhFCuPJJumOjLJaWbvw0ppuxh1EIjL_QMFIKuh-73M6Q

Feb  5 11:38:13 App(Foundation)[32793] <Notice>: [CredentialAssertion] User ID: dXNlcjEyMw

Feb  5 11:38:13 App(Foundation)[32793] <Notice>: [CredentialAssertion] PRF Result: **nmKfLMSLd0vuV8xgAMQQ40rajzkSdHM_f3V4mq5UUqo**

When using cross platform as in the images below, i get the following logs:




Feb  5 11:40:29 App(Foundation)[32793] <Notice>: [AuthenticationInput] Starting authentication with parameters



Feb  5 11:40:29 App(Foundation)[32793] <Notice>: [AuthenticationInput] Challenge: randomChallenge123



Feb  5 11:40:29 App(Foundation)[32793] <Notice>: [AuthenticationInput] Relying Party ID: umbrellasoftware.ro



Feb  5 11:40:29 App(Foundation)[32793] <Notice>: [AuthenticationInput] PRF Extension: Optional(["eval": ["first": "MTI1LDMxLDUwLDYsMjQyLDE5Niw0NCw5OSwyMTIsMTQwLDEzLDEzNSwxNjUsNzYsMTM5LDIzNCwxMzAsMjM1LDE4OSwyNDYsMTMxLDM4LDIxNywyMzYsMTcyLDE3NCw2Nyw4MiwxODAsNzksMTM3LDE1MA=="]])



Feb  5 11:40:29 App(Foundation)[32793] <Notice>: [AuthenticationInput] PRF Eval Data: Optional(["first": "MTI1LDMxLDUwLDYsMjQyLDE5Niw0NCw5OSwyMTIsMTQwLDEzLDEzNSwxNjUsNzYsMTM5LDIzNCwxMzAsMjM1LDE4OSwyNDYsMTMxLDM4LDIxNywyMzYsMTcyLDE3NCw2Nyw4MiwxODAsNzksMTM3LDE1MA=="])






Feb  5 11:41:00 App(Foundation)[32793] <Notice>: [CredentialAssertion] Starting to process credential assertion



Feb  5 11:41:00 App(Foundation)[32793] <Notice>: [CredentialAssertion] Credential ID: NxlQaXK64UAY0EsehesfFy9rt-0



Feb  5 11:41:00 App(Foundation)[32793] <Notice>: [CredentialAssertion] Raw Client Data JSON: eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoicmFuZG9tQ2hhbGxlbmdlMTJ3Iiwib3JpZ2luIjoiaHR0cHM6Ly91bWJyZWxsYXNvZnR3YXJlLnJvIn0



Feb  5 11:41:00 App(Foundation)[32793] <Notice>: [CredentialAssertion] Raw Authenticator Data: vldv6g0NnZfh2nEqmP9eXTqSD1qTiN4v-J9WG1xlVsYdAAAAAA



Feb  5 11:41:00 App(Foundation)[32793] <Notice>: [CredentialAssertion] Signature: MEYCIQDbC5w5qwZ8shQbSQOTQ08n1WaYrQsYBvNrtc8IyvzXEgIhAJHydA8N0eoTLCwNyPV846sCtkzF0tGpzI2sRaOrd1Pv



Feb  5 11:41:00 App(Foundation)[32793] <Notice>: [CredentialAssertion] User ID: dXNlcjEyMw



Feb  5 11:41:00 App(Foundation)[32793] <Notice>: [CredentialAssertion] PRF Result: **HztucMbG6UQ5QOVhM17gwi3C7S1kxC7isCYfaS8zbNw**



as you can see, the PRF output is different

I've faced with the same issue

@andreigiura did you submit via Feedback Assistant? Any response?

@Systems Engineer can you confirm if this is a bug that will be fixed or expected behaviour? I have also submitted the feedback assistant ticket (awaiting response)

Accepted Answer

Yes this was a bug. The PRF values returned over hybrid should match the ones returned locally for the same input. This issue should be fixed in the current iOS 18.4 and macOS 15.4 betas.

i can confirm that it works correctly with the above versions. Thanks

@Systems Engineer while this does fix the problem, we do face another problem at the moment. Is there a method that we can use to recover the old generated "keys"? Some of the users might not be able to decrypt what they have encrypted with the wrong keys with older OS versions. Also, is there a way to stop new users that use the older OS version to use this passkeys in our app? We can check the version of the OS if is a platform authentication, but we do not have control in checking the cross platform device OS version. How should we deal with this? Any suggestions would be highly appreciated

@Systems Engineer Hello Apple Team,

I would like to bring to your attention that this issue also occurs when using cross-device authentication on Windows when using Iphone 18.3.1

Could you clarify whether this is a broader cross-device issue, an iOS/macOS-specific issue, or something else?

Additionally, how can we ensure backward compatibility with older versions, or at least prevent users from using versions where this issue persists?

@Systems Engineer

We've conducted thorough testing across multiple scenarios and observed that:

  1. The PRF inconsistency affects all cross-platform authentication flows when using iOS 18.0-18.3 devices as authenticators (via QR code scanning), regardless of whether the relying party is on macOS, Windows OS

  2. Our testing confirms data is permanently inaccessible when:

  • A user encrypts data using the incorrect PRF output from cross-platform authentication on iOS 18.0-18.3
  • They upgrade to iOS 18.4+ which generates different (correct) PRF outputs
  • This creates a critical data loss situation for applications using PRF for encryption
  1. We currently have no reliable method to detect iOS version during cross-platform authentication or prevent affected users from using this flow.

This presents significant challenges for applications using PRF extension for cryptographic purposes. Could you please provide:

  1. Guidance on how to safely migrate users with data encrypted using incorrect PRF values
  2. Any API or mechanism to identify potentially affected devices during cross-platform authentication
  3. Recommendations for maintaining backward compatibility while ensuring data integrity
  4. Is it possible to work around this issue by implementing a custom QR code solution, or does the inconsistency occur at a deeper system level within the cross-platform authentication process?

Thank you for addressing this issue in iOS 18.4, but we urgently need guidance on handling the transition period for existing users.

I got the same problems with this one Do we have any solution to migrate data from old version to new version?

I encountered the same problem.

Different PRF output when using platform or cross-platform authentication attachement
 
 
Q