リーダーレス WebAuthn 完成 — スマホNFCがマイナを認証器にする最後の1マイル

前回の記事で、自作の 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 で端から端まで繋がりました。次は企業の機密文書ゲートのような具体ユースケースに、このリーダーレス経路を載せていきます。

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

IP: 取得中...
216.73.216.103216.73.216.103