久々にちょっとPKI関連ネタです。いわゆるデジタル証明書(X.509証明書)には、主体者名(Subject Name)や発行者名(Issuer Name)に識別名(DN: Distinguished Name)を使います。例えば、
CN=yourname@example.com,O=example,C=JPのようなものです。カンマで区切った一つ一つを相対識別名(RDN: Relative Distinguished Name)と呼んでいます。
O=example一般的には相対識別名(RDN)は、「一つの」属性タイプと属性値のペア(AttributeTypeAndValue) より構成されます。
属性タイプ=属性値ただ、「一般的には」と書いた通り、RDNについて複数のAttributeTypeAndValueを持つことも可能です。これをMulti-valued RDNと呼んでおり、プラス"+"記号でつないで以下のように表現します。
O=example
属性タイプ1=属性値1+属性タイプ2=属性値2...Googleとかで「Multi-valued RDN」で検索するとわかると思うんですが、英語では結構あるのに、日本語で触れている記事って、自分のブログ以外みつからないみたいなんですよね。 今日は、拙作の暗号ライブラリ jsrsasign や OpenSSL を使いながら、証明書識別名のMulti-valued RDNや、識別名について掘り下げてみたいます。
CN=User1+serialNumber=123
エントリと識別名
LDAPや、その元となっているX.500ディレクトリサービスでは「エントリ」のツリー構造により情報を管理し、例えば会社、部門、社員は以下のように管理することができます。
LDAPでは、あるエントリを特定するために「○×商事」の「総務部」の「佐藤二朗」さんという特定の仕方をします。エントリの名前、「総務部」や「佐藤二朗」という値は、属性タイプという型をつけることができ、組織名(O: Organization Name)、部署名(OU: Organizational Unit Name)、一般名(CN: Common Name)などのタイプがあります。
例えば、営業の鈴木さんを特定するときに一番上までのエントリを辿って、以下のように表現します。これを「識別名(DN: Distinguished Name)」と呼びます。これにより他の部署のSuzukiさんとも区別できます。
CN=Suzuki,OU=Sales,O=MaruBatsu識別名のうち、「OU=Sales」のようにエントリの丸の中を相対識別名(RDN: Relative Distinguished Name)と呼びます。
また、このエントリのツリー構造をDIT(Directory Information Tree)と呼びます。
Muti-valued RDNとは?なぜ必要か?
上記で説明した識別名(DN)で、同じ営業部に鈴木花子さんが二人いたらどうしましょう。一般名に区別するための数字を追加したり、追加の値として、社員番号やメールアドレスで区別することもでき、エントリを追加しても良いのですが、どれもイマイチ。
そこで、一つのエントリに複数の値をつけて識別することもできます。これを Multi-valued RDNと呼んでいます。
同性同名の人は多分いるでしょうから、社員番号やメールアドレスなど他の一意なものと組み合わせて管理するのはスマートな管理方法だと思いますし、一部の商用のディレクトリサーバー製品では、利用者数ベースでライセンス課金するために、エントリ数を使うものもありますので、Multi-valued RDNを使うことによってコスト削減を狙うこともできます。ただ、Multi-valued RDNは、すべての製品で使えるというものでもないので(例えば、とある製品のスマートカードとか802.1X認証とかで後になって問題になったことがありましたよね、、、)本当に使ってしまってよいかどうかは、アプリケーションと相談して決める必要があるでしょう。
識別名の文字列表現
識別名の文字列表現にはざっくり2つの表現があります。
CN=Matsuda Kenji,OU=Sales,O=MaruBatsuDITのツリー構造の下から順にカンマ","でつないだ方法と、上から順にスラッシュ"/"でつなぐ方法です。
/O=MaruBatsu/OU=Sales/CN=Matsuda Kenji
カンマで逆順につなぐ方法はRFC 2253 Lightweight Directory Access Protocol (v3): UTF-8 String Representation of Distinguished Namesや後継の4514で規定されています。LDAPのアプリケーションソフトウェアでは一般的に使われている方法です。
もう一方の、先頭にスラッシュを付け、スラッシュで正順でつなぐ方法はOpenSSL compatフォーマットと呼ばれ、OpenSSLで標準的に使われるとともに、OpenSSL系のウェブサーバーであるApache HTTP Server、nginx、lighttpdなどの設定などで使われる方法です。
Multi-valued RDNの場合には、どちらの形式でも値をプラス"+"記号でつないで表現します。
CN=Matsuda Kenji+emailAddress=matsu@mb.com,OU=Sales,O=MaruBatsuプラスで繋がれた値の表示順序については、特に決まりは無いと認識しており、以下のMulti-valued RDNでCNとemailAddressのどちらを先にしても良いはずです。これがどのようにASN.1でエンコードされるかは後で述べます。
/O=MaruBatsu/OU=Sales/CN=Matsuda Kenji+emailAddress=matsu@mb.com
CN=Matsuda Kenji+emailAddress=matsu@mb.com
emailAddress=matsu@mb.com+CN=Matsuda Kenji
次にCNやOUなどの属性タイプの文字列表現ですが、どのように表記しなければならいといった厳格な標準はなく、実装もバラバラであることがわかっています。8年前にXAdES長期署名に関連して、識別名の中の属性タイプの表記の実装状況について調査しており、その時にまとめた表を再掲します。
X.509証明書プロファイルを定めたRFC 5280の4.1.2.4節 発行者名(Issuer)では、識別名の属性タイプとして対応しなければならない(MUST)リストと、対応すべき(SHOULD)属性タイプのリストが掲載されており、表中ではMUSTを黄緑、SHOULDを黄色、その他、証明書で実際に使われることのある属性タイプのリストを白とし、.NETや各種Javaベースの暗号ライブラリでどのように属性タイプが表記されるかをテストしました。表を見ればわかるとおり、結果はかなりバラバラです。また、S/MIMEのために使用される事があり、実際の証明書でもかなり含まれているemailAddressの属性タイプも、標準では実装を求めていないために対応にばらつきが出ているように思います。
今、見直してみると当時はなかったEV証明書用の以下の属性タイプも、今ならテストすべきだったかなぁと思います。
- jurisdictionOfIncorporationL - 法人登録管轄地(市町村)
- jurisdictionOfIncorporationSP - 法人登録管轄地(都道府県)
- jurisdictionOfIncorporationC - 法人登録管轄地(国)
また、 カンマつなぎの識別名表記であるRFC 2253とその後継のRFC 4584の違いについて8年前の記事 でまとめており、仕様の改定によって、より識別名表記が一意になる方向に修正されていますが、 仕様の中で「RFC 4514は識別名文字列は一意にならない(=正規化しない)」という 事が明記されており、識別名文字列は、様々な表現が許されており、 単純な文字列比較では同じであるかどうかを判断できない事に注意しなければなりません。
識別名のASN.1定義と構造
次に、識別名が、ASN.1 DERエンコーディングにより、どのようにバイト列にエンコードされるのかを、
説明したいと思います。まず最初に、識別名のASN.1定義を紹介しましょう。
RFC 5280 4.1.2.4 Issuerより
つまり、// X.500名、識別名(DN)はRDNの並び(SEQUENCE) Name ::= CHOICE { rdnSequence RDNSequence } RDNSequence ::= SEQUENCE OF RelativeDistinguishedName // RDNは、AttributeTypeAndValue 1つ以上のSET // つまり、複数AttributeTypeAndValueがあってもよい。 // これが複数あれば Multi-valued RDN RelativeDistinguishedName ::= SET SIZE (1..MAX) OF AttributeTypeAndValue // 属性タイプと属性値のペア AttributeTypeAndValue ::= SEQUENCE { type AttributeType, value AttributeValue } AttributeType ::= OBJECT IDENTIFIER AttributeValue ::= ANY // 属性値はANYと定義していながらも、DirectoryStringで // 定義されたいずれかの文字タイプを使用する DirectoryString ::= CHOICE { teletexString TeletexString (SIZE (1..MAX)), printableString PrintableString (SIZE (1..MAX)), universalString UniversalString (SIZE (1..MAX)), utf8String UTF8String (SIZE (1..MAX)), bmpString BMPString (SIZE (1..MAX)) }
- 識別名(DN)は、相対識別名(RDN)の並び(SEQUENCE OF)であり
- 相対識別名(RDN)は、属性タイプと値(AttributeTypeAndValue)の集合(SET OF)であり
- 属性タイプと値(AttributeTypeAndValue)は、属性タイプと値の並び(SEQUENCE)である
- SEQUENCEは配列のようなもので、順序関係のある並びを表す際に使います。
- SETは集合のようなもので、構成要素の中には特に順序関係はない場合に使います。
- 単にSEQUENCEやSETとなっている場合には、構成要素のASN.1クラスが異なる場合に 使います。上の例ではAttributeTypeAndValueがそれに当たります。
- SEQUENCE OF、SET OFとした場合、構成要素のASN.1クラスが同じ型の場合に 使います。上の例では、NameやRDNがそれに当たります。
それでは、例として以下の識別名をASN.1 DERエンコーディングしてみましょう。
CN=aaa,O=TEST,C=JPRFC 2253の場合には、逆順でRDNが並ぶので、以下のようにエンコードされます。
ASN.1データはデータ型を表すタグ、バイト長、値データより構成され、上の例の最後の行では、0CがUTF8String型、03がバイト長(=3)、616161(=aaa)が値を表しています。302A SEQUENCE(30) OF -- DN 310B SET(31) OF -- RDN[1] 3009 SEQUENCE(30) -- AttributeTypeAndValue 0603550406 ObjectIdentifier(06) countryName 13024A50 PrintableString(13) "JP" 310D SET(31) OF -- RDN[2] 300B SEQUENCE(30) -- AttributeTypeAndValue 060355040A ObjectIdentifier(06) organizationName 0C0454455354 UTF8String(0C) "TEST" 310C SET(31) OF -- RDN[3] 300A SEQUENCE(30) -- AttributeTypeAndValue 0603550403 ObjectIdentifier(06) commonName 0C03616161 UTF8String(0C) "aaa"
さて、次にMulti-valued RDNの場合にはどのようにエンコードされるのか、下の例を元に見てみましょう。ここでは、CN=aaaとCN=aの2つのAttributeTypeAndValueが使用されています。
CN=aaa+CN=a,O=TEST,C=JPこれをASN.1 DERエンコーディングすると以下のようになります。最後のRDNに注目してください。CN=aとCN=aaaと二つのAttributeTypeAndValuesがあることが確認できます。また、また、CN=aとCN=aaaでは、必ずCN=aが先に来ることにも注目です。
このRDN中のCN=a、CN=aaaの順序関係にはASN.1 DERとBERのちょっとした違いが関係があります。DERはBERのサブセットでなんですが、BERでは複数の表現が許されるのに対し、DERでは必ず一意な表現になります。その違いを表にまとめました。3034 DN 310B RDN[1] C=JP 3009 0603550406 13024A50 310D RDN[2] O=TEST 300B 060355040A 0C0454455354 3116 RDN[3] CN=aaa+CN=a SEQUENCE(30)が2つある 3008 ATV[1] CN=a CN=aの方が先に来ている 0603550403 0C0161 300A ATV[2] CN=aaa 0603550403 0C03616161
ASN.1 DER | ASN.1 BER | |
---|---|---|
概要 | ASN.1の一意なエンコード規則 | ASN.1のエンコード規則。DERのスーパーセットでDERであればBER |
共通の特徴 | 通信の世界では長い歴史のある、CPUや整数型のサイズに制限されない、巨大なデータも扱える、任意の構造化データを扱えるデータ表現。XML, JSONに比べコンパクト。 | |
用途 | 証明書、CRL、OCSP、RFC3161タイムスタンプ | S/MIMEデータ、CMS署名・暗号化データ、PKCS#12 |
比較 | 必ず表現は一意。超巨大なデータでも長さが予めわかっていないといけないので、ストリーム処理など不向き | 複数の表現がある。超大きなデータでも取り扱い可能 |
SET | 要素のバイト列で昇順ソートする | ソートしなくて良い |
BOOLEAN | TRUEのみ使え、FALSEは省略するようクラス定義 | TRUE、FALSEが使える |
不定長表現 | 長さ表現は一意で、予めデータサイズがわかっていないといけない。 | 長さ表現で不定長表現が使え、長さを8000とした場合それは開始記号で0000が続くまで一つの要素であり、大きなデータも扱いやすい。 |
SETの要素は、各要素をASN.1エンコードしたときの昇順の辞書順でソートされ、ざっくり言えば、
- 要素の短い物程先
- 同じ長さなら属性タイプの長さが短い方が先
全体の長さが同じ時、3008 0603550403 0C0161 CN=a 300A 0603550403 0C03616161 CN=aaa ^^ 全体の長さLが08, 0Aの順になるので同じ属性タイプ長なら属性値の短い方が先 C,O,OU,CNなど主要な属性タイプはOIDの値が2.5.4.xになるので同一属性タイプ長
^^ 全体の長さは同じなら 3011 0603550403 0C0A6162636465666768696A CN=abcdefghij 3011 060B2B0601040182373C020103 0C024A50 jurisdictionOfIncorporateC=JP ^^ 属性タイプの値の短い方が先
OpenSSLのMulti-valued RDN対応
OpenSSLはMULTI-valued RDNに対応しており、"-multivalue-rdn"をつけるだけです。 例えば、既存の秘密鍵でワンライナーでMulti-valued RDNの自己署名証明書を作りたい時
openssl genrsa 2048 > a.prvMulti-valued RDNの証明書発行要求を作りたいとき
openssl req -new -key a.prv -x509 -subj /C=JP/O=Test/OU=b+CN=a -out c.cer -multivalue-rdn
openssl req -new -key a.prv -subj /C=JP/O=Test/OU=b+CN=a -out c.csr -multivalue-rdnとなります。
jsrsasignのMulti-valued RDN対応
jsrsasignは、私が趣味で作ったPure JavaScriptによる暗号ライブラリでして、2010年ぐらいからボチボチ暇を見つけては昨日を追加しており、最初はRSA署名だけだったものが、ASN.1や証明書やタイムスタンプやJOSEなんか、自分が「欲しいな」と思った時に増築を繰り返しており、PKIやASN.1やJOSE(JWS,JWT,JWK)関係でちょっと試したいなと思った時に重宝しています。
ウェブブラウザ上でも、Nodeでも使え、APIドキュメントやサンプルも充実させているので、結構ユーザは世界中にいたり、最近はSONYや横河(や勝手にうちの会社(^^;)のハードウェア商品でも使われていることが発覚したり、Nodeのnpmパッケージは月間10万弱のダウンロードがあるようで、ホントありがたい話です。
JavaScriptの暗号ライブラリのAPIとしては、W3C Web Crypto APIなどあるんですが、モバイルブラウザでサポートしていないケースがあったり、古い暗号が使えなかったり、ちょっと書こうと思っても何行も書かなければいけなかったり、面倒くさいんですよね。そこで、jsrsasignでは、「なるべく少ない行数でやりたい事ができる」っていうのを目標にしていて、例えば鍵なんかは秘密鍵でも公開鍵でもPKCS#5でもPKCS#8でもJSON Web KeyでもなんでもKEYUTIL.getKeyに渡してしまえば適当に処理します。また、PCでもスマホでもNodeでも、多少古い環境でもJavaScriptさえ動けば使えるようになっています。また、APIドキュメントやチュートリアルの資料もできる限り潤沢に用意したつもりです。
割と最新の話まで入っている英語の入門スライドがあったり、
またちょっと古いですが、2013年にJNSAのWGでお話したjsrsasignとjsjwsが別の開発ラインだった時の入門スライド
があるのでよかったら参考にしてください。
ドキュメント類は拙い英語のものしかなくて申し訳ないですが、問題とかあれば、Issueには日本語で入れて頂いて構わないので入れて頂ければと思います。
で、jsrsasignをMulti-valued RDN対応させたり、カンマ繋ぎDN対応したいなと思っていて、ようやく6.2.2をリリースした最近になってから対応させました。 例えば、Multi-valued RDNの識別名がどのようにASN.1 DERエンコードされるのかなんて話は、次のように確認できます。
あとは、証明書発行要求(CSR)を作ったり、% node > var X509Name = require("jsrsasign").KJUR.asn1.x509.X500Name; > new X509Name({str: "/C=JP/O=T1+CN=kjur"}).getEncodedHex(); '3027310b3009060355040613024a5031183009060355040a0c025431300b06035504030c046b6a7572'
証明書を発行したりする時にもMulti-valued RDNが使えます。var rs = require("jsrsasign"); var kp = rs.KEYUTIL.generateKeypair("RSA", 2048); pem = rs.KJUR.asn1.csr.CSRUtil.newCSRPEM({ subject: {ldapstr: 'OU=T1+CN=example.com,O=Test,C=US'}, ext: [ {subjectAltName: {array: [{dns: 'example.net'}]} ], sbjpubkey: pubKeyPEM, sigalg: "SHA256withRSA", sbjprvkey: prvKeyPEM });
割と融通が利くので、よかったら使ってやってください。var pem = KJUR.asn1.x509.X509Util.newCertPEM({ serial: {int: 4}, sigalg: {name: 'SHA1withRSA', paramempty: true}, issuer: {str: '/C=US/O=a'}, notbefore: {str: '130504235959Z'}, notafter: {str: '140504235959Z'}, subject: {ldapstr: 'OU=kjur+CN=kjur,O=b,C=US'}, sbjpubkey: kp.pubKeyObj, ext: [ {basicConstraints: {cA: true, critical: true}}, {keyUsage: {bin: '11'}}, ], cakey: kp.pubKeyObj });
おわりに
というわけで長々、Multi-valued RDNや識別名(DN)のことでダラダラ書いてしまいました。ごめんなさい。誰かの参考になれば良いかな、と思います。
追記(2016.12.19)
あっ、誤解されないように書いておきますと、私としては、Multi-valued RDNを広めたいとか、使うべきだとか言うつもりは毛頭ありません。相互運用性が高い方向でインフラ設計するのが原則であり、使わなくて済むなら使わない方がいいでしょう。ただ、受け取ったとしても、びっくりしないでね、と、、、、w