🐕

証明書を理解するため、自作証明書パーサーを作ろう(Part2 DERParser作成編)

2022/11/04に公開約3,800字

あらすじ

https://zenn.dev/cube/articles/2b4a225176a3bd

前回のPart1では証明書の構造とDER,ASN.1の記法について学びました。
今回はDERをASN.1に戻すパーサーを書いていきましょう。

パーサーはGo言語で書きますが、読みやすい言語なのであまり困らないかと思います。(筆者の書き方で読みにくい等発生するかもしれませんが許してください)

また、要点だけを書くので、もし自分でも書いてみよう!という方がいらっしゃましたら、
コードを省略する部分に関しては対応するレポジトリのフォルダ名、関数名を記載するので、そちらを確認いただければと思います。
レポジトリには証明書では使わないDERの処理(長さが不定腸とか)も入っていますが、そこは無視しても大丈夫です。

PEM -> DER

パーサーにDERを渡すためにPEMからDERの変換をしなければなりません。
方法としては、PEMの最初と最後の---BEGIN ~---,---END ~---を切り取って、
base64DecodeをすればDERの出来上がりです。

[レポジトリ x509.go GetContentFromFIle]

DERパーサーの作成

DERパーサーを構造体として書いていきます。

type DERParser struct {
	Position int
	Data     []byte
}

DataはDERのbyteです。Positionは今Dataのどこを読んでいるかです。

取得したDataから最初の1byteを読み込んでいきましょう。

型の情報を取得する

func (p *DERParser) getFirst1byte() (class int, structured bool, tag int) {
	class = p.getClass()
	structured = p.getStructured()
	tag = p.getTag()
	p.MovePosition()

	return class, structured, tag
}

最初の1byteからClass,Structured,Tagの情報を取得します。
MovePositionの役目はPositionを+1し、今Data中のどこを読んでいるかを更新します。
上記では最初の1byteを読んだのでMovePositionして次の長さを読めるようにしています。

ビット計算が含まれるのでそこも解説しながら話していきます。
筆者はビット計算をコード上でみると頭がこんがらがってしまいとても苦手ですが、皆さんはどうでしょうか。

getClass

func (p *DERParser) getClass() int {
	return int((p.Data[p.Position] & 0xc0) / 64)
}

先頭1byte目から&0xc0で前半2bitを抜き出し、/64で6bit右シフトすると、元の1byte(8bit)から先頭2bitしか残らない。

例えば最初の1byteがdf = 11011111とすると、
0xdf & 0xc0 = 11011111 = 11000000 となる。
& 11000000
さらに/64をすると6bit右シフトとなるので、
00000011となり、
最初の11が残って、うまく最初の2bitを取り出せます。

getStructured

func (p *DERParser) getStructured() bool {
	return (p.Data[p.Position] & 0x20) == 0x20
}

structuredは先頭1byteの5bit目にあるので0x20で5bit目だけ抜き出してビットが立ってるかは == すればOK。

getTag

func (p *DERParser) getTag() int {
return int(p.Data[p.Position] & 0x1f)
}
tagは先頭1byte目の4~0bitで、0x1fで1byte中の4~0bitを抽出します。
0xdfを使うと、
0xdf & 0x1f = 11011111 = 00011111 となります。
& 00011111

長さの情報を取得する

func (p *DERParser) getLength() int {

	//0x80以下の時(0x80でもそのまま返す)
	if p.Data[p.Position] <= 0x80 {
		length := p.Data[p.Position]
		p.MovePosition()
		return int(length)
	}

	//続きの何byteが長さを表しているか
	byteNum := int(p.Data[p.Position] & 0x7f)
	p.MovePosition()

	length := 0
	//例えばbyteNum=2で、次の2byteが02 10だったら
	// 2 * 256 + 16と計算する
	for i := 0; i < byteNum; i++ {
		length = length*256 + int(p.Data[p.Position])
		p.MovePosition()
	}

	return length

}

可変長の時は先頭1bit目が立っていて、
先頭1bit目が立っていれば必ず0x80以上になるのでそこをチェックしてあげます。

0x80以下なら楽なのですが、0x81以上だと計算が必要になってきます。
ここでも例を使いましょう。

30 82 02 10 ......

を使って0x82なので、0x81以上です。
まず0x7fと&をして、先頭1bitを除外し、後続何byteが長さを表すかを取得します。

0x82 & 0x7f = 10000010 = 00000010
& 01111111
となり後続2byteが長さを表します。

length := 0

for i := 0; i < byteNum; i++ {
		length = length*256 + int(p.Data[p.Position])
		p.MovePosition()
	}

ここでは後続のbyteを読み込んでいきます。
256としているのは8bit左シフトです。
02 10の例で確認すると、
まず02のところを読むときは
length = 0
256 + int(0x02)
となり、length = 2となります。

次に10のところを読むときです。
length = 2*256 + int(0x10)
= 512 + 16
= 528
となります。

必要なデータを構造体に格納

ここまででデータの読み取りができたので、下記の構造体に情報を格納してあげましょう。

type Data struct {
	Class      int
	Structured bool
	Tag        int
	ByteLength int
	Contents   []byte
	Raw        []byte
}

ByteLengthはそのままLengthを入れて、Rawはそのままパーサーに渡したデータを入れればよいです。
Contentsは型、長さを除いた値の部分を入れれば大丈夫です。

[レポジトリ x509.go (DERParser)Parse]

ここまででDERパーサーの作成は終了です。

Data{
  Class      int
  Structured bool
  ...
  Children:[]*Data
}

上記のようにSEQUENCEのようなネストしたものに対応するように、Dataもネストした部分を含む構造にする形もありますが、今回はDERパーサーではネストを扱わず、次回のCERTパーサーでネスト部分には対応していきます。
ただDERパーサーでネストに対応しない分CERTパーサーに複雑性を任せることになるのでどちらがよかったのかなと思っています。

おわりに

今回はDERパーサーを書き終わるところまで学びました。
僕のようにバイト列に出合うと面食らってこんがらがってしまうという方もいらっしゃると思いますが、一つずつ二進数に一回戻してあげると理解が進むことがあるかと思います。

次回はDERパーサーを用いて証明書を読み込むCERTパーサーを書いていきます。
やっと具体的な使用方法が見えてくる回になりますね。

次回

https://zenn.dev/cube/articles/13da493c0dfbba

Discussion

ログインするとコメントできます