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

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

このコミットは、Go言語のencoding/asn1パッケージにおける時間型(time.Time)のエンコーディングに関する問題を修正するものです。具体的には、UTCTimeの表現範囲外の時刻を適切に処理するために、GeneralizedTimeの使用を導入しています。また、関連してcrypto/tlsパッケージ内の証明書生成ロジックも更新されています。

変更されたファイルは以下の通りです。

  • src/pkg/crypto/tls/generate_cert.go: TLS証明書生成に関するコード。
  • src/pkg/encoding/asn1/asn1.go: ASN.1の基本的な型定義とパースロジック。
  • src/pkg/encoding/asn1/marshal.go: Goの型をASN.1形式にマーシャリング(エンコード)するロジック。
  • src/pkg/encoding/asn1/marshal_test.go: encoding/asn1パッケージのマーシャリング機能のテストコード。

コミット

  • コミットハッシュ: 050b60a36975a805921de71d6bfda310cd50e7ca
  • 作者: Adam Langley agl@golang.org
  • 日付: Fri Mar 21 11:14:38 2014 -0400

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

https://github.com/golang/go/commit/050b60a36975a805921de71d6bfda310cd50e7ca

元コミット内容

encoding/asn1: use GeneralizedTime for times outside the range of UTCTime.

Fixes issue #6976.

LGTM=r
R=golang-codereviews, r
CC=golang-codereviews
https://golang.org/cl/72080044

変更の背景

この変更は、Go言語のencoding/asn1パッケージが、ASN.1のUTCTime型で表現できる範囲(通常、1950年から2049年まで)を超える日付を適切にマーシャリングできないという問題(Issue #6976)を解決するために導入されました。

UTCTimeは、年を2桁で表現するため、2050年以降の日付を正確に表現することができません。これは、いわゆる「Y2K38問題」に似た「Y2K50問題」として知られる潜在的な問題を引き起こします。具体的には、2050年以降の日付をUTCTimeとしてエンコードしようとすると、誤った年が表現されたり、エンコードに失敗したりする可能性がありました。

このコミット以前は、crypto/tls/generate_cert.goのようなコードでは、証明書の有効期限が2049年12月31日を超える場合に、強制的にその日付に制限するような回避策が取られていました。しかし、これは将来の日付を扱うアプリケーションにとって不便であり、正しい解決策ではありませんでした。

この問題に対処するため、ASN.1標準で定義されているもう一つの時間型であるGeneralizedTimeを使用することが決定されました。GeneralizedTimeは年を4桁で表現するため、より広い範囲の日付を扱うことができます。このコミットの目的は、UTCTimeの範囲外の時刻に対して自動的にGeneralizedTimeを使用するようにencoding/asn1パッケージを修正することです。

前提知識の解説

ASN.1 (Abstract Syntax Notation One)

ASN.1は、データ構造を記述するための標準的な記法であり、異なるシステム間でデータを交換する際に、そのデータの表現方法を明確に定義するために使用されます。通信プロトコル、特にX.509証明書、LDAP、SNMPなどで広く利用されています。ASN.1自体はデータの抽象的な構文を定義するものであり、具体的なバイト列へのエンコーディング方法はBER (Basic Encoding Rules) やDER (Distinguished Encoding Rules) などのエンコーディングルールによって定められます。

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

  • BER (Basic Encoding Rules): ASN.1で定義されたデータ構造をバイト列にエンコードするための基本的なルールセットです。BERは柔軟性が高く、同じデータでも複数のエンコーディング方法が存在する場合があります。
  • DER (Distinguished Encoding Rules): BERのサブセットであり、特定のデータ構造に対して常に一意のバイト列エンコーディングを保証するルールセットです。セキュリティ関連のアプリケーション(例: X.509証明書)では、データの改ざんを防ぎ、一貫性を保つためにDERが厳密に要求されることがよくあります。

ASN.1における時間型: UTCTime と GeneralizedTime

ASN.1には、日付と時刻を表現するためのいくつかの組み込み型があります。このコミットに関連するのはUTCTimeGeneralizedTimeです。

  • UTCTime:

    • 形式: YYMMDDhhmmssZ または YYMMDDhhmmss+hhmm / YYMMDDhhmmss-hhmm
    • 年を2桁で表現します(例: 99は1999年、00は2000年)。
    • この2桁の年の解釈には、通常、1950年から2049年までの範囲が使用されます。つまり、50から99は1950年から1999年、00から49は2000年から2049年と解釈されます。
    • このため、2049年12月31日23時59分59秒以降の時刻を正確に表現することができません。
    • 末尾のZはUTC(協定世界時)であることを示します。
  • GeneralizedTime:

    • 形式: YYYYMMDDhhmmssZ または YYYYMMDDhhmmss.fffZ など、より柔軟な形式。
    • 年を4桁で表現します(例: 1999, 2050)。
    • これにより、UTCTimeの制限を超える広い範囲の日付を表現できます。
    • 秒の小数部(ミリ秒、マイクロ秒など)も表現可能です。
    • 末尾のZはUTCであることを示します。

Go言語の time.Time

Go言語の標準ライブラリには、日付と時刻を扱うためのtime.Time型が提供されています。この型は、ナノ秒の精度で時刻を表現でき、タイムゾーン情報も保持できます。encoding/asn1パッケージは、このtime.Time型とASN.1の時間型との間のマーシャリング(Goのtime.TimeからASN.1バイト列へ)およびアンマーシャリング(ASN.1バイト列からGoのtime.Timeへ)を処理します。

encoding/asn1 パッケージ

Go言語のencoding/asn1パッケージは、ASN.1構造体のエンコードとデコードをサポートします。Goの構造体フィールドにタグを付けることで、ASN.1のタグ、クラス、型、オプションなどを指定できます。このパッケージは、X.509証明書やその他のASN.1ベースのプロトコルを扱う際に不可欠です。

技術的詳細

このコミットの主要な目的は、Goのtime.TimeオブジェクトをASN.1形式にマーシャリングする際に、その時刻がUTCTimeの表現範囲(1950年〜2049年)外である場合に、自動的にGeneralizedTimeを使用するようにすることです。

具体的な変更点は以下の通りです。

  1. UTCTimeGeneralizedTimeの切り替えロジックの導入:

    • marshal.gooutsideUTCRange(t time.Time) bool関数が追加されました。この関数は、与えられたtime.Timeオブジェクトの年が1950年未満または2050年以上である場合にtrueを返します。
    • marshalBody関数内でtime.Time型を処理する際に、このoutsideUTCRange関数を呼び出し、結果に基づいてmarshalUTCTimeまたは新しく追加されたmarshalGeneralizedTimeを呼び出すように変更されました。
    • 同様に、marshalField関数でも、タグがtagUTCTimeである場合にoutsideUTCRangeをチェックし、必要に応じてタグをtagGeneralizedTimeに切り替えるロジックが追加されました。
  2. GeneralizedTimeのマーシャリング実装:

    • marshal.gomarshalGeneralizedTime(out *forkableWriter, t time.Time) (err error)関数が追加されました。この関数は、time.TimeオブジェクトをYYYYMMDDhhmmssZ形式のGeneralizedTimeとしてエンコードします。
    • 年の部分を4桁で出力するために、marshalFourDigitsヘルパー関数が新しく導入されました。
    • marshalUTCTimemarshalGeneralizedTimeで共通する日付・時刻部分のマーシャリングロジックをmarshalTimeCommon関数に抽出し、コードの重複を排除しました。
  3. UTCTimeGeneralizedTimeのパースロジックの統一:

    • asn1.goparseSequenceOf関数内で、tagGeneralizedTimetagUTCTimeの両方が同じように扱われるように変更されました。これにより、どちらの形式でエンコードされた時間も、Goのtime.Timeオブジェクトとして適切にデコードできるようになります。
  4. crypto/tlsの証明書生成ロジックの簡素化:

    • src/pkg/crypto/tls/generate_cert.goから、証明書のnotAfter(有効期限終了日時)が2049年12月31日を超える場合に、強制的にその日付に制限していたコードが削除されました。これは、encoding/asn1パッケージがGeneralizedTimeを適切に扱えるようになったため、もはや不要になったためです。
  5. テストケースの追加:

    • marshal_test.gofarFuture()ヘルパー関数が追加され、2100年の日付を生成します。
    • このfarFuture()を使用して、GeneralizedTimeとして正しくマーシャリングされることを確認する新しいテストケースが追加されました。

これらの変更により、Goのencoding/asn1パッケージは、UTCTimeの範囲外の時刻を透過的にGeneralizedTimeとして扱い、より堅牢な日付処理を提供できるようになりました。

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

src/pkg/crypto/tls/generate_cert.go

--- a/src/pkg/crypto/tls/generate_cert.go
+++ b/src/pkg/crypto/tls/generate_cert.go
@@ -58,12 +58,6 @@ func main() {
 
 	notAfter := notBefore.Add(*validFor)
 
-	// end of ASN.1 time
-	endOfTime := time.Date(2049, 12, 31, 23, 59, 59, 0, time.UTC)
-	if notAfter.After(endOfTime) {
-		notAfter = endOfTime
-	}
-
 	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
 	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
 	if err != nil {

src/pkg/encoding/asn1/asn1.go

--- a/src/pkg/encoding/asn1/asn1.go
+++ b/src/pkg/encoding/asn1/asn1.go
@@ -465,11 +465,15 @@ func parseSequenceOf(bytes []byte, sliceType reflect.Type, elemType reflect.Type
 		if err != nil {
 			return
 		}
-		// We pretend that various other string types are PRINTABLE STRINGs
-		// so that a sequence of them can be parsed into a []string.
 		switch t.tag {
 		case tagIA5String, tagGeneralString, tagT61String, tagUTF8String:
+			// We pretend that various other string types are
+			// PRINTABLE STRINGs so that a sequence of them can be
+			// parsed into a []string.
 			t.tag = tagPrintableString
+		case tagGeneralizedTime, tagUTCTime:
+			// Likewise, both time types are treated the same.
+			t.tag = tagUTCTime
 		}
 
 		if t.class != classUniversal || t.isCompound != compoundType || t.tag != expectedTag {

src/pkg/encoding/asn1/marshal.go

--- a/src/pkg/encoding/asn1/marshal.go
+++ b/src/pkg/encoding/asn1/marshal.go
@@ -295,8 +295,23 @@ func marshalTwoDigits(out *forkableWriter, v int) (err error) {
 	return out.WriteByte(byte('0' + v%10))
 }
 
+func marshalFourDigits(out *forkableWriter, v int) (err error) {
+	var bytes [4]byte
+	for i := range bytes {
+		bytes[3-i] = '0' + byte(v%10)
+		v /= 10
+	}
+	_, err = out.Write(bytes[:])
+	return
+}
+
+func outsideUTCRange(t time.Time) bool {
+	year := t.Year()
+	return year < 1950 || year >= 2050
+}
+
 func marshalUTCTime(out *forkableWriter, t time.Time) (err error) {
-	year, month, day := t.Date()
+	year := t.Year()
 
 	switch {
 	case 1950 <= year && year < 2000:
@@ -310,6 +325,24 @@ func marshalUTCTime(out *forkableWriter, t time.Time) (err error) {
 		return
 	}
 
+	return marshalTimeCommon(out, t)
+}
+
+func marshalGeneralizedTime(out *forkableWriter, t time.Time) (err error) {
+	year := t.Year()
+	if year < 0 || year > 9999 {
+		return StructuralError{"cannot represent time as GeneralizedTime"}
+	}
+	if err = marshalFourDigits(out, year); err != nil {
+		return
+	}
+
+	return marshalTimeCommon(out, t)
+}
+
+func marshalTimeCommon(out *forkableWriter, t time.Time) (err error) {
+	_, month, day := t.Date()
+
 	err = marshalTwoDigits(out, int(month))
 	if err != nil {
 		return
@@ -378,7 +411,12 @@ func stripTagAndLength(in []byte) []byte {
 func marshalBody(out *forkableWriter, value reflect.Value, params fieldParameters) (err error) {
 	switch value.Type() {
 	case timeType:
-		return marshalUTCTime(out, value.Interface().(time.Time))
+		t := value.Interface().(time.Time)
+		if outsideUTCRange(t) {
+			return marshalGeneralizedTime(t)
+		} else {
+			return marshalUTCTime(t)
+		}
 	case bitStringType:
 		return marshalBitString(out, value.Interface().(BitString))
 	case objectIdentifierType:
@@ -504,7 +542,8 @@ func marshalField(out *forkableWriter, v reflect.Value, params fieldParameters)
 		return StructuralError{"explicit string type given to non-string member"}
 	}
 
-	if tag == tagPrintableString {
+	switch tag {
+	case 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
@@ -521,6 +560,10 @@ func marshalField(out *forkableWriter, v reflect.Value, params fieldParameters)
 		} else {
 			tag = params.stringType
 		}
+	case tagUTCTime:
+		if outsideUTCRange(v.Interface().(time.Time)) {
+			tag = tagGeneralizedTime
+		}
 	}
 
 	if params.set {

src/pkg/encoding/asn1/marshal_test.go

--- a/src/pkg/encoding/asn1/marshal_test.go
+++ b/src/pkg/encoding/asn1/marshal_test.go
@@ -67,6 +67,14 @@ type marshalTest struct {
 	out string // hex encoded
 }
 
+func farFuture() time.Time {
+	t, err := time.Parse(time.RFC3339, "2100-04-05T12:01:01Z")
+	if err != nil {
+		panic(err)
+	}
+	return t
+}
+
 var marshalTests = []marshalTest{
 	{10, "02010a"},
 	{127, "02017f"},
@@ -83,6 +91,7 @@ var marshalTests = []marshalTest{
 	{time.Unix(0, 0).UTC(), "170d3730303130313030303030305a"},
 	{time.Unix(1258325776, 0).UTC(), "170d3039313131353232353631365a"},
 	{time.Unix(1258325776, 0).In(PST), "17113039313131353134353631362d30383030"},
+	{farFuture(), "180f32313030303430353132303130315a"},
 	{BitString{[]byte{0x80}, 1}, "03020780"},
 	{BitString{[]byte{0x81, 0xf0}, 12}, "03030481f0"},
 	{ObjectIdentifier([]int{1, 2, 3, 4}), "06032a0304"},

コアとなるコードの解説

src/pkg/crypto/tls/generate_cert.go

このファイルでは、TLS証明書を生成する際のnotAfter(有効期限終了日時)の制限が削除されました。以前は、UTCTimeの2049年までの制限に対応するため、notAfterが2049年12月31日を超える場合に強制的にその日付に設定されていました。encoding/asn1パッケージがGeneralizedTimeを適切に扱えるようになったため、この回避策は不要となり、コードが簡素化されました。これにより、GoのTLS証明書は2050年以降の有効期限を持つことができるようになります。

src/pkg/encoding/asn1/asn1.go

parseSequenceOf関数内の変更は、ASN.1シーケンスをパースする際に、GeneralizedTimeUTCTimeの両方を同じtagUTCTimeとして扱うようにしています。これは、デコード時に両方の時間型を透過的に処理し、Goのtime.Time型に変換できるようにするためです。これにより、エンコード時にどちらの時間型が使用されたかに関わらず、一貫したデコード動作が保証されます。

src/pkg/encoding/asn1/marshal.go

このファイルは、time.TimeオブジェクトをASN.1形式にマーシャリングするロジックの核心部分です。

  • marshalFourDigits(out *forkableWriter, v int) (err error):

    • この新しいヘルパー関数は、整数vを4桁のASCII数字としてoutライターに書き込みます。これはGeneralizedTimeの年(YYYY)部分をエンコードするために使用されます。
  • outsideUTCRange(t time.Time) bool:

    • この新しい関数は、与えられたtime.Timeオブジェクトtの年が1950年未満または2050年以上である場合にtrueを返します。この関数が、UTCTimeの表現範囲外の時刻を検出するための主要なロジックを提供します。
  • marshalUTCTime(out *forkableWriter, t time.Time) (err error):

    • この関数は、UTCTime形式で時刻をマーシャリングします。変更点として、年の部分の処理が簡素化され、共通のマーシャリングロジックがmarshalTimeCommonに抽出されました。
  • marshalGeneralizedTime(out *forkableWriter, t time.Time) (err error):

    • この新しい関数は、GeneralizedTime形式で時刻をマーシャリングします。年の部分をmarshalFourDigitsを使って4桁で出力し、残りの日付・時刻部分はmarshalTimeCommonを呼び出して処理します。これにより、2050年以降の時刻も正確にエンコードできるようになります。
  • marshalTimeCommon(out *forkableWriter, t time.Time) (err error):

    • marshalUTCTimemarshalGeneralizedTimeの両方で共通して使用される、月、日、時、分、秒、タイムゾーン(UTCを示す'Z')のマーシャリングロジックがこの関数にまとめられました。これにより、コードの重複が解消され、保守性が向上しています。
  • marshalBody関数内の変更:

    • timeTypetime.Time型)を処理するcaseブロックが変更されました。outsideUTCRange(t)をチェックし、結果に応じてmarshalGeneralizedTimeまたはmarshalUTCTimeを呼び出すようになりました。これにより、適切な時間型が自動的に選択されます。
  • marshalField関数内の変更:

    • tagUTCTimeを処理するcaseブロックが追加されました。ここでもoutsideUTCRange(v.Interface().(time.Time))をチェックし、必要に応じてタグをtagGeneralizedTimeに切り替えることで、最終的なASN.1エンコーディングのタグが正しく設定されるようにします。

src/pkg/encoding/asn1/marshal_test.go

  • farFuture() time.Time:

    • このヘルパー関数は、2100年4月5日12時01分01秒UTCという、UTCTimeの範囲を大きく超える未来の時刻を生成します。
  • marshalTestsへの追加:

    • {farFuture(), "180f32313030303430353132303130315a"}という新しいテストケースが追加されました。これは、farFuture()で生成された時刻が、GeneralizedTimeとして正しくエンコードされること(タグ18、長さ0f、値21000405120101Z)を検証します。このテストケースの追加により、GeneralizedTimeのマーシャリング機能が期待通りに動作することが保証されます。

関連リンク

参考にした情報源リンク