(OpenSSL is written by monkeys)
Marco Peereboom, 2009年
原文: https://www.peereboom.us/assl/assl/html/openssl.html
日本語訳: 新山 祐介
(訳注: OpenSSL は SSL/TLS などの暗号化通信を行うための オープンソースのライブラリである。世の中の多くの webサーバは これに依存している。2013年、OpenSSL に有名な脆弱性 (HeartBleed) が 見つかって以来、ソースコードの改善が進められているが、本記事は HeartBleed や POODLE などの事件よりもはるかに前 (2009年) に 書かれていることは注目に値する。)
まあ聞いてくれ…。俺の名前はマルコ。 すごいプログラマーというわけでは決してないが、それなりのことはやっている。 最近、暗号化通信をしなきゃいけないコードを書く必要があって、 よく知られている一般的なライブラリを使うのが一番いいだろうと思った。
基本的に、やることは2つだ:
- CA (認証局) を提供するアプリを書く。
- すべての電子証明書を LDAP ツリーの中に入れる。
当初、こんなのはネットから文書とコード例をひろってくりゃいいだけで、 朝飯前の仕事だと思った。結局、ひと月ぐらいをこの仕事に費やしたあげく、 この経験をネット上の迷える子羊のために書いておこうと決心した。
俺の結論はこうだ。OpenSSL はこれまで書かれてきた中でもダントツで 史上最悪のライブラリである。これはまさにサルが壁にウンコを投げつけているに等しい。 俺はいまだに全世界のインターネットがこのアホらしく複雑かつウンコな 「OpenSSLプロジェクト」とか呼ばれるコードの上で走っているのが信じられない。
ふーむ、ぜんぜんサンプルコードが見つからないぞ。 見つかったのは CA と関係ない部分ばかりだ。わんさとある HOWTO文書は どれも問題をぜんぜんわかってない人間どもが書いたように見える。 悪意はないのだろうけど、彼らはコミュニティの質を下げてるよな。 まあいいや、B&N (訳注: 紀伊国屋みたいな本屋) へいって何かないか見てみよう。
...
ちくしょう。何も見つからん。本が2冊あったが、 どっちもコマンドラインの使い方が書いてあるだけだ。 どうやらみんな「opensslコマンド」のことしか頭にないみたいだ。 実際のコードをちょっと見てみたが、眩暈がしたのでやめておいた。 この時点で俺はどうやらこの仕事は思ったほど簡単じゃないかもしれない、と思い始めていた。 まあいいや、帰って一杯やろう。
ああもう、しょうがねえからコードを見るぞ。少なくとも どの関数を呼べばいいのかはわかるだろうから、 そしたら文書を見りゃいいだろう。まずはこの 「opensslコマンド」の使い方を習わないとな。
...8時間後...
ふう、ようやくなんとか動かせるようになったぞ。 果てしなき HOWTO文書やら掲示板やらをあさってようやくこれだ。 文書の出来がもうとてつもなくひどい。とりあえず 3つのスクリプトを書いた。 今日のところはもうこれでいいや。帰る。
さて「コマンド」についてはわかったから実際のソースを見るか。 ふーむ、main はどこだ? どうやって個々のモジュールを呼んでいるんだ? アンダースコアと CamelCase が適当に入り乱れている。ワーオ! ああちくしょう MAIN がマクロだと? オーケイ、このドロドロをほじくり返すには真剣な作業が必要だ。
数時間の格闘の後、おぼろげに形がつかめてきた (ありがとうvimよ!)。 今度はこのコードを逆にたどっていって「コマンド」を使わずに CA が 生成できるようにしなきゃならない。この後、さらに数時間を 不完全なmanページとか、存在しないmanページに費やしていたら、どっと疲れがきた。 GoogleもBingも助けてくんない。ドキュメントは純粋に存在していないんだ。 あるものはすでに時代遅れで、現実に即していない。 もういいや、帰るぞ。
とりあえず「コマンド」にあったコードをもとにして
自分のコードを書き始めた…。進捗はすさまじくのろい。
ひどいコーディングスタイル、そしてインデント、それから解読不能な
#ifdef
の嵐。愚痴りだしたらきりがない。
#ifdef
といえば、部分的に命令を食っているやつを数回発見した:
#ifdef (OMG)
if (moo) {
...
} else
#endif /* OMG */
yeah();
実際には、これはきれいなほうだ。 俺が見たものは数百行にもおよぶ範囲で、そのインデントたるや 大の男ですら涙を流すほどのしろものである。もし
if (moo)
{
dome_something_dumb();
}
else
{
or_not();
}
とか
if ( moo)
{
blah();
}
とか
if (bad)
goto err;
...
if (0) {
err:
do_something_horrible();
}
みたいなのが読みやすいと思うんなら、眼科に行ったほうがいい。 あるいは肛門科でもいいかも。本当にあったコードはこうだ:
if ((OBJ_obj2nid(obj) == NID_pkcs9_emailAddress)
&&
(str->type != V_ASN1_IA5STRING))
{
BIO_printf(bio_err,"\nemailAddress type needs to
be of type IA5STRING\n");
goto err;
}
if ((str->type != V_ASN1_BMPSTRING) &&
(str->type != V_ASN1_UTF8STRING))
{
j=ASN1_PRINTABLE_type(str->data,str->length);
if ( ((j == V_ASN1_T61STRING) &&
(str->type != V_ASN1_T61STRING)) ||
((j == V_ASN1_IA5STRING) &&
(str->type ==
V_ASN1_PRINTABLESTRING)))
{
BIO_printf(bio_err,"\nThe string
contains characters that are illegal for
the ASN.1 type\n");
goto err;
}
}
実際の呼び出し順序はこうなっている。vim ありがたや!
if (!SSL_CTX_use_certificate_file(ctx, "server/server.crt",
SSL_FILETYPE_PEM))
ctrl ]
...
else if (type == SSL_FILETYPE_PEM)
{
j=ERR_R_PEM_LIB;
x=PEM_read_bio_X509(in,NULL,ctx->default_passwd_callback,ctx->default_passwd_callback_userdata);
ctrl ]
...
#define PEM_read_bio_X509(bp,x,cb,u) (X509 *)PEM_ASN1_read_bio( \
(char *(*)())d2i_X509,PEM_STRING_X509,bp,(char **)x,cb,u)
ctrl ]
...
if (!PEM_bytes_read_bio(&data;, &len;, NULL, name, bp, cb, u))
return NULL;
ctrl ]
...
if (!PEM_read_bio(bp,&nm;,&header;,&data;,&len;)) {
ctrl ]
...
i=BIO_gets(bp,buf,254);
ctrl ]
...
i=b->method->bgets(b,in,inl);
これが 5つのファイルにまたがり、6つの間接呼び出しにまたがっていて
最終的にやってるのはファイルを fgets
するだけだ。
これだけの行ったり来たりのあげく、俺がやりたいのはただ
PEM形式の証明書 (ファイルに入ってるわけでもない) を X509形式に変換するだけなんだ。
関数はごまんとあるにもかかわらず、こうした便利そうな関数はひとつもない。
あるのかも知れないが、どのみち文書が存在しないのでなんとも言いようがない。
そしてぜひともお見せしたいのが、この逸品である:
#ifndef OPENSSL_NO_STDIO
/*!
* ファイルから ::STACK にCA証明書を読み込む。この名前にはやや語弊がある。
* この関数はクライアントとはまったく関係ない (ただ CA の階層をクライアントに
* 送るのに使うだけである)。実は、これは CA ともあまり関係がない、
* なぜなら実際にやっていることは古い証明書を読み込んでいるだけなので。
* \param 1つあるいは複数の証明書を含んだファイル。
* \return 証明書を含んだ ::STACK。
*/
STACK_OF(X509_NAME) *SSL_load_client_CA_file(const char *file)
{
BIO *in;
X509 *x=NULL;
X509_NAME *xn=NULL;
STACK_OF(X509_NAME) *ret = NULL,*sk;
sk=sk_X509_NAME_new(xname_cmp);
in=BIO_new(BIO_s_file_internal());
if ((sk == NULL) || (in == NULL))
{
SSLerr(SSL_F_SSL_LOAD_CLIENT_CA_FILE,ERR_R_MALLOC_FAILURE);
goto err;
}
if (!BIO_read_filename(in,file))
goto err;
for (;;)
{
if (PEM_read_bio_X509(in,&x;,NULL,NULL) == NULL)
break;
if (ret == NULL)
{
ret = sk_X509_NAME_new_null();
if (ret == NULL)
{
SSLerr(SSL_F_SSL_LOAD_CLIENT_CA_FILE,ERR_R_MALLOC_FAILURE);
goto err;
}
}
if ((xn=X509_get_subject_name(x)) == NULL) goto err;
/* check for duplicates */
xn=X509_NAME_dup(xn);
if (xn == NULL) goto err;
if (sk_X509_NAME_find(sk,xn) >= 0)
X509_NAME_free(xn);
else
{
sk_X509_NAME_push(sk,xn);
sk_X509_NAME_push(ret,xn);
}
}
if (0)
{
err:
if (ret != NULL)
sk_X509_NAME_pop_free(ret,X509_NAME_free);
ret=NULL;
}
if (sk != NULL) sk_X509_NAME_free(sk);
if (in != NULL) BIO_free(in);
if (x != NULL) X509_free(x);
if (ret != NULL)
ERR_clear_error();
return(ret);
}
#endif
ワーオ、こいつは全部揃ってるよ! 解読不能、間接参照しまくり、そして
関数が結局何をしたいのかぜんぜん明らかでない。秀逸なのが if (0)
の部分、
もう最高。こういったコードが日夜「セキュアな」インターネットを
実現していると聞けば、みんな感涙にむせぶことだろう。
ただメモリから証明書を読むだけの関数を欲していた俺がバカだった。
ほとんどコードは書けていない。家に帰って何もかも忘れたい気分だ。
こいつはマジでアッタマくる! 数時間のすったもんだの後、どうにかコードをひねり出すことができた。 見てわかるように、エラーのトレースを表示しようとして醜悪な方法を使っている。 その恐るべきマクロはこうだ:
/* errors */
#define ERR_LIBC (0)
#define ERR_SSL (1)
#define ERR_OWN (2)
#define ERROR_OUT(e, g) do { push_error(__FILE__, __FUNCTION__, __LINE__, e); goto g; } while(0)
慈愛にみちた $DEITY 様どうかお許しください。
でもこれでようやく CA ができた〜
歓喜! OMG!!!いちイチ壱!!!111〜
帰るぞ!
家に帰ってから、俺はシャワーの中ですこし泣いた。 待て、これは血か???
LDAP 関連の部分はあとで書くことにしよう。 次は SSL/TLS を使ったクライアント/サーバ部分をやることにする。 まずネットで例を検索…
ダメダメな結果
しょうがないので manページを見ることにするか。 とーぜん例が書いてあるだろうからな!
さらにダメダメな結果
ネットでひろった例をもとに、しばらく悪あがきする。
それでもダメダメ
もういいや、帰ろ。
それじゃ大好きな部分に戻るとするか!
「opensslコマンド」は s_server と s_client という、 なんとか動いているらしい部分がある。 これをもとにコードをでっちあげる。 自慢できる結果じゃないが、なんとかできた。 帰るぞ!
LDAP とつなげる部分をさらに作る。 会議やらその他のアホなことに嫌気がさしたのでこの愚痴を書きつけることにした。 なんとしてでもこのウンコを投げるサルがごとき OpenSSL を 手なずけなきゃならん。
-- ここで述べられている意見は著者自身のものであり、 なんら他者の意見を代弁するものでもありません。
Opinion and code (c) 2009 Marco Peereboom.
Code snippets from the OpenSSL project are (c) 1998-2009 The OpenSSL Project.