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

[インデックス 16536] ファイルの概要

このコミットは、Go言語のencoding/asn1パッケージにおけるObject Identifier (OID) 型のエンコーディングおよびデコーディングの改善に関するものです。具体的には、OIDの最初の2つの識別子(ノード)のエンコーディング方法が、より標準的なASN.1の規則に準拠するように修正されています。

コミット

commit 02a891b30ba44fd2185ad6292ff6d862b3946084
Author: Gerasimos Dimitriadis <gedimitr@gmail.com>
Date:   Mon Jun 10 18:14:47 2013 -0400

    asn1: Improved encoding/decoding for OID types
    
    The first identifier in an Object Identifer must be between 0 and 2
    inclusive. The range of values that the second one can take depends
    on the value of the first one.
    The two first identifiers are not necessarily encoded in a single octet,
    but in a varint.
    
    R=golang-dev, agl
    CC=golang-dev
    https://golang.org/cl/10140046

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/02a891b30ba44fd2185ad6292ff6d862b3946084

元コミット内容

このコミットの元のメッセージは以下の通りです。

asn1: Improved encoding/decoding for OID types

The first identifier in an Object Identifer must be between 0 and 2
inclusive. The range of values that the second one can take depends
on the value of the first one.
The two first identifiers are not necessarily encoded in a single octet,
but in a varint.

R=golang-dev, agl
CC=golang-dev
https://golang.org/cl/10140046

変更の背景

この変更の背景には、ASN.1 (Abstract Syntax Notation One) のObject Identifier (OID) のエンコーディング規則に関する正確な実装の必要性があります。

ASN.1は、データ構造を記述するための標準であり、通信プロトコルやデータストレージにおいて広く利用されています。OIDは、情報オブジェクトを一意に識別するための階層的な命名メカニズムです。例えば、X.509証明書、SNMP (Simple Network Management Protocol)、LDAP (Lightweight Directory Access Protocol) など、多くの標準でOIDが使用されています。

OIDは、一連の非負の整数で構成され、ドットで区切られて表現されます(例: 1.2.840.113549.1.1.5)。これらの整数は、ASN.1のBER (Basic Encoding Rules) やDER (Distinguished Encoding Rules) などのエンコーディング規則に従ってバイト列に変換されます。

特に、OIDの最初の2つのサブ識別子(ノード)のエンコーディングには特殊な規則があります。これは、ISO/IEC 8825-1 (ITU-T X.690) で定義されています。

  1. 最初のサブ識別子 (x) は0, 1, 2のいずれかである。
  2. 2番目のサブ識別子 (y) は、xの値によって取りうる範囲が異なる。
    • x = 0 または x = 1 の場合、y は 0 から 39 の範囲でなければならない。
    • x = 2 の場合、y には制限がない。
  3. 最初の2つのサブ識別子 (x, y) は、40 * x + y という単一の値としてエンコードされる。
  4. この 40 * x + y の値は、可変長整数 (varint) としてエンコードされる。

以前の実装では、この最初の2つのサブ識別子のエンコーディングが、単一のオクテット(バイト)として扱われていたか、または可変長整数としてのエンコーディングが不完全であった可能性があります。このコミットは、この点を修正し、より正確なASN.1 OIDエンコーディング/デコーディングを保証することを目的としています。これにより、Goのencoding/asn1パッケージが生成または解析するOIDが、他のASN.1実装と相互運用可能になります。

前提知識の解説

ASN.1 (Abstract Syntax Notation One)

ASN.1は、データ構造をプラットフォームやプログラミング言語に依存しない形で記述するための国際標準です。主に通信プロトコルやデータ交換フォーマットの定義に用いられます。ASN.1で定義されたデータ構造は、BER (Basic Encoding Rules) やDER (Distinguished Encoding Rules) などのエンコーディング規則に従ってバイト列に変換されます。

Object Identifier (OID)

OIDは、ASN.1で定義されるデータ型の一つで、情報オブジェクトを一意に識別するための階層的な命名システムです。OIDは、ドットで区切られた一連の非負の整数(サブ識別子またはノード)で構成されます。例えば、1.2.840.113549.1.1.5 は、PKCS#1のSHA-1 with RSA暗号化アルゴリズムを識別するOIDです。

OIDの階層は、国際機関によって管理されており、ルートノードから始まり、各ノードが特定の組織や目的を表します。

  • 0: itu-t (ITU-T勧告)
  • 1: iso (ISO標準)
  • 2: joint-iso-itu-t (ISOとITU-Tの共同標準)

BER (Basic Encoding Rules) と DER (Distinguished Encoding Rules)

BERは、ASN.1データをバイト列にエンコードするための基本的な規則です。BERは柔軟性が高く、同じASN.1データでも複数の異なるバイト列表現が存在する可能性があります。

DERは、BERのサブセットであり、同じASN.1データに対して常に一意のバイト列表現を生成するように制限を設けています。これにより、デジタル署名やハッシュ計算など、データの同一性が保証される必要がある場合にDERが好んで使用されます。Goのencoding/asn1パッケージは、通常DERに準拠したエンコーディングを行います。

可変長整数 (Varint) エンコーディング

ASN.1のOIDのサブ識別子は、可変長整数としてエンコードされます。これは、数値の大きさに応じて必要なバイト数が変わるエンコーディング方式です。具体的には、各バイトの最上位ビット (MSB) が「もっと続く」ことを示すフラグとして使用されます。

  • 各バイトのビット7 (MSB): 1 の場合、次のバイトも同じ数値の一部であることを示します。0 の場合、そのバイトが数値の最後のバイトであることを示します。
  • 残りの7ビット: 数値のデータ部分を表します。

例えば、数値 100 をエンコードする場合:

  • 100 は1バイトで表現できます。01100100 (バイナリ) となり、MSBは 0 なので、これだけで完結します。

数値 200 をエンコードする場合:

  • 200 は1バイトでは表現できません(最大127)。
  • 200 を7ビット単位で分割します。
    • 200 (10進数) = 11001000 (2進数)
    • 下位7ビット: 1001000 (10進数で72)
    • 上位ビット: 1 (10進数で1)
  • エンコードされたバイト列は、上位の7ビットから順に並べられます。
    • 最初のバイト: 1 (MSB) + 0000001 (上位7ビット) = 10000001 (0x81)
    • 次のバイト: 0 (MSB) + 1001000 (下位7ビット) = 01001000 (0x48)
  • したがって、2000x81 0x48 とエンコードされます。

OIDの最初の2つのサブ識別子の特殊なエンコーディング

前述の通り、OIDの最初の2つのサブ識別子 xy は、40 * x + y という単一の値としてエンコードされます。この値が可変長整数としてエンコードされます。

  • x = 0, y = 0 の場合: 40 * 0 + 0 = 0
  • x = 1, y = 2 の場合: 40 * 1 + 2 = 42
  • x = 2, y = 5 の場合: 40 * 2 + 5 = 85

この規則により、OIDのエンコーディングは効率的かつ標準に準拠したものとなります。

技術的詳細

このコミットは、src/pkg/encoding/asn1/asn1.gosrc/pkg/encoding/asn1/marshal.go の2つの主要なファイルに変更を加えています。

src/pkg/encoding/asn1/asn1.go の変更点 (parseObjectIdentifier 関数)

このファイルでは、バイト列からObject Identifierをデコードする parseObjectIdentifier 関数が修正されています。

変更前:

func parseObjectIdentifier(bytes []byte) (s []int, err error) {
	// encoded differently) and then every varint is a single byte long.
	s = make([]int, len(bytes)+1)

	// The first byte is 40*value1 + value2:
	s[0] = int(bytes[0]) / 40
	s[1] = int(bytes[0]) % 40
	i := 2
	for offset := 1; offset < len(bytes); i++ {
		var v int
		v, offset, err = parseBase128Int(bytes, offset)
		if err != nil {
			return
		}
		s[i] = v
	}
	return
}

変更前は、最初のバイトが直接 40*value1 + value2 の値であると仮定し、それを40で割って value1 を、40で割った余りで value2 を計算していました。これは、最初の2つのサブ識別子が常に単一のオクテットでエンコードされるという誤った仮定に基づいています。

変更後:

func parseObjectIdentifier(bytes []byte) (s []int, err error) {
	// encoded differently) and then every varint is a single byte long.
	s = make([]int, len(bytes)+1)

	// The first varint is 40*value1 + value2:
	// According to this packing, value1 can take the values 0, 1 and 2 only.
	// When value1 = 0 or value1 = 1, then value2 is <= 39. When value1 = 2,
	// then there are no restrictions on value2.
	v, offset, err := parseBase128Int(bytes, 0)
	if err != nil {
		return
	}
	if v < 80 {
		s[0] = v / 40
		s[1] = v % 40
	} else {
		s[0] = 2
		s[1] = v - 80
	}

	i := 2
	for ; offset < len(bytes); i++ {
		v, offset, err = parseBase128Int(bytes, offset)
		if err != nil {
			return
		}
		s[i] = v
	}
	return
}

変更後では、まず parseBase128Int 関数を使用して、バイト列の先頭から最初の可変長整数 v を読み取ります。この v40*value1 + value2 の値です。

  • v < 80 の場合(つまり value1 が0または1の場合)、v / 40value1 を、v % 40value2 を計算します。これは、value2 が39以下であるという制約があるためです。
  • v >= 80 の場合(つまり value1 が2の場合)、value1 は常に 2 であり、value2v - 80 として計算されます。これは、40 * 2 + y = 80 + y という関係に基づいています。value1 = 2 の場合、value2 には制限がないため、この計算が適用されます。

この修正により、最初の2つのサブ識別子が単一のオクテットに収まらない場合でも、正しくデコードできるようになりました。

src/pkg/encoding/asn1/marshal.go の変更点 (marshalObjectIdentifier 関数)

このファイルでは、Object Identifierをバイト列にエンコードする marshalObjectIdentifier 関数が修正されています。

変更前:

func marshalObjectIdentifier(out *forkableWriter, oid []int) (err error) {
	if len(oid) < 2 || oid[0] > 6 || oid[1] >= 40 {
		return StructuralError{"invalid object identifier"}
	}

	err = out.WriteByte(byte(oid[0]*40 + oid[1]))
	if err != nil {
		return
	}
	// ... (後続のサブ識別子のエンコード)
}

変更前は、最初の2つのサブ識別子のバリデーションが oid[0] > 6 となっており、これはASN.1の規則 (oid[0] は0, 1, 2のみ) とは異なっていました。また、out.WriteByte(byte(oid[0]*40 + oid[1])) の部分で、40*value1 + value2 の値を単一のバイトとして書き込んでいました。これは、値が127を超える場合に問題となります。

変更後:

func marshalObjectIdentifier(out *forkableWriter, oid []int) (err error) {
	if len(oid) < 2 || oid[0] > 2 || (oid[0] < 2 && oid[1] >= 40) {
		return StructuralError{"invalid object identifier"}
	}

	err = marshalBase128Int(out, int64(oid[0]*40+oid[1]))
	if err != nil {
		return
	}
	// ... (後続のサブ識別子のエンコード)
}

変更後では、バリデーションが oid[0] > 2 || (oid[0] < 2 && oid[1] >= 40) となり、ASN.1の規則に厳密に準拠するようになりました。

  • oid[0] が2を超える場合はエラー。
  • oid[0] が0または1の場合に oid[1] が40以上の場合はエラー。

そして、out.WriteByte(...) の代わりに marshalBase128Int(out, int64(oid[0]*40+oid[1])) が使用されています。これにより、40*value1 + value2 の値が可変長整数として正しくエンコードされるようになりました。値が127を超える場合でも、複数のバイトを使用して表現されます。

テストファイルの変更

  • src/pkg/crypto/x509/x509_test.go: testUnknownExtKeyUsage のテストデータが []int{2, 59, 1} に変更されています。これは、2, 59 の組み合わせが 40*2 + 59 = 80 + 59 = 139 となり、単一バイトでは表現できない値(127超)となるため、可変長整数エンコーディングのテストケースとして適切です。
  • src/pkg/encoding/asn1/asn1_test.go: objectIdentifierTestData[]byte{0x81, 0x34, 0x03}, true, []int{2, 100, 3} という新しいテストケースが追加されています。
    • 0x81 0x34 は、可変長整数として (0x81 & 0x7f) << 7 | (0x34 & 0x7f) = 1 << 7 | 52 = 128 + 52 = 180 を表します。
    • 18040 * x + y の値です。x = 2 の場合、y = 180 - 80 = 100 となります。
    • したがって、{2, 100, 3} というOIDが正しくデコードされることをテストしています。
  • src/pkg/encoding/asn1/marshal_test.go: marshalTestsObjectIdentifier([]int{2, 100, 3}), "0603813403" という新しいテストケースが追加されています。これは、{2, 100, 3} というOIDが 0x06 0x03 0x81 0x34 0x03 とエンコードされることをテストしています。0x06 はOIDのタグ、0x03 は長さ、0x81 0x3440*2 + 100 = 180 の可変長エンコーディング、0x03 は3番目のサブ識別子です。

これらのテストケースの追加は、変更が正しく機能し、ASN.1のOIDエンコーディング規則に準拠していることを確認するために重要です。

コアとなるコードの変更箇所

src/pkg/encoding/asn1/asn1.go (デコーディング)

--- a/src/pkg/encoding/asn1/asn1.go
+++ b/src/pkg/encoding/asn1/asn1.go
@@ -210,12 +210,24 @@ func parseObjectIdentifier(bytes []byte) (s []int, err error) {
 	// encoded differently) and then every varint is a single byte long.
 	s = make([]int, len(bytes)+1)
 
-	// The first byte is 40*value1 + value2:
-	s[0] = int(bytes[0]) / 40
-	s[1] = int(bytes[0]) % 40
+	// The first varint is 40*value1 + value2:
+	// According to this packing, value1 can take the values 0, 1 and 2 only.
+	// When value1 = 0 or value1 = 1, then value2 is <= 39. When value1 = 2,
+	// then there are no restrictions on value2.
+	v, offset, err := parseBase128Int(bytes, 0)
+	if err != nil {
+		return
+	}
+	if v < 80 {
+		s[0] = v / 40
+		s[1] = v % 40
+	} else {
+		s[0] = 2
+		s[1] = v - 80
+	}
+
 	i := 2
-	for offset := 1; offset < len(bytes); i++ {
-		var v int
+	for ; offset < len(bytes); i++ {
 		v, offset, err = parseBase128Int(bytes, offset)
 		if err != nil {
 			return

src/pkg/encoding/asn1/marshal.go (エンコーディング)

--- a/src/pkg/encoding/asn1/marshal.go
+++ b/src/pkg/encoding/asn1/marshal.go
@@ -240,11 +240,11 @@ func marshalBitString(out *forkableWriter, b BitString) (err error) {
 }
 
 func marshalObjectIdentifier(out *forkableWriter, oid []int) (err error) {
-	if len(oid) < 2 || oid[0] > 6 || oid[1] >= 40 {
+	if len(oid) < 2 || oid[0] > 2 || (oid[0] < 2 && oid[1] >= 40) {
 		return StructuralError{"invalid object identifier"}
 	}
 
-	err = out.WriteByte(byte(oid[0]*40 + oid[1]))
+	err = marshalBase128Int(out, int64(oid[0]*40+oid[1]))
 	if err != nil {
 		return
 	}

コアとなるコードの解説

parseObjectIdentifier の変更点

  • v, offset, err := parseBase128Int(bytes, 0): 以前は最初のバイトを直接読み取っていましたが、この変更により、parseBase128Int 関数を使って、最初の可変長整数(40*value1 + value2 の値)を読み取るようになりました。これにより、最初の2つのサブ識別子が複数バイトでエンコードされている場合でも正しく処理できます。
  • if v < 80 { ... } else { ... }: 読み取った v の値に基づいて、最初のサブ識別子 s[0] と2番目のサブ識別子 s[1] を計算するロジックが追加されました。
    • v < 80 の場合: s[0] = v / 40s[1] = v % 40。これは、value1 が0または1の場合に適用されます。
    • v >= 80 の場合: s[0] = 2s[1] = v - 80。これは、value1 が2の場合に適用されます。ASN.1の規則により、value1 が2の場合、40*value1 + value280 + value2 となるためです。
  • for ; offset < len(bytes); i++: ループの開始条件が offset := 1 から offset < len(bytes) に変更されました。これは、最初の可変長整数が複数バイトを消費する可能性があるため、offset の初期値が parseBase128Int から返される値に依存するように修正されたためです。

marshalObjectIdentifier の変更点

  • if len(oid) < 2 || oid[0] > 2 || (oid[0] < 2 && oid[1] >= 40): OIDのバリデーションロジックが強化されました。
    • oid[0] > 2: 最初のサブ識別子が0, 1, 2の範囲外である場合はエラー。
    • (oid[0] < 2 && oid[1] >= 40): 最初のサブ識別子が0または1の場合に、2番目のサブ識別子が40以上である場合はエラー。これはASN.1の規則に厳密に準拠しています。
  • err = marshalBase128Int(out, int64(oid[0]*40+oid[1])): 以前は out.WriteByte を使用して単一バイトで書き込んでいましたが、この変更により marshalBase128Int 関数を使用して、40*value1 + value2 の値を可変長整数としてエンコードするようになりました。これにより、値が127を超える場合でも正しくエンコードされ、ASN.1の標準に準拠した出力が生成されます。

これらの変更により、Goのencoding/asn1パッケージは、Object Identifierのエンコーディングとデコーディングにおいて、ASN.1のBER/DER標準にさらに厳密に準拠するようになりました。これにより、他のシステムとの相互運用性が向上し、より堅牢なASN.1処理が可能になります。

関連リンク

参考にした情報源リンク