zebedeeプロトコル解析

はじめに

これは、私がzebedee2.1.3のソースからプロトコル仕様を抜き出したものです。 2.1.3では、古いバージョンとの後方互換性を保っていますが、ここでは最新のプロトコルバージョンV201について解説します。

使用アルゴリズム

blowfish暗号化

電文の暗号化のアルゴリズムとして、秘密鍵暗号アルゴリズムであるblowfishを使用しています。 実装としては、SSLeayから抜き出したものを使用しているようです。

Diffie-Hellmanの鍵交換アルゴリズム

blowfishの鍵を交換するために、diffieHellmanアルゴリズムを使用しています。 diffieHellmanのためには、多倍長整数の計算が必要ですが、その処理はPythonのソースから抜き出したものを使用しているようです。

鍵交換とは何をするものなのか説明すると、 一般に暗号アルゴリズムの実行には、サーバとクライアントで同じ鍵を用意する必要があります。 この鍵をインターネットで転送しようとすると、 それを盗聴されてしまい、暗号を解読されてしまう恐れがあります。 そのため、安全に通信するためには、鍵を(フロッピーディスクを郵送するなど)オフラインの安全な手段で転送する必要がありますが、 これは非常に手間がかかり、非現実的です。 鍵交換とは、インターネットを通して(仮に盗聴されているとしても)鍵を盗まれないように交換する処理です。 サーバとクライアントは、数学的に特殊な処理を行ないある種のデータをインターネットで交換するのですが、 その交換したデータにさらに特殊な計算をすると、サーバとクライアントで同じ鍵を生成できます。 ここに数学的な魔術があって、第3者が交換したデータを使っても、その鍵を値は計算できません。

sha

ハッシングのアルゴリズムとしてshaを使用しています。 shaはランダムキーの生成や、認証のために使用しています。 こちらはperl用モジュールのソースを修正して使用しているようです。

zlibとbzlib

圧縮アルゴリズムとしては、linux等で標準となっているzlibとbzlibを使用しています。

プロトコルの概略

プロトコルバージョンとヘッダーの交換

サーバとクライアントは、最初にプロトコルバージョンを交換し、合意したバージョンのヘッダーを交換します。 ヘッダーには次のデータが含まれています。

項目説明方向
UDP/TCPトンネリングするセッションがUDPかTCPかC<=>S
バッファーサイズ1メッセージの長さC<=>S
圧縮レベル圧縮に使用するアルゴリズムとレベルC<=>S
ポートターゲットのポートC=>S
キー長blowfishに使用する鍵の流さC<=>S
トークン鍵の再利用に使用するトークンC<=>S
Nonce鍵の生成に使用する乱数C<=>S
ターゲットアドレスターゲットのIPアドレスC=>S

キー長や圧縮レベルは、このヘッダー交換でネゴシエートされます。

鍵交換

diffieHellmanに従った鍵交換の処理を行います。 サーバとクライアントでおのおの特別な計算を行ない、その結果を送信します。 両者は受けとった結果からさらにまたある計算を行ない、秘密鍵と呼ばれる値を計算します。

実際に暗号化に使う鍵は、さらにこの秘密鍵とヘッダーにあるNonceという乱数から生成します。 Nonceが混じることで、毎回違う鍵が使われるようになります。

zebedeeでは、DH keyで認証の処理も行ないます。 DH Keyのshaハッシュを計算して、その結果がファイルに設定されたもの(のどれかと)一致しているか確認して相手を認証します。

一度交換した鍵は記憶しておき、一定時間内に再度セッションを開く時には、これを再利用します。 この再利用の合意をとるためにトークンが使用されます。

鍵が交換されるまでのデータは平文で送信され、それ以降のデータはblowfishによって暗号化されて送信されます。

チャレンジの実行

暗号化された安全な通信路が確保された後に、チャレンジとレスポンスの交換を相互に行ないます。 zebedeeのチャレンジレスポンスは、固定の数値をチャレンジとXORするという非常に単純な処理になっています。

トンネリング

以下は、セッションの終了までトンネリングするデータを送受信します。 データは、1ブロックごとに圧縮されてから暗号化されて送信されます。

鍵交換と認証の詳細

ModulusとGenerator

Diffie-Hellmanでは、ModulusとGeneratorという一組の特殊な数値が必要になります。 このペアは非常に大きな桁数の整数で、数学的に特別な性質を満たしていなくてはいけません。 zebedeeでは、この数値は固定となっていて、プログラム中に埋めこまれています。

session keyの生成

blowfish暗号化のために生成されるsession keyは、以下のものからshaハッシュアルゴリズムによって生成されます。

Nonceは、以下のものからshaハッシュアルゴリズムによって生成されます。

Nonceのモトネタとなる値は、いずれも毎回違う値であり外部から推定したり予測するのは困難ですから、 session keyを予測したり推定するのも同様に困難だと思われます。

秘密鍵の生成

工事中です。

認証

checkidfileオプションを指定した場合、zebedeeは送信相手の認証を行います。

認証の第1段階は、鍵交換の手順の中で相手が送信してきた値(DH KEY)が、idfileに記録されている値と一致するかどうかをチェックするだけです。 DH KEYの送信は、session keyの交換の前に行なわれますので、平文で送信されるし、毎回同じ値となります。 そのために、もし通信が盗聴されており、その盗聴されたデータを使用すると、正規ユーザになりすましてこの段階をパスすることは簡単です。

しかし、なりすましで認証をパスしても、DH KEYのモトとなるプライベートキーを逆算することはできませんから、 なりすまし野郎は、session keyを正しく復元することはできません。

従って、暗号化されて行なわれる第2段階(チャレンジレスポンスの交換)で、なりすまし野郎ははじかれます。 チャレンジレスポンスは固定値とのXORという単純な手順ですが、session keyを知らずにこれをマネすることは困難です。

鍵交換の一部を認証に使用しているため、認証をパスした上で、別の値をsession keyとして使用することも原理的にできないようになっています。

鍵の再利用

工事中です。

クライアントの処理

以下は、zebedeeのソースから、本質的な処理だけ抜き出してコメントを付加したものです。 ただしエラー処理と旧プロトコルバージョンの処理と、セッションキーの再利用に関する処理は省略してあります。


void
client(FnArgs_t *argP)
{
  /* */
  /* プロトコルバージョンの交換 */
  requestResponse(serverFd, protocol, &response) ;
  /*
    requestResponseは、protocol(クライアントが要求するバージョン番号)
    を送信し、response(サーバの理解するバージョン番号)を受信する。
    クライアントはサーバの返答したバージョン番号が既知のものであれば、
    以降、そのプロトコルバージョンで通信する。
   */

  /* Nonceの生成 */
  generateNonce(clientNonce);
  /*
    generateNonceは(プロセスID、スレッドID、時刻)+αをshaでかきまぜて
    乱数値を生成する。αの部分はOS依存で、windowsではQueryPerformanceCounter、
    linuxではtick=times()から取り出した値
   */
  
  /* ヘッダーの作成*/
  /*
    設定ファイルより読み出した各オプションの値をヘッダーに設定する。
   */


  /* ヘッダーの送信*/
  writeData(serverFd, hdrData, hdrSize) ;
  /* ヘッダーの受信*/
  readData(serverFd, hdrData, hdrSize) ;

  /* DH generator を受信する */
  /* DH modulus を受信する */
  /* server DH key を受信する */

  /* IdentityFileが指定されていたら、チェックする  */
  if (IdentityFile) {
    checkIdentity(IdentityFile, generator, modulus, serverDhKey) ;
    /*
      checkIdentityは    
      hashStrings(keySig, generator, modulus, key, NULL)で、shaの
      ハッシュ値を計算し、その値がIdentityFileに存在するかチェック
      する。
     */
  }

  /* キーを生成する*/
  exponent = generateKey();
  /*
    generateKeyはPrivateKeyがオプションで指定されていれば、その値を
    そのまま返却する。されてなければ、generateNonceと同様の処理に
    加え、/dev/ramdomを利用した乱数でさらにハッシュをかきまぜてから、
    新しいキーを生成する。
   */

  /* DHアルゴリズム */
  dhKey = diffieHellman(generator, modulus, exponent);

  /* DH key を送信する*/

  /* DHアルゴリズムで秘密鍵を算出する */
  secretKeyStr = diffieHellman(serverDhKey, modulus, exponent);

  /* 
     クライアント側は DH(KEY1,DH(KEY2))を計算する。
     サーバ側は,      DH(KEY2,DH(KEY1))を計算する。
     両者はDHアルゴリズムの特性により、同じ値になることが保証されて
     いるので、これを秘密鍵として使用する。DH(KEY1),DH(KEY2)は伝送
     されるが、KEY1,KEY2及び秘密鍵は伝送されない。DH(KEY1),DH(KEY2)
     からは、秘密鍵やKEY1,KEY2の値を計算することはできない(非常に
     計算量が多くなる)
  */

  /* 交換した秘密鍵とNonceからセッションキーを生成する*/
  sessionKeyStr = generateSessionKey(secretKeyStr, clientNonce, serverNonce, keyBits);
  /*
    generateSessionKeyは、渡された3つの値からshaハッシュを計算し、その中からkeyBitsで
    指示された長さのビット列を切り出し、返却する。
   */

  /* 暗号化開始 */
  /*
    sessionKeyStrでblowfishの処理を初期化する。以降の通信は全て、このキーによって
    blowfishアルゴリズムで暗号化される
   */

  /* チャレンジレスポンス*/
  clientPerformChallenge(serverFd, msg) ;
  /*
    クライアントは時刻のtick値を送信し、サーバがその各バイトを42でXORした値を
    返してくる。その後、攻守を変えて同じことする。単純な処理だが、これで相手が
    暗号を正しく解読しているか確認できる。checkIdentityオプションを使用している
    時は、この処理で相手が正しい秘密鍵を持っていることを確認できる。
   */

  /* トンネリング処理*/
  filterLoop(clientFd, serverFd, msg, &(argP->addr), argP->listenFd) ;
  /*
    セッションが終わるまで、合意した圧縮と暗号化の処理を行ない送受信する。
   */
}

サーバの処理

void
server(FnArgs_t *argP)
{
  /* プロトコルバージョンを受信する */
  readUShort(clientFd, &request);
  /*
    クライアントの要求したプロトコルバージョンが既知のものならば、
    それを採用する。未知のものならば最新バージョン(V201)を使用する
   */
    
  /* プロトコルバージョンを送信する */
  writeUShort(clientFd, protocol) ;

  /* ヘッダーを受信する */
  readData(clientFd, hdrData, hdrSize);

  /* 
     クライアントの要求と自分の設定ファイルを突き合わせて、応答ヘッダー
     を作成する。
  */
     

  /* クライアントの要求したアドレスとポートは設定ファイルで許可されているか? */
  if (allowRedirect(request, &localAddr, &targetHost))
    {
      /* ターゲットに接続できるか?*/
      if ((localFd = makeConnection(targetHost, request, UDPMode, &localAddr)) == -1)
	{
	}
      else
	{ /* エラー処理 */ }
    }
  else
    { /* エラー処理 */ }

  generateNonce(serverNonce);

  /* 応答ヘッダー送信 */
  writeData(clientFd, hdrData, hdrSize) ;

  /* Generator Modulusを送信する */

  /* 秘密鍵を生成する */
  exponent = generateKey();

  dhKey = diffieHellman(Generator, Modulus, exponent) ;

  /* dhKeyを送信する */

  /* クライアントのdhKeyを受信する */

  if (IdentityFile)
    {
      checkIdentity(IdentityFile, Generator, Modulus, clientDhKey) ;
    }

  secretKeyStr = diffieHellman(clientDhKey, Modulus, exponent);

  sessionKeyStr = generateSessionKey(secretKeyStr, clientNonce, serverNonce, keyBits);

  /* 暗号化開始 */
  /*
    sessionKeyStrでblowfishの処理を初期化する。以降の通信は全て、このキーによって
    blowfishアルゴリズムで暗号化される
   */

  /* チャレンジレスポンス*/

  serverPerformChallenge(clientFd, msg) ;

  /* トンネリング処理*/
  filterLoop(localFd, clientFd, msg, &localAddr, localFd);
  /*
    セッションが終わるまで、合意した圧縮と暗号化の処理を行ない送受信する。
   */
}