Hatena::Grouprubyist

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

Ruby ロゴ (C) Ruby Association LLC

2011年08月27日(土)

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

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

暗号、MAC、圧縮 と DH

Ruby でそれぞれやってみます。標準ライブラリの openssl と zlib を使います。

暗号

AES 128bit CBC モードを試してみます。まず暗号。

require 'openssl'

cipher = OpenSSL::Cipher.new('aes-128-cbc')
cipher.encrypt     # 暗号化モード
cipher.padding = 0

# SSH では鍵交換で生成した IV を使う
iv = cipher.iv = OpenSSL::Random.random_bytes(cipher.iv_len)

# SSH では鍵交換で生成した鍵を使う
key = cipher.key = OpenSSL::Random.random_bytes(cipher.key_len)

# SSH ではパケットは暗号ブロックサイズにアラインされてます
data = [*("A".."Z")][0...cipher.block_size].join # => "ABCDEFGHIJKLMNOP"

# 暗号化
s = cipher.update data   

復号してみます。

cipher2 = OpenSSL::Cipher.new('aes-128-cbc')
cipher2.decrypt  # 復号モード
cipher2.padding = 0

# SSH では鍵交換で生成した IV を使う
cipher2.iv = iv
# SSH では鍵交換で生成した 鍵 を使う
cipher2.key = key

# 復号
decrypted = cipher2.update s   # => "ABCDEFGHIJKLMNOP"

SSH 仕様と OpenSSL ではアルゴリズムの名前が微妙に異なります。

以下は YAML 形式ですが、SSH 仕様と OpenSSL の名前の対応表です。(gmrw-ssh2-server の実装で使っているもののみ)

:openssl_name: 
#  <SSH>       <OpenSSL>
  aes128-cbc: aes-128-cbc
  aes256-cbc: aes-256-cbc
  aes192-cbc: aes-192-cbc
  blowfish-cbc: bf-cbc
  cast128-cbc: cast-cbc
  3des-cbc: des-ede3-cbc

---

その他の、SSH 仕様と OpenSSL の差異としては、ストリーム暗号では、OpenSSL::Cipher#black_size と SSHプロトコル上のブロックサイズが一致せず調整が必要なものもあるようです。

gmrw-ssh2-server では、OpenSSL::Cipher#black_size と SSHプロトコル上のブロックサイズが一致する Cipher のみ扱っています。

HMAC

HMAC を試してみます。ダイジェストは SHA1、鍵長さは 20、MAC長さも 20 ('hmac-sha1' と同じ仕様です)とします。

require 'openssl'

sha1    = OpenSSL::Digest::SHA1
key_len = 20
mac_len = 20

# SSH では鍵交換で生成した鍵を使う
key = OpenSSL::Random.random_bytes(key_len)

data = ([*("A".."Z")] * 3).join  # => "ABCDEFGHIJKLMNOPQR ..."

mac = OpenSSL::HMAC.digest(sha1.new, key, data)[0, mac_len]

macMAC です。MAC 鍵長さや MAC 長さは RFC 4253 (や関連RFC) で確認できます。

HMACダイジェスト鍵長MAC
hmac-md5MD51616
hmac-md5-96MD51612
hmac-sha1SHA12020
hmac-sha1-96SHA12012

----

余談。

MAC は Message Authentication Code です。

Media Access Code (= MAC)と紛らわしいです。(Mac = Macintosh や マック = マクドナルド とも紛らわしいです)

HMAC の H は Hash(Hashed?)です。HMAC 以外にどういう MAC があるかよく知りません。

圧縮

圧縮を試します。 標準ライブラリ zlib を使います。

require 'zlib'

d = Zlib::Deflate.new  # 圧縮器
i = Zlib::Inflate.new  # 伸長器

data = ([*("A".."Z")] * 3).join # => "ABCDEFGHIJKL ... "

# 圧縮と伸長
z = d.deflate(data, Zlib::SYNC_FLUSH)
s = i.inflate(z)                        # => "ABCDEFGHIJKL ..."

圧縮はパケット毎に部分 flush すると RFC に記載されているので上記コードのようにします。

DH

DH も試します。

DH は以下のようなものです。

  (1) 公開の共通パラメータがある(Oakley Group など)

  (2) A が公開の共通パラメータから DH の公開鍵/私有鍵ペアを作る

  (3) B が公開の共通パラメータから DH の公開鍵/私有鍵ペアを作る

  (4) A は B に自分の公開鍵を渡す

  (5) B は A に自分の公開鍵を渡す

  (6) A は B の公開鍵から DH により共有秘密 K を計算する

  (7) B は A の公開鍵から DH により共有秘密 K を計算する   

   A の計算した K と B の計算した K は一致します。
   両者は私有鍵を晒すことなく、共有の秘密鍵を保持したことになります。

SSH では公開の共通パラメータのパラメータとして Oakley Group の P と G を使ったりします。ただし、通常の DH では別の方法で共通パラメータを両者に渡すようです。*1

以下は、gmrw-ssh2-server の config から切り出した、Oakley Gourp の群が記述された YAML ファイル(ファイル名:oakley_group.yaml)です。

:oakley_group:
  # Group 14 [RFC3526 3. 2048-bit MODP Group]
  :group14:
    :bits: 2048
    :g: 2
    :p:
    - FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF
    - 16

  # Group 1 [RFC2409 6.2 Second Oakley Group] (1024-bit MODP Group)
  :group1:
    :bits: 1024
    :g: 2
    :p:
    - FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF
    - 16

では、試します。

require 'yaml'
require 'openssl'

og = YAML.load open 'oakley_group.yaml' # (1) 公開の共通パラメータがある
group14 = og[:oakley_group][:group14]   # ここでは、Group 14 を採用する

# dh1 と dh2 の 2つの DHインスタンスを作る。A を dh1、B を dh2 とします。
dh1 = OpenSSL::PKey::DH.new
dh1.g = group14[:g]
dh1.p = OpenSSL::BN.new *group14[:p]
dh1.generate_key! # (2) A が公開の共通パラメータから DH の公開鍵/私有鍵ペアを作る

dh2 = OpenSSL::PKey::DH.new
dh2.g = group14[:g]
dh2.p = OpenSSL::BN.new *group14[:p]
dh2.generate_key! # (3) B が公開の共通パラメータから DH の公開鍵/私有鍵ペアを作る

# dh1 と dh2 の公開鍵
e1 = dh1.pub_key # (4) A は B に自分の公開鍵を渡す
e2 = dh2.pub_key # (5) B は A に自分の公開鍵を渡す

# それぞれ共有秘密を計算
k1 = dh1.compute_key(e2) # (6) A は B の公開鍵から DH により共有秘密 K を計算する
k2 = dh2.compute_key(e1) # (7) B は A の公開鍵から DH により共有秘密 K を計算する

p k1 == k2   # ==> true (一致する)

ここでは省略しましたが、本来は、DH#generate_key! で妥当な鍵が作成されているかチェックが要るようです。

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

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

鍵のエンコーディング

(鍵のフォーマットの詳細は RFC 4253 を参照してください)

DSA 鍵

DSA 公開鍵は以下のようにエンコーディングします。

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

      SSH2::Message::Field.pack [:string, 'ssh-dss' ],
                                [ :mpint, p         ],
                                [ :mpint, q         ],
                                [ :mpint, g         ],
                                [ :mpint, pub_key   ]

p, q, g は DSA 鍵パラメータです。pub_key は公開鍵です。

DSA 署名

DSA 鍵の署名フォーマットのエンコーディングです。

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

     SSH2::Message::Field.pack [:string, 'ssh-dss'],
                               [:string, s        ]

s には署名 BLOB が入ります。(詳細は後述)

RSA

RSA 公開鍵は以下のようにエンコーディングします。

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

      SSH2::Message::Field.pack [:string, 'ssh-rsa'],
                                [:mpint,  e        ],
                                [:mpint,  n        ]
RSA 署名

RSA 鍵の署名フォーマットのエンコーディングです。

署名フォーマットは DSA のそれと同じです。ただし、鍵の識別子は異なります。

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

     SSH2::Message::Field.pack [:string, 'ssh-rsa'], # 識別子 'ssh-rsa'
                               [:string, s        ]

s には署名 BLOB が入ります。

Ruby で試してみる

標準ライブラリ openssl で、試しに鍵を取り扱ってみます。

鍵のファイルフォーマット

OpenSSH やその他の SSH 実装系では、それぞれの SSH 形式ファイルフォーマットで鍵を格納しているようです。

gmrw-ssh2-server では(OpenSSL がデフォルトで出力する)PEM 形式でホスト鍵を取り扱います。鍵は openssl コマンドで作成できます。(ここでは鍵ビット数を1024 としています)

 $ openssl dsaparam 1024 >dsa_keyparam.pem      # DSA鍵パラメータの作成
 $ openssl gendsa dsa_keyparam.pem >dsa_key.pem # DSA 鍵の作成

 $ openssl genrsa 1024 >rsa_key.pem             # RSA 鍵の作成

以下、鍵を作成する Rakefile です。(ここでは鍵ビット数を1024 としています)

dsa_key      = "dsa_key.pem"
dsa_keyparam = "dsa_keyparam.pem"
dsa_key_bits = 1024

rsa_key      = "rsa_key.pem"
rsa_key_bits = 1024

task :default => [dsa_key, rsa_key]

file dsa_key => dsa_keyparam do
  sh "openssl gendsa #{dsa_keyparam} > #{dsa_key}"
end

file dsa_keyparam do
  sh "openssl dsaparam #{dsa_key_bits} > #{dsa_keyparam}"
end

file rsa_key do
  sh "openssl genrsa #{rsa_key_bits} > #{rsa_key}"
end

Rakefile があるディレクトリで以下をタイプすると RSA、DSA 鍵が作成されます。*2

$ rake

これで、dsa_key.pem、dsa_keyparam.pem*3、rsa_key.pem が作成されました。

RSA

irb を起動して、RSA 鍵を取り込んでみます。

$ irb -r openssl   # openssl を使います
>> rsa_key = OpenSSL::PKey::RSA.new open 'rsa_key.pem'
=> -----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQCYSZF5QNSHBGnKrINVdbK1ncgOTouapGxIbyjqN9JbkPuH54/e
vvmMGCqDpWLE7dOq7JFILtLX2a7HyDpaAQdQL6bgaKcLUSBYo2Q6ILRML0nj3SPI
13+WqMnqJagqdR4r1n6Td51haMh4sbGmgFoMMBYj8jQIxoV1JI7TX4QBZwIDAQAB
AoGAZ4SSy66jCUiZtTiBaiVAnq4rYHZJ9YvRQ4e0c0oQx0eNg7uHk62TqoR+2dAh
jKXx3V1EAKI7lpiVc56K88WBNxcziUBZ/clVM+omMpJzTnVdZTic3531U0y+U0n8
Ce2jxUqjTWOp/e3tjZPiwc0MxJPJhK1UuTtMaCEp+lNchMECQQDK+tSv7zHcoDTb
DvjAO6y1DPjddkL6iRgbO5arttXxTQfgo7/hHJVQPU3kS0FnIpWNDG8Y12J/x531
AnYD4XwHAkEAwBD1B4M0gw/ryuUzWgkrpE2UUxNyaYoUakBxnt+aTVGtm/cEwr+z
WKMZL9XVHaNffKI1qzciHOXT74mh3ZS3oQJBALGO49iXJAX8OuBZY249hWHHXDfW
SNcCcAz6fl3tjY8MtFRkyefw6giL6ARJdIf0r9/3vYN8fhSLGPkH0xan+6cCQQC/
N2Krgw3huhUXoA77AYxIfNBB0Wej+UuP6O71rEoz1jCQyWIc/bvxcBegTzRo2IE1
VU03U6GLwai5n33eW7RhAkAyOko9CZ69PmnUZwl/URGcyRD7HpnAfOGbhgm4Df+4
aWmDQqqpAMtiieJsDARUZDsTDlNKHEnwM4r3oSoK8X3m
-----END RSA PRIVATE KEY-----
>> 

パラメータ e, n を見てみます。

>> rsa_key.e
=> 65537
>> rsa_key.n
=> 106939832173593483789536303389091569710268316468569494880447723891975786778031817586417145498926717453877656702494211000768653555232521018580121449737211750263559847160616969048046406684404556092945266588650751272608236677607633805418727347003166042890645501178140981121597445905653109569789022946245327716711

署名してみます。ダイジェスト関数は OpenSSL::Digest::SHA1 を使用します。

>> sha1 = OpenSSL::Digest::SHA1
=> OpenSSL::Digest::SHA1
>> data = "hello, world"
=> "hello, world"
>> s = rsa_key.sign(sha1.new, data)
=> "C\xFFC\x8DFf\xC8\xB7_c\xC7\xE4\xB0bBo\x89\xA1\xFB\x89\xB7n\xAC\xF9\x92\x14+\xF9\xF0G\xC9/\\\xCB\xF6\xF4\x99\x94^\xD2p\x87\v\xFF\x89\x1A\xD2nO\xD8m\r!\x8E\xD2C^\xD8Z\x0F\xA1\xFD\xF7\x91\xF0\x18z\xC7\xA8\xD3<\xCFX\xC2G\xA7\x0E{\xEF\x84\xB1\x16\xCA)#4\xF2\xAFNF\xF9\xD3\b\xA7\xEBR\xA2\x9A\t1\x82\xB3\x1A\xECx\xC0\t\xB0\x85\xD6\xC9\xA6\xBA\xA9nDg9\xB2\xD4\x95ny\x16U\xBC\xD3\xCD"

data の署名が s です。署名できました。

DSA 鍵

DSA 鍵を取り込んでみます。

>> dsa_key = OpenSSL::PKey::DSA.new open 'dsa_key.pem'
=> -----BEGIN DSA PRIVATE KEY-----
MIIBugIBAAKBgQDUK2hPpo+2gsRky+F+NvVFBH1c/LCBGkYyVoi3W4ShI37Tk+lC
Dtlhv3B+RZpz49nXZghPv6P9L1RtaBJ4ronsfXJPPKfexaJNPEhLErP7eN0L9+8f
kzaz0DfEe9Va9OIfA5ekjxkhciCagHumAD+0l1VMwZJtM+yDOSRE7IA4AwIVAIgJ
xd8obA+b8NiLUhlQhG9OYluXAoGAS8Rlj9LsYiqt8+BAIi1+hmPyROCtLCP/FzXm
Kr78sbQcWTFzF8eR7La2+raasxHJO+XrgjJsliyxtcqhQ4ejkzvNUbCTqp+bZo1u
JSnQSMA2iKRer30bIwpXgfXG4j13vzaUPSPNyWo2MjBXahDUAr7wd7+s5QIpQxzk
spaMdogCgYAymN9c6Oeq6+/+mmlZnU2i20+LtO5O8FsfTqY/pv3e6wBqrYRrFYYq
eKvVSw0apx456Ge3HTzzNpkXLuZxyyIOHXuLve8lLADoEL7E/JAAYF7lzXURKn9M
axIe8ayyRJIbsct867X9sDUxiLouXF3MtIpmQAnVf/sikigr1pK0cgIUVgXrAFJe
I+Q6YkXrKKD4pgwMhWE=
-----END DSA PRIVATE KEY-----

パラメータ p, q, g, pub_key を見てみます。

>> dsa_key.p
=> 148990532101057325137086464885956496333190869577367513200320656731464319693117105086978501994757598464455577343555237684119774536657019070263801076205522493166982969414618088884804395540195383145197675511157804725365346739056671270091904911162423939967147031669700532359095660444596225967261034570336522221571
>> dsa_key.q
=> 776640688595855535525013955129338720670598716311
>> dsa_key.g
=> 53205519462720372214277166424337573428104991444415572840493743444909568795437254379979216371116399547768619741516422612166129724832474521178335006959829651793152684512112491892876379841340245213107715755262411581474895678219717468357517991806846360356608406292417561781017423754298742238729473422737680397960
>> dsa_key.pub_key
=> 35530532822515792548014287576340980422972722598249316400960077060965277428553810649632197279469625616358191549727035060517398793515249678517402634682721443302827593940475124140869785869268161942728146863188758977029086003661837266381454016824818071043925901549552542585688575917945790602589674645309720015986
>> 

署名してみます。ダイジェスト関数は OpenSSL::Digest::DSS1 を使用します。

>> dss1 = OpenSSL::Digest::DSS1
=> OpenSSL::Digest::DSS1
>> data = "hello, world"
=> "hello, world"
>> s = dsa_key.sign(dss1.new, data)
=> "0,\x02\x14O\n\x1D\x18\xDF\x83)I\xAF\xFE\x8ET\xCDj\"-\x7F\x8BZ\x9C\x02\x14u'_\x12R?+,R\x92W\xB4\xD6+\xBE\xD7\x19*\xD2\xA6"

data の署名が s です。署名できました。

...ただし、s は DER 形式でエンコーディングされているので、RSA 署名の場合と異なり、SSH 署名フォーマットの s (BLOB) にはそのままでは格納できません。

以下のエンコーディングが必要になります。

s  = 署名データ
ss = OpenSSL::ASN1.decode(s).value.map {|v| v.value.to_s(2).rjust(20, "\0") }
!ss.find {|v| v.length > 20 } or raise OpenSSL::PKey::DSAError, "bad sig size"
blob = ss.join

blobSSH 署名フォーマットの s として使用できます。

----

OpenSSL::ASN1.decode で DER をデコードしているのはいいのですが、それを値ごとに 20バイトにアラインしている理由が不明です。

このコードは、数年前に書いたので自分でも忘れてしまいました。

RFC 4253 や RFC が参照先として示している FIPS-186-2 をちょっと見たのですが分かりませんでした。

(20111.08.28 追記)-----------------

分かりました。

ASN1 で decode した各 value は、OpenSSL::ASN1::Integer にデコードされた(DSA 署名結果の) r と s です。

 OpenSSL::ASN1.decode(s).value.map {|v| v # <- OpenSSL::ASN1::Integer の r と s

これらの value は OpenSSL::BN です。

  v.value.class # => OpenSSL::BN

OpenSSL::BN#to_s(2) はバイナリ(バイト)列に変換します。

Integer#to_s(2) とは異なります。

  n = 10
  p n         # => 10
  n.to_s(2)   # => "1010"

  bn = OpenSSL::BN.new(n.to_s)
  p bn        # => 10
  bn.to_s(2)  # => "\n"  ('\n' == 0x0A == 10)

なので、r と s それぞれを、20byte * 8bit = 160 bit にエンコードしているのです。

これらの値を 「160 ビットの数値にエンコードする」ことは RFC 4253 に記されています。

私は、"010101..." の "0" と "1" を 20 文字(=バイト)にしてると勘違いしてました。パディングが "0" でなく "\0" だったので、考えれば気付きそうですが、すっかり忘れてしまってました。

--------------(20111.08.28 追記) 終わり --------------

----

余談。

DSA とか DSS といったりしますが、DSS という規格("S"tandard) の(署名用の)アルゴリズム("A"lgorithm)が DSA です。

署名検証

(2011.08.31 追記)

署名を検証するのは verify メソッドです。

rsa_key = OpenSSL::PKey::RSA.new open('rsa_key.pem')
s = rsa_key.sign('sha1', data)   #           署名 (ダイジェスト 'sha1')
rsa_key.verify('sha1', s, data)  # => true   検証

dsa_key = OpenSSL::PKey::DSA.new open('dsa_key.pem')
s = dsa_key.sign('dss1', data)   #           署名 (ダイジェスト 'dss1')
dsa_key.verify('dss1', s, data)  # => true   検証

実際に使う時

OpenSSH の ssh クライアントでは、サーバに初めてアクセスしたとき、サーバホスト鍵を覚えておくか? ということをユーザに問い合わせます。

The authenticity of host '[localhost]:50022 ([127.0.0.1]:50022)' can't be established.
RSA key fingerprint is bd:e3:bf:6a:96:dc:7c:79:77:23:ad:fd:22:c2:09:4b.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '[localhost]:50022' (RSA) to the list of known hosts.

上の例では yes と応えています。これで ssh クライアントはサーバホスト鍵を覚えました。

SSH サーバでホスト鍵を更新した後、ssh クライアントがアクセスすると、ホスト鍵が違うといってエラーになります。

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the RSA key sent by the remote host is
ef:e3:a9:15:56:e0:30:6c:03:aa:63:25:36:25:7d:fc.
Please contact your system administrator.
Add correct host key in /home/lnznt/.ssh/known_hosts to get rid of this message.
Offending RSA key in /home/lnznt/.ssh/known_hosts:3
  remove with: ssh-keygen -f "/home/lnznt/.ssh/known_hosts" -R [localhost]:50022
RSA host key for [localhost]:50022 has changed and you have requested strict checking.
Host key verification failed.

この時は、メッセージに出ている通り、ホスト鍵の記憶を消去してアクセスしなおします。

# ホスト鍵の記憶を消去
$ ssh-keygen -f "/home/lnznt/.ssh/known_hosts" -R [localhost]:50022

*1ruby の OpenSSL::PKey::DH のマニュアルには p=, q= はよく考えて使え、と警告が書いてあります

*2:勿論、Ruby(Rake 含む)、OpenSSL が環境にインストールされてなければいけません

*3:DSA鍵のパラメータファイル、DSA鍵作成後は使わない