update 2018-06-25 userHandle に関して追記
2018-10-20 この記事より詳細な記事書きました webauthn における ResidentKey について
とりあえず動画をみれば大体わかる
Single Factor Login using Security Key by Yubico #yubikey #webauthnhttps://t.co/7rUjkoUmdK
— 82@FIDO2勉強会? (@watahani) 2018年6月17日
どうも、先日DMM英会話の退会を忘れていて、4万円ほど失った @watahani です。かなしい。
さて、先日 Insider Preview Build 17682
で WebAuthN のサポートがされたと聞いて、とりあえずは webauthn.org で使えることまでは試しました。
試してみた(url張り間違えた)https://t.co/c3SzRaqHsAhttps://t.co/UpTAmRJZ4l https://t.co/TLMYH8E5c2
— 82@FIDO2勉強会? (@watahani) 2018年6月15日
WebAuthN自体動くことは分かったけど、
Edge support resident key?
— 82@FIDO2勉強会? (@watahani) 2018年6月15日
Resident Key のサポートとかいろいろ気になるよね…?
ということで調べてみました。
Resident Key とは?
FIDO2.0 から新しく追加された機能で、要はキーの中にユーザ情報を保存できる。
細かく言うと、 PublicKeyCredentialUserEntity と Credential ID を保存しておいて、次回以降のログインに利用することができる。
Edge show Credentials in residentKey pic.twitter.com/SYNq6menui
— 82@FIDO2勉強会? (@watahani) 2018年6月17日
こんな感じ。
キーの中に Challenge へのサインに必要な情報をキャッシュしておくことで、次回以降のログインに User 情報を入力する必要なくログインができる。
今回は、 Security Key by Yubico を利用しました。
U2F と FIDO2 の違い
ゼロからコードを書くスキルは無いので、https://github.com/fido-alliance/webauthn-demo をベースに改造を加えてみる。
認証部分
2018-06-19 Attestation を認証部分と書いてしまっていたので修正
認証部分に関して、Yubico の Security Key は fido-u2f
ではなく packed
で動くので認証部分を追加する。
UV も検証したほうがいいかもだけど、ほかのブラウザがどういう方針で行くのか不明なのでとりあえずそのまま
}else if(authr.fmt === 'packed') { //別にこれ fido-u2f と分ける必要ない、というか attestation ない場合もあるので...
let authrDataStruct = parseGetAssertAuthData(authenticatorData);
if(!(authrDataStruct.flags & U2F_USER_PRESENTED)) //FIDO2 の場合は UP に加え、UV をチェックすべき(0x05)
throw new Error('User was NOT presented durring authentication!');
let clientDataHash = hash(base64url.toBuffer(webAuthnResponse.response.clientDataJSON))
let signatureBase = Buffer.concat(
[
authrDataStruct.rpIdHash,
authrDataStruct.flagsBuf,
authrDataStruct.counterBuf,
clientDataHash
]
);
let publicKey = ASN1toPEM(base64url.toBuffer(authr.publicKey));
let signature = base64url.toBuffer(webAuthnResponse.response.signature);
response.verified = verifySignature(signature, signatureBase, publicKey)
if(response.verified) {
if(response.counter <= authr.counter)
throw new Error('Authr counter did not increase!');
authr.counter = authrDataStruct.counter
}
}
Attestation Data の検証に関しては、Extension がない場合、
- RP ID Hash
- Flags
- Counter
- aaguid,
- Credential ID Length
- Credential ID
- Public Key(CBOR)
- Client Data Hash
に対して行う。あとは U2F と一緒っぽい。
+ (ctapMakeCredResp.fmt === 'packed'){
+ let authrDataStruct = parseMakeCredAuthData(ctapMakeCredResp.authData);
+
+ if(!(authrDataStruct.flags & U2F_USER_PRESENTED))
+ throw new Error('User was NOT presented durring authentication!');
+
+ let clientDataHash = hash(base64url.toBuffer(webAuthnResponse.response.clientDataJSON))
+ let reservedByte = Buffer.from([0x00]);
+ let publicKey = COSEECDHAtoPKCS(authrDataStruct.COSEPublicKey)
+ let signatureBase = Buffer.concat(
+ [
+ authrDataStruct.rpIdHash,
+ authrDataStruct.flagsBuf,
+ authrDataStruct.counterBuf,
+ authrDataStruct.aaguid,
+ authrDataStruct.credIDLenBuf,
+ authrDataStruct.credID,
+ authrDataStruct.COSEPublicKey,
+ clientDataHash
+ ]);
+
+ let PEMCertificate = ASN1toPEM(ctapMakeCredResp.attStmt.x5c[0]);
+ let signature = ctapMakeCredResp.attStmt.sig;
+
+ response.verified = verifySignature(signature, signatureBase, PEMCertificate)
+
+ if(response.verified) {
+ response.authrInfo = {
+ fmt: 'packed',
+ publicKey: base64url.encode(publicKey),
+ counter: authrDataStruct.counter,
+ credID: base64url.encode(authrDataStruct.credID)
+ }
+ }
}
WebAuthN 部分
WebAuthN 部分については、まず登録時に Resident Key が登録できるように authenticatorSelection
に requiredResidentKey
true
をセットするだけ。まあそんな難しくない。
getMakeCredentialsChallenge({username, name})
.then((response) => {
let publicKey = {
'challenge' : challenge,
'user': {
'id': 'xxxxxxxxxxxxxxxxxx',
'displayName': 'Hoge Fuga',
'user': 'hoge'
}
};
if(residentKey){
publicKey.authenticatorSelection = {
'requireResidentKey': true
}
}
return navigator.credentials.create({ publicKey })
})
...
ただ、このままだと無限にキーを登録し続けられてしまう。実際には aaguid などから、キーがすでに登録済みか調べて除外するなどする必要があると思う。
と思ったけど、 aaguid を
excludeCredentials
には追加できない ので、ユーザが重複登録してしまわないようにするにはサーバーに送ってから登録情報を見て登録済みならはじくとかかなあ…。
それだとキー自体に登録してしまってから送っちゃうしだめか…
次に認証時だけど、これも簡単で credentials.get オプションに渡す allowCredentials を空にすればいい。
let publicKey = {
'challenge': challenge
}
return navigator.credentials.get({publicKey});
当然 CredentialID 等はいらないのでこれでOK。
U2F では Challenge と ユーザを紐づけて管理していたが、FIDO2 の One Factor Authentication の場合、セッションなどと紐づけて保存しておかなくてはならなくなった。
また、サーバー側には CredentialID のみが送られるため、どのユーザがログインしようとしているかは CredentialID から逆引きする機構が必要になる。
2018-06-25
Edgeの実装を確認していると getAssertion のレスポンス内に含まれるuser handle
内に、user id
が含まれていた。
スペックでは nullable なので、使えたら使う?ってスタンスでよいのかなあ…。要調査。
+let getUsernameFromCredentialID = function(credentialId){
+ let matchedUsername;
+ Object.keys(database).forEach((username) => {
+ var authenticators = database[username].authenticators
+ authenticators.forEach((authenticator) => {
+ if(authenticator.credID === credentialId){
+ matchedUsername = username;
+ }
+ })
+ })
+ return matchedUsername;
+}
追記し忘れていたけれども CTAP2 のスペック に
FIDO devices - device resident credentials: For device resident keys on FIDO devices, at least user "id" is mandatory.
とあるので、Resident Key においては必ず UserHandle が帰ってくると考えてよいと思う
ということで上記のコードは不要なハズ
実際の動作
実際に Edge で動作確認をしてみる。
Resister
まずは、登録作業。
どうも Edge では User Verify (PINの入力) は必須らしく、PIN 設定していないキーでも関らず PIN の設定をしろと言われてしまう。
追記: User Verification はデフォルトで “preffered” です。
UV が不要であれば “discourage” オプションの指定が必要です。
詳しくは は webauthn における ResidentKey について
One Factor Authentication するには PIN なり、指紋なりで保護しないとだめなのはわかる。けど、認証レベルによってタップでログインして、重要な情報を変更するときのみ PIN の入力する…みたいな使い方は出来なさそう。
User Verification の後は User Presence のためにタッチする。
多分、生体認証などのキーでは User Verification は User Presence な動作を必要とするまず。つまりもう一度タッチしなくていいのかと思うけど、実機が無いのでわからない。誰か試してみてほしい。
ともあれ、タッチまでして登録完了。
Assertion
次にログインの場合。先ほども言った通り、Credential ID
等はキーに保存できるため allowCredentials
を空にして、getAssertion
を呼べばよい。
PIN を入力すると、キーが与えられた challenge
に対し、保存している Credentials すべてに対応する attestationResponse
を Edge に返す。
Edge は Credential が一つしかない場合、自動でログインするらしい。
正直どのユーザでログインしようとしているかは表示してほしい。
Credential が複数ある場合はユーザリストが表示される。
ちなみに 25 ユーザほど登録した時点で、キーの登録容量が足りなくなった。
FIDO2 の residentKey 試してたらもう入らんくなった pic.twitter.com/xNLZxnO2nc
— 82@FIDO2勉強会? (@watahani) 2018年6月17日
雑感
ソースはここに置いておいた。
https://github.com/HWataru/webauthn-demo/tree/fido2-support
キーの登録自体は動画の通りうまく動いたけども、実際の運用には重複登録への対策や、他のブラウザごとの実装などがどうなるかなど、検討しないといけないことは色々あると感じた。
正直キーだけで Web認証のすべてを行うのは難しいと思う。実際には PC や スマホの platform Authenticator を利用したり、何かしらの IdP にのみキーを登録して、フェデレーションなどを利用していく必要があるだろう。
何より DMM 英会話で失った 4万円が重くボディーブローのように響いてきてつらたん。
記事がためになったらだれかご飯おごってください。