前回の記事で、自作の Android アプリがマイナンバーカードを NFC で読んで署名できるところまで確認しました。残った宿題は「次は、このスマホ署名からサイト別鍵の導出と PC ブラウザへのリレーを実装して、リーダーレスでの WebAuthn 認証を通す」こと。今回それが実際の WebAuthn サイト(webauthn.io)で、USB リーダーを一切使わずに通りました。「You’re logged in!」まで到達した、シリーズの集大成です。
何が「最後の1マイル」だったか
これまでに部品は揃っていました。Mac の Safari 拡張は navigator.credentials を乗っ取って WebAuthn の応答(COSE 鍵・authenticatorData・署名)を組み立てられる。Android アプリはマイナを NFC で読んで署名できる。両者で同じ鍵が導出されることも、バイト単位で確認済みでした(rpId と userId からカードの RSA 署名を種にして Ed25519 / P-256 を導出)。
問題は「Mac のブラウザ」と「手元のスマホ」をどう繋ぐか。標準には、まさにこの用途——スマホを認証器として PC のログインに使う——のための仕組み caBLE / FIDO ハイブリッドがあります。が、これは third-party に開かれておらず、自分で実装した認証器をここに載せることはできません。そこで、土管を自分で作りました。
スマホを「ネットワーク越しの native host」にする
発想はシンプルです。Safari 拡張は本来、署名要求をローカルのネイティブアプリ(native messaging host)に渡します。この渡す先を、ローカルではなく LAN 上のスマホに付け替えるだけ。間に最小の HTTP リレー(Rust / axum、long-poll)を1枚挟みます。
[ webauthn.io (RP) ]
↕ navigator.credentials override(Safari 拡張)
[ Mac 拡張 background.js ] ← USB リーダー無し!
↕ HTTP リレー(自作 long-poll / 同一 Wi-Fi)
[ Android アプリ(NFC) ]
↕ ISO7816 APDU
[ マイナンバーカード ] ← RSA 署名 → 種 → サイト別鍵を導出 → 署名
肝は、スマホがやり取りするメッセージの形を、ローカルの native host とまったく同じにしたこと。{mode, alg, rpId, userId, message} を受け取り {result, publicKey, credentialId, signature} を返す——この約束さえ守れば、拡張側の WebAuthn 応答組み立てコードは1行も変えずにそのまま使えます。スマホは「LAN の向こうにいる native host」になりきるわけです。
リレー自体はインメモリの 2 スロット(リクエスト用 / レスポンス用)だけ。拡張がリクエストを置く → スマホが long-poll で取り出してカードに署名させる → 結果を置く → 拡張が取り出す。PIN はスマホ側で入力するので、拡張側の PIN ポップアップはリレーモードでは省略します。
結果:リーダーレスで登録も認証も通った
webauthn.io で Register → スマホにカードをかざす → 登録成功。続けて Authenticate → もう一度かざす → 画面にこう出ました。
You’re logged in!
You just logged in using Web Authentication … you used a piece of secure hardware to create a strong, attested, and scoped credential that is virtually unphishable!
RP(webauthn.io)から見れば、これはごく普通の WebAuthn 認証器による「フィッシング困難な credential」です。その実体が、専用リーダーもパスキー対応端末も使わず、手元のスマホでマイナをかざしただけ——という所がこのシリーズのゴールでした。読み取りハード問題は、これで解けたと言えます。
プライバシー設計はそのまま維持
リーダーレスにしても、これまでの設計上の性質は崩していません。
- サイトごとに別の鍵(rpId + userId を署名 → 種 → 鍵導出)。サイト間で名寄せできない。
- JPKI 証明書もマイナンバーも漏れない。カードは「種の供給源」としてしか使っていない。
- RP に渡る credential の AAGUID は nil(全ゼロ)。これが「Touch ID ではなく、我々の認証器が応答した」証拠になります。
実装でハマった所(運用メモ)
- 「Authenticator is busy」:拡張は同時実行を1件に絞る排他ロックを持っています。連打したりサイトが再試行すると2件目が弾かれてこのエラーに。1クリック=1タップを守るのが正解。
- 「Transceive failed」:署名中にカードがずれると NFC が切れます。かざしたら平らに 2〜3 秒静止。
- 状態が詰まったら、拡張をオフ→オン + リレー/アプリ再起動でクリーンに。
正直な限界と、次へ
まず断っておくと、今回の Wi-Fi リレーはあくまで「疎通を通すためのプロトタイプ」です。平文・同一 Wi-Fi 前提というだけでなく、決定的に欠けているのが近接性の証明(proximity proof)。同じ LAN にいれば誰でもスマホ役になれてしまい、「カードを持った本人が、今この PC の目の前にいる」を何も保証していません。配管が通ることを示しただけ、という位置づけです。
そこで本命は BLE トランスポートです。標準の FIDO ハイブリッド(caBLE)が BLE を使う理由がまさにここで、BLE は「電波が届く=数 m 以内にいる」という物理的な近接そのものをフィッシング(遠隔リレー)対策にする役割を担います。ただし標準 caBLE は third-party の認証器には開かれていないため、我々はスマホ ↔ Mac を直接 BLE GATT で繋ぐ自前トランスポートを作る方針です。これは標準より我々にとってむしろ素直で、BLE の到達距離そのものが近接証明になり、トンネルサーバも LAN 依存も不要になります(どの経路でもペアリング鍵による E2E 暗号は必須)。
また以前書いたとおり、鍵の導出・署名はソフト側で行うため耐タンパー性は専用キー(YubiKey 等)に及びません。位置づけは「金庫」ではなく「全員が持つカードで、所持 + PIN によるフィッシング耐性を低コストで足す層」。重要操作は OIDC(デジタル認証アプリ)で本人性を再確認する多層で補う設計です。
とはいえ、「専用ハード無しで、全員が既に持っているカードを、フィッシング困難な WebAuthn 認証器にする」という当初の絵が、実在の RP で端から端まで繋がりました。次は企業の機密文書ゲートのような具体ユースケースに、このリーダーレス経路を載せていきます。

コメントを残す