tmytのらくがき

個人の日記レベルです

FIDO 2.0を実装してみた感じ

TL;DR

  • Passport.jsでFIDO 2.0実装してみた
  • EdgeでWindows Helloした
  • あとあとよく見たらpassport-mspassport*1ってのがありました

やりたかったこと

WebサービスWindows Hello認証をやってみたくていろいろ試してみた感じ。Windows HelloはFIDO 2.0の実装となっていて、公開鍵暗号をベースとしたチャレンジレスポンス認証。

あくまでもユーザとデバイスの組の確実性しか保証されないので、ログインしようとしているユーザがリソースにアクセスする権限があるかどうかについては関与できない。なので、実際に使うとなると既知のユーザアカウント認証に対して公開鍵を登録するという実装になるのかな。という感じ。 今回実装したものも、GitHubOAuth認証がすでに実装されているのでそこに対してFIDO 2.0の公開鍵を登録する仕組みにしました。

GitHub認証はPassport.jsで実装しているのでWindows Hello認証もそこに混ぜてみることにしました。

用意したもの

  • express.js
  • passport.js
  • passport-fido2*2
  • webauthn.js

実装

Passport.jsのStrategyを作るには、Passport-strategyを継承してauthenticate関数を実装すればいいらしい。 そんでもって、Windows HelloとかFIDO2.0とかでぐぐるとNodeな実装がちらほら見つかるのでいろいろ参考にしながら実装したものがこちら。

github.com

サーバが送信したChallengeであることを検証するためにChallengeをHMAC-SHA256したものをクライアントから送信させているのに、 検証に使っていない…のはそのうち修正するとして…

このStrategyは送信されたIDに対応するサーバにすでに格納されている公開鍵を使ってリクエストを検証するところまでしか面倒を見ません。 なので実際に使うには既知のアカウントでログインし、公開鍵を保存する必要があります。 その時のクライアント実装はこういう感じにしてます。

const p = this.context.profile;
const account = { rpDisplayName: p.username, displayName: p.username, imageUri: p.avater };
const cryptoParams = [{ type: 'FIDO_2_0', algorithm: 'RSASSA-PKCS1-v1_5' }];
navigator.authentication.makeCredential(account, cryptoParams)
.then(result => {
  const key = JSON.stringify(result.publicKey);
  const id = result.credential.id;
  Http.post('/api/credentials/fido/register', {key, id},
    () => Toast.show('FIDO key registered', 'success'),
    () => Toast.show('Key registration failed', 'warning')
  );
  console.log(result);
});

認証済みエリアに証明書登録ボタンをつくって、最初にこれで登録。そのあとログイン画面に移ると

sign(challenge){
  const q = qs.parse(this.context.router.location.search.substring(1));
  navigator.authentication.getAssertion(challenge.c)
  .then(result => {
    const uri = '/auth/fido2?'
      + `c=${challenge.c}&cs=${challenge.cs}&`
      + `authenticatorData=${result.authenticatorData}&`
      + `clientData=${result.clientData}&`
      + `signature=${result.signature}&`
      + `id=${result.credential.id}`
      + (q._redir ? `&_redir=${encodeURIComponent(q._redir)}` : '');
    window.location.href = uri;
  });
}
authenticate(){
  Http.get('/auth/fido2/challenge', {},
    c => this.sign(c),
    () => Toast.show('Failed to exchange challenge', 'error')
  );
}

ってやってます。最初にサーバからチャレンジをもらって、それに対して秘密鍵で署名。そのあと、passport-fido2を呼び出しています。

Edgeのfido2周りの実装が結構標準から離れてるらしく、Webauthn.jsを使ったほうがいいよってid:shibayan に教えてもらったりしたので使ったほうがよさそうです。 最新のドラフトと見比べてみてもRequiredな引数がRequiredじゃなかったりするし、もう少し変更ありそうな気もしたり。

まとめ

よくよく探したらpassport-mspassport ってのがありました。

github.com