https原理以及golang基本實現


關於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.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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM