为什么 WebAuthn 服务器 (go) 和客户端 (JavaScript) 之间的质询验证失败?

Why does a challenge validation fail between WebAuthn server (go) and client (JavaScript)?

提问人:Christian Rößner 提问时间:9/11/2023 最后编辑:Christian Rößner 更新时间:9/12/2023 访问量:114

问:

我正在开发客户端服务器应用程序。服务器部分是用纯 Go 编写的,使用 go-webauthn/webauthn 库。客户端是纯 JavaScript。

目前,我已经实现了一个寄存器开始和寄存器结束钩子,它已经进行了一些通信。

当用户使用他/她的 Web 浏览器登录时,可以注册 webauthn 设备。通过单击对接,会出现一个对话框,我可以触摸我的 Yubikey 的传感器。之后,数据被发送到服务器,服务器解析数据并进行验证。不幸的是,这失败了!

由于我对 JavaScript 非常陌生,我花了好几个小时,这是我目前的草稿:

const register_device = async (event) => {
    event.preventDefault();

    const buffer_encode = array_buffer => {
        return btoa(String.fromCharCode.apply(null, new Uint8Array(array_buffer)))
            .replace(/\+/g, "-")
            .replace(/\//g, "_")
            .replace(/=/g, "");
    }

    const buffer_decode = array_buffer => {
        return Uint8Array.from(array_buffer, c => c.charCodeAt(0))
    }

    // Check whether current browser supports WebAuthn
    if (!window.PublicKeyCredential) {
        alert("Error: this browser does not support WebAuthn");

        return;
    }

    let status_code

    // The username is stored in a cookie. Start the registration process...
    await fetch("/2fa/v1/webauthn/register/begin")
        .then(async response => {
            const credentialCreationOptions = await response.json();

            // console.log(credentialCreationOptions.publicKey.challenge);

            credentialCreationOptions.publicKey.challenge = buffer_decode(
                credentialCreationOptions.publicKey.challenge
            );
            credentialCreationOptions.publicKey.user.id = buffer_decode(
                credentialCreationOptions.publicKey.user.id
            );

            return navigator.credentials.create({
                publicKey: credentialCreationOptions.publicKey
            })
        })
        .then(async credential => {
            let attestationObject = credential.response.attestationObject;
            let clientDataJSON = credential.response.clientDataJSON;
            let rawId = credential.rawId;

            /*
            // Decode the clientDataJSON into a utf-8 string
            const utf8Decoder = new TextDecoder('utf-8');
            const decodedClientData = utf8Decoder.decode(clientDataJSON);

            // Parse the string as an object
            const clientDataObj = JSON.parse(decodedClientData);

            console.log(clientDataObj)
            */

            return await fetch("/2fa/v1/webauthn/register/finish", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json"
                },
                body: JSON.stringify({
                    id: credential.id,
                    rawId: buffer_encode(rawId),
                    type: credential.type,
                    response: {
                        attestationObject: buffer_encode(attestationObject),
                        clientDataJSON: buffer_encode(clientDataJSON),
                    }
                })
            })
        })
        .then (async response => {
            status_code = response.status;

            return await response.json()
        })
        .then (result => {
            console.log(`Registration result: code=${status_code} message=${result}`);
        })
        .catch (error => {
            console.error(`An error occurred when registering the user: ${error}`);
        });
}

device.addEventListener("click", register_device);

我有一个最好的猜测:

我真的不知道buffer_encode和buffer_decode功能是什么。它确实在某种程度上起作用,但我认为它破坏了一些东西。

我不明白的是,为什么我在buffer_enocde例程中需要这些 .replace() 语句?如果我删除这些行,为什么它根本不起作用?

有没有人可以向我展示正确的buffer_encode和buffer_decode功能,以便 webauthn 的其余部分正常工作?

目前,服务器在结束钩子处返回验证失败。

    response, err := protocol.ParseCredentialCreationResponseBody(ctx.Request.Body)
    if err != nil {
        ctx.JSON(http.StatusBadRequest, err.Error())

        return
    }

    credential, err := webAuthn.CreateCredential(user, *sessionData, response)
    if err != nil {
        ctx.JSON(http.StatusBadRequest, err.Error())

        return
    }

I use go-webauthn/webauthn with go-gingonic.

CreateCredential() throws the error I am asking about.

Many thanks in advance.

JavaScript Go 编码 解码 WebAuthn

评论

0赞 aseigler 9/11/2023
Those .replace()-statements are converting the buffer to URL safe base64 encoding without padding. Are there any exceptions being raised out of the server library? There should be some clues coming from the server.
0赞 aseigler 9/12/2023
Also, have you seen github.com/go-webauthn/example?
0赞 Christian Rößner 9/12/2023
Thanks for the example repo. I lokked into it and I grabbed to ProtoErrToFields-method. Here is, what the result error looks like: ``` Registration result: code=400 message=[{Key:err Type:26 Integer:0 String: Interface:Error validating challenge} {Key:details Type:15 Integer:0 String:Error validating challenge Interface:<nil>} {Key:info Type:15 Integer:0 String:Expected b Value: "roLau8pXNhIromdBNn6s3POQOFAwcmcV9s8PV53shRk" Received b: "cm9MYXU4cFhOaElyb21kQk5uNnMzUE9RT0ZBd2NtY1Y5czhQVjUzc2hSaw" Interface:<nil>} {Key:type Type:15 Integer:0 String:verification_error Interface:<nil>}] ```
0赞 Christian Rößner 9/12/2023
@aseigler As you see: b received and b stored are different and I can not figure out why

答:

0赞 aseigler 9/12/2023 #1

The challenges don't match. The second string () is a base64 encoded version of the first string ():cm9MYXU4cFhOaElyb21kQk5uNnMzUE9RT0ZBd2NtY1Y5czhQVjUzc2hSawroLau8pXNhIromdBNn6s3POQOFAwcmcV9s8PV53shRk

https://cyberchef.org/#recipe=From_Base64('A-Za-z0-9%2B/%3D',false,false)&input=Y205TVlYVTRjRmhPYUVseWIyMWtRazV1Tm5NelVFOVJUMFpCZDJOdFkxWTVjemhRVmpVemMyaFNhdw

Don't double base64 encode the challenge.

评论

0赞 Christian Rößner 9/12/2023
@seigler, first of all thanks to find the main issue. Can you show me an example for the buffer_encode and/or buffer_decode that solves the problem? I still have no clue how to solve it.
0赞 aseigler 9/12/2023
The example site does a good job of showing how to do this: github.com/go-webauthn/example/blob/…