ASN.1 データ生成/解析の事始

1.ASN.1とは

1.1 ASN.1定義

ASN.1、という規格をご存知でしょうか。RFCを見ると時々出てくる、以下の定義を指します。以下はRFC 5280(インターネットX.509 PKI - 証明書と CRL のプロファイル)の抜粋です。

Certificate ::= SEQUENCE {
    tbsCertificate TBSCertificate,
    signatureAlgorithm AlgorithmIdentifier,
    signatureValue BIT STRING }

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 shall be v2 or v3 subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
    -- If present, version shall be v2 or v3 extensions [3] EXPLICIT Extensions OPTIONAL
    -- If present, version shall be 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
}

ASN.1規格の詳細な定義内容、例えば、上記での「Certificate ::= SEQUENCE」 の”SEQUENCE”って何?とかは、参考文献「ASN.1詳細」「Wikipedia」「ASN.1の定義」 をご覧ください。特に、「ASN.1詳細」 は、非常に分かりやすく書いてあってお勧めです。

ここでは、とりあえず”ASN.1”というデータを生成、および解析をしなきゃいけない人用に、とっかかりを書いておこうと思います。

1.2 ASN.1定義例

いきなり電子証明書は難しいので、簡単なところで例を示します。

あるシステムで、AホストからBホストへデータを通信するものとします。

通信データを”Price”データとすると、”Price”データは、”amonut”というINTE GE R型のデータと、”name”というOCTET STRING型データの集合(SEQUENCE)である、という意味の定義が以下になります。

Price ::= SEQUENCE {
   amount INTEGER
   name OCTET STRING
}

例として、上記ASN.1定義でamonntを0xa(10)、nameを”testdata”とした ASN.1データは以下の感じになります。

30 0d 02 01 0a 04 08 74 65 73 74 64 61 74 61

上記をぱっと見て分かるのは、以下のようなことです。

  • ASN.1定義と具体的な値が一緒ならがデータは一緒ってこと?
  • データ内を見ても”amount”というものは出てこなくね?

ASN.1定義が一緒なら、どのような実装でもデータ内容は同一です。小難しいエンディアンとかは、とりあえず関係ありません。それがASN.1のいいところです。

そして、データ内を見ても”amount”というものは出てきません。”amount”というのは、「あくまで定義」であることを忘れてはいけません。

実務では、上記ASN.1データが飛び交ってるパケットキャプチャを見て、不良原因 を解析するなんてことを、よくやることになります。

不良解析の都度、ASN.1定義から探していたら調査にならないので、見た瞬間どこがどのデータであるかを一瞬でわかるようになる必要があります。

1.3 ASN.1と実データの関連

ASN.1での記述内容は、あくまで「定義」であり、具体的なデータ内容を指していません。

しかし、実務上、ASN.1といえば、ASN.1定義をDER形式、あるいはPEM形式にした、具体的なデータ自体を指すことが多いです。ASN.1定義とデータ自体を、区別することなく言い表しています。

DER形式とは、バイナリ形式と考えて差し支えありません。PEM形式はテキスト形式です。PEM形式は、DER形式をBase64でエンコードしただけですので、とりあえずここでは扱いません。

また、BER形式等の、DER形式とは別の情報符号化方法も、あることはありますが、複雑になるので、ここでは扱いません。

ASN.1定義は、以下のような流れで、実データとなります。

ASN.1定義 -> 定義へ具体的な値を設定 -> DER形式ファイル

ここでは便宜上、ASN.1定義に具体的な値を設定し、DER形式にしたデータを「 ASN.1データ」と呼びます。

では、ASN.1定義をDER形式で表すとは、具体的にはどのように行うのでしょうか。次はASN.1データの生成方法を見ていきたいと思います。

2.ASN.1データの生成

2.1 生成方法

ASN.1データを生成する方法は大まかに二種類の方法があります。

  • (1)自力生成

    C言語、Java及びリトルランゲージ(Perl/Ruby/Python)などでASN.1データを生成(解釈)する方法です。

    大体の場合、ASN.1用処理を行うためのライブラリ(OpenSSL等。以下ASN.1ライブラリ)を使用して実装することが多いです。以下は各言語での代表的なASN.1ライブラリです。詳細は参考文献で示したURL先をご覧ください。

    • C言語

      「OpenSSL」

    • Java

      「Bouncy Castle」

    • Perl

      「Convert-ASN1」

    • Python

      「PyASN1」

    • Ruby

      「OpenSSL::ASN1」

    なお、ASN.1ライブラリは上記だけではありません。探せば複数あると思います。

  • (2)ASN.1コンパイラ

    ASN.1定義を読み込み、ASN.1データを生成するようなソースコード等を出力するプログラムを指します。業務用の製品として何社かありますが、無料ではありません。

2.2 生成方法の具体例(OpenSSL)

例として、以下のようなASN.1定義によるASN.1データを生成したいと思います。データ内容は、amonntを0xa(10)、nameを”testdata”としたデータとします。

Price ::= SEQUENCE {
   amount INTEGER
   name OCTET STRING
}

2つの処理系での実装例を示してみます。まずはC言語です。ASN.1ライブラリはOpenSSLを選択しました。[asn1sample] に実装を置いています。

asn1sample.lzhの中身は以下です。

  • asn1.config

    ASN.1定義+実データ設定ファイル(cnfファイル)

  • asn1make

    Linux用実行ファイル

  • asn1make.c

    ソースコード

  • der.out

    実行結果(ASN.1データ)

  • makefile

    Makefile

ソースコードの中心は以下です。以下の内容は 「OpenSSL」内のドキュメントや「OpenSSL」内の実装例を参考にしました。

//ASN1内部情報の作成
asn1data = ASN1_generate_nconf(genstr, cnf);
NCONF_free(cnf);
cnf = NULL;

if (asn1data == NULL) {
    printf("Can't find inner data\n");
    return -1;
}

//内部形式(i)からDER形式(d)にした場合の長さを取得する
len = i2d_ASN1_TYPE(asn1data, NULL);

if (!BUF_MEM_grow(buf,len))
    return -1;

p=(unsigned char *)buf->data;

//pに実際のDER形式データを取得する。
i2d_ASN1_TYPE(asn1data, );

今回は、ASN1_generate_nconf()という関数を使用して作ってみました。ASN1_generate_nconf()の肝は第二引数のcnfです。cnfはASN.1定義+実データを定義しているファイルです。上記ではasn1.configが該当します。

なお、第一引数は、生成するASN.1定義(以下では”asn1”)を設定しています。デフォルトの”asn1”でいいようです。

具体的なcnfファイル内容が以下です。

asn1 = SEQUENCE:seq_section

[seq_section]

field1 = INTEGER:0xa
field2 = OCTETSTRING:testdata

ASN1_generate_nconf()が返すのは、OpenSSLの内部構造体による生成データなので i2d_ASN1_TYPE(i2dとは、inner(内部)toDERの略)にてDER形式の長さとデータの 取得しています。

起動は以下のように行います。

$ ./asn1make -c asn1.config -d der2.out
$ ls
Makefile asn1.config asn1make asn1make.c der.out  der2.out

生成されたASN.1データは以下になります。

$ hd der2.out
00000000  30 0d 02 01 0a 04 08 74  65 73 74 64 61 74 61     |0......testdata|

2.3 生成方法の具体例(Python)

もうひとつ、Pythonでの例も挙げておきます。python-2.6.5を使用しました。ASN.1ライブラリは、「PyASN1」(pyasn1-0.1.4) を使用しています。以下がコードです。

#!/usr/bin/python
#-*- coding: utf-8 -*-

#pyasn1用設定
#univ(プリミティブ型),namedtype(名前宣言)
from pyasn1.type import univ, namedtype
#DER形式出力用
from pyasn1.codec.der import encoder as der_encoder

#引数設定
import optparse;
parser = optparse.OptionParser();
parser.add_option('-d', action='store', dest='deroutfile',type='string',
        help="DER output file");

options, args = parser.parse_args();

#ASN.1定義
class Price(univ.Sequence):
    componentType = namedtype.NamedTypes(
    namedtype.NamedType('amount', univ.Integer()),
    namedtype.NamedType('name', univ.OctetString())
    );

#ASN.1定義への値設定
p=Price();
p.setComponentByName('amount', 0xa);
p.setComponentByName('name', 'testdata');

#DER形式への出力
b=der_encoder.encode(p);

deroutfile=options.deroutfile
if deroutfile == None:
    deroutfile="der.out"

asn1data=open(deroutfile,"w");
asn1data.write(b);
asn1data.close();

起動方法は以下のようにしています。

$ python asn1make.py -d der2.out
$ ls
asn1make.py der2.out

当たり前のことですが、ASN.1定義が同じであれば、実装が異なっていてもデータは同一です。

$ hd der2.out
00000000  30 0d 02 01 0a 04 08 74  65 73 74 64 61 74 61     |0......testdata|

次に生成したASN.1データを、ASN.1定義がない状態と仮定して見て行きたいと思い ます。

3. ASN.1データの解析

前述の通り、パケットキャプチャなどから、データを見出す必要がある場合も多々 あります。主に以下の二手法による解析方法について考えました。

  • (1)手動解析
  • (2)ツールによる解析

3.1 手動解析

ASN.1定義が見つからなくても、以下のような感じで、おおよそ分解できます。

  • (1)”特定の型”+長さとなるデータがあるかチェックする。

    ASN.1構造は、特定の型を表すデータ値+長さで構成しています。”特定の型”で多いのは”SEQUENCE”と”プリミティブデータ”です。

  • (2)”30”(SEQUENCE)+長さを見出すSEQUENCE型は、特に使われることが多いです。

  • (3)プリミティブデータ+長さを見出す

    • “02”(INTEGER)
    • “04”(OCTET STRING)
    • “05”(NULL)
    • “06”(OBJECT IDENTIFIER) などはよく使われるプリミティブデータとして、覚えておくことになります。

上記のように考えれば、以下データは

30 0d 02 01 0a 04 08 74  65 73 74 64 61 74 61
  1. 一バイト目の”30”の後が0x0d(13バイト)なので、13バイトまで一塊になっている 可能性が高い
  2. “02”,”04”のようなプリミティブデータ+長さのデータ列になっている

という理由で、以下に分解できます。0xaという数字(INTEGER)と、ASCIIで”te stdata”とある、8文字(OCTET STRING)の集まり(SEQUENCE)であることが分かります。

30 0d
02 01 0a
04 08 74 65 73 74 64 61 74 61

3.2 ツールによる解析

さすがに目視だけでは限界がありますのでASN.1を解析してくれるツールがあります。

3.2.1 dumpasn1

dumpasn1というツールがあります。CUIとしては重宝します。dumpasn1を起動した結果は以下のように表示されます。

$ dumpasn1 der.out
   0   13: SEQUENCE {
   2    1:   INTEGER 10
   5    8:   OCTET STRING 'testdata'
         :   }

0 warnings, 0 errors.

3.2.2 aatool2

なかなか素晴らしいツールです。ASN.1データを解析した結果がGUI表示されます。しかも、業務で使用する可能性のある機能がついてきます。(CMSの解析結果等)

../../_images/aatool2.png

4.参考文献

  1. ASN.1詳細 」(ASN.1について詳しい)
  2. ASN.1の定義
  3. Wikipedia
  4. OpenSSL 」内のドキュメント(展開後の./doc/openssl.txt)
  5. OpenSSL 」内の実装例(展開後の./apps/asn1pars.c)
  6. Bouncy Castle
  7. OpenSSL for Ruby(ASN.1)
  8. PyASN1
  9. Convert-ASN1
  10. aatool2
記事執筆者: tanino
記事公開日: 2012年9月3日