Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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文字や、上記以外の記号は使用できません。
  • 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形式にマーシャルする際の挙動変更にあります。

  1. デフォルトの挙動の変更:

    • 以前は、Goの構造体フィールドにASN.1の文字列型が明示的に指定されていない場合、encoding/asn1はデフォルトでその文字列をPrintableStringとして扱おうとしました。
    • このコミットにより、明示的な型指定がない文字列(またはPrintableStringとしてタグ付けされた文字列)に対して、その内容を検査するロジックが追加されました。
  2. PrintableStringからUTF8Stringへの昇格ロジック:

    • src/pkg/encoding/asn1/marshal.gomarshalField関数内で、文字列がPrintableStringとして処理されるべきと判断された場合(明示的なタグがないか、printableタグが付いている場合)、文字列内の各文字(rune)が検査されます。
    • 検査は、unicode/utf8パッケージとisPrintableヘルパー関数(PrintableStringの文字セットチェック)を使用して行われます。
    • もし文字列内にPrintableStringで表現できない文字(例: utf8.RuneSelf以上の値を持つUnicode文字、またはisPrintableがfalseを返す文字)が一つでも見つかった場合、その文字列はUTF8Stringとしてエンコードされるように動的にタグが変更されます。
    • この際、文字列全体が有効なUTF-8であるかどうかもutf8.ValidStringでチェックされ、無効な場合はエラーが返されます。
  3. 明示的なutf8タグのサポート:

    • src/pkg/encoding/asn1/common.goparseFieldParameters関数に、構造体タグでasn1:"utf8"と明示的に指定された場合にtagUTF8Stringを認識するロジックが追加されました。これにより、開発者は必要に応じて明示的にUTF8Stringを指定できるようになりました。
  4. 新しいマーシャル関数の追加:

    • marshalUTF8String関数がsrc/pkg/encoding/asn1/marshal.goに追加されました。これは、UTF-8文字列をASN.1形式でエンコードする際に使用されます。
  5. テストケースの追加:

    • 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ファイルに集中しています。

  1. marshalUTF8String関数の追加:

    func marshalUTF8String(out *forkableWriter, s string) (err error) {
    	_, err = out.Write([]byte(s))
    	return
    }
    

    この関数は、UTF-8文字列をバイト列として直接forkableWriterに書き込むシンプルなものです。ASN.1のUTF8Stringは、UTF-8エンコードされたバイト列をそのまま値として持ちます。

  2. 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を使用するようになりました。

  3. 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でない場合(つまり、IA5StringUTF8Stringが指定されているのに、tagPrintableStringでない場合)、構造エラーを返します。これは、型指定の矛盾を防ぐためのチェックです。
    • if tag == tagPrintableString: ここが主要なロジックです。tagPrintableStringであると判断された場合(これは、明示的な型指定がないか、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で表現できない文字を含む場合、tagtagUTF8Stringに設定し、後続の処理でUTF8Stringとしてエンコードされるようにします。
        • break: 一度でもPrintableStringで表現できない文字が見つかれば、それ以上ループする必要はないため、ループを抜けます。
      • else { tag = params.stringType }: 明示的にprintableタグが指定されている場合は、そのままparams.stringType(つまりtagPrintableString)を使用します。

コアとなるコードの解説

marshalField関数内の変更は、Goのencoding/asn1パッケージが文字列をASN.1形式にエンコードする際の「賢い」挙動を導入しています。

  1. デフォルトのPrintableStringの挙動の変更: 以前は、Goの文字列がASN.1の型を明示的に指定していない場合、無条件にPrintableStringとして扱われました。これは、非ASCII文字を含む文字列をエンコードしようとするとエラーになる原因でした。

  2. 動的な型昇格の導入: 新しいロジックでは、まず文字列がPrintableStringとして扱われるべきかどうかを判断します(これは、Goの構造体フィールドにasn1:"printable"タグが付いているか、または何のタグも付いていない場合に発生します)。 もしそうであれば、文字列の各文字(rune)を走査し、その文字がPrintableStringの許容文字セットに含まれているかをチェックします。

    • isPrintable(byte(r)): このヘルパー関数は、与えられたバイトがPrintableStringで許可されているASCII文字(英数字、一部の記号)であるかを判定します。
    • r >= utf8.RuneSelf: utf8.RuneSelfは128(0x80)です。これは、UTF-8エンコードされたマルチバイト文字の最初のバイトが常に128以上であるという事実を利用しています。つまり、この条件は、文字がASCII範囲外(非ASCII文字)であるかを効率的にチェックします。
  3. UTF8Stringへの切り替え: もし文字列内にPrintableStringで表現できない文字(非ASCII文字など)が一つでも見つかった場合、システムは「この文字列はPrintableStringでは表現できない」と判断します。この時点で、文字列全体が有効なUTF-8であるかどうかの最終チェックが行われます(utf8.ValidString)。このチェックが成功すれば、ASN.1のエンコーディングタグはtagUTF8Stringに動的に変更され、文字列はUTF8Stringとしてエンコードされます。これにより、非ASCII文字を含む文字列もエラーなく処理できるようになります。

  4. 互換性と堅牢性: このアプローチは、既存のPrintableStringに準拠したデータはそのままPrintableStringとして処理しつつ、より広範な文字セットを必要とする新しいデータに対しては自動的にUTF8Stringに切り替えることで、後方互換性を保ちながら機能性を向上させています。また、不正なUTF-8文字列を検出してエラーを返すことで、データの整合性も確保しています。

この変更は、Goのencoding/asn1パッケージが、より現代的なアプリケーションの要件(特に国際化されたデータの処理)に対応するための重要なステップと言えます。

関連リンク

参考にした情報源リンク