OpenSSL::Cipher::Cipher#padding= でパディング処理の有無を制御できる。では実際にどのようなパディング処理が行われているのだろう?
暗号化時にはパディングを有効にして、復号時に無効にすれば確認できる。
# Ruby 1.8.7 or 1.9
require 'openssl'
def enc_dec_base(operation)
password = 'fixed password'
lambda do | cipher, data, padding |
c = OpenSSL::Cipher::Cipher.new(cipher)
c.send(operation)
c.padding = padding
c.pkcs5_keyivgen(password)
c.update(data) + c.final
end
end
encrypt = enc_dec_base(:encrypt)
decrypt = enc_dec_base(:decrypt)
cipher = 'aes-256-cbc'
msg = 'ABCDEFGHIJKLMNOP' # 16bytes = AES block size
(1..msg.size).each do |len|
plain = msg[0, len]
encrypted = encrypt[cipher, plain, 1]
decrypted = decrypt[cipher, encrypted, 0] # disable padding
puts "original message: #{plain}"
p plain.bytes.to_a
p decrypted.bytes.to_a
puts
end
結果、PKCS#5 の PBES1, PBES2 と同じく RFC 1423 で定義されたパディング処理を行っていることが分かった。このパディングアルゴリズムは一般には PKCS#5 Padding などと呼ばれているらしい。
PHP の mcrypt ライブラリと相互運用する場合は、PHP 側で適切に処理する必要がある。PHP Manual の User Contributed Notes に実装例を見つけた:duerra_NOT_THIS_ at pushitlive dot net。
一応実行結果も載せておく。見事に階段状になっていてちょっと面白い。
(続きを読む…)
PKCS#5 section 4.1 より引用:
For instance, suppose two legitimate parties exchange a encrypted message, where the encryption key is an 80-bit key derived from a shared password with some salt. An opponent could take the salt from that encryption and provide it to one of the parties as though it were for a 40-bit key. If the party reveals the result of decryption with the 40-bit key, the opponent may be able to solve for the 40-bit key. In the case that 40-bit key is the first half of the 80-bit key, the opponent can then readily solve for the remaining 40 bits of the 80-bit key.
何回も読み直してようやく理解できた、と思う。
パスワードを元に鍵を導出する関数:PBKDF は、Nビットの鍵を得るために、パスワードとソルトから作ったビット列を先頭からNビットだけ切り出して鍵とする。つまり同じパスワードとソルトから異なる長さの鍵を生成した場合、短い方は長い方の先頭部分と一致する。これは必ずしも鍵導出関数一般の性質とは言えないのでは無いかと思うが、少なくとも PBKDF1(PKCS#5 v1.5)、PBKDF2(PKCS#5 v2.0)、EVP_BytesToKeys、いずれにも当てはまる。
それを前提として、引用文の例は次のようなものである:今「80ビットの鍵で暗号化された暗号文」と、その鍵を導出するのに使ったソルトとが公開されていたとする。攻撃者は両者を「40ビットの鍵で暗号化した暗号文」およびそのソルトだと偽って正規の復号者に渡す。だまされた復号者はソルトと正規のパスワードから40ビットの鍵を導出して復号処理を行う(この鍵は本来の80ビットの鍵の前半40ビットである)。結果、「元の暗号文を40ビットの鍵で復号処理したバイナリ」が作り出されることになる。もし攻撃者がこのバイナリを手に入れた場合、高々40ビットの鍵なので、ブルートフォース等で鍵を特定することは可能である。すると残る鍵は後半40ビットだけなので、これも同様に破ることができるようになる。つまり「80ビットの鍵」が「40ビットの鍵2つ」に分割されてしまった!しかもこの操作は(鍵長とアルゴリズムの都合さえ合えば)何度も繰り返すことができる。一般に N ビットの鍵に対して M 回繰り返せば、 N / (M+1) + M ビットにまで鍵を弱めることができる。こういった攻撃を防ぐために、PKCS#5 ではソルトの中に非ランダムな構造を持たせ、鍵の長さや用途(暗号化かメッセージ認証か)等を含める方法を提案している。
ただ、常に対策が必要なほどの脅威だとは、PKCS#5 には書いていない。”there is no concern about interactions between multiple uses of the same key” なら、ランダムに選ぶだけで良いとある(ただし長さは8ビット以上)。要するに固定的な用途で、単発的に用いるだけなら問題にしなくて良い、ということになるだろうか。
(実際のところ、上の例自体はかなり特殊な状況設定だと思う。”If the party reveals the result of decryption with the 40-bit key” なんてサラリと書いてあるが、そんなことが起こり得る状況なら元の平文が漏れる危険性の方が高いだろう)。
GitHubにて、PKCS#5 の PBKDF2 を Ruby で実装された方がいたので fork させていただいた。
tkyk’s pbkdf2-ruby at master – GitHub
とりあえず Ruby 1.9 対応を行った。テスト(spec)は主にRFC 3962に基づいているようなので、併せて詳細を調べていく予定。
結論から書くと、pkcs5_keyivgen に相当する処理を pure Ruby で実装すれば次のようになる。
def evp_bytes_to_key(digest, key_len, iv_len, pass, salt="", count=2048)
total_len = key_len + iv_len
return_buf = ""
digest_buf = ""
until return_buf.size >= total_len
digest_buf < < pass << salt
count.times { digest_buf = digest.digest(digest_buf) }
return_buf << digest_buf
end
return return_buf[0,key_len], return_buf[key_len,iv_len]
end
まず Ruby のソースを find & grep したところ、pkcs5_keyivgen の実装は ext/openssl/ossl_cipher.c にあることが分かった。そのコードを注意深く読むと、実際の処理は openssl の EVP_BytesToKey という関数で行われていることが分かった。そこで次に openssl のソースを同様に調査したところ、crypto/evp/evp_key.c に実装があることが分かった。また OpenSSL のサイトで概要を記したドキュメントも見つけた。これら手がかりから同等の処理を組み立てたのが上記の evp_bytes_to_key である。
その調査の過程で、pkcs5_keyivgen という名前は PKCS#5 という、「パスワードに基づく暗号化」を規定した標準規格に由来するということが分かった。しかし現在の Ruby の実装(つまり EVP_BytesToKey に基づく実装)は PKCS#5 の古いバージョン(v1.5)に、限定的に適合しているだけだということも分かった。PKCS#5 の最新のバージョンは2.0であり、Ruby 1.9 のソースコードにも、前出のドキュメントにも、今後は v2.0 を使うように、と注意書きがしてある。
その点から言うと先日のエントリの実行例は、PKCS#5 v2.0 は勿論のこと、v1.5 にさえ適合していない。なぜなら v1.5 ではハッシュ関数の出力長を超える長さの鍵を導出する方法は規定されていないからだ。その部分は EVP_BytesToKey の独自拡張という扱いになる。
この妥協によって、どのような用途でどのような脅威が生じ得るのかは、今の私にはちょっと計り難い。少なくとも、ビットが反転できるとか、暗号文が毎回同じになるとか、そういう分かりやすい現象は出てこない。しかし、それでも推奨されている方法があるのなら、できるだけそちらを使いたい。
ということで今度は PKCS#5 v2.0、すなわち RPC 2898 について調べていくことにする。
参考:
- EVP_BytesToKeyをRubyで実装する – lambda {|diary| lambda { diary.succ! } }.call(hatena)
- 私と同じく Ruby で EVP_BytesToKey を実装している。こちらの方がC言語からのベタ移植に近い。
- RFC 2898 – PKCS #5: Password-Based Cryptography Specification Version 2.0
- PKCS#5 はもとは RSA 社の規格だったらしいが、現在は RFC にもなっている。和訳は、今のところは存在しないようだ。あと v1.5 の仕様も探したのだが、見つけられなかった。どうやら v2.0 の中に含まれているらしい。
最後に Ruby 実装版 EVP_BytesToKey の動作確認の方法を載せておく。オリジナルの pkcs5_keyivgen が生成した鍵と初期化ベクトルは見ることができないので、暗号文が全く同じになることで間接的に確認する。
(続きを読む…)
Ruby で共通鍵暗号を使用する場合、openssl ライブラリを使用するのが簡単らしい。
require 'openssl'
def encrypt(cipher, msg, pass)
salt = OpenSSL::Random.random_bytes(8)
enc = OpenSSL::Cipher::Cipher.new(cipher)
enc.encrypt
enc.pkcs5_keyivgen(pass, salt)
return enc.update(msg) + enc.final, salt
end
def decrypt(cipher, crypt_msg, pass, salt)
dec = OpenSSL::Cipher::Cipher.new(cipher)
dec.decrypt
dec.pkcs5_keyivgen(pass, salt)
dec.update(crypt_msg) + dec.final
end
使用例:
cipher = 'aes-256-cbc' #鍵長256ビットのAES
msg = "秘密のメッセージ"
pass = "PASSWORD"
crypt, salt = encrypt(cipher, msg, pass)
p crypt.unpack("H*").join
p salt.unpack("H*").join
msg2 = decrypt(cipher, crypt, pass, salt)
p msg2
PHP + mcrypt の場合と違って、鍵や初期化ベクトルを直接設定する必要はない(不可能というわけでは無い)。代わりに pkcs5_keyivgen メソッドを使って、パスワードとソルトから鍵と初期化ベクトルを生成する。
パスワードとソルトが揃っていないと復号できないので、ソルトは暗号文とともに転送・保存する必要がある。それ故にソルトは、毎回異なる安全な乱数でなければならない。しかし、ここが重要なところだが、秘密にしておく必要はない。初期化ベクトルと違って、本当に秘密にしておく必要は無い。なぜならソルトだけでは鍵も初期化ベクトルも作り出せないし、ソルトを改竄しても平文を意図的に改竄することはできないからだ。
とても優れた仕組みだと思う。ただしその仕組みは、結局のところ pkcs5_keyivgen というただ一つのメソッドに立脚しているので、次はこのメソッドの詳細について調べてみたい。
参考:
- ウノウラボ Unoh Labs: rubyで手軽に暗号化文字列やハッシュ値を生成
- 参考にはなった…が、ソルトについて触れていないとか、パスワードのことをソルト(solt)と呼んでいるとか、hashの戻り値が文字列になっていないとか、突っ込みどころがいろいろある…。あと、2007年の記事なので仕方ないかもだが、Ruby 1.9 では動かない。
- class OpenSSL::Cipher::Cipher
- Ruby 1.9.1 リファレンスマニュアル OpenSSL::Cipher::Cipher クラス
CBC モードにおいて、初期化ベクトルのビットが反転すると、平文の最初のブロックの対応するビットも反転する。だから初期化ベクトルはそのまま転送すべきではない。
…しかしよく考えたら、CTR モードなど、平文と直接 XOR をとるモードでは、暗号文のビットを反転させればそのまま対応する平文のビットが反転してしまう。CBC モードと違って全文に対して可能な上、本質的に避けようがない。
「ブロック暗号 カウンターモード OR CTR ビット反転」でググってみると、当然、その欠点を指摘する情報がいろいろと見つかった。特にGoogleブック検索で引っかかった『C/C++セキュアプログラミングクックブック VOLUME2』の中の一節が分かりやすい。
暗号文のビットを反転させると対応する平文のビットが反転するので、平文のビットを反転させるのは非常に簡単です(この問題はすべてのストリーム暗号モードに共通します)。他の暗号化アルゴリズムと同様に、セキュリティを保つにはメッセージの完全性検査が必須です。
この欠点は、暗号で守るべき「機密性」というより、MACなどで守るべき「真正性(完全性)」の問題だから、CTR モードを責めるのは筋違いということか。同著によれば CTR と完全性検査を組み合わせた CWC や CCM といったモードもあるらしい。
この本おもしろそうだなあ…買ってもまず間違いなく読めないだろうけど。『新版暗号技術入門 秘密の国のアリス』は、全体としては素晴らしいのだが、ブロック暗号のモードについての説明だけは物足りなかった。
先日の続き。
実際に初期化ベクトルが問題になった事例として、 TLS 1.0 に対する CBC 攻撃というのがあったようだ。この問題は TLS 1.1(RFC4346) で対処された。以下、RFC4346 の中で関連する箇所:
この問題について指摘する OpenSSL サイト内の情報も見つけた。
既存の「暗号文」の一部を次のIVとして使用してしまうことが問題の本質らしい。しかし『Note that for most application protocols such attacks are not really feasible.』とあるので、実際に攻撃が発生して被害が出たことはなかったのではないかと思われる。
なお、Wikipedia のTLSについての説明は間違っている。CBC 攻撃に対する耐性が追加されたのは 1.1 だし、AESが正式に追加されたのは 1.2 である(他にもいろいろ間違っている気がする)。もう少し調べたら修正する、予定。
追記:修正しておいた。AESについてのくだりは迷ったが、TLSAES(RFC 3268)に言及しないなら1.1の方が相応しいと思われたので、そのまま残した。結果としてTLS 1.2 -> 1.1に内容を移動する形の修正になった。
ブルートフォースアタックからの防衛ツール BlockHosts 導入手順。環境は CentOS 4。
インストール
rpmで導入する場合
wget http://www.aczoom.com/tools/blockhosts/BlockHosts-2.4.0-1.noarch.rpm
sudo rpm -Uvh BlockHosts-2.4.0-1.noarch.rpm
もしくはソースからインストールする場合
wget http://www.aczoom.com/tools/blockhosts/BlockHosts-2.4.0.tar.gz
tar xvzf BlockHosts-2.4.0.tar.gz
cd BlockHosts-2.4.0
sudo python setup.py install
どちらの方法を使っても、設定ファイルは /etc/blockhosts.cfg にインストールされるし、ログローテーションのための設定ファイル /etc/logrotate.d/blockhosts もインストールされる。
/etc/blockhosts.cfg の設定
まずは最低限の設定を行う。CentOS ならほとんどの場合はデフォルトで良いと思うが、
HOSTS_BLOCKFILE = "/etc/hosts.allow"
WHITELIST = [ "127.0.0.1", "192\.168\.2\..*", ]
LOGFILES = [ "/var/log/secure", ]
このあたりは明示的に設定しておいた方が良いと思う。WHITELIST に追加する設定は自分の環境にあわせて。
/etc/hosts.allow に設定追加
こちらもまずは最低限の情報を書いて、動作確認をする。
#---- BlockHosts Additions
#---- BlockHosts Additions
この状態で以下のコマンドを実行し、正しく解析結果が表示されることを確認。
sudo blockhosts.py --verbose --dry-run
次に –dry-run オプションを外し、/etc/hosts.allow の書き換えが正しく行われることを確認する(BlockHosts Additions の間に情報が書き込まれる)。
sudo blockhosts.py --verbose
最後に /etc/hosts.allow の末尾に blockhosts.py の起動設定を追加する。以下は一例
sshd: ALL: spawn /usr/bin/blockhosts.py --verbose --echo "%c-%s" >> /var/log/blockhosts.log 2>&1 : allow
その後の設定
ほとんどの設定は /etc/blockhosts.cfg の中でも、blockhosts.py のコマンドラインオプションとしても(つまり /etc/hosts.allow の中でも)設定できる。詳しくは blockhosts.cfg の中のコメントや blockhosts.py –help を参照。
試していないが、「ipblock」の設定を行うことで、TCP/IP レベルでのブロックも行えるようだ。ip コマンドで無効なルートを設定するか、iptables でフィルタリングするか選べるらしい。blockhosts.cfg より:
#IPBLOCK = "" # (default)
#IPBLOCK = "ip route" # or use full path "/sbin/ip route"
#IPBLOCK = "iptables" # or use full path "/sbin/iptables"
# "ip route": Do TCP/IP blocking using route commands to setup null-routes.
# ip route add via 127.0.0.1
# "iptables": Do TCP/IP blocking, using iptables packet filtering.
# iptables --append blockhosts --source -j DROP
最初に決めておくこと
以下の情報は暗号化においても復号においても必要になる。全てが一致しなければ復号はできない。
- 鍵(以下 KEY)
- 暗号アルゴリズム(以下 CIPHER) … (1)
- ブロック暗号のモード(以下 MODE)… (2)
暗号化
KEY, CIPHER, MODE を用いてメッセージ $msg を暗号化する関数 encrypt は次のようになる(わざと冗長な書き方をしている)。
function encrypt($msg)
{
//初期化ベクトルを生成
$ivSize = mcrypt_get_iv_size(CIPHER, MODE);
$iv = mcrypt_create_iv($ivSize, MCRYPT_DEV_URANDOM);
$dummyIV = str_repeat("x", $ivSize);
//メッセージの暗号化 ... (3)
$cryptMsg = mcrypt_encrypt(CIPHER, KEY, base64_encode($msg), MODE, $iv);
//初期化ベクトルの暗号化 ... (4)
$cryptIV = mcrypt_encrypt(CIPHER, KEY, base64_encode($iv), MODE, $dummyIV);
return array($cryptMsg, $cryptIV);
}
戻り値は次の2要素からなる配列である。
- 暗号化されたメッセージ
- 暗号化された初期化ベクトル
この2つが揃っていないと復号できないので、必ずペアで保存・転送すること。
復号
暗号化されたメッセージ $cryptMsg と暗号化された初期化ベクトル $cryptIV を KEY, CIPHER, MODE で復号する関数 decrypt は次のようになる。
function decrypt($cryptMsg, $cryptIV)
{
//ダミーの初期化ベクトルを生成
$ivSize = mcrypt_get_iv_size(CIPHER, MODE);
$dummyIV = str_repeat("x", $ivSize);
//初期化ベクトルの復号
$iv = _decryptSupport($cryptIV, $dummyIV);
//メッセージの復号
$msg = _decryptSupport($cryptMsg, $iv);
return $msg;
}
function _decryptSupport($cryptMsg, $iv)
{
//復号してNULLバイトを取り除いてbase64デコード ... (5)
return base64_decode(rtrim(
mcrypt_decrypt(CIPHER, KEY, $cryptMsg, MODE, $iv), "\0"));
}
細かい説明
- (1) 暗号アルゴリズム
- 使用できる暗号アルゴリズムは libmcrypt のバージョンに依存するので、関数 mcrypt_list_algorithms で一覧表示して確認しておくこと。PHP Manual に定数として載っているもの(MCRYPT_RIJNDAEL_128 など)はあまり当てにならないので、文字列で指定した方が良い。どのアルゴリズムを使うべきか分からなければ「AES」を使えば良い(…と『暗号技術入門』に書いてあった)。AES を使用したい場合は rijndael-128 を指定する。rijndael-192 や rijndael-256 はブロック長が異なるため、「AES」の規格には合致しないので注意。
- (2) ブロック暗号のモード
-
これも PHP 定数で指定するよりは mcrypt_list_modes で一覧表示して文字列として指定した方が良い。どのモードを使うべきか分からなければ「CBCモード」か「CTRモード」を使えば良い(…とこれまた『暗号技術入門』に書いてあった)。すなわち cbc か ctr を指定する。
- (3) 暗号化の際の base64 エンコード
- 暗号化の際に NULL でパディングされる場合があるので、事前に base64 エンコードしておく。
- (4) 初期化ベクトルの暗号化
- 初期化ベクトルの暗号化については先日のエントリを参照。
- (5) 復号の際の NULL バイト除去
-
暗号化の際にパディングされた NULL は復号後もそのまま残されるので、base64 デコードする前に rtrim で NULL を取り除いていておく。もっとも、少なくとも PHP 5.2.9 の base64_decode 関数は NULL を文字列終端と見なす(いわゆる”バイナリセーフでは無い”関数)ので、今のところは取り除かなくても問題ない。
参考
- PHP: Mcrypt – Manual
- 各ページの User Contributed Notes にも有用な情報がある
- 新版暗号技術入門 秘密の国のアリス (結城浩)
- 暗号についての知識は、ほぼ全てこの書籍から得た
はてな人力検索より:
PHPのmcrypt関数で使用する初期化ベクトル(IV)とは公開されてはまずいものなのでしょうか?
参考:
CBCモードにおいて、中間者攻撃によって初期化ベクトルのビットが反転させられると、復号された平文ブロックの対応するビットも反転する。よって攻撃者は最初の平文ブロックに限り、任意のビットを操作できることになる。攻撃者がメッセージの構造を熟知している場合、脅威となり得る。
それを防ぐためのも最も簡単な方法は、「初期化ベクトルを暗号化すること」である。
ではその初期化ベクトルを暗号化するための初期化ベクトルはどうすれば良いのだろう…と一瞬だけ悩んだが、初期化ベクトルはブロック長と同じ長さで、しかも毎回異なるランダムな値なのだから、どのように暗号化しても同じことか。