Hatena::Grouprubyist

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

Ruby ロゴ (C) Ruby Association LLC

2011年08月31日(水)

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

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

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

gmrw-ssh2-server ではサーバホスト鍵のフォーマットは OpenSSL コマンドのデフォルトの出力形式にしています。(変換なしで読み込めるため)

しかし、通常使われる OpenSSH の ssh-keygen で作成した公開鍵ファイルのフォーマット*1は以下のようになっています。(~/.ssh/{id_dsa,id_rsa}.pub など)

(DSA の場合)
ssh-dss AACBq3NNHQZWavYnLTpJsZFvwQnWJCKm+RRAAF... foobar@example.org

(RSA の場合)
ssh-rsa AACBq3NNHQZWavYnLTpJsZFvwQnWJCKm+RRAAF... foobar@example.org

1行で1個の鍵なので、鍵が複数個ある場合は複数行になっていると思います。

空白で区切られた 2 番目の文字列が BASE64 でエンコードされた公開鍵です。

(この例では適当な文字列が書かれています。正しくありません)

これらの公開鍵は SSH プロトコルで使用されている以下の形式でエンコーディングされています。

(DSA 鍵の場合)

       [:string, 'ssh-dss' ],
       [ :mpint, p         ],
       [ :mpint, q         ],
       [ :mpint, g         ],
       [ :mpint, pub_key   ]

(RSA 鍵の場合)

       [:string, 'ssh-rsa'],
       [:mpint,  e        ],
       [:mpint,  n        ]

OpenSSL の SSH2 形式の公開鍵の内容を表示するスクリプトを書いてみました。

以下のようにして使用します。(詳しいことはスクリプトの内容を見てください)

  $ print_ssh_foromat_pubkey.rb < ~/.ssh/id_dsa.pub
または
  $ print_ssh_foromat_pubkey.rb < ~/.ssh/id_rsa.pub
#!/usr/bin/env ruby

require 'openssl'

def sep(s, result=[])
  n, s = s.unpack("Na*")
  v, s = s.unpack("a#{n} a*")

  result << v

  s.empty? ? result : sep(s, result)
end

def dec(id, *nums)
  [id] + nums.map {|n| OpenSSL::BN.new(n, 2) }
end

ARGF.each do |line|
  form_id, s, mail = line.split(/\s+/)
  parsed = dec *(sep s.unpack("m")[0])

  case form_id
    when 'ssh-rsa'
      id, e, n = parsed
      puts <<-RSA
id = #{form_id}, mail = #{mail}
  id: #{id}
  e : #{e}
  n : #{n}

RSA

    when 'ssh-dss'
      id, p_, q, g, key = parsed
      puts <<-DSA
id = #{form_id}, mail = #{mail}
  id : #{id}
  p  : #{p_}
  q  : #{q}
  g  : #{g}
  key: #{key}

DSA

    else
      raise "unexpected format"
  end
end

# vim:set ts=2 sw=2 et ai:

DSA 署名フォーマット

DSA 署名は r, s を出力します。SSH プロトコルの署名フォーマットの BLOB には、これらを 各 160 bit (20オクテット)の整数にエンコーディングします。(合計 40オクテット)

dsa_key = OpenSSL::PKey::DSA.new open 'dsa_key.pem'
sig = dsa_key.sign('dss1', data)  # sig は DER 形式の文字列

# SSH プロトコルの署名フォーマット(BLOB)に合うようにエンコーディング
s = OpenSSL::ASN1.decode(sig).value.map {|v| v.value.to_s(2).rjust(20, "\0") }.join

# 通常、ここで署名フォーマットに格納して相手に送信

# エンコードしたものを戻す
sig2 = OpenSSL::ASN1::Sequence.new(s.unpack("a20 a20").map {|v| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(v,2)) }).to_der

sig == sig2 # => true (一致)

# 署名検証
dsa_key.verify('dss1', sig, data)  # => true
dsa_key.verify('dss1', sig2, data) # => true

RSA では sign で出力した s をそのまま署名フォーマットの BLOB として通信します。

s = rsa_key.sign('sha1', data)  # 署名

rsa_key.verify('sha1', s, data) # 検証

SSH プロトコルの署名フォーマット

     [:string, 'ssh-dss'],   # RSA の場合は 'ssh-rsa'
     [:string, s        ]

DSA 署名を ASN.1 記法で表す。(あってるかどうか自信なし)

DSAsignature :: SEQUENCE {
  r INTEGER,
  s INTEGER
}

*1:OpenSSL の SSH2 形式

2011年08月30日(火)

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

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

メッセージについておさらいです。

メッセージ

(一部、以前の日記の再掲です)

パケットの 'payload' にはメッセージが格納されます。

メッセージの最初のフィールドは以下のようになっています。

フィールド名説明
bytetypeメッセージ番号
... ... (以降のフィールドはメッセージ毎に定義されている)

メッセージ番号はメッセージの種類を示します。以下のようにカテゴライズされています。

カテゴリ番号説明
トランスポート層プロトコル 1 .. 19 トランスポート層一般
20 .. 29 アルゴリズムのネゴシエーション
30 .. 49 鍵交換方式ごとに特有(再利用可能)
認証プロトコル 50 .. 59 ユーザ認証一般
60 .. 79 ユーザ認証法ごとに特有(再利用可能)
コネクション・プロトコル 80 .. 89 コネクションプロトコル一般
90 .. 127 チャネルに関連したメッセージ
クライアントプロトコルのための予約 128 .. 191 予約
ローカルな拡張 192 .. 255 ローカルな拡張

再利用可能と説明されているカテゴリは、メッセージ番号に対応するメッセージがアルゴリズムやスキームによって決まります。

トランスポート層のメッセージ

番号メッセージ方向備考
1DISCONNECTC->S, S->C
2IGNOREC->S, S->C
3UNIMPLEMENTEDC->S, S->C
4DEBUGC->S, S->C
5SERVICE_REQUESTC->S
6SERVICE_ACCEPTS->C
--------------
20KEXINITC->S, S->C
21NEWKEYSC->S, S->C
--------------
30KEXDH_INITC->S鍵交換法が DH の場合
31KEXDH_REPLYS->C鍵交換法が DH の場合
--------------
30KEX_DH_GEX_REQUEST_OLD 後方互換性のため残されている
31KEX_DH_GEX_GROUPS->C鍵交換法が DH(GEX) の場合
32KEX_DH_GEX_INITC->S鍵交換法が DH(GEX) の場合
33KEX_DH_GEX_REPLYS->C鍵交換法が DH(GEX) の場合
34KEX_DH_GEX_REQUESTC->S鍵交換法が DH(GEX) の場合

サービス要求メッセージ

SERVICE_REQUEST
クライアントがサービスを要求するため送信する
SERVICE_ACCEPT
サーバが要求されたサービスは要求を受け付けることを通知するため送信する

NEWKEYS が走った後、クライアントは サービス 'ssh-userauth'(ユーザー認証) を要求するため、SERVICE_REQUEST を送信してきます。

サーバは(要求を受けつけるなら)SERVICE_ACCEPT を返信します。

  <client>                         <server>
    |                                |
    |  <SERVICE_RESUQST(5)>          |
    |------------------------------->|  
    |                                |  * 要求サービスの受け付け
    |           <SERVICE_ACCEPT(6)>  |   (ここでは service = 'ssh-userauth')
    |<-------------------------------|
    |                                |
        (以降、ユーザ認証プロトコル)

トランスポート層一般のメッセージ

DISCONNECT
コネクションの切断を通知する
IGNORE
無視すべきメッセージ
UNIMPLEMENTED
相手から送信されたメッセージが未実装であることを通知する
DEBUG
デバッグ用データを送信する

パラメータなど詳細については RFC 4253 を参照してください。

gmrw-ssh2-server でのメッセージ定義コードを示しておきます。

module GMRW::SSH2::Message
  disconnect_reason = proc do |tag|
    {
      :HOST_NOT_ALLOWED_TO_CONNECT    =>   1,
      :PROTOCOL_ERROR                 =>   2,
      :KEY_EXCHANGE_FAILED            =>   3,
      :RESERVED                       =>   4,
      :MAC_ERROR                      =>   5,
      :COMPRESSION_ERROR              =>   6,
      :SERVICE_NOT_AVAILABLE          =>   7,
      :PROTOCOL_VERSION_NOT_SUPPORTED =>   8,
      :HOST_KEY_NOT_VERIFIABLE        =>   9,
      :CONNECTION_LOST                =>  10,
      :BY_APPLICATION                 =>  11,
      :TOO_MANY_CONNECTIONS           =>  12,
      :AUTH_CANCELLED_BY_USER         =>  13,
      :NO_MORE_AUTH_METHODS_AVAILABLE =>  14,
      :ILLEGAL_USER_NAME              =>  15,
    }[tag] || tag
  end

  def_message :disconnect, [
    [ :byte,   :type         ,1                      ],
    [ :uint32, :reason_code  ,nil, disconnect_reason ],
    [ :string, :description                          ],
    [ :string, :language_tag                         ],
  ]

  def_message :ignore, [
    [ :byte,   :type  ,2 ],
    [ :string, :data     ],
  ]

  def_message :unimplemented, [
    [ :byte,   :type            ,3 ],
    [ :uint32, :sequence_number    ],
  ]

  def_message :debug, [
    [ :byte,   :type         ,4 ],
    [ :string, :message         ],
    [ :string, :language_tag    ],
  ]
  def_message :service_request, [
    [ :byte,   :type         ,5 ],
    [ :string, :service_name    ],
  ]

  def_message :service_accept, [
    [ :byte,   :type         ,6 ],
    [ :string, :service_name    ],
  ]
end

2011年08月28日(日)

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

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

パケットについておさらいです。

どこを暗号化/MAC/圧縮するか?

以前の日記( SSHサーバを作る (その0) 下調べ - Going My Ruby Way - Rubyist)の一部を再掲します。

フィールド名説明暗号化MAC圧縮
uint32packet_length'packet_length'自体 と 'mac' を含まないパケット長(byte 単位)
bytepadding_length'padding' の長さ(byte単位)
byte[n1]payloadメッセージ
byte[n2]paddingランダムなbyte値によるパディング。
byte[m]macMAC*1

payload の長さ n1 は、

  n1 = 'packet_length' の値 - 'padding_length' の値 - 1 ('padding_length'自体の長さ)

です。

padding の長さ n2 は'padding_length' の値(最小 4、最大 255)です。

これは、packet_length + padding_length + payload + padding の長さが、暗号ブロックサイズか 8 の大きい方の倍数になるように決めます。

現時点の gmrw-ssh2-server 実装でのパケットを組み立てコードです。

  def pack(payload)
    n1    = 4 # packect_length field size
    n2    = 1 # padding_length field size
    mn    = 4 # minimum padding size

    zipped_data = compress[ payload ]
    total_len   = block_align[ zipped_data.length + n1 + n2 + mn ]
    pack_len    = total_len - n1
    padd_len    = pack_len - n2 - zipped_data.length
    padding     = OpenSSL::Random.random_bytes(padd_len)
    packet      = [pack_len, padd_len].pack("NC") + zipped_data + padding

    encrypt[ packet ] + compute_mac[ packet ]
  end

mac の長さ m は MACアルゴリズムにより決まります。初期は 0 です。

また、RFC では(mac まで含めた)全体が 35000 バイト以下のパケットが処理できなければならないと記載されています。


暗号化

暗号化は

packet_length + padding_length + payload + padding

に対して行ないます。サイズは padding により暗号ブロックサイズ(か 8 のどちらか大きい方)にアラインされてます。

payload は圧縮済み(圧縮アルゴリズムが選択されている場合)のものを暗号化します。

MAC

MAC は、暗号化前/復号した後のパケットの

  unencrypted_packet = packet_length + padding_length + payload + padding
  data_for_mac = [ sequence_number, unencrypted_packet ].pack("Na*")

の data_for_mac に対して行ないます。

sequence_number はシーケンスナンバーです。以前の日記*2でも書きましたが、これは送信方向毎に独立した送信/受信カウンタです。初期値が 0 で、符号無し32ビット整数の精度を持ちます。(0xffffffff までいったら 0 に戻ります)

  (初期値)    sequence_number = 0

  (送信/受信) sequenct_number = (sequenct_number + 1) % 0xffff_ffff
圧縮

圧縮/伸長は payload に対して行ないます。パケット毎に flush します。

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鍵作成後は使わない

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セッション鍵は保存します。

2011年08月25日(木)

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

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

DH 鍵合意

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

これらはダイジェスト関数に SHA1 を使います。

また、(素数の)群(=グループ)にそれぞれ Group14、Group1 を使います。

  <client>                         <server>
    |         <KEXINIT(20)>          |
    |<==============================>| (※送信順序不定)
    |                                |
    |................................|.............................
    |                                |
    |  <KEXDH_INIT(30)>              |
    |------------------------------->|   
    |                                | (※DH鍵合意プロトコル)
    |              <KEXDH_REPLY(31)> |
    |<-------------------------------|
    |................................|.............................
    |                                |
    |         <NEWKEYS(21)>          |
    |<==============================>| (※送信順序不定)
    |                                |
メッセージ番号
SSH_MSG_KEXDH_INIT30
SSH_MSG_KEXDH_REPLY31

KEXDH_INIT (Client -> Server)

クライアントは最初 KEXDH_INIT を送信します。パラメータは以下です。

e
クライアントDH公開鍵

KEXDH_REPLY (Server -> Client)

DH 初期化の後、サーバは KEX_KEXDH_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 実装での該当するコードです。

  class DH
         :
        (略) 
         :
    #
    # :section: digester / group / dh
    #
    private
    property    :initialize

    property_ro :digester, 'OpenSSL::Digest.const_get(initialize[:digester])'
    forward    [:digest] => :digester

    property_ro :groups,   'SSH2.config.oakley_group'
    property_ro :group,    'groups[initialize[:group]]'

    property_ro :dh,       'OpenSSL::PKey::DH.new'

    #
    # :section: protocol framework
    #
    private
    def ready
      dh.g = group[:g]
      dh.p = OpenSSL::BN.new(*group[:p])

      dh.generate_key! until (0...dh.p).include?(dh.pub_key) &&
                             dh.pub_key.to_i.count_bit > 1
    end

    public
    def key_exchange(service)
      @service = service

      ready ; agree

      [k, h]
    end

    #
    # :section: protocol parameters
    #
    private
    property_ro :v_c,                  'client.version'
    property_ro :v_s,                  'server.version'
    property_ro :i_c,                  'client[:kexinit].dump'
    property_ro :i_s,                  'server[:kexinit].dump'
    property_ro :k_s,                  'host_key.dump'
    property_ro :f,                    'dh.pub_key'

    property_ro :shared_secret,        'dh.compute_key(e)'
    property_ro :binary_shared_secret, 'OpenSSL::BN.new(shared_secret, 2)'
    property_ro :k,                    'encode(:mpint, binary_shared_secret)'

    property_ro :h,                    'digest(h0)'
    property_ro :s,                    'host_key.dumped_sign(h)'

    #
    # :section: DH Key Agreement
    #
    private
    def agree
      send_message :kexdh_reply,
            :host_key_and_certificates => k_s,
            :f                         => f,
            :signature_of_hash         => s
    end

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

    def h0
      pack([:string, v_c ],
           [:string, v_s ],
           [:string, i_c ],
           [:string, i_s ],
           [:string, k_s ],
           [:mpint , e   ],
           [:mpint , f   ]) + k
    end
  end

出力された鍵

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

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

2011年08月24日(水)

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

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

実装 phase1 もほぼ完了なので、SSH プロトコルの最下層・トランスポート層プロトコルについて、忘れないうちに、まとめていきます。

トランスポート層プロトコルは、主に RFC4253 の範囲になります。

位置プロトコル提供する機能
最下層トランスポート層プロトコル*1気密性(暗号化)、完全性、サーバ認証、圧縮<==ここ
中間層認証プロトコルクライアント認証(ユーザ認証)
最上層コネクションプロトコル多重化チャネル

トランスポート層(SSHプロトコル)の下

TCP です。サーバは通常 22 番ポートを listen します。*2

クライアント-サーバモデルです。*3

プロトコルバージョンの交換

最初はプロトコルバージョンの交換です。

サーバとクライアント双方がバージョン文字列(識別子)を送信し合います。(※送信順序不定)

  <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
KEXdiffie-hellman-group-exchange-sha256
diffie-hellman-group-exchange-sha1
diffie-hellman-group14-sha1REQUIRED
diffie-hellman-group1-sha1REQUIRED
ホスト鍵ssh-dssREQUIRED
ssh-rsaRECOMMENDED
対称暗号aes128-cbcRECOMMENDED
aes256-cbc
aes192-cbc
blowfish-cbc
cast128-cbc
3des-cbcREQUIRED
MAC hmac-sha1REQUIRED
hmac-sha1-96RECOMMENDED
hmac-md5
hmac-md5-96
圧縮noneREQUIRED
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 で動作させる

| 19:13 | SSHサーバを作る(その9) Ruby1.8 で動作させる - Going My Ruby Way を含むブックマーク はてなブックマーク - SSHサーバを作る(その9) Ruby1.8 で動作させる - Going My Ruby Way SSHサーバを作る(その9) Ruby1.8 で動作させる - Going My Ruby Way のブックマークコメント

トランスポート層の実装もほぼ終わり、そろそろ 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

以下の方法でも特異メソッドは定義できます。

ただし、classdef の外のスコープのオブジェクトを参照したい場合は上のようにします。

 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(標準クラスの拡張)に残し、代替ライブラリも削除しました。(スッキリ)


*1OSI 7階層モデルでいうトランスポート層ではありません

*2:gmrw-ssh2-server ではデフォルトで 50022 番ポートを listen します

*3:gmrw-ssh2-server は、標準ライブラリの gserver のサービスとして実装されてます。サービスのインスタンスは 1 つの確立コネクションに対して 1 つです

*4RFC には、古いバージョン(SSH-1.0)との互換性や、バージョン文字列以外の情報の送信方法などが記載されていますが、実装には関係ないので反映させてません

*5:環境にインストールされる OpenSSL、Zlib、および関連ライブラリ依存になります

*6SSH仕様上のREQUIRED,RECOMMENDED はすべて含みます

*7:OpenSSH_5.8p1 Debian-1ubuntu3 クライアントがデフォルトで選択するもの

2011年08月23日(火)

OpenSSL::Cipher の名前

| 23:07 | OpenSSL::Cipher の名前 - Going My Ruby Way を含むブックマーク はてなブックマーク - OpenSSL::Cipher の名前 - Going My Ruby Way OpenSSL::Cipher の名前 - Going My Ruby Way のブックマークコメント

OpenSSL::Cipher.new の引数に受け付ける文字列と、Cipherの名前とブロックサイズ、IV 長、鍵長を一覧にして表示するスクリプトです。

require 'openssl'

FORMAT = "  %-14s: %-14s: %6s %6s %6s  "

puts head = FORMAT % [:name, :cipher, :block, :iv, :key]
puts "-" * head.length

class OpenSSL::Cipher
  ciphers.sort.each do |cipher_name|
    new(cipher_name).instance_eval do
      puts FORMAT % [cipher_name, name, block_size, iv_len, key_len]
    end
  end
end

続きを読む

2011年08月21日(日)

OpenSSL::Cipherで暗号化/復号をする簡単なスクリプト

| 21:19 | OpenSSL::Cipherで暗号化/復号をする簡単なスクリプト - Going My Ruby Way を含むブックマーク はてなブックマーク - OpenSSL::Cipherで暗号化/復号をする簡単なスクリプト - Going My Ruby Way OpenSSL::Cipherで暗号化/復号をする簡単なスクリプト - Going My Ruby Way のブックマークコメント

OpenSSL::Cipher で暗号化/復号をする簡単なスクリプト

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-

require 'openssl'

algorithm = 'aes-128-cbc'
c1 = OpenSSL::Cipher.new(algorithm)
c2 = OpenSSL::Cipher.new(algorithm)

c1.encrypt
c2.decrypt

c1.iv  = c2.iv  = OpenSSL::Random.random_bytes(c1.iv_len)
c1.key = c2.key = OpenSSL::Random.random_bytes(c1.key_len)
c1.padding = c2.padding = 0

p s = [*("A".."Z")].join[0, c1.block_size]
puts "-" * 8
p es = c1.update(s)
p ds = c2.update(es)
puts "-" * 8
p es = c1.update(s)
p ds = c2.update(es)


# vim:set ts=2 sw=2 et fenc=utf-8:

2011年08月17日(水)

SSH サーバを作る(その8) バージョン交換とバイナリパケット受信

| 20:40 | SSH サーバを作る(その8) バージョン交換とバイナリパケット受信 - Going My Ruby Way を含むブックマーク はてなブックマーク - SSH サーバを作る(その8) バージョン交換とバイナリパケット受信 - Going My Ruby Way SSH サーバを作る(その8) バージョン交換とバイナリパケット受信 - Going My Ruby Way のブックマークコメント

SSH プロトコルのトランスポート層の実装を進めています。

以下、概要のメモです。

SSH プロトコルの詳細は RFC(4250 〜 4254 等)を参照してください。

----

バージョン交換

SSH プロトコルの最初はバージョン交換です。

  SSH-2.0-xxxxxxxxxx yyyyyyyy CR LF  # yyyyyyyy の部分は省略可

という文字列でサーバとクライアントは互いのバージョンを確認し合います。

クライアントが SSH-2.0 でない場合はコネクションを切断します。(SSH-1.0 には対応しません)

実装では、Server::VersionString というクラスを作成して対応しました。

バイナリパケット

バージョン交換が終わると、バイナリパケットでのやり取りになります。

最初は、SSH_MSG_KEXINIT というメッセージをクライアントと交換して、アルゴリズムのネゴシエーションをしなければなりません。

実装としては、

  • パケットの受信とペイロードの取り出し
  • メッセージのデコード

をまずしないといけません。

reader / writer

Server::Service クラスは、サーバ側(ローカル)とクライアント側(リモート)を Server::Writer と Server::Reader クラスに任せます。

これらは、Server::Side クラスを基底クラスにしています。

Server::Side は、SSH プロトコル通信のエンドが持つ基本的な機能(コネクションの read/write、シーケンスカウント、ブロックサイズの計算、cipher などの保管など)を持ちます。

Server::Writer と Server::Reader の役割は、送信/受信の手続きと、送受信メッセージの保存です。

Server::Reader でパケットの受信とペイロードの取り出しを行なっています。

state

Server::Service は Server::State クラスにプロトコルの状態を持たせます。

再割り当て可能なメッセージ番号などはプロトコル状態によって変わるので、Server::State が持つメッセージカタログを参照して対応付けをします。

(実装では、Server::Reader は、Server::Service を介して、Server::State のメッセージカタログを参照する)

また、Server::State は Server::Reader のオブザーバになっていて、受信イベントを監視しています。

メッセージ

メッセージは Message.def_message で定義します。

以下のような感じです。

module GMRW::SSH2::Message
  def_message :kexinit, [
    [ :byte,      :type                                    , 20 ],
    [ 16,         :cookie                                       ],
    [ :namelist,  :kex_algorithms                               ],
    [ :namelist,  :server_host_key_algorithms                   ],
    [ :namelist,  :encryption_algorithms_client_to_server       ],
    [ :namelist,  :encryption_algorithms_server_to_client       ],
    [ :namelist,  :mac_algorithms_client_to_server              ],
    [ :namelist,  :mac_algorithms_server_to_client              ],
    [ :namelist,  :compression_algorithms_client_to_server      ],
    [ :namelist,  :compression_algorithms_server_to_client      ],
    [ :namelist,  :languages_client_to_server                   ],
    [ :namelist,  :languages_server_to_client                   ],
    [ :boolean,   :first_kex_packet_follows                     ],
    [ :uint32,    :reserved                                     ],
  ]
end

フィールドのデコード/エンコード、チェックなどはヘルパーである。Message::Fields のモジュール関数で行ないます。

SSH の各データ型は 以下のように表しています。

SSHの型Ruby制限
boolean true または false
byte Integer 範囲は(0...(1<< 8))
uint32 Integer 範囲は(0...(1<<32))
uint64 Integer 範囲は(0...(1<<64))
mpint Integer OpenSSL::BN
string String
name-list Array 要素は String (空でなく、"," は含まない)
byte[n] Array 要素は byte、長さは n

(2011/08/21 追記)

name-listnameRFC 4250 (4.6. Names) にもう少し詳しい規約が載ってました。

  • 長さは 64 バイト以下
  • 使用する文字は、US-ASCII グラフ文字(21h .. 7eh)で、「,」を除く
  • 「@」についてはローカル拡張の場合の以下の形式のみ(1個だけ)使用できる
   名前 @ ローカルドメイン名

---

現在実装しているのは、こんなところです。

2011年08月15日(月)

SSH サーバを作る (その7) リポジトリのブランチ

| 23:19 | SSH サーバを作る (その7) リポジトリのブランチ - Going My Ruby Way を含むブックマーク はてなブックマーク - SSH サーバを作る (その7) リポジトリのブランチ - Going My Ruby Way SSH サーバを作る (その7) リポジトリのブランチ - Going My Ruby Way のブックマークコメント

GitHub リポジトリに、フェーズ 0 完了の "phase0" タグをつけました。

フェーズ1 はブランチ phase1 にコミットしていきます。

フェーズ1 では、バージョン交換まで実装しました。

SSH サーバを作る (その6) logger仕様変更

| 23:12 | SSH サーバを作る (その6) logger仕様変更 - Going My Ruby Way を含むブックマーク はてなブックマーク - SSH サーバを作る (その6) logger仕様変更 - Going My Ruby Way SSH サーバを作る (その6) logger仕様変更 - Going My Ruby Way のブックマークコメント

「挑戦:SSHサーバを作る」です。

logger 仕様の変更しました。変更点は以下です。

(とりあえずのメモです。そのうち、ちゃんとまとめないと。。。)

------

log メソッドは以下のようにしました。

 log(severity=nil, *message) {|l, *msgs| ... }

severity にはメッセージの重要度を指定します。falsy な値を与えると前回と同じ重要度を選択します。省略時は nil です。

messages はログメッセージです。内部で format[] メソッドの引数に与えられます。

  format[*messgaes]

format.[] の戻り値が内部で private メソッドの write に与えられます。

デフォルトの format は以下のようになっています。

  proc {|*msgs| msgs.map(&:to_s).join(": ") }

ブロックが与えられた場合、write の呼び出しに優先して、ブロックがコールされます。ブロックの最初の引数は、logger 自身です。2番目以降は messages です。

  {|logger, *message| ... }

write によるログ出力も、ブロック呼び出しも、threshold による抑止対象になります。メッセージの重要度が threshold より低い場合処理が行なわれません。

また、

  logger << message

は、

  logger.log(nil, message)

と同じです。

2011年08月10日(水)

Python との比較 (その1)

| 09:38 | Python との比較 (その1) - Going My Ruby Way を含むブックマーク はてなブックマーク - Python との比較 (その1) - Going My Ruby Way Python との比較 (その1) - Going My Ruby Way のブックマークコメント

Python との比較です。

。。。というか Pyhton をちょっと勉強したくて、Ruby をベースにして、自分に差分プログラミングです。

インタプリタと環境

インタプリタ起動
$ python
Python 2.7.1+ (r271:86832, Apr 11 2011, 18:13:53) 
[GCC 4.5.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> 
$ ruby
               # 何も出ない...。寡黙だ。

ruby の場合、インタプリタとして irb が付属します。

$ irb
>>
インタプリタの 変数 _

インタプリタの 変数 _ には直前の評価値が保存されている。

>>> 10
10
>>> _
10

irb でも同じ。

>> 10
=> 10
>> _
=> 10
ワンライナーで hello, world

オプションは -e でなく '-c'。print の出力は改行される。

$ python -c 'print "hello, world"'
hello, world
$ ruby -e 'puts "hello, world"'  # ruby の print は文字列そのまま出力
hello, world
$ perl -e 'print "hello, world\n"'  # perl の場合
hello, world

続きを読む

2011年08月09日(火)

Module#property_*

| 23:12 | Module#property_* - Going My Ruby Way を含むブックマーク はてなブックマーク - Module#property_* - Going My Ruby Way Module#property_* - Going My Ruby Way のブックマークコメント

gmrw-ssh2-server では、Module#property_* として以下を設定しています。

  • property_ro
  • property_rw
  • property_roa
  • property_rwa

gmrw-ssh2-server のソース

  class C
    property_rwa :foo, '10' #=> デフォルト値は文字列で渡す
  end

  c = C.new
  p c.foo    # => 10
  c.foo(20)
  p.c.foo    # => 20

----

違いは以下の通りです。(引数で :foo を指定した場合)

メソッドデフォルト設定値の参照引数による値の設定「=」による値の設定
attr_reader不可foo不可不可
attr_writer不可不可不可foo = 10
attr_accessor不可foo不可foo = 10
property_rofoo不可不可
property_rwfoofoo(10)不可
property_roafoo不可foo = 10
property_rwafoofoo(10)foo = 10

property は property_rwa のエイリアスです。

続きを読む

mixin

| 22:59 | mixin - Going My Ruby Way を含むブックマーク はてなブックマーク - mixin - Going My Ruby Way mixin - Going My Ruby Way のブックマークコメント

gmrw-ssh2-server では、標準クラスへの mixin は以下のようにしています。

require 'gmrw/extension/extension'

module GMRW::Extension
  mixin Array do
    def to_hash
      Hash[ *flatten(1) ]
    end

    def mapping(*keys)
      keys.empty? ? mapping_by_index : mapping_by_name(*keys)
    end

    def mapping_by_name(*names)
      names.zip(self).to_hash
    end

    def mapping_by_index
      (0...count).zip(self).to_hash
    end
  end
end

GMRW::Extension#.mixin は、引数のクラス(orモジュール)にブロックから動的生成したモジュールを include させます。

クラス(orモジュール)に既に重複するメソッドがある場合は例外(RuntimeError)を発生させます。

----

(標準クラスを拡張するので)重複チェックがしたかったのと、include するためにモジュール(定数や名前)を増やしたくなかったのでこうしました。

include したモジュールは reopen されることはありません。(と、思っているが正しい???)

(2011/08/10 [追記]) 。。。正しくなかったです。reopen するには以下の方法があります。

>> require 'gmrw/extension/array'
=> true
>> Array.ancestors
=> [Array, #<Module:0x0000000223eea0>, Enumerable, Object, PP::ObjectMixin, Kernel, BasicObject]
>> M = Array.ancestors[1]
=> M
>> Array.ancestors  # モジュールが定数 M に入れられたので名前付けされた
=> [Array, M, Enumerable, Object, PP::ObjectMixin, Kernel, BasicObject]
>> p M.instance_methods
[:to_hash, :mapping, :mapping_by_name, :mapping_by_index]
=> [:to_hash, :mapping, :mapping_by_name, :mapping_by_index]
>> module M
>>   # undef_method したりとか、いろいろいじくれる
>> end

----

似たメソッドに GMRW::Extension#.compatibility があります。

こちらは、重複するメソッドがある場合は、動的生成したモジュールからそのメソッドを削除します。(オーバーライドしません)

require 'gmrw/extension/extension'

module GMRW::Extension  #:nodoc:
  compatibility Object do
    def presence  #:nodoc:
      present? ? self : nil
    end
  end
end

メソッドがない場合だけ include させたクラス(orモジュール)にメソッド定義が引き継がれます。

FizzBuzz

| 22:49 | FizzBuzz - Going My Ruby Way を含むブックマーク はてなブックマーク - FizzBuzz - Going My Ruby Way FizzBuzz - Going My Ruby Way のブックマークコメント

ans = (1..100).map do |n|
  n % 15 == 0 ? 'FizzBuzz' :
  n %  3 == 0 ? 'Fizz'     :
  n %  5 == 0 ? 'Buzz'     : n
end

剰余「%」を使わない方法。

ans = (1..100).each_slice( 3).map {|a| a[ 2] = 'Fizz'     ; a }.flatten
              .each_slice( 5).map {|a| a[ 4] = 'Buzz'     ; a }.flatten
              .each_slice(15).map {|a| a[14] = 'FizzBuzz' ; a }.flatten[0,100]

---

こんくらいしか思い浮ばない。。。

OptionParser のメモ

| 22:27 | OptionParser のメモ - Going My Ruby Way を含むブックマーク はてなブックマーク - OptionParser のメモ - Going My Ruby Way OptionParser のメモ - Going My Ruby Way のブックマークコメント

先日調べたことを忘れないうちにメモしておきます。

----

OptionParser は以下のような感じで使えます。

#!/usr/bin/env ruby

require 'optparse'
require 'ostruct'
require 'pp'

options = OpenStruct.new

OptionParser.new do |opt|
  opt.on('-p PORT', '--port=PORT','port number') {|v| options.port    = v.to_i }
  opt.on('-d',                    'debug mode')  {|v| options.debug   = v      }
  opt.on(           '--verbose',  'verbose mode'){|v| options.verbose = v      }
  opt.on('-l',      '--[no-]log', 'logging')     {|v| options.logging = v      }
  opt.on('-i [IF]',               'interface')   {|v| options.interface = v    }
  opt.on('-b',      '--[no-]bar[=VAL]')          {|v| options.bar     = v      }

  opt.parse!
end

pp ARGV
pp options

# vim:set ts=2 sw=2 et fenc=UTF-8:

オプションが引数を取らない時

  opt.on('-a'             )  {|v| ... }  # v は true
  opt.on(          '--foo')  {|v| ... }  # v は true

オプションが引数を取る時

  opt.on('-a VAL'             )  {|v| ... }  # v は引数の値(String)
  opt.on(          '--foo=VAL')  {|v| ... }  # v は引数の値(String)

オプションが引数(省略可)を取る時

  opt.on('-a [VAL]'             )  {|v| ... }  # v は String か nil(省略時)
  opt.on(          '--foo[=VAL]')  {|v| ... }  # v は String か nil(省略時)

否定オプションが指定可能な時

  opt.on('--[no-]foo')  {|v| ... }  # v は true か false(否定時)

否定オプションが指定可能、かつ、引数(省略可)を取る時

  opt.on('--[no-]foo[=VAL]')  {|v| ... }  # v は true か false か String

ただし、実行時に否定と引数指定を同時にするとエラー

  $ prog.rb --no-foo=10    # => エラーになる

オプションの -h, --help はデフォルトでヘルプが表示される。

$ ruby example_optparse.rb --help
Usage: example_optparse [options]
    -p, --port=PORT                  port number
    -d                               debug mode
        --verbose                    verbose mode
    -l, --[no-]log                   logging
    -i [IF]                          interface
    -b, --[no-]bar[=VAL]

irb 用バナー

| 10:34 | irb 用バナー - Going My Ruby Way を含むブックマーク はてなブックマーク - irb 用バナー - Going My Ruby Way irb 用バナー - Going My Ruby Way のブックマークコメント

バナーを表示するスクリプトを GitHub にアップしました。

単体実行もできますが、irb の起動時のバナー用に作りました。

バナーの色は、ruby1.8 では「緑」、ruby1.9 では「赤」です。

また、“RUBY”の部分はスクロールしながら表示されます。

----

スクリーンショット

f:id:lnznt:20110809100740j:image

f:id:lnznt:20110809100741j:image

----

GitHubWiki にスクリーンショットを載せる方法が分かりませんでした。。。

2011年08月07日(日)

SSH サーバを作る (その5) 現時点のまとめ

| 23:50 | SSH サーバを作る (その5) 現時点のまとめ - Going My Ruby Way を含むブックマーク はてなブックマーク - SSH サーバを作る (その5) 現時点のまとめ - Going My Ruby Way SSH サーバを作る (その5) 現時点のまとめ - Going My Ruby Way のブックマークコメント

現時点のスクリプトは GitHub にコミットしました。

----

スクリプトの構成は以下のように変更しました。

gmrw-ssh2-server
   |--- lib
   |    |--- gmrw
   |    |    |--- extension
   |    |    |    |--- object.rb
   |    |    |    |--- array.rb
   |    |    |    |--- string.rb
   |    |    |    |--- extension.rb
   |    |    |    |--- module.rb
   |    |    |--- alternative
   |    |    |    |--- active_support.rb
   |    |    |    |--- active_support
   |    |    |    |    |--- forwardable.rb
   |    |    |    |    |--- object.rb
   |    |    |    |    |--- string.rb
   |    |    |    |    |--- object-compat.rb
   |    |    |    |    |--- nilclass.rb
   |    |    |--- ssh2
   |    |    |    |--- server.rb
   |    |    |    |--- server
   |    |    |    |    |--- constants.rb
   |    |    |    |    |--- gserver.rb
   |    |    |    |    |--- service.rb
   |    |    |--- utils
   |    |    |    |--- loggable.rb
   |--- bin
   |    |--- ssh2-server.rb
   |--- README.rdoc

ActiveSupport を使用する方針にしました。

ActiveSupport がない場合は代替スクリプトを使用します。(lib/gmrw/alternative/ 以下)

サーバを起動するスクリプトは bin/ssh2-server.rb に切り出しました。

その他、構成を変更しました。

(2011/08/09 [修正] compatibility.rb はスクリプトの構成から削除)

2011年08月06日(土)

SSH サーバを作る (その4) ログ出力 (3)

| 11:45 | SSH サーバを作る (その4) ログ出力 (3) - Going My Ruby Way を含むブックマーク はてなブックマーク - SSH サーバを作る (その4) ログ出力 (3) - Going My Ruby Way SSH サーバを作る (その4) ログ出力 (3) - Going My Ruby Way のブックマークコメント

「挑戦:SSHサーバを作る」です。

作った logger を Server::Service で使えるようにします。

続きを読む

また、日記改題

10:53 | また、日記改題  - Going My Ruby Way を含むブックマーク はてなブックマーク - また、日記改題  - Going My Ruby Way また、日記改題  - Going My Ruby Way のブックマークコメント

旧題「lnznt の Going My Ruby Way」

新題「Going My Ruby Way」

2011年08月05日(金)

SSHサーバを作る (その3) ログ出力 (2)

| 23:22 | SSHサーバを作る (その3) ログ出力 (2) - Going My Ruby Way を含むブックマーク はてなブックマーク - SSHサーバを作る (その3) ログ出力 (2) - Going My Ruby Way SSHサーバを作る (その3) ログ出力 (2) - Going My Ruby Way のブックマークコメント

「挑戦:SSHサーバを作る」です。

logger を作ろうと思います。

続きを読む

2011年08月03日(水)

SSHサーバを作る (その2) ログ出力 (1)

| 21:11 | SSHサーバを作る (その2) ログ出力 (1) - Going My Ruby Way を含むブックマーク はてなブックマーク - SSHサーバを作る (その2) ログ出力 (1) - Going My Ruby Way SSHサーバを作る (その2) ログ出力 (1) - Going My Ruby Way のブックマークコメント

「挑戦: SSHサーバを作る」です。

ログ出力機構を作ろうと思います。

Server::Loggable

何らかのログ出力を行うオブジェクト(=logger) にログ出力を委譲する形にします。

委譲の部分をモジュールにまとめます。委譲するメソッドは log です。

gmrw/ssh2/server/logger.rb (後で追加予定)

#!/usr/bin/env ruby
# -*- coding: UTF-8 -*-
require 'gmrw/utils/null'
require 'gmrw/utils/property'
require 'gmrw/utils/forwardable'
require 'gmrw/ssh2/server/constants'

module GMRW::SSH2::Server::Loggable
  property :logger, :null
  delegate [:log] => :logger
end

property は アクセサ定義 property - Going My Ruby Way - Rubyist で書いたものを移植しました。

null は NULL オブジェクトパターンを実現するものです。

delegate は標準ライブラリの ForwardableRuby1.9 から使えます*1。def_delegators と同等のものです。Ruby1.8 でも使えるようにラッパを作りました。

以下、それぞれのスクリプトです。これらは gmrw/utils/ に置きました。

続きを読む

*1:ですが、ドキュメントには見当たらないようです