あどけない話

インターネットに関する技術的な話など

asn1dump

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 しか表示しないのは、どうしてなんでしょう?

二種類の自動判別

という訳で、可視化ツールを作ることにしました。二種類の自動判別を用いて、ユーザはファイル名だけ指定すればよいようにします。

  • 入力ファイルを判別する
    • DER ならそのまま
    • Base64 なら、復号化して DER へ
    • PEM なら、Base64 を取り出て、復号化して DER へ (複数の PEM にも対応する)
  • DER を解析した ASN.1 の構造を判別する
    • CMS なら OID が付いているので、signedData か envelopedData か分る
    • PKCS-1 の RSAPrivateKey は、構造が簡単なので判別できる
    • それ以外なら X.509 の Certificate だと思う

使用言語

入力ファイルが PEM だったら、Base64 を取り出して、復号化する必要があります。大富豪プログラマなら、ファイル全体を読み込んで、処理するでしょう。それだと、なんの技術的な挑戦もありません。

以前、pgpdump を書いたときは、複数のバッファが連携し合うコードを書きました。UNIX のパイプを実装したようなものですね。もう C は使わないので、この方法を取るなら D で書くんですが、できるのは分っているのでつまりません。

やっぱり、遅延評価を使いたいと思いました。しかし、UNIX のパイプが遅延評価だといっても、シェルスクリプトは書きたくありません。今までに、いくらでも書いたことがあるからです。

というわけで、Haskell を使うことにしました。Haskell では関数レベルで遅延評価をやってくれます。また、Parsec という強力なモナド・パーサーがあります。

入力部の実装

まず、先頭の数バイトを見て、DER か Base64 か PEM かを判断します。たとえば PEM だと判断した場合、以下のような処理をします。

  • Parsec で書いた「行パーサ」で、Base64 部分を取り出す
  • Codec.Binary.Base64.String ライブラリで、Base64 を復号化し、DER を取り出す
  • Char の配列を Int の配列にする

解析部の実装

  • 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