[インデックス 13455] ファイルの概要
このコミットは、Go言語の標準ライブラリであるencoding/asn1
パッケージにおける文字列のエンコーディング挙動を改善するものです。具体的には、ASN.1の型が明示されていない文字列が、その内容に応じてPrintableString
からUTF8String
へ自動的に昇格されるように変更されました。これにより、より広範な文字セット(特に非ASCII文字)を含む文字列が、エラーなくASN.1形式でエンコードできるようになります。
変更されたファイルは以下の通りです。
src/pkg/crypto/x509/x509_test.go
: X.509証明書のテストファイル。非ASCII文字を含む組織名で証明書を生成するテストケースが追加され、この変更の必要性を示しています。src/pkg/encoding/asn1/common.go
: ASN.1のフィールドパラメータを解析する共通関数が含まれています。utf8
という文字列型指定を認識するように変更されました。src/pkg/encoding/asn1/marshal.go
: ASN.1構造体をバイト列にマーシャル(エンコード)する主要なロジックが含まれています。文字列の型推論と変換の核心的な変更がここで行われました。src/pkg/encoding/asn1/marshal_test.go
:encoding/asn1
パッケージのマーシャル処理に関するテストファイル。非ASCII文字を含む文字列のマーシャルテストと、不正なUTF-8文字列が拒否されることを確認するテストが追加されました。
コミット
commit eeffa738a912a8c4d283c37d84628b64ecc1b98f
Author: Adam Langley <agl@golang.org>
Date: Tue Jul 10 18:23:30 2012 -0400
encoding/asn1: promote untyped strings to UTF8 as needed.
Previously, strings that didn't have an explicit ASN.1 string type
were taken to be ASN.1 PrintableStrings. This resulted in an error if
a unrepresentable charactor was included.
For compatibility reasons, I'm too afraid to switch the default string
type to UTF8String, but this patch causes untyped strings to become
UTF8Strings if they contain a charactor that's not valid in a
PrintableString.
Fixes #3791.
R=golang-dev, bradfitz, r, r
CC=golang-dev
https://golang.org/cl/6348074
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/eeffa738a912a8c4d283c37d84628b64ecc1b98f
元コミット内容
このコミットが適用される前は、Goのencoding/asn1
パッケージにおいて、Goの構造体フィールドがASN.1の特定の文字列型(例: IA5String
, PrintableString
, UTF8String
など)を明示的に指定していない場合、その文字列はデフォルトでASN.1のPrintableString
として扱われていました。
PrintableString
は、その名の通り「印刷可能な」ASCII文字(英数字、一部の記号、スペースなど)のみを許容する非常に制限された文字セットを持つASN.1の文字列型です。そのため、Goの文字列がPrintableString
の許容範囲外の文字(例えば、日本語、ギリシャ文字、絵文字などの非ASCII文字や、PrintableString
で許可されていないASCII記号)を含んでいた場合、マーシャル処理中にエラーが発生し、ASN.1エンコーディングが失敗していました。
この挙動は、特にX.509証明書のような、国際化された情報(組織名、国名など)を含む可能性のあるASN.1構造体を扱う際に問題となっていました。
変更の背景
変更の主な背景は、前述のPrintableString
の文字セット制限によるエンコーディングエラーの発生です。Go言語の文字列はUTF-8エンコードされており、任意のUnicode文字を扱うことができます。しかし、encoding/asn1
パッケージがデフォルトでPrintableString
にフォールバックする挙動は、Goの文字列の柔軟性と矛盾し、ユーザーが非ASCII文字を含むデータをASN.1形式で扱うことを妨げていました。
具体的には、Issue #3791で報告された問題に対応するためにこの変更が導入されました。この問題は、X.509証明書のCommon NameやOrganizationフィールドに非ASCII文字(例: Σ
)が含まれている場合に、証明書の生成(ASN.1エンコーディング)が失敗するというものでした。
開発者は、互換性の問題を避けるため、デフォルトの文字列型をUTF8String
に一律で切り替えることを避けました。代わりに、既存のコードベースへの影響を最小限に抑えつつ、必要な場合にのみUTF8String
への「昇格」を行うという、より段階的なアプローチが選択されました。これにより、既存のPrintableString
に準拠したデータは引き続き正しく処理され、同時に、より広範な文字セットを必要とする新しいデータも適切にエンコードできるようになりました。
前提知識の解説
ASN.1 (Abstract Syntax Notation One)
ASN.1は、データ構造を記述するための国際標準です。異なるシステム間でデータを交換する際に、データのフォーマットを明確に定義するために使用されます。特に、通信プロトコル、暗号化(X.509証明書、PKCS#7など)、ネットワーク管理(SNMP)などの分野で広く利用されています。
ASN.1は、データの型(整数、文字列、シーケンス、セットなど)と、それらの型がどのようにエンコードされるか(BER, DER, PERなどのエンコーディングルール)を定義します。
ASN.1の文字列型
ASN.1には、様々な種類の文字列型が定義されており、それぞれが異なる文字セットとエンコーディング規則を持ちます。このコミットに関連する主要な文字列型は以下の通りです。
PrintableString
:- 許容文字: 大文字A-Z、小文字a-z、数字0-9、スペース、および以下の記号:
' () + , - . / : = ?
- 非常に制限された文字セットであり、主に古いシステムや特定のプロトコルで使用されます。非ASCII文字や、上記以外の記号は使用できません。
- 許容文字: 大文字A-Z、小文字a-z、数字0-9、スペース、および以下の記号:
IA5String
(International Alphabet No. 5 String):- 許容文字: ASCII文字セット(0x00から0x7Fまでの128文字)。
- 電子メールアドレスやドメイン名など、ASCII文字のみが保証される場合に使用されます。
UTF8String
:- 許容文字: UTF-8エンコードされた任意のUnicode文字。
- 最も柔軟な文字列型であり、多言語対応が必要な場合に推奨されます。現代のシステムでは広く採用されています。
Goのencoding/asn1
パッケージ
Go言語の標準ライブラリであるencoding/asn1
パッケージは、Goの構造体とASN.1のデータ構造との間でマーシャル(エンコード)およびアンマーシャル(デコード)を行う機能を提供します。Goの構造体フィールドにタグ(例: asn1:"ia5"
, asn1:"printable"
, asn1:"utf8"
)を付与することで、対応するASN.1の型を指定できます。タグがない場合、パッケージはデフォルトのルールに基づいて型を推論します。
X.509証明書
X.509は、公開鍵証明書のフォーマットを定義するITU-Tの標準です。インターネット上のTLS/SSL通信、コード署名、電子メールの暗号化など、様々なセキュリティアプリケーションで利用されています。X.509証明書はASN.1とDER(Distinguished Encoding Rules、ASN.1のエンコーディングルールの一つ)を使用してエンコードされます。証明書には、発行者、サブジェクト(所有者)、公開鍵、有効期間などの情報が含まれ、これらの情報の一部は文字列として表現されます。
技術的詳細
このコミットの技術的な核心は、encoding/asn1
パッケージがGoの文字列をASN.1形式にマーシャルする際の挙動変更にあります。
-
デフォルトの挙動の変更:
- 以前は、Goの構造体フィールドにASN.1の文字列型が明示的に指定されていない場合、
encoding/asn1
はデフォルトでその文字列をPrintableString
として扱おうとしました。 - このコミットにより、明示的な型指定がない文字列(または
PrintableString
としてタグ付けされた文字列)に対して、その内容を検査するロジックが追加されました。
- 以前は、Goの構造体フィールドにASN.1の文字列型が明示的に指定されていない場合、
-
PrintableString
からUTF8String
への昇格ロジック:src/pkg/encoding/asn1/marshal.go
のmarshalField
関数内で、文字列がPrintableString
として処理されるべきと判断された場合(明示的なタグがないか、printable
タグが付いている場合)、文字列内の各文字(rune)が検査されます。- 検査は、
unicode/utf8
パッケージとisPrintable
ヘルパー関数(PrintableString
の文字セットチェック)を使用して行われます。 - もし文字列内に
PrintableString
で表現できない文字(例:utf8.RuneSelf
以上の値を持つUnicode文字、またはisPrintable
がfalseを返す文字)が一つでも見つかった場合、その文字列はUTF8String
としてエンコードされるように動的にタグが変更されます。 - この際、文字列全体が有効なUTF-8であるかどうかも
utf8.ValidString
でチェックされ、無効な場合はエラーが返されます。
-
明示的な
utf8
タグのサポート:src/pkg/encoding/asn1/common.go
のparseFieldParameters
関数に、構造体タグでasn1:"utf8"
と明示的に指定された場合にtagUTF8String
を認識するロジックが追加されました。これにより、開発者は必要に応じて明示的にUTF8String
を指定できるようになりました。
-
新しいマーシャル関数の追加:
marshalUTF8String
関数がsrc/pkg/encoding/asn1/marshal.go
に追加されました。これは、UTF-8文字列をASN.1形式でエンコードする際に使用されます。
-
テストケースの追加:
src/pkg/crypto/x509/x509_test.go
では、組織名にギリシャ文字のΣ
を含む証明書を生成するテストが追加され、この変更がX.509証明書の国際化をサポートすることを示しています。src/pkg/encoding/asn1/marshal_test.go
では、Σ
を含む文字列が正しくUTF8String
としてマーシャルされること、および不正なUTF-8バイト列が拒否されることを確認するテストが追加されました。
この変更により、Goのencoding/asn1
パッケージは、より堅牢で柔軟な文字列エンコーディングを提供し、国際化されたデータを含むASN.1構造体の扱いが容易になりました。
コアとなるコードの変更箇所
このコミットの核心的な変更は、src/pkg/encoding/asn1/marshal.go
ファイルに集中しています。
-
marshalUTF8String
関数の追加:func marshalUTF8String(out *forkableWriter, s string) (err error) { _, err = out.Write([]byte(s)) return }
この関数は、UTF-8文字列をバイト列として直接
forkableWriter
に書き込むシンプルなものです。ASN.1のUTF8String
は、UTF-8エンコードされたバイト列をそのまま値として持ちます。 -
marshalBody
関数の変更:reflect.String
型を処理するcase
文が変更されました。--- a/src/pkg/encoding/asn1/marshal.go +++ b/src/pkg/encoding/asn1/marshal.go @@ -446,10 +453,13 @@ func marshalBody(out *forkableWriter, value reflect.Value, params fieldParameter } return case reflect.String: - if params.stringType == tagIA5String { + switch params.stringType { + case tagIA5String: return marshalIA5String(out, v.String()) - } else { + case tagPrintableString: return marshalPrintableString(out, v.String()) + default: + return marshalUTF8String(out, v.String()) } return }
以前は
IA5String
でなければ一律PrintableString
としてマーシャルしていましたが、この変更により、IA5String
でもPrintableString
でもない明示的な文字列型が指定された場合(またはデフォルトの場合)は、新しく追加されたmarshalUTF8String
を使用するようになりました。 -
marshalField
関数の変更(最も重要): この部分が、文字列の動的な型昇格ロジックを実装しています。--- a/src/pkg/encoding/asn1/marshal.go +++ b/src/pkg/encoding/asn1/marshal.go @@ -492,11 +502,27 @@ func marshalField(out *forkableWriter, v reflect.Value, params fieldParameters) } class := classUniversal - if params.stringType != 0 { - if tag != tagPrintableString { - return StructuralError{"Explicit string type given to non-string member"} + if params.stringType != 0 && tag != tagPrintableString { + return StructuralError{"Explicit string type given to non-string member"} + } + + if tag == tagPrintableString { + if params.stringType == 0 { + // This is a string without an explicit string type. We'll use + // a PrintableString if the character set in the string is + // sufficiently limited, otherwise we'll use a UTF8String. + for _, r := range v.String() { + if r >= utf8.RuneSelf || !isPrintable(byte(r)) { + if !utf8.ValidString(v.String()) { + return errors.New("asn1: string not valid UTF-8") + } + tag = tagUTF8String + break + } + } + } else { + tag = params.stringType } - tag = params.stringType } if params.set {
if params.stringType != 0 && tag != tagPrintableString
: 明示的な文字列型が指定されているにもかかわらず、それがPrintableString
でない場合(つまり、IA5String
やUTF8String
が指定されているのに、tag
がPrintableString
でない場合)、構造エラーを返します。これは、型指定の矛盾を防ぐためのチェックです。if tag == tagPrintableString
: ここが主要なロジックです。tag
がPrintableString
であると判断された場合(これは、明示的な型指定がないか、printable
タグが付いている場合に発生します)。if params.stringType == 0
: 明示的な文字列型が指定されていない場合(つまり、デフォルトのPrintableString
として扱われる場合)。for _, r := range v.String()
: 文字列内の各Unicodeコードポイント(rune)をループで検査します。if r >= utf8.RuneSelf || !isPrintable(byte(r))
: もしruneがutf8.RuneSelf
(ASCII範囲外のマルチバイト文字の開始バイト)以上であるか、またはisPrintable
関数がfalse
を返す(PrintableString
の許容文字セット外)場合、その文字列はPrintableString
では表現できないと判断されます。if !utf8.ValidString(v.String())
: その後、文字列全体が有効なUTF-8であるかを確認します。無効な場合はエラーを返します。tag = tagUTF8String
: 有効なUTF-8であり、かつPrintableString
で表現できない文字を含む場合、tag
をtagUTF8String
に設定し、後続の処理でUTF8String
としてエンコードされるようにします。break
: 一度でもPrintableString
で表現できない文字が見つかれば、それ以上ループする必要はないため、ループを抜けます。
else { tag = params.stringType }
: 明示的にprintable
タグが指定されている場合は、そのままparams.stringType
(つまりtagPrintableString
)を使用します。
コアとなるコードの解説
marshalField
関数内の変更は、Goのencoding/asn1
パッケージが文字列をASN.1形式にエンコードする際の「賢い」挙動を導入しています。
-
デフォルトの
PrintableString
の挙動の変更: 以前は、Goの文字列がASN.1の型を明示的に指定していない場合、無条件にPrintableString
として扱われました。これは、非ASCII文字を含む文字列をエンコードしようとするとエラーになる原因でした。 -
動的な型昇格の導入: 新しいロジックでは、まず文字列が
PrintableString
として扱われるべきかどうかを判断します(これは、Goの構造体フィールドにasn1:"printable"
タグが付いているか、または何のタグも付いていない場合に発生します)。 もしそうであれば、文字列の各文字(rune)を走査し、その文字がPrintableString
の許容文字セットに含まれているかをチェックします。isPrintable(byte(r))
: このヘルパー関数は、与えられたバイトがPrintableString
で許可されているASCII文字(英数字、一部の記号)であるかを判定します。r >= utf8.RuneSelf
:utf8.RuneSelf
は128(0x80)です。これは、UTF-8エンコードされたマルチバイト文字の最初のバイトが常に128以上であるという事実を利用しています。つまり、この条件は、文字がASCII範囲外(非ASCII文字)であるかを効率的にチェックします。
-
UTF8String
への切り替え: もし文字列内にPrintableString
で表現できない文字(非ASCII文字など)が一つでも見つかった場合、システムは「この文字列はPrintableString
では表現できない」と判断します。この時点で、文字列全体が有効なUTF-8であるかどうかの最終チェックが行われます(utf8.ValidString
)。このチェックが成功すれば、ASN.1のエンコーディングタグはtagUTF8String
に動的に変更され、文字列はUTF8String
としてエンコードされます。これにより、非ASCII文字を含む文字列もエラーなく処理できるようになります。 -
互換性と堅牢性: このアプローチは、既存の
PrintableString
に準拠したデータはそのままPrintableString
として処理しつつ、より広範な文字セットを必要とする新しいデータに対しては自動的にUTF8String
に切り替えることで、後方互換性を保ちながら機能性を向上させています。また、不正なUTF-8文字列を検出してエラーを返すことで、データの整合性も確保しています。
この変更は、Goのencoding/asn1
パッケージが、より現代的なアプリケーションの要件(特に国際化されたデータの処理)に対応するための重要なステップと言えます。
関連リンク
- Go Issue #3791: encoding/asn1: untyped strings should be UTF8String if they contain non-PrintableString characters - golang/go
- Go Code Review (CL 6348074): https://golang.org/cl/6348074
参考にした情報源リンク
- Abstract Syntax Notation One (ASN.1):
- ASN.1 String Types:
- Go
encoding/asn1
package documentation: - Go
unicode/utf8
package documentation: - X.509 Certificate Standard:
- UTF-8: