[インデックス 10070] Go HTTPクライアントのTLS証明書検証制御機能の実装
コミット
コミットハッシュ: 2cab897ce055fd753821a85a2134affe64ffe8cb
著者: Brad Fitzpatrick bradfitz@golang.org
日付: 2011年10月21日 08:14:38 -0700
メッセージ: http: Transport: with TLS InsecureSkipVerify, skip hostname check
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2cab897ce055fd753821a85a2134affe64ffe8cb
元コミット内容
このコミットは、Go言語のHTTPクライアントにおけるTLS証明書のホスト名検証を制御する機能を実装しました。具体的には、Transport
のTLSClientConfig
でInsecureSkipVerify
がtrue
に設定されている場合、証明書のホスト名検証をスキップするようにしました。
変更されたファイル:
src/pkg/http/transport.go
: 6行追加、2行削除src/pkg/http/client_test.go
: 24行追加
Issue: #2386を解決
Code Review: https://golang.org/cl/5312045
変更の背景
Go 1.0(2011年)の時点では、HTTPクライアントでTLS接続を行う際、証明書のホスト名検証が必須でした。これは、サーバーの証明書に含まれるホスト名(Common NameやSubject Alternative Name)が実際の接続先ホスト名と一致しない場合、接続が失敗することを意味していました。
Issue #2386では、この制限により以下のような問題が発生していました:
- 開発・テスト環境での問題: 自己署名証明書やテスト用証明書を使用する場合、ホスト名が一致しないため接続できない
- 分散システムでの問題: 動的にホスト名が変わるシステムや、証明書のCNと実際のホスト名が異なるシステムでの接続問題
- 証明書検証の粒度の問題: 証明書の真正性は検証したいが、ホスト名の検証だけはスキップしたい場合に対応できない
当時はInsecureSkipVerify
をtrue
にすると、証明書検証とホスト名検証の両方が無効になってしまい、きめ細かな制御ができませんでした。
前提知識の解説
TLS証明書検証とホスト名検証
TLS(Transport Layer Security)接続において、クライアントはサーバーの身元を確認するために以下の2つの検証を行います:
- 証明書の真正性検証: 証明書が信頼できるCA(Certificate Authority)によって署名されているかを確認
- ホスト名検証: 証明書に記載されたホスト名が実際の接続先ホスト名と一致するかを確認
RFC 2818とホスト名検証
RFC 2818「HTTP Over TLS」は、HTTPS接続におけるホスト名検証の標準を定義しています。主な要点:
- サーバー証明書のSubject DNのCommon Name(CN)フィールドにホスト名を記載する方法は非推奨
- 現在はSubject Alternative Name(SAN)拡張にDNS名を含める方法が推奨される
- RFC 6125では、証明書にはDNS-IDを含むべきであり、CAは他の仕様で明示的に要求されない限りCN-IDを含む証明書を発行すべきではないとされている
Go言語のTLS実装
Go言語のcrypto/tls
パッケージは、TLS接続の確立と検証を担当します:
tls.Conn.Handshake()
: TLSハンドシェイクを実行tls.Conn.VerifyHostname()
: ホスト名検証を実行tls.Config.InsecureSkipVerify
: 証明書検証全体をスキップする設定
技術的詳細
変更前の問題
変更前のコードでは、TLSハンドシェイク後に必ずVerifyHostname()
が呼ばれていました:
if err = conn.(*tls.Conn).Handshake(); err != nil {
return nil, err
}
if err = conn.(*tls.Conn).VerifyHostname(cm.tlsHost()); err != nil {
return nil, err
}
この実装では、InsecureSkipVerify
の設定に関係なく、必ずホスト名検証が実行されていました。
変更後の改善
新しい実装では、InsecureSkipVerify
の設定を確認してからホスト名検証を実行します:
if err = conn.(*tls.Conn).Handshake(); err != nil {
return nil, err
}
if t.TLSClientConfig == nil || !t.TLSClientConfig.InsecureSkipVerify {
if err = conn.(*tls.Conn).VerifyHostname(cm.tlsHost()); err != nil {
return nil, err
}
}
この変更により、以下の条件でホスト名検証がスキップされます:
TLSClientConfig
がnil
でない、かつInsecureSkipVerify
がtrue
の場合
コアとなるコードの変更箇所
transport.go の変更
変更前 (src/pkg/http/transport.go:362-367):
if err = conn.(*tls.Conn).Handshake(); err != nil {
return nil, err
}
if err = conn.(*tls.Conn).VerifyHostname(cm.tlsHost()); err != nil {
return nil, err
}
変更後 (src/pkg/http/transport.go:362-370):
if err = conn.(*tls.Conn).Handshake(); err != nil {
return nil, err
}
if t.TLSClientConfig == nil || !t.TLSClientConfig.InsecureSkipVerify {
if err = conn.(*tls.Conn).VerifyHostname(cm.tlsHost()); err != nil {
return nil, err
}
}
client_test.go の追加
新しいテスト関数TestClientInsecureTransport
が追加されました:
func TestClientInsecureTransport(t *testing.T) {
ts := httptest.NewTLSServer(HandlerFunc(func(w ResponseWriter, r *Request) {
w.Write([]byte("Hello"))
}))
defer ts.Close()
for _, insecure := range []bool{true, false} {
tr := &Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: insecure,
},
}
c := &Client{Transport: tr}
_, err := c.Get(ts.URL)
if (err == nil) != insecure {
t.Errorf("insecure=%v: got unexpected err=%v", insecure, err)
}
}
}
コアとなるコードの解説
条件分岐の論理構造
if t.TLSClientConfig == nil || !t.TLSClientConfig.InsecureSkipVerify {
// ホスト名検証を実行
}
この条件は以下の論理で構成されています:
t.TLSClientConfig == nil
: TLS設定が未設定の場合は、デフォルトの安全な動作(ホスト名検証実行)を採用!t.TLSClientConfig.InsecureSkipVerify
: TLS設定が存在し、InsecureSkipVerify
がfalse
の場合は、ホスト名検証を実行
つまり、ホスト名検証をスキップするのは、明示的にInsecureSkipVerify
がtrue
に設定された場合のみです。
テストケースの設計
テストでは、InsecureSkipVerify
のtrue
とfalse
の両方の場合をテストしています:
insecure=true
の場合:エラーが発生しないことを期待(err == nil
)insecure=false
の場合:エラーが発生することを期待(err != nil
)
これは、テストサーバーが自己署名証明書を使用しているため、通常の証明書検証では失敗することを前提としています。
HTTPTestServerの活用
httptest.NewTLSServer()
を使用してテスト用のTLSサーバーを作成しています。このサーバーは:
- 自己署名証明書を使用
- ホスト名検証で失敗する証明書を提供
- テスト完了後に
defer ts.Close()
でクリーンアップ
関連リンク
- Go Issue #2386: Transport VerifyHostname w/ TLS currently non optional
- Go Code Review 5312045
- RFC 2818: HTTP Over TLS
- RFC 6125: Best Practices for Checking of Server Identities in the Context of Transport Layer Security (TLS)
- Go crypto/tls パッケージドキュメント
- Go net/http パッケージドキュメント
参考にした情報源リンク
- Go TLS InsecureSkipVerify パターン
- Go Issue #21971: crypto/tls: feature request: add option to JUST skip hostname verification
- Go Issue #11076: net/http: Transport VerifyHostname should be optional when using TLS
- Stack Overflow: How to do a https request with bad certificate?
- Sling Academy: How to Verify SSL/TLS Certificates in Go
- FreeCodeCamp: How to Validate SSL Certificates in Go