這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
關於https
背景知識
密碼學的一些基本知識
大致上分為兩類,基於key的密碼編譯演算法與不基於key的密碼編譯演算法。現在的演算法基本都是基於key的,key就以一串隨機數數,更換了key之後,演算法還可以繼續使用。
基於key的密碼編譯演算法又分為兩類,對稱式加密和不對稱式加密,比如DES,AES那種的,通訊雙方一方用key加密之後,另一方用相同的key進行反向的運算就可以解密。
不對稱式加密比較著名的就是RSA,加密的時候有一個公開金鑰和一個私密金鑰,公開金鑰是可以交給對方的,a給b發送資訊,a用自己的私密金鑰加密,b用a的公開金鑰解密,反之,b給a發送資訊,b用自己的私密金鑰加密。
在通訊之前,需要經過一些握手的過程,雙方交換公開金鑰,這個就是key exchange的過程,https最開始的階段就包含了這個key exchange的過程,大概原理是這樣,有些地方還要稍微複雜一些。
數位憑證與CA
數位憑證相當於是伺服器的一個“身份證”,用於唯一標識一個伺服器。一般而言,數位憑證從受信的權威認證授權機構 (Certification Authority,認證授權機構)買來的(免費的很少),瀏覽器裡面一般就內建好了一些權威的CA,在使用https的時候,只要是這些CA簽發的認證,瀏覽器都是可以認證的,要是在與伺服器通訊的時候,收到一個沒有權威CA認證的認證,就會報出提醒不受信任認證的錯誤,就像登入12306一樣,但是也可以選擇接受。
在自己的一些項目中,通常是自己簽發一個ca根憑證,之後這個根憑證簽發一個server.crt,以及server.key給服務端,server.key是服務端的私密金鑰,server.crt包含了服務端的公開金鑰還有服務端的一些身份資訊。在用戶端和服務端通訊的時候(特別是使用代碼編寫的用戶端訪問的時候),要指定ca根憑證,作用就相當於是瀏覽器中內建的那些權威認證一樣,用於進行服務端的身份檢測。
認證的格式:
ca認證在為server.crt認證簽名時候的大致流程參考這個(http://www.tuicool.com/articles/aymYbmM):
數位憑證由兩部分組成:
1、C:認證相關資訊(對象名稱+到期時間+認證發行者+認證簽名演算法….)
2、S:認證的數位簽章 (由CA認證通過密碼編譯演算法產生的)
其中的數位簽章是通過公式S = F(Digest(C))得到的。
Digest為摘要函數,也就是 md5、sha-1或sha256等單向散列演算法,用於將無限輸入值轉換為一個有限長度的“濃縮”輸出值。比如我們常用md5值來驗證下載的大檔案是否完整。大檔案的內容就是一個無限輸入。大檔案被放在網站上用於下載時,網站會對大檔案做一次md5計算,得出一個128bit的值作為大檔案的摘要一同放在網站上。使用者在下載檔案後,對下載後的檔案再進行一次本地的md5計算,用得出的值與網站上的md5值進行比較,如果一致,則大 檔案下載完好,否則下載過程大檔案內容有損壞或源檔案被篡改。這裡還有一個小技巧常常在機器之間copy或者下載壓縮檔的時候也可以用md5sum的命令來進行檢驗,看看檔案是否完整。
F為簽名函數。CA自己的私密金鑰是唯一標識CA簽名的,因此CA用於產生數位憑證的簽名函數一定要以自己的私密金鑰作為一個輸入參數。在RSA加密系統中,發送端的解密函數就是一個以私密金鑰作為參數的函數,因此常常被用作簽名函數使用。因此CA用私密金鑰解密函數作為F,以CA認證中的私密金鑰進行加密,產生最後的數位簽章,正如最後一部分實踐時候給出的認證產生過程,產生server.crt的時候需要ca.crt(包含根憑證的資訊)和ca.key(根憑證的私密金鑰)都加入進去。
接收端接收服務端數位憑證後,如何驗證數位憑證上攜帶的簽名是這個CA的簽名呢?當然接收端首先需要指定對應的CA,接收端會運用下面演算法對數位憑證的簽名進行校正:
F'(S) ?= Digest(C)
接收端進行兩個計算,並將計算結果進行比對:
1、首先通過Digest(C),接收端計算出認證內容(除簽名之外)的摘要,C的內容都是明文可以看到到的。
2、數位憑證攜帶的簽名是CA通過CA祕密金鑰加密摘要後的結果,因此接收端通過一個解密函數F'對S進行“解密”。就像最開始介紹的那樣,在RSA系統中,接收端使用CA公開金鑰(包含在ca.crt中)對S進行“解密”,這恰是CA用私密金鑰對S進行“加密”的逆過程。
將上述兩個運算的結果進行比較,如果一致,說明簽名的確屬於該CA,該認證有效,否則要麼認證不是該CA的,要麼就是中途被人篡改了。
對於self-signed(自簽發)認證來說,接收端並沒有你這個self-CA的數位憑證,也就是沒有CA公開金鑰,也就沒有辦法對數位憑證的簽名進行驗證。因此如果要編寫一個可以對self-signed認證進行校正的接收端程式的話,首先我們要做的就是建立一個屬於自己的CA,用該CA簽發我們的server端認證,之後給用戶端發送資訊的話,需要對這個根憑證進行指定,之後按上面的方式進行驗證。
可以使用openssl x509 -text -in client.crt -noout 查看某個認證檔案所包含的具體資訊。
HTTPS基本過程概述
https協議是在http協議的基礎上組成的secure的協議。主要功能包含一下兩個方面:
1 通訊雙方的身份認證
2 通訊雙方的通訊過程加密
下面通過詳細分析https的通訊過程來解釋這兩個功能。
具體參考這兩個文章:
http://www.fenesky.com/blog/2014/07/19/how-https-works.html
http://www.ruanyifeng.com/blog/2014/02/ssl_tls.html
1、client 發送 sayhello給server端,說明client所支援的加密套件,還有一個隨機數1。
2、server 發送 sayhello給client端,端把server.crt發送給用戶端,server.crt採用還有一個隨機數2。
3、client端產生preMaster key 這個是隨機數3,之後三個隨機數結合在一起產生MasterSecret,之後產生session secret,使用指定的ca進行身份認證,就像之前介紹的那樣,都正常的話,就切換到加密模式。
4、client端使用server.crt中的公開金鑰對preMasterSecret進行加密,如果要進行雙向認證的話,client端會把client.crt一併發送過去,server端接受到資料,解密之後,也有了三個隨機數,採用同樣的方式,三個隨機數產生通訊所使用的session secret。具體session secret的結構可以參考前面列出的兩個部落格。server端完成相關工作之後,會發一個ChangeCipherSpec給client,通知client說明自己已經切換到相關的加解密模式,之後發一段加密資訊給client看是否正常。
5、client端解密正常,之後就可以按照之前的協議,使用session secret進行加密的通訊了。
整體看下,開始的時候建立握手的過程就是身份認證的過程,之後認證完畢之後,就是加密通訊的過程了,https的兩個主要做用就實現了。
相關實踐
比較典型的認證產生的過程:
openssl genrsa -out ca.key 2048#這裡可以使用 -subj 不用進行互動 當然還可以添加更多的資訊openssl req -x509 -new -nodes -key ca.key -subj "/CN=zju.com" -days 5000 -out ca.crtopenssl genrsa -out server.key 2048#這裡的/cn可以是必須添加的 是服務端的網域名稱 或者是etc/hosts中的ip別名openssl req -new -key server.key -subj "/CN=server" -out server.csropenssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 5000
注意產生client端認證的時候,注意要多添加一個欄位,golang的server端認證程式會對這個欄位進行認證:
openssl genrsa -out client.key 2048openssl req -new -key client.key -subj "/CN=client" -out client.csrecho extendedKeyUsage=clientAuth > extfile.cnfopenssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -extfile extfile.cnf -out client.crt -days 5000
https用戶端和服務端單向校正
這部分參考了這個(http://www.tuicool.com/articles/aymYbmM
),裡面代碼部分講得比較細緻。
服務端採用認證,用戶端採用普通方式訪問:
//server端代碼package mainimport ("fmt""net/http""os")func handler(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w,"Hi, This is an example of https service in golang!")}func main() {http.HandleFunc("/", handler)//http.ListenAndServe(":8080", nil)_, err := os.Open("cert_server/server.crt")if err != nil {panic(err)}http.ListenAndServeTLS(":8081", "cert_server/server.crt","cert_server/server.key", nil)}
client端直接發請求,什麼都不加,會報如下錯誤:
2015/07/11 18:13:50 http: TLS handshake error from 10.183.47.203:58042: remote error: bad certificate
使用瀏覽器直接存取的話,之後點擊信賴認證,這個時候就可以正常get到訊息
或者使用curl -k https:// 來經行訪問,相當於忽略了第一步的身分識別驗證的工作。
要是不加-k的話 使用curl -v 參數列印出來詳細的資訊,會看到如下的錯誤:
curl: (60) SSL certificate problem: Invalid certificate chain
說明是認證沒有通過,因為用戶端這面並沒有提供可以信賴的根憑證來對服務端發過來的認證進行驗,/CN使用的直接是ip地址,就會報下面的錯誤:
Get https://10.183.47.206:8081: x509: cannot validate certificate for 10.183.47.206 because it doesn't contain any IP SANs
最好是產生認證的時候使用網域名稱,或者是在/etc/hosts中加上對應的映射。
可以發送請求的用戶端的代碼如下,注意匯入根憑證的方式:
package mainimport (//"io"//"log""crypto/tls""crypto/x509"//"encoding/json""fmt""io/ioutil""net/http"//"strings")func main() {//x509.Certificate.pool := x509.NewCertPool()//caCertPath := "etcdcerts/ca.crt"caCertPath := "certs/cert_server/ca.crt"caCrt, err := ioutil.ReadFile(caCertPath)if err != nil {fmt.Println("ReadFile err:", err)return}pool.AppendCertsFromPEM(caCrt)//pool.AddCert(caCrt)tr := &http.Transport{TLSClientConfig: &tls.Config{RootCAs: pool},DisableCompression: true,}client := &http.Client{Transport: tr}resp, err := client.Get("https://server:8081")if err != nil {panic(err)}body, _ := ioutil.ReadAll(resp.Body)fmt.Println(string(body))fmt.Println(resp.Status)}
使用curl命令的話,就加上--cacrt ca.crt認證,這樣就相當於添加了可信賴的認證,身份認證的操作就可以成功了。
比如產生服務端認證的時候/CN寫的是server 那client發送的時候也發送給https://server:8081就好,不過在本地的/etc/hosts中要加上對應的映射。
用戶端和服務端的雙向校正:
按照之前的方式,用戶端產生認證,根憑證就按之前的那個:
openssl genrsa -out client.key 2048openssl req -new -key client.key -subj "/CN=client" -out client.csropenssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 5000
server端代碼進行改進,添加受信任的根憑證。
// gohttps/6-dual-verify-certs/server.gopackage mainimport ("crypto/tls""crypto/x509""fmt""io/ioutil""net/http")type myhandler struct {}func (h *myhandler) ServeHTTP(w http.ResponseWriter,r *http.Request) {fmt.Fprintf(w,"Hi, This is an example of http service in golang!\n")}func main() {pool := x509.NewCertPool()caCertPath := "cert_server/ca.crt"caCrt, err := ioutil.ReadFile(caCertPath)if err != nil {fmt.Println("ReadFile err:", err)return}pool.AppendCertsFromPEM(caCrt)s := &http.Server{Addr: ":8081",Handler: &myhandler{},TLSConfig: &tls.Config{ClientCAs: pool,ClientAuth: tls.RequireAndVerifyClientCert,},}err = s.ListenAndServeTLS("cert_server/server.crt", "cert_server/server.key")if err != nil {fmt.Println("ListenAndServeTLS err:", err)}}
用戶端代碼改進,發送的時候把指定client端的client.crt以及client.key
// gohttps/6-dual-verify-certs/client.gopackage mainimport ("crypto/tls""crypto/x509""fmt""io/ioutil""net/http")func main() {pool := x509.NewCertPool()caCertPath := "certs/cert_server/ca.crt"caCrt, err := ioutil.ReadFile(caCertPath)if err != nil {fmt.Println("ReadFile err:", err)return}pool.AppendCertsFromPEM(caCrt)cliCrt, err := tls.LoadX509KeyPair("certs/cert_server/client.crt", "certs/cert_server/client.key")if err != nil {fmt.Println("Loadx509keypair err:", err)return}tr := &http.Transport{TLSClientConfig: &tls.Config{RootCAs: pool,Certificates: []tls.Certificate{cliCrt},},}client := &http.Client{Transport: tr}resp, err := client.Get("https://server:8081")if err != nil {fmt.Println("Get error:", err)return}defer resp.Body.Close()body, err := ioutil.ReadAll(resp.Body)fmt.Println(string(body))}
但實際上,這樣是不行的,server端會報這樣的錯誤:
client's certificate's extended key usage doesn't permit it to be used for client authentication
因為client的認證產生方式有一點不一樣,向開始介紹的那樣,goalng對於client端的認證要多一個參數,產生認證的時候,要加上一個單獨的認證資訊:
openssl genrsa -out client.key 2048openssl req -new -key client.key -subj "/CN=client" -out client.csrecho extendedKeyUsage=clientAuth > extfile.cnfopenssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -extfile extfile.cnf -out client.crt -days 5000
就是多添加一個認證檔案的資訊,之後使用新的認證就可以實現雙向認證了,這樣只有那些持有被認證過的認證的用戶端才能向服務端發送請求。
etcd的https的配置
docker 的https配置
k8的 apiserver的https的配置
相關參考
http://www.fenesky.com/blog/2014/07/19/how-https-works.html
http://www.ruanyifeng.com/blog/2014/02/ssl_tls.html
http://www.tuicool.com/articles/aymYbmM