仙石浩明の日記

2010年3月1日

x86_64 Linux などの 64bit 環境で MD5 を使うときの注意点

MD5 (Message Digest Algorithm 5) は、 RFC 1321 でアルゴリズムが紹介されていて、 Appendix (付録) として C によるリファレンス実装が付属しているが、 その global.h に

/* UINT4 defines a four byte word */
typedef unsigned long int UINT4;

と書いてある。 すなわち 32bit 整数として UINT4 型を定義している。 x86_64 Linux を始め多くの 64bit Unix は LP64 すなわち long int (とポインタ) が 64bit な整数データモデルを採用している。 したがって UINT4 型の定義が 「unsigned long int」 のままで、 この MD5 リファレンス実装を使ってしまうと、 32bit であるべき UINT4 型が 64bit になってしまい、 間違ったハッシュ値を算出してしまう。

16bit CPU が主流だった大昔なら 「int が 16bit なデータモデルを採用している環境」 が多かったのかもしれないが、 RFC 1321 が出た 1992年ごろは既に 32bit CPU が主流だったわけで、 UINT4 型を 「int」 と定義しておいてくれてもよかったのにと思う。 そうすれば、 「long が 64bit なデータモデルを採用している環境」 が多くなる昨今でも (int は 32bit のままなので) 問題を起こさずに済んだだろうに。

試しにテストプログラムを書いてみる:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include "global.h"
#include "md5.h"
#define DIGEST_LEN 16
#define BUFFER_LEN 256

int main(int argc, char *argv[]) {
    MD5_CTX context;
    unsigned char digest[DIGEST_LEN];
    unsigned char buf[BUFFER_LEN];
    int i;
    MD5Init(&context);
    while ((i=read(0, buf, BUFFER_LEN)) > 0) MD5Update(&context, buf, i);
    MD5Final(digest, &context);
    for (i=0; i < DIGEST_LEN; i++) printf("%02x", digest[i]);
    printf("\n");
    return 0;
}

32bit 環境 (i686 Linux) では正しく動く:

senri:/home/sengoku/src/md5 % uname -m
i686
senri:/home/sengoku/src/md5 % ls
global.h  main.c  md5.h  md5c.c
senri:/home/sengoku/src/md5 % cc -Wall main.c md5c.c
senri:/home/sengoku/src/md5 % file a.out
a.out: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.0.0, dynamically linked (uses shared libs), not stripped
senri:/home/sengoku/src/md5 % echo "Hello, world" | ./a.out
a7966bf58e23583c9a5a4059383ff850
senri:/home/sengoku/src/md5 % echo "Hello, world" | openssl md5
a7966bf58e23583c9a5a4059383ff850

ところが、 64bit 環境 (x86_64 Linux) だと:

senri:/home/sengoku/src/md5 % uname -m
x86_64
senri:/home/sengoku/src/md5 % cc -Wall main.c md5c.c
senri:/home/sengoku/src/md5 % file a.out
a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), for GNU/Linux 2.4.0, dynamically linked (uses shared libs), not stripped
senri:/home/sengoku/src/md5 % echo "Hello, world" | ./a.out
fd578222c6a471623ea1e3eb2b6e6f6b

などと、 誤った MD5 の値が出力されてしまう。

MD5 の値を求めること自体が目的であれば、 誤ったハッシュ値が出力されればすぐ気付くのでいいのだが、 値そのものが目的であることは (当然ながら) あまりなくて、 普通はアプリケーションの中で MD5 を利用するので、 32bit 環境で使っていたアプリケーションを 64bit 環境でコンパイルし直して使おうとするとハマる。

私は qmapop-0.3 (Bert Gijsbers 氏が開発した、qmail 用 APOP 認証パッケージ。 とうの昔に obsolete になってる ^^;) を x86_64 Linux でコンパイルし直したら APOP 認証が通らなくなってしまって焦った。

なにぶん古いプログラム (1995年5月リリース) に いろいろ手を加えて使っていたので、 APOP 認証が失敗するのは必要なパッチをあてていないためだろうと思ってしまった。 しかし最後にコンパイルしたのは何年も前 (2002年4月) なので、 どんな修正を行ったか思い出せない。

仕方ないのでソースをながめて原因を探る羽目になり、 動作させつつ処理を追っていたら、 何のことはない単に算出した MD5 の値が誤ってるだけということに気付き、 (qmapop-0.3 が使ってた) MD5 リファレンス実装がデータモデルに依存していることを見つけた次第。

C99stdint.h が定義されて、 整数データモデルに依存しない、 移植性の高い書き方が可能になった。 以下のように global.h を修正すればよい:

senri:/home/sengoku/src/md5 % diff -ub global.h~ global.h
--- global.h~	1993-05-14 02:15:17.000000000 +0900
+++ global.h	2010-02-27 23:49:36.553633806 +0900
@@ -13,11 +13,13 @@
 /* POINTER defines a generic pointer type */
 typedef unsigned char *POINTER;
 
+#include <stdint.h>
+
 /* UINT2 defines a two byte word */
-typedef unsigned short int UINT2;
+typedef uint16_t UINT2;
 
 /* UINT4 defines a four byte word */
-typedef unsigned long int UINT4;
+typedef uint32_t UINT4;
 
 /* PROTO_LIST is defined depending on how PROTOTYPES is defined above.
 If using PROTOTYPES, then PROTO_LIST returns the list, otherwise it
Filed under: プログラミングと開発環境 — hiroaki_sengoku @ 09:34

2件のコメント »

  1. 助かりました。同じ問題ではまってました。

    コメント by masao — 2011年3月23日 @ 22:18

  2. 私も同じ問題ではまりましたが検索でこの記事を見てすぐに解決できました。大変助かりました。ありがとうございました。

    コメント by miyachi — 2012年12月25日 @ 16:43

この投稿へのコメントの RSS フィード。

コメントする