duo-labs / android-webauthn-authenticator

A WebAuthn Authenticator for Android leveraging hardware-backed key storage and biometric user verification.
BSD 3-Clause "New" or "Revised" License
110 stars 20 forks source link

Error validating the assertion signature #4

Closed thavel closed 5 years ago

thavel commented 5 years ago

Hello,

I'm trying to make Webauthn works on my Google Pixel 3XL (running under Android 9 Pie) with this library.

I've started building an authentication backend using duo-labs/webauthn Go library (amazing job, btw!), and I wrote a basic Android app. (SDK >= 28) with this lib., to register and to use my phone's TPM as an authenticator.

I've managed to make the registration works, but I ran into an issue during authentication:

Error validating the assertion signature: <nil>

Which is one of the last steps of the webauthn.FinishLogin() call.

Hence, I've updated my app. to use https://webauthn.io API instead, and ran into the issue:

POST https://webauthn.io/assertion
Error validating the assertion signature: <nil>

I'm probably doing something wrong when mapping API inputs/outputs with this lib parameters, but I can't figure out where...

Here is the Android (Kotlin) code I wrote so far:

val authenticator = Authenticator(applicationContext, true, true)
val user = "tester@test.io"

fun register() {
    Thread(Runnable {
        // Begin authenticator registration
        val (_, res2, payload2) = Fuel.get("https://webauthn.io/makeCredential/$user?attType=none&authType=platform")
            .responseObject(RegistrationRequest.Deserializer())
            .throwError()
        val session = res2.headers["Set-Cookie"]?.get(0) as String
        val beginRegistration = payload2.get()
        Log.d("FIDO", "--- RegistrationRequest ---\n${beginRegistration.format()}")
        val attestation = authenticator.makeCredential(
            beginRegistration.toOptions(),
            applicationContext,
            CancellationSignal()
        )

        // Finish authenticator registration
        val data3 = RegistrationResponse.makeResponse(beginRegistration, attestation)
        Log.d("FIDO", "--- RegistrationResponse ---\n${data3.toJson()}")
        val (_, _, payload3) = Fuel.post("https://webauthn.io/makeCredential")
            .header("cookie" to session)
            .body(data3.toJson())
            .responseString()
            .throwError()
        val res = payload3.get()
        Log.d("FIDO", "--- RegistrationResult ---\n$res")
    }).start()
}

fun signin() {
    Thread(Runnable {
        // Begin login with authenticator
        val (_, res1, payload1) = Fuel.get("https://webauthn.io/assertion/$user")
            .responseObject(SigninRequest.Deserializer())
            .throwError()
        val beginLogin = payload1.get()
        val session = res1.headers["Set-Cookie"]?.get(0) as String
        Log.d("FIDO", "--- LoginRequest ---\n${beginLogin.format()}")
        val assertion = authenticator.getAssertion(
            beginLogin.toOptions(),
            { credentialList -> credentialList[0] },  // I've only one credential registered anyway
            applicationContext,
            CancellationSignal()
        )

        // Finish authenticator login
        val data2 = SigninResponse.makeResponse(beginLogin, assertion)
        Log.d("FIDO", "--- LoginRequest ---\n${data2.toJson()}")
        val (_, _, payload2) = Fuel.post("https://webauthn.io/assertion")
            .header("cookie" to session)
            .body(data2.toJson())
            .responseString()
            .throwError()
        val res = payload2.get()
        Log.d("FIDO", "--- LoginRequest ---\n$res")
    }).start()
}

Here is the code I wrote for data classes/mappers:

The ByteArrayUtils I'm using is:

import android.util.Base64
object ByteArrayUtils {
    fun base64URL(str: String): String {
        val decoded = Base64.decode(str, Base64.DEFAULT)
        return encodeBase64URL(decoded)
    }
    fun encodeBase64URL(bytes: ByteArray): String {
        return Base64.encodeToString(bytes, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
    }
    fun decodeBase64URL(str: String): ByteArray {
        return Base64.decode(str, Base64.NO_PADDING or Base64.NO_WRAP)
    }
}

You might notice that I built attestation and assertion options manually instead of using .fromJSON() methods suggested in the README. I tried .fromJSON() methods, but I still got the Error validating the assertion signature anyway.

I tried to use https://webauthn.io demo webapp from Chrome on my phone, and both registration and signin work (using my phone's TPM as authenticator type).

Any chance you guys can tell me what I am doing wrong? Thanks in advance!

thavel commented 5 years ago

I finally figured out why it didn't work. I went through the webauthn spec again : Assertion signature And I realized that I did something wrong with the clientDataHash (inputs) and the clientDataJSON (outputs), as I was giving only the base64url encoded challenge to the getAssertion() options. Instead, I properly formatted the clientDataHash as a sha256 encoded JSON with my base64url encoded challenge, origin and operation's type as described in the spec. And it works great now!