PRF ExtentionはFIDOクレデンシャルでE2E暗号化をするやつ
概要
WebAuthn Pseudo-random function(prf, 読み方: スードウランダムファンクション)extensionがSafari 18で実装され、はや3ヶ月が経過しました。
このPRF Extensionを用いることで、WebAuthn APIを通じてFIDOクレデンシャルに対応するキーマテリアルを取得することができます。このキーマテリアルからユーザデータをE2E暗号化(End-to-End Encryption: E2EE)するための鍵を導出することができます。
考えられるユースケース
主なユースケースとしてパスワードマネージャの機密情報のE2EEがあります。従来ではE2EE用の鍵を導出するためにマスターパスワードを利用していました。そのマスターパスワード入力をFIDO認証で代用することで、パスワードの入力の手間を削減できます。(フィッシング耐性を獲得するメリットも考えられますが、そのためにはフィッシング耐性のない鍵導出方法を排除する必要があります。このメリットを享受するには他の工夫も考慮する必要がありそうです。)
また、WebAuthnの仕組みに則っているため同期パスキーを利用することができます。これによりマスターパスワード忘れのリスクを低減できそうです。一方でパスキーには利用できなくなるユースケースもあるため、従来のパスワード忘れに備えたバックアップコードの発行やマスターパスワードでの鍵導出は引き続きサポートしていく必要があるかもしれません。
さらに、一般的に利用されているプラットフォーム認証器のパスキーはパスワードマネージャを通じて管理されることが多いです。そのため「パスワードマネージャのセキュリティを他のパスワードマネージャに依存させたくない」というニーズにはセキュリティキーを利用することになると思います。
実際のデプロイケースはbitwardenがわかりやすいです。E2EEとしては常識なのかもしれませんが、複数のE2EE用の鍵を用いて1つの暗号化オブジェクトを管理していく方法がとても興味深いです。
Log in with Passkeys | Bitwarden Help Center
仕組み
prf extentionを用いたE2EE実現イメージは以下を想像しています。
簡単に説明すると、navigator.credentials.get
で取得したキーマテリアルから任意の鍵を導出し、それを用いてE2EEを実施します。
WebAuthnのprf extentionを用いたE2EE実現イメージ
PRF Extensionでキーマテリアルを取得する
E2EEの鍵を導出するためには、キーマテリアルをnavigator.credentials.get
を用いて取得する必要があります。このとき、以下のようにextensions.prf.eval
でソルトを指定することで、レスポンスにキーマテリアルが含まれるようになります。利用するFIDOクレデンシャルと入力したソルトが同じなら、同じキーマテリアルが出力されます。そのため、E2EEの鍵導出に利用できるという仕組みです。
const options = {
publicKey: {
challenge: challenge,
userVerification: "discouraged", // or "required" // ※
extensions: {
prf: {
eval: {
first: salt, // ※
},
},
},
}
};
const credential = await navigator.credentials.get(options);
const extensionResults = credential.getClientExtensionResults();
if (extensionResults.prf?.results?.first) { // ※
const keyMaterial = new Uint8Array(extensionResults.prf.results.first);
}
※memo
- UVのtrue/falseによってprfの出力が変わる
- なので"discouraged" or "required"を推奨
- この仕様はCTAP2で記述されている
-
extensions.prf.eval
はfirst
とsecond
でsaltを指定可能- おそらくsaltの変更/ローテーションのため
-
eval
以外にevalByCredential
でクレデンシャルごとにsaltを個別に指定可能- おそらくクレデンシャルごとにsaltを管理するケースのため
-
extensionResults.prf?.results?.first
が存在しないことがある- ブラウザ/認証器/FIDOクレデンシャルがprfをサポートしていなかった場合など
- サポートの判定方法は後述
- ここで出力されたレスポンスにはキーマテリアルが含まれている
-
toJSON()
の結果にも含まれるのでそのままサーバに送らないように注意
-
PRF ExtensionでE2EEをする前のクレデンシャル作成
navigator.credentials.get
でprf extentionを利用するためにはブラウザ/認証器/FIDOクレデンシャルがprfをサポートしている必要があります。これはパスキーを生成したタイミングで確認することができます。実装としてはextensionsにprf
指定することで、返り値のClientExtensionResults内にサポートしているかの判定結果が含まれるようになります。
const options = {
publicKey: {
challenge: challenge,
rp: {
name: "Test Page"
},
user: {
id: userId,
name: "demo-user",
},
pubKeyCredParams: [
{ alg: -7, type: 'public-key' },
{ alg: -257, type: 'public-key' },
]
extensions: {
prf: { // ※「prf:{}」でも良い
eval: {
first: state.salt,
},
},
},
}
};
const credential = await navigator.credentials.create(options);
const extensionResults = credential.getClientExtensionResults();
if (extensionResults?.prf?.enabled) { // ※
console.log("このクレデンシャルはprfに対応しています")
} else {
console.log("このクレデンシャルはprfに対応していません")
}
※memo
- 正確にはこの方法はprf extentionに対応するクレデンシャル生成する手法
- ただしCTAP2ではcreateでprfを指定しなくてもprfが利用できるクレデンシャルの生成を推奨している
- よって結果的にここでのprfの指定は対応判定の結果がレスポンスに含めるかどうかになる
- ただしCTAP2の仕様では
SHOULD
表記なので確実ではない
- extentionsは
prf:{}
でも判定結果は確認ができる- WebAuthnの仕様としてはgetと同じくsaltを渡せるI/Fになっているが利用されていない
- おそらく将来的にパスキー作成時でもキーマテリアル取得するため
まとめ
- prf extentionでFIDOクレデンシャルに紐づくキーマテリアルを取得できる
- ここで取得されたキーマテリアルからE2EE用の暗号鍵を導出する
- 現状は
navigator.credentials.get
でのみキーマテリアルを取得可能 - 認証器が管理するFIDOクレデンシャルと入力したsaltから一意のキーマテリアルが取得できる
参考資料
Discussion