2013年3月30日土曜日

C#でYubikeyのChallenge Response認証を行なうプログラム

はじめに

以前Yubikeyのチャレンジレスポンスのサンプルプログラムを書きますと書いておいて全く 書いていません。
私はやるといったら結構やらない男ですが、今までやらなかったのは理由があります。 使えるプログラムを考えていたら、機能がたくさんできてしまって、 作る気がなくなってしまったことです。これではいけませんので、 全く使えないサンプルを作ることにします。

チャレンジレスポンスの説明

とりあえずチャレンジレスポンスの説明をします。チャレンジレスポンスはいわば合言葉です。 合言葉といえば山賊とかが「山」といわれたら「谷」と言えとかそんなやつです。 合言葉を知っていれば信用してやろうとなるわけです。 しかし、これはやり取りを後ろで聞いていればすぐにバレてしまいます。 ということで、毎回違うやり取りをしようということになります。 で、このやり取りの初めにいう言葉をチャレンジ、返す言葉をレスポンスと言います。 チャレンジを送る側が今回作るプログラム、レスポンスを返す側がYubikeyとなります。
でも毎回違うやり取りを行うと返す言葉がなんであるか、 また、仮に帰ってきたとしてもそれが正しいのか分かりません。 というわけでルールを決めます。ルールを決めてやれば、どんなレスポンスを返すのか、 そのレスポンスが正しいのかがわかるようになります。
Yubikeyで使用出来るルールにはYubiko OTPというのと、 HMAC-SHA1というものがあります。Yubiko OTPについては調べていないので詳細はわかりませんが、 HMAC-SHA1については、データの送り主を確認するというとても重要な用途があるために インターネット上でもよく使われます。 C#でも簡単に処理できるようになっていますので、これを使用することにします。
また、HMAC-SHA1というルールがわかれば簡単に レスポンスがわかってしまうんではないかと思いますが、 HMAC-SHA1ではもう一つシークレットキーというのが必要になります。 このシークレットキーがわかっていないと、返す言葉がわからないという仕組みになっています。 なので、このシークレットキーをチャレンジを送る側のプログラム、 レスポンスを返す側のYubikey双方で設定しておく必要があります。

プログラムの説明

今回作成するプログラムですが、コマンドラインから実行させます。 実行させるとYubikeyが1つだけ接続されていることを確認します。 チャレンジを表示して、Yubikeyに送ります。Yubikeyから返ってきたレスポンスを表示し、 自らもレスポンス値を計算し、表示します。 そして、プログラムでレスポンスを確認するのがめんどくさかったので、 レスポンスを確認してくださいというメッセージを表示して終了します。
なお、プライベートキーがプログラム中になんと直接書き込まれています。 解析されれば簡単に破られますので、プライベートキーの置き場には考慮が必要です。 以前に書いたWindowsログオンモジュールのように次のレスポンスを記憶しておく方式の方が、 シークレットキーもいらないのでいいかと思います。 まあ、ここは動作をわかりやすく見るサンプルということで。

Yubikeyの設定

ということで、Yubikeyを設定しましょう。 と、その前にお使いのYubikeyがチャレンジレスポンスに対応しているか確認しましょう。 私は知りませんが、古くはチャレンジレスポンスはなかったと何処かに書いてありました。
YubiKey Personalization Toolを開いたら、画面上のChallenge-Responseを選び、 HMAC-SHA1を押します。
https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhEN4iBBvBsGNj3_ql9cTu8UGHSPbE6a4ZM8lkLHXS-gzqt5zFOMtH177qGX8yO5mlrHy148BvykhIOsBKIuZ2lGNSKeNZVnDJvrg_lnvijkqrSid3D4QZ45jnTar5UMz9srR003lhUi5vN/s1600/screenshot.20130330_144532.png
YubikeyにHMAC-SHA1の設定を書き込みます。 ここではSlot2に設定を書き込むことにします。 「Slot2」を選択し、下のSecret Keyの右の「Generate」ボタンを気が済むまで押します。 気がすんだら、画面下の「Write Configuration」を押します。
ここで忘れてはいけないのが、「Generate」ボタンを押した時に作成された「Secret key」 を何処かに書き留めておくことです。 これがわからないとチャレンジレスポンスしてもレスポンスが正しいか分かりません。 ということでシークレットキーをコピーしてメモ帳にでも貼り付けておきます。
https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEir8bpom2zfe7WEAPo6dvFVfNheEpmDFiFSjWljvinvajt3LCKYZQRDbQziF4heZNJiJm0uCYTaO1ZW3RsNmCPm3w7zrA9_HYNlQF2RC7MTr0ImbL18Ri4vUZPzC9H00qMJclM7-NGPF3JX/s1600/screenshot.20130330_143757.png

プログラムを作成する

プログラムを書く準備を行いましょう。まずYubikeyを扱うプログラムを作るためには、 クライアントAPIというものをインストールしなければなりません。32bit版、64bit版がありますので、 ライブラリーのページ から自分の環境にあったものをダウンロードし、インストールします。
ちなみに私はWindows 8 64bit版、Visual Studio 2010、.NetFramework 4 という環境で作成しました。以下その前提で進めさせて頂きます。
コマンドプロンプトのプロジェクトを作成します。 ここでは「YubikeyCRTest」という名前で作成しました。 プロジェクトを作成したら「C:\Program Files\Yubico\Yubikey Client API\Bin\x64\YubiClientAPI.dll」 を参照に追加します。 またプラットフォームターゲットをx64にしないと実行した時にエラーが出ます。 下記サンプルをProgram.csに書き込みます。 また、hexStringTo20Bytesの後ろにYubikeyを設定した時にメモしておいたプライベートキーをいれて下さい。 チェックはしていないので、変な文字をいれるのはやめてください。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
  using System;
  using System.Security.Cryptography;
  using YubiClientAPILib;

  namespace YubikeyCRTest
  {
      class Program
      {
          static void Main(string[] args)
          {
              // set yubikey's HMAC-SHA1 secret key, it is 20bytes hex string.
              byte[] key = HexStringTo20Bytes("10 1f cb 53 c6 8d d4 1d d7 db 45 02 4c fb fc 65 6f 75 33 fc");

              // set HMAC-SHA1 slot.
              short slot = 2;

              // challenge size. 63 is variable input max.
              int challengeSize = 63;


              YubiClient yubi = new YubiClient();

              // check yubikey is inserted.
              if (yubi.isInserted != ycRETCODE.ycRETCODE_OK)
              {
                  OutputMessageAndPause("multiple or no device inserted.");
                  return;
              }

              // create challenge.
              byte[] challenge = new byte[challengeSize];
              var random = new RNGCryptoServiceProvider();
              random.GetBytes(challenge);
              PrintBytes("Challenge", challenge);

              // send challenge to yubikey.
              yubi.dataEncoding = ycENCODING.ycENCODING_BYTE_ARRAY;
              yubi.dataBuffer = challenge;
              var ret = yubi.get_hmacSha1(slot, ycCALL_MODE.ycCALL_BLOCKING);
              if (ret != ycRETCODE.ycRETCODE_OK)
              {
                  OutputMessageAndPause("get_hmacSha1 error." + ret);
                  return;
              }

              // get yubikey response.
              byte[] response = yubi.dataBuffer;
              PrintBytes("Yubikey Response", response);

              // compute valid response.
              HMACSHA1 hmac = new HMACSHA1(key);
              byte[] bs = hmac.ComputeHash(challenge);
              PrintBytes("Valid Response", bs);

              Console.WriteLine("");
              OutputMessageAndPause("Please check response code yourself.");
          }

          static byte[] HexStringTo20Bytes(string str)
          {
              str = str.Replace(" ", "");
              byte[] b = new byte[20];
              for (int i = 0; i < 20; i++)
                  b[i] = Convert.ToByte(str.Substring(i * 2, 2), 16);

              return b;
          }

          static void PrintBytes(string name, byte[] b)
          {
              String challengeString = BitConverter.ToString(b);
              challengeString = challengeString.Replace("-", "");
              Console.WriteLine(name + ":");
              Console.WriteLine("  " + challengeString);
              Console.WriteLine("");
          }

          static void OutputMessageAndPause(string mes)
          {
              Console.WriteLine(mes);
              Console.ReadKey();
          }

      }
  }
ちなみに、変な英語のコメントは気にしないでください。 では実行してみます。
Challenge:
 C3D9AAB0105F4FCFC65BEE729F9026B2B040E35727066A7CBB8459D192408C75AE888F93BD17C0
7234878FE6D1BE3377AB56E516C9872BC7D2BB1C9DEDB3B7

Yubikey Response:
  79E30357C59F86DAF785E7C5A4D7502A01DC63F3

Valid Response:
  79E30357C59F86DAF785E7C5A4D7502A01DC63F3

Please check response code yourself.
今度はYubikeyのシークレットコードを別のものに変えて実行してみます。
Challenge:
 FD7BFFCFD5FCB97D7F5C70DB61316C5B472E106136F333715D73E6D8C1694AFF97089DB5685A67
8328713C2E3861072722DE4950CF18366AF3BB9E0C8C9448

Yubikey Response:
  38BAE12CB61DE7A16817EAACB9621685B87AD7FF

Valid Response:
  A8BFFDA6658A2FBF190FC99B972FDFC20B7E70C8


Please check response code yourself.
終わり。

2 件のコメント:

  1. >yubi.dataBuffer = challenge;
    ここでヒープ破損しますよね・・・。

    返信削除
  2. 自分もUnknown氏の指摘する箇所
    yubi.dataBuffer = challenge;のところで躓きました。
    これを参考に組んだプログラムが、ある日を境に実行時に必ず落ちる。その瞬間以前は動いていたので原因不明だが、トレースしたらココだった。
    原因はわからないが、Dynamic型なので試しにByte型以外のもので・・・と
    エンコードのモードを
    バイト配列からBASE64に変えたら通りました。
    同じ問題を抱えている方は、試しにBASE64やHEX文字列を試してみてはどうだろうか。

    返信削除