8
6

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-07-14

とあるイベントで青い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

参考

8
6
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
6