[インデックス 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) で定義されています。
- 最初のサブ識別子 (x) は0, 1, 2のいずれかである。
- 2番目のサブ識別子 (y) は、xの値によって取りうる範囲が異なる。
- x = 0 または x = 1 の場合、y は 0 から 39 の範囲でなければならない。
- x = 2 の場合、y には制限がない。
- 最初の2つのサブ識別子 (x, y) は、
40 * x + y
という単一の値としてエンコードされる。 - この
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)
- 最初のバイト:
- したがって、
200
は0x81 0x48
とエンコードされます。
OIDの最初の2つのサブ識別子の特殊なエンコーディング
前述の通り、OIDの最初の2つのサブ識別子 x
と y
は、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.go
と src/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
を読み取ります。この v
が 40*value1 + value2
の値です。
v < 80
の場合(つまりvalue1
が0または1の場合)、v / 40
でvalue1
を、v % 40
でvalue2
を計算します。これは、value2
が39以下であるという制約があるためです。v >= 80
の場合(つまりvalue1
が2の場合)、value1
は常に2
であり、value2
はv - 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
を表します。180
は40 * x + y
の値です。x = 2
の場合、y = 180 - 80 = 100
となります。- したがって、
{2, 100, 3}
というOIDが正しくデコードされることをテストしています。
src/pkg/encoding/asn1/marshal_test.go
:marshalTests
にObjectIdentifier([]int{2, 100, 3}), "0603813403"
という新しいテストケースが追加されています。これは、{2, 100, 3}
というOIDが0x06 0x03 0x81 0x34 0x03
とエンコードされることをテストしています。0x06
はOIDのタグ、0x03
は長さ、0x81 0x34
は40*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 / 40
、s[1] = v % 40
。これは、value1
が0または1の場合に適用されます。v >= 80
の場合:s[0] = 2
、s[1] = v - 80
。これは、value1
が2の場合に適用されます。ASN.1の規則により、value1
が2の場合、40*value1 + value2
は80 + 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処理が可能になります。
関連リンク
参考にした情報源リンク
- https://github.com/golang/go/commit/02a891b30ba44fd2185ad6292ff6d862b3946084
- https://golang.org/cl/10140046
- ASN.1に関する一般的な知識と、OIDのエンコーディング規則に関する情報源(ITU-T X.690など)
- Go言語の
encoding/asn1
パッケージのドキュメント - 可変長整数エンコーディングに関する一般的な情報