🔑

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

2022/11/05に公開

あらすじ

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

前回CERTパーサーまで書き終わり、証明書の情報を取得することができました。
今回はVerifyをやっていきます。
といってもVerifyのアルゴリズム自体はGoのものを使っているので非常に短いです。

Verifyについて

Verifyは電子署名を使って、証明書の内容が改ざんされていないかどうかを調べる機能となります。

下記が詳しいです。
https://blog.serverworks.co.jp/server-cert-verification
https://www.neteng.co/?p=694

Verify

type Certificate struct {
	TbsCertificate     *TBSCertificate
	SignatureAlgorithm *AlgorithmIdentifier
	SignatureValue     *SignatureValue
}

前回証明書から取り出したデータを格納する構造体です。
この構造体を使ってVerifyしていきましょう。
必要なものは
1.公開鍵
2.電子署名
3.署名に使われたアルゴリズム
4.証明書のTBSCertificateのrawデータ
です。

func (c *Certificate) Verify(pubKey crypto.PublicKey -1 ) bool {

	sigValue := c.SignatureValue.Value -2
	hashType, err := GetHashType(c.SignatureAlgorithm.Algorithm) -3 
	if err != nil {
		return false
	}

	data := c.TbsCertificate.ASN1.Raw -4

	....
}

var (
	MD5WithRSAEncryption    = "1.2.840.113549.1.1.4"
	SHA1WithRSAEncryption   = "1.2.840.113549.1.1.5"
	SHA512WithRSAEncryption = "1.2.840.113549.1.1.13"
)

func GetHashType(sigType string) (crypto.Hash, error) {
	switch sigType {
	case SHA512WithRSAEncryption:
		return crypto.SHA512, nil
	}

	return 0, ErrUnSupportedAlgorithm
}

上記の1,2,3,4でそれぞれ必要なものを取り出しています。
sigValueが電子署名。
署名に使われたアルゴリズムはSignatureAlgorithmから取り出しています。
TbsCertificate.ASN1.Rawから送られてきた証明書のデータを取り出します。
現時点では送られてきたデータ内の公開鍵が安全なものかわからないので、TbsCertificateにある公開鍵を使うわけにはいきません。
この公開鍵が使えるのはVerifyが終わった後です。

では、Verifyの引数にある公開鍵はどこからもってくるかというと、
通常はOSやブラウザ内に元々入っているルート証明書にある公開鍵を使用してVerify,
Verify済みの証明書内の公開鍵を使って証明書チェーンの次の証明書を検証...という流れです。
今回はオレオレ証明書一つだけなので作ったpubKeyを作って、そのpubKeyが絶対正しいとして検証を行います。

公開鍵の作成と読み込み

 openssl genrsa 2048 > private.key

Part1で作成した秘密鍵から公開鍵を作ります。

openssl rsa -pubout < private.key > pub.key

読み込みは下記のようにやります。

bytes, err := os.ReadFile("../testData/pub.key")
block, _ := pem.Decode(bytes)
pubKey, err := x509.ParsePKIXPublicKey(block.Bytes)

Verify続き

func (c *Certificate) Verify(pubKey crypto.PublicKey) bool {
        verify続きから
	...
	switch hashType {
	//go本家だと、MD5やSHA1はエラーにしていた
	default:
		h := hashType.New()
		h.Write(data)
		data = h.Sum(nil)
	}

	switch pub := pubKey.(type) {
	case *rsa.PublicKey:
		if err := verifyWithRSA(data, sigValue, pub, hashType); err != nil {
			return false
		}
		return true
	default:
		return false
	}
}

func verifyWithRSA(hashed []byte, sig []byte, key *rsa.PublicKey, hashType crypto.Hash) error {
	return rsa.VerifyPKCS1v15(key, hashType, hashed, sig)
}

今回は署名で使われているアルゴリズムがRSAにしか対応させていませんが、DSAやECDSAである場合もあります。
参考
https://blog.fujimisakari.com/ssl-tls-flow/

前回に比べればだいぶ短めですが、これでVerifyは終了です。
Verifyにより無事証明書の公開鍵が使えるようになったので、この公開鍵を使ってTLS通信に役立てていくことになります。

おわりに

今回で自作証明書パーサー作成は終了です。
自作証明書パーサー作りを通すことで、証明書がどう扱われているかがブラックボックスではなくしっかり理解できるようになるのではないかと思います。
レポジトリのコードまで見なくても理解できるように書いたつもりですが、質問等ございましたらコメントしていただければありがたいです。

レポジトリの方にはTLSプロトコルも作成しているのでTLSの実装に興味がある方はtlsフォルダとdebugフォルダを見て頂ければと思います。
TLSの実装は下記の記事がとてもおすすめですのでこちらも参考になると思います。
https://zenn.dev/satoken/articles/golang-tls1_2

Discussion