Hatena::Grouprubyist

Going My Ruby Way このページをアンテナに追加 RSSフィード

Ruby ロゴ (C) Ruby Association LLC

2011年08月26日(金)

SSH サーバを作る(その13) トランスポート層のおさらい(4)

| 21:35 | SSH サーバを作る(その13) トランスポート層のおさらい(4) - Going My Ruby Way を含むブックマーク はてなブックマーク - SSH サーバを作る(その13) トランスポート層のおさらい(4) - Going My Ruby Way SSH サーバを作る(その13) トランスポート層のおさらい(4) - Going My Ruby Way のブックマークコメント

鍵 と IV の生成

鍵交換で取得した共有秘密 K と交換ハッシュ H と セッション鍵から、鍵とIV(初期化ベクタ)を生成します*1

通信の方向毎に以下の鍵とIVが必要です。

  • 暗号化IV
  • 暗号化鍵
  • MAC
   HASH       = 鍵交換のダイジェスト関数によるハッシュ出力関数
   K          = 共有秘密
   H          = 交換ハッシュ
   session_id = セッション識別子
   salt       = クライアント(からサーバ方向)の暗号化IVの場合 "A"
              | サーバ(からクライアント方向)の暗号化IVの場合 "B"
              | クライアント(からサーバ方向)の暗号化鍵の場合 "C"
              | サーバ(からクライアント方向)の暗号化鍵の場合 "D"
              | クライアント(からサーバ方向)のMAC鍵の場合    "E"
              | サーバ(からクライアント方向)のMAC鍵の場合    "F"

   鍵 = HASH(K + H + salt + session_id)
   
   鍵の長さが足りない場合は、
   
     鍵 = HASH(K + H + 鍵)

   を長さが足りるまで繰りかえします。
   (最終的に鍵の長さはそれぞれの鍵長にそろえます)

gmrw-ssh2-server での該当コードです。

  #
  # :section: Key Exchange
  #
  def do_kex
    @secret, @hash, = kex.key_exchange(self) ; @session_id ||= @hash
  end

  def keys_into_use
    key = proc do |salt, len|
      y =  kex.digest(@secret + @hash + salt + @session_id)
      y << kex.digest(@secret + @hash + y)                  while y.length < len
      y[0...len]
    end

    client.keys_into_use :iv => key<<"A", :key => key<<"C", :mac => key<<"E"
    server.keys_into_use :iv => key<<"B", :key => key<<"D", :mac => key<<"F"
  end

gmrw-ssh2-server では key として鍵そのものでなく、鍵を出力する proc を作成して client、server に渡しています。

ちなみに「key<<"A"」の Proc#<< は gmrw-ssh2-server の標準クラス拡張です。

proc の引数の部分適用です。(Ruby1.9 でなら curry でも代用できると思います)

  mixin Proc do
      :
    def first_arg(*x)
      proc {|*a| call( *(x + a) )}
    end

    alias << first_arg
      :
  end

mixin については前の日記(mixin - Going My Ruby Way - Rubyist)を参照ください。

curry を部分適用に応用するには、こんな感じです。(Ruby1.9 で可能)

 sub = proc {|a, b| a - b }
 sub10 = sub.curry.call(10)  # .curry.call する
 sub10[3]  # => 7

暗号化、MAC、圧縮の開始

NEWKYES の送信と受信が終わった後、鍵と IV を適用して暗号化、MAC、圧縮の開始します。

トランスポート層全体の流れを以下に示します。

  <client>                         <server>
    |       <バージョン識別子>       |
    |<==============================>|  * SSH バージョン交換
    |                                |  
    |................................|....................................
    |                                |  * バイナリ通信開始
    |         <KEXINIT(20)>          |
    |<==============================>|  * アルゴリズムネゴシエーション
    |                                |  
    |................................|....................................
    |                                |
    |     DH など鍵交換プロトコル    |  * K と H の取得 
    |      (30..49番のメッセージ)    |    --> 鍵と IV の生成
    |................................|....................................
    |                                |
    |         <NEYKEYS(21)>          |
    |<==============================>|
    |                                |
    |................................|....................................
    |                                |  * 暗号化、MAC、圧縮の開始

暗号化、MAC、圧縮のアルゴリズムは次に KEXINIT が走り鍵交換されるまで変わりません。

また、最初の NEWKEYS が走るまでの間は、暗号化、MAC、圧縮とも 'none'です。

(暗号化 と MAC の 'none' は仕様では推奨されてません。なので、通常、暗号化 と MAC が 'none' なのはこの最初の間だけです)

SSH サーバを作る(その12) トランスポート層プロトコルのおさらい(3)

| 08:20 | SSH サーバを作る(その12) トランスポート層プロトコルのおさらい(3) - Going My Ruby Way を含むブックマーク はてなブックマーク - SSH サーバを作る(その12) トランスポート層プロトコルのおさらい(3) - Going My Ruby Way SSH サーバを作る(その12) トランスポート層プロトコルのおさらい(3) - Going My Ruby Way のブックマークコメント

DH 鍵合意 (GEX)

以下のプロトコル*2を見てみます。

...-sha256、...-sha1 はそれぞれダイジェスト関数に SHA256、SHA1 を使います。

これらのプロトコルの特徴は、(素数の)群(=グループ)が通信時の合意によって決められることです。(GEX = Group Exchange)

  <client>                         <server>
    |         <KEXINIT(20)>          |
    |<==============================>| (※送信順序不定)
    |                                |
    |................................|..............................
    |                                |
    |  <KEX_DH_GEX_REQUEST(34)>      |
    |------------------------------->|   
    |                                |
    |         <KEX_DH_GEX_GROUP(31)> |
    |<-------------------------------|
    |                                | (※DH鍵合意(GEX)プロトコル)
    |  <KEX_DH_GEX_INIT(32)>         |
    |------------------------------->|   
    |                                | 
    |         <KEX_DH_GEX_REPLY(33)> |
    |<-------------------------------|
    |                                |
    |................................|..............................
    |                                |
    |         <NEWKEYS(21)>          |
    |<==============================>| (※送信順序不定)
    |                                |
メッセージ番号
SSH_MSG_KEX_DH_GEX_REQUEST_OLD30後方互換性のため残されている
SSH_MSG_KEX_DH_GEX_REQUEST34
SSH_MSG_KEX_DH_GEX_GROUP31
SSH_MSG_KEX_DH_GEX_INIT32
SSH_MSG_KEX_DH_GEX_REPLY33

KEX_DH_GEX_REQUEST (Client -> Server)

クライアントは最初 KEX_DH_GEX_REQUEST を送ってきます。パラメータは以下です。

min
受け入れられる(素数の)群の最小ビット長(仕様上の推奨 1024)
n
希望する(素数の)群のビット長
max
受け入れられる(素数の)群の最大ビット長(仕様上の推奨 8192)

サーバはこれらから安全な(素数の)群を生成(or選択)します。

RFC には素数の生成方法が記載されています。

gmrw-ssh2-server 実装では、n=1024 の時 Group1*3 、n=2048 の時 Group14*4 を使用しています。

OpenSSH の ssh クライアントはデフォルトで

  • (n, min, max) = (1024, 1024, 8192)

を送信してきます。

KEX_DH_GEX_GROUP (Server -> Client)

素数生成(or選択)、DH 初期化の後、サーバは KEX_DH_GEX_GROUP を返信します。

パラメータは以下です。

p
DH パラメータの p
g
DH パラメータの g

KEX_DH_GEX_INIT (Client -> Server)

次にクライアントは KEX_DH_GEX_INIT を送信します。パラメータは以下です。

e
クライアントDH公開鍵

KEX_DH_GEX_REPLY (Server -> Client)

サーバは KEX_DH_GEX_REPLY を返信します。パラメータは以下です。

k_s
サーバホスト鍵
f
サーバDH公開鍵
s
サーバホスト鍵で署名された交換ハッシュ H

共有秘密 K と 交換ハッシュ H

K と H の意味合いは、鍵交換のアルゴリズムに関わらず同じです。

共有秘密 K
DH により計算される共有鍵
交換ハッシュ H
K やその他のプロトコルのパラメータの連結文字列から生成するハッシュ。
セッション識別子
最初の 交換ハッシュ H。鍵交換が再度行なわれても更新されない。

K の生成の手順です。

 (1) クライアント DH 公開鍵 e から DH により共有秘密を計算する
 (2) OpenSSL::PKey::DH#compute_key は バイナリの文字列形式で結果を返すので   数値化する
 (3) mpint 型にエンコードする

H の生成の手順です。

 (1) SSH のデータ型でエンコードされた各種パラメタの連結文字列 h0 を求める
 (2) h0 からダイジェスト関数によりハッシュ h を求める
 (3) ハッシュ h にホスト鍵で署名する(署名は署名フォーマット(次回以降、説明)にエンコードする

gmrw-ssh2-server 実装での該当コードです。

DH クラス*5を継承しているので共通部分はコードに表れてません。

  class DHGex < DH
    private
    def group
      a = groups.each_value.find {|g| g[:bits] == n }
      b = groups.each_value.find {|g| (min..max).include?(g[:bits]) }

      (a || b) or raise "DH Group Error: n:#{n}, min..max#{min}..#{max}"
    end

    #
    # :section: DH Key Agreement
    #
    def agree
      send_message :kex_dh_gex_group, :p => dh.p, :g => dh.g

      send_message :kex_dh_gex_reply,
            :host_key_and_certificates => k_s,
            :f                         => f,
            :signature_of_hash         => s
    end

    property_ro :max, 'client.message(:kex_dh_gex_request)[:max]'
    property_ro :n,   'client.message(:kex_dh_gex_request)[:n  ]'
    property_ro :min, 'client.message(:kex_dh_gex_request)[:min]'

    property_ro :e,   'client.message(:kex_dh_gex_init)[:e]'

    def h0
      pack([:string, v_c  ],
           [:string, v_s  ],
           [:string, i_c  ],
           [:string, i_s  ],
           [:string, k_s  ],
           [:uint32, min  ],
           [:uint32, n    ],
           [:uint32, max  ],
           [:mpint , dh.p ],
           [:mpint , dh.g ],
           [:mpint , e    ],
           [:mpint , f    ]) + k
    end
  end

出力された鍵

セッション識別子は、最初の鍵交換の場合、H をそのままコピーします。

KHセッション鍵は保存します。