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

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