Unexpected RP ID hash when registering a passkey in an iOS app

421 views Asked by At

I am trying to implement a login with passkeys in a SwiftUI app, based on the sample code provided by Apple (food truck app). On the server side, I am using node.js with @simplewebauthn package. As of now, the web version of the signin/ login process works fine. But when I create a passkey from the swiftUI app and send the registration response back to the server the rpIdHash doesn't match the expected rpIdHash.

At the end of the registration process in swift, in handleAuthorizationResult(_ authorizationResult: ASAuthorizationResult, username: String? = nil), I receive an ASAuthorizationResult, which leads to a .passkeyRegistration case (when registering a new passkey). I then recreate a credential object that has the following structure :

{
    id: passkeyRegistration.credentialID,
    rawId: passkeyRegistration.credentialID,
    type: “public-key“,
    authenticatorAttachment: “platform“,
    response: {
        clientDataJSON: passkeyRegistration.rawClientDataJSON,
        attestationObject: passkeyRegistration.rawAttestationObject,
        transports: ["internal","hybrid"]
    }
}

Which I manage to decode properly on the server-side, but the rpIdHash (203,189,6,160,8,217,220,110,195,214,214,218,140,243,77,214,79,53,154,17,152,74,224,35,148,70,128,25,42,14,122,101) of the attestation object from Apple doesn’t match the expected rpIdHash (203,189,6,160,8,217,220,110,195,214,214,218,140,243,77,214,79,53,154,17,152,74,239,227,148,70,190,25,42,14,122,101). But what is weird is that they only differ from three “keys“ :

  • Expected "22":239, "23":227, "26":190
  • Recieved "22":224, "23":35, "26":128

So I tried to sample some different rpIdHash to find out where this error might come from but I couldn’t get any hash that would match the one from Apple’s ASAuthorizationResult.

I have no idea how to fix this as both hashing process do not depend on me, so I would really appreciate any help.

PS: For reference, here is the hashing function used by @simplewebauthn:

/**
 * Returns hash digest of the given data, using the given algorithm when provided. Defaults to using
 * SHA-256.
 */
export function toHash(
  data: Uint8Array | string,
  algorithm: COSEALG = -7,
): Promise<Uint8Array> {
  if (typeof data === 'string') {
    data = isoUint8Array.fromUTF8String(data);
  }

  const digest = isoCrypto.digest(data, algorithm);

  return digest;
}
2

There are 2 answers

1
Rodrigue On BEST ANSWER

Ok I think I have found where does the problem comes from. In the .passkeyRegistration case, I tried to get the attestationObject as follows:

let rawAttestationObject = passkeyRegistration.rawAttestationObject
let unsafeUnwrap = rawAttestationObject.unsafelyUnwrapped

let uint8Ptr = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: 182)
let uint8PtrCount = uint8Ptr.count

print("uint8ptr :: \(uint8Ptr)")
var bytes: [UInt8] = []
for i in uint8Ptr {
    print("\(i)")
    bytes.append(i)
}

Which when I then passed into the server directly :

const bytesFromNSData = new Uint8Array([163,99,102,109,116,100,110,111,110,101,103,97,116,116,83,116,109,116,160,104,97,117,116,104,68,97,116,97,88,152,203,189,6,160,8,217,220,110,195,214,214,218,140,243,77,214,79,53,154,17,152,74,239,227,148,70,190,25,42,14,122,101,93,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,143,66,13,222,33,34,220,187,195,26,161,164,71,230,9,96,61,48,165,42,165,1,2,3,38,32,1,33,88,32,188,210,91,139,14,0,82,46,250,20,230,184,210,55,142,180,31,135,163,47,22,232,245,35,39,18,12,137,210,250,44,245,34,88,32,226,27,251,25,117,245,106,27,182,131,130,8,160,141,4,119,6,128,114,13,14,82,185,119,129,26,216,161,160,10,6,63])

const decodedFromNSData = decodeAttestationObject(bytesFromNSData)
const authNSData = decodedFromNSData.get('authData');
const parsedAuthNSData = parseAuthenticatorData(authNSData);

const {
   aaguid,
   rpIdHash,
   flags,
   credentialID,
   counter,
   credentialPublicKey,
   extensionsData,
} = parsedAuthNSData;

And this then gave me the right expected RPid hash!

So I suppose the issue comes from casting the rawAttestationObject in the following swift struct:

public struct RawPublicKeyCredential: Encodable {
    public var id: Data = Data("".utf8)
    public var rawId: Data = Data("".utf8)
    public var type: String = "public-key"
    public var authenticatorAttachment: String = "platform"
    public var response: RawPublicKeyResponse = RawPublicKeyResponse()
    public var user_id: String = ""
}

public struct RawPublicKeyResponse: Encodable {
    public var clientDataJSON: String = ""
    public var attestationObject: Data? = Data("".utf8)
    public var transports: [String] = ["internal","hybrid"]
}

Which I originally did like that:

var res = RawPublicKeyCredential()
res.id = credentialID
res.rawId = credentialID
res.response.attestationObject = rawAttestationObject

Hence I then tried to cast it as follows:

let data = Data(bytes: bytes, count: bytes.count)
res.response.attestationObject = data

But it still didn't work. My guess would be that the issue come from the Data() type, but I don't know what to replace it with to fix this. So far, I have tried the following:

let data = Data(bytes: bytes, count: bytes.count)
let str = NSString(data: data, encoding: NSUTF8StringEncoding)
let bytestStr = String(bytes: bytes, encoding: .utf8)
let dataStr = String(data: data, encoding: .utf8)

but none of them worked (bytestStr and dataStr were set to nil and NSString doesn't conform to Encodable).

So if someone has an idea on how to fix this I would be very gratefull.

UPDATE: Ok I'm actually an idiot, from the beginning in was a Base64 encoded String when it needed to be Base64URL. Didn't know it could make such a difference. At least I got to understand a bit more of what's happening under the hood during the passkey registration process.

So if anyone is wondering or happens to have the same problem, the final 2/3 lines I added are:

let data = Data(bytes: bytes, count: bytes.count)
let base64URLattestation = Base64URL.encode(data)
res.response.attestationObject = base64URLattestation

or just:

let base64URLattestation = Base64URL.encode(rawAttestationObject!)
res.response.attestationObject = base64URLattestation

Anyway, thank you @agl for the help and hope this post can help if someone runs into the same issue.

2
agl On

It would be almost impossible to find a preimage for a SHA-256 hash that differs only in a few places. So this is not a question of the input to SHA-256 being wrong, but rather that the hash has been corrupted.

What's the RP ID in question?

Try to print the authenticatorData value as soon as possible within the iOS app and see whether the hash is correct there or not. (The first 32 bytes are the RP ID hash.)

If the hash is corrupt at this point then see, if you ignore the RP ID mismatch, does the signature over the authenticator data verify? If so, then the issue must be within iOS.

If the hash is not corrupt then somewhere within your app, or in transmission to the server, something is corrupting it. You'll probably need to print the value out at lots of points to narrow down where the corruption is happening.