パスキー(passkey)について

注釈

ここは概要のみとしておきます。 詳細については各自で調べると良いでしょう。

パスキー(passkey)は、FIDOアライアンスとW3Cによって策定された、パスワードに代わる認証方法です。パスキーは、公開鍵暗号方式を利用しており、ユーザーのデバイスに秘密鍵が安全に保存され、サーバーには対応する公開鍵が保存されます。これにより、パスワードのように盗まれたり推測されたりするリスクが大幅に減少します。

パスキーの日本語的表現として、カタカナで「パスキー」と書く一方で、技術文書ではpasskeyと英字(小文字)で書かれていることあります。ここでは以降、パスキーと書くことにします。

パスキーの特徴

パスキーは、従来型パスワードが抱えていた「推測・総当たり・漏洩リスト再利用」といった典型的攻撃面そのものを回避する設計になっています。 利用者はフォームに文字列を入力する代わりに、端末上で生体認証やデバイス解除操作を一度行うだけで、端末が保持する秘密鍵によって署名を生成します。 この操作には「所有要素(端末)」と「生体またはPIN」という二要素が事実上一体化しており、ユーザー体験を損なわず多要素性が担保されます。 秘密鍵は端末のセキュア領域から外部へ抽出できない形で保持され、サービス側には対応する公開鍵のみが保存されます。

主要プラットフォームは、ユーザの明示的同意の下で安全な同期基盤を提供し、複数デバイス間でパスキーをローミング可能にします。 さらに、各サービスごとに異なる鍵ペアが発行されるため、横断的トラッキングの余地を抑制しプライバシー保護にも寄与します。 フィッシング耐性についても、署名生成時にオリジン(RP ID)境界[1] が強制されるため、偽サイト上では正しい署名が構成できません。 毎回サーバが提示するランダムなチャレンジに対して新規署名を行う方式はリプレイを封じ、仮に端末が盗難されたとしても生体もしくはPINコードによる検証を通過しない限り署名生成が成立しません。

結果として、入力・再設定など運用負荷の高い体験を減らしつつ、攻撃面の多くを構造的に縮減する認証形態となっています。

パスキーの仕組み

高レベルでは「登録(credential 作成)」と「認証(assertion 生成)」の2フェーズになっています。 かなり難しい書き方になっていますが、すごく簡潔に言うと、以下のようになります。

  • いわゆる公開鍵暗号の仕組みを使っている

  • 秘密鍵はユーザーのデバイス側に保存され、その利用に関しては生体認証やPINコードを用いる形で保護されている

  • 許可したサービス毎に鍵ペアを作成し、公開鍵をサーバに保存する

  • 認証においては、サーバー側から対象となるチャレンジコードを送信してもらい、クライアント側で受信する

  • ユーザーは生体認証やPINコードを用いて秘密鍵を利用し、チャレンジコードに署名を行って送信する

  • サーバー側は保存している公開鍵で署名を検証し、成功すれば認証完了となる

これらを細かく書こうとすると以下のようになるが、複雑なのであまり考えなくていいです。 ポイントは秘密鍵をユーザーのデバイスに保存することで、サーバー側ではパスワードをそもそも管理していません(鍵情報のみ)。『デバイスのセキュリティが侵害されなければ』という条件は付きますが、パスワード漏洩・偽装認証のリスクが大幅に減ることになります。

  1. 登録 (Create Credential)

    • サーバ(RP)が challenge と RP ID をクライアントへ送る

    • クライアントプラットフォームが認証器に鍵ペア生成を要求

    • 認証器はユーザ存在 (生体/デバイス解除) を確認し、秘密鍵を安全領域へ保存し公開鍵+credentialId+attestation を返す

    • サーバは attestation / 署名を検証し、公開鍵を保存

  2. 認証 (Get Assertion)

    • サーバが毎回新しい challenge を返す

    • 認証器がユーザを再確認し、(credentialId, authenticator data, signature, カウンタ) を生成

    • サーバは保存済み公開鍵で署名とカウンタ(リプレイ/クローン検出用)を検証し成功ならセッション確立

なおRPとは「Relying Party」の略で、ユーザの認証を行うサービス提供者を指します。

登録時(パスキーがまだ存在しない場合)のシーケンスは、大雑把には以下のようになっています。

sequenceDiagram autonumber participant User participant Client as ブラウザ/OS participant Auth as 認証器 participant RP as サーバ(RP) %% 登録フェーズ User->>Client: サインアップ Client->>RP: create開始 RP-->>Client: challenge + RP ID Client->>Auth: makeCredential() Auth-->>Auth: 生体/PIN検証 Auth-->>Client: 公開鍵 + credId + attestation Client->>RP: 登録結果 RP-->>RP: 検証 & 保存 RP-->>Client: 登録OK

補足: ここで challenge は毎回新鮮な乱数であり再利用攻撃を防ぎます。 authenticatorData には RP ID ハッシュ、ユーザー存在・検証フラグ、署名カウンタなどが含まれ、署名対象は authenticatorDataclientDataHash(内部に challenge / origin / type を内包)を連結したものです。サーバは origin / RP ID 整合性と署名、さらにカウンタの単調増加を検証し、増加停止や巻き戻りがあればクローンや不正抽出の兆候として扱います。

すでにパスキーが存在しているときの認証シーケンスは以下のようになります。 なお、実際には認証をするサーバー側にはパスキーがあるけど、ユーザーのデバイス側にはパスキーが無い(新しいデバイスを使う等)場合もあります。 この場合にQRコードを画面に提示して、すでにパスキーが存在しているデバイス(主にスマートフォン)で読み取って認証を行う場合もあります。

補足:

  • challenge: 毎回ランダム。再利用(リプレイ)防止

  • authenticatorData: RP ID ハッシュ, フラグ(ユーザ存在/検証), 署名カウンタ等

  • 署名対象: authenticatorData + clientDataHash(含: challenge, origin, type)

  • origin / RP ID 検証によりフィッシングサイトでは正しい署名が成立しない

  • 署名カウンタ増加停止 = クローン/不正抽出疑い

sequenceDiagram autonumber participant User participant Client as ブラウザ/OS participant Auth as 認証器 participant RP as サーバ(RP) %% 認証フェーズ User->>Client: サインイン Client->>RP: assertion開始 RP-->>Client: challenge + allowCredentials Client->>Auth: getAssertion() Auth-->>Auth: 生体/PIN検証 Auth-->>Client: credentialId + signature + authData Client->>RP: 認証結果 RP-->>RP: 署名検証 & カウンタ確認 RP-->>Client: 認証OK & セッション確立 Client-->>User: ログイン完了