メインコンテンツへスキップ

Go Proposal Weekly Digest

Go言語のproposal更新を毎週お届け

挙動変更

crypto/x509がpkix.Nameの属性値として文字列型以外のASN.1型も受理するようになった

crypto/x509

この項目の注釈は AI により生成されており、誤りを含む場合があります。
使用例のコンパイル検証: 検証済み

概要

crypto/x509 の証明書パーサーが、Subject/Issuerの識別名(pkix.Name)に含まれる属性値(pkix.AttributeTypeAndValue.Value)として、これまで拒否していた文字列型以外のASN.1型も受理するようになった。従来は PrintableStringIA5StringNumericStringBMPStringT61StringUTF8String の6種類の文字列型以外の属性値を持つ証明書は x509: invalid RDNSequence: invalid attribute value: unsupported string type: N というエラーで一律に拒否されていたが、今後は INTEGERint64BIT STRINGasn1.BitStringOCTET STRING[]byteOBJECT IDENTIFIERasn1.ObjectIdentifierUTCTime/GeneralizedTimetime.TimeBOOLEANboolNULLnil としてパースされ、それ以外の未知のASN.1型(RELATIVE-OID など)は asn1.RawValue として格納される。この型対応関係は pkix.AttributeTypeAndValue のドキュメントにも明記された。

導入経緯

発端はMerkle Tree CertificatesTLS Trust Anchor IDsといった新しいTLS関連ドラフト仕様が、属性値に RELATIVE-OID 型を使用する点だった。これらの仕様に沿った証明書を x509.ParseCertificate でパースしようとすると unsupported string type: 13 のエラーになることがissue本文で報告された。調査の結果、問題の所在は x/crypto/cryptobyte ではなく crypto/x509 内部の文字列パース処理(属性値を文字列型のみと仮定していた実装)にあることが判明し、RFC 5280上 AttributeValueANY 型として定義されている以上、任意のASN.1型を受理するのが仕様準拠であるという方向で議論が進んだ。

議論タイムライン

  • 2026-04-22: 提案レビューミーティングでactive列に追加。
  • 2026-05-06: 合意が得られ、likely acceptと判断。
  • 2026-05-13: 前回から合意に変更がないため、acceptedとして正式に受理。

議論のハイライト

  • 未知の属性値型の表現方法として asn1.RawValue を使う案(選択肢6)が採用された。既存の encoding/asn1any 型へのアンマーシャル時に未知の型を nil にしてしまい情報を失っていた問題を踏まえ、生のASN.1要素をそのまま保持できる asn1.RawValue が選ばれた。
  • OBJECT IDENTIFIER の表現には、大きなOID成分もサポートする新しい x509.OID ではなく、既存の asn1.ObjectIdentifier が採用された。crypto/x509/pkix パッケージが crypto/x509 を参照する構造は望ましくないという理由による。
  • 型対応関係のドキュメント化の場所については、pkix.AttributeTypeAndValue.Value がすでに any 型であるため冗長ではという意見もあったが、最終的に「利用者が最も見つけやすい場所」として pkix.AttributeTypeAndValue のドキュメントに明記する方針になった。
  • Go 1.16以前は encoding/asn1 経由の汎用的な Unmarshal によって一部の型(OCTET STRINGOBJECT IDENTIFIER など)が偶然パースできていたが、Go 1.17での x/crypto/cryptobyte への移行(CL 274234)で現在の厳格な挙動になっていたことが調査で判明した。

使用例

Before

package main

import (
	"encoding/pem"
	"fmt"

	"crypto/x509"
)

// OCTET STRING や RELATIVE-OID など、文字列型以外の属性値を
// Subject名に含む証明書。
const certPEM = `
-----BEGIN CERTIFICATE-----
MIIB0DCCAXagAwIBAgIJAIZft7jy3RcpMAoGCCqGSM49BAMCMA8xDTALBgNVBAMM
BFRlc3QwHhcNMjUwOTAyMTg0MzE3WhcNMjUxMDAyMTg0MzE3WjCByzEeMBwGDSqG
SIb3EgQBhLcJAgEMC3V0Zjgtc3RyaW5nMR8wHQYNKoZIhvcSBAGEtwkCAgQMb2N0
ZXQtc3RyaW5nMRgwFgYNKoZIhvcSBAGEtwkCAw0FAQIDBAUxEzARBg0qhkiG9xIE
AYS3CQIEBQAxITAfBg0qhkiG9xIEAYS3CQIFMA4MBWhlbGxvDAV3b3JsZDEUMBIG
DSqGSIb3EgQBhLcJAgYCASoxIDAeBg0qhkiG9xIEAYS3CQIHBg0qhkiG9xIEAYS3
CQIHMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7M4zoqQtXbvGsudKaM5gd8em
xyk68AFTjIYU4PO1AtiYX3wyL89kwbHjxgvmh/9aBg1LOj6kfsJIxULUmUpdzTAK
BggqhkjOPQQDAgNIADBFAiBvKz3oALhCqKrRFLUbax6+tI1s1B14IPVk2ZHbBEou
5gIhAOpvJRNj5qluPXKLXmZvIK8uOjUhiZoowYvborSS1EBK
-----END CERTIFICATE-----
`

func main() {
	block, _ := pem.Decode([]byte(certPEM))
	_, err := x509.ParseCertificate(block.Bytes)
	// err: x509: invalid RDNSequence: invalid attribute value: unsupported string type: 4
	fmt.Println(err)
}

After

package main

import (
	"encoding/asn1"
	"encoding/pem"
	"fmt"

	"crypto/x509"
)

const certPEM = `
-----BEGIN CERTIFICATE-----
MIIB0DCCAXagAwIBAgIJAIZft7jy3RcpMAoGCCqGSM49BAMCMA8xDTALBgNVBAMM
BFRlc3QwHhcNMjUwOTAyMTg0MzE3WhcNMjUxMDAyMTg0MzE3WjCByzEeMBwGDSqG
SIb3EgQBhLcJAgEMC3V0Zjgtc3RyaW5nMR8wHQYNKoZIhvcSBAGEtwkCAgQMb2N0
ZXQtc3RyaW5nMRgwFgYNKoZIhvcSBAGEtwkCAw0FAQIDBAUxEzARBg0qhkiG9xIE
AYS3CQIEBQAxITAfBg0qhkiG9xIEAYS3CQIFMA4MBWhlbGxvDAV3b3JsZDEUMBIG
DSqGSIb3EgQBhLcJAgYCASoxIDAeBg0qhkiG9xIEAYS3CQIHBg0qhkiG9xIEAYS3
CQIHMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7M4zoqQtXbvGsudKaM5gd8em
xyk68AFTjIYU4PO1AtiYX3wyL89kwbHjxgvmh/9aBg1LOj6kfsJIxULUmUpdzTAK
BggqhkjOPQQDAgNIADBFAiBvKz3oALhCqKrRFLUbax6+tI1s1B14IPVk2ZHbBEou
5gIhAOpvJRNj5qluPXKLXmZvIK8uOjUhiZoowYvborSS1EBK
-----END CERTIFICATE-----
`

func main() {
	block, _ := pem.Decode([]byte(certPEM))
	cert, err := x509.ParseCertificate(block.Bytes)
	if err != nil {
		panic(err)
	}
	for _, attr := range cert.Subject.Names {
		switch v := attr.Value.(type) {
		case string:
			fmt.Printf("string: %s = %s\n", attr.Type, v)
		case []byte:
			fmt.Printf("OCTET STRING: %s = %x\n", attr.Type, v)
		case int64:
			fmt.Printf("INTEGER: %s = %d\n", attr.Type, v)
		case asn1.ObjectIdentifier:
			fmt.Printf("OBJECT IDENTIFIER: %s = %s\n", attr.Type, v)
		case nil:
			fmt.Printf("NULL: %s\n", attr.Type)
		case asn1.RawValue:
			fmt.Printf("RawValue (unknown type): %s tag=%d\n", attr.Type, v.Tag)
		default:
			fmt.Printf("other: %s = %v (%T)\n", attr.Type, v, v)
		}
	}
}

移行時の注意

pkix.AttributeTypeAndValue.Value はもともと any 型であるため型定義自体に変更はないが、これまでパースエラーになっていた証明書がエラーなくパースできるようになる点に注意する。未知の属性値型を asn1.RawValue として受け取る前提のコードでは、パース成功後に attr.Value の型スイッチに asn1.RawValue ケースを追加しておくとよい。なお、未知の型として RawValue に一度バインドした値を後から既知の型に「昇格」させる変更は、将来的に非互換になり得る点が議論中で指摘されている。

実装解説

実装は crypto/x509 の内部関数 readASN1Any に集約されている。readASN1Any はASN.1のタグに応じて値を分岐させ、文字列型は既存の parseASN1String に委譲し、INTEGERBIT STRINGOCTET STRINGOBJECT IDENTIFIERUTCTime/GeneralizedTimeBOOLEANNULL をそれぞれ対応するGoの型に変換する。default節では、上記のいずれにも該当しないタグを asn1.RawValue(Class/IsCompound/Tag/FullBytes/Bytesを埋めたもの)としてそのまま保持する。この関数は parseName から呼ばれ、RDNSequence 内の各 AttributeTypeAndValue.Value の生成に使われている。

関連リンク