https://www.prakharsrivastav.com/posts/from-http-to-https-using-go/ が原典
Goを使ってHTTPからHTTPSへ
2019-08-02 :: プラカール・スリバスタフ
序章
この記事では、Go で TLS 暗号化を設定する方法を学びます。さらに、相互にTLS暗号化を設定する方法を探っていきます。このブログ記事で紹介されているコードはこちらからご覧いただけます。この記事では、関連するスニペットを表示しています。興味のある読者は、リポジトリをクローンしてそれに従ってください。
まず、シンプルな Http サーバとクライアントを Go で書くことから始めます。次に、サーバで TLS を設定することで、両者間のトラフィックを暗号化します。この記事の最後に、両者間の相互 TLS を設定します。
シンプルなhttpサーバー
まず、Go で Http クライアント・サーバの実装を作成してみましょう。localhost:8080 に到達可能な Http エンドポイント /server を公開します。そして、http.Clientを使ってエンドポイントを呼び出し、その結果を表示します。
完全な実装はこちらを参照してください。
// Server code
mux := http.NewServeMux()
mux.HandleFunc("/server", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Protect Me...")
})
log.Fatal(http.ListenAndServe(":8080", mux))
// Client code
if r, err = http.NewRequest(http.MethodGet, "http://localhost:8080/server", nil); err != nil {
log.Fatalf("request failed : %v", err)
}
c := http.Client{
Timeout: time.Second * 5,
Transport: &http.Transport{IdleConnTimeout: 10 * time.Second},
}
if data, err = callServer(c, r); err != nil {
log.Fatal(err)
}
log.Println(data) // Should print "Protect Me..."
次のセクションでは、TLS を使用してクライアントとサーバ間のトラフィックを暗号化します。その前に、公開鍵インフラストラクチャ (PKI) をセットアップする必要があります。
PKI のセットアップ
ミニ PKI インフラストラクチャをセットアップするために、minica という Go ユーティリティを使用して、ルート、サーバ、クライアントの鍵ペアと証明書を作成します。実際には、認証局 (CA) またはドメイン管理者 (組織内) が鍵ペアと署名付き証明書を提供してくれます。私たちの場合は、minicaを使ってこれをプロビジョニングしてもらうことにします。
鍵ペアと証明書の生成
注: これらを生成するのが面倒に思える場合は、Github リポジトリでコミットされた証明書を再利用することができます。
以下の手順で証明書を生成します。
minicaをインストールする: github.com/jsha/minicaを取得してください。
minica --domains server-certを実行してサーバ証明書を作成します。
初めて実行すると4つのファイルが生成されます。
minica.pem(ルート証明書
minica-key.pem (root 用の秘密鍵)
server-cert/cert.pem (ドメイン「server-cert」の証明書、ルートの公開鍵で署名されています)
server-cert/key.pem (ドメイン「server-cert」の秘密鍵)
minica --domains client-certを実行してクライアント証明書を作成します。2つの新しいファイルが生成されます。
client-cert/cert.pem (ドメイン "client-cert "の証明書)
client-cert/key.pem (ドメイン "client-cert "の秘密鍵)
また、minicaでドメインの代わりにIPを使用して鍵ペアや証明書を生成することもできます。
etc/hosts にエイリアスを設定する
上記で生成したクライアント証明書とサーバ証明書は、それぞれドメイン server-cert と client-cert で有効です。これらのドメインは存在しないので、localhost(127.0.0.1)のエイリアスを作成します。これを設定すると、localhost の代わりに server-cert を使用して Http サーバにアクセスできるようになります。
Linux以外のプラットフォームを使っている場合は、OSに合わせた設定方法をググってみてください。私はLinuxマシンを使っていますが、ドメインエイリアスの設定はとても簡単です。etc/hostsファイルを開き、以下の項目を追加します。
127.0.0.1 server-cert
127.0.0.1 client-cert
この時点で、インフラストラクチャの設定は完了です。次のセクションでは、クライアントとサーバ間のトラフィックを暗号化するために、これらの証明書を使ってサーバを設定します。
サーバーでTLSを設定する
サーバ-certドメインに生成された鍵と証明書を使って、サーバにTLSを設定してみましょう。クライアントは先ほどと同じです。唯一の違いは、3つの異なるURLでサーバを呼び出すことで、何が起こっているのかを理解することです。
完全な実装はこちら
// Server configuration
mux := http.NewServeMux()
mux.HandleFunc("/server", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "i am protected")
})
log.Println("starting server")
// Here we use ListenAndServerTLS() instead of ListenAndServe()
// CertPath and KeyPath are location for certificate and key for server-cer
log.Fatal(http.ListenAndServeTLS(":8080", CertPath, KeyPath, mux))
// Server configuration
c := http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{IdleConnTimeout: 10 * time.Second,},
}
if r, err = http.NewRequest(http.MethodGet, "http://localhost:8080/server", nil); err != nil { // 1
//if r, err = http.NewRequest(http.MethodGet, "https://localhost:8080/server", nil); err != nil { // 2
//if r, err = http.NewRequest(http.MethodGet, "https://server-cert:8080/server", nil); err != nil { // 3
log.Fatalf("request failed : %v", err)
}
if data, err = callServer(c, r); err != nil {
log.Fatal(err)
}
log.Println(data)
http.ListenAndServeTLS()を使用してサーバを起動します。これにはポート、公開証明書へのパス、秘密鍵へのパス、そしてHttp-handlerの4つの引数が必要です。サーバからのレスポンスを見てみましょう。私たちは失敗しますが、私たちはどのようにHttp暗号化が動作するかについてのより多くの洞察を与える3つの異なる要求を送信します。
Attepmt 1 http://localhost:8080/server に送信すると、応答があります。
Client Error. Get http://localhost:8080/server: net/http: HTTP/1.x transport connection broken: malformed HTTP response "\x15\x03x01x00x02\x02"
サーバエラー: http: 127.0.0.1:35694 からの TLS ハンドシェイクエラー: tls: 最初のレコードが TLS ハンドシェイクのように見えません。
これは、サーバーが暗号化されたデータを送信していることを意味する良いニュースです。Http経由では誰も意味をなさないでしょう。
Attempt 2 to https://localhost:8080/server、レスポンスは以下の通りです。
クライアントエラーです。Get https://localhost:8080/server: x509: certificate is valid for server-cert, not localhost
サーバエラー: http: 127.0.0.1:35698 からの TLS ハンドシェイクエラー: リモートエラー: tls: 不正な証明書
これはまたしても朗報です。これは、ドメインサーバ証明書に発行された証明書を他のドメイン(ローカルホスト)で使用することができないことを意味します。
Attempt 3 to https://server-cert:8080/server、応答があります。
クライアントエラーです。Get https://server-cert:8080/server: x509: certificate signed by unknown authority
サーバエラー: http: 127.0.0.1:35700 からの TLS ハンドシェイクエラー: リモートエラー: tls: 不正な証明書
このエラーは、クライアントがその証明書に署名したことを信頼していないことを示しています。クライアントは証明書に署名した CA を認識していなければなりません。
このセクションの全体的な考えは、TLS が保証する 3 つの保証を実証することでした。
メッセージは常に暗号化されている。
サーバが実際に言っている通りのものであること。
クライアントはサーバの証明書を盲目的に信じてはいけない。クライアントは、CA を通じてサーバの身元を確認できるようにしなければなりません。
クライアントでCA証明書を設定する
クライアント側のCA証明書を設定して、ルートCAの証明書とサーバの身元を照合できるようにします。サーバ証明書はルートCAの公開鍵を使って署名されているので、TLSハンドシェイクが有効になり、通信が暗号化されます。
完全な実装はこちらにあります。
// create a Certificate pool to hold one or more CA certificates
rootCAPool := x509.NewCertPool()
// read minica certificate (which is CA in our case) and add to the Certificate Pool
rootCA, err := ioutil.ReadFile(RootCertificatePath)
if err != nil {
log.Fatalf("reading cert failed : %v", err)
}
rootCAPool.AppendCertsFromPEM(rootCA)
log.Println("RootCA loaded")
// in the http client configuration, add TLS configuration and add the RootCAs
c := http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{
IdleConnTimeout: 10 * time.Second,
TLSClientConfig: &tls.Config{RootCAs: rootCAPool},
},
}
if r, err = http.NewRequest(http.MethodGet, "https://server-cert:8080/server", nil); err != nil {
log.Fatalf("request failed : %v", err)
}
if data, err = callServer(c, r); err != nil {
log.Fatal(err)
}
log.Println(data)
// server response
prakhar@tardis (master)? % go run client.go
RootCA loaded
i am protected # response from server
これにより、先ほど説明した3つの保証がすべて保証されます。
相互TLSの設定
サーバーにクライアントの信頼を確立しています。しかし、多くのユースケースでは、サーバーがクライアントを信頼する必要があります。例えば、金融、医療、公共サービス業界などです。これらのシナリオのために、クライアントとサーバーの間で相互にTLSを設定して、双方がお互いを信頼できるようにします。
TLSプロトコルは、最初からこれをサポートしています。相互TLS認証を設定するために必要な手順は以下の通りです。
1.サーバはCA(CA-1)から証明書を取得します。クライアントは、サーバの証明書に署名したCA-1の公開証明書を持っている必要があります。
2.クライアントは CA (CA-2) から証明書を取得します。サーバは、クライアントの証明書に署名したCA-2の公開証明書を持っていなければなりません。簡単にするために、クライアント証明書とサーバ証明書の両方に署名するために同じ CA (CA-1 == CA-2) を使用します。
3.サーバは、すべてのクライアントを検証するためにCA証明書プールを作成します。この時点で、サーバはCA-2の公開証明書を含む。
4.同様に、クライアントは独自のCA証明書プールを作成し、CA-1の公開証明書を含む。
5.両者は、CA 証明書プールに対して受信要求を検証します。どちらか一方に検証エラーがあった場合、接続は中断されます。
実際に動作を見てみましょう。この機能の完全な実装はこちらを参照してください。
サーバーの設定
mux := http.NewServeMux()
mux.HandleFunc("/server", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "i am protected")
})
clientCA, err := ioutil.ReadFile(RootCertificatePath)
if err != nil {
log.Fatalf("reading cert failed : %v", err)
}
clientCAPool := x509.NewCertPool()
clientCAPool.AppendCertsFromPEM(clientCA)
log.Println("ClientCA loaded")
s := &http.Server{
Handler: mux,
Addr: ":8080",
TLSConfig: &tls.Config{
ClientCAs: clientCAPool,
ClientAuth: tls.RequireAndVerifyClientCert,
GetCertificate: func(info *tls.ClientHelloInfo) (certificate *tls.Certificate, e error) {
c, err := tls.LoadX509KeyPair(CertPath, KeyPath)
if err != nil {
fmt.Printf("Error loading key pair: %v\n", err)
return nil, err
}
return &c, nil
},
},
}
log.Fatal(s.ListenAndServeTLS("", ""))
この設定で注意すべき点がいくつかあります。
- http.ListenAndServeTLS() の代わりに server.ListenAndServerTLS() を使用します。
- サーバ証明書と鍵を tls.Config.GetCertificate 関数の中にロードします。
- サーバが信頼すべきクライアント CA 証明書のプールを作成します。
- tls.Config.ClientAuth = tls.RequireAndVerifyClientCertを設定し、接続しようとするすべてのクライアントの証明書を常に検証します。検証されたクライアントのみが会話を続けることができます。
クライアント設定
http.Clientの設定は、クライアントの設定も少し変わります。
rootCA, err := ioutil.ReadFile(RootCertificatePath)
if err != nil {
log.Fatalf("reading cert failed : %v", err)
}
rootCAPool := x509.NewCertPool()
rootCAPool.AppendCertsFromPEM(rootCA)
log.Println("RootCA loaded")
c := http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{
IdleConnTimeout: 10 * time.Second,
TLSClientConfig: &tls.Config{
RootCAs: rootCAPool,
GetClientCertificate: func(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
c, err := tls.LoadX509KeyPair(ClientCertPath, ClientKeyPath)
if err != nil {
fmt.Printf("Error loading key pair: %v\n", err)
return nil, err
}
return &c, nil
},
},
},
}
サーバと比較した場合の設定の違いに注目してください。
- tls.Configでは、サーバー上のClientCAの設定に対して証明書プールをロードするためにRootCAを使用しています。
- tls.Config.GetClientCertificate を使用して、サーバー上の tls.Config.GetCertificate に対してクライアント証明書をロードしています。
- GitHub の実際のコードにはいくつかのコールバックがあり、これを使って証明書の情報を見ることもできます。
クライアントとサーバの相互TLS認証の実行
# Server logs
2019/08/01 20:00:50 starting server
2019/08/01 20:00:50 ClientCA loaded
2019/08/01 20:01:01 client requested certificate
Verified certificate chain from peer:
Cert 0:
Subject [client-cert] # Server shows the client certificate details
Usage [1 2]
Issued by minica root ca 5b4bc5
Issued by
Cert 1:
Self-signed certificate minica root ca 5b4bc5
# Client logs
2019/08/01 20:01:01 RootCA loaded
Verified certificate chain from peer:
Cert 0:
Subject [server-cert] # Client knows the server certificate details
Usage [1 2]
Issued by minica root ca 5b4bc5
Issued by
Cert 1:
Self-signed certificate minica root ca 5b4bc5
2019/08/01 20:01:01 request from server
2019/08/01 20:01:01 i am protected
結論
TLS の設定は、実装の問題というよりも証明書の管理の問題が常にあります。TLS 設定における典型的な混乱は、実装というよりも正しい証明書の使用に関連していることが多いです。TLS プロトコルとハンドシェイクを正しく理解していれば、Go は箱から出してすぐに必要なものをすべて提供してくれます。
また、理論的な観点からTLSの暗号化とセキュリティを探求した以前の記事もチェックしてみてください。
参考文献
この記事は、Gophercon-2018でのLiz Riceの素晴らしいトークに大きく影響されていますので、ぜひチェックしてみてください。その他の参考文献は以下の通りです。
secure-connections: gophercon のためのレポ
minica 認証局
Eric Chiangによるこの驚くべき記事。必読です。
step-by-step-guide-to-mtls-in-go.
mediumのこの記事。