🧇

パスキーもどきのwebページを70行くらいで書いてみた

2024/08/06に公開

やりたかったこと

先日パスキーの実装を試してみた。結構複雑で難易度が高いなぁと思いました。
https://zenn.dev/ncdc/articles/2d7b13c79e31f7
シンプルに考える為に、見た目だけそれっぽいwebページを最小限の実装で作ってみようと思いました。

コード

index.html
<!DOCTYPE html>
<html lang="ja">

<body>
  <h1>パスキーもどき</h1>
  <p>
    パスキー認証 :
    <button onclick="authenticatePasskey()">パスキーで認証</button>
  </p>
  <p>
    パスキー登録 :
    <input type="name" id="name" placeholder="なまえ">
    <button onclick="registerPasskey()">パスキーを登録</button>
  </p>

  <script>
    // パスキー認証
    async function authenticatePasskey() {
      const publicKeyCredentialRequestOptions = {
        challenge: new Uint8Array([0]),
      };

      try {
        const assertion = await navigator.credentials.get({
          publicKey: publicKeyCredentialRequestOptions,
        });
        console.log("認証成功:", assertion);
        alert('認証成功');
      } catch (error) {
        console.error("認証エラー:", error);
        alert('認証失敗');
      }
    }

    // パスキー登録
    async function registerPasskey() {
      const name = document.getElementById('name').value;

      if (!name) {
        alert('名前を入力してください。');
        return;
      }

      const publicKeyCredentialCreationOptions = {
        challenge: new Uint8Array([0]),
        rp: {
          name: "パスキーもどき",
        },
        user: {
          id: new TextEncoder().encode(name),
          name: name,
          displayName: name,
        },
        pubKeyCredParams: [{ alg: -7, type: "public-key", }],
      };

      try {
        const credential = await navigator.credentials.create({
          publicKey: publicKeyCredentialCreationOptions,
        });
        console.log("登録成功:", credential);
        alert('登録成功');
      } catch (error) {
        console.error("登録失敗:", error);
        alert('登録失敗');
      }
    }
  </script>
</body>

</html>

使い方

  1. 上記のコードをコピーしてindex.htmlの名前で適当なフォルダに保存
  2. httpサーバーを立ち上げる。例えばnpx http-server
  3. ブラウザからlocalhostに接続する。例えばhttp://localhost:8080
    • 注意:IPアドレス指定だとパスキーが使えないっぽいのでhttp://127.0.0.1:8080はダメ
  4. パスキーの登録(もどき)とパスキーの認証(もどき)ができる

GitHub Pagesで公開しました。

https://k-ibaraki.github.io/fake-passkey/

解説

何を実装していて、何を実装していないか

上記のコードは、パスキーの登録処理とパスキーの認証処理を、サーバーサイドの実装を全て無視してブラウザでの処理だけ実装しました。かつ、セキュリティを高めるための各種パラメータをなるべく使用せずに最小限の必須パラメータだけで雑に作りました。

本来あるべき処理を学びたい方はMDNのドキュメントをご参照ください。
https://developer.mozilla.org/ja/docs/Web/API/Web_Authentication_API

パスキー登録もどきの処理を説明

パスキーの登録処理はnavigator.credentials.createを使うことで実行できます。
この関数の入力引数はpublicKeyCredentialCreationOptionsです。

https://developer.mozilla.org/ja/docs/Web/API/CredentialsContainer/create#publickey_オブジェクトの構造

正しいパスキーは、publicKeyCredentialCreationOptionsの値はサーバー側で生成しますが、今回はもどきなので実行直前に必須項目だけ適当に埋めます。上記のドキュメントによると必須項目は以下です。

  • challenge
    • 所謂チャレンジレスポンス認証で使うパラメータ。
    • 本来はサーバーから提供されるランダムな値だが、今回は0固定。
  • user
    • ユーザーアカウントの情報。
    • 今回はテキストボックスに入力されたテキストを必須項目である displayName,id,name 全てに適用。
  • rp
    • Replying Party。信頼当事者。要するに認証をするサーバーのこと。
    • 本来はサーバーの情報が入るが、今回は必須のnameだけを適当に設定。
  • pubKeyCredParams
    • 鍵の種類と署名アルゴリズム
    • ドキュメントを参考に、alg-7:ES256, typespublic-keyを指定

navigator.credentials.createを実行すると、パスキーが生成されクライアント側に秘密鍵が保存され、戻り値として公開鍵が返ってきます。
正しいパスキーでは公開鍵をサーバーに登録しますが、今回は何もせずに捨てます。

「捨てたら認証できなくない?」と思われるかもしれませんが、秘密鍵は保存されており署名ができるので問題ありません。(正しいパスキーとしては署名の検証ができないので問題しかありません。)

パスキー認証もどきの処理を説明

パスキーの認証処理はnavigator.credentials.getを使うことで実行できます。
この関数の入力引数はpublicKeyCredentialRequestOptionsです。この引数、何故かMDNのページが見当たらないので、W3Cのドキュメントを貼っておきます。
https://www.w3.org/TR/webauthn-2/#dictdef-publickeycredentialrequestoptions

正しいパスキーではpublicKeyCredentialRequestOptionsも当然サーバーから取得しますが、今回は例によってクライアント側で必須項目だけを埋めます。そして、なんと、必須項目が1つしかありません。

  • challenge
    • 所謂チャレンジレスポンス認証で使うパラメータ。
    • 本来はサーバーから提供されるランダムな値だが、今回は0固定。

navigator.credentials.getを実行するとchallengeを秘密鍵で署名したデータが取得できます。正しいパスキーではこれをサーバーに送って公開鍵で検証しますが、今回は例によって何もせずに捨てます。

以上、サーバー側の処理を一切書いてないのに、なんかそれっぽい動きをするwebページの解説でした。

感想

パスキーの処理って一見複雑でわかりにくいイメージですが、こうやって切り出して見てみると本質的には一般的な電子署名であることが分かりました。半分以上ネタで作ったのですが、意外と勉強になりました。
また、適当に実装した割に一見すると普通に認証している処理に見えるので怖いなぁと思いました。

NCDCエンジニアブログ

Discussion