JavaScript
yubikey
FIDO
CTAP
WebAuthn

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

とあるイベントで青いYubikeyをもらったので、FIDO2.0の認証をやってみた。

  • FIDO2.0はWebauthn(サーバー側のWebAPI仕様)とCTAP(クライアントのJavascriptAPI仕様)があるかと思いますが、ここでは主にCTAP側の説明となっているかと思います、基本的な認識まちがっているかもしれないですが。
  • WindowsPC1台と青いYubikeyでやってます。
  • Javascript使っていますが初心者です。

環境

  • OS=Windows10(1803)
  • 青いYubikey=Security Key by Yubico
  • WebServer=IIS 10
  • ブラウザ=Google Chrome(67.0.3396.99)
  • 言語=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://gebo-pc:8080/yubikey/index.html ってURLで実行
    • test1()、テスト2()とも突然not definedエラーとなり、まったくダメ
    • もしかしてhttpsでないといけない

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

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

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

これで https://gebo-pc:8080/yubikey/index.html というURLでもって実行するとtest1()、test2()共に、not definedはなくなり、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()に入る、credentialというJSONobjectを持って入ってくる
  5. credentialの中から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 (credential) {
        //alert("navigator.credentials.create(assertion)-OK");

        //このサンプルでほしいのはcredentialIdだけ
        const {id, rawId, response, type} = credential; // 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

参考