[インデックス 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ハンドシェイクの過程で、クライアントはサーバーから提示された証明書が、接続しようとしているホスト名に対して有効であるかを確認します。この検証は、主に証明書内の以下のフィールドに基づいて行われます。
- Subject Alternative Name (SAN): 証明書が有効なホスト名を複数指定できる拡張フィールドです。DNS名、IPアドレス、URIなど、様々な形式でIDを記述できます。現代のTLSでは、SANがホスト名検証の主要な手段として推奨されています。
- Common Name (CN): 証明書のSubjectフィールドの一部であり、通常は証明書が発行されたエンティティの一般的な名前(例: ホスト名)を含みます。SANが存在しない場合のみ、CNがホスト名検証に使用されますが、SANの使用が推奨されています。
ホスト名の大文字・小文字の扱い (RFC 6125)
DNS (Domain Name System) のドメイン名自体は、大文字・小文字を区別しません。例えば、example.com
と EXAMPLE.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つのファイルが変更されています。
src/pkg/crypto/x509/verify.go
: ホスト名検証のコアロジックが含まれるファイル。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
用の証明書)に対して、dnsName
を WwW.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)
によって小文字化されます。 - 証明書内の名前の小文字化: 証明書内の
DNSNames
やSubject.CommonName
も、matchHostnames
関数に渡される前にtoLowerCaseASCII
を通して小文字化されます。 - 一貫した比較: これにより、
matchHostnames
関数は常に小文字化された文字列同士を比較することになり、大文字・小文字を区別しないホスト名検証が実現されます。
この変更は、Goの crypto/x509
パッケージがTLS/SSLの標準仕様に準拠し、より堅牢で互換性の高い証明書検証を提供する上で非常に重要です。
関連リンク
- Go CL 5590045: https://golang.org/cl/5590045
- Go Issue 2792: https://code.google.com/p/go/issues/detail?id=2792 (古いIssueトラッカーのリンクですが、当時の問題報告を示唆しています)
参考にした情報源リンク
- RFC 6125: Representation and Verification of Domain-Based Application Service Identity in the Transport Layer Security (TLS)
- 特にセクション 6.4.1. "DNS-ID Comparison": https://datatracker.ietf.org/doc/html/rfc6125#section-6.4.1
- X.509 Certificate: https://en.wikipedia.org/wiki/X.509
- Transport Layer Security (TLS): https://en.wikipedia.org/wiki/Transport_Layer_Security
- Subject Alternative Name: https://en.wikipedia.org/wiki/Subject_Alternative_Name
- Common Name (CN): https://en.wikipedia.org/wiki/Common_Name
- Go
unicode/utf8
package documentation: https://pkg.go.dev/unicode/utf8