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

[インデックス 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証明書のホスト名検証を制御する機能を実装しました。具体的には、TransportTLSClientConfigInsecureSkipVerifytrueに設定されている場合、証明書のホスト名検証をスキップするようにしました。

変更されたファイル:

  • 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では、この制限により以下のような問題が発生していました:

  1. 開発・テスト環境での問題: 自己署名証明書やテスト用証明書を使用する場合、ホスト名が一致しないため接続できない
  2. 分散システムでの問題: 動的にホスト名が変わるシステムや、証明書のCNと実際のホスト名が異なるシステムでの接続問題
  3. 証明書検証の粒度の問題: 証明書の真正性は検証したいが、ホスト名の検証だけはスキップしたい場合に対応できない

当時はInsecureSkipVerifytrueにすると、証明書検証とホスト名検証の両方が無効になってしまい、きめ細かな制御ができませんでした。

前提知識の解説

TLS証明書検証とホスト名検証

TLS(Transport Layer Security)接続において、クライアントはサーバーの身元を確認するために以下の2つの検証を行います:

  1. 証明書の真正性検証: 証明書が信頼できるCA(Certificate Authority)によって署名されているかを確認
  2. ホスト名検証: 証明書に記載されたホスト名が実際の接続先ホスト名と一致するかを確認

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
    }
}

この変更により、以下の条件でホスト名検証がスキップされます:

  • TLSClientConfignilでない、かつ
  • InsecureSkipVerifytrueの場合

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

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 {
    // ホスト名検証を実行
}

この条件は以下の論理で構成されています:

  1. t.TLSClientConfig == nil: TLS設定が未設定の場合は、デフォルトの安全な動作(ホスト名検証実行)を採用
  2. !t.TLSClientConfig.InsecureSkipVerify: TLS設定が存在し、InsecureSkipVerifyfalseの場合は、ホスト名検証を実行

つまり、ホスト名検証をスキップするのは、明示的にInsecureSkipVerifytrueに設定された場合のみです。

テストケースの設計

テストでは、InsecureSkipVerifytruefalseの両方の場合をテストしています:

  • insecure=trueの場合:エラーが発生しないことを期待(err == nil
  • insecure=falseの場合:エラーが発生することを期待(err != nil

これは、テストサーバーが自己署名証明書を使用しているため、通常の証明書検証では失敗することを前提としています。

HTTPTestServerの活用

httptest.NewTLSServer()を使用してテスト用のTLSサーバーを作成しています。このサーバーは:

  • 自己署名証明書を使用
  • ホスト名検証で失敗する証明書を提供
  • テスト完了後にdefer ts.Close()でクリーンアップ

関連リンク

参考にした情報源リンク