https 原理分析進階-模擬https通訊過程

2023-06-28 15:00:29

大家好,我是藍胖子,之前出過一篇https的原理分析 ,完整的介紹了https概念以及通訊過程,今天我們就來比較完整的模擬實現https通訊的過程,通過這篇文章,你能瞭解到https核心的概念以及原理,https證書是如何申請的,以及如何用golang實現https通訊,https雙向認證。

本章程式碼已經上傳到github

https://github.com/HobbyBear/codelearning/tree/master/httpsdemo

https原理回顧

在開始之前,讓我們來了解下https相關的核心知識,可以作為上篇https原理分析的補充。學習一個東西一定要先知道為什麼要用它,我總結了兩點:

1,https 第一個好處是使原本的http明文傳輸變成了密文傳輸,增加了安全性。

2,https第二好處是採用數位憑證來解決了身份認證問題,起碼對端通訊是經過ca認證的。

那麼https又是通過什麼技術來實現上述兩點的呢?

數位憑證原理

我先聊聊數位憑證的實現原理,在https的握手階段,伺服器端會傳送自身的證書給使用者端,使用者端會去驗證這個證書的有效性,有效性是這樣保證的:

數位憑證上會寫明證書的簽名演演算法和證書的簽名,如下圖所示

證書經過簽名演演算法中指定的SHA-256演演算法將證書內容進行hash得到訊息摘要,然後再將這個摘要值經過RSA演演算法用證書頒發機構的私鑰進行加密就得到了證書的簽名。

而使用者端拿到這個證書就會用證書頒發機構的公鑰去解密簽名,然後按SHA-256演演算法也對證書內容進行hash,也得到一個訊息摘要值,使用者端就去比對自己計算的訊息摘要和公鑰解密簽名得到的訊息摘要是否一致,一致則說明證書未被篡改並且是證書頒發機構頒發的。

有同學可能會疑惑,證書頒發機構的公鑰是從哪裡獲取的,證書頒發機構的公鑰就在頒發機構其自身的證書裡,如下圖所示。

https密文加密原理

知道了數位憑證的驗證原理,我們來看看https通訊中涉及到的加密過程,在https的握手階段,伺服器端會選擇一個與使用者端都支援的金鑰套件用於後續的加密,金鑰套件一般會有如下元件:

  1. 金鑰交換演演算法:用於在使用者端和伺服器之間安全地交換加密金鑰。常見的金鑰交換演演算法有RSA和Diffie-Hellman等。

  2. 對稱加密演演算法:用於對通訊資料進行加密和解密。常見的對稱加密演演算法有AES、DES和3DES等。

  3. 摘要演演算法:用於生成和驗證訊息的完整性。常見的摘要演演算法有MD5和SHA-256等。

https採用非對稱加密的方式交換金鑰,然後使用對稱加密的方式對資料進行加密,並且對訊息的內容採用摘要演演算法得到訊息摘要,這樣對端在解密資料後可以通過相同的訊息摘要演演算法對計算後的訊息摘要和傳過來的訊息摘要進行對比,從而判斷資料是否經過篡改。

具體步驟如下:

  1. 使用者端向伺服器傳送一個初始的握手請求,該請求中包含了使用者端支援的密碼套件列表。
  2. 伺服器收到握手請求後,會從使用者端提供的密碼套件列表中選擇一個與自己支援的密碼套件相匹配的套件。
  3. 伺服器將選定的密碼套件資訊返回給使用者端。
  4. 使用者端收到伺服器返回的密碼套件資訊後,會選擇一個與伺服器相匹配的密碼套件。
  5. 使用者端生成一個隨機的對稱加密金鑰,並使用伺服器的公鑰對該金鑰進行加密。
  6. 使用者端將加密後的對稱加密金鑰傳送給伺服器。
  7. 伺服器使用自己的私鑰對接收到的加密的對稱加密金鑰進行解密。
  8. 使用者端和伺服器現在都擁有了相同的對稱加密金鑰,可以使用該金鑰進行加密和解密通訊資料。
  9. 使用者端和伺服器使用對稱加密金鑰對通訊資料進行加密和解密,並使用摘要演演算法對資料進行完整性驗證。

通過以上步驟,使用者端和伺服器可以建立一個安全的HTTPS連線,並使用密碼套件來保護通訊的安全性。

模擬證書頒發

接下來,我們就要開始實現下https的通訊了,由於只是實驗,我們不會真正的去為我的伺服器去申請一個數位憑證,所以我們暫時在本地用openssl來模擬下證書頒發的邏輯。

模擬根認證ca機構

我們知道證書頒發的機構是ca,而ca根證書是預設信任的,一般內建在瀏覽器和作業系統裡,所以首先來生成一個根證書,並且讓系統預設信任它。

先生成ca的私鑰

openssl genpkey -algorithm RSA -out ca_private.key 

然後生成ca的證書請求

openssl req -new -key ca_private.key -out ca_csr.csr

生成ca證書

openssl x509 -req -in ca_csr.csr -signkey ca_private.key -out ca_cert.crt

我用的是mac系統,所以我這裡演示下mac系統如何新增證書信任,

開啟鑰匙串應用-> 將證書拖進登入那一欄 -> 右擊證書點選顯示簡介-> 將信任那一欄改為始終信任

模擬ca機構向伺服器頒發證書

生成 伺服器自身的私鑰

openssl genpkey -algorithm RSA -out final_private.key

接著就是生成證書請求,和前面生成證書請求不同,因為目前主流瀏覽器都要求證書需要設定subjectAltName,如果沒有設定SAN會報證書錯誤。

所以我們要換種方式生成證書請求,首先建立一個檔案,比如我建立一個san.txt的檔案

[req]
default_bits = 4096
distinguished_name = req_distinguished_name
req_extensions = v3_req
[req_distinguished_name]
countryName = country
stateOrProvinceName = province
localityName = city
organizationName = company name
## 換成自己的域名
commonName = lanpangzi.com  
[v3_req]
subjectAltName = @alt_names
[alt_names]
## 換成自己的域名
DNS.1=*.lanpangzi.com
DNS.2=*.lanpangzi2.com

到時候上述檔案只需要更換為自己的域名即可。由於我的域名設定為了.lanpangzi.com 和.lanpangzi2.com,所以我還要改下原生的hosts檔案。

## /etc/hosts
127.0.0.1 www.lanpangzi2.com
127.0.0.1       www.lanpangzi.com

接著生成伺服器證書請求

openssl req -new -key final_private.key -out final_csr.csr -config san.txt -sha256

生成伺服器證書

openssl x509 -req -days 365 -in final_csr.csr -CA ca_cert.crt -CAkey ca_private.key -set_serial 01 -out final_csr.crt -extfile san.txt -extensions v3_req

golang實現https服務驗證證書

經過了上述步驟後算是生成了一個由ca機構頒發的證書,然後我們用golang程式碼實現一個https伺服器。需要為https伺服器傳入證書以及伺服器自身的私鑰。

func main() {  
   http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {  
      fmt.Fprintf(w, "Hello, World!\n")  
   })  
   fmt.Println(http.ListenAndServeTLS(":443",  
      "./final_csr.crt",  
      "./final_private.key", nil))  
}

接著實現下使用者端程式碼

func main() {  
   client := &http.Client{Transport: tr}  
   resp, err := client.Get("https://www.lanpangzi.com")  
   if err != nil {  
      fmt.Println("Get error:", err)  
      return  
   }  
   defer resp.Body.Close()  
   body, err := ioutil.ReadAll(resp.Body)  
   fmt.Println(string(body))  
}

啟動伺服器端和使用者端後能看到服務正常返回了。

/private/var/folders/yp/g914gkcd54qdm5d0qyc9ljm00000gn/T/GoLand/___go_build_codelearning_httpsdemo_client
Hello, World!

說明證書設定已經成功,而使用者端驗證證書的邏輯已經在本文開始講解了。

golang實現https雙向認證

上述程式碼只是實現了https的單向認證,即使用者端對伺服器端的域名進行認證,在某些情況下,伺服器端也需要檢驗使用者端是否合法,所以下面我們就來看下如何用golang實現雙向認證的。首先我們還是要用ca位使用者端頒發一個證書。

模擬ca機構向用戶端頒發證書

生成 伺服器自身的私鑰

openssl genpkey -algorithm RSA -out client_private.key

建立一個san_client.txt的檔案

[req]
default_bits = 4096
distinguished_name = req_distinguished_name
req_extensions = v3_req
[req_distinguished_name]
countryName = country
stateOrProvinceName = province
localityName = city
organizationName = company name
## 換成自己的域名
commonName = lanpangziclient.com  
[v3_req]
subjectAltName = @alt_names
[alt_names]
## 換成自己的域名
DNS.1=*.lanpangziclient.com
DNS.2=*.lanpangziclient2.com

到時候上述檔案只需要更換為自己的域名即可。由於我的域名設定為了.lanpangzi.com 和.lanpangzi2.com,所以我還要改下原生的hosts檔案。

## /etc/hosts
127.0.0.1 www.lanpangziclient2.com
127.0.0.1       www.lanpangziclient.com

接著生成伺服器證書請求

openssl req -new -key client_private.key -out client_csr.csr -config san_client.txt -sha256

生成伺服器證書

openssl x509 -req -days 365 -in client_csr.csr -CA ca_cert.crt -CAkey ca_private.key -set_serial 01 -out client_csr.crt -extfile san_client.txt -extensions v3_req

伺服器端和使用者端需要做下改動,伺服器端預設不會去校驗使用者端身份,但是現在改成強制校驗

func main() {  
  
   s := &http.Server{  
      Addr: ":443",  
      Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {  
         fmt.Fprintf(w, "Hello, World!\n")  
      }),  
      TLSConfig: &tls.Config{  
         ClientAuth: tls.RequireAndVerifyClientCert,  
      },  
   }  
  
   fmt.Println(s.ListenAndServeTLS("./final_csr.crt",  
      "./final_private.key"))  
}

使用者端程式碼請求時需要帶上自己的證書

func main() {  
   cliCrt, err := tls.LoadX509KeyPair("./client_csr.crt", "./client_private.key")  
   if err != nil {  
      fmt.Println("Loadx509keypair err:", err)  
      return  
   }  
   tr := &http.Transport{  
      TLSClientConfig: &tls.Config{  
         Certificates: []tls.Certificate{cliCrt},  
      },  
   }  
   client := &http.Client{Transport: tr}  
   resp, err := client.Get("https://www.lanpangzi.com")  
   if err != nil {  
      fmt.Println("Get error:", err)  
      return  
   }  
   defer resp.Body.Close()  
   body, err := ioutil.ReadAll(resp.Body)  
   fmt.Println(string(body))  
}

這樣就完成了一個https的雙向認證。