🌐

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

2022/11/05に公開約25,300字

あらすじ

https://zenn.dev/cube/articles/1939d81fd98152

前回DERパーサーを作成しました。
今回はこのDERパーサーを使って証明書の読み込みをするCERTパーサーを書いていきます。

証明書の構造とCERTパーサー、DERパーサーについて

ここではCERTパーサーをどのように書いていくかを見ていきます。

証明書は下記のようにASN.1で表せるのでした。

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

上記のようにSEQUENCEとなっていますね。
前回作ったDERパーサーではCertificateを読み込むとしたら下記のようになります。

Data  {
        Class      = その時による
	Structured = true
	Tag        = SEQUENCE
	ByteLength = その時による
	Contents   = [tbsCertificate,tbsCertificate,signature]
}

上記のようになり、読み込んだSEQUENCEのContentsにはtbsCertificate等の情報がbyteで入ることになります。
なのでCERTパーサーではこのContentsに対しまたDERパーサーを適用していくという形になります。

CERTパーサーの構造体を下記のように作ります。

type CertParser struct {
	ASN1      *Data
	DERParser *DERParser
}

func NewCertParser(data []byte) *CertParser {
	return &CertParser{
		DERParser: NewDERParser(data),
	}
}

CERTパーサーは内部にDERパーサーを持ちこのDERパーサーで証明書を読み込んでいきます。
流れとしては下記のような感じです。

func (p *CertParser) Parse() (*Certificate, error) {
	asn1, err := p.DERParser.Parse() -1
	if err != nil {
		return nil, err
	}

	if asn1.Class != 0 || asn1.Tag != 16 || !asn1.Structured {
		return nil, ErrInvalidCertificate
	}  -2

	p.DERParser.Reset()  

	p.ASN1 = asn1
	pieces, err := p.parseDERList(p.ASN1.Contents) -3
	if err != nil {
		return nil, err
	}
        
	...
}
  1. まずSEQUENCE部分を得るためにパースします。
  2. 得られたデータはSEQUENCEであるはずなのでそうでなかったらエラー
  3. parseDERListでSEQUENCEを読み込み、ASN.1の配列が返る。

上記で得られたpiecesをつかってtbsCertificate、signatureAlgorithm、signatureを読み込んでいきます。

TBSCertificateの読み込み

まずはTBSCertificateから読み込んでいきます。
TBSCertificateの構造は下記となっています。

 TBSCertificate  ::=  SEQUENCE  {
 	version         [0]  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]  Extensions OPTIONAL
	-- If present, version MUST be v3 --  }

ここで出てくる[0]や[1]とはOptionalなフィールドのことで、存在するかわからない値です。
加えてVersionのところにあるDefaultは存在しなければDefaultの横にある値,v1を適用するという意味となっています。

これを上から読み込んでいきます。
TBSCertificateもSEQUENCEなのでparseDERListを使って読み込み、ここの要素をさらに読み込んでいきます。

func (p *CertParser) parseTBSCertificate(asn1 *Data) (*TBSCertificate, error) {
	if asn1.Class != 0 || asn1.Tag != 16 || !asn1.Structured {
		return nil, ErrInvalidCertificate
	}

	tbs := &TBSCertificate{
		ASN1: asn1,
	}
	pieces, err := p.parseDERList(asn1.Contents)
	if err != nil {
		return nil, err
	}

	//versionを除いて最低6個は子供がいないといけない(versionはないときもある)
	if len(pieces) < 6 {
		return nil, ErrInvalidTBSCertChild
	}

	curPieceNum := 0
	versionExists := false

	tbs.Version, versionExists, err = p.parseVersion(pieces[curPieceNum])
	if err != nil {
		return nil, err
	}

	if versionExists {
		curPieceNum = 1
	}

	tbs.SerialNumber = p.parseToHex(pieces[curPieceNum])
	tbs.Signature, err = p.parseAlgorithmIdentifier(pieces[curPieceNum+1])
	if err != nil {
		return nil, err
	}
	tbs.Issuer, err = p.parseName(pieces[curPieceNum+2])
	if err != nil {
		return nil, err
	}
	tbs.Validity, err = p.parseValidity(pieces[curPieceNum+3])
	if err != nil {
		return nil, err
	}
	tbs.Subject, err = p.parseName(pieces[curPieceNum+4])
	if err != nil {
		return nil, err
	}
	tbs.SubjectPublicKeyInfo, err = p.parseSubjectPublicKeyInfo(pieces[curPieceNum+5])
	if err != nil {
		return nil, err
	}

	err = p.parseOptional(pieces[curPieceNum+6:], tbs)
	if err != nil {
		return nil, err
	}

	return tbs, nil

}

versionが存在したかしないかでpiecesをずらすか、ずらさないかを決めています。

version

parseVersionを作って読み込んでいきます。
versionはOptionalなパラメータでしたが、このときPart1で後述するとした型の中のClassの一つであるCONTXET_SPECIFICが関わってきます。

|型    (1byte)                           |
|class(2bit),isStructured(1bit),tag(5bit)|

Classは0,1,2,3の4種類あり、それぞれ
UNIVERSAL=0
APPLICATION=1
CONTEXT_SPECIFIC=2
PRIVATE=3
となります。
今までTagはINTEGERであったりSTRINGであったりSEQUENCEであったり構造を表していましたが、
Optionalなフィールドの場合、ClassがCONTEXT_SPECIFICとなり、
ClassがCONTEXT_SPECIFICの場合、TagはOptionalの中でも何番目か?ということを表します。

 TBSCertificate  ::=  SEQUENCE  {
 	version         [0]  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]  Extensions OPTIONAL
	-- If present, version MUST be v3 --  }

の例では、
versionは0番目なのでTag=0
issuerUniqueIDはTag=1
...
となっていきます。

これを踏まえてコードを見ていきましょう。(後で思ったのですが、ここのコードはparseOptionalに統合しても良かったかもしれません)

 Version  ::=  INTEGER  {  v1(0), v2(1), v3(2)  }
func (p *CertParser) parseVersion(asn1 *Data) (int, bool, error) {
	if asn1.Tag != 0 || asn1.Class != ASN1_CONTEXT_SPECIFIC {
		return 1, false, nil

	}

	p.DERParser.ResetWithNewData(asn1.Contents)

	version, err := p.DERParser.Parse()
	if err != nil {
		return 1, false, err
	}
	p.DERParser.Reset()

	//expect version.Content is only 1byte
	if len(version.Contents) != 1 {
		return 1, false, ErrInvalidVersion
	}

	versionNum, err := Bytes2Int(version.Contents)
	if err != nil {
		return 1, false, err
	}

	return versionNum + 1, true, nil
}

CONTEXT_SPECIFICかどうかを確認し違うならVersionは存在しません。
IntegerなのでContentsはIntのByte表現となってるのでByteをIntに戻してあげます。
v1=0,v2=1...と一個ずれているので+1して戻しています。

serialNumber

SerialNumber  ::=  INTEGER

で、INTEGERなのですが、
Part1の証明書の構造を表示したときにSerialNumberは16進数で表示されていたので、今回は16進数に変換してあげましょう。

  Serial Number:
            1c:38:e2:3e:1c:c2:cd:9f:09:f9:d2:56:ce:97:fe:bc:ca:92:24:34
func parseHex(b []byte) string {
	return hex.EncodeToString(b)
}

func (p *CertParser) parseToHex(asn1 *Data) string {
	return parseHex(asn1.Contents)
}

asn.1の値を16進数にしてあげるだけです。

Signature

signatureはAlgorithmIdentifierとなっていました。
AlgorithmIdentifierは下記の構造です。

 AlgorithmIdentifier  ::=  SEQUENCE  {
        algorithm               OBJECT IDENTIFIER,
        parameters              ANY DEFINED BY algorithm OPTIONAL  
	}

parametersはOptionalです。

OBJECT IDENTIFIERはTagが06となっているあらかじめ用意されている型です。
値には、下記のようにbyte列で表されます。

[]byte{0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x04}

このbyte列はOIDと呼ばれるオブジェクト識別子をencodeしたもので、oidは下記のようなintと.で表されています。下記はmd5WithRsaEncryptionのことです。

1.2.840.113549.1.1.4

byte列表現か、int表現のどちらであっても結局やりたいことはあどんなアルゴリズムを使っているかの特定です。

実際のコードでは下記の構造体を作って、Algorithmに1.2.840.113549.1.1.4みたいな形式のstringを入れています。

type AlgorithmIdentifier struct {
	Algorithm  string
	Parameters *Parameters //Optional
}

ただ、やりたいことは後にOIDを使ってどんなアルゴリズムを使っているかの特定なので、
1.2.840.113549.1.1.4 -> []byte{0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x04}のデコードをせずbyte列をそのまま使ってもよさそうです。

[レポジトリ x509.go parseAlgorithmIdentifier]

デコードについては下記が詳しいです。
https://tex2e.github.io/blog/protocol/oids

issuer,subject

issuer,subjectともに型がNameとなっています。
Nameの構造は結構複雑で

Name = SEQUENCE OF RelativeDistinguishedName
RelativeDistinguishedName =
     SET SIZE (1..MAX) OF AttributeTypeAndValue
AttributeTypeAndValue = SEQUENCE {
     type     AttributeType,
     value    AttributeValue }
        
AttributeType = OBJECT IDENTIFIER
        
AttributeValue = ANY -- DEFINED BY AttributeType

となっているので、まとめると下記のような感じとなります。

{
   [AttributeTypeAndValue,AttributeTypeAndValue,...],
   [AttributeTypeAndValue,AttributeTypeAndValue,...],
   [AttributeTypeAndValue,AttributeTypeAndValue,...],
    ...
}

Issuer: C = JP, ST = Tokyo, L = Shibuya, O = TestCorp, OU = TestUnit, CN = Cube
上記の具体例をもとに考えてみると、

{
 [{type:C value:JP},]
 [{type:ST value:Tokyo},], 
 [{type:L value:Shibuya},],
 ...
}

となるのですが、これだと単純に下記のような構造で良い気がしないでしょうか。

{
   AttributeTypeAndValue,
   AttributeTypeAndValue,
   AttributeTypeAndValue,
    ...
}

わざわざ [AttributeTypeAndValue,AttributeTypeAndValue,...],とするのには、
Multi-valued RDNという要素が関わってくるのですが、下記が詳しいです。
http://blog.livedoor.jp/k_urushima/archives/1808377.html

今回はMulti-valuedではないのでSETの要素が一つとしてパースしています。

func (p *CertParser) parseName(asn1 *Data) (*Name, error) {

	piece, err := p.parseDERList(asn1.Contents) -1 
	if err != nil {
		return nil, err
	}

	if len(piece) != 6 {
		return nil, ErrInvalidNameChild
	}

	country, err := p.getNameChildContent(piece[0]) -2
	if err != nil {
		return nil, err
	}

	...
	
	
	p.DERParser.Reset()

	return &Name{
		Country:          country,
		StateOrProvince:  stateOrProvince,
		Locality:         locality,
		Organization:     organization,
		OrganizationUnit: organizationUnit,
		Common:           common,
	}, nil
}

  1. まずは最初のSEQUENCE OF 部分をParse,IssuerとSubjectでは要素が6つある想定。
  2. 順々にCountry,StateOrProvince...とParseしていく。
var (
	OID_CommonName             []byte = []byte{0x55, 0x04, 0x03}
	OID_CountryName            []byte = []byte{0x55, 0x04, 0x06}
	OID_LocalityName           []byte = []byte{0x55, 0x04, 0x07}
	OID_StateOrProvinceName    []byte = []byte{0x55, 0x04, 0x08}
	OID_OrganizationName       []byte = []byte{0x55, 0x04, 0x0A}
	OID_OrganizationalUnitName []byte = []byte{0x55, 0x04, 0x0B}

	OIDNames [][]byte = [][]byte{OID_CommonName, OID_CountryName, OID_LocalityName, OID_StateOrProvinceName, OID_OrganizationName, OID_OrganizationalUnitName}
)

func (p *CertParser) getNameChildContent(childRoot *Data) (string, error) {

	p.DERParser.ResetWithNewData(childRoot.Contents)
	attr, err := p.DERParser.Parse() -1
	if err != nil {
		return "", err
	}

	checkOID := func(oid []byte) bool {
		for _, name := range OIDNames {
			if bytes.Equal(oid, name) {
				return true
			}
		}
		return false
	}

	oidRawdata := attr.Contents[:5] -2
	p.DERParser.ResetWithNewData(oidRawdata)
	oid, err := p.DERParser.Parse()
	if err != nil {
		return "", err
	}
	if !checkOID(oid.Contents) {
		return "", ErrInvalidNameOID
	} 
	
	contentRawData := attr.Contents[5:]
	p.DERParser.ResetWithNewData(contentRawData)
	content, err := p.DERParser.Parse() -3
	if err != nil {
		return "", err
	}

	p.DERParser.Reset()

	return string(content.Contents), nil
}
  1. setOF部分をパース,出力されるのはAttributeTypeAndValue(Sequence)
  2. AttributeTypeAndValueのtype(OID)だけを抽出してパース
  3. AttributeTypeAndValueのvalueをパース

Validity

まずはasn.1から。

   Validity ::= SEQUENCE {
        notBefore      Time,
        notAfter       Time }
        
   Time ::= CHOICE {
        utcTime        UTCTime,
        generalTime    GeneralizedTime 
	}

CHOICEはどちらかの型になるという意味です。
今回は UTCTime or GeneralizedTimeですね。
UTCTimeは2049年まで、GeneralizedTimeは2050年から使用するとなっているので、
今回はUTCTimeの場合だけ考えます。
UTCTimeはtag=17で、値がYYMMDDhhmmssZのフォーマットで書かれます。
例として、値が下記とします。

"32 32 31 30 31 33 30 34 34 34 34 37 5a"

これをstringにすると下記になります。

"221013044447Z"

上記より
data = "221013044447Z"として
data[:2]が年、
data[2:4]が月、
data[4:6]が日、
data[6:8]が時間、
data[8:10]が分数、
data[10:12]が秒数
data[13]=Z
となっていることがわかります。

あとはこれを各種言語のTime型に合わせる形です。

func (p *CertParser) parseValidity(asn1 *Data) (*VerifyPeriod, error) {
	pieces, err := p.parseDERList(asn1.Contents) -1
	if err != nil {
		return nil, err
	}

	if len(pieces) != 2 {
		return nil, ErrInvalidValidityChild
	}

	toTime := func(b []byte) (time.Time, error) {
		data := string(b)
		year, err := strconv.Atoi(data[:2])
		if err != nil {
			return time.Time{}, err
		}
		year += 2000

		month, err := strconv.Atoi(data[2:4])
		if err != nil {
			return time.Time{}, err
		}
		day, err := strconv.Atoi(data[4:6])
		if err != nil {
			return time.Time{}, err
		}
		hour, err := strconv.Atoi(data[6:8])
		if err != nil {
			return time.Time{}, err
		}
		minute, err := strconv.Atoi(data[8:10])
		if err != nil {
			return time.Time{}, err
		}
		sec, err := strconv.Atoi(data[10:12])
		if err != nil {
			return time.Time{}, err
		}
		gmt, err := time.LoadLocation("GMT")
		if err != nil {
			return time.Time{}, err
		}

		return time.Date(
			year,
			time.Month(month),
			day,
			hour,
			minute,
			sec,
			0,
			gmt,
		), nil
	} -2 
	
	notBefore, err := toTime(pieces[0].Contents)
	if err != nil {
		return nil, err
	}
	notAfter, err := toTime(pieces[1].Contents)
	if err != nil {
		return nil, err
	}

	return &VerifyPeriod{
		NotBefore: notBefore,
		NotAfter:  notAfter,
	}, nil
}
  1. SEQUENCE部分をパース
  2. asn.1の値をtimeに変換

subjectPublicKeyInfo

まずはasn.1から。

   SubjectPublicKeyInfo  ::=  SEQUENCE  {
        algorithm            AlgorithmIdentifier,
        subjectPublicKey     BIT STRING  }

AlgorithmIdentifierはsignatureのところで解説しました。
今回はalgorithmがRSA(1.2.840.113549.1.1.1)として解説します。

BIT STRINGについて解説していきます。

someMember BIT STRING  - primitive
someMember BIT STRING{ - structed
  yellow
  red
  green
}                      

上記のようにBIT STRINGには二種類あり、structedだとBIT STRINGのあとに構造体のメンバーが書かれます。
今回のsubjectPublicKeyはprimitiveです。

structedの場合値はそのままで、
primitiveの場合は、
先頭1byteがUnusedBitとなり0~7の数となります。
UnusedBitは値部分の後ろの何bitが使われていないかを表しているので、下記のような構造です。

|UnusedBit(1byte)|後続のbyte...|未使用bit|

さて、BITSTRINGから上手くUsedBitだけを取り出した後はBITSTRING自体が何を表すかを解析しなければいけません。
今回のSubjectPublicKeyInfoでBITSTRINGが何を表しているかはalgorithmによって変わってきます。

RSA
RSAPublicKey ::= SEQUENCE {
     modulus INTEGER, -- n
     publicExponent INTEGER -- e }
     
DH
   DHPublicKey ::= INTEGER

なのでBITSTRINGをさらにパースして情報を取り出していくという形をとっていきます。

var (
	OID_RSA = "1.2.840.113549.1.1.1"

	ValidAlgos = []string{OID_RSA}
)

func (p *CertParser) parseSubjectPublicKeyInfo(asn1 *Data) (*SubjectPublicKeyInfo, error) {
	if asn1.Class != 0 || asn1.Tag != 16 || !asn1.Structured {
		return nil, ErrInvalidSubjectPublicKey
	}

	pubKey := &SubjectPublicKeyInfo{}
	pieces, err := p.parseDERList(asn1.Contents) -1
	if err != nil {
		return nil, err
	}

	if len(pieces) != 2 {
		return nil, ErrInvalidSubjectPublicKeyChild
	}

	checkAlgo := func(str string) bool {
		for _, algo := range ValidAlgos {
			if str == algo {
				return true
			}
		}
		return false
	}

	algorithm, err := p.parseAlgorithmIdentifier(pieces[0]) -2 
	if err != nil {
		return nil, err
	}

	if !checkAlgo(algorithm.Algorithm) {
		return nil, ErrInvalidSigAlgo
	}

	subjectPublicKey := p.parseBitString(pieces[1]) -3

	//subjectPublicKeyの中身のmodulesとexponentを取り出す
	p.DERParser.ResetWithNewData(subjectPublicKey.Bytes)
	keyValueData, err := p.DERParser.Parse()
	if err != nil {
		return nil, err
	}
	//TODO algorithmに応じてBIT STRINGのパースを変更
	pieces, err = p.parseDERList(keyValueData.Contents) -4
	if err != nil {
		return nil, err
	}
	if len(pieces) != 2 {
		return nil, ErrInvalidKeyValueChild
	}

	modulus := p.parseToHex(pieces[0])
	exponent := p.parseToHex(pieces[1])

	pubKey.Algorithm = algorithm
	pubKey.SubjectPublicKey = &SubjectPublicKey{
		Modulus:  modulus,
		Exponent: exponent,
	}

	return pubKey, nil
}
  1. SEQUENCEをパース
  2. AlgorithmIdentifierをパース
  3. BIT STRINGをパース
  4. 今回はRSAとしているのでSEQUENCEをパースし、ModulusとExponentを取り出す

extensions

 TBSCertificate  ::=  SEQUENCE  {
 	...
	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]  Extensions OPTIONAL
	-- If present, version MUST be v3 --  }

issuerUniqueID,subjectUniqueID,extensionsはOptionalとなっていました。
今回作成した証明書ではissuerUniqueID,subjectUniqueIDは存在しないので取り扱わず、
またextensionsも様々なextensionがあるのですが、作成した証明書に存在する三種(SubjectKeyIdentifier、AuthorityKeyIdentifier、BasicConstraints)のみ取り扱います。

最初にasn.1から。

Extensions  ::=  SEQUENCE SIZE (1..MAX) OF Extension

Extension  ::=  SEQUENCE  {
	extnID      OBJECT IDENTIFIER,
	critical    BOOLEAN DEFAULT FALSE,
 	extnValue   OCTET STRING
	-- contains the DER encoding of an ASN.1 value
	-- corresponding to the extension type identified
	-- by extnID
}

とあるので、まずExtensionsをパースしてからここのExtensionをさらにパースしていくことになります。

func (p *CertParser) parseOptional(data []*Data, tbs *TBSCertificate) error {
	//ここもdataを回してoptionのTagごとに分岐
	for _, d := range data {
		if d.Class != ASN1_CONTEXT_SPECIFIC {
			return ErrContextSpecific
		}
		switch d.Tag {
		case 1:
			continue
		case 2:
			continue
		case 3:
			extensions, err := p.parseExtensions(data[0])
			if err != nil {
				return err
			}
			tbs.Extensions = extensions
		default:
			return ErrInvalidOptionalNum
		}

	}

	return nil
}

Optional部分を取り扱う際に、以前説明したようにCONTEXT_SPECIFICとTagでSEQUENCE中のどの要素なのかを判別しています。

func (p *CertParser) parseExtensions(asn1 *Data) ([]Extension, error) {
	p.DERParser.ResetWithNewData(asn1.Contents)
	extensionsData, err := p.DERParser.Parse()
	if err != nil {
		return nil, err
	}
	pieces, err := p.parseDERList(extensionsData.Contents) -1
	if err != nil {
		return nil, err
	}
	extensionLen := len(pieces) -2 

	extensions := make([]Extension, extensionLen)

	for i := 0; i < extensionLen; i++ {
		extension, err := p.parseExtension(pieces[i]) -3
		if err != nil {
			return nil, err
		}
		extensions[i] = extension
	}

	return extensions, nil
}
  1. SEQUENCE OFをパース
  2. Extensionが何個あるかを取得
  3. Extensionをパース
func (p *CertParser) parseExtension(asn1 *Data) (Extension, error) {
	pieces, err := p.parseDERList(asn1.Contents) -1
	if err != nil {
		return nil, err
	}

	if len(pieces) != 2 && len(pieces) != 3 { -2
		return nil, ErrInvalidExtensionChild
	}

	oid := p.parseObjectIdent(pieces[0].Contents) -3

	switch oid {
	case oid_subjectKeyIdentifier:
		return p.parseSubjectKeyIdentifier(pieces[1:], oid)
	case oid_authorityKeyIdentifier:
		return p.parseAuthorityKeyIdentifier(pieces[1:], oid)
	case oid_basicConstraints:
		return p.parseBasicConstraints(pieces[1:], oid)
	default:
		return nil, ErrInvalidExtension
	}

}

parseExtensionを見ていくにあたり、asn.1を再掲します。

Extension  ::=  SEQUENCE  {
	extnID      OBJECT IDENTIFIER,
	critical    BOOLEAN DEFAULT FALSE,
 	extnValue   OCTET STRING
	-- contains the DER encoding of an ASN.1 value
	-- corresponding to the extension type identified
	-- by extnID
}
  1. SEQUENCEをパース
  2. criticalがdefaultなのでSEQUENCEの子供は2か3となる
  3. oidをパース

oidによってどうパースするかを分岐させて、criticalとextnValueを渡しています。

ExtensionのextnValueがSubjectKeyIdentifier等になっていきます。

次からここのExtensionについて見ていきます。
今回取り上げなかったExtensionも同じように実装すればできるはずですので、興味がある方はぜひ。(SANとか)

SubjectKeyIdentifier

まずはasn.1から。

doesn't have critical
SubjectKeyIdentifier ::= KeyIdentifier
KeyIdentifire ::= OCTET STRING

SubjectKeyIdentifierはcriticalを含みません。

func (p *CertParser) parseSubjectKeyIdentifier(data []*Data, oid string) (*SubjectKeyIdentifier, error) {
	if len(data) == 2 {  -1
		return nil, ErrExtensionCanNotBeCritical
	}

	p.DERParser.ResetWithNewData(data[0].Contents)
	keyIdent, err := p.DERParser.Parse() -2
	if err != nil {
		return nil, err
	}

	return &SubjectKeyIdentifier{
		OID:           oid,
		KeyIdentidier: p.parseToHex(keyIdent), -3
	}, nil
}
  1. criticalを含んでいる場合はエラー
  2. OCTET STRINGをパース
  3. 16進数で格納

3.について
証明書のデータを表示すると下記のようになっていたので16進数で格納します。

 X509v3 Subject Key Identifier: 
                B4:7D:B7:8D:C3:78:94:3B:C9:24:11:6A:A9:A4:26:B7:91:85:BA:FF

AuthorityKeyIdentifier

doesn't have critical
AuthorityKeyIdentifier ::= SEQUENCE {
    keyIdentifier             [0] KeyIdentifier           OPTIONAL,
    authorityCertIssuer       [1] GeneralNames            OPTIONAL,
    authorityCertSerialNumber [2] CertificateSerialNumber OPTIONAL }
 KeyIdentifire ::= OCTET STRING   
 CertificateSerialNumber  ::=  INTEGER 

AuthorityKeyIdentifierもcriticalを含みません。
OptionalがあるのでここでもContextSpecificが使用されます。

extnValueがAuthorityKeyIdentifierの場合、OCTET STRINGの中身はSEQUENCEなのでもう一度パースが必要になります。

func (p *CertParser) parseAuthorityKeyIdentifier(data []*Data, oid string) (*AuthorityKeyIdentifier, error) {
	if len(data) == 2 {  -1
		return nil, ErrExtensionCanNotBeCritical
	}

	p.DERParser.ResetWithNewData(data[0].Contents)
	seq, err := p.DERParser.Parse() -2
	if err != nil {
		return nil, err
	}

	pieces, err := p.parseDERList(seq.Contents) -3
	if err != nil {
		return nil, err
	}

	ident := &AuthorityKeyIdentifier{
		OID: oid,
	}

	for _, piece := range pieces {  -4

		if piece.Class != ASN1_CONTEXT_SPECIFIC {
			return nil, ErrContextSpecific
		}

		switch piece.Tag {
		case 0:
			hex := p.parseToHex(piece)
			ident.KeyIdentifier = &hex
		case 1:
			name, err := p.parseGeneralNames(piece)
			if err != nil {
				return nil, err
			}
			ident.AuthorityCertIssuer = name
		case 2:
			hex := p.parseToHex(piece)
			ident.AuthorityKeyIdentifier = &hex
		default:
			return nil, ErrInvalidOptionalNum
		}
	}

	return ident, nil
}
  1. criticalがあればエラー
  2. extnValue(OCTET STRING)をパース
  3. SEQUENCEをパース
  4. Optional部分を処理

GenralNamesについてですが、構造が難しく、本証明書には含まれていないため今回は実装していません。一応asn.1だけ載せておきます。

GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName

GeneralName ::= CHOICE {
    otherName                 [0] OtherName,
    rfc822Name                [1] IA5String,
    dNSName                   [2] IA5String,
    x400Address               [3] ORAddress,
    directoryName             [4] Name,
    ediPartyName              [5] EDIPartyName,
    uniformResourceIdentifier [6] IA5String,
    iPAddress                 [7] OCTET STRING,
    registeredID              [8] OBJECT IDENTIFIER }

OtherName ::= SEQUENCE {
    type-id      OBJECT IDENTIFIER,
    value        [0] EXPLICIT ANY DEFINED BY type-id }

EDIPartyName ::= SEQUENCE {
    nameAssigner [0] DirectoryString OPTIONAL,
    partyName    [1] DirectoryString }

CHOICEは番号の中からどれか一つを選んで使うというものです。

BasicConstraints

may have critical
BasicConstraints ::= SEQUENCE {
  cA                   BOOLEAN DEFAULT FALSE,
  pathLenConstraint    INTEGER (0..MAX) OPTIONAL
}

BasicConstraintsは以前に二つと違いcriticalを含む可能性があります。
後は以前に出てきたものばかりですが、Optionalがあったりdefaultがあるので処理の部分が複雑になります。

func (p *CertParser) parseBasicConstraints(data []*Data, oid string) (*BasicConstraints, error) {

	critical := false
	component := data[0]
	if len(data) == 2 {  -1
		var err error
		critical, err = p.parseBoolean(component)
		if err != nil {
			return nil, err
		}
		component = data[1]
	}

	basicConstraints := &BasicConstraints{OID: oid, Critical: critical}

	p.DERParser.ResetWithNewData(component.Contents)
	seq, err := p.DERParser.Parse() -2
	if err != nil {
		return nil, err
	}

	pieces, err := p.parseDERList(seq.Contents) -3
	if err != nil {
		return nil, err
	}

	for _, d := range pieces { -4
		switch d.Tag {
		case ASN1_INTEGER:
			i, err := Bytes2Int(d.Contents)
			if err != nil {
				return nil, err
			}
			basicConstraints.PathLenConstraint = &i
		case ASN1_BOOLEAN:
			isCA, err := p.parseBoolean(d)
			if err != nil {
				return nil, err
			}
			basicConstraints.CA = isCA
		default:
			return nil, ErrInvalidOptionalNum
		}
	}

	return basicConstraints, nil
}
  1. len=2の場合はcrtiticalを含みます。
  2. extnValue(OCTET STRING)をパース
  3. SEQUENCEをパース
  4. あとはOptional部分をパースしていきます。
func (p *CertParser) parseBoolean(asn1 *Data) (bool, error) {
	if asn1.Tag != ASN1_BOOLEAN {
		return false, ErrInvalidBasicConstraintsBoolean
	}

	i, err := Bytes2Int(asn1.Contents)
	if err != nil {
		return false, err
	}

	if i == 0 {
		return false, nil
	}

	return true, nil

booleanは値が0だったらfalse,それ以外だったらtrueです。

ここまででようやくTBSCertificateが終わりです。お疲れさまでした。

Certificateの残りの部分

後は、

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

signatureAlgorithmとsignatureだけですが、この二つの型はTBSCertificateの中で出てきた型ですのでこれまでのようにやるだけです。

func (p *CertParser) Parse() (*Certificate, error) {
	...
	
	sigAlgo, err := p.parseAlgorithmIdentifier(pieces[1]) -1
	if err != nil {
		return nil, err
	}
	sigVal, err := p.parseSignatureValue(pieces[2]) -2
	if err != nil {
		return nil, err
	}

	return &Certificate{
		TbsCertificate:     tbsCert,
		SignatureAlgorithm: sigAlgo,
		SignatureValue:     sigVal,
	}, nil
}

func (p *CertParser) parseSignatureValue(asn1 *Data) (*SignatureValue, error) {
	if asn1.Class != 0 || asn1.Tag != 3 || asn1.Structured {
		return nil, ErrInvalidSigValue
	}

	sig := &SignatureValue{}

	bits := p.parseBitString(asn1)
	sig.Value = bits.Bytes

	return sig, nil
}

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

  1. AlgorithmIdentifierをパースします。
  2. SignatureValueはBIT STRINGなのでパースしていきます。
    sig.ValueにBIT STRINGのUsedBitを入れていますが、値を取りやすくするためにUsedBitを入れているだけなので別にBIT STRINGをそのまま使っても良いと思います。

これでCertificateから情報の読み取りは完了しました。お疲れ様です。

おわりに

随分と長くなってしまいましたが、ここまで読んでくださりありがとうございます。
アルゴリズムをRSAに絞ったり、限られたExtensionしか実装しない等の省略はありますが、

openssl x509 -in server.crt -noout -text

基本的に上記のコマンドで表示された情報を出力することを目的にしたので、

  1. 実装したい項目がある証明書を作り、ゴールとなる値を理解する
  2. RFC等を読んでASN.1を知る
  3. ゴールの値を出力するコードを書く
    の順番でやるのが良いかと思います。

パーサーを書いてよかった点として、Multi-valued RDNのような例があると知れたことです。
最初なんでCとかOUが複数ある構造になるんだろう?asn.1の読み取り間違いか?と思ったりしま した。
実際は、Multi-valued RDNに対応するためだった訳ですが、悩んだ分資料に当たる力もつきましたし良しとします。

次回は情報を取得することができたので証明書のVerifyをやっていきます。

次回

https://zenn.dev/cube/articles/c2b28e6aff020d

Discussion

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