HTTPS 雙向認證原理和實現方式

雙向認證的含義就是服務端和客戶端都需要驗證對方的身份,相比普通的單向認證多了一些步驟。

基礎概念

下面先講一些 https 相關的概念。

對稱加密

對稱加密是一種加密算法,使用相同的密鑰來加密和解密數據。 這意味着發送和接收方都必須共享相同的密鑰。 對稱加密是加密領域中最快的一種加密方式,因爲它使用的是相對較小的密鑰和簡單的運算。

非對稱加密

非對稱加密是一種密碼學方法,與對稱加密不同,它使用一對密鑰而不是一個密鑰。 這對密鑰包括一個公鑰和一個私鑰。公鑰用於加密數據,而私鑰用於解密數據。

  1. 1. 消息摘要:使用哈希算法把原文生成一份摘要。

  2. 2. 私鑰加密:使用私鑰對摘要進行加密,得到數字簽名。

  3. 3. 發送消息和簽名:把原數據和加密後的數據摘要打包一起發給對方。

  4. 4. 驗證:接收方使用發送方的公鑰來解密數字簽名,得到摘要。然後接收方對收到的消息使用同樣的哈希算法得到一個新的摘要。如果這兩個摘要匹配,說明消息未被篡改,且確實是由私鑰的所有者簽名的。

RSA、DSA、ECDSA 和 Elliptic Curve Diffie-Hellman (ECDH) 是一些常見的非對稱加密算法。 這些算法每個有其獨特的優點和應用場景,比如 RSA 用於數字簽名,ECDH 適用於密鑰協商。 與對稱加密相比,非對稱加密算法的性能都比較低。

證書

證書是建立信任的關鍵,包括 CA 根證書、https 證書和自簽名證書。CA 提供權威認證,而自簽名證書適用於開發環境。

https 通信步驟

https 通信大致上是分爲 3 個階段。

  1. 1. 握手階段(Handshake):
  1. 2. 密鑰協商階段:
  1. 3. 加密通信階段:

配置 https 證書

常見的 web 服務器都有配置 https 證書的功能,例如 nginx、caddy 等。

基本上只需要把證書和私鑰配置到某一個目錄,更改 web 服務器的配置即可生效。

https 單向認證

顧名思義,就是隻有一個方向進行了認證,這裏指的是客戶端認證,通常瀏覽器都會對網站上的 https 證書進行驗證。

正常情況下訪問 https 站點,瀏覽器左上角的小鎖就會是灰色的。(如果你的瀏覽器版本過低,有可能是綠色的。)

當 https 站點的證書不正確時,就會出現一個出現【不安全】這個三個紅色的大字,有下面幾種原因會導致不安全。

  1. 1. 當前域名和證書籤名的域名不匹配。(這種情況就需要重新進行域名簽發了。)

  2. 2. 當前 IP 和證書籤名的 IP 不匹配。(這種情況較少,因爲之前市面上的機構都不簽發 IP 證書,現在好像也有了不少。)

  3. 3. 證書過期了。(需要定期檢查域名是否過期並及時更新)

https 雙向認證

與 https 單向認證不同的是,服務端也會要求驗證客戶端的證書,除了域名證書和私鑰外(開啓 https 用),還需要一張客戶端 CA 證書用於驗證客戶端提供的普通證書。

客戶端在訪問服務端時,需要將自己的證書發送給對方,經過服務端的驗證後才能夠正常通信。

客戶端證書認證提供額外的安全層,確保只有授權的客戶端能夠連接。生成和管理客戶端證書時,需要採取措施保護私鑰,確保其不被泄露。

效果如下圖所示:

golang 實現 https 雙向認證

生成服務端 CA 證書,域名證書和私鑰

具體的生成證書部分代碼可以查看文章末尾的 GitHub 倉庫。

serverCACsr, serverCAKey, err := utils.LoadOrCreateCA("server-ca.crt""server-ca.key")
if err != nil {
  log.Fatal(err)
}
serverCert, serverKey, err := utils.SignCertWithCA(serverCACsr, serverCAKey, false, "localhost")
if err != nil {
  log.Fatal(err)
}
os.WriteFile("server.crt", serverCert, 0755)
os.WriteFile("server.key", serverKey, 0755)

生成服務端 CA 證書,普通證書和私鑰

clientCACsr, clientCAKey, err := utils.LoadOrCreateCA("client-ca.crt""client-ca.key")
if err != nil {
    log.Fatal(err)
}
clientCert, clientKey, err := utils.SignCertWithCA(clientCACsr, clientCAKey, true, "localhost")
if err != nil {
    fmt.Println(err)
    return
}
os.WriteFile("client.crt", clientCert, 0755)
os.WriteFile("client.key", clientKey, 0755)

導入證書

如果你想要用瀏覽器體驗雙向認證,還需要把客戶端證書和私鑰轉換成 p12 證書並導入到本地計算機中。

  // 將證書和私鑰轉換爲 PKCS#12 格式,用於導入到本地計算機中測試瀏覽器
  certificate, fkey, err := utils.ParseCertAndPrivateKey(clientCert, clientKey)
  if err != nil {
      fmt.Println("Error:", err)
      return
  }
  p12Data, err := gopkcs12.Legacy.WithRand(rand.Reader).Encode(fkey, certificate, nil, "password")
  if err != nil {
      fmt.Println("Encode pem to pkcs12 Error:", err)
      return
  }
  // 將 PKCS#12 數據保存到文件
  os.WriteFile("client.p12", p12Data, 0755)

這裏就不再贅述如何導入了,和導入抓包軟件 CA 證書的流程是相同的。

配置服務端

package server

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

func handler(w http.ResponseWriter, r *http.Request) {
    io.WriteString(w, "OK")
}

func Serv() {
    http.HandleFunc("/", handler)

    // 服務器證書和私鑰
    cert, err := tls.LoadX509KeyPair("server.crt""server.key")
    if err != nil {
        fmt.Println(err)
        return
    }

    // 客戶端 CA 證書池
    caCert, err := os.ReadFile("client-ca.crt")
    if err != nil {
        fmt.Println(err)
        return
    }
    caCertPool := x509.NewCertPool()
    caCertPool.AppendCertsFromPEM(caCert)

    // HTTPS 配置
    tlsConfig := &tls.Config{
        Certificates: []tls.Certificate{cert},
        ClientCAs:    caCertPool,
        ClientAuth:   tls.RequireAndVerifyClientCert,
    }

    server := &http.Server{
        Addr:      ":8443",
        TLSConfig: tlsConfig,
    }

    fmt.Println("Server is running on https://localhost:8443")
    err = server.ListenAndServeTLS("""")
    if err != nil {
        fmt.Println(err)
    }
}

配置客戶端

package client

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

func Request() {
    // 服務器 CA 證書池
    caCert, err := os.ReadFile("server-ca.crt")
    if err != nil {
        fmt.Println(err)
        return
    }
    caCertPool := x509.NewCertPool()
    caCertPool.AppendCertsFromPEM(caCert)

    // 客戶端證書和私鑰
    cert, err := tls.LoadX509KeyPair("client.crt""client.key")
    if err != nil {
        fmt.Println(err)
        return
    }

    // HTTPS 配置
    tlsConfig := &tls.Config{
        RootCAs:      caCertPool,
        Certificates: []tls.Certificate{cert},
    }

    client := &http.Client{
        Transport: &http.Transport{TLSClientConfig: tlsConfig},
    }

    resp, err := client.Get("https://localhost:8443")
    if err != nil {
        log.Fatal("get ", err)
        return
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        log.Fatal("read ", err)
        return
    }

    fmt.Println(string(body))
}

此時提前說一句,如果你想要訪問服務端 https 是安全的,還需要導入並信任服務端 CA 證書,原因上面已經說過。

認證過程

TLS v1.2 和 TLS v1.3 有很大不同,當然這些並不是最重要的,因爲我們並不能改變或者控制這些協議。

我整理了 TLS v1.2 和 TLS v1.3 在雙向認證的過程中不同的地方,如下圖,僅供參考。

紅色標記部分是雙向認證時纔有的通信步驟。

TLS v1.2

TLS v1.3

TLS v1.3 使用了 ECDH 等密鑰協商算法,因此在交互的過程中只需要雙方計算一個臨時密鑰併發送給對方,雙方就能通過這個臨時密鑰計算出最終要使用的加密密鑰。 減少了很多步驟,對於性能和安全性方便有着極大提高。

golang 代碼地址

https://github.com/dushixiang/https-two-way-auth

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/_8toY107hVldpi8xAzJLoA