1. 程式人生 > >SSL/TLS及證書概述

SSL/TLS及證書概述

每次配置HTTPS或者SSL時,都需要指定一些cacert,cert,key之類的東西,他們的具體作用是什麼呢?為什麼配置了他們之後通訊就安全了呢?怎麼用openssl命令來生成它們呢?程式中應該如何使用這些檔案呢?

本篇以TLS 1.2作為參考,只介紹原理,不深入演算法的細節

SSL和TLS的關係

SSL(Secure Sockets Layer)和TLS(Transport Layer Security)的關係就像windows XP和windows 7的關係,升級後改了個名字而已。下面這張表格列出了它們的歷史:

協議 建立時間 建立者 RFC 註釋
SSL1.0 n/a Netscape n/a 由於有很多安全問題,所以網景公司沒有將它公之於眾
SSL2.0 1995 Netscape n/a 這是第一個被公眾所瞭解的SSL版本
SSL3.0 1996 Netscape rfc6101 由於2.0還是被發現有很多安全問題,Netscape於是設計了3.0,並且IETF將它整理成RFC釋出了出來
TLS1.0 1999 IETF rfc2246 基於SSL 3.0,修改不大,在某些場合也被稱之為SSL 3.1,改名主要是為了和Netscape撇清關係,表示一個新時代的來臨。類似於飯店換老闆了,然後改了個名字,廚師還是原來的
TLS1.1 2006 IETF rfc4346
TLS1.2 2008 IETF rfc5246
TLS1.3 TBD IETF TBD 還在開發過程中,draft

最初的SSL只支援TCP,不過現在已經可以支援UDP了,請參考Datagram Transport Layer Security Version 1.2

HTTPS和TLS的關係

HTTPS=HTTP+TLS,其它的協議也類似,如FTPS=FTP+TLS。

注意:SSH和SSL/TLS是兩個不同的協議,SSH並不依賴於SSL/TLS

加密相關的概念

在正式開始介紹TLS之前,先澄清一些跟加密相關的概念:

對稱加密

這是我們加密檔案常用的方式,加密的時候輸入一個密碼,解密的時候也用這個密碼,加密和解密都用同一個密碼,所以叫對稱加密。常見的演算法有AES、3DES。

非對稱加密

非對稱加密是一個很神奇的東西,它有兩個不一樣的密碼,一個叫私鑰,另一個叫公鑰,用其中一個加密的資料只能用另一個密碼解開,用自己的都解不了,也就是說用公鑰加密的資料只能由私鑰解開,反之亦然。

私鑰一般自己儲存,而公鑰是公開的,同等加密強度下,非對稱加密演算法的速度比不上對稱加密演算法的速度,所以非對稱加密一般用於數字簽名和密碼(對稱加密演算法的密碼)的交換。常見的演算法有RSA、DSA、ECC。

摘要演算法

摘要演算法不是用來加密的,其輸出長度固定,相當於計算資料的指紋,主要用來做資料校驗,驗證資料的完整性和正確性。常見的演算法有CRC、MD5、SHA1、SHA256。

數字簽名

數字簽名就是“非對稱加密+摘要演算法”,其目的不是為了加密,而是用來防止他人篡改資料。

其核心思想是:比如A要給B傳送資料,A先用摘要演算法得到資料的指紋,然後用A的私鑰加密指紋,加密後的指紋就是A的簽名,B收到資料和A的簽名後,也用同樣的摘要演算法計算指紋,然後用A公開的公鑰解密簽名,比較兩個指紋,如果相同,說明資料沒有被篡改,確實是A發過來的資料。假設C想改A發給B的資料來欺騙B,因為篡改資料後指紋會變,要想跟A的簽名裡面的指紋一致,就得改簽名,但由於沒有A的私鑰,所以改不了,如果C用自己的私鑰生成一個新的簽名,B收到資料後用A的公鑰根本就解不開。

TLS握手過程

TLS主要包含兩部分協議,一部分是Record Protocol,描述了資料的格式,另一部分是Handshaking Protocols,描述了握手過程,本篇中只介紹握手過程,不介紹具體的通訊資料格式。

握手的目的有兩個,一個是保證通訊的雙方都是自己期待的對方,任何一方都不可能被冒充,另一個是交換加密密碼,使得只有通訊的雙方知道這個密碼,而別人不知道。前一個就是我們常說的認證,而後一個就是密碼交換。認證是通過證書來達到的,而密碼交換是通過證書裡面的非對稱加密演算法(公私鑰)來實現的。

先看握手的互動圖:
在這裡插入圖片描述

注意: 下面解釋過程中用到的具體協議版本、演算法和值都是示例,實際中可能不是這些

ClientHello
client->server: 
hello,咱建立個連線唄,我這邊的支援的最高版本是TLS1.1,
支援的密碼套件(cipher suite)有“TLS_RSA_WITH_AES_128_CBC_SHA”和“TLS_RSA_WITH_AES_256_CBC_SHA256”,
支援的壓縮演算法有DEFLATE,我這邊生成的隨機串是abc123456。

這裡有幾點需要解釋一下:

  • 客戶端會把自己最喜歡的密碼套件放在最前面,這樣伺服器端就會根據客戶端的要求優先選擇排在前面的演算法套件
  • 密碼套件就是一個密碼演算法三件套,裡面包含了一個非對稱加密演算法,一個對稱加密演算法,以及一個數據摘要演算法。以TLS_RSA_WITH_AES_128_CBC_SHA為例,RSA是非對稱加密演算法,表示後面用到的證書裡面的公鑰用的是RSA演算法,通訊的過程中需要簽名的地方也用這個演算法,並且密碼(key)的交換過程也使用這個演算法;AES_128_CBC是對稱加密演算法,用來加密握手後傳輸的資料,其密碼由RSA負責協商生成;SHA是資料摘要演算法,表示後面交換的證書裡簽名
    用到的摘要演算法是sha1,並且後續通訊過程中需要用到資料校驗的地方也是用的這個演算法。在Record
    Protocol協議中,摘要演算法是必須的,即資料包都需要有校驗碼,而簽名是可選的。
  • ClientHello裡面還可以包含session id,即表示重用前面session裡的一些內容,比如已經協商好的演算法套件等,伺服器收到session id後會去記憶體裡面找,如果這是一個合法的session id,那麼它就可以選擇重用前面的session,這樣可以省去很多握手的過程。為了簡化討論,這裡不介紹session重用的問題。
ServerHello

server收到client的hello訊息後,就在自己載入的證書中去找一個和客戶支援的演算法套件相匹配的證書,並且挑選一個自己也支援的對稱加密演算法(證書裡面只有非對稱加密和摘要演算法,不包含對稱加密演算法)。如果出現下面幾種情況,握手失敗:

  • 客戶端支援的TLS版本太低,比如server要求最低版本為1.2,而客戶端支援的最高版本是1.1
  • 根據客戶端所支援的密碼套件,找不到相應要求的證書
  • 無法就支援的對稱加密演算法達成一致

如果一切都OK,那麼伺服器端將返回ServerHello:

server->client: 
hello,沒問題,我們就使用TLS1.1吧,
演算法採用“TLS_RSA_WITH_AES_256_CBC_SHA256”,這個加密強度更高更安全,
壓縮就算了,我這邊不支援,我這邊生成的隨機數是654321def。

如果server支援session重用的話,這裡還會返回session id

Certificate

伺服器在傳送完ServerHello之後緊接著傳送Certificate訊息,裡面包含自己的證書。

當然這步在有些情況下可以忽略掉,就是非對稱加密演算法選擇使用dh_anon,當然這是特殊的情況,並且也不安全,所以這裡就不展開討論。

server->client: 這是我的證書(身份證),請過目
ServerKeyExchange(可選)

在前面的ServerHello中,雙方已經協商好了密碼套件,對於套件裡面的非對稱加密演算法,有些需要更多的資訊才能生成一個可靠的密碼,而有些不需要,比如RSA,就不需要傳送這個訊息,客戶端自己生成一個準密碼(premaster)就可以了,而有些演算法,比如DHE_RSA,就需要傳送一點特殊的資訊給客戶端,便於它生成premaster。

premaster可以理解為最終密碼的初級版本,有了這個密碼之後,稍微再做一下計算就可以得到最終要使用的對稱加密的密碼

server->client: 這是生成premaster所需要的一些資訊,請查收
CertificateRequest(可選)

只有在需要驗證客戶端的身份的時候才用得著,在大部分情況下,尤其是HTTPS,這一步不需要。比如我們訪問銀行的網站,我們只要保證那確實是銀行的網站就可以了,銀行驗證我們是通過賬號密碼,而不是我們的證書。而U盾就是一個驗證客戶端的例子,銀行給你的U盾裡面有你的證書,你通過U盾訪問銀行的時候,銀行會驗證U盾裡面證書是不是你的,這種情況下,你和銀行之間進行TLS握手的時候,銀行會給你發這個CertificateRequest請求。
你的數字證書有一對,一份在U盾裡的私鑰,一份在銀行的公鑰(其實兩份銀行都有)。U盾的原理很 類似於雙向認證的TLS(SSL) ,或者其它用到RSA的雙向證書驗證手段,以下步驟可能和U盾實際執行的有所區別,但本質相同:

銀行先給你一個"衝擊",它包含了隨機數,以及該隨機數HASH,它們都由公鑰加密,這樣就可以保證只有你能解密這個"衝擊"
你計算該隨機數的HASH,並和用私鑰解出的HASH,兩者相同後,便可確認銀行的身份
接下來,以一個只有你和銀行知道的演算法,利這個隨機數和一些其它資訊,生成"響應"和相應的HASH,再用私鑰加密後發回銀行。(此時銀行也以相同的演算法計算該"響應")
銀行用公鑰解密,並驗證HASH正確,接下來銀行比較兩個"響應"是否相同,相同的話客戶的身份也確認了

至於私鑰的保密性由U盾來完成。U盾的控制晶片被設計為只能寫入證書,不能讀取證書,並且所有利用證書進行的運算都在U盾中進行。所以,只能從U盾讀出運算結果。

server->client: 把你的證書(身份證)也給我看看,我要確認一下你是不是XXX。
ServerHelloDone
server->client: 我要告訴你的就是這麼多了,處理完了給我個回話吧。
Certificate(可選)

如果客戶端在前面收到了伺服器的CertificateRequest請求,那麼將會在這裡給伺服器傳送自己的證書,就算自己沒有證書,也要傳送這個訊息告訴伺服器端自己沒有證書,然後由伺服器端來決定是否繼續。

client->server: 這是我的證書(身份證),請過目
ClientKeyExchange

客戶端驗證完伺服器端的證書後(怎麼驗證證書將在後面介紹),就會生成一個premaster,生成的方式跟採用的密碼交換演算法有關,以TLS_RSA_WITH_AES_128_CBC_SHA為例,其密碼交換演算法是RSA,於是客戶端自己直接生成一個48位元組長度的premaster即可,不需要伺服器發過來的ServerKeyExchange。

client->server: 
這是計算真正密碼要用到的premaster,它是用你證書裡的公鑰加密了的哦,
記得用你的私鑰解密後才能看到哦
CertificateVerify(可選)

如果客戶端給伺服器發了證書,就需要傳送該訊息給伺服器,主要用於驗證證書對應的私鑰確實是在客戶端手裡。

client->server: 
這是一段用我私鑰加密的資料,你用我給你的證書裡的公鑰解密看看,
如果能解開,說明我沒騙你,私鑰確實是在我手裡,
並不是我隨便找了一個別人的證書忽悠你

傳送的訊息裡面都帶有校驗碼,所以解密後計算下校驗碼,能對上說明解密成功

Finished

當前面的過程都沒問題後,伺服器和客戶端都根據得到的資訊計算對稱加密用的密碼,這是RFC裡面給出的計算方法:

master_secret = PRF(pre_master_secret, "master secret",
                          ClientHello.random + ServerHello.random)
                          [0..47];

雖然不太瞭解PRF的細節,但至少客戶端和伺服器端用的演算法和輸入都是一樣的,所以得到的master密碼也是一樣的。這裡pre_master_secret就是ClientKeyExchange裡面客戶端發給伺服器端的premaster,ClientHello.random和ServerHello.random分別是握手開始時雙方傳送的hello請求中的隨機字串。

這裡加入隨機數的原因主要是為了防止重放攻擊,保證每次握手後得到的密碼都是不一樣的

然後雙方將自己快取的握手過程中的資料計算一個校驗碼,並用對稱加密演算法和剛算出來的master密碼加密,發給對方,這一步有兩目的,一個是保證雙方算出來的master密碼都是一樣的,即我這邊加密的資料你那邊能解開;另一個目的是確保我們兩個人的通訊過程中的每一步都沒有被其他人篡改,因為握手的前半部分都是明文,所以有可能被篡改,只要雙方根據各自快取的握手過程的資料算出來的校驗碼是一樣的,說明中間沒人篡改過。

client->server: 這是用我們協商的對稱加密演算法和密碼加密過的握手資料的指紋,看能不能解開,並且和你那邊算出來的指紋是一樣的
server->client: 這是用我們協商的對稱加密演算法和密碼加密過的握手資料的指紋,你也看看能不能解開,並且和你那邊算出來的指紋是一樣的

如果雙方傳送完Finished而對方沒有報錯,握手就完成了,雙發都得到了密碼,並且這個密碼別人不知道,後續的所有資料傳輸過程都會用這個密碼進行加密,加密演算法就是ServerHello裡面協商好的對稱加密演算法。

在上面握手的過程中,一旦有任何一方覺得有問題,都可能隨時終止握手過程

握手不成功常見問題

配置好了之後還是連不上,一般會是下面幾種問題:

  • 版本不一致,有一方的版本太低,另一方為了安全不同意跟它通訊
  • 無法就cipher suite達成一致,有一方支援的加密演算法太弱,安全程度不夠
  • 證書有問題,沒法通過驗證
  • 伺服器端需要驗證客戶端的證書,而客戶端沒有配置

證書相關

開始之前,看看我們常說的那些跟證書相關的概念

基本概念

私鑰

私鑰就是一個演算法名稱加上密碼串,自己儲存,從不給任何人看

公鑰

公鑰也是一個演算法名稱加上密碼串,一般不會單獨給別人,而是嵌在證書裡面一起給別人

CA

專門用自己的私鑰給別人進行簽名的單位或者機構

申請(簽名)檔案

在公鑰的基礎上加上一些申請人的屬性資訊,比如我是誰,來自哪裡,名字叫什麼,證書適用於什麼場景等的資訊,然後帶上進行的簽名,發給CA(私下安全的方式傳送),帶上自己簽名的目的是為了防止別人篡改檔案。

證書檔案

證書由公鑰加上描述資訊,然後經過私鑰簽名之後得到,一般都是一個人的私鑰給另一個人的公鑰簽名,如果是自己的私鑰給自己的公鑰簽名,就叫自簽名。

簽名過程

CA收到申請檔案後,會走核實流程,確保申請人確實是證書中描述的申請人,防止別人冒充申請者申請證書,核實通過後,會用CA的私鑰對申請檔案進行簽名,簽名後的證書包含申請者的基本資訊,CA的基本資訊,證書的使用年限,申請人的公鑰,簽名用到的摘要演算法,CA的簽名。

簽完名之後,證書就可以用了。

證書找誰簽名合適

別人認不認你的證書要看上面籤的是誰的名,所以簽名一定要找權威的人來籤,否則別人不認,哪誰是權威的人呢?那就是CA,哪些CA是受人相信的呢?那就要看軟體的配置,配置相信誰就相信誰,比如瀏覽器裡面預設配置的那些,只要是那些CA簽名的證書,瀏覽器都會相信,而你自己寫的程式,可以由你自己指定信任的CA。

信任一個CA就是說你相信你手上拿到的CA的證書是正確的,這是安全的前提,CA的證書是怎麼到你手裡的,這個不屬於規範的範疇,不管你是U盤拷貝的,還是怎麼弄來得,反正你得確保拿到的CA證書沒問題,比如瀏覽器、作業系統等,安裝好了之後裡面就內建了很多信任的CA的證書。

那麼CA的證書又是誰籤的名呢?一般CA都是分級的,CA的證書都是由上一級的CA來簽名,而最上一級CA的證書是自簽名證書。

證書如何驗證

下面以瀏覽器為例,說明證書的驗證過程:

  1. 在TLS握手的過程中,瀏覽器得到了網站的證書
  2. 開啟證書,檢視是哪個CA簽名的這個證書
  3. 在自己信任的CA庫中,找相應CA的證書
  4. 用CA證書裡面的公鑰解密網站證書上的簽名,取出網站證書的校驗碼(指紋),然後用同樣的演算法(比如sha256)算出出網站證書的校驗碼,如果校驗碼和簽名中的校驗碼對的上,說明這個證書是合法的,且沒被人篡改過
  5. 讀出裡面的CN,對於網站的證書,裡面一般包含的是域名
  6. 檢查裡面的域名和自己訪問網站的域名對不對的上,對的上的話,就說明這個證書確實是頒發給這個網站的
  7. 到此為止檢查通過

如果瀏覽器發現證書有問題,一般是證書裡面的簽名者不是瀏覽器認為值得信任的CA,瀏覽器就會給出警告頁面,這時候需要謹慎,有可能證書被掉包了。如訪問12306網站,由於12306的證書是自己籤的名,並且瀏覽器不認為12306是受信的CA,所以就會給警告,但是一旦你把12306的根證書安裝到了你的瀏覽器中,那麼下次就不會警告了,因為你配置了瀏覽器讓它相信12306是一個受信的CA。

證書生成示例

下面以實際的例子來看看怎麼生成證書。

生成CA的私鑰和證書
#建立一個cert目錄,後續操作都在該目錄下進行
[email protected]:~$ mkdir cert && cd cert

[email protected]:~/cert$ openssl req -newkey rsa:2048 -nodes -sha256 -keyout ca.key -x509 -days 365 -out ca.crt
......
Common Name (e.g. server FQDN or YOUR name) []:ca.com
......
  • -newkey rsa:2048:生成一個長度為2048的採用RSA演算法的私鑰
  • -nodes:這個私鑰在本地儲存的時候不加密(可以通過其它引數來加密私鑰,這樣儲存比較安全)
  • -sha256:生成的證書裡面使用sha256作為摘要演算法
  • -keyout ca.key: 輸出私鑰到key.pem
  • -x509:證書檔案格式為x509,目前TLS預設只支援這種格式的證書
  • -days 365:證書有效期1年
  • -out ca.crt:生成的證書檔案儲存到ca.crt

生成的過程中會要求填一些資訊,除了Common Name要取一個容易區分的名字之外,其它都可以隨便填寫,我們在這裡將它填為ca.com.

生成私鑰和證書籤名申請檔案
[email protected]:~/cert$ openssl req -newkey rsa:2048 -nodes -sha256 -keyout domain.key -new -out domain.csr
......
Common Name (e.g. server FQDN or YOUR name) []:domain.com
......

#這裡將CN設定成domain.com

這裡和上面的區別就是這裡是-new生成一個證書籤名申請檔案,而上面用-x509生成一個自簽名檔案,其它的引數意義都一樣。

從這裡可以看出,CA的私鑰和普通人的私鑰沒什麼區別,唯一的區別就是CA用私鑰自簽名的證書受別人相信,而普通人的自簽名證書別人不信,所以需要CA來給證書籤名。

使用CA的私鑰對申請檔案進行簽名
[email protected]:~/cert$ openssl x509 -CA ca.crt -CAkey ca.key -in domain.csr -req -days 365 -out domain.crt -CAcreateserial -sha256

由於需要往生成的證書裡寫入簽名者的資訊,所以這裡需要ca.crt,因為只有這裡有CA的描述資訊,ca.key裡面只有私鑰的資訊。

檢視證書內容

上面生成的證書檔案格式都是pem格式。通過下面這個命令可以看到證書的內容:

[email protected]:~/cert$ openssl x509 -text -noout -in ca.crt
[email protected]:~/cert$ openssl x509 -text -noout -in domain.crt

程式支援TLS需要哪些檔案

回到最開始的問題,cacert,cert,key對應於上面的哪些東西呢? cacert就是CA的證書,cert就是程式自己的證書,key就是程式自己的私鑰。對於伺服器來說,至少需要有自己的私鑰和證書,而對於客戶端來說,至少需要一個cacert,不然沒法驗證伺服器的證書是否正確。

TLS開發示例

server

伺服器採用python開發,只需要指定server的私鑰和證書就可以了,程式碼如下:

import BaseHTTPServer, SimpleHTTPServer
import ssl

httpd = BaseHTTPServer.HTTPServer(('localhost', 443), SimpleHTTPServer.SimpleHTTPRequestHandler)
httpd.socket = ssl.wrap_socket (httpd.socket, keyfile="./domain.key", certfile='./domain.crt', server_side=True)
httpd.serve_forever()
#監聽443埠需要root許可權
[email protected]:~/cert$ sudo python server.py

client

這裡使用大家都熟悉的curl作為客戶端來測試:

#直接訪問報錯,提示證書驗證失敗,
#那是因為domain.crt是我們自己的CA簽名的,curl根本就不認識,更談不上相信它了
[email protected]:~/cert$ curl https://127.0.0.1
curl: (60) server certificate verification failed. CAfile: /etc/ssl/certs/ca-certificates.crt CRLfile: none
More details here: http://curl.haxx.se/docs/sslcerts.html
......

#引數中顯式的指定我們CA的證書,讓它成為curl信任的CA,這樣curl就認為我們的證書沒問題了
#但curl還是報錯,說這個證書是發給domain.com的,而不是127.0.0.1
[email protected]:~/cert$ curl --cacert ./ca.crt https://127.0.0.1
curl: (51) SSL: certificate subject name (domain.com) does not match target host name '127.0.0.1'

#往/etc/hosts加上一條記錄,設定域名domain.com的ip地址為127.0.0.1
[email protected]:~/cert$ sudo sh -c "echo '127.0.0.1 domain.com' >> /etc/hosts"

#然後通過域名來訪問,得到了伺服器的正確返回
[email protected]:~/cert$ curl --cacert ./ca.crt  https://domain.com
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"><html>
<title>Directory listing for /</title>
<body>
<h2>Directory listing for /</h2>
<hr>
<ul>
<li><a href="ca.crt">ca.crt</a>
<li><a href="ca.key">ca.key</a>
<li><a href="ca.srl">ca.srl</a>
<li><a href="domain.crt">domain.crt</a>
<li><a href="domain.csr">domain.csr</a>
<li><a href="domain.key">domain.key</a>
<li><a href="server.py">server.py</a>
</ul>
<hr>
</body>
</html>

#測試完成之後記得手動將domain.com從/etc/hosts裡面刪掉