https原理以及golang基本實現

關於https

背景知識

密碼學的一些基本知識

大體上分爲兩類,基於key的加密算法與不基於key的加密算法。如今的算法基本都是基於key的,key就以一串隨機數數,更換了key以後,算法還能夠繼續使用。html

基於key的加密算法又分爲兩類,對稱加密和不對稱加密,好比DES,AES那種的,通訊雙方一方用key加密以後,另外一方用相同的key進行反向的運算就能夠解密。node

不對稱加密比較著名的就是RSA,加密的時候有一個公鑰和一個私鑰,公鑰是能夠交給對方的,a給b發送信息,a用本身的私鑰加密,b用a的公鑰解密,反之,b給a發送信息,b用本身的私鑰加密。git

在通訊以前,須要通過一些握手的過程,雙方交換公鑰,這個就是key exchange的過程,https最開始的階段就包含了這個key exchange的過程,大概原理是這樣,有些地方還要稍微複雜一些。github

數字證書與CA

數字證書至關因而服務器的一個「身份證」,用於惟一標識一個服務器。通常而言,數字證書從受信的權威證書受權機構 (Certification Authority,證書受權機構)買來的(免費的不多),瀏覽器裏面通常就內置好了一些權威的CA,在使用https的時候,只要是這些CA簽發的證書,瀏覽器都是能夠認證的,要是在與服務器通訊的時候,收到一個沒有權威CA認證的證書,就會報出提醒不受信任證書的錯誤,就像登陸12306同樣,可是也能夠選擇接受。golang

在本身的一些項目中,一般是本身簽發一個ca根證書,以後這個根證書籤發一個server.crt,以及server.key給服務端,server.key是服務端的私鑰,server.crt包含了服務端的公鑰還有服務端的一些身份信息。在客戶端和服務端通訊的時候(特別是使用代碼編寫的客戶端訪問的時候),要指定ca根證書,做用就至關因而瀏覽器中內置的那些權威證書同樣,用於進行服務端的身份檢測。算法

證書的格式:docker

ca證書在爲server.crt證書籤名時候的大體流程參考這個(http://www.tuicool.com/articles/aymYbmM):json

數字證書由兩部分組成:api

一、C:證書相關信息(對象名稱+過時時間+證書發佈者+證書籤名算法….)瀏覽器

二、S:證書的數字簽名 (由CA證書經過加密算法生成的)

其中的數字簽名是經過公式S = F(Digest(C))獲得的。

Digest爲摘要函數,也就是 md五、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)

接收端進行兩個計算,並將計算結果進行比對:

一、首先經過Digest(C),接收端計算出證書內容(除簽名以外)的摘要,C的內容都是明文能夠看到到的。

二、數字證書攜帶的簽名是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

一、client 發送 sayhello給server端,說明client所支持的加密套件,還有一個隨機數1。
二、server 發送 sayhello給client端,端把server.crt發送給客戶端,server.crt採用還有一個隨機數2。
三、client端生成preMaster key 這個是隨機數3,以後三個隨機數結合在一塊兒生成MasterSecret,以後生成session secret,使用指定的ca進行身份認證,就像以前介紹的那樣,都正常的話,就切換到加密模式。
四、client端使用server.crt中的公鑰對preMasterSecret進行加密,若是要進行雙向認證的話,client端會把client.crt一併發送過去,server端接受到數據,解密以後,也有了三個隨機數,採用一樣的方式,三個隨機數生成通訊所使用的session secret。具體session secret的結構能夠參考前面列出的兩個博客。server端完成相關工做以後,會發一個ChangeCipherSpec給client,通知client說明本身已經切換到相關的加解密模式,以後發一段加密信息給client看是否正常。
五、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.crt

openssl genrsa -out server.key 2048

#這裏的/cn能夠是必須添加的 是服務端的域名 或者是etc/hosts中的ip別名
openssl req -new -key server.key -subj "/CN=server" -out server.csr

openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 5000

#查詢證書的狀況
openssl x509 -in ./server.crt -noout -text

注意生成client端證書的時候,注意要多添加一個字段,golang的server端認證程序會對這個字段進行認證:

openssl genrsa -out client.key 2048

openssl req -new -key client.key -subj "/CN=client" -out client.csr

echo extendedKeyUsage=clientAuth > extfile.cnf

openssl 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 main

import (
    "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 main

import (
    //"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中要加上對應的映射。

補充:最近發現k8中的GCE證書生成的時候使用了esyrsa的一個包,貌似能夠處理IPSAN的一些狀況,能夠在生成證書的時候把 ipsan 添加進去。

貌似在ssl生成證書的時候也能夠把ipsan添加進去,好比這幾個文檔(http://blog.csdn.net/linsanhua/article/details/16986701,http://apetec.com/support/GenerateSAN-CSR.htm),可是參數比較多,尚未實驗成功。

主要是設置一個subjectAltname 的字段,能夠再參考這個:(http://wiki.cacert.org/FAQ/subjectAltName),資料中大多都是從一個配置文件中將信息讀入,貌似能夠在-subj的參數中制定?

客戶端和服務端的雙向校驗:

按照以前的方式,客戶端生成證書,根證書就按以前的那個:

openssl genrsa -out client.key 2048

openssl req -new -key client.key -subj "/CN=client" -out client.csr

openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 5000

server端代碼進行改進,添加受信任的根證書。

// gohttps/6-dual-verify-certs/server.go
package main

import (
    "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.go

package main

import (
    "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 2048

openssl req -new -key client.key -subj "/CN=client" -out client.csr

echo extendedKeyUsage=clientAuth > extfile.cnf

openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -extfile extfile.cnf -out client.crt -days 5000

就是多添加一個認證文件的信息,以後使用新的證書就能夠實現雙向認證了,這樣只有那些持有被認證過的證書的客戶端才能向服務端發送請求。

單向https加token的使用方式

在實際操做的過程當中,有的時候可能用到https的方式更多的是但願用到其安全傳輸的特性,身份驗證的地方可能弱一點,好比在服務端放了server.crt以及server.key的證書,客戶端單向使用https發請求的時候,必須還要指定本身的受信根證書,這時候還得把服務端的根證書提早分發給客戶端,比較麻煩,能夠在配置客戶端的Transport的時候,把InsecureSkipVerify參數設置爲true,這樣就不會對服務端的證書進行身份驗證了。服務端對客戶端的驗證能夠經過在Header信息中添加token的參數來進行。可是這種只能用在測試的環境中,因爲客戶端沒有對服務端傳遞過來的請求進行身份驗證,極可能傳遞回來的請求被進行了篡改或者劫持,具體的細節不太清楚,總之仍是有風險的,只適用於某些特殊的場合。

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
這個文章介紹了生成ca文件的一些具體參數:
http://blog.csdn.net/linsanhua/article/details/16878817

相關文章
相關標籤/搜索