從Chrome源碼看HTTPS

我在《https鏈接的前幾毫秒發生了什麼》詳細地介紹了https鏈接的過程,該篇經過抓包工具分析整個過程,本篇將從Chrome源碼的角度着重介紹加密和解密的過程,並補充更多的細節。html

Chrome/Chromium是使用BoringSSL作爲TLS層的庫,它是OpenSSL的一個fork,是Chrome改於openssl以適應本身產品的特色,代碼位於src/third_party/boringssl/.nginx


HTTPS鏈接的第一步——發送Client Hello,瀏覽器在Client Hello報文裏面填充了使用的TLS版本、client隨機數、加密列表(cipher suites)和包含了hostname的擴展。git

瀏覽器支持的TLS版本總共有5個:算法

#define SSL3_VERSION 0x0300 // 3.0
#define TLS1_VERSION 0x0301 // 3.1
#define TLS1_1_VERSION 0x0302 // 3.2
#define TLS1_2_VERSION 0x0303 // 3.3 (TLS 1.2)
#define TLS1_3_VERSION 0x0304 // 3.4 (TLS 1.3)複製代碼

最新的版本爲TLS 1.3,目前只有Chrome和Firefox支持,nginx 1.13(非穩定版本)/cloudflare支持,當前使用比較普遍的仍是TLS 1.2版本。Chrome在Client Hello裏面設置的TLS爲1.2:chrome

// hs爲SSL_HandShake
hs->client_version =
    hs->max_version >= TLS1_2_VERSION ? TLS1_2_VERSION : hs->max_version;複製代碼

除了TLS外,還有支持UDP的DTLS:數組

#define DTLS1_VERSION 0xfeff
#define DTLS1_2_VERSION 0xfefd複製代碼

打印出來的加密列表cipher suite總共有13個:瀏覽器

TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
TLS_RSA_WITH_AES_128_GCM_SHA256
TLS_RSA_WITH_AES_256_GCM_SHA384
TLS_RSA_WITH_AES_128_CBC_SHA
TLS_RSA_WITH_AES_256_CBC_SHA
TLS_RSA_WITH_3DES_EDE_CBC_SHA
複製代碼

這是瀏覽器支持的加密方式,放在Client Hello裏面發給服務端選擇一個。上面的每個加密方式都是用的兩個字節的數字編號表示,如第一個編號爲0xc02B,這個是在RFC5289進行的規定。安全

這一長串的加密名字表示什麼呢?以TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256爲例,以下圖所示:bash

密鑰交換使用ECDHE算法,服務身份驗證使用RSA算法,數據傳輸加密使用AES(+GCM),握手使用SHA256檢驗。服務器

換句話說,證書籤名使用RSA,若是證書驗證正確,那麼將使用ECDHE算法進行密鑰交換,保證瀏覽器和服務擁有相同的私有密鑰,而後一方使用這把密鑰進行AES數據加密,另外一方使用相同的密鑰進行AES數據解密。驗證證書籤名合法性和密鑰交換的身份確認都是使用SHA256這個哈希算法進行檢驗。具體過程下文展開描述。

接下來,服務端進行Server Hello的響應,包括服務端要使用TLS版本,咱們訪問google.com的時候谷歌返回的版本爲TLS 1.2(0x303,即十進制的771):

若是服務返回的TLS版本爲1.3,那麼Chrome將使用1.3版本。

Server Hello還返回一個32個字節的隨機數server random,和瀏覽器發送的隨機數client random類似,這種隨機數叫作nonce,用於一次性使用,一般會帶有時間戳,在後面生成master key的時候用到。

還會返回一個session id用於下次複用當前握手的信息,避免短期內重複握手。

同時返回所選擇的加密方式,以下圖所示:

谷歌服務器使用了上面舉例的加密方式,根據觀察,這也是不少服務器選擇的方式,這應該是權衡了安全性和計算複雜度的一種比較好的方式。知道了加密方式以後(包括證書是使用RSA簽名的),接下來等收到服務發過來的證書後,讀取證書並檢驗證書的合法性

證書的檢驗是Post一個Task給TaskScheduler線程獨立檢驗的,其它的握手操做都是在Chrome的IO線程進行,應該是考慮到證書的檢驗比較複雜,因此搞成異步的。

證書的檢驗Chrome沒有使用BoringSSL提供的API,而是本身實現的,在src/net/cert目錄。這個過程是這樣的,首先會檢驗是否在黑名單裏,這個黑名單以下源碼的註釋:

// CloudFlare revoked all certificates issued prior to April 2nd, 2014. Thus
  // all certificates where the CN ends with ".cloudflare.com" with a prior
  // issuance date are rejected.
  //
  // The old certs had a lifetime of five years, so this can be removed April
  // 2nd, 2019.複製代碼

大意是說證書的通用名(一般就是證書的域名)是以.cloudflare.com結尾的證書,而且是2014.4.2前簽發的,已經被取消掉了,這些證書有5年的有效期,如今仍然處於有效期,因此須要認爲是無效的。

接着檢驗證書籤名的合法性,在Mac上Chrome是調的系統函數SecTrustEvaluate作的檢驗。檢驗的過程我在《https鏈接的前幾毫秒發生了什麼》已作了詳細介紹,大概來講,先對證書進行SHA256獲得一個哈希值,而後用證書的公鑰對證書的簽名進行解密從中取得另外一個哈希值,若是這兩個哈希值相等,說明證書沒有被篡改過,確實是權威機構頒發。

通常來講,所謂數字簽名,就是對所發送的內容作一個哈希,而後接收方用內容計算一個哈希值,若是這個值等於簽名裏的哈希,就說明內容沒有被第三方篡改過。而這個簽名一般是加密的,在證書裏面,這個簽名是使用證書的私鑰進行加密,任何人均可以拿證書裏提供的公鑰進行解密,可是任何人沒有私鑰沒法正確地加密,由於私鑰和公鑰是一一配對的,若是拿另一把私鑰進行加密,再拿原先的公鑰進行解密一定不是原先的內容。

因此若是簽名檢驗正確,那麼發送的內容即證書是合法的(證書裏面有域名、公鑰等信息)。若是這一步的檢驗不合法,將返回CERT_STATUS_AUTHORITY_INVALID的錯誤。

再接着檢驗證書裏指定的Common Name通用名是否匹配,以下圖所示:

當前訪問的hostname爲www.google.co.kr,而證書裏面的通用名爲*.google.co.kr:

www.google.co.kr包含在通配符*.google.co.kr裏的,因此這個檢驗是經過的。若是不經過瀏覽器將會顯示CERT_STATUS_COMMON_NAME_INVALID的錯誤。

關於這個通配符,有一個小細節,若是通配符是*.com這種頂級域名的那麼認爲是不合法的,只容許私人註冊的域:(這種支持泛域名的證書會比只支持固定域名的貴)

// Do not allow wildcards for public/ICANN registry controlled domains -
// that is, prevent *.com or *.co.uk as valid presented names, but do not
// prevent *.appspot.com (a private registry controlled domain).複製代碼


而後檢測證書是否在公共的黑名單裏面:

若是是的話返回證書被取消的狀態:CERT_STATUS_REVOKED,這些黑名單列表可見blacklist。這些黑名單包括China Internet Network Information Center (CNNIC)等,由於公鑰固定致使不安全的緣由,具體能夠見文檔附上的連接說明。

再接着檢查證書是否使用了弱簽名算法如SHA1/MD5:

若是的的話,返回CERT_STATUS_WEAK_SIGNATURE_ALGORITHM,由於SHA1和MD5都被認爲是不安全的哈希算法,容易被碰撞攻擊(如2017年2月23日,Google宣佈了一個成功的SHA-1碰撞攻擊,發佈了兩分內容不一樣但SHA-1散列值相同的PDF文件做爲概念證實,詳見維基百科)。

緊接着檢驗證書是不是賽門鐵克頒發的:

// Distrust Symantec-issued certificates, as described at
// https://security.googleblog.com/2017/09/chromes-plan-to-distrust-symantec.html複製代碼

若是是Symantec頒發的,將會在Chrome 66版本(2018.4.17發佈穩定版本)取消信任,賽門鐵克是全球幾大證書機構之一,旗下的根證書包括GeoTrust、VeriSign等:

爲何谷歌要取消對它的信任,谷歌的blog是這麼說的:

During the subsequent investigation, it was revealed that Symantec had entrusted several organizations with the ability to issue certificates without the appropriate or necessary oversight, and had been aware of security deficiencies at these organizations for some time.

大意是說通過調查,在沒有被監督的狀況下它隨意委任幾家機構頒發證書。當咱們打開某些網站,控制檯提示:

The SSL certificate used to load resources from https://***.com will be distrusted in M70. Once distrusted, users will be prevented from loading these resources. See https://g.co/chrome/symantecpkicerts for more information.

就是由於它們使用了GeoTrust頒發的證書。

Chrome還會進行其它的檢驗,包括證書的有效期是否過長,以下源碼註釋:

// For certificates issued after 1 July 2012: 60 months.
// For certificates issued after 1 April 2015: 39 months.
// For certificates issued after 1 March 2018: 825 days.複製代碼

還有證書自己的格式是否合法(CERT_STATUS_INVALID)等等。若是是EV加強型證書還有一些特殊的檢驗,有些證書須要使用在線證書狀態協議(OCSP)進行檢驗。

檢驗證書的合法性和握手(HandShake)是同步進行的,由於它是運行在獨立的線程。正常來講在Server Hello以後服務發送證書給瀏覽器進行檢驗,檢驗成功才進行下一步的操做,可能Chrome考慮到檢驗比較耗時,因此弄成異步的。


無論怎麼樣,在Server Hello以後便進行密鑰交換,密鑰交換的目的是爲了雙方共享密鑰,使用同一把密鑰進行加密和解密。密鑰交換的方式有兩種RSA和ECDHE,RSA的方式比較簡單,瀏覽器生成一把密鑰,而後使用證書RSA的公鑰進行加密發給服務端,服務再使用它的密鑰進行解密獲得密鑰,這樣就可以共享密鑰了,它的缺點是攻擊者雖然在發送的過程當中沒法破解,可是若是它保存了全部加密的數據,等到證書到期沒有被維護之類的緣由致使私鑰泄露,那麼它就可使用這把私鑰去解密以前傳送過的全部數據。而使用ECDHE是一種更安全的密鑰交換算法。

ECDHE的全稱叫Elliptic Curve Diffie–Hellman key Exchange橢圓曲線迪非-赫爾曼密鑰交換,它是迪非-赫爾曼密鑰交換的變種,使用橢圓曲線加密提升安全性。

迪非-赫爾曼密鑰交換的過程是這樣的:交換密鑰雙方甲和乙選取一個基數g,例如g = 2,而後甲和乙產生本身的密鑰a和b,甲發送A = g^a和g給乙,乙收到後計算獲得共享密鑰K = A ^ b = g^(ab),同時把B = g ^ b發給甲,這樣甲也能獲得共享密鑰 K = B ^ a = g ^ (ab)。以下圖所示:

因爲a和b一般會很大,作a或b次冪會是一個天文數字,因此須要模以一個大素數p。

竊聽者可以知道g、A、B,可是不知道任何一方的密鑰a或者b,因此他沒法知道共享密鑰K是什麼。爲了保證傳遞的信息不會被人篡改,密鑰交換的數據須要使用證書的RSA進行簽名。更詳細的說明可參見維基百科

經過冪方的計算值傳遞,較容易被破解獲得雙方各自的密鑰,這種的安全係數不是很高,因此引入了曲線橢圓加密ECC。ECC和RSA同樣也能夠看成證書的加密算法,ECC和RSA的共同特色是加密步驟很簡單,可是解密很是困難,RSA的困難之處在於把一個大數拆成兩個素數相乘,而ECC的難點在於找到一個點的係數。不一樣點是ECC的破解難度要遠遠大於RSA,舉例來講2048位的RSA的破解難度至關於224位的ECC,長度越短就意味着CPU計算消耗越少,速度越快。ECC在很高級別的加密場合有較普遍的應用。愈來愈多的證書使用ECC加密,如*.google.com的域名都是使用的EC加密的證書,相對於其它RSA證書2048位的公鑰,EC證書只有256位:

具體來講,所謂橢圓曲線就是指如下方程:

y^3 = x ^ 2 + ax +b

以下圖所示:

上圖由一個起始點P計算2P——先畫一條線與P點相切,與曲線的-2P點相交,作這個點的反射與曲線的交點就是2P,而計算3P就是2P + P,以下圖所示,鏈接P與2P,與曲線的第三個交點就是-3P,反射一下就獲得3P:(任意一條直線與橢圓曲線最多隻有3個交點)

依此類推,4P = 3P + P,鏈接3P與P與曲線的交點的反射就是4P。若是通過n次後最後連線與x軸垂直,說明全部的點已用完,總共有n(或者叫order)個點,在這個計算過程當中會取一個大數p用來作模數,當點的座標值大於p時就模一下,起始點P(x, y)叫Generator點,再加上方程參數的兩個係數ab——{a, b, order, x, y}就構成了一組橢圓曲線的基本參數。

橢圓曲線難以破解的地方在於——給定點P和Q,Q = kP (1 < k < n),想要推導出k是一件很困難的事情(一般n會很大)。

所以使用橢圓曲線加密的密鑰交換過程就變成:

中間人或者竊聽者可以知道Q1和Q2以及方程係數a、b和起始點P,可是它沒法推導出雙方各自的密鑰x、y,所以它沒有辦法計算獲得共享密鑰K = xyP。而且這個破解的難度要遠遠大於使用冪方的方式。這個就是ECDHE。更詳細的信息能夠查看這個視頻教程

在實際的實現裏,基本參數不是在密鑰交換中傳遞的,而是約定的固定的曲線,在調試過程當中,咱們發現Chrome總共支持3種曲線 :

static const uint16_t kDefaultGroups[] = {
    SSL_CURVE_X25519,
    SSL_CURVE_SECP256R1,
    SSL_CURVE_SECP384R1,
};複製代碼

www.google.co.kr使用的是Curve X25519,X25519使用的曲線方程爲:

y^2 = x^3 + 486662x2 + x

而*.google.com使用的是Curve secp256r1,簡稱爲P-256,這個是在Server Key Exchange裏面指定的:

它的參數組是這樣的:

若是轉換成十進制的話:

a = 115792089129476408780076832771566570560534619664239564663761773211729002495996
b = 99593677540221402957765480916910020772520766868399186769503856397241456836063
n = 115792089210356248762697446949407573529996955224135760342422259061068512044369複製代碼

咱們看到n是一個78位的數字,因此暴力破解k(P = kG,1 < k < n)基本上是不可能的。

肯定基本方程後,雙方Q1和Q2值是在Server Key Exchange和Client Key Exchange以公鑰的形式進行交換。

爲了確保密鑰交換不會被篡改,須要進行簽名,若是簽名使用的是RSA的話,那麼方法和驗證證書有效性同樣。若是證書是EC的證書,那麼會使用ECDSA(ecdsa_secp256r1_sha256(0x0403,))進行簽名:

具體驗證的函數是使用的ECDSA_do_verify這個函數,過程說明可參考維基百科,步驟比較多,這裏不深刻討論。EC證書也有公鑰和密鑰,最後驗證合法的標準是使用公鑰解密的簽名裏面的r值若是等於手動計算的值,則說明正確。



接着Client Key Exchange,Chrome根據曲線類型(x25519或P-256)使用相應的參數和算法生成公鑰和密鑰對,如X25519的密鑰是使用隨機數生成的:

有了密鑰再計算配套的公鑰,而後把公鑰保存起來發出去,並計算共享密鑰,最核心的代碼應該是如下幾行:

// Compute the x-coordinate of |peer_key| * |private_key_|.
EC_POINT_mul(group.get(), result.get(), NULL, peer_point.get(),
                  private_key_.get(), bn_ctx.get()複製代碼

使用對方的公鑰peer_key * 本身的私鑰private_key_,獲得K = yQ1.

緊接着用這個共享密鑰通過PRF計算獲得主密鑰master key。咱們能夠把某次握手獲得的密鑰打印出來,以下所示:

鏈接域名:www.google.com
加密方式:TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
曲線名稱:SSL_CURVE_X25519
Peer Public Key (64B):   8b4364a862a7a7f19404973237079b692c1208b8ecf7828d9eae2b76e68e5012
Chrome Public Key (64B): cddd4c2d0c9d49903438a953076fb3baebd38cfa4a3b18144365b67756b4c075
Share Key (78B):         653d6e28202ff88dff92db77c91406b7992a0f15325b0192f17a317e7ff71930404dc7d4857f03
Master Key (96B):        eb584819ae738a45fe9a2e60734d0ae833dfb2d63a1900ee820a36db27a3844e5b6259e2c84e06fd1474c7e1857989ad

鏈接域名:www.baidu.com
加密方式:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
曲線名稱:SSL_CURVE_SECP256R1
Peer Public Key (142B):   04ac277ce63eb420e9e973c96cdf67e37a5956b949af4b053ca5b1b4b1f884b7f6cadbe2d64a91d43a2e280da528d6b6505bc6be10455e70aeabe569562ccc7bdebc7b5df80705
Chrome Public Key (130B): 04941ec80392f0bf13268a9791e7ee673df0a00af6e59335655b0519fbc575bfcb39eabd80f81118dca4906f776c801aee26f8f4fc195917dc94f9c324886bebc4
Share Key (78B):          52d0f6fc4ecd83107fb8c1cc7fa3f978152c0936c58d8d62d6885f7a672cf87c21212121212103
Mater Key (96B):          1e95a25c356a170c6829ec27a0216c50738b758f93606e8503a2e306796fd99db6ec49f65818a125bba6449b07648262複製代碼

密鑰交換以後,雙方已經有了相同的密鑰,而後經過發送Change Cipher Spec通知對方下一個包將會使用以前約定的方式進行加密。因爲傳送數據指定的是GCM加密,它是一種AEAD的加密方式,Chrome會在Change Cipher的過程當中作AEAD的配置,這個加密方式的特色是會給數據添加認證標籤,若是標籤對得上說明數據完整沒有被破壞。

至此整個TLS握手完成,而後就是發送HTTP請求和接收響應數據了。


數據傳送使用的AES加密的特色是使用一把密鑰加密,再使用相同的密鑰就能夠解密,具體加密和解密的過程比較複雜,這裏不深刻研究。不過咱們能夠把加密前和加密後的數據打印出來,以下圖所示:

能夠看到這是一個HTTP請求,加密前的數據有572B,加密後的數據有601B,體積增加了5%。

這個請求收到如下解密後的響應數據:

還有緊接着的gzip壓縮的數據。


至此整個過程就說明完了,本篇重點說了Chrome是怎麼檢驗證書合法性的、Diff-Hellman算法是怎麼樣的、橢圓曲線是怎麼加密,怎樣使用ECDHE進行密鑰交換,等等。本文不少東西沒有講得很深刻,都是點到爲止,看完本篇應該對HTTPS整一個加密的過程有一個輪廓的瞭解,而且對一些加密算法原理有所瞭解。


相關閱讀:

  1. 爲何要把網站升級到HTTPS
  2. https鏈接的前幾毫秒發生了什麼
相關文章
相關標籤/搜索