パスキー(FIDO2/WebAuthn)は便利ですが「端末を無くしたら詰む」という不安がつきまといます。そこで「誰もが持っているマイナンバーカードをFIDO2認証器として登録しておけば、リカバリー手段になるのでは」という jpki/myna の試み(HAMANOさんによる Chrome 拡張)を、私は Safari に移植してみました。本記事はその実装記録です。Chrome では chrome.webAuthenticationProxy という専用APIで実現していますが、Safari には相当物が無く、いくつもの macOS 特有の壁を越える必要がありました。
何を作ったか
macOS Safari の拡張機能として、Webサイトの navigator.credentials.create() / .get() を横取りし、マイナンバーカードで WebAuthn の応答を生成する skeleton です。登録時にカードの利用者証明用鍵で Ed25519 鍵を導出し、その公開鍵を RP(サイト)に登録、認証時はその鍵で署名します。コードは公開しています。
- リポジトリ: Ryujiyasu/myna-fido-safari
- 元プロジェクト: jpki/myna(core の
mynaクレートは参照利用のみ・非改変)
壁①: Safari には webAuthenticationProxy が無い
Chrome 版はブラウザが WebAuthn 呼び出しを横取りしてくれる chrome.webAuthenticationProxy を使います。Safari にこのAPIは存在しません。代わりに、ページのメインワールド(MAIN world)に content script を注入して navigator.credentials 自体を上書きする方式を採りました。これは HAMANOさんが Safari 向けに提案していた方針でもあります。
// inject.js(MAIN world content script)
const origCreate = navigator.credentials.create.bind(navigator.credentials);
navigator.credentials.create = function (options) {
if (!options || !options.publicKey) return origCreate(options);
// 拡張のバックグラウンドへ委譲し、合成した PublicKeyCredential を返す
return callExtension("create", serialize(options.publicKey))
.then(buildCredential);
};
ポイントは、content script は通常「分離ワールド(ISOLATED)」で動くので navigator.credentials の上書きがページに効かないこと。Safari 17+ では manifest で "world": "MAIN" を指定でき、これでページ本体のオブジェクトを直接書き換えられます。WebAuthn の応答(CBOR/COSE/authenticatorData/attestationObject)を組み立てる処理は、Chrome 版の background.js をほぼそのまま流用できました。差分は実質「横取りの経路」だけです。
全体アーキテクチャ
ページ(RP) navigator.credentials.create/get │ inject.js (MAIN world) ← navigator.credentials を override │ window.postMessage content.js (ISOLATED world) ← 中継 │ browser.runtime.sendMessage background.js ← WebAuthn 応答(CBOR/COSE)を構築 │ browser.runtime.sendNativeMessage SafariWebExtensionHandler.swift (拡張アプリ) │ in-process FFI(子プロセス起動なし) libmyna_fido.a (Rust) ← カードで Ed25519 導出/署名 │ PC/SC(App Sandbox + smartcard entitlement) USBリーダー → マイナンバーカード
壁②: macOS には NFC が無い
カード読み取りをどうするか。iPhone の Core NFC を想像していましたが、Mac には NFC ハードウェアが無く、Core NFC は実質 iPhone 専用です。結論として macOS では USB の PC/SC リーダー(接触型、または PaSoRi 等の非接触リーダー)を使います。幸い macOS は PC/SC を標準搭載(PCSC.framework)しており、jpki/myna が使う Rust の pcsc クレートはそのまま arm64 macOS でビルド・動作しました。カードのバックエンドは Chrome 版と同じです。
壁③: 拡張は App Sandbox 必須・adhoc 署名では動かない
ここが一番ハマりました。Safari Web 拡張の本体(appex)は App Sandbox が必須で、これを無効化すると pluginkit に登録されず Safari の拡張一覧にすら出てきません。さらに adhoc 署名では登録されず、Apple Development 証明書(無料の個人 Apple ID でも発行可)での署名が必要でした。デバッグ中、テストRPで「登録成功」と出たのに、よく調べると macOS 内蔵の Touch ID パスキー(ES256/P-256)が応答していた、という偽陽性も踏みました。横取りが効いていなかったのです。
原因は拡張のサイトアクセス権限。Safari は拡張を有効化しただけでは content script を注入せず、「すべての Web サイトで許可」を与えて初めて override が効きました。
壁④: Sandbox 下ではカードに触れない → in-process FFI
Chrome 版はネイティブメッセージングで外部バイナリ(Rust製ホスト)を子プロセスとして起動します。しかし Sandbox 化された appex は別プロセス起動も外部パスへのアクセスも禁止されており、この方式は使えません。「myna-fido が存在しません」というエラーで気付きました。
解決策は、カード操作を拡張プロセス内(in-process)で直接行うこと。myna を C-ABI の静的ライブラリにし、Swift から FFI で呼び、Sandbox には com.apple.security.smartcard entitlement を付与して PC/SC を許可します。これで子プロセス不要・Sandbox 維持のままカードに到達できました。
// Rust 側(C-ABI 静的ライブラリ)
#[no_mangle]
pub unsafe extern "C" fn myna_fido_derive(
pin: *const c_char, rp_id: *const c_char,
user_id: *const u8, user_id_len: usize,
out_pubkey: *mut u8, out_cred_id: *mut u8,
err: *mut c_char, err_len: usize) -> i32 { /* ... */ }
// ビルド時に付与する設定(要点)
OTHER_LDFLAGS = -lmyna_fido_safari -framework PCSC
SWIFT_OBJC_BRIDGING_HEADER = native/MynaFidoFFI.h
CODE_SIGN_ENTITLEMENTS = native/MynaFido.entitlements // app-sandbox + smartcard
検証: 本物のマイナカードの Ed25519 か
「登録成功」を鵜呑みにせず、返ってきた attestationObject の COSE 公開鍵をデコードして確認しました。
- 登録:
kty=OKP / alg=-8 (EdDSA) / crv=Ed25519 / x のみ、aaguid=nil、fmt=none→ 紛れもなく Myna 由来の Ed25519(Touch ID の EC2/ES256/P-256 とは別物) - 認証: 返ってきた 64 バイトの生 Ed25519 署名を、登録時の公開鍵で検証 → 成功
鍵導出は Chrome 版と同一で、利用者証明用鍵で DER SEQUENCE{rpId, userId} を RSA 署名し、seed = SHA-256(署名) から Ed25519 を導出、credentialId = SHA-256(公開鍵) とします。PIN は鍵そのものには混ぜません(PIN 変更でも鍵が follow する設計)。CTAP2 の attestation を付けないことで、JPKI の証明書チェーンが漏れない設計になっているのも要点です。
まとめ
Safari でマイナンバーカードを WebAuthn 認証器にするには、(1) navigator.credentials override、(2) NFC 不在ゆえの PC/SC、(3) App Sandbox + 署名、(4) Sandbox 下のカードアクセスを in-process FFI + smartcard entitlement で解く、という4つの壁を越える必要がありました。WebAuthn 応答の組み立て自体は Chrome 版がそのまま流用でき、Safari 固有の難所は「横取り経路」と「Sandbox とカードの両立」に集約されます。
まだ skeleton 段階(既知の制約あり・production 非対応)ですが、実カードで登録・認証が一気通貫で動くところまで確認できました。コードは GitHub に置いてあります。元になった jpki/myna と HAMANO さんに感謝します。

コメントを残す