青い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](http://amzn.asia/4sROLtu)
- WebServer＝IIS 10
- ブラウザ＝Google Chrome(67.0.3396.99)、またはMicrosoft Edge
	- 2019/6/1時点はChrome＝CTAP1,Edge＝CTAP2の実装となっており、動作が若干異なる（Edgeの方が新しいFIDOを実装している）
- 言語＝JavaScript
- ライブラリ＝[cbor-js](https://github.com/paroga/cbor-js)

#青いYubikeyとは
[製品ページ](http://yubikey.yubion.com/yubikey_lineup.html)を見ると

- U2FとFIDO2に対応
- google,FaceBookあたりのログインに使える

と、ほかのYubikeyと比べると機能が少なく、FIDOのAuthenticator専用のようだ。

#とりあえずやってみる

```html:index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>サンプル１</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>
```
```javascript: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サーバー](https://www.vector.co.jp/soft/dl/winnt/net/se381431.html)立てて `http://localhost/Fido2YubikeyTest/index.html` ってURLで実行
	- 2019/6/1時点：これで動作します。以降で書かれているHTTPSは不要です。
	- ~~test1()、テスト2()とも突然not definedエラーとなり、まったくダメ~~
	- ~~もしかして**httpsでないといけない**？~~

<details><summary>ローカル環境でHTTPSを構築する方法はここをみてください</summary><div>
#というわけで、ローカル環境でHTTPSを構築する
2019/6/1時点：`http://localhost/Fido2YubikeyTest/index.html`で動きます。HTTPSは不要です。
ちゃんとWebServer立てないとダメ、サッサと済ませたいのでWindows10に最初から入っているIISでやる。

1. IISを有効化する
1. IISマネージャ→gebo-pcのホーム→サーバー証明書→自己署名入り証明書の作成
1. 証明書のフレンドリ名＝適当、新しい証明書のストア＝個人、でOK→自己署名入り証明書ができる
2. WebSiteホーム→右バインド→追加→種類＝https、種類＝さっき作った自己署名入り証明書、その他はデフォルトのままOK
3. これでOK、IISで設定されているindex.htmlにHTTPSで接続できるようになる

</div></details>

###これで `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](https://github.com/paroga/cbor-js)参照
- 結果(credential)の細かいフォーマットはネット調べまくり→最後の参考サイトを参照

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

```javascript: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)参照**
**https://github.com/gebogebogebo/Fido2YubikeyTest**

#参考
- ★必見★ [Web Authentication API で FIDO U2F(YubiKey) 認証](https://blog.jxck.io/entries/2018-05-15/webauthentication-api.html)
- ★必見★ [W3C Web Authentication - #idcon vol.24](https://www.slideshare.net/matake/w3c-web-authentication-idcon-vol24)
- [WEBAUTHN DEMO](https://webauthn.org/)
- [WebAuthn.io](https://webauthn.io/)