手把手教你用 Go 實現一個 mTLS

zhuyaguang1368發表於2021-11-12

想知道什麼是 mTLS(雙向 TLS)?來吧,讓我們用 Golang 和 OpenSSL 實現一個 mTLS。

介紹

TLS(安全傳輸層協議簡稱)為網路通訊的應用程式提供必要的加密。HTTPS (超文字安全協議) 是 HTTP 的一種擴充套件,利用 TLS 實現安全性。TLS 技術要求 CA (證書頒發機構) 向服務頒發 X.509 數字證書,然後將該數字證書移交給服務的消費者,由其使用 CA 本身進行驗證。mTLS 將同樣的思想擴充套件到應用程式中,例如,在微服務中,提供者和消費者都需要向對方生成自己的證書。這些證書由雙方使用各自的 CA 進行驗證。一旦經過驗證,伺服器/客戶端或提供者/使用者之間的通訊就會安全地進行。

實現

第一步 - 構建一個簡單的 HTTP 服務端和客戶端

讓我們先 在server.go 裡建立一個簡單的 HTTP 服務,當訪問 8080 埠,請求 /hello 資源時回覆 Hello, world!

package main

import (
"io"
"log"
"net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
// Write "Hello, world!" to the response body
io.WriteString(w, "Hello, world!\n")
}

func main() {
// Set up a /hello resource handler
http.HandleFunc("/hello", helloHandler)

// Listen to port 8080 and wait
log.Fatal(http.ListenAndServe(":8080", nil))
}

客戶端通過 8080 埠請求 /hello 資源,然後將響應體通過 stdout 列印。下面是 client.go 程式碼:

package main

import (
"fmt"
"io/ioutil"
"log"
"net/http"
)

func main() {
// Request /hello over port 8080 via the GET method
r, err := http.Get("http://localhost:8080/hello")
if err != nil {
log.Fatal(err)
}

// Read the response body
defer r.Body.Close()
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Fatal(err)
}

// Print the response body to stdout
fmt.Printf("%s\n", body)
}

開啟一個終端並執行服務端:

go run -v server.go

開啟另外一個終端執行客戶端:

go run -v client.go

你可以從客戶端看到以下輸出:

Hello, world!

第二步 - 生成和使用服務端證書

使用以下命令生成證書。該命令將建立一個有效期為 10 年的 2048 位金鑰證書。此外,CN=localhost 說明該證書對 localhost 域是有效的。

openssl req -newkey rsa:2048 \
  -new -nodes -x509 \
  -days 3650 \
  -out cert.pem \
  -keyout key.pem \
  -subj "/C=US/ST=California/L=Mountain View/O=Your Organization/OU=Your Unit/CN=localhost"

你應該目錄裡面有 cert.pemkey.pem 證書了。

現在讓我們啟用伺服器 HTTP 的 TLS,也就是 HTTPS。server.go 裡面的 http.ListenAndServe(":8080", nil) 呼叫替換成 http.ListenAndServeTLS(":8443", "cert.pem", "key.pem", nil) ,通過 8443 埠監聽連結。同時提供前面生成的證書。

-// Listen to port 8080 and wait
-log.Fatal(http.ListenAndServe(":8080", nil))
+// Listen to HTTPS connections on port 8443 and wait
+log.Fatal(http.ListenAndServeTLS(":8443", "cert.pem", "key.pem", nil))

你可以通過執行並在瀏覽器輸入 https://localhost:8443/hello 來驗證伺服器是否工作。

Secure Server

現在我們更新 client.go 程式碼來通過 HTTPS 連線服務端。

-// Request /hello over port 8080 via the GET method
-r, err := http.Get("http://localhost:8080/hello")
+// Request /hello over HTTPS port 8443 via the GET method
+r, err := http.Get("https://localhost:8443/hello")

由於我們客戶端還不知道證書,直接執行伺服器會顯示下面的錯誤:

http: TLS handshake error from [::1]:59436: remote error: tls: bad certificate

在客戶端,你需要注意以下幾點:

x509: certificate is not valid for any names, but wanted to match localhost

第三步- 向客戶端提供證書

更新 client.go 程式碼讀取之前生成的證書,程式碼如下:

-// Request /hello over HTTPS port 8443 via the GET method
-r, err := http.Get("https://localhost:8443/hello")

+// Create a CA certificate pool and add cert.pem to it
+caCert, err := ioutil.ReadFile("cert.pem")
+if err != nil {
+log.Fatal(err)
+}
+caCertPool := x509.NewCertPool()
+caCertPool.AppendCertsFromPEM(caCert)
+
+// Create a HTTPS client and supply the created CA pool
+client := &http.Client{
+Transport: &http.Transport{
+TLSClientConfig: &tls.Config{
+RootCAs: caCertPool,
+},
+},
+}
+
+// Request /hello via the created HTTPS client over port 8443 via GET
+r, err := client.Get("https://localhost:8443/hello")

這裡,我們讀取 cert.pem 檔案並在建立客戶端時提供根 CA 。執行客戶端現在應該可以成功顯示以下內容:

Hello, world!

最後一步 - 啟用 mTLS

在客戶端,讀取並提供金鑰對作為客戶端證書。

+// Read the key pair to create certificate
+cert, err := tls.LoadX509KeyPair("cert.pem", "key.pem")
+if err != nil {
+log.Fatal(err)
+}

...

-// Create a HTTPS client and supply the created CA pool
+// Create a HTTPS client and supply the created CA pool and certificate
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: caCertPool,
+Certificates: []tls.Certificate{cert},
},
},
}

在服務端,我們建立一個類似於 CA 池 ,並將其提供給 TLS 配置,來作為驗證客戶端證書的權威。我們還對伺服器證書使用相同的金鑰對。

-// Listen to HTTPS connections on port 8443 and wait
-log.Fatal(http.ListenAndServeTLS(":8443", "cert.pem", "key.pem", nil))

+// Create a CA certificate pool and add cert.pem to it
+caCert, err := ioutil.ReadFile("cert.pem")
+if err != nil {
+log.Fatal(err)
+}
+caCertPool := x509.NewCertPool()
+caCertPool.AppendCertsFromPEM(caCert)
+
+// Create the TLS Config with the CA pool and enable Client certificate validation
+tlsConfig := &tls.Config{
+ClientCAs: caCertPool,
+ClientAuth: tls.RequireAndVerifyClientCert,
+}
+tlsConfig.BuildNameToCertificate()
+
+// Create a Server instance to listen on port 8443 with the TLS config
+server := &http.Server{
+Addr:      ":8443",
+TLSConfig: tlsConfig,
+}
+
+// Listen to HTTPS connections with the server certificate and wait
+log.Fatal(server.ListenAndServeTLS("cert.pem", "key.pem"))

先 執行 server.go 然後執行 client.go,然後你可以在客戶端上看到如下一條成功的訊息:

Hello, world!

完整程式碼

最終, server.go 程式碼如下:

package main

import (
"crypto/tls"
"crypto/x509"
"io"
"io/ioutil"
"log"
"net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
// Write "Hello, world!" to the response body
io.WriteString(w, "Hello, world!\n")
}

func main() {
// Set up a /hello resource handler
http.HandleFunc("/hello", helloHandler)

// Create a CA certificate pool and add cert.pem to it
caCert, err := ioutil.ReadFile("cert.pem")
if err != nil {
log.Fatal(err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)

// Create the TLS Config with the CA pool and enable Client certificate validation
tlsConfig := &tls.Config{
ClientCAs: caCertPool,
ClientAuth: tls.RequireAndVerifyClientCert,
}
tlsConfig.BuildNameToCertificate()

// Create a Server instance to listen on port 8443 with the TLS config
server := &http.Server{
Addr:      ":8443",
TLSConfig: tlsConfig,
}

// Listen to HTTPS connections with the server certificate and wait
log.Fatal(server.ListenAndServeTLS("cert.pem", "key.pem"))
}

client.go 程式碼如下:

package main

import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"log"
"net/http"
)

func main() {
// Read the key pair to create certificate
cert, err := tls.LoadX509KeyPair("cert.pem", "key.pem")
if err != nil {
log.Fatal(err)
}

// Create a CA certificate pool and add cert.pem to it
caCert, err := ioutil.ReadFile("cert.pem")
if err != nil {
log.Fatal(err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)

// Create a HTTPS client and supply the created CA pool and certificate
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: caCertPool,
Certificates: []tls.Certificate{cert},
},
},
}

// Request /hello via the created HTTPS client over port 8443 via GET
r, err := client.Get("https://localhost:8443/hello")
if err != nil {
log.Fatal(err)
}

// Read the response body
defer r.Body.Close()
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Fatal(err)
}

// Print the response body to stdout
fmt.Printf("%s\n", body)
}

結論

Golang 讓實現 mTLS 變得非常容易,而且不到 100 行程式碼。


我很想聽聽你的意見。

更多原創文章乾貨分享,請關注公眾號
  • 手把手教你用 Go 實現一個 mTLS
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章