GolangでPKI入門 - 6

# 1.この記事の対象の人
- Golang で DN (Subject や Issuer) を扱いたい人
- Golang で DN (Subject や Issuer) を扱う[ライブラリ](https://github.com/tardevnull/dnutil)を探している人

# 2.DNとは
DN(Distinguished Name) は X.500 と X.501 で定義されたディレクトリサービスモデルにおいて、そのオブジェクトを一意に識別するための識別名です。X.509 PKI は、ディレクトリサービスモデルを採用しているので、そのオブジェクトの一つである 主体者(Subject)や発行者(Issuer) も、DN によって表現されます。ディレクトリでは、オブジェクトはオブジェクトクラスに基づきエントリとして表現されます。ディレクトリはエントリによるツリー構造で構成され、このツリーは DIT (Directory Information Tree)と呼ばれます。エントリはクラスに基づき複数の属性をもち、属性は属性型と属性値をもちます。このエントリを DIT 内の同一階層内で（上位エントリから見て）一意に識別するためのものが RDN(Relative Distinguished Name)、 DIT 内で一意に識別するためのものが DN です。
RDN は、あるエントリを構成する一つ以上の属性の SET（順序なし）で表されます。あるエントリの RDN となる「属性の SET」は、同一階層内にある他のエントリの RDN として選ばれた「属性の SET」と重複してはいけません。RDN は、同一階層内において、エントリの一意な識別子として利用可能な「属性の SET」になります。
DN は、そのオブジェクトを表すエントリの RDN とその上位エントリすべて（ツリーの最上位まで）の RDN のシークエンス（順序あり）で表されます。DN は、DIT 内の全ての階層でそれぞれ一意になる RDN のシークエンスであるので、DIT 内で一意になるエントリの識別子になります。 DN と RDN の詳細につきましては、[X.501](https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-X.501-201210-S!!PDF-E&type=items)をご参照ください。

DN は具体的には以下のようなものです。
```CN=hoge@example.com,O=example,C=JP```
繰り返しになりますが、DN は RDN のシークエンス（順序あり）で構成されます。RDN は、複数の順序のない属性 (AttributeTypeAndValue) で構成されます。属性 (AttributeTypeAndValue)は、属性型 (AttributeType)と属性値 (AttributeValue)で構成されます。
先述の例で言えば、DN: ```CN=hoge@example.com,O=example,C=JP``` は、
DIT 上で3階層のエントリで構成されるオブジェクトを表しています。
```CN=hoge@example.com``` は、末端のエントリを表す RDN、```O=example``` が中間のエントリを表す RDN、```C=JP``` がルートエントリを表す RDN となります。これら3つの RDN で構成されるシーケンス(ツリーのルートエントリの RDN が先頭)が 当該オブジェクトの DN です。
ここで、RDN ```C=JP``` を例にとると、 ```C=JP``` は、属性型と属性値のペアである属性値(AttributeTypeAndValue)を一つもち、その属性型が ```C (CountryName)```、属性値が ```JP```です。
RDN は、複数の属性を持つことができるので、例えば以下のような属性を複数もつ RDN も定義可能です。
```OU=Sales + OU=Dev```
これは、1つの RDN に2つの属性(OU=Sales と OU=Dev)が含まれている事を示します。DN として以下の2つは別の物になるのでご注意ください。
1. ```CN=hoge@example.com,OU=Sales,OU=Dev,O=example,C=JP```
DN は、5つの RDN を持つシークエンスを持ち、それぞれの RDN は属性を1つずつ持っています。

2. ```CN=hoge@example.com,OU=Sales + OU=Dev,O=example,C=JP```
DN は、4つの RDN を持つシークエンスを持ち、3つの RDN は属性をそれぞれ1つ持ち、1つの RDN は属性を2つ持っています。(Multi Value RDN)

[RFC 5280](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.4)では、 DN を ASN.1 の構造体として以下の様に定義しています。
```
   Name ::= CHOICE { -- only one possibility for now --
     rdnSequence  RDNSequence }

   RDNSequence ::= SEQUENCE OF RelativeDistinguishedName

   RelativeDistinguishedName ::=
     SET SIZE (1..MAX) OF AttributeTypeAndValue

   AttributeTypeAndValue ::= SEQUENCE {
     type     AttributeType,
     value    AttributeValue }

   AttributeType ::= OBJECT IDENTIFIER

   AttributeValue ::= ANY -- DEFINED BY AttributeType
```
これは、
```Distinguished name の型は Name```
```Name は RDNSequence 型の情報要素 rdnSequence をもつ```
```RDNSequence 型は、RelativeDistinguishedName 型の SEQUENCE 型である```
```RelativeDistinguishedName 型は、AttributeTypeAndValue 型の SET 型である```
```AttributeTypeAndValue 型は AttributeType 型の type と AttributeValue 型の value の2つの情報要素をもつ SEQUENCE 型である```
```AttributeType 型は OBJECT IDENTIFIER 型である```
```AttributeValue 型は OBJECT IDENTIFIER 型に対応する任意の型である```
ことを示します。



# 3.Golang で DN を扱うときの課題
Golang で、Issuer や Subject などの DN を扱う必要がでてくるのは、[CSR](https://pkg.go.dev/crypto/x509#CreateCertificateRequest) や[証明書](https://pkg.go.dev/crypto/x509#CreateCertificate)の作成や [CRL](https://pkg.go.dev/crypto/x509#CreateRevocationList) の作成の時です。このような場合 Golang では DN を扱うのに、

 - [pkix.Name](https://pkg.go.dev/crypto/x509/pkix#Name) 構造体

 - [pkix.RDNSequence](https://pkg.go.dev/crypto/x509/pkix#RDNSequence) 構造体


を使う方法があります。どちらの構造体を使っても、課題があります。
### [pkix.Name](https://pkg.go.dev/crypto/x509/pkix#Name) 構造体を使う場合の課題
[pkix.Name](https://pkg.go.dev/crypto/x509/pkix#Name) 構造体を使う場合、以下の3つの課題があります。

#### ・ 生成される DN の RDN の順番が RDN に含まれる AttributeTypeAndValue の AttributeType によって決められてしまう
 [pkix.Name](https://pkg.go.dev/crypto/x509/pkix#Name) 構造体を使うと、RDN の順番が RDN に含まれる AttributeTypeAndValue の AttributeType によって決定されます。その結果 RDN の順番を指定することはできません。

#### ・ RDN に含まれる AttributeTypeAndValue の AttributeType が同じ RDN を持てない
AttributeType が同じ RDN を複数もつことができません。(Multi Value RDN は持つことはできます。)
具体的には、下記のような(OU が2つある) DN は作ることができません。
```C=JP,O=example,OU=Sales,OU=Dev,CN=hoge@example.com```

#### ・ AttributeValue でエンコーディングを指定できない
 AttributeValue でのエンコーディングの指定（ASN.1 文字型の指定）は、代入する文字の種類から自動的に決定されるため指定することはできません

### [pkix.RDNSequence](https://pkg.go.dev/crypto/x509/pkix#RDNSequence) 構造体
 [pkix.RDNSequence](https://pkg.go.dev/crypto/x509/pkix#RDNSequence) 構造体を使う場合、RDN の構造や順序は自由に設定できます。しかし、AttributeValue でのエンコーディングの指定に課題があります。

#### ・ AttributeValue でエンコーディングの指定(設定)がめんどくさい
[pkix.RDNSequence](https://pkg.go.dev/crypto/x509/pkix#RDNSequence) 構造体で AttributeValue でのエンコーディングの指定(設定)は簡単にはできません。単純に [AttributeTypeAndValue](https://pkg.go.dev/crypto/x509/pkix#AttributeTypeAndValue) 構造体の AttributeValue に string 型のインスタンスで文字列を代入すると [pkix.Name](https://pkg.go.dev/crypto/x509/pkix#Name) の時と同様、string 型のインスタンスで使っている文字の種類から AttributeValue のエンコーディングが自動的に決定され、指定できません。
エンコーディングを明示的に指定(設定)するには、 [AttributeTypeAndValue](https://pkg.go.dev/crypto/x509/pkix#AttributeTypeAndValue) 構造体の AttributeValue に対して、指定(設定)したいASN.1文字型の [asn1.RawValue](https://pkg.go.dev/encoding/asn1#RawValue) インスタンスを代入してやる必要があります。（わかりにくいと思うので、[サンプルコード](https://go.dev/play/p/ZcgJutNvcJ6)をご覧ください）


```go
	var b []byte
	st := "あ"
	b, err := asn1.MarshalWithParams(st, "utf8") //"あ"をUTF8String でエンコード。範囲外であればエラー
	if err != nil {
		log.Fatalf("ERROR:%v\n", err)
	}
	atv1 := pkix.AttributeTypeAndValue{
		Type: asn1.ObjectIdentifier{2, 5, 4, 3}, //CommonName
		Value: asn1.RawValue{
			Tag:       asn1.TagUTF8String, //UTF8String の ObjectIdentifier
			FullBytes: b, //UTF8String でエンコードされたバイナリ
		},
	}
    //CN=あ　の DN
	dn1 := pkix.RDNSequence{pkix.RelativeDistinguishedNameSET{atv1}}
```


# 4. 解決方法
以上から Golang で DN を扱う場合、従来の方法ではいくつか課題があることがわかりました。
そこでそれら課題を解決するため [dnutil](https://github.com/tardevnull/dnutil) というライブラリを作りました。このライブラリを使うと、自由に複雑な構造の DN を作成することができ、簡単に各 RDN の AttributeValue のエンコーディングを指定することができます。

# 5. [dnutil](https://github.com/tardevnull/dnutil) の使い方
[dnutil](https://github.com/tardevnull/dnutil) での DN の作成は簡単です。次のような DN を作りたければ、
　
```
C=JP,O=example,OU=Ext,OU=Dev+OU=Sales,CN=ex+E=ex@example.com
AttributeType: ASN.1文字型
C: PrintableString
O: UTF8String
OU=Ext: UTF8String
OU=Dev: UTF8String
OU=Sales: UTF8String
CN:UTF8String
E(ElectronicMailAddress):IA5String
```
以下の様に DN 構造体をインスタンス化するだけです。
```go
var d = dnutil.DN{
	dnutil.RDN{dnutil.AttributeTypeAndValue{Type: dnutil.CountryName, Value: dnutil.AttributeValue{Encoding: dnutil.PrintableString, Value: "JP"}}},
	dnutil.RDN{dnutil.AttributeTypeAndValue{Type: dnutil.OrganizationName, Value: dnutil.AttributeValue{Encoding: dnutil.UTF8String, Value: "example"}}},
	dnutil.RDN{dnutil.AttributeTypeAndValue{Type: dnutil.OrganizationalUnit, Value: dnutil.AttributeValue{Encoding: dnutil.UTF8String, Value: "Ext"}}},
	dnutil.RDN{
		dnutil.AttributeTypeAndValue{Type: dnutil.OrganizationalUnit, Value: dnutil.AttributeValue{Encoding: dnutil.UTF8String, Value: "Dev"}},
		dnutil.AttributeTypeAndValue{Type: dnutil.OrganizationalUnit, Value: dnutil.AttributeValue{Encoding: dnutil.UTF8String, Value: "Sales"}},},
	dnutil.RDN{
		dnutil.AttributeTypeAndValue{Type: dnutil.CommonName, Value: dnutil.AttributeValue{Encoding: dnutil.UTF8String, Value: "ex"}},
		dnutil.AttributeTypeAndValue{Type: dnutil.ElectronicMailAddress, Value: dnutil.AttributeValue{Encoding: dnutil.IA5String, Value: "ex@example.com"}}},
}
```

作成した DN のインスタンスから ASN.1の DER 形式の DN を作成するには、``` MarshalDN() ```関数を使います。

```go
b, err := dnutil.MarshalDN(d)
```
作成した DN を PEM 形式に変換して出力し、openssl で中身を確認してみました。目的の DN の形になっていることがわかります。
```sh
$ openssl asn1parse -in dn01.pem
    0:d=0  hl=2 l= 115 cons: SEQUENCE
    2:d=1  hl=2 l=  11 cons: SET
    4:d=2  hl=2 l=   9 cons: SEQUENCE
    6:d=3  hl=2 l=   3 prim: OBJECT            :countryName
   11:d=3  hl=2 l=   2 prim: PRINTABLESTRING   :JP
   15:d=1  hl=2 l=  16 cons: SET
   17:d=2  hl=2 l=  14 cons: SEQUENCE
   19:d=3  hl=2 l=   3 prim: OBJECT            :organizationName
   24:d=3  hl=2 l=   7 prim: UTF8STRING        :example
   33:d=1  hl=2 l=  12 cons: SET
   35:d=2  hl=2 l=  10 cons: SEQUENCE
   37:d=3  hl=2 l=   3 prim: OBJECT            :organizationalUnitName
   42:d=3  hl=2 l=   3 prim: UTF8STRING        :Ext
   47:d=1  hl=2 l=  26 cons: SET
   49:d=2  hl=2 l=  10 cons: SEQUENCE
   51:d=3  hl=2 l=   3 prim: OBJECT            :organizationalUnitName
   56:d=3  hl=2 l=   3 prim: UTF8STRING        :Dev
   61:d=2  hl=2 l=  12 cons: SEQUENCE
   63:d=3  hl=2 l=   3 prim: OBJECT            :organizationalUnitName
   68:d=3  hl=2 l=   5 prim: UTF8STRING        :Sales
   75:d=1  hl=2 l=  40 cons: SET
   77:d=2  hl=2 l=   9 cons: SEQUENCE
   79:d=3  hl=2 l=   3 prim: OBJECT            :commonName
   84:d=3  hl=2 l=   2 prim: UTF8STRING        :ex
   88:d=2  hl=2 l=  27 cons: SEQUENCE
   90:d=3  hl=2 l=   9 prim: OBJECT            :emailAddress
  101:d=3  hl=2 l=  14 prim: IA5STRING         :ex@example.com


```

逆に ASN.1 の DER 形式の DN から DN インスタンスを作成したい場合は、``` ParseDERDN() ```関数を使います。
```go
//CN=abc (UTF8String)
b := []byte{0x30, 0x0e, 0x31, 0x0c, 0x30, 0x0a, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x03, 0x61, 0x62, 0x63}
dn, err := dnutil.ParseDERDn(b)
```
[dnutil](https://github.com/tardevnull/dnutil) を使って [ASN1.のDER 形式の DN を作成し、CSR を 作成する例](https://go.dev/play/p/s7SxXlzm-TK)のコードをつくってみました。
気になる方は試してみてください

# 5. 制限事項
AttributeValue で利用可能な ASN.1 のエンコーディングは以下の3種類です。[DirectoryString](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.4) のうち、TeletexString、UniversalString、BMPString は サポートしていません。

```
  PrintableString 
  UTF8String
  IA5String
```
AttributeType で利用可能なものは以下の通りです。

```
2.5.4.6  CountryName
2.5.4.10  OrganizationName
2.5.4.11  OrganizationalUnit
2.5.4.46  DnQualifier
2.5.4.8  StateOrProvinceName
2.5.4.3  CommonName
2.5.4.5  SerialNumber
2.5.4.7  LocalityName
2.5.4.12  Title
2.5.4.4  Surname
2.5.4.42  GivenName
2.5.4.43  Initials
2.5.4.65  Pseudonym
2.5.4.44  GenerationQualifier
1.2.840.113549.1.9.1  ElectronicMailAddress
0.9.2342.19200300.100.1.25  DomainComponent
```
各 AttributeType に[対応](https://datatracker.ietf.org/doc/html/rfc5280#appendix-A)する AttributeValue で利用可能な ASN.1 のエンコーディングは、次の通りです。
```
  CountryName : PrintableString
  OrganizationName : PrintableString or UTF8String
  OrganizationalUnit : PrintableString or UTF8String
  DnQualifier : PrintableString
  StateOrProvinceName : PrintableString or UTF8String
  CommonName : PrintableString or UTF8String
  SerialNumber : PrintableString
  LocalityName : PrintableString or UTF8String
  Title : PrintableString or UTF8String
  Surname : PrintableString or UTF8String
  GivenName : PrintableString or UTF8String
  Initials : PrintableString or UTF8String
  Pseudonym : PrintableString or UTF8String
  GenerationQualifier : PrintableString or UTF8String
  ElectronicMailAddress : IA5String
  DomainComponent : IA5String
```

# 5. ライセンス
[dnutil](https://github.com/tardevnull/dnutil) のライセンスは [BSD 3-Clause](https://github.com/tardevnull/dnutil/blob/main/LICENSE) です。