HTTPS原理與證書生成
HTTPS與HTTP是什麼關係呢?我們可以對比下HTTP與HTTPS的請求過程:
HTTPS 在 TCP 和 HTTP 之間增加了 TLS(Transport Layer Security,傳輸層安全),提供了 內容加密 、 身份認證 和 資料完整性 三大功能。
HTTPS也就是HTTP over SSL/TLS,所有的http資料都是在SSL/TLS協議封裝之上傳輸的。Https協議在Http協議的基礎上,添加了SSL/TLS握手以及資料加密傳輸,也屬於應用層協議。所以,研究Https協議原理,最終其實是研究SSL/TLS協議。
可以看到,假設服務端和客戶端之間單次傳輸耗時 28ms,那麼客戶端需要等到 168ms 之後才能開始傳送 HTTP 請求報文,這還沒把客戶端和服務端處理時間算進去。光是 TLS 握手就需要消耗兩個 RTT(Round-Trip Time,往返時間),這就是造成 HTTPS 更慢的主要原因。當然,HTTPS 要求資料加密傳輸,加解密相比 HTTP 也會帶來額外的開銷,不過對稱加密本來就很快,加上硬體效能越來越好,所以這部分開銷還好。
TLS歷史
傳輸層安全協議(Transport Layer Security,縮寫:TLS),及其前身安全套接層(Secure Sockets Layer,縮寫:SSL)是一種安全協議,目的是為網際網路通訊提供安全及資料完整性保障。
SSL包含記錄層(Record Layer)和傳輸層,記錄層協議確定了傳輸層資料的封裝格式。傳輸層安全協議使用X.509認證,之後利用非對稱加密演算來對通訊方做身份認證,之後交換對稱金鑰作為會談金鑰(Session key)。這個會談金鑰是用來將通訊兩方交換的資料做加密,保證兩個應用間通訊的保密性和可靠性,使客戶與伺服器應用之間的通訊不被攻擊者竊聽。
1994年早期,NetScape公司設計了SSL協議(Secure Sockets Layer)的1.0版,但是未釋出。
1994年11月,NetScape公司釋出SSL 2.0版,很快發現有嚴重漏洞。
1996年11月,SSL 3.0版問世,得到大規模應用。
1999年1月,網際網路標準化組織ISOC接替NetScape公司,釋出了SSL的升級版 ofollow,noindex">TLS 1.0版 。
2006年4月和2008年8月,TLS進行了兩次升級,分別為 TLS 1.1 版和 TLS 1.2 版。最新的變動是2011年TLS 1.2的修訂版。
現在正在制定 tls 1.3 。
TLS握手過程
TLS的握手階段是發生在TCP握手之後。握手實際上是一種協商的過程,對協議所必需的一些引數進行協商。TLS握手過程如下:
如上圖所示,簡述如下:
- ClientHello:客戶端生成一個隨機數
random-client
,傳到伺服器端(Say Hello) - ServerHello:伺服器端生成一個隨機數
random-server
,和著公鑰,一起回饋給客戶端(I got it) - 客戶端收到的東西原封不動,加上
premaster secret
(通過random-client
、random-server
經過一定演算法生成的東西),再一次送給伺服器端,這次傳過去的東西會使用公鑰加密 - 伺服器端先使用私鑰解密,拿到
premaster secret
,此時客戶端和伺服器端都擁有了三個要素:random-client
、random-server
和premaster secret
- 此時安全通道已經建立,以後的交流都會校檢上面的三個要素通過演算法算出的
session key
Client Hello =>
在TLS握手階段,客戶端首先要告知服務端,自己支援哪些加密演算法,所以客戶端需要將本地支援的加密套件(Cipher Suite)的列表傳送給服務端。除此之外,客戶端還要產生一個隨機數,傳送給服務端,客戶端的隨機數需要跟服務端產生的隨機數結合起來產生後面要講到的Master Secret。即ClientHello主要包含以下資訊:
- 支援的協議版本,比如 TLS 1.2
- 一個客戶端⽣成的隨機數,稍後用於生成”對話金鑰”
- 支援的加密方法,⽐如 RSA 公鑰加密。
- 支援的壓縮方法。
Server Hello <=
上圖中,從Server Hello到Server Done,有些服務端的實現是每條單獨傳送,有服務端實現是合併到一起傳送。Sever Hello和Server Done都是隻有頭沒有內容的資料。
服務端在接收到客戶端的Client Hello之後,服務端需要將自己的證書傳送給客戶端。證書是需要申請,並由專門的數字證書認證機構(CA)通過非常嚴格的稽核之後頒發的電子證書。頒發證書的同時會產生一個私鑰和公鑰。私鑰由服務端自己儲存,不可洩漏。公鑰則是附帶在證書的資訊中,可以公開。證書本身也附帶一個證書電子簽名,這個簽名用來驗證證書的完整性和真實性,可以防止證書被篡改。另外,證書還有有效期。
在服務端向客戶端傳送的證書中沒有提供足夠的資訊的時候,還可以向客戶端傳送一個Server Key Exchange。
此外,對於非常重要的保密資料,服務端還需要對客戶端進行驗證,以保證資料傳送給了安全的合法的客戶端。服務端可以向客戶端發出Cerficate Request訊息,要求客戶端傳送證書對客戶端的合法性進行驗證。
跟客戶端一樣,服務端也需要產生一個隨機數傳送給客戶端。客戶端和服務端都需要使用這兩個隨機數來產生Master Secret。最後服務端會發送一個Server Hello Done訊息給客戶端,表示Server Hello訊息結束了。
- 確認使⽤的加密通訊協議版本,⽐如 TLS 1.0 版本。如果瀏覽器與伺服器⽀持的版本不一致,伺服器關閉加密通訊。
- 一個伺服器⽣成的隨機數,稍後用於生成”對話金鑰”。
- 確認使用的加密方法,⽐如 RSA 公鑰加密。
- 伺服器證書
Client Key Exchange =>
如果服務端需要對客戶端進行驗證,在客戶端收到服務端的Server Hello訊息之後,首先需要向服務端傳送客戶端的證書,讓服務端來驗證客戶端的合法性。
在此之前的所有TLS握手資訊都是明文傳送的。在收到服務端的證書等資訊之後,客戶端會使用一些加密演算法(例如:RSA, Diffie-Hellman)產生一個48個位元組的隨機數Key,這個Key叫PreMaster Secret,很多材料上也被稱作 PreMaster Key
。這是整個TLS握手期間的第三個隨機數。最終通過 Master secret
生成 session secret
, session secret
就是用來對應用資料進行加解密的會話祕鑰。為什麼需要 PreMaster secret
這第三個隨機數的Key呢?因為前兩個隨機數Client Random和Server Random都是明文傳輸的。中間人可能早已監聽到了。如果只使用這兩個隨機數計算最終的會話祕鑰中間人也可以生成。所以 PreMaster secret
使用RSA非對稱加密的方式,使用服務端傳過來的公鑰進行加密,然後傳給服務端。
接著,客戶端需要對服務端的證書進行檢查,檢查證書的完整性以及證書跟服務端域名是否吻合。
ChangeCipherSpec是一個獨立的協議,體現在資料包中就是一個位元組的資料,用於告知服務端,客戶端已經切換到之前協商好的加密套件的狀態,準備使用之前協商好的加密套件加密資料並傳輸了。
在ChangecipherSpec傳輸完畢之後,客戶端會使用之前協商好的加密套件和session secret加密一段Finish的資料傳送給服務端,此資料是為了在正式傳輸應用資料之前對剛剛握手建立起來的加解密通道進行驗證。
PreMaster Key
Server Finish <=
服務端在接收到客戶端傳過來的PreMaster加密資料之後,使用私鑰對這段加密資料進行解密,並對資料進行驗證,也會使用跟客戶端同樣的方式生成 session secret
,一切準備好之後,會給客戶端傳送一個ChangeCipherSpec,告知客戶端已經切換到協商過的加密套件狀態,準備使用加密套件和 session secret
加密資料了。之後,服務端也會使用 session secret
加密後一段Finish訊息傳送給客戶端,以驗證之前通過握手建立起來的加解密通道是否成功。
- 編碼改變通知,表示隨後的資訊都將⽤雙方商定的加密⽅法和金鑰傳送。
- 伺服器握⼿結束通知,表示伺服器的握⼿階段已經結束。這⼀項同時也是前⾯傳送的所有內容的 hash 值,⽤來供客戶端校驗。
應用資料傳輸
在所有的握手階段都完成之後,就可以開始傳送應用資料了。應用資料在傳輸之前,首先要附加上MAC secret,然後再對這個資料包使用write encryption key進行加密。在服務端收到密文之後,使用Client write encryption key進行解密,客戶端收到服務端的資料之後使用Server write encryption key進行解密,然後使用各自的write MAC key對資料的完整性包括是否被串改進行驗證。
幾個名詞
在詳述過程之前,我們需要了解一下,在過程中會出現的內容。
-
session key
: 這是 TLS/SSL 最後協商的結果,用來進行對稱加密。 -
client random
: 是一個 32B 的序列值,每次連線來時,都會動態生成,即,每次連線生成的值都不會一樣。因為,他包含了 4B 的時間戳和 28B 的隨機數。 -
server random
: 和client random
一樣,只是由 server 端生成。 -
premaster secret
: 這是 48B 的 blob 資料。它能和 client & server random 通過pseudorandom
(PRF) 一起生成 session key。 -
cipher suite
: 用來定義 TLS 連線用到的演算法。通常有 4 部分:- 非對稱加密 (ECDH 或 RSA)
- 證書驗證 (證書的型別)
- 保密性 (對稱加密演算法)
- 資料完整性 (產生 hash 的函式) 比如
AES128-SHA
代表著:- RSA 演算法進行非對稱加密
- RSA 進行證書驗證
- 128bit AES 對稱加密
- 160bit SHA 資料加密演算法
- 比如
ECDHE-ECDSA-AES256-GCM-SHA384
代表著- ECDHE 演算法進行非對稱加密
- ECDSA 進行證書驗證
- 265bit AES 對稱加密
- 384bit SHA 資料加密演算法
上面主要是根據 RSA 加密方式來講解的。因為 RSA 才會在 TLS/SSL 過程中,將 pre-master secret 顯示的進行傳輸,這樣的結果有可能造成,hacker 拿到了 private key 那麼他也可以生成一模一樣的 sessionKey。即,該次連線的安全性就沒了。
接下來,我們主要講解一下另外一種加密方式 DH。它和 RSA 的主要區別就是,到底傳不傳 pre-master secret。RSA 傳而 DH 不傳。根據 cloudflare 的講解可以清楚的瞭解到兩者的區別:
這是 RSA 的傳輸方式,基本過程如上述。
而 DH 具體區別在下圖:
這裡先補充一下 DH 演算法的知識。因為,PreMaster secret 就是根據這個生成的。DH 基本過程也不算太難,詳情可以參考 wiki 。 它主要運用到的公式就是:
為了防止在 DH 引數交換時,資料過大,DH 使用的是取模數的方式,這樣就能限制傳輸的值永遠在 [1,p-1]。這裡,先說明一下 DH 演算法的基本條件:
- 公共條件: p 和 g 都是已知,並且公開。即,第三方也可以隨便獲取到。
- 私有條件: a 和 b 是兩端自己生成的,第三方獲取不到。
基本流程就是:
我們只要把上圖的 DH parameter 替換為相對應的 X/Y 即可。而最後的 Z 就是我們想要的 Premaster secret。 之後,就和 RSA 加密演算法一致,加上兩邊的 random-num 生成 sessionKey。通過,我們常常稱 DH 也叫作 Ephemeral Diffie-Hellman handshake
。 因為,他每次一的 sessionKey 都是不同的。
而 RSA 和 DH 兩者之間的具體的區別就在於:RSA 會將 premaster secret 顯示的傳輸,這樣有可能會造成私鑰洩露引起的安全問題。而 DH 不會將 premaster secret 顯示的傳輸。
實際抓包
通過 Wireshark 抓包可以清楚地看到完整 TLS 握手過程所需的兩個 RTT,如下圖(來自: TLS 握手優化詳解 ):
私鑰的作用
握手階段有三點需要注意。
(1)生成對話金鑰一共需要三個隨機數。
(2)握手之後的對話使用”對話金鑰”加密(對稱加密),伺服器的公鑰和私鑰只用於加密和解密”對話金鑰”(非對稱加密),無其他作用。
(3)伺服器公鑰放在伺服器的數字證書之中。
從上面第二點可知,整個對話過程中(握手階段和其後的對話),伺服器的公鑰和私鑰只需要用到一次。這就是Flare/">CloudFlare能夠提供Keyless服務的根本原因。
某些客戶(比如銀行)想要使用外部CDN,加快自家網站的訪問速度,但是出於安全考慮,不能把私鑰交給CDN服務商。這時,完全可以把私鑰留在自家伺服器,只用來解密對話金鑰,其他步驟都讓CDN服務商去完成。
上圖中,銀行的伺服器只參與第四步,後面的對話都不再會用到私鑰了。
自簽名證書
什麼叫自簽名呢?就是自己通過keytool去生成一個證書,然後使用,並不是CA機構去頒發的。使用自簽名證書的網站,大家在使用瀏覽器訪問的時候,一般都是報風險警告,比如之前的12306就是這麼幹的, https://kyfw.12306.cn/otn/ ,點選進入12306的購票頁面就能看到了。當然現在我重新試了一下已經不是這樣了。
服務端證書生成
Golang服務端可以參考這裡: https://gist.github.com/denji/12b3a568f092ab951456
生成服務端私鑰 Generate private key (.key)
# Key considerations for algorithm "RSA" ≥ 2048-bit openssl genrsa -out server.key 2048 # Key considerations for algorithm "ECDSA" ≥ secp384r1 # List ECDSA the supported curves (openssl ecparam -list_curves) openssl ecparam -genkey -name secp384r1 -out server.key
生成服務端自簽名證書 Generation of self-signed(x509) public key (PEM-encodings .pem
| .crt
) based on the private ( .key
)
openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
在這裡需要大家填寫資料(有些地方可以空著)
Country Name (2 letter code) [AU]:CNState or Province Name (full name) [Some-State]:GuangdongLocality Name (eg, city) []:FoShanOrganization Name (eg, company) [Internet Widgits Pty Ltd]:TestCAOrganizational Unit Name (eg, section) []:Common Name (e.g. server FQDN or YOUR name) []:localhostEmail Address []:
這裡有點要注意, Common Name (e.g. server FQDN or YOUR name) []:
這一項,是最後可以訪問的域名,我這裡為了方便測試,寫成 localhost ,如果是為了給網站生成證書,需要寫成 xxxx.com 。
Simple Golang HTTPS/TLS Server
package main import ( // "fmt" // "io" "net/http" "log" ) func HelloServer(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "text/plain") w.Write([]byte("This is an example server.\n")) // fmt.Fprintf(w, "This is an example server.\n") // io.WriteString(w, "This is an example server.\n") } func main() { http.HandleFunc("/hello", HelloServer) err := http.ListenAndServeTLS(":443", "server.crt", "server.key", nil) if err != nil { log.Fatal("ListenAndServe: ", err) } }
客戶端訪問(Android)
當大家使用Android 中的OkHttp訪問一個使用自簽名的HTTPS的站點,它會丟擲一個 SSLHandshakeException
的異常。
javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found. at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:322) at com.android.okhttp.Connection.upgradeToTls(Connection.java:201) at com.android.okhttp.Connection.connect(Connection.java:155) at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:276) at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:211) at com.android.okhttp.internal.http.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:382) at com.android.okhttp.internal.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:332) at com.android.okhttp.internal.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:199) at com.android.okhttp.internal.http.DelegatingHttpsURLConnection.getInputStream(DelegatingHttpsURLConnection.java:210) at com.android.okhttp.internal.http.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:25) at me.longerian.abcandroid.datetimepicker.TestDateTimePickerActivity$1.run(TestDateTimePickerActivity.java:236) Caused by: java.security.cert.CertificateException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found. at com.android.org.conscrypt.TrustManagerImpl.checkTrusted(TrustManagerImpl.java:318) at com.android.org.conscrypt.TrustManagerImpl.checkServerTrusted(TrustManagerImpl.java:219) at com.android.org.conscrypt.Platform.checkServerTrusted(Platform.java:114) at com.android.org.conscrypt.OpenSSLSocketImpl.verifyCertificateChain(OpenSSLSocketImpl.java:550) at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method) at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:318) ... 10 more Caused by: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found. ... 16 more
這是因為Android 手機有一套共享證書的機制,如果目標 URL 伺服器下發的證書不在已信任的證書列表裡,或者該證書是自簽名的,不是由權威機構頒發,那麼會出異常。我們可以通過自定義的驗證機制讓證書通過驗證。解決方案大家可以移步這裡: pingguohe.net/2016/02/26/Android-App-secure-ssl.html" target="_blank" rel="nofollow,noindex">Android App 安全的HTTPS 通訊