パスキーもどきのwebページを70行くらいで書いてみた
GitHub Pagesで公開しました。
このページにアクセスすることで気軽にパスキーの挙動を試せます!!
やりたかったこと
先日パスキーの実装を試してみた。結構複雑で難易度が高いなぁと思いました。
シンプルに考える為に、見た目だけそれっぽいwebページを最小限の実装で作ってみようと思いました。コード
<!DOCTYPE html>
<html lang="ja">
<body>
<h1>パスキーもどき v0.5</h1>
<p>
パスキー認証: <button onclick="authenticate()">認証</button>
</p>
<p>
パスキー登録: <input type="text" id="name" placeholder="パスキー名">
<button onclick="register()">登録</button>
</p>
<script>
// パスキー認証
async function authenticate() {
const options = {
challenge: new Uint8Array([0]),
};
try {
const assertion = await navigator.credentials.get({
publicKey: options
});
console.log("認証成功:", assertion);
alert("認証成功:");
} catch (e) {
alert('認証エラー: ' + e);
console.error(e);
}
}
// 登録
async function register() {
const name = document.getElementById('name').value;
if (!name) {
alert('名前を入力してください');
return;
}
const publicKeyCredentialCreationOptions = {
challenge: new Uint8Array([0]),
rp: {
name: "fake-passkey",
},
user: {
id: new TextEncoder().encode(name),
name: name,
displayName: name
},
pubKeyCredParams: [
{ type: "public-key", alg: -7 },
],
authenticatorSelection: {
residentKey: "required"
}
};
try {
const credential = await navigator.credentials.create({
publicKey: publicKeyCredentialCreationOptions
});
alert('登録成功:', credential);
} catch (e) {
console.error(e);
alert('登録失敗');
}
}
</script>
</body>
</html>
使い方
- 上記のコードをコピーして
index.html
の名前で適当なフォルダに保存 - httpサーバーを立ち上げる。例えば
npx http-server
- ブラウザからlocalhostに接続する。例えば
http://localhost:8080
- 注意:IPアドレス指定だとパスキーが使えないっぽいので
http://127.0.0.1:8080
はダメ
- 注意:IPアドレス指定だとパスキーが使えないっぽいので
- パスキーの登録(もどき)とパスキーの認証(もどき)ができる
解説
何を実装していて、何を実装していないか
上記のコードは、パスキーの登録処理とパスキーの認証処理を、サーバーサイドの実装を全て無視してブラウザでの処理だけ実装しました。かつ、セキュリティを高めるための各種パラメータをなるべく使用せずに最小限の必須パラメータだけで雑に作りました。
本来あるべき処理を学びたい方はMDNのドキュメントをご参照ください。
パスキー登録もどきの処理を説明
パスキーの登録処理はnavigator.credentials.create
を使うことで実行できます。
この関数の入力引数はpublicKeyCredentialCreationOptions
です。
正しいパスキーは、publicKeyCredentialCreationOptions
の値はサーバー側で生成しますが、今回はもどきなので実行直前に必須項目だけ適当に埋めます。上記のドキュメントによると必須項目は以下です。
- challenge
- 所謂チャレンジレスポンス認証で使うパラメータ。
- 本来はサーバーから提供されるランダムな値だが、今回は0固定。
- user
- ユーザーアカウントの情報。
- 今回はテキストボックスに入力されたテキストを必須項目である
displayName
,id
,name
全てに適用。
- rp
- Replying Party。信頼当事者。要するに認証をするサーバーのこと。
- 本来はサーバーの情報が入るが、今回は必須の
name
だけを適当に設定。
- pubKeyCredParams
- 鍵の種類と署名アルゴリズム
- ドキュメントを参考に、
alg
は-7:ES256
,types
はpublic-key
を指定
- authenticatorSelection (2024/11 追記)
- 本来は省略可能な項目ですが、Androidの場合は何故かこの項目の以下の設定がないとパスキー登録が出来なかった(レスポンスは正常なのに実際は登録されていない)ので追加しました
residentKey: "required"
- iOS、Mac、Windowsではこの項目がなくても動きます。謎です。
- MDSのドキュメントはこちら。
- 本来は省略可能な項目ですが、Androidの場合は何故かこの項目の以下の設定がないとパスキー登録が出来なかった(レスポンスは正常なのに実際は登録されていない)ので追加しました
navigator.credentials.create
を実行すると、パスキーが生成されクライアント側に秘密鍵が保存され、戻り値として公開鍵が返ってきます。
正しいパスキーでは公開鍵をサーバーに登録しますが、今回は何もせずに捨てます。
「捨てたら認証できなくない?」と思われるかもしれませんが、秘密鍵は保存されており署名ができるので問題ありません。(正しいパスキーとしては署名の検証ができないので問題しかありません。)
パスキー認証もどきの処理を説明
パスキーの認証処理はnavigator.credentials.get
を使うことで実行できます。
この関数の入力引数はpublicKeyCredentialRequestOptions
です。この引数、何故かMDNのページが見当たらないので、W3Cのドキュメントを貼っておきます。
正しいパスキーではpublicKeyCredentialRequestOptions
も当然サーバーから取得しますが、今回は例によってクライアント側で必須項目だけを埋めます。そして、なんと、必須項目が1つしかありません。
- challenge
- 所謂チャレンジレスポンス認証で使うパラメータ。
- 本来はサーバーから提供されるランダムな値だが、今回は0固定。
navigator.credentials.get
を実行するとchallenge
を秘密鍵で署名したデータが取得できます。正しいパスキーではこれをサーバーに送って公開鍵で検証しますが、今回は例によって何もせずに捨てます。
以上、サーバー側の処理を一切書いてないのに、なんかそれっぽい動きをするwebページの解説でした。
感想
パスキーの処理って一見複雑でわかりにくいイメージですが、こうやって切り出して見てみると本質的には一般的な電子署名であることが分かりました。半分以上ネタで作ったのですが、意外と勉強になりました。
また、適当に実装した割に一見すると普通に認証している処理に見えるので怖いなぁと思いました。
NCDC株式会社( ncdc.co.jp/ )のエンジニアチームです。 募集中のエンジニアのポジションや、採用している技術スタックの紹介などはこちら( github.com/ncdcdev/recruitment )をご覧ください! ※エンジニア以外も記事を投稿することがあります
Discussion