Displaying posts written in

6月 2009

多重バッククォートとアンクォート

頭が混乱したときのためにメモ。

同等なlistによる表現に書き直す

内側のバッククォートから順に書き直していく。例えば

``(,,n ,,g)

に対するlistによる表現は

`(list ,n ,g)

となる。同様に

`(a ,b `(,,a ,b))

なら

`(a ,b (list ,a b))

である。

評価した結果を求める

右側のアンクォートから評価していく。例えば a=c, b=1 という束縛が存在する環境で

``(,,a ,,b)

を評価すると

`(,c ,1)

になる。また’によるクォートと併用する場合も右から順に考えれば分かりやすい。例えば

`(a ,b `(,',a ,b))

を評価すれば

(a 1 `(c ,b))

を得る(クォートとアンクォートは打ち消し合う)。

『実践Common Lisp』8章 once-onlyマクロ

自分で考えて書いてみた。本に載っている例とは少しだけロジックが異なるが、同様に動作する。

(defmacro once-only ((&rest names) &body body)
  (let ((gensyms (loop for n in names collect (gensym))))
    `(let (,@(loop for n in names for g in gensyms collect `(,g ,n)))
       (let (,@(loop for n in names collect `(,n (gensym))))
	 `(let (,,@(loop for n in names for g in gensyms collect ``(,,n ,,g)))
	    ,,@body)))))

ところでこの中の (,@(loop …)) となっている部分を、次のように ,(loop …) に置き換えると、

(defmacro once-only ((&rest names) &body body)
  (let ((gensyms (loop for n in names collect (gensym))))
    `(let ,(loop for n in names for g in gensyms collect `(,g ,n))
       (let ,(loop for n in names collect `(,n (gensym)))
	 `(let ,,(loop for n in names for g in gensyms collect ``(,,n ,,g))
	    ,,@body)))))

なぜか動作しなくなる(do-primesのコンパイルでエラー)。理由がよく分からない。with-gensyms は後者の形で動いているのに…。

『実践Common Lisp』3.8のwhereを関数で実装する

マクロで実装された where を他の言語的な発想で、つまり関数(クロージャ)を使って実装したらどうなるか、考えてみた。

(defun make-comparison-fun (field value)
  (lambda (cd) (equal (getf cd field) value)))
 
(defun all (fun lst)
  (cond ((not lst) t)
	((not (funcall fun (car lst))) nil)
	(t (all fun (cdr lst)))))
 
(defun where-fun (&rest clauses)
  #'(lambda (cd)
      (all #'(lambda (comp-fun) (funcall comp-fun cd))
	   (loop while clauses
	      collecting (make-comparison-fun (pop clauses) (pop clauses))))))

こうして書き比べてみると、関数という単位でしか処理を分割できないのはとても不自由で、不自然だとすら感じてしまう(そう感じてしまう自分に驚く)。make-comparison-fun の本当の意図は「今すぐには評価しない、後で評価するためのコードブロックを作る」ことであって、関数という形式にはあまり意味がない。all や where-fun の中で funcall を呼ぶのも煩わしい。

実際、普通は上のようなコードは書かないだろう。不自然で不自由で、抽象化過剰のように感じられるからだ。この程度の内容ならば、一切の抽象化を放棄して単純なループで書いてしまう気がする。

function where() {
    var fields = arguments;
    return function(cd) {
	for(var i=0; i<fields.length; i+=2) {
	    if(cd[fields[i]] != fields[i+1])
		return false;
	}
	return true;
    }
}

しかしこの手の妥協を繰り返していると、やがてテストするのがしんどくなってくる。どこまで抽象化すべきか、どこで抽象化を諦めるか、特に再利用可能なライブラリを作っているときにはかなり悩ましい問題となる。

Lispではそういう妥協は不要ということだろうか。今後の内容に期待しよう。

mymemcheckをCentOSで動かす

サーバ/インフラを支える技術』に載っていた mymemcheck を試してみようと思って CentOS 5 上で実行してみたところ、いくつかの Perl モジュールが足りなくて動かなかった。例によって rpmforge で検索してみたら、やはり見つかった。

sudo yum --enablerepo=rpmforge install perl-Readonly perl-UNIVERSAL-require

相変わらず便利過ぎるぜ rpmforge。

実践 Common Lisp 3.8

少しずつだが『実践Common Lisp』を読み始めた。3章の where の例で早くも感動。Lispのマクロはとにかく凄いのだと、著者がくどいくらいに主張する理由が分かってきた。確かにこれは凄い。

練習のために update のマクロ版も書いてみた。後の章で出てくるのかも知れないが。

(defun make-update-expr (field value)
  `(setf (getf cd ,field) ,value))
 
(defmacro update (selector-fn &rest clauses)
  `(setf *db*
	 (mapcar
	  #'(lambda (cd)
	      (when (funcall ,selector-fn cd)
		,@(make-expr-list #'make-update-expr clauses))
	      cd)
	  *db*)))

make-expr-list は make-comparison-list を一般化したもので、次の通り。

(defun make-expr-list (make-expr-fn fields)
  (loop while fields
       collecting (funcall make-expr-fn (pop fields) (pop fields))))

Ruby 1.9/1.8.7 では PKCS5 PBKDF2 が使える

ドキュメントには記されていないが、OpenSSL::PKCS5 というクラスが存在し、OpenSSL の PBKDF2 実装にアクセスすることができる。ただし OpenSSL 自体の制約により、バージョン0.9.8kの時点ではハッシュ関数として HMAC-SHA1 しか使用できない。

keys = OpenSSL::PKCS5.pbkdf2_hmac_sha1("password", "saltsalt", 1000, 16)
p keys.unpack("H*").join #=> "e9febff54bfce668fde301acc85563cc"

OpenSSL の将来のバージョン(1.0.0?)ではより汎用的な関数が追加される予定であり、Ruby からも利用できるようになるようだ。

keys = OpenSSL::PKCS5.pbkdf2_hmac("password", "saltsalt", 1000, 32, "sha256")
# => OpenSSL 0.9.8k では NotImplementedError

このあたりがまだ中途半端なので、ドキュメントには記されていないのだろうか。

Ruby のリポジトリをチェックしてみると、2007年4月5日にまず trunk に対して追加され、その後1.8系にバックポートされたらしい。

暗号ライブラリ LibTomCrypt

非常に多機能な暗号ライブラリ LibTomCrypt。各種の対称鍵暗号、公開鍵暗号、疑似乱数生成器、MAC、デジタル署名、完全性検証機能付きのブロック暗号のモード(CCMやGCMなど)、厳密にモジュール化されたAPI、さらに200ページに及ぶpdfマニュアルまでついてくる。ライセンスはパブリックドメイン。

素晴らしいライブラリなのだが、2007年からパタリと更新が止まってしまったようだし、サイトの移転も中途半端な状態のまま。今は一体どういう状況なんだろう?と思っていろいろ検索したところ、ショッキングな事実が判明した…。

”なりすまし”の嫌がらせを受けて云々、という部分の詳しい事実関係は不明だが、ライブラリの作者Tom St Denis氏が既にメンテナンスするつもりがない、というのは間違いないようだ。2008年12月31日の投稿の中で全ソースツリーを公開して、どのようにでも使って良い、と宣言されている(また1年以上変更していない、とも)。

http://home.libtom.org/lt_tree.tar.bz2

公式にリリースされた最新バージョンはミラーサイトにアーカイブが残っている。

設計の美しさや充実したドキュメントに感動していただけに、かなりショックだ…。このライブラリに対する Ruby や PHP のバインディングが作れたらステキだと思っていたのだが……。

PHP で Non-blocking I/O

stream_set_blocking関数を使う。

C/C++セキュアプログラミングクックブック〈VOLUME2〉』に載っている「レシピII-4.3 Unix標準の乱数インフラストラクチャを使用する」をPHPで実装すれば、次のようになると思われる。
(続きを読む…)

暗号乱数インフラの初期化処理

暗号ライブラリ内部でどんな処理が行われているのか、Ruby と PHP について調べてみた。調査に使用したバージョンは ruby-1.9.1-p129, php-5.2.9, openssl-0.9.8k

ruby-openssl ライブラリの場合

乱数用のインターフェイスは OpenSSL::Random モジュール。例えば

salt = OpenSSL::Random.random_bytes(8)

とすると8バイトのランダムなバイト列が取得できるのだが、こんな風に初期化処理なしで、いきなり乱数の取得メソッドを使っても大丈夫なのだろうか?

  1. Ruby のソースを見ると random_bytes の定義は ossl_rand.c にあり、実体は OpenSSL の RAND_bytes() に対するアダプタ
  2. OpenSSL のソースを見ると RAND_bytes の定義は rand_lib.c にあり、実体は(非FIPSモードでは)md_rand.c の中の ssleay_rand_bytes()
  3. ssleay_rand_bytes() の中で初期化済みか否か(initialized)を調べて、初期化済みでなかったら RAND_poll() を呼び出す
  4. RAND_poll の定義はOS/環境ごとに分かれているが、可能な限りのソースからエントロピーを収集するようになっている
    • UNIX: rand_unix.c 通常はまず乱数デバイスからエントロピーの読み出しを試みる。試行対象となるデバイスのパスは e_os.h で DEVRANDOM として定義されており、デフォルトでは /dev/urandom が最優先となる(この際、Linux では poll, その他では select を使って非同期で読み出される)。それでも十分なエントロピーが集まらなかった場合、エントロピー収集デーモン(EGD)のソケットファイルから読み出しを試みる(こちらも e_os.h に試行対象となるパスが定義されている)。さらに pid と uid と現在時刻もエントロピーとして追加する。
    • Windows: rand_win.c の場合は CryptAPI の CryptGenRandom() 関数から取得した疑似乱数をエントロピーとして追加する。さらにメモリ使用量やプロセスIDといったデータもエントロピーとして追加する。

ということで、ほとんどのケースではいきなり random_bytes メソッドなどを使っても大丈夫そうである。

ちなみに OpenSSL::Random モジュールには十分なエントロピーで初期化されたかどうかを調べる status? メソッドもある(現在はUndocumentedだが)。こちらもソースをたどって行けば初期化コードに繋がっているので、システムに十分なエントロピーが存在する環境ならば、常に true を返すはずである。

OpenSSL::Random.status?  #=> true

php-mcrypt ライブラリの場合

直接的な乱数インターフェイスは存在しないが、初期化ベクトルの生成用として mcrypt_create_iv() 関数が用意されている。 この関数の定義は ext/mcrypt/mcrypt.c にあり、中身は指定されたソース(/dev/random または /dev/urandom)からそのままデータを読み出しているだけだった。

fd = open(source == RANDOM ? "/dev/random" : "/dev/urandom", O_RDONLY);
if (fd < 0) {
	efree(iv);
	php_error_docref(NULL TSRMLS_CC, E_WARNING, "Cannot open source device");
	RETURN_FALSE;
}
while (read_bytes < size) {
	n = read(fd, iv + read_bytes, size - read_bytes);
	if (n < 0) {
		break;
	}
	read_bytes += n;
}
n = read_bytes;
close(fd);

結構ナイーブな実装だな、というのが率直な感想。てっきり mcrypt に対応するインターフェイスがあって、処理を委譲しているのかと思っていたが…(気になって libmcrypt-2.5.8 のソースもざっとチェックしてみたが、やはり乱数の処理は入っていないようだ)。

Non-blocking I/O を使うなどの工夫は無いため、/dev/random では読み出しでブロックする可能性がある。また MCRYPT_RAND は単に rand 関数を必要な回数呼び出すだけなので、暗号用途では使うべきではない(つまりWindows環境では実質使用できない、ということになる)。