GnuPG 2 の gpgsm がどのような CMS を生成しているのか可視化する必要にせまられ、asn1dump というコマンドを Haskell で書きました。
openssl
openssl x509 では、証明書を可視化することができます。一方、openssl smime では、CMS の可視化はできないようです。また、openssl pkcs7 では、CMS の証明書だけが表示されます。まったく役に立ちません。
そもそも、僕は OpenSSL が大嫌いです。ソースコードを読んだことがある人なら、こんな汚いコードが世界中の安全を支えているなんて背筋が凍る思いだ、ということに同意してもらえるでしょう。いつまでたってもセキュリティ・ホールはなくならないだろうと、確信できます。
openssl コマンドのユーザインターフェイスも最悪です。引数はまったく覚えられません。openssl x509 にしろ、openssl pkcs7 にしろ、どうして入力の書式を指定しないといけないのでしょうか? DER なのか PEM なのかは、ファイルの先頭で判別できるでしょう?
どうして、x509 や pkcs7 を指定する必要があるのでしょうか? ASN.1 を解析すれば、何を表示しようとしているか分るはずです。
openssl pkcs12 は PEM が複数入ったファイルを生成するのに、それを openssl x509 に指定すると最初の PEM しか表示しないのは、どうしてなんでしょう?
二種類の自動判別
という訳で、可視化ツールを作ることにしました。二種類の自動判別を用いて、ユーザはファイル名だけ指定すればよいようにします。
使用言語
入力ファイルが PEM だったら、Base64 を取り出して、復号化する必要があります。大富豪プログラマなら、ファイル全体を読み込んで、処理するでしょう。それだと、なんの技術的な挑戦もありません。
以前、pgpdump を書いたときは、複数のバッファが連携し合うコードを書きました。UNIX のパイプを実装したようなものですね。もう C は使わないので、この方法を取るなら D で書くんですが、できるのは分っているのでつまりません。
やっぱり、遅延評価を使いたいと思いました。しかし、UNIX のパイプが遅延評価だといっても、シェルスクリプトは書きたくありません。今までに、いくらでも書いたことがあるからです。
というわけで、Haskell を使うことにしました。Haskell では関数レベルで遅延評価をやってくれます。また、Parsec という強力なモナド・パーサーがあります。
入力部の実装
まず、先頭の数バイトを見て、DER か Base64 か PEM かを判断します。たとえば PEM だと判断した場合、以下のような処理をします。
解析部の実装
- Parsec で書いた「Intパーサ」で、DER を TLV(type length value)の木に変換する
- Parsec で書いた「木パータ」で、TLV の木を簡略化する (実装の都合で本質ではない)
- 簡略化した木を判別する (signedData/EnvelopedData/RSAPrivateKey/Certificate)
- 判別した書式に従って、簡略化した木を可視化する
DER と ASN.1
今回、両方の仕様書を読みましたが、とっても分りにくいです。こんな仕様書を読むより、ASN.1 概要の方が断然お勧めです。分りやすく、必要十分な情報があります。Ishida So さんに感謝します!
勉強したお陰で、いままで謎だったことが分りました。
たとえば、TBSCertificate はこんな感じです。
TBSCertificate ::= SEQUENCE { version [0] EXPLICIT Version DEFAULT v1, serialNumber CertificateSerialNumber, signature AlgorithmIdentifier, issuer Name, validity Validity, subject Name, subjectPublicKeyInfo SubjectPublicKeyInfo, issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL, -- If present, version MUST be v2 or v3 subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL, -- If present, version MUST be v2 or v3 extensions [3] EXPLICIT Extensions OPTIONAL -- If present, version MUST be v3 }
- DEFAULT や OPTIONAL が付いているフィールドは省略できます
- それらには、省略しているか否かを判定できるようにタグを付けます ([0] など)
- EXPLICIT なら tag { seq { ... } } のようになります
- IMPLICIT なら tag { ... } のようになります (中身の型(seq など)は分っているので省略できるということ)
BasicConstraints の定義は意味不明でした。両方のフィールドが省略できるのに、タグが付いていません。フィールドが一個しかない場合は、どっちらだと判断するのでしょう? (案の定、このフィールドの値がおかしい証明書も見つけました。)
BasicConstraints ::= SEQUENCE { cA BOOLEAN DEFAULT FALSE, pathLenConstraint INTEGER (0..MAX) OPTIONAL }
分ったこと
作成したコマンドは asn1dump という名前にしました。asn1dump で、証明書や S/MIME の署名、暗号文を可視化して、いくつか分ったことがあります
- openssl x509 は、知らない version 3 extensions は表示しません
- asn1dump では、実装してない version 3 extensions があったら、内部表現をそのまま表示します。show するだけです!
- gpgsm で S/MIME の暗号化すると、デフォルトの共有鍵暗号は 3DES CBC でした!
- いや、man を読めば分ることですが
- AES じゃなくていいんでしょうか?
- gpgsm では、RSAES-OAEP も RSASSA-PSS も実装していません!はぁ。
- Outlook Express が生成する S/MIME の署名には、"Outlook Express" という署名者属性が付いています
- 独自拡張は、MS らしいですね:p