😎

WebAuthnで"Client-side discoverable Credential"の挙動を確認する

2022/08/19に公開

ritouです。

これまでResidentKeyって言われてた "Client-side discoverable Credential" の挙動を確認しておきます。

環境は Chrome 104.0.5112.101@macOS というところです。

Client-side discoverable Credential is 何

仕様ではこの辺りです。

https://www.w3.org/TR/webauthn-2/#client-side-discoverable-credential

Client-side discoverable Credential を指定して Authenticator を登録

publicKey.authenticatorSelectionresidentKey, requireResidentKey あたりを指定します。

residentKey, of type DOMString
  Specifies the extent to which the Relying Party desires to create a client-side discoverable credential. For historical reasons the naming retains the deprecated “resident” terminology. The value SHOULD be a member of ResidentKeyRequirement but client platforms MUST ignore unknown values, treating an unknown value as if the member does not exist. If no value is given then the effective value is required if requireResidentKey is true or discouraged if it is false or absent.
  See ResidentKeyRequirement for the description of residentKey's values and semantics.

requireResidentKey, of type boolean, defaulting to false
  This member is retained for backwards compatibility with WebAuthn Level 1 and, for historical reasons, its naming retains the deprecated “resident” terminology for discoverable credentials. Relying Parties SHOULD set it to true if, and only if, residentKey is set to required.

https://example.com を訪れ、Chromeのコンソールから以下を実行したら動きます。

var rp = {
    name: "example.com",
    id: "example.com"
};

var user = {
    id: decode_b64_to_array_buffer("MjMzODQyMjQ0OTgyODUwMDA2MA=="),
    name: "user_12345",
    displayName: "FIDO太郎"
};

var createCredentialDefaultArgs = {
    publicKey: {
        rp: rp,
        user: user,

        pubKeyCredParams: [{
            type: "public-key",
            alg: -7
        }, {
            type: "public-key",
            alg: -37
        }],

        attestation: "none",

        challenge: decode_b64_to_array_buffer("KM5P053z9HKDKnfDBCdE6g"),

        authenticatorSelection: {
            authenticatorAttachment: "platform",
            userVerification: "required",
            residentKey: "required",
            requireResidentKey: true
        }
    }
};

function encode_array_buffer_to_b64(array_buffer) {
    return btoa(String.fromCharCode.apply(null, new Uint8Array(array_buffer))).replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '');
};

function decode_b64_to_array_buffer(b64) {
    return Uint8Array.from(window.atob(b64.replace(/_/g, '/').replace(/\-/g, '+')), c => c.charCodeAt(0)).buffer;
};

navigator.credentials.create(createCredentialDefaultArgs)
    .then((cred) => {
        // Base64URLエンコードしてdump
        console.log(encode_array_buffer_to_b64(cred.response.clientDataJSON));
        console.log(encode_array_buffer_to_b64(cred.response.attestationObject));
    })
    .catch((err) => {
        console.log("ERROR", err);
        alert("セキュリティキー/デバイスの設定に失敗しました。");
    });

見た目は普通のAuthenticator登録と一緒です。

https://www.youtube.com/watch?v=ezVWuqGGdLs

Client-side discoverable Credential で認証

上記同様、 https://example.com を訪れ、Chromeのコンソールから以下を実行したら動きます。

var CredentialRequestOptions = {
    publicKey: {
        userVerification: "required",
        challenge: decode_b64_to_array_buffer("KM5P053z9HKDKnfDBCdE6g"),
        rpId: "example.com",
        allowCredentials: []
    },
};

function encode_array_buffer_to_b64(array_buffer) {
    return btoa(String.fromCharCode.apply(null, new Uint8Array(array_buffer))).replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '');
};

function decode_b64_to_array_buffer(b64) {
    return Uint8Array.from(window.atob(b64.replace(/_/g, '/').replace(/\-/g, '+')), c => c.charCodeAt(0)).buffer;
};


navigator.credentials.get(CredentialRequestOptions)
    .then((assertion) => {
        console.log("ASSERTION", assertion)
        console.log(assertion.id);
        console.log(encode_array_buffer_to_b64(assertion.response.authenticatorData));
        console.log(encode_array_buffer_to_b64(assertion.response.clientDataJSON));
        console.log(encode_array_buffer_to_b64(assertion.response.signature));
    })
    .catch((err) => {
        console.log("ERROR.name", err.name);
        console.log("ERROR.message", err.message);
        alert("セキュリティキー/デバイスによるログインに失敗しました。");
    });

ローカル認証、ここではTouchIDの認証の後に、ユーザー情報の選択が求められます。

https://www.youtube.com/watch?v=9mCEajgyRsA

便利ですが、全ての環境で対応されているわけではありません。

(2022/8/20追記: Safari@MacOSはユーザー選択が先に来ます。前にbuildersconでWebAuthnの挙動調べた時も割とバラバラで、なんというかお前らそういうとこやぞって感じですね。)

Client-side discoverable Credential のIDを指定して認証

当然、ユーザーを識別した後に公開鍵のIDを指定してログインさせることも可能です。

Note: Client-side discoverable credentials are also usable in authentication ceremonies where credential IDs are given, i.e., when calling navigator.credentials.get() with a non-empty allowCredentials argument.

publicKey.allowCredentials にIDを指定してやればいいです。

var CredentialRequestOptions = {
    publicKey: {
        userVerification: "required",
        challenge: decode_b64_to_array_buffer("KM5P053z9HKDKnfDBCdE6g"),
        rpId: "example.com",
        allowCredentials: [{
                    // cred_id
                    id: decode_b64_to_array_buffer("KDvhBDip6v9G7VHt_1KsynLcOt6Gw6lTVPSLWAqzVqtiN-aFhWHU41Lg5Gz94PAtzPCQhaasakbqtEDixz9xgSX7C0TuZSMl"),
                    type: "public-key"
                }]
    },
};

https://www.youtube.com/watch?v=IJu_Uzr1ejE

これができるってことは、以下のように移行することもできそうです。

  1. Client-side discoverable Credentialとして登録
  2. 最初は公開鍵のIDを指定して認証要求
  3. 後からClient-side discoverable Credentialとして認証要求

今回はここまでにして、次回はClient-side discoverable Credentialに非対応環境での動作も見てみましょう。

ではまた!

おまけ:猫かわいい

https://www.youtube.com/watch?v=Pv77mqIvtTw

Discussion