とあるイベントで青いYubikeyをもらったので、FIDO2.0の認証をやってみた。
FIDO2.0はWebAuthn(サーバー側のWebAPI仕様)とCTAP(クライアントのJavaScriptAPI仕様)があるかと思いますが、ここでは主にCTAP側の説明となっているかと思います、基本的な認識まちがっているかもしれないですが。- 訂正:FIDO2.0はWebAuthn(ブラウザから利用するJavaScriptAPI仕様)とCTAP(認証デバイスとのIF仕様)があるかと思いますが、ここでは主にJavaScriptを使った話となっています。
- WindowsPC1台と青いYubikeyでやってます。
- Javascript使っていますが初心者です。
環境
- OS=Windows10(1803)
- 青いYubikey=Security Key by Yubico
- WebServer=IIS 10
- ブラウザ=Google Chrome(67.0.3396.99)、またはMicrosoft Edge
- 2019/6/1時点はChrome=CTAP1,Edge=CTAP2の実装となっており、動作が若干異なる(Edgeの方が新しいFIDOを実装している)
- 言語=JavaScript
- ライブラリ=cbor-js
青いYubikeyとは
製品ページを見ると
- U2FとFIDO2に対応
- google,FaceBookあたりのログインに使える
と、ほかのYubikeyと比べると機能が少なく、FIDOのAuthenticator専用のようだ。
とりあえずやってみる
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>サンプル1</title>
<!--外部ファイルに記述-->
<script type="text/javascript" src="script.js"></script>
<style type="text/css">
body{font-size:16px}
</style>
</head>
<body>
<h1>青いユビキーテスト(思考停止コピペ)</h1>
<input type="button" value="PublicKeyCredentialテスト" onclick="test1();"/>
<input type="button" value="getテスト" onclick="test2();"/>
</body>
</html>
function test1(){
if (PublicKeyCredential)
{
alert("PublicKeyCredential-対応!");
}else{
alert("PublicKeyCredential-未対応です");
}
}
function test2(){
var options = {
rp: {
name: "localhost",
},
user: {
name: "gebo",
displayName: "gebo",
},
challenge: new Uint8Array([0, 0, 0, 0]),
allowCredentials: [
{
type: "public-key",
id: new Uint8Array([0,0,0,0]),
}
],
}
navigator.credentials.get({ "publicKey": options })
.then(function (assertion) {
alert("navigator.credentials.get(assertion)-OK");
}).catch(function (err) {
let msg = "エラー\n";
msg = msg + err;
alert(msg);
});
}
全然動かない!
- index.htmlをダルクリックして
file:///C:/work/yubikey/index.html
で実行- test1()は
PublicKeyCredential-対応!
と表示されるのでOK - test2()はエラー
NotAllowedError: Public-key credentials are only available to secure HTTP or HTTPS origins. See https://crbug.com/824383
となる - HTTPだったらいいのか?ということで↓
- test1()は
-
簡易Webサーバー立てて
http://localhost/Fido2YubikeyTest/index.html
ってURLで実行- 2019/6/1時点:これで動作します。以降で書かれているHTTPSは不要です。
test1()、テスト2()とも突然not definedエラーとなり、まったくダメもしかしてhttpsでないといけない?
2019/6/1時点:ローカル環境でHTTPSを構築する方法はここをみてください
というわけで、ローカル環境でHTTPSを構築する
http://localhost/Fido2YubikeyTest/index.html
で動きます。HTTPSは不要です。
ちゃんとWebServer立てないとダメ、サッサと済ませたいのでWindows10に最初から入っているIISでやる。
これで https://localhost/Fido2YubikeyTest/index.html
というURLでもって実行するとtest1()、test2()共に、Yukikeyがピカピカ光るようになる(喜)。しかし、そのあとエラー(泣)
さらに試行錯誤した結果わかったこと
- いきなりnavigator.credentials.get()しても通るわけない
- navigator.credentials.create()を最初にやって、credential idを取得しないといけない(Yubikey内に何かを記録しているのか?)
- navigator.credentials.create()して得られたID(credential id)を指定してget()しないといけないぽい
つまり以下の手順
◆create()
- navigator.credentials.create()をCallする→OSに制御が移る
- Yubikeyが光る
- Yubikeyをタッチする
- create()から戻ってthen()に入る、attestationというJSONobjectを持って入ってくる
- attestationの中からcredential idを取り出し、保管しておく
◆get()
- navigator.credentials.get()をCallする、このときcreate()で取得したcredential idを渡す→OSに制御が移る ※Yubikeyは自分が発行したcredential idかどうかわかるようで、credential idに適当な値を設定してもエラーになる。
- Yubikeyが光る
- Yubikeyをタッチする
- get()から戻ってthen()に入る、assertionというJSONobjectを持って入ってくる
- assertionの中に電子署名などが入っていて、それらを検証することで認証を確認することができる
ちゃんとやる
create()
- 要は navigator.credentials.create() でYubikeyを光らせて、結果(credential)を取得、結果の中からcredentialIdを取り出せばよい。
- credentialIdは credential.response.attestationObject.authData の中に入っている。
- ただし、CBORとかいう形式でエンコードされているのでライブラリを使ってデコードする→CBOR.decode(attestationObject);
- CBORのデコードはcbor-js参照
- 結果(credential)の細かいフォーマットはネット調べまくり→最後の参考サイトを参照
function createYubikey(){
var options = {
rp: {
id: location.host,
name: location.host,
},
user: {
id: new Uint8Array([129, 230, 232]),
name: "gebo",
displayName: "gebo",
},
challenge: window.crypto.getRandomValues(new Uint8Array(32)),
pubKeyCredParams: [
{
type: "public-key",
alg: -7, // cose_alg_ECDSA_w_SHA256,
},
],
}
console.log(options);
// このあと、Yubikeyがピカピカ光るのでタッチするとthenかcatchに入る
navigator.credentials.create({ "publicKey": options })
.then(function (attestation) {
//alert("navigator.credentials.create(attestation)-OK");
//このサンプルでほしいのはcredentialIdだけ
const {id, rawId, response, type} = attestation; // type = "public-key"
const {attestationObject, clientDataJSON} = response;
// <attestationObject>
// attestationObjectをCBORパース
let attestationObject_json = CBOR.decode(attestationObject);
const {attStmt, authData, fmt} = attestationObject_json;
const rpidHash = authData.slice( 0, 32);
const flag = authData.slice(32, 33); //.readUInt8(0)
const counter = authData.slice(33, 37); //.readUInt32BE(0)
const aaguid = authData.slice(37, 53)
const tmp = authData.slice(53, 55); //.readUInt16BE(0)
// tmp は Uint8Array[2] これをビッグエンディアンのUint16にする
var credentialIdLength = (tmp[0] << 8) + tmp[1];
const credentialId = authData.slice(55, 55 + credentialIdLength);
let credentialId_base64 =Uint8ArraytoBase64(credentialId);
let msg = "credentialId\n";
msg = msg + credentialId_base64;
alert(msg);
}).catch(function (err) {
let msg = "エラー\n";
msg = msg + err;
alert(msg);
});
}
get()
- allowCredentials.idにcredentialIdを指定すればよいだけ。
- navigator.credentials.get()するとYubikeyが光るのでタッチするとassertionがGetできる。
- とってきたassertionの中身にいろいろ入っているが、もうめんどくさいのでまた今度→→最後の参考サイトみればわかるかも。
function getYubikey(){
let credentialId = create()で取得したものをセット、Base64;
var options = {
challenge: window.crypto.getRandomValues(new Uint8Array(32)),
allowCredentials: [
{
type: "public-key",
id: Base64toUint8Array(credentialId),
}
],
}
navigator.credentials.get({ "publicKey": options })
.then(function (assertion) {
alert("navigator.credentials.get(assertion)-OK");
}).catch(function (err) {
// No acceptable credential or user refused consent. Handle appropriately.
let msg = "エラー\n";
msg = msg + err;
alert(msg);
});
}
ソース
GitHub参照
https://github.com/gebogebogebo/Fido2YubikeyTest