RDNSequence.String/Name.Stringが、未知のOIDでも文字列型の属性値は文字列としてレンダリングするようになった
crypto/x509/pkix
概要
RDNSequence.String(および内部で利用されるName.String)が、属性のOIDが未知(パッケージが名前を認識できない)であっても、属性値がstring型であればそのまま文字列としてレンダリングするようになった。従来はそのような値も常にDER形式のままhexエンコードして出力していた。
導入経緯
OID 1.2.3.4 のような独自のExtraNameを持つpkix.Nameを文字列化すると、値がPrintableStringやUTF8Stringのように文字列表現を持つ型であっても#130673616d706c65のようにhexエンコードされてしまい、RFC 2253が要求する人間可読な文字列表現(例: 1.2.3.4=sample)にならないという報告が#33093で挙がった。
RFC 2253 §2.4は、AttributeValueが文字列表現を持つ型であればUTF-8文字列に変換して出力し、文字列表現を持たない型の場合にのみhexエンコードすべきと規定している。従来の実装はこの区別をせず、認識できないOIDの値を一律hexエンコードしていた。
議論タイムライン
- 2019-08-14: #33093 が報告される(hex-encodeされたExtraNameがOpenSSLの出力と異なる)。
- 2019-10-24: メンテナからRFC 2253の該当箇所の解釈について質問があり、サンプル証明書の提示が求められる。
- 2022-05-04: RFC 2253の仕様解釈について詳細な分析コメントが投稿され、UTF8String等は文字列表現を持つため変換不要であると指摘。
- 2023-12-19: CL 549075 がissueに言及される形で送付される。
- 2023-12-19: メンテナが分析に同意し、Go 1.21での取り込みに前向きな姿勢を示す。
- 2026年: レビューが進まなかったため、Go 1.27マイルストーンへ再度繰り越された上で、新たにCL 773800が送付され取り込まれた。
議論のハイライト
- 当初、メンテナはRFC 2253の
#によるhexエンコードの規定を「ドット区切りOIDで表現される非標準属性は常にhexエンコードすべき」という意味で解釈していたが、報告者はRFC本文が言及しているのは「文字列表現を持たない型」であって「非標準属性」ではないと指摘し、この解釈の誤りが後に修正の起点となった。 - 報告者はOpenSSLの出力(
subject=/CN=foobar/1.2.3.4=sample)を引き合いに出し、Goの出力が人間可読でない点を問題視した。
使用例
Before
package main
import (
"crypto/x509/pkix"
"encoding/asn1"
"fmt"
)
func main() {
name := pkix.Name{
ExtraNames: []pkix.AttributeTypeAndValue{
{
Type: asn1.ObjectIdentifier{1, 2, 3, 4},
Value: "sample",
},
},
}
// Go 1.26以前は文字列型の値であってもOIDが未知のためhexエンコードされ、
// "1.2.3.4=#130673616d706c65" のように出力されていた。
fmt.Println(name.String())
}
After
package main
import (
"crypto/x509/pkix"
"encoding/asn1"
"fmt"
)
func main() {
name := pkix.Name{
ExtraNames: []pkix.AttributeTypeAndValue{
{
Type: asn1.ObjectIdentifier{1, 2, 3, 4},
Value: "sample",
},
},
}
// Go 1.27では値がstring型であるため、そのまま "1.2.3.4=sample" として出力される。
fmt.Println(name.String())
}
移行時の注意
この変更により、未知のOIDに対応するstring型の属性値を持つpkix.Name/pkix.RDNSequenceをString()で文字列化した際の出力形式が変わる。従来のhexエンコードされた出力(OID=#...形式)をパースまたは文字列比較に依存しているコードがある場合、出力がOID=値形式に変わることで影響を受ける可能性がある。なお、string型でない値([]byteやasn1.RawValueなど、文字列表現を持たない型)については、従来通りhexエンコードされる。
実装解説
RDNSequence.Stringは、属性のOIDがattributeTypeNamesマップ(C、O、CNなど既知の短縮名)に存在しない場合、こちらの分岐で処理を行う。変更後は、まずtv.Valueがstring型であるかどうかを型アサーションで判定し、string型でない場合にのみASN.1として再マーシャルしてhexエンコードするようになった。string型の場合はこの分岐を素通りし、後段の共通処理(RDNSequence.String内、55行目以降)で通常の属性値と同様にRFC 2253のエスケープルールを適用した上で文字列としてレンダリングされる。
AttributeTypeAndValueのドキュメントコメントにある通り、ASN.1のPrintableString・IA5String・NumericString・BMPString・T61String・UTF8Stringはパース時にGoのstring型にマッピングされるため、これらの型であれば今回の変更でhexエンコードを回避できる。一方でOCTET STRINGは[]byteにマッピングされるため、引き続きhexエンコードの対象となる。