OpenLDAP 上のパスワード情報

OpenLDAP で userPassword アトリビュートにパスワード情報を保存する場合、推奨はソルト付き SHA(SSHA)とされていると思います。実際、CentOS Ver 6.3 上の OpenLDAP に付いてくる slappasswd コマンドは、オプションを指定しないデフォルトは、SSHA の形式を出力します。

しかし、高速な GPU が廉価に販売され、その GPU を駆使して並列計算をすると、ブルートフォースによる解析が、実用的な時間で成功するようになりました。

去年の 12 月に飛び込んできた、「NTLM ハッシュが 8 文字までのパスワードなら5時間版」*1は衝撃的で、私もそのことを日記に書きました*2。NTLM ハッシュは Unicode で表現されたパスワード文字列の MD4 ハッシュ値で、SHA-1 よりは出力されるハッシュ値の bit 長も短く、1つのハッシュ値を求める時間はおそらく、SHA-1 より MD4 の方が短いとは思いますが、それでも、何百倍、何千倍も違う、という事ではないでしょう。

http://www.cryptopp.com/benchmarks.html

上記サイトに、様々はハッシュ値計算や暗号化処理のベンチマーク結果がありますが、MD5SHA-1 の比較で、およそ2倍(MD5 の方が高速)という結果になっています。MD4 がどのくらいになるのか分かりませんが、大きく見積もっても 10 倍にはならないと思います。

仮に、SHA-1 の計算が MD4 より 10 倍の時間がかかる、としても、先の「5時間半」は「55 時間」にしかなりません。2日余りで必ず解ける、という事になってしまいます。「塩加減は重要? - JULYの日記」でも書きましたが、ソルトの有無は Rainbow Table 対策にはなっても、ブルートフォース対策にはなりません。

また、実際に SHA の値を単一の GPU で 33 日間で見つけられたという2年前の記事*3もあります*4

ブルートフォース対策にはストレッチング、なのですが、自分で作る Web アプリケーションならともかく、LDAP のパスワードフィールドに対して、独自のストレッチングを行う OpenLDAP 用の overlay や plugin を作る、というのも気が引けます。

{CRYPT} は crypt(3)

OpenLDAP のパスワード・スキームに、{CRYPT} というのがあります。これは、古くから UNIX 系 OS の /etc/passwd や /etc/shadow のパスワード・フィールドに記述するされる形式で、古典的には ASCII のパスワード文字列を DES の鍵として、全ビット 0 のデータを出発点に、繰り返し暗号化した結果になります*5

しかし、今時、この DES を使った形式の値を保存している物を見ません。商用 UNIX でも、Linux でも、10 年以上前から、MD5 を使った形式などに変わってきています。

これは、このパスワード・フィールドに記述する内容を計算する crypt という C 言語用の関数を拡張する形で実現されました。具体的には crypt の引数に渡すソルトで、先頭 3 文字に特別な意味を持たせ、それによって計算処理を変えるようになっています。例えば、「$1$」で始まったソルトが渡されたら、MD5 を使った計算処理を行う、といった感じです。今では MD5 の代わりに SHA-256、SHA-512 を使った形式もあり、最近の Linux ディストリビューションでは SHA-512 を使う「$6$」になっています。

ところが、設定したパスワードとソルトから MD5 の値を求めようとしても、実際に /etc/shadow に書き込まれた物とは、似ても似つかない結果になります。で、実際に crypt の処理を調べたことがあります。

調べてみると、結構複雑 - JULYの日記

実は MD5 を使った crypt というのは、1,000 回のストレッチングを行った処理でした。ちなみに SHA-256 や SHA-512 の場合も、同様のストレッチングをしています。

で、改めて OpenLDAP のドキュメントを読むと、slapd.conf の設定に password-crypt-salt-format*6 という項目がありました。

OpenLDAP Faq-O-Matic: How do I specify the crypt(3) salt format to use?

password-crypt-salt-format に指定するのは printf の書式指定子の形式で記述します。上記ページでは、「password-crypt-salt-format "$1$%.8s"」という例が出ていますが、こうすれば、「$1$」で始まり、その後ろに 8 桁のソルト文字列が続くことになります。

つまり、

  • password-hash に {CRYPT} を指定し
  • password-crypt-salt-format に "$1$%.8s" と設定する

と、MD5 を 1,000 回繰り返すストレッチング処理をした結果が、userPassword のアトリビュートに書き込まれる事になります。MD5 の計算時間が SHA-1 の半分だっとしても、ブルートフォースで解くには、単純計算で {SSHA} の 500 倍の時間がかかる事になります。仮に、MD5 と MD4 の計算時間が同程度、と控えめに見積もっても、先の 5.5 時間の装置で 2,750 時間、4 ヶ月近い時間がかかる事になります。もし SHA-512 の crypt を使うようにすれば、先に紹介したベンチマーク結果を見ると、さらに 2.5 倍程度の時間を要するので 10 ヶ月の期間が必要、という事になります。

制限事項

slapd を動かす OS 上の crypt(3) の機能

OpenLDAP の {CRYPT} は、その OS 上の crypt 関数の実装に依存します。なので、複数の slapd で同期をしているような場合、それぞれの OS 上の crypt の実装で揃っている必要があります*7MD5 を使った形式であれば、よほどの事が無い限り、対応していない事は考えられませんが、SHA-256、SHA-512 を使うのであれば要注意です。

ちなみに、RHEL / CentOS の場合、pam_unix で SHA-256、SHA-512 対応となったのは Ver. 5.2 です*8ので、少なくともこのバージョン以降であれば大丈夫なはずです。

LDAP を使うプログラムの実装

ユーザ認証を行うプログラムが、そのバックエンドとして LDAP を使う場合、大きく分けて 2 つの実装方法があります。

  • 入力されたアカウント名、パスワードを使って、LDAP サーバへのバインドを試みる。
  • userPassword アトリビュートを直接参照・比較する。

前者の場合は、slapd がどんなパスワード・スキームを使ってるかは全く関係ありませんが、後者の場合はプログラム側が {CRYPT} で DES 以外の形式が扱えるか、という問題になります。

例えば、Dovecot でユーザ認証に LDAP を使う場合、

の二通りの方法があり、auth_bind を yes にすれば前者、no にすれば後者になります。

もし、POP3APOPIMAP で DIGEST-MD5 や CRAM-MD5 を使いたければ後者しか選択の余地がありませんが、そうでなければ、前者の方式が使えます。

前者の方式が使えるのであれば、パスワード・スキームの問題は生じませんが、後者の場合は、Dovecot がうまく扱えるかどうか、確認する必要があります*9

RFC 3062 非対応

パスワードを変更する際、RFC 3062 の「LDAP Password Modify Extended Operation」に対応しているプログラムからであれば、slapd が userPassword を設定したパスワード・スキームで保存してくれますが、これに非対応の場合、userPassword に保存する内容を直接書き込む物があります。コマンドラインの ldappasswd は RFC 3062 対応なので問題ないのですが、例えば Apache Directory Studio*10 は対応していません*11

これは、password-crypt-salt-format を指定してるか否かにかかわらず、クライアント側で勝手にパスワード・スキームを変更できる事になるのでうれしくないのですが、もし、これらのツールを使うのであれば、slappasswd で -c オプションに password-crypt-salt-format に指定したのと同じ物を指定し、-h オプションに {CRYPT} を指定すれば、userPassword アトリビュートに保存される値が計算できます。

$ slappasswd -h {CRYPT} -c '$1$%.8s'
New password:
Re-enter new password:
{CRYPT}$1$t6PXjyr8$R9LHggt9sf2ietomynfRo0

ただ、slappasswd が使えるなら、ldappasswd で変えた方が早いですが...。

パフォーマンス

そもそも、1度の SHA-1 の計算処理で綱渡り状態にあるようなサーバの場合、そのままパスワード・スキームを変更すれば破綻します。そもそも、認証処理でそこまで重い状態な時点で、システム増強などを検討すべきですが...。

まとめ

ということで、もし、システム全体で問題が無いのであれば、OpenLDAP でのパスワードスキームは、

password-crypt-salt-format を指定した上で、{CRYPT} を使う。

が、今のところ正解、という事になります。ブルートフォース耐性は、"$1$%.8s" で {SSHA} のおよそ 500 倍に上がります。パスワード長を1文字増やしたより高い効果が得られる事になります。

ただし、短いパスワードや単語を使ったようなパスワードなどを設定したら、ほとんど効果はありません。十分に長く、複雑なパスワードで、使い回しをしない、という鉄則は、何ら変わることはありません。

*1:8文字の全パスワードを5時間半で解析するコンピュータクラスタが登場 - CNET Japan

*2:こんなに早く、この日が来るとは... - JULYの日記

*3:エフセキュアブログ : 「SHA-1+salt」はパスワードに十分だと思いますか?

*4:指数表現は実感しにくい - JULYの日記

*5:crypt (C) - Wikipedia Traditional DES-based scheme 参照

*6:今時の cn=config からオンラインで設定変更をする場合だと olcPasswordCryptSaltFormat に該当します。

*7:試してはいませんが、同期自体は正常に行われるかもしれません。ただ、userPassword アトリビュート自体が正しく同期できても、バインド時に正しく認証できない、という事態になる事が想像されます。

*8:Bug 435804 – RHEL5.2 Release Notes: SHA-256 and SHA-512 support in password hashing

*9:未確認。Dovecot が {CRYPT} だった時にそのまま crypt 関数を呼び出していれば、大丈夫なはず。

*10:Welcome to Apache Directory Studio — Apache Directory

*11:Ver. 1.5 系の場合。Ver. 2 系は不明ですが、試した感じでは対応している気配が無かったです。サーバ側の設定の関係とか、あるのかなぁ。