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

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

このコミットは、Go言語の標準ライブラリ encoding/asn1 パッケージ内の marshal.go および marshal_test.go ファイルに対する変更です。具体的には、ASN.1 の UTCTime 型のエンコーディングロジックが修正され、それに伴うテストケースが更新されています。

コミット

  • コミットハッシュ: 4d17fe3cd62788b9b15af471806063f8cc071c97
  • Author: Adam Langley agl@golang.org
  • Date: Mon Oct 29 11:16:05 2012 -0400

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

https://github.com/golang/go/commit/4d17fe3cd62788b9b15af471806063f8cc071c97

元コミット内容

encoding/asn1: don't convert UTCTime to UTC.

Previously we converted a time to UTC *and* serialized the timezone of
the original time. With this change, we serialize a UTCTime in the
original timezone.

R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/6817048

変更の背景

このコミットの背景には、Go言語の encoding/asn1 パッケージにおける UTCTime 型のエンコーディングに関する不正確な挙動がありました。以前の実装では、time.Time オブジェクトを UTCTime としてASN.1形式にシリアライズする際に、まず time.Time オブジェクトをUTC(協定世界時)に変換していました。しかし、その変換後のUTC時刻に対して、元の time.Time オブジェクトが持っていたタイムゾーン情報(オフセット)を付加してシリアライズしていました。

これは論理的な矛盾を生じさせます。例えば、日本時間(JST, UTC+9)の時刻を UTCTime としてエンコードしようとした場合、まず内部的にUTCに変換され、そのUTC時刻に対して「+0900」のようなJSTのオフセットが付加されてしまうことになります。結果として、エンコードされた時刻は、意図した時刻とは異なる時刻(UTC時刻にJSTオフセットが付いた、存在しない時刻)として解釈される可能性がありました。

このコミットは、この二重の変換(UTCへの変換と元のタイムゾーンのシリアライズ)を修正し、UTCTime をエンコードする際には、元の time.Time オブジェクトが保持しているタイムゾーン情報をそのまま利用してシリアライズするように変更することで、正確なASN.1 UTCTime の表現を実現することを目的としています。

前提知識の解説

ASN.1 (Abstract Syntax Notation One)

ASN.1は、データ構造を記述するための国際標準です。異なるシステム間でデータを交換する際に、データの形式を明確に定義するために使用されます。通信プロトコル(例: X.509証明書、SNMP、LDAP)や暗号化技術(例: PKCS#7、PKCS#10)などで広く利用されています。ASN.1は、データの抽象的な構文を定義し、そのデータをバイト列に変換するためのエンコーディングルール(例: DER, BER, PER)も提供します。

UTCTime

UTCTime は、ASN.1で定義されている時間型の一つです。主に1950年から2049年までの日付と時刻を表すために使用されます。UTCTime のフォーマットは通常 YYMMDDhhmmssZ または YYMMDDhhmmss+hhmm / YYMMDDhhmmss-hhmm の形式を取ります。

  • YY: 年の下2桁 (例: 2012年なら 12)
  • MM: 月 (01-12)
  • DD: 日 (01-31)
  • hh: 時 (00-23)
  • mm: 分 (00-59)
  • ss: 秒 (00-59)
  • Z: UTC(協定世界時)であることを示す。
  • +hhmm / -hhmm: UTCからのオフセットを示す。

例えば、121029111605Z は2012年10月29日11時16分05秒UTCを表し、121029111605-0400 は2012年10月29日11時16分05秒、UTCから4時間遅れたタイムゾーン(例: 東部夏時間EDT)を表します。

Go言語の time.Time とタイムゾーン

Go言語の time パッケージは、日付と時刻を扱うための強力な機能を提供します。time.Time 型は、特定の時点を表す構造体です。time.Time オブジェクトは、内部的にはUTCからの経過時間を保持していますが、表示や計算の際には、そのオブジェクトが関連付けられているタイムゾーン(Location)の情報を利用します。

  • t.UTC(): time.Time オブジェクト t をUTCに変換した新しい time.Time オブジェクトを返します。この操作は、時刻の絶対的な時点は変えずに、表示されるタイムゾーンをUTCに変更します。
  • t.Date(), t.Clock(): time.Time オブジェクトが現在設定されているタイムゾーンに基づいて、年、月、日、時、分、秒などの各要素を返します。
  • t.In(loc *time.Location): time.Time オブジェクトを、指定された time.Location に関連付けられた新しい time.Time オブジェクトを返します。これも時刻の絶対的な時点は変えずに、表示されるタイムゾーンを変更します。

このコミットの文脈では、time.Time オブジェクトが持つ「どのタイムゾーンでこの時刻が表現されているか」という情報が重要になります。

技術的詳細

このコミットの技術的な核心は、ASN.1 UTCTime のエンコーディングにおいて、Goの time.Time オブジェクトのタイムゾーン情報を正しく扱うことにあります。

以前の実装では、encoding/asn1 パッケージの marshalUTCTime 関数内で、引数として渡された time.Time オブジェクト t をまず t.UTC() を呼び出してUTCに変換していました。そして、このUTCに変換された時刻から年、月、日、時、分、秒の各要素を取り出していました。しかし、その後にタイムゾーンオフセットをシリアライズする際には、元の t が持っていたタイムゾーン情報(例えば、t.Location() から得られるオフセット)を使用していました。

この挙動は、以下のような問題を引き起こします。

  1. 時刻の不一致: t.UTC() で得られた時刻はUTCですが、それに元のタイムゾーンのオフセットを付加すると、結果としてエンコードされる時刻は、元のタイムゾーンでの時刻がUTCに変換されたものとは異なる、誤った時刻表現になってしまいます。例えば、日本時間2012年10月29日11時16分05秒(UTC+9)をエンコードしようとした場合、まずUTCに変換されて2012年10月29日02時16分05秒UTCとなります。しかし、これに「+0900」というオフセットが付加されると、ASN.1のデコーダは「2012年10月29日02時16分05秒、UTCから9時間進んだタイムゾーンの時刻」と解釈してしまい、これは実際には2012年10月29日11時16分05秒UTC+9とは異なる時刻(2012年10月29日02時16分05秒+0900 = 2012年10月29日11時16分05秒UTC)を指すことになります。つまり、元の時刻がUTC+9であったにもかかわらず、エンコードされた結果はUTC時刻として解釈されてしまうという矛盾が生じていました。

  2. 標準への不適合: ASN.1の UTCTime は、通常、指定されたタイムゾーンの時刻をそのまま表現するか、またはUTC時刻を明示的に Z で示すかのいずれかです。元のタイムゾーンの時刻をUTCに変換してから、元のタイムゾーンのオフセットを付加するという方法は、ASN.1の標準的な UTCTime の解釈とは異なります。

このコミットでは、この問題を解決するために、marshalUTCTime 関数から t.UTC() の呼び出しを削除しました。これにより、t.Date()t.Clock() から取得される年、月、日、時、分、秒の各要素は、time.Time オブジェクト t が元々持っていたタイムゾーン(Location)に基づいて計算されるようになります。そして、そのタイムゾーンのオフセットがそのままシリアライズされるため、ASN.1 UTCTime は元の time.Time オブジェクトが表現していた時刻とタイムゾーンを正確に反映するようになります。

テストケース marshal_test.go の変更もこの修正を反映しています。特に、time.Unix(1258325776, 0).In(PST) のテストケースでは、以前は 17113039311152256162d30383030 というエンコード結果を期待していましたが、修正後は 17113039311151456162d30383030 となっています。これは、PST(太平洋標準時、UTC-8)の時刻が正しくエンコードされるようになったことを示しています。具体的には、1258325776 はUnixエポックからの秒数で、これは2009年11月15日22時56分16秒UTCに相当します。PSTでは、これは2009年11月15日14時56分16秒PST(UTC-8)となります。修正前のエンコード結果は、UTC時刻(22時56分16秒)にPSTのオフセット(-0800)が付加されたような形になっていましたが、修正後はPSTの時刻(14時56分16秒)にPSTのオフセット(-0800)が正しく付加されるようになっています。

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

src/pkg/encoding/asn1/marshal.gomarshalUTCTime 関数が変更されています。

--- a/src/pkg/encoding/asn1/marshal.go
+++ b/src/pkg/encoding/asn1/marshal.go
@@ -296,8 +296,7 @@ func marshalTwoDigits(out *forkableWriter, v int) (err error) {
 }
 
 func marshalUTCTime(out *forkableWriter, t time.Time) (err error) {
-	utc := t.UTC()
-	year, month, day := utc.Date()
+	year, month, day := t.Date()
 
 	switch {
 	case 1950 <= year && year < 2000:
@@ -321,7 +320,7 @@ func marshalUTCTime(out *forkableWriter, t time.Time) (err error) {\n 		return
 	}\n 
-	hour, min, sec := utc.Clock()
+	hour, min, sec := t.Clock()
 
 	err = marshalTwoDigits(out, hour)
 	if err != nil {

また、src/pkg/encoding/asn1/marshal_test.go のテストケースも変更されています。

--- a/src/pkg/encoding/asn1/marshal_test.go
+++ b/src/pkg/encoding/asn1/marshal_test.go
@@ -82,7 +82,7 @@ var marshalTests = []marshalTest{\
 	{explicitTagTest{64}, "3005a503020140"},\
 	{time.Unix(0, 0).UTC(), "170d3730303130313030303030305a"},\
 	{time.Unix(1258325776, 0).UTC(), "170d3039313131353232353631365a"},\
-	{time.Unix(1258325776, 0).In(PST), "17113039313131353232353631362d30383030"},\
+	{time.Unix(1258325776, 0).In(PST), "17113039311151456162d30383030"},\
 	{BitString{[]byte{0x80}, 1}, "03020780"},\
 	{BitString{[]byte{0x81, 0xf0}, 12}, "03030481f0"},\
 	{ObjectIdentifier([]int{1, 2, 3, 4}), "06032a0304"},\

コアとなるコードの解説

marshal.go の変更は非常にシンプルですが、その影響は大きいです。

変更前:

func marshalUTCTime(out *forkableWriter, t time.Time) (err error) {
	utc := t.UTC() // ここで時刻をUTCに変換
	year, month, day := utc.Date() // UTC時刻から日付要素を取得
	// ...
	hour, min, sec := utc.Clock() // UTC時刻から時間要素を取得
	// ...
}

変更前は、t.UTC() を呼び出して time.Time オブジェクトを明示的にUTCに変換し、その utc 変数から年、月、日、時、分、秒の各要素を取得していました。しかし、ASN.1 UTCTime のエンコーディングでは、時刻の各要素とタイムゾーンオフセットが整合している必要があります。この場合、時刻要素はUTCで、タイムゾーンオフセットは元のタイムゾーンのもの、という不整合が生じていました。

変更後:

func marshalUTCTime(out *forkableWriter, t time.Time) (err error) {
	year, month, day := t.Date() // 元のtime.Timeオブジェクトから日付要素を取得
	// ...
	hour, min, sec := t.Clock() // 元のtime.Timeオブジェクトから時間要素を取得
	// ...
}

変更後では、t.UTC() の呼び出しが削除され、直接 t.Date()t.Clock() を呼び出して年、月、日、時、分、秒の各要素を取得しています。Goの time.Time オブジェクトの Date()Clock() メソッドは、その time.Time オブジェクトが現在関連付けられている time.Location に基づいて時刻の各要素を返します。これにより、marshalUTCTime 関数は、引数として渡された time.Time オブジェクト t が持つタイムゾーン情報(Location)をそのまま利用して時刻の各要素を抽出し、その後にそのタイムゾーンのオフセットをシリアライズするようになります。

この修正により、UTCTime のエンコーディングは、元の time.Time オブジェクトが表現していた時刻とタイムゾーンを正確に反映するようになり、ASN.1の標準的な解釈に合致するようになりました。

marshal_test.go の変更は、この新しい正しいエンコーディング結果を反映するためのテストデータの更新です。特に time.Unix(1258325776, 0).In(PST) のテストケースは、PSTタイムゾーンでの時刻が正しくエンコードされることを検証しています。

関連リンク

参考にした情報源リンク