2011年08月24日(水)
SSH サーバを作る(その10) トランスポート層プロトコルのおさらい(1)
挑戦 | |
![]()
実装 phase1 もほぼ完了なので、SSH プロトコルの最下層・トランスポート層プロトコルについて、忘れないうちに、まとめていきます。
トランスポート層プロトコルは、主に RFC4253 の範囲になります。
| 位置 | プロトコル | 提供する機能 | |
|---|---|---|---|
| 最下層 | トランスポート層プロトコル*1 | 気密性(暗号化)、完全性、サーバ認証、圧縮 | <==ここ |
| 中間層 | 認証プロトコル | クライアント認証(ユーザ認証) | |
| 最上層 | コネクションプロトコル | 多重化チャネル |
トランスポート層(SSHプロトコル)の下
TCP です。サーバは通常 22 番ポートを listen します。*2
プロトコルバージョンの交換
最初はプロトコルバージョンの交換です。
サーバとクライアント双方がバージョン文字列(識別子)を送信し合います。(※送信順序不定)
<client> <server>
| <バージョン識別子> |
|<==============================>| (※送信順序不定)
| |
バージョン文字列(識別子)は以下のフォーマットです。
SSH-2.0-xxxxxxxxx [SP] yyyyyyyyy [CR][LF]
- SP は 空白(ASCII 20h)、CR は復帰(ASCII 0Dh)、LF は改行(ASCII 0Ah) です
- SSH-2.0 は固定です
- xxxxxxxxx はソフトウェアバージョンです
- yyyyyyyyy はコメントです。これ(とその前の空白(SP))はオプションです
バージョンがマッチしない場合はコネクションを切断します。
実装では、SSH-2.0 を扱うので(SSH-1.0 には非対応です)、それ以外のバージョンの場合はコネクションを切断して処理を終了させます。*4
プロトコルバージョンの交換以後は、バイナリパケットプロトコルに移行します。
シーケンスナンバー
バイナリパケットプロトコルでは陽に表れないパラメータとしてシーケンスナンバーがあります。
これはパケット送信毎に加算されていく送信方向毎に独立のカウンタです。精度は符号無し32ビット整数で、初期値は 0 です。最大値を超えるとまた 0 に戻ります。
通常、次に紹介する KEXINT がカウント値 0 になります。
アルゴリズムネゴシエーション
鍵交換では最初に KEXINIT(番号=20) をクライアントとサーバ双方が送信し合います(送信順序不定)。
<client> <server>
| <KEXINIT(20)> |
|<==============================>| (※送信順序不定)
| |
KEXINIT には各項目毎のアルゴリズムのリストが載っています。
| フィールド名 | 説明 |
|---|---|
| kex_algorithms | KEX(鍵交換)アルゴリズム |
| server_host_key_algorithms | ホスト鍵アルゴリズム |
| encryption_algorithms_client_to_server | 対称暗号アルゴリズム(クライアントtoサーバ) |
| encryption_algorithms_server_to_client | 対称暗号アルゴリズム(サーバtoクライアント) |
| mac_algorithms_client_to_server | MACアルゴリズム(クライアントtoサーバ) |
| mac_algorithms_server_to_client | MACアルゴリズム(サーバtoクライアント) |
| compression_algorithms_client_to_server | 圧縮アルゴリズム(クライアントtoサーバ) |
| compression_algorithms_server_to_client | 圧縮アルゴリズム(サーバtoクライアント) |
暗号、MAC、圧縮のアルゴリズムは送信方向ごとに独立に選択されます。
アルゴリズムは、
- クライアントKEXINIT のリストの先頭から順に見ていき
- サーバ KEXINIT のリストに存在するもの
を選択します。
例) クライアントKEXINIT [A, B, C] サーバKEXINIT [X, B, A] ... この場合 A が選択される
アルゴリズム名については、RFC4253, RFC4250 などに載っています。
gmrw-ssh2-server で現在サポートするアルゴリズム*5は以下です*6。
| 種類 | アルゴリズム | SSH仕様上の要求 | OpenSSH の選択*7 |
|---|---|---|---|
| KEX | diffie-hellman-group-exchange-sha256 | ○ | |
| diffie-hellman-group-exchange-sha1 | |||
| diffie-hellman-group14-sha1 | REQUIRED | ||
| diffie-hellman-group1-sha1 | REQUIRED | ||
| ホスト鍵 | ssh-dss | REQUIRED | ○ |
| ssh-rsa | RECOMMENDED | ||
| 対称暗号 | aes128-cbc | RECOMMENDED | ○ |
| aes256-cbc | |||
| aes192-cbc | |||
| blowfish-cbc | |||
| cast128-cbc | |||
| 3des-cbc | REQUIRED | ||
| MAC | hmac-sha1 | REQUIRED | |
| hmac-sha1-96 | RECOMMENDED | ||
| hmac-md5 | ○ | ||
| hmac-md5-96 | |||
| 圧縮 | none | REQUIRED | ○ |
| zlib |
KEXINIT には他の項目に、cookie があります。これはランダムバイト列です。
gmrw-ssh2-server では KEXINIT メッセージインスタンスを作る毎にランダムな値を生成しています。
また、languages_client_to_server、languages_server_to_client (言語に関するタグ)、first_kex_packet_follows(後続KEXパケットの有無フラッグ) がありますが、gmrw-ssh2-server では Don't care です。
KEXINIT 以降
KEXINIT の次は、鍵交換アルゴリズムになります。
鍵交換アルゴリズムでのプロトコルはネゴシエートしたアルゴリズムにより決まります。また、そこで使われるメッセージの番号も 30..49 (鍵交換方式ごとに特有(再割り当て可))になります。
鍵交換アルゴリズムについては次回記述します。
鍵交換が終わると NEWKEYS(番号=21) をクライアントとサーバ双方が送信し合います(送信順序不定)。そこでトランスポートプロトコルが終了します。
<client> <server>
| <KEXINIT(20)> |
|<==============================>| (※送信順序不定)
| |
|................................|
| |
| 鍵交換アルゴリズム |
| (鍵交換法毎に特有) |
|................................|
| |
| <NEYKEYS(21)> |
|<==============================>| (※送信順序不定)
| |
(暗号、MAC、圧縮の開始)
(これ以降、ユーザ認証プロトコル)
余談。
KEX は Key Exchange のことです。(多分)
SSHサーバを作る(その9) Ruby1.8 で動作させる
挑戦 | |
![]()
トランスポート層の実装もほぼ終わり、そろそろ phase1 ブランチを master にマージしようかと思ってるところです。(まだ先は長いです)
マージ前の総括として、Ruby1.8 で動くか試したところ、define_singleton_method は 1.8 にない、とエラーになりました。
その修正方法のメモです。
(修正前) # Ruby1.9 では OK xxx.define_singleton_method(:foo) { ...... } (修正後) # Ruby1.8 でも OK c = class << xxx ; self ; end c.send(:define_method, :foo) { ......} または m = Module.new { define_method(:foo) { ...... } } c.extend m
以下の方法でも特異メソッドは定義できます。
ただし、class と def の外のスコープのオブジェクトを参照したい場合は上のようにします。
a = 10 class << xxx def foo a # <= a はスコープの外なので... end end xxx.foo # <= NameError: undefined local variable or method `a' for ...
a = 10 c = class << xxx ; self ; end # xxx の特異クラスを c に取得 c.send(:define_method, :foo) { a } # define_method は private なので send を使う xxx.foo # => 10
b = 20 m = Module.new { define_method(:bar) { b } } xxx.extend m xxx.bar # => 20
Ruby1.8 と Ruby1.9 では、Hash を "#{hash}" した時の表示が異なります。
# Ruby1.9 >> h = {:a=>10,:b=>20} => {:a=>10, :b=>20} >> "#{h}" => "{:a=>10, :b=>20}" # Ruby1.8 >> h = {:a=>10,:b=>20} => {:a=>10, :b=>20} >> "#{h}" => "a10b20"
この辺りを修正したら、Ruby1.8 でも動作しました。
現在、ユーザ認証プロトコルが開始する辺りまで実装済みです。
あと、Active Support はあまり使わないので基本的に使用しないことにしました。
一部だけ extension(標準クラスの拡張)に残し、代替ライブラリも削除しました。(スッキリ)
*1:OSI 7階層モデルでいうトランスポート層ではありません
*2:gmrw-ssh2-server ではデフォルトで 50022 番ポートを listen します
*3:gmrw-ssh2-server は、標準ライブラリの gserver のサービスとして実装されてます。サービスのインスタンスは 1 つの確立コネクションに対して 1 つです
*4:RFC には、古いバージョン(SSH-1.0)との互換性や、バージョン文字列以外の情報の送信方法などが記載されていますが、実装には関係ないので反映させてません
*5:環境にインストールされる OpenSSL、Zlib、および関連ライブラリ依存になります