HTTPS 雙向認證原理和實現方式
雙向認證的含義就是服務端和客戶端都需要驗證對方的身份,相比普通的單向認證多了一些步驟。
基礎概念
下面先講一些 https 相關的概念。
對稱加密
對稱加密是一種加密算法,使用相同的密鑰來加密和解密數據。 這意味着發送和接收方都必須共享相同的密鑰。 對稱加密是加密領域中最快的一種加密方式,因爲它使用的是相對較小的密鑰和簡單的運算。
非對稱加密
非對稱加密是一種密碼學方法,與對稱加密不同,它使用一對密鑰而不是一個密鑰。 這對密鑰包括一個公鑰和一個私鑰。公鑰用於加密數據,而私鑰用於解密數據。
-
• 公鑰: 公鑰是一個用於加密的密鑰,可以公開被任何人訪問,公鑰加密的數據只有私鑰可以解開。
-
• 私鑰: 私鑰是與公鑰配對的另一個密鑰,需要妥善保管避免被泄露。
-
• 加密和解密: 如果使用公鑰加密了一段數據,只有擁有相應私鑰的人才能解密它。反之亦然,如果使用私鑰加密了數據,只有擁有相應公鑰的人才能解密。
-
• 數字簽名:簡單點說數字簽名就是私鑰加密摘要,而非加密原文,分爲下面幾個步驟:
-
1. 消息摘要:使用哈希算法把原文生成一份摘要。
-
2. 私鑰加密:使用私鑰對摘要進行加密,得到數字簽名。
-
3. 發送消息和簽名:把原數據和加密後的數據摘要打包一起發給對方。
-
4. 驗證:接收方使用發送方的公鑰來解密數字簽名,得到摘要。然後接收方對收到的消息使用同樣的哈希算法得到一個新的摘要。如果這兩個摘要匹配,說明消息未被篡改,且確實是由私鑰的所有者簽名的。
RSA、DSA、ECDSA 和 Elliptic Curve Diffie-Hellman (ECDH) 是一些常見的非對稱加密算法。 這些算法每個有其獨特的優點和應用場景,比如 RSA 用於數字簽名,ECDH 適用於密鑰協商。 與對稱加密相比,非對稱加密算法的性能都比較低。
證書
-
• CA:也就是常說的根證書,操作系統默認集成了很多權威機構的根證書,因此不必再自己安裝和信任一遍。
-
• https 證書:通常包括一個公鑰證書和一個私鑰,它們由權威機構(CA)簽發,這些權威機構通常是要收費的,也有免費的機構,類似 Let's Encrypt。 另一種方式是自己簽發,不過需要讓客戶端信任自己簽發的 CA 證書,目的是爲了讓自己簽發的域名證書通過校驗。
-
• 證書籤發:就是用權威機構幫你生成一個私鑰,並使用它的根證書和這個私鑰對你的域名進行證書籤發,最後將簽發後的公鑰證書和這個私鑰給你。
證書是建立信任的關鍵,包括 CA 根證書、https 證書和自簽名證書。CA 提供權威認證,而自簽名證書適用於開發環境。
https 通信步驟
https 通信大致上是分爲 3 個階段。
- 1. 握手階段(Handshake):
-
• 客戶端向服務器發送一個加密通信的請求,請求中包含支持的加密算法和其他相關信息。
-
• 服務器將自己的證書、支持的加密算法等信息發送給客戶端。
-
• 客戶端驗證服務器的證書是否有效。
- 2. 密鑰協商階段:
- • 利用非對稱加密算法使得雙方安全的獲取到一個會話密鑰。
- 3. 加密通信階段:
- • 客戶端和服務器使用協商好的會話密鑰進行對稱加密通信,保護數據的機密性。
配置 https 證書
常見的 web 服務器都有配置 https 證書的功能,例如 nginx、caddy 等。
基本上只需要把證書和私鑰配置到某一個目錄,更改 web 服務器的配置即可生效。
https 單向認證
顧名思義,就是隻有一個方向進行了認證,這裏指的是客戶端認證,通常瀏覽器都會對網站上的 https 證書進行驗證。
正常情況下訪問 https 站點,瀏覽器左上角的小鎖就會是灰色的。(如果你的瀏覽器版本過低,有可能是綠色的。)
當 https 站點的證書不正確時,就會出現一個出現【不安全】這個三個紅色的大字,有下面幾種原因會導致不安全。
-
1. 當前域名和證書籤名的域名不匹配。(這種情況就需要重新進行域名簽發了。)
-
2. 當前 IP 和證書籤名的 IP 不匹配。(這種情況較少,因爲之前市面上的機構都不簽發 IP 證書,現在好像也有了不少。)
-
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