Edited at

青いYubiKeyをFIDO2.0で光らせたい

とあるイベントで青い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専用のようだ。


とりあえずやってみる


index.html

<!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>



script.js

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だったらいいのか?ということで↓




  • 簡易Webサーバー立てて http://localhost/Fido2YubikeyTest/index.html ってURLで実行


    • 2019/6/1時点:これで動作します。以降で書かれているHTTPSは不要です。

    • test1()、テスト2()とも突然not definedエラーとなり、まったくダメ

    • もしかしてhttpsでないといけない



ローカル環境でHTTPSを構築する方法はここをみてください


というわけで、ローカル環境でHTTPSを構築する

2019/6/1時点:http://localhost/Fido2YubikeyTest/index.htmlで動きます。HTTPSは不要です。

ちゃんとWebServer立てないとダメ、サッサと済ませたいのでWindows10に最初から入っているIISでやる。


  1. IISを有効化する

  2. IISマネージャ→gebo-pcのホーム→サーバー証明書→自己署名入り証明書の作成

  3. 証明書のフレンドリ名=適当、新しい証明書のストア=個人、でOK→自己署名入り証明書ができる

  4. WebSiteホーム→右バインド→追加→種類=https、種類=さっき作った自己署名入り証明書、その他はデフォルトのままOK

  5. これでOK、IISで設定されているindex.htmlにHTTPSで接続できるようになる



これで 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()


  1. navigator.credentials.create()をCallする→OSに制御が移る

  2. Yubikeyが光る

  3. Yubikeyをタッチする

  4. create()から戻ってthen()に入る、attestationというJSONobjectを持って入ってくる

  5. attestationの中からcredential idを取り出し、保管しておく


◆get()


  1. navigator.credentials.get()をCallする、このときcreate()で取得したcredential idを渡す→OSに制御が移る ※Yubikeyは自分が発行したcredential idかどうかわかるようで、credential idに適当な値を設定してもエラーになる。

  2. Yubikeyが光る

  3. Yubikeyをタッチする

  4. get()から戻ってthen()に入る、assertionというJSONobjectを持って入ってくる

  5. assertionの中に電子署名などが入っていて、それらを検証することで認証を確認することができる


ちゃんとやる


create()


  • 要は navigator.credentials.create() でYubikeyを光らせて、結果(credential)を取得、結果の中からcredentialIdを取り出せばよい。

  • credentialIdは credential.response.attestationObject.authData の中に入っている。

  • ただし、CBORとかいう形式でエンコードされているのでライブラリを使ってデコードする→CBOR.decode(attestationObject);

  • CBORのデコードはcbor-js参照

  • 結果(credential)の細かいフォーマットはネット調べまくり→最後の参考サイトを参照


navigator.credentials.create()

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の中身にいろいろ入っているが、もうめんどくさいのでまた今度→→最後の参考サイトみればわかるかも。


navigator.credentials.get()

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


参考