はじめに
FIDO2認証は秘密鍵と公開鍵の鍵ペアを使用して認証を行います。
身近な鍵ペアといえばマイナンバーカードです。
マイナンバーカードは秘密鍵がカード内で保護されている上に、署名機能まで持っています。
それならマイナンバーカードでFIDO2認証ができるのか?
ということで実際にやってみました。
マイナンバーカードには鍵ペアが利用者証明用と署名用の2つしかなく、RPごとに異なる鍵ペアを生成できないので、鍵ペアを使いまわすことになってしまいます。そのため、FIDO2の仕様に準拠しません。
あくまで、マイナンバーカードでFIDO2っぽい認証をして遊んでみたよという趣旨の記事です。
動いている様子
今回作ったマイナンバーカードを使用したFIDO2っぽい認証器(長いのでマイナキーとします)を使用してWebauthn.ioというFIDO2認証のデモサイトで登録と認証を行う様子を載せています。まずは、構成について見てください。
構成について
今回構築したものは下記の構成図のようになっています。OSSの仮想FIDO2認証器passlessを改造して、登録時や認証時にマイナンバーカード内にある鍵ペアを使用するようにしています。
このソフトウェアは仮想的なHIDデバイスとして振る舞うので、OSから見ると物理的な認証器が刺さっているように見えます。
ブラウザと認証器がやりとりするためのCTAPプロトコルはpasslessに喋っていただき、鍵の生成・保存・署名についての部分を改造して、マイナンバーカードの鍵ペアを使用するようにします。

物理的にはこんなかんじです。
登録の様子
登録時のGIF画像です。
webAuthn.ioというFIDO2認証のデモ用サイトにマイナンバーカードの利用者証明用公開鍵を送ってユーザー登録を行っています。
ブラウザがセキュリティーキーとしてマイナキーを認識していて、マイナキーからRPへ送られた情報でユーザー登録が成功しています。
認証の様子
認証時のGIF画像です。
マイナンバーカードの利用者証明用秘密鍵で作成した署名をRPが検証することによってユーザー認証に成功しています。

マイナキーは何をしているのか?
マイナキーの動作について、FIDO2認証における認証器の役目にフォーカスして登録時と認証時の動作について確認しながら、「マイナキーではこうしている」という対比の形式でお伝えできればと思います。
登録時
FIDO2認証を行うためには、事前に認証器で生成した公開鍵をRPに登録する必要があります。
登録時のフローを簡略化して日本語で説明したものです。

ブラウザと認証器の間にフォーカスしてより詳しく見ていきます。
4. ブラウザが鍵の作成を依頼する
ブラウザが認証器に鍵の作成を依頼する際は、下記フォーマットのClientDataJSONを生成します。
# ClientDataJSON
{
"type": "webauthn.create", // 操作種別(登録)
"challenge": "Y2hhbGxlbmdl...", // RPから受け取ったchallenge
"origin": "https://example.com", // RPのorigin
"crossOrigin": false // iframeからの呼び出しか
}
それに加えて、RP名・ユーザー名・RPが受け入れ可能な鍵アルゴリズム・鍵の保存方式・ユーザー確認するか、といった情報を認証器に渡します。
# ブラウザから認証器に送る情報を簡略化したもの
{
"clientDataHash": "<clientDataJSONのSHA-256ハッシュ>",
"rp": "example.com", //ドメイン名
"user": "todoroki", //ユーザー名
"pubKeyCredParams": [ //RPが受け入れ可能な鍵アルゴリズム
{ "type": "public-key", "alg": -7 }, // -7はES256
{ "type": "public-key", "alg": -257 } // -257はRS256
],
"options": {
"rk": true, // 認証時にユーザー名の入力なしでログインできる形式
"uv": true // 認証器が行うユーザー認証を必須とする
}
}
後の工程で、認証器がclientDataHash + 認証器情報に対して秘密鍵で署名をします。
仮に、ブラウザがフィッシングサイト(https://evil.com)から認証情報の作成を依頼された場合にはoriginにhttps://evil.comが入るため、example.comでの認証情報の検証に失敗するようになっています。
5. 認証器がユーザーの存在確認をする
認証器がユーザーの存在確認をします。Yubikeyで鍵にタッチする動作や、WindowsデバイスでFIDO2認証を行う際のWindows Helloがこれに該当します。
6, 7. 鍵ペア作成と保存
ブラウザからRPが受け入れ可能な公開鍵アルゴリズムのリストが送られてくるので、認証器側で使用可能なアルゴリズムで鍵ペアを作成し、credentialIDごとに鍵ペアを保存します。
マイナンバーカードには鍵ペアを保存できないので、今回作るマイナキーは鍵ペア作成を行わず、マイナンバーカードに入っている鍵ペアを使います。
鍵を認証器内に保存せず、必要になるたびにマイナンバーカードからとってきます。
さらに、本来はドメインやユーザーごとに変えるべきcredentialIDもマイナキーでは固定値jpki-mynumber-card-credentialとしています。
マイナンバーカードから利用者証明用電子証明書を取り出し、その中から公開鍵を取り出しているログです。
[2026-02-18T15:50:52.762Z INFO] JPKI: Registration for RP: webauthn.io
[2026-02-18T15:50:52.762Z DEBUG] JPKI: clientDataHash: 6dc0f99aca42f759b6a6de4f2632f07596dcf8590ad8ad01378ad925029c63c1
[2026-02-18T15:50:52.762Z INFO] ╔══════════════════════════════════════════════════════════════════╗
[2026-02-18T15:50:52.762Z INFO] ║ マイナンバーカードから公開鍵を読み込み中... ║
[2026-02-18T15:50:52.762Z INFO] ╚══════════════════════════════════════════════════════════════════╝
[2026-02-18T15:50:56.084Z INFO] ╔══════════════════════════════════════════════════════════════════╗
[2026-02-18T15:50:56.085Z INFO] ║ マイナンバーカード証明書情報 (JPKI) ║
[2026-02-18T15:50:56.085Z INFO] ╠══════════════════════════════════════════════════════════════════╣
[2026-02-18T15:50:56.085Z INFO] ║ シリアル番号: xx:xx:xx:xx
[2026-02-18T15:50:56.085Z INFO] ║ 発行者 (Issuer): (C=JP, O=JPKI, OU=JPKI for user authentication, OU=Japan Agency for Local Authority Information Systems)
[2026-02-18T15:50:56.085Z INFO] ║ 主体者 (Subject): xxxxxxxx (C=JP, CN=xxxxxxxx)
[2026-02-18T15:50:56.085Z INFO] ║ 有効期間開始: Fri, 01 Mar 2024 07:21:08 +0000
[2026-02-18T15:50:56.085Z INFO] ║ 有効期間終了: Wed, 02 May 2029 14:59:59 +0000
[2026-02-18T15:50:56.085Z INFO] ║ 署名アルゴリズム: 1.2.840.113549.1.1.11
[2026-02-18T15:50:56.085Z INFO] ║ 公開鍵アルゴリズム: 1.2.840.113549.1.1.1
[2026-02-18T15:50:56.085Z INFO] ║ 鍵長: 2056 bits
[2026-02-18T15:50:56.085Z INFO] ╠══════════════════════════════════════════════════════════════════╣
[2026-02-18T15:50:56.085Z INFO] ║ 証明書フィンガープリント (SHA-256):
[2026-02-18T15:50:56.085Z INFO] ║ ED:16:5D:A4:C5:74:9D:4B:9A:E2:9A:8D:02:46:5F:3A:AE:EE:59:60:54:C4:6A:DD:E1:14:C4:6B:49:49:BD:7E
[2026-02-18T15:50:56.085Z INFO] ╠══════════════════════════════════════════════════════════════════╣
[2026-02-18T15:50:56.085Z INFO] ║ RSA公開鍵:
[2026-02-18T15:50:56.085Z INFO] ║ n (modulus): 256 bytes (2048 bits)
[2026-02-18T15:50:56.085Z INFO] ║ e (exponent): 3 bytes = 65537
[2026-02-18T15:50:56.085Z INFO] ║ n (先頭16バイト): 9b57bfa0f8a334debb7a37bbd3f61965
[2026-02-18T15:50:56.085Z INFO] ╚══════════════════════════════════════════════════════════════════╝
[2026-02-18T15:50:56.085Z DEBUG] JPKI: RSA public key n (256 bytes): 9b57bfa0f8a334debb7a37bbd3f61965610d0fce871f485eb66848c334eba49e...
8. ブラウザに返す情報を作成
認証器からブラウザに返すattestaionObjectを作成します。attestationObjectは次の3つのフィールドで構成されています。
attestationObject = {
fmt: text, // Attestation Statement Format識別子
authData: bytes, // 認証器データ
attStmt: { ... } // Attestation Statement。署名はここに入っている。
}
8.1 fmt
認証器はRPに対して自らの出自を証明するためにattStmtを作成しますが、そのフォーマットを指定します。
代表的なものとして下記のものがあります。
| 形式 | 内容 |
|---|---|
Basic attestation |
ベンダーのCAが発行したattestation証明書で署名(x5cに証明書チェーンあり) |
Self attestation |
クレデンシャル秘密鍵で署名(x5cなし、認証器の出自は証明できない) |
None |
attStmtが空(RPは出自を検証できない) |
今回作っているマイナキーではSelf attestationとしています。通常のFIDO2認証器では実装次第です。
今回作っているマイナキーのようにCTAPを喋ってはいるものの、FIDO2に準拠しない得体の知れない認証器もあるかもしれません。
RPとしては自己署名やattStmtなしのポリシーとすることもできます。
8.2 authData
authDataについては詳細に記述していると量が多すぎるので触れません。主要な情報として、rpID、credentialID、 公開鍵、認証器のAAGUIDが含まれています。
通常の認証器ではRPごとに異なるcredentialIDと公開鍵を送りますが、マイナキーでは全てのRPに同じcredentialIDと公開鍵を送ります。
AAGUIDは認証器側で好きな値を設定できるので、mynakeyとしています。
8.3 attStmt
attStmtは認証器の出自を証明するための情報で、fmtによって変化します。
代表的なフォーマットでは下記のようになっています。どのフォーマットでも、認証器の出自を証明するという目的は同じです。
StmtFormat = {
"alg": "署名アルゴリズム",
"sig": "署名",
"x5c": [ Cert1, Cert2, ... ] //証明書チェーン。自己署名の場合は空にする。
}
sigはSign(authData || clientDataHash)です。
認証器で生成したauthDataと、ブラウザが生成したclientDataJSON(origin、challenge等を含む)のハッシュを連結して署名しています。
ここまで作ってきた情報を合体させたattestationObjectブラウザ経由でRPへ送ります。
RPが検証を行い、問題がなければ公開鍵が登録されます。
実際に、マイナキーがAttestaionを作成した際のログは下記のようになっています。
マイナキーは"fmt":"Self attestation"と設定しているので、署名を付与しています。
[2026-02-18T15:50:56.085Z DEBUG] JPKI: Credential ID: jpki-mynumber-card-credential
[2026-02-18T15:50:56.085Z DEBUG] JPKI: AAGUID: 6d796e61-6b65-7900-0000-000000000001
[2026-02-18T15:50:56.085Z INFO] ╔══════════════════════════════════════════════════════════════════╗
[2026-02-18T15:50:56.085Z INFO] ║ マイナンバーカードで署名を実行中... ║
[2026-02-18T15:50:56.085Z INFO] ╚══════════════════════════════════════════════════════════════════╝
[2026-02-18T15:51:00.495Z INFO] ╔══════════════════════════════════════════════════════════════════╗
[2026-02-18T15:51:00.496Z INFO] ║ Attestation情報 (登録応答) ║
[2026-02-18T15:51:00.496Z INFO] ╠══════════════════════════════════════════════════════════════════╣
[2026-02-18T15:51:00.496Z INFO] ║ RP ID: webauthn.io
[2026-02-18T15:51:00.496Z INFO] ║ フォーマット: packed (Self Attestation: クレデンシャル鍵で署名)
[2026-02-18T15:51:00.496Z INFO] ║ AAGUID: 6d796e61-6b65-7900-0000-000000000001
[2026-02-18T15:51:00.496Z INFO] ║ AAGUID (ASCII): "mynakey"
[2026-02-18T15:51:00.496Z INFO] ║ Credential ID: jpki-mynumber-card-credential
[2026-02-18T15:51:00.496Z INFO] ║ Credential ID (hex): 6a706b692d6d796e756d6265722d636172642d63726564656e7469616c
[2026-02-18T15:51:00.496Z INFO] ╠══════════════════════════════════════════════════════════════════╣
[2026-02-18T15:51:00.496Z INFO] ║ AuthenticatorData構造:
[2026-02-18T15:51:00.496Z INFO] ║ rpIdHash (32 bytes): 74a6ea9213c99c2f74b22492b320cf40...
[2026-02-18T15:51:00.496Z INFO] ║ flags: 0x45 (UP=true, UV=true, AT=true, ED=false)
[2026-02-18T15:51:00.496Z INFO] ║ signCount: 0
[2026-02-18T15:51:00.496Z INFO] ╠══════════════════════════════════════════════════════════════════╣
[2026-02-18T15:51:00.496Z INFO] ║ COSE公開鍵 (RSA):
[2026-02-18T15:51:00.496Z INFO] ║ kty: 3 (RSA)
[2026-02-18T15:51:00.496Z INFO] ║ alg: -257 (RS256)
[2026-02-18T15:51:00.496Z INFO] ║ n: 256 bytes (2048 bits)
[2026-02-18T15:51:00.496Z INFO] ║ e: 65537 (65537)
[2026-02-18T15:51:00.496Z INFO] ╠══════════════════════════════════════════════════════════════════╣
[2026-02-18T15:51:00.496Z INFO] ║ 署名データ:
[2026-02-18T15:51:00.496Z INFO] ║ 署名対象: authData (356 bytes) || clientDataHash (32 bytes)
[2026-02-18T15:51:00.496Z INFO] ║ 署名対象の合計: 388 bytes
[2026-02-18T15:51:00.496Z INFO] ║ clientDataHash: 6dc0f99aca42f759b6a6de4f2632f075...
[2026-02-18T15:51:00.496Z INFO] ╠══════════════════════════════════════════════════════════════════╣
[2026-02-18T15:51:00.496Z INFO] ║ 署名値:
[2026-02-18T15:51:00.496Z INFO] ║ サイズ: 256 bytes (2048 bits)
[2026-02-18T15:51:00.496Z INFO] ║ 先頭16バイト: 6f59a7fdaca76dc0188be4f3b6382fbc...
[2026-02-18T15:51:00.496Z INFO] ║ 末尾16バイト: ...757fda59c428c6b81de9ff3b733b26a0
[2026-02-18T15:51:00.496Z INFO] ╚══════════════════════════════════════════════════════════════════╝
ここまででマイナキーは「credentialIDを固定にする」「同じ鍵を全ドメインに使いまわす」といったFIDO2仕様に準拠しないことをしてきました。
しかし、RPの立場から見ると認証器による署名が公開鍵で検証できるため、問題なく登録できます。
認証時
認証器はRPから送られてくるcredentialIDで認証器内に保存されている認証情報を検索します。そして、認証情報を作成し秘密鍵で署名を作成します。
マイナキーのログでもRPからallowListとしてcredentialID: jpki-mynumber-card-credentialが送られてきていることが確認できます。
[2026-02-18T15:54:46.750Z INFO] JPKI: Authentication for RP: webauthn.io
[2026-02-18T15:54:46.750Z DEBUG] JPKI: clientDataHash: 09a9aff105e2eafcaae6fc527748739e6a416efabb2825eeadfc0f5f2dd6d8df
[2026-02-18T15:54:46.751Z DEBUG] JPKI: allowList contains 1 credentials
[2026-02-18T15:54:46.751Z DEBUG] JPKI: allowList[0]: 6a706b692d6d796e756d6265722d636172642d63726564656e7469616c
(jpki-mynumber-card-credential)
署名対象は登録時と異なります。
具体的には、公開鍵、AAGUIDといった情報が含まれません。RPとしてはすでに登録時にこれらの情報を送ってもらっているためです。
マイナキーログからも、RPに送る情報にそれらが含まれていないことが確認できます。
[2026-02-18T15:54:46.751Z INFO] ╔══════════════════════════════════════════════════════════════════╗
[2026-02-18T15:54:46.751Z INFO] ║ マイナンバーカードで署名を実行中... ║
[2026-02-18T15:54:46.751Z INFO] ╚══════════════════════════════════════════════════════════════════╝
[2026-02-18T15:54:51.658Z INFO] ╔══════════════════════════════════════════════════════════════════╗
[2026-02-18T15:54:51.658Z INFO] ║ Assertion情報 (認証応答) ║
[2026-02-18T15:54:51.658Z INFO] ╠══════════════════════════════════════════════════════════════════╣
[2026-02-18T15:54:51.658Z INFO] ║ RP ID: webauthn.io
[2026-02-18T15:54:51.658Z INFO] ║ Credential ID: jpki-mynumber-card-credential
[2026-02-18T15:54:51.658Z INFO] ║ Credential ID (hex): 6a706b692d6d796e756d6265722d636172642d63726564656e7469616c
[2026-02-18T15:54:51.658Z INFO] ╠══════════════════════════════════════════════════════════════════╣
[2026-02-18T15:54:51.658Z INFO] ║ AuthenticatorData構造:
[2026-02-18T15:54:51.658Z INFO] ║ rpIdHash (32 bytes): 74a6ea9213c99c2f74b22492b320cf40...
[2026-02-18T15:54:51.658Z INFO] ║ flags: 0x05 (UP=true, UV=true, AT=false, ED=false)
[2026-02-18T15:54:51.658Z INFO] ║ signCount: 1
[2026-02-18T15:54:51.658Z INFO] ╠══════════════════════════════════════════════════════════════════╣
[2026-02-18T15:54:51.658Z INFO] ║ 署名データ:
[2026-02-18T15:54:51.658Z INFO] ║ 署名対象: authData (37 bytes) || clientDataHash (32 bytes)
[2026-02-18T15:54:51.658Z INFO] ║ 署名対象の合計: 69 bytes
[2026-02-18T15:54:51.658Z INFO] ║ clientDataHash: 09a9aff105e2eafcaae6fc527748739e...
[2026-02-18T15:54:51.658Z INFO] ╠══════════════════════════════════════════════════════════════════╣
[2026-02-18T15:54:51.658Z INFO] ║ 署名値:
[2026-02-18T15:54:51.658Z INFO] ║ サイズ: 256 bytes (2048 bits)
[2026-02-18T15:54:51.659Z INFO] ║ 先頭16バイト: 377e976b1f7ffd31cfcc7a1dbec952fe...
[2026-02-18T15:54:51.659Z INFO] ║ 末尾16バイト: ...009beb18a697f8a031a64eb1a80d9256
[2026-02-18T15:54:51.659Z INFO] ╚══════════════════════════════════════════════════════════════════╝
登録時と同様に、認証器による署名をRPに保存されている公開鍵で検証することによって認証成功となります。
通常のFIDO2認証器では、RPから送られてきたcredentialIDによって署名用の秘密鍵を決定しますが、
マイナキーではRPからどんなcredentialIDが送られてきても、利用者証明用秘密鍵で署名をします。
まとめ
マイナンバーカードの鍵ペアを使っても(一応)FIDO2認証できることがわかりました。
今回は仮想FIDO2認証器を改造して遊んでみましたが、できれば実物のFIDO2認証器をいじってみたいものです。
何かいいものはないかと調べていたら、マイコンボードにGoogleが出しているファームウェアを書き込むと実物のFIDO2認証器を作れるようです。
https://kthrtty.hatenadiary.org/entry/2020/06/08/001140#OpenSK
何かネタを思いついたら今度は実物の認証器でやってみたいです(実物だと何かと罠も多くて大変そうですが、、)
Qiitaは読みやすさ優先で詳細を削りがちなので、完全版を会社ブログで作成予定です。


