ぼくがかんがえたさいきょうの LDAP テスト法 | サイバーエージェント 公式エンジニアブログ
こんにちは。
全社システムの吉田です。

私の所属する全社システムでは、主に社内向けシステムの構築や運用を行っています。先日、社内の認証に用いている LDAP サーバーのバッチを作成しました。

全社システムでは、コーディングをする際には主に Ruby を用いています。
Ruby から LDAP とお話するには、ActiveLdap というモジュールを使用しました。ActiveLdap の利用法についてはるびまに記事が載っています。Rails を使用した事のある人ならば、きっと直感的に使用出来ると思います。

ところで、このバッチ処理のテストはどうすれば良いでしょう??
出来れば LDAP のモックを使った Unit Test を書きたい所です。でも、適当なモックを探して見たのですが、なかなか見つかりませんでした。Schema 登録無しで OpenLDAP と ActiveDirectory のデータが登録できて、Ruby で簡単に動かせるモックが欲しかったのですが。

結局、今回は  Unit Test は書かずにリリースしましたが、何となく気になっていたので、引きこもりの時間を利用してモックを自作してみる事にしました。バレンタイン前後に外出すると精神衛生上よろしく無いですし、2014 年は記録的大雪が降りました。今月はいつもより余計に引きこもっております。(編集部注: 執筆は2月)

その時に LDAP のプロトコルも調べてみたので、今回はその内容を簡単にご紹介します。
RFC4511 によると、LDAP 通信の流れは以下の様になっています。

BER とは Basic Encoding Rules の略で、簡単に言うと構造化されたデータをシリアライズする方法です。構造化されたデータのシリアライズという意味で JSON と似た使い方をしますが、人間にとっての可読性よりプログラムにとっての可読性を重視している点で JSON と大きく異なっています。

LDAPMessage とは、各 Request や Response 等に固有の MessageID をつけた物です。
LDAP では認証等の一部の通信を除き、一つの TCP セッションで複数の Request を同時に投げる事が可能です。例えば、SearchRequest を投げて、その結果が返ってくる前に AddRequest を投げる事が出来ます。サーバー側も、SearchRequest の結果を返しながら途中で AddRequest の結果を返す事も有ります。なので、Response がどの Request に対応する物なのか判別する為に MessageID を使用するんですね。

では、もう少し詳しく見てみましょう。
少し長くなりますが、おつきあい下さい。

初めにBER のエンコード方法について簡単に調べてみます。

BER は ITU-T Recommendations で定められている ASN.1 (Abstract Syntax Notation One)  のエンコード方法の事ですが、生憎このドキュメントは有料となっています。そこで、ちょっとズルをして Wikipedia (英語) を参考にしました。

BER は以下の様に成っています。  
(Wikipedia の図では、この後に "End-of-contents octets" が来ると書いてありますが、
これは滅多に使わないので今回の説明では省きます。)

Identifier octets TypeLength octets LengthContent octets Value
データの型Value のバイト数Value

例えば、整数の 1 を BER でエンコードする事を考えてみましょう。


まず、Value はデータの中身です。今回は 1 です。

次に、Length は Value の長さを表します。今回、Value の長さは 1byte なので、Length は 1 となります。
(Value の長さが 127 byte 以下の場合、Length はそのバイト数です。)

最後に、Type はデータの型を表します。(Json で整数の 1 と文字列の "1" が異なるように、BER ではここで ASCII コード、整数などの型を指定します。)
Type は 最上位 2 bit, 次の 1 bit, 最下位の 5 bit に分かれ、次のような構造に成っています。

87654321
ClassP/Ctag

Class は ASN.1 で定められた共通の Type の場合 0 に、独自拡張した物の場合はその方法によって0以外の値に成ります。(ASN.1 では、独自拡張がプロトコルとして許されているんですね。)
また、P/C は Primitive (単体の値) か Constructed (配列、集合のような値) かを、
最後の 5 byte は tag (実際に何の型なのか) を表します。

ちなみに、この ASN.1 で定められた Type を表す Class を Universal と言い、独自拡張した Class には Application, Context-specific, Private の 3種類が有ります。

また、独自拡張において 31 以上の tag を使う場合は longform と言う、上記とは少し違うフォームになる様ですのでご注意を。

今回、Universal Class (ASN.1 で定められた場合) の INTEGER 型でエンコードする事にすると、Class は Universal Class を表す 0、P/C は Primitive を表す 0, tag は INTEGER を表す物で、先ほどの wikipedia によると 2に成るそうです。
つまり、Type は 2 になるわけですね。

結果として、整数の 1 を BER でエンコードすると、次の 3 byte になります。

TypeLengthValue
211

では、Ruby で [1, 2] と表されるデータを BER で送信するにはどうしたら良いでしょう?
今回は Array を Universal SEQUENCE 型でエンコードしてみます。

1 を BER でエンコードすると、2 1 1 の 3 byte でした。同様に 2 を BER でエンコードすると、2 1 2 の 3 byte に成ります。
なので、[1, 2] を Universal SEQUCNCE でエンコードする場合、そのValue は上記の 3 byte を 2 個合わせた 2 1 1 2 1 2 の 6 byte に成ります。
当然、Length は6、Type は P/C が 1 (Constructed) に成る事に注意すると 48 です。

なので、Ruby の [1, 2] を BER でエンコードすると次の 8 byte になります。

SEQUENCE [1, 2]
TypeLengthValue
486INTEGER 1INTEGER 2
TypeLengthValueTypeLengthValue
211212

色々と端折ってしまいましたが、BER について、何となくご理解いただけたでしょうか?
ソースは Wikipedia (キリッ

次は LDAP のプロトコルを見てみましょう。
今回は uid=user,dc=cyberagent,dc=co,dc=jp というユーザーが openSesami というパスワードを用いて LDAP v3 のシンプルバインド (認証) する事を例に考えてみます。

まず、認証を行う為の BindRequest について調べてみましょう。
BindRequest のデータ型については、Section 4.2 を見てください。次の様に書いてあります。
BindRequest ::= [APPLICATION 0] SEQUENCE {
             version INTEGER (1 .. 127),
             name LDAPDN, 
             authentication AuthenticationChoice }


BindRequest は、「INTEGER 型の version, LDAPDN 型の name, AuthenticstionChoice 型の authentication を [APPLICATION 0] SEQUENCE でエンコードしたもの」だそうです。

分からない事だらけなので、上から順に調べていきます。

 [APPLICATION 0] SEQUENCE とは何でしょう?
これは、「SEQUENCE 型の BER、ただし、Identifier octets Type は Class を Application の独自拡張に, tag が 0  にしなさい」という意味です。
つまり、「Class が Application class を表す 1、P/C が Constructed 型の 1、最後の 5 bit が 0、合計  96 を Type として、{} の中にくくられた値の SEQUENCE を作れ」という事ですね。

次の「version INTEGER」は、「LDAP Protocol の version を Universal INTEGER 型でエンコードしたもの」という意味です。そして、1 から 127 という制限も有るようです。
今回は version 3 を使うので、3 をエンコードした次の 3 byte に成ります。

TypeLengthValue
213

では、その次の「LDAPDN 型の name」とは?
これは、「name を LDAPDN 型でエンコードしたもの」という意味です。
じゃあ、「LDAPDN 型って何よ?」と思ってもう一度 RFC を読み返すと、section 4.1.3 に「LDAPString 型、ただし RFC  4514 の制限を満たすもの」と書いてあります。

LDAPDN ::= LDAPString
           -- Constrained to <distinguishedName> [RFC4514]
 


で、LDAPString について探すと、ちょっと上の section 4.1.2 に「ISO 10646 に規定された文字を UTF-8 でエンコードしたものを OCTET STRING 型でエンコードした物」と記載されています。

LDAPString ::= OCTET STRING -- UTF-8 encoded,
                            -- [ISO10646] characters


なんか、たらい回しにされている感がありますが、要約すると LDAPDN 型とは
「OCTET STRING 型でエンコードした文字列。ただし、DN として有効な文字列であり、マルチバイトの際は UTF-8 でエンコードしたもの」という事ですね。
UTF-8 なのに UTF8String では無く OCTET STRING を使うのは少し気持ちが悪いですが、互換の問題でしょうか?(よく分からない)

ただ、LDAPDN 型については分かりましたが、私には name が何の事か、RFC から判断する事はできませんでした。仕方ないので既存の LDAP Clinet をハッキングしてみた所、どうやら LDAP の bind dn の事の様です。
なので、今回の name は uid=user,dc=cyberagent,dc=co,dc=jp を OCTET STRING 型でエンコードすれば良いでしょう。

uid=user,dc=cyberagent,dc=co,dc=jp の ASCII Code は次の 34 byte です。
117 105 100 61 117 115 101 114 44 100 99 61 99 121 98 101 114 97 103 101 110 116 44 100 99 61 99 111 44 100 99 61 106 112
結果として、name は以下の36 byte になります。

TypeLengthValue
434117 105 100 ...... 112


BindRequest 最後の AuthenticationChoice 型 の authentication とは何でしょう?
これは、BindRequest のすぐ下に書いてあります。
「[0] OCTET STRING 型の simple か  [3] SaslCredentials 型の sasl を選べ」だそうです。今回は simple auth を使用するので、simple を使いましょう。

AuthenticationChoice ::= CHOICE {
     simple                  [0] OCTET STRING,
                             -- 1 and 2 reserved
     sasl                    [3] SaslCredentials,
     ... }


[0] OCTET STRING 型とは、「OCTET STRING 型、ただし、Type は Class を Context-specific の独自拡張に、tag  は 0しろ」という意味です。すると、Type は上位 2 bit が Context-specific を表す 1 0 に、次の 1 bit は Primary を表す 0 に、最後の 5 bit は 0 になるので 128 に成ります。

しかし、やはり私には simple で何を送信したら良いのか、RFC からだけでは判断できませんでした。これも先ほどと同様に既存の LDAP Client ツールをハッキングした限りではパスワードをそのまま記載すれば良いようです。
今回、パスワードは openSesami なので、最終的に 
authentication は次の様に成ります。

TypeLengthValue
12810111 112 101 ...... 105
(openSesami の ASCII Code は 111 112 101 110 83 101 115 97 109 105)

これで、version, name, authentication のそれぞれが分かりました。
後は、
 [APPLICATION 0] SEQUENCE 型でまとめれば BindRequest の完成です。
今回の BindRequest は以下のようになるはずです。

BindRequest
TypeLengthValue
9651versionnameauthentication
TypeLengthValueTypeLengthValueTypeLengthValue
213434117 ...... 11212810111 ...... 105

さて、BindRequest が出来たので、今度はこれを LDAPMessage でラップします。
LDAPMessage については RFC4511 section 4.1.1 をご覧下さい。

LDAPMessage ::= SEQUENCE {
     messageID       MessageID,
     protocolOp      CHOICE {
          bindRequest           BindRequest,
          bindResponse          BindResponse,
          unbindRequest         UnbindRequest,
          searchRequest         SearchRequest,
          searchResEntry        SearchResultEntry,
          searchResDone         SearchResultDone,
          searchResRef          SearchResultReference,
          modifyRequest         ModifyRequest,
          modifyResponse        ModifyResponse,
          addRequest            AddRequest,
          addResponse           AddResponse,
          delRequest            DelRequest,
          delResponse           DelResponse,
          modDNRequest          ModifyDNRequest,
          modDNResponse         ModifyDNResponse,
          compareRequest        CompareRequest,
          compareResponse       CompareResponse,
          abandonRequest        AbandonRequest,
          extendedReq           ExtendedRequest,
          extendedResp          ExtendedResponse,
          ...,
          intermediateResponse  IntermediateResponse },
     controls       [0] Controls OPTIONAL }

MessageID ::= INTEGER (0 ..  maxInt)

maxInt INTEGER ::= 2147483647 -- (2^^31 - 1) --

一見長く見えますが、早い話が「INTEGER 型の MessageID と ProtocolOp (今回は BindRequest) を SEQUENCE にしろ。オプションで controls をつける事もある」との事です。
また、MessageID については「0 でない値にしろ。同一セッション中で、サーバーが処理を終了するまでは同じ値を再利用するな。クライアントは毎リクエスト毎にインクリメントするのが良いだろう」とすぐ下の section 4.1.1.1 に書いてあります。

今回は controls は使いません。
また、大抵の場合、BindRequest は各セッションの最初に行うと思われるのでここでは 1 を使う事にします。
つまり、LDAP Client が Server に投げる LDAPMessage は MessageID の 1 と先ほどの作成した BindRequest を SEQUENCE にした物なんですね。ここは普通に実装すれば大丈夫の様です。

この LDAPMessage を受け取ったサーバーはどうするのでしょうか?
ざっくり言うと、Client と逆の事をすれば良いんです。

まず、LDAPMessage をほどいて Request を取り出します。 Request の Type を見ると、それが Application Class, tag が 0 の BER で有る事が分かるでしょう。
このような BER は LDAP プロトコルにおいては BindRequest しかありません。(LDAP において Application Class で Type が同じ BER は同じ型と思って大丈夫です。)
なので、中身の 1個目が version, 2個目が name, 3個目が authentication という事が分かります。
また、authentication の Type は Context-specific Class で tag が 0 です。Context-specific Class については前後関係を確認しないと何の型なのか分からないのですが、少なくとも BindRequest 中の authentication では simple を表し、その Value はパスワードのはずです。

後は、実際に認証を行い、その結果から BindResponse を作成し、Request と同じ MessageID (今回は 1) を使って LDAPMessage を作成し、Client に返せば終了です。

以上、駆け足でしたが LDAP の通信についてイメージできましたでしょうか?
今回は BindRequest を例にご説明しましたが、同様に他の Request も調べれば
LDAP Server のモックは実装できると思います。

今回、私は、普段とは違うレイヤーの技術を調べてみて「少し面白いな」と思いました。
この気持ちを他の人と共有したいと思って記事にしてみたのですが、皆さんに少しでも伝われば幸いです。

最後に、私が作成した LDAP のモックを github で公開しました。
まだα版ですしドキュメントも揃っていませんが、興味のある方は使ってみてください。
Ruby 2.0 と Mac OSX で動作確認をしました。

残念な事に Ruby の ActiveLdap はまだ動きません。でもnet-ldap という別の Ruby の gem や ldapadd, ldapsearch, ldapmodify, ldapdelete という Unix 系の各種コマンドの動作確認は取れました。
詳細は README.md をご覧下さい。

少し長かったですが、おつきあい頂きありがとうございました。