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

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

このコミットは、Go言語の crypto/x509 パッケージにおける証明書のホスト名検証ロジックを修正し、ホスト名のマッチングを大文字・小文字を区別しない(ケースインセンシティブ)ように変更するものです。これにより、RFC 6125で推奨される挙動に準拠し、証明書検証の堅牢性が向上します。

コミット

commit 8efb304440474a4c168c30fc07f2787eaf16cfbf
Author: Adam Langley <agl@golang.org>
Date:   Tue Jan 31 11:00:16 2012 -0500

    crypto/x509: use case-insensitive hostname matching.
    
    Fixes #2792.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/5590045

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

https://github.com/golang/go/commit/8efb304440474a4c168c30fc07f2787eaf16cfbf

元コミット内容

crypto/x509: use case-insensitive hostname matching.

Fixes #2792.

R=golang-dev, r
CC=golang-dev
https://golang.org/cl/5590045

変更の背景

この変更は、Go言語の crypto/x509 パッケージにおけるTLS/SSL証明書のホスト名検証が、大文字・小文字を区別して行われていた問題を修正するために導入されました。元の実装では、証明書内のDNS名(Subject Alternative Name: SAN)やCommon Name (CN) と、接続先のホスト名との比較において、大文字・小文字が厳密に一致する必要がありました。

しかし、DNS名やホスト名の比較においては、一般的に大文字・小文字を区別しないのが標準的な慣習です。特に、RFC 6125「TLSにおけるIDの表現と検証」のセクション6.4.1では、ホスト名の比較はASCIIの大文字・小文字を区別しない方法で行うべきであると明確に規定されています。

この不一致は、例えば www.example.com という証明書が WWW.EXAMPLE.COM というホスト名に対して無効と判断されるといった、相互運用性の問題や不必要な検証エラーを引き起こす可能性がありました。コミットメッセージにある Fixes #2792 は、この問題がGoのIssueトラッカーで報告されていたことを示しています。この修正により、GoのTLSスタックがより堅牢になり、標準に準拠した挙動を示すようになります。

前提知識の解説

X.509 証明書とTLS/SSL

X.509証明書は、公開鍵暗号基盤 (PKI) において、公開鍵の所有者の身元を証明するために使用されるデジタル証明書の標準です。TLS (Transport Layer Security) やその前身であるSSL (Secure Sockets Layer) は、インターネット上での安全な通信を確立するためのプロトコルであり、X.509証明書を利用してサーバーの身元をクライアントに証明します。

ホスト名検証

TLSハンドシェイクの過程で、クライアントはサーバーから提示された証明書が、接続しようとしているホスト名に対して有効であるかを確認します。この検証は、主に証明書内の以下のフィールドに基づいて行われます。

  1. Subject Alternative Name (SAN): 証明書が有効なホスト名を複数指定できる拡張フィールドです。DNS名、IPアドレス、URIなど、様々な形式でIDを記述できます。現代のTLSでは、SANがホスト名検証の主要な手段として推奨されています。
  2. Common Name (CN): 証明書のSubjectフィールドの一部であり、通常は証明書が発行されたエンティティの一般的な名前(例: ホスト名)を含みます。SANが存在しない場合のみ、CNがホスト名検証に使用されますが、SANの使用が推奨されています。

ホスト名の大文字・小文字の扱い (RFC 6125)

DNS (Domain Name System) のドメイン名自体は、大文字・小文字を区別しません。例えば、example.comEXAMPLE.COM は同じドメインを指します。このため、TLSにおけるホスト名検証においても、一般的には大文字・小文字を区別しない比較が求められます。

RFC 6125「TLSにおけるIDの表現と検証」のセクション6.4.1「DNS-IDの比較」では、以下のように明確に述べられています。

For DNS-ID, the comparison MUST be case-insensitive. The DNS-ID is compared against the reference identifier by converting both to their lowercase ASCII representation.

これは、DNS名を含むIDの比較は、両者をASCIIの小文字表現に変換した上で行うべきであることを意味します。このコミットは、まさにこのRFCの要件を満たすための変更です。

Go言語の unicode/utf8 パッケージ

unicode/utf8 パッケージは、Go言語でUTF-8エンコードされたテキストを扱うためのユーティリティを提供します。UTF-8はUnicode文字をバイト列にエンコードするための可変長エンコーディングです。このパッケージは、UTF-8シーケンスの有効性をチェックしたり、ルーン(Unicodeコードポイント)を抽出したりする機能を提供します。

このコミットでは、toLowerCaseASCII 関数内で utf8.RuneError をチェックしています。これは、入力文字列が有効なUTF-8ではない場合に発生する可能性のあるエラーを検出するためです。無効なUTF-8シーケンス中に大文字のASCIIバイトが含まれている可能性を考慮し、その場合も小文字化処理が必要であると判断しています。

技術的詳細

このコミットの主要な変更点は、crypto/x509 パッケージ内のホスト名検証ロジックに、大文字・小文字を区別しない比較を導入したことです。具体的には、以下の2つのファイルが変更されています。

  1. src/pkg/crypto/x509/verify.go: ホスト名検証のコアロジックが含まれるファイル。
  2. src/pkg/crypto/x509/verify_test.go: ホスト名検証のテストケースが含まれるファイル。

src/pkg/crypto/x509/verify.go の変更

このファイルには、新しいヘルパー関数 toLowerCaseASCII が追加され、既存の VerifyHostname 関数が修正されています。

toLowerCaseASCII 関数の追加

この関数は、入力された文字列をASCIIの小文字に変換します。RFC 6125の要件を満たすために、明示的にASCII文字のみを対象としています。

// toLowerCaseASCII returns a lower-case version of in. See RFC 6125 6.4.1. We use
// an explicitly ASCII function to avoid any sharp corners resulting from
// performing Unicode operations on DNS labels.
func toLowerCaseASCII(in string) string {
	// If the string is already lower-case then there's nothing to do.
	isAlreadyLowerCase := true
	for _, c := range in {
		if c == utf8.RuneError {
			// If we get a UTF-8 error then there might be
			// upper-case ASCII bytes in the invalid sequence.
			isAlreadyLowerCase = false
			break
		}
		if 'A' <= c && c <= 'Z' {
			isAlreadyLowerCase = false
			break
		}
	}

	if isAlreadyLowerCase {
		return in
	}

	out := []byte(in)
	for i, c := range out {
		if 'A' <= c && c <= 'Z' {
			out[i] += 'a' - 'A'
		}
	}
	return string(out)
}

この関数のロジックは以下の通りです。

  • 最適化: まず、入力文字列が既に全て小文字のASCII文字であるかをチェックします。もしそうであれば、新しい文字列を生成せずに元の文字列をそのまま返します。これにより、不要なメモリ割り当てと処理を避けます。
  • UTF-8エラーハンドリング: utf8.RuneError のチェックが含まれています。これは、入力文字列が有効なUTF-8ではない場合に発生する可能性のあるエラーです。無効なシーケンス中に大文字のASCIIバイトが含まれている可能性を考慮し、その場合も小文字化処理に進むようにしています。
  • ASCII変換: 文字列をバイトスライスに変換し、各バイトをループで処理します。もしバイトがASCIIの大文字 ('A' から 'Z') であれば、対応する小文字に変換します ('a' - 'A' を加算することで実現)。
  • 文字列再変換: 最後に、変更されたバイトスライスを新しい文字列として返します。

VerifyHostname 関数の修正

Certificate 型の VerifyHostname メソッドは、証明書が指定されたホスト名に対して有効であるかを検証する関数です。この関数が、toLowerCaseASCII 関数を使用して、比較対象となるホスト名と証明書内のDNS名/Common Nameを小文字化してから matchHostnames 関数に渡すように変更されました。

 func (c *Certificate) VerifyHostname(h string) error {
+	lowered := toLowerCaseASCII(h)
+
 	if len(c.DNSNames) > 0 {
 		for _, match := range c.DNSNames {
-\t\t\tif matchHostnames(match, h) {
+\t\t\tif matchHostnames(toLowerCaseASCII(match), lowered) {
 				return nil
 			}
 		}
 		// If Subject Alt Name is given, we ignore the common name.
-\t} else if matchHostnames(c.Subject.CommonName, h) {
+\t} else if matchHostnames(toLowerCaseASCII(c.Subject.CommonName), lowered) {
 		return nil
 	}
  • 入力ホスト名 h は、まず toLowerCaseASCII(h) によって小文字化され、lowered 変数に格納されます。
  • 証明書の DNSNames フィールド内の各エントリも、matchHostnames に渡される前に toLowerCaseASCII(match) によって小文字化されます。
  • DNSNames が存在しない場合に使用される c.Subject.CommonName も同様に、matchHostnames に渡される前に toLowerCaseASCII(c.Subject.CommonName) によって小文字化されます。

これにより、matchHostnames 関数は常に小文字化されたホスト名とパターンを受け取るため、大文字・小文字を区別しない比較が実現されます。

src/pkg/crypto/x509/verify_test.go の変更

新しいテストケースが verifyTests スライスに追加され、大文字・小文字が混在するホスト名でも正しく検証が成功することを確認しています。

 	{
 		leaf:          googleLeaf,
 		intermediates: []string{thawteIntermediate},
 		roots:         []string{verisignRoot},
 		currentTime:   1302726541,
 		dnsName:       "WwW.GooGLE.coM",

 		expectedChains: [][]string{
 			{"Google", "Thawte", "VeriSign"},
 		},
 	},

このテストケースでは、googleLeaf 証明書(おそらく google.com 用の証明書)に対して、dnsNameWwW.GooGLE.coM という大文字・小文字が混在した形式で指定しています。このテストが成功することで、VerifyHostname 関数が大文字・小文字を区別せずにホスト名を正しく検証できるようになったことが確認されます。

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

diff --git a/src/pkg/crypto/x509/verify.go b/src/pkg/crypto/x509/verify.go
index 50a3b66e55..87b1cb7bb1 100644
--- a/src/pkg/crypto/x509/verify.go
+++ b/src/pkg/crypto/x509/verify.go
@@ -7,6 +7,7 @@ package x509
 import (
  	"strings"
  	"time"
+	"unicode/utf8"
 )
 
 type InvalidReason int
@@ -225,17 +226,51 @@ func matchHostnames(pattern, host string) bool {
  	return true
  }\n 
+// toLowerCaseASCII returns a lower-case version of in. See RFC 6125 6.4.1. We use
+// an explicitly ASCII function to avoid any sharp corners resulting from
+// performing Unicode operations on DNS labels.
+func toLowerCaseASCII(in string) string {
+\t// If the string is already lower-case then there\'s nothing to do.
+\tisAlreadyLowerCase := true
+\tfor _, c := range in {
+\t\tif c == utf8.RuneError {
+\t\t\t// If we get a UTF-8 error then there might be
+\t\t\t// upper-case ASCII bytes in the invalid sequence.\n+\t\t\tisAlreadyLowerCase = false
+\t\t\tbreak
+\t\t}\n+\t\tif \'A\' <= c && c <= \'Z\' {
+\t\t\tisAlreadyLowerCase = false
+\t\t\tbreak
+\t\t}\n+\t}\n+\n+\tif isAlreadyLowerCase {
+\t\treturn in
+\t}\n+\n+\tout := []byte(in)\n+\tfor i, c := range out {
+\t\tif \'A\' <= c && c <= \'Z\' {
+\t\t\tout[i] += \'a\' - \'A\'
+\t\t}\n+\t}\n+\treturn string(out)
+}\n+\n // VerifyHostname returns nil if c is a valid certificate for the named host.\n // Otherwise it returns an error describing the mismatch.\n func (c *Certificate) VerifyHostname(h string) error {
+\tlowered := toLowerCaseASCII(h)
+\n \tif len(c.DNSNames) > 0 {
 \t\tfor _, match := range c.DNSNames {
-\t\t\tif matchHostnames(match, h) {
+\t\t\tif matchHostnames(toLowerCaseASCII(match), lowered) {
 \t\t\t\treturn nil
 \t\t\t}\n \t\t}\n \t\t// If Subject Alt Name is given, we ignore the common name.\n-\t} else if matchHostnames(c.Subject.CommonName, h) {
+\t} else if matchHostnames(toLowerCaseASCII(c.Subject.CommonName), lowered) {
 \t\treturn nil
 \t}\n \ndiff --git a/src/pkg/crypto/x509/verify_test.go b/src/pkg/crypto/x509/verify_test.go
index 2016858307..2cdd66a558 100644
--- a/src/pkg/crypto/x509/verify_test.go
+++ b/src/pkg/crypto/x509/verify_test.go
@@ -37,6 +37,17 @@ var verifyTests = []verifyTest{\n \t\t\t{\"Google\", \"Thawte\", \"VeriSign\"},\n \t\t},\n \t},\n+\t{\n+\t\tleaf:          googleLeaf,\n+\t\tintermediates: []string{thawteIntermediate},\n+\t\troots:         []string{verisignRoot},\n+\t\tcurrentTime:   1302726541,\n+\t\tdnsName:       "WwW.GooGLE.coM",\n+\n+\t\texpectedChains: [][]string{\n+\t\t\t{\"Google\", \"Thawte\", \"VeriSign\"},\n+\t\t},\n+\t},\n \t{\n \t\tleaf:          googleLeaf,\n \t\tintermediates: []string{thawteIntermediate},\

コアとなるコードの解説

toLowerCaseASCII 関数

この関数は、RFC 6125の要件に従い、DNSラベルの比較をASCIIの大文字・小文字を区別しない形で行うために導入されました。

  • 目的: 入力文字列をASCIIの小文字に変換します。Unicodeの複雑なケースマッピングルールを避け、DNS名に特化したシンプルなASCII変換を行います。
  • 効率性: 既に小文字である場合は、無駄な処理を避けるために元の文字列をそのまま返します。
  • 堅牢性: utf8.RuneError のチェックにより、無効なUTF-8シーケンスが含まれていても、その中にASCIIの大文字が含まれていれば適切に処理されるようにしています。これは、入力データが常に完全にクリーンであるとは限らない現実世界のシナリオに対応するためです。

VerifyHostname 関数の変更

VerifyHostname 関数は、証明書内のDNS名やCommon Nameと、検証対象のホスト名を比較する際に、この新しく追加された toLowerCaseASCII 関数を使用するように変更されました。

  • ホスト名の小文字化: VerifyHostname に渡されるホスト名 h は、まず toLowerCaseASCII(h) によって小文字化されます。
  • 証明書内の名前の小文字化: 証明書内の DNSNamesSubject.CommonName も、matchHostnames 関数に渡される前に toLowerCaseASCII を通して小文字化されます。
  • 一貫した比較: これにより、matchHostnames 関数は常に小文字化された文字列同士を比較することになり、大文字・小文字を区別しないホスト名検証が実現されます。

この変更は、Goの crypto/x509 パッケージがTLS/SSLの標準仕様に準拠し、より堅牢で互換性の高い証明書検証を提供する上で非常に重要です。

関連リンク

参考にした情報源リンク