Go和HTTPS

Go和HTTPS

<small class="c9" style="box-sizing: border-box; font-size: 12.04px; color: rgb(153, 153, 153);">bigwhite · 2015-05-01 12:11:47 · 44128 次點(diǎn)擊 · 預(yù)計(jì)閱讀時(shí)間 18 分鐘 · 42分鐘之前 開始瀏覽 </small>

這是一個創(chuàng)建于 2015-05-01 12:11:47 的文章,其中的信息可能已經(jīng)有所發(fā)展或是發(fā)生改變。

近期在構(gòu)思一個產(chǎn)品,考慮到安全性的原因,可能需要使用到HTTPS協(xié)議以及雙向數(shù)字證書校驗(yàn)。之前只是粗淺接觸過HTTP(使用Golang開 發(fā)微信系列)。對HTTPS的了解則始于那次自行搭建ngrok服務(wù),在那個過程中照貓畫虎地為服務(wù)端生成了一些私鑰和證書,雖然結(jié)果是好 的:ngrok服務(wù)成功搭建起來了,但對HTTPS、數(shù)字證書等的基本原理并未求甚解。于是想趁這次的機(jī)會,對HTTPS做一些深度挖掘。主要途 徑:翻閱網(wǎng)上資料、書籍,并利用golang編寫一些實(shí)驗(yàn)examples。

一、HTTPS簡介

日常生活中,我們上網(wǎng)用的最多的應(yīng)用層協(xié)議就是HTTP協(xié)議了,直至目前全世界的網(wǎng)站中大多數(shù)依然只支持HTTP訪問。

使用Go創(chuàng)建一個HTTP Server十分Easy,十幾行代碼就能搞定:

//gohttps/1-http/server.go
package main

import (
    "fmt"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w,
     "Hi, This is an example of http service in golang!")
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

執(zhí)行這段代碼:
$ go run server.go

打開瀏覽器,在地址欄輸入"http://localhost:8080", 你會看到“ Hi, This is an example of http service in golang!"輸出到瀏覽器窗口。

不過HTTP畢竟是明文的,在這樣一個不安全的世界里,隨時(shí)存在著竊聽(sniffer工具可以簡單辦到)、篡改甚至是冒充等風(fēng)險(xiǎn),因此對于一些 對安全比較care的站點(diǎn)或服務(wù),它們需要一種安全的HTTP協(xié)議,于是就有了HTTPS。

HTTPS只是我們在瀏覽器地址欄中看到協(xié)議標(biāo)識,實(shí)際上它可以被理解為運(yùn)行在SSL(Secure Sockets Layer)或TLS(Transport Layer Security)協(xié)議所構(gòu)建的安全層之上的HTTP協(xié)議,協(xié)議的傳輸安全性以及內(nèi)容完整性實(shí)際上是由SSL或TLS保證的。

關(guān)于HTTPS協(xié)議原理的詳細(xì)說明,沒有個百八十頁是搞不定的,后續(xù)我會在各個實(shí)驗(yàn)之前將相關(guān)的原理先作一些說明,整體原理這里就不贅述了。有興 趣的朋友可以參考以下資料:
1、《HTTP權(quán)威指南》第十四章
2、《圖解HTTP》第七章
3、阮一峰老師的兩篇博文“SSL/TLS協(xié)議運(yùn)行機(jī)制的概述"和"圖解SSL/TLS協(xié)議"。

二、實(shí)現(xiàn)一個最簡單的HTTPS Web Server

Golang的標(biāo)準(zhǔn)庫net/http提供了https server的基本實(shí)現(xiàn),我們修改兩行代碼就能將上面的HTTP Server改為一個HTTPS Web Server:

// gohttps/2-https/server.go
package main

import (
    "fmt"
    "net/http"
)

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.ListenAndServeTLS(":8081", "server.crt",
                           "server.key", nil)
}

我們用http.ListenAndServeTLS替換掉了http.ListenAndServe,就將一個HTTP Server轉(zhuǎn)換為HTTPS Web Server了。不過ListenAndServeTLS 新增了兩個參數(shù)certFile和keyFile,需要我們傳入兩個文件路徑。到這里,我們不得不再學(xué)習(xí)一點(diǎn)HTTPS協(xié)議的原理了。不過為 了讓這個例子能先Run起來,我們先執(zhí)行下面命令,利用openssl生成server.crt和server.key文件,供程序使用,原 理后續(xù)詳述:

$openssl genrsa -out server.key 2048

Generating RSA private key, 2048 bit long modulus
…………….+++
……………+++
e is 65537 (0×10001)

$openssl req -new -x509 -key server.key -out server.crt -days 365

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
—–
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:<u style="box-sizing: border-box;">**localhost**</u>
Email Address []:

執(zhí)行程序:go run server.go
通過瀏覽器訪問:https://localhost:8081,chrome瀏覽器會顯示如下畫面:

image.png

[圖片上傳失敗...(image-18122e-1634696815533)]

忽略繼續(xù)后,才能看到"Hi, This is an example of https service in golang!"這個結(jié)果輸出在窗口上。

也可以使用curl工具驗(yàn)證這個HTTPS server:

curl -k [https://localhost:8081](https://localhost:8081/)
Hi, This is an example of http service in golang!

注意如果不加-k,curl會報(bào)如下錯誤:

$curl [https://localhost:8081](https://localhost:8081/)
curl: (60) SSL certificate problem: Invalid certificate chain
More details here: [http://curl.haxx.se/docs/sslcerts.html](http://curl.haxx.se/docs/sslcerts.html)

curl performs SSL certificate verification by default, using a "bundle"
of Certificate Authority (CA) public keys (CA certs). If the default
bundle file isn't adequate, you can specify an alternate file
using the –cacert option.
If this HTTPS server uses a certificate signed by a CA represented in
the bundle, the certificate verification probably failed due to a
problem with the certificate (it might be expired, or the name might
not match the domain name in the URL).
If you'd like to turn off curl's verification of the certificate, use
the -k (or –insecure) option.

三、非對稱加密和數(shù)字證書

前面說過,HTTPS的數(shù)據(jù)傳輸是加密的。實(shí)際使用中,HTTPS利用的是對稱與非對稱加密算法結(jié)合的方式。

對稱加密,就是通信雙方使用一個密鑰,該密鑰既用于數(shù)據(jù)加密(發(fā)送方),也用于數(shù)據(jù)解密(接收方)。
非對稱加密,使用兩個密鑰。發(fā)送方使用公鑰(公開密鑰)對數(shù)據(jù)進(jìn)行加密,數(shù)據(jù)接收方使用私鑰對數(shù)據(jù)進(jìn)行解密。

實(shí)際操作中,單純使用對稱加密或單純使用非對稱加密都會存在一些問題,比如對稱加密的密鑰管理復(fù)雜;非對稱加密的處理性能低、資源占用高等,因 此HTTPS結(jié)合了這兩種方式。

HTTPS服務(wù)端在連接建立過程(ssl shaking握手協(xié)議)中,會將自身的公鑰發(fā)送給客戶端??蛻舳四玫焦€后,與服務(wù)端協(xié)商數(shù)據(jù)傳輸通道的對稱加密密鑰-對話密鑰,隨后的這個協(xié)商過程則 是基于非對稱加密的(因?yàn)檫@時(shí)客戶端已經(jīng)拿到了公鑰,而服務(wù)端有私鑰)。一旦雙方協(xié)商出對話密鑰,則后續(xù)的數(shù)據(jù)通訊就會一直使用基于該對話密 鑰的對稱加密算法了。

上述過程有一個問題,那就是雙方握手過程中,如何保障HTTPS服務(wù)端發(fā)送給客戶端的公鑰信息沒有被篡改呢?實(shí)際應(yīng)用中,HTTPS并非直接 傳輸公鑰信息,而是使用攜帶公鑰信息的數(shù)字證書來保證公鑰的安全性和完整性。

數(shù)字證書,又稱互聯(lián)網(wǎng)上的"身份證",用于唯一標(biāo)識一個組織或一個服務(wù)器的,這就好比我們?nèi)粘I钪惺褂玫?居民身份證",用于唯一標(biāo)識一個 人。服務(wù)端將數(shù)字證書傳輸給客戶端,客戶端如何校驗(yàn)這個證書的真?zhèn)文??我們知道居民身份證是由國家統(tǒng)一制作和頒發(fā)的,個人向戶 口所在地公安機(jī)關(guān)申請,國家頒發(fā)的身份證才具有法律 效力,任何地方這個身份證都是有效和可被接納的。大悅城的會員卡也是一種身份標(biāo)識,但你若用大悅城的會員卡去買機(jī)票,對不起, 不賣。航空公司可不認(rèn)大悅城的會員卡,只認(rèn)居民身份證。網(wǎng)站的證書也是同樣的道理。一般來說數(shù)字證書從受信的權(quán)威證書授權(quán)機(jī)構(gòu) (Certification Authority,證書授權(quán)機(jī)構(gòu))買來的(免費(fèi)的很少)。一般瀏覽器在出廠時(shí)就內(nèi)置了諸多知名CA(如Verisign、GoDaddy、美國國防部、 CNNIC等)的數(shù)字證書校驗(yàn)方法,只要是這些CA機(jī)構(gòu)頒發(fā)的證書,瀏覽器都能校驗(yàn)。對于CA未知的證書,瀏覽器則會報(bào)錯(就像上面那個截圖一 樣)。主流瀏覽器都有證書管理功能,但鑒于這些功能比較高級,一般用戶是不用去關(guān)心的。

初步原理先講到這,我們再回到上面的例子。

四、服務(wù)端私鑰與證書

接上面的例子,我們來說說服務(wù)端私鑰與證書的生成。

go的http.ListenAndServeTLS需要兩個特別參數(shù),一個是服務(wù)端的私鑰 文件路徑,另外一個是服務(wù)端的數(shù)字證書文件路徑。在測試環(huán)境,我們沒有必要花錢去購買什么證書,利用openssl工具,我們可以自己生成相 關(guān)私鑰和自簽發(fā)的數(shù)字證書。

openssl genrsa -out server.key 2048 用于生成服務(wù)端私鑰文件server.key,后面的參數(shù)2048單位是bit,是私鑰的長度。
openssl生成的私鑰中包含了公鑰的信息,我們可以根據(jù)私鑰生成公鑰:

$openssl rsa -in server.key -out server.key.public

我們也可以根據(jù)私鑰直接生成自簽發(fā)的數(shù)字證書:

$openssl req -new -x509 -key server.key -out server.crt -days 365

server.key和server.crt將作為ListenAndServeTLS的兩個輸入?yún)?shù)。

我們編寫一個Go程序來嘗試與這個HTTPS server建立連接并通信。

//gohttps/4-https/client1.go
package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
)

func main() {
    resp, err := http.Get(["https://localhost:8081"](https://localhost:8081/))
    if err != nil {
        fmt.Println("error:", err)
        return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    fmt.Println(string(body))
}

運(yùn)行這個client,我們得到如下錯誤:

$go run client1.go
error: Get [https://localhost:8081](https://localhost:8081/): x509: certificate signed by unknown authority

此時(shí)服務(wù)端也給出了錯誤日志提示:
2015/04/30 16:03:31 http: TLS handshake error from 127.0.0.1:62004: remote error: bad certificate

顯然從客戶端日志來看,go實(shí)現(xiàn)的Client端默認(rèn)也是要對服務(wù)端傳過來的數(shù)字證書進(jìn)行校驗(yàn)的,但客戶端提示:這個證書是由不知名CA簽發(fā) 的!

我們可以修改一下client1.go的代碼,讓client端略過對證書的校驗(yàn):

//gohttps/4-https/client2.go
package main

import (
    "crypto/tls"
    "fmt"
    "io/ioutil"
    "net/http"
)

func main() {
    tr := &http.Transport{
        **TLSClientConfig:    &tls.Config{InsecureSkipVerify: true},**
    }
    client := &http.Client{Transport: tr}
    resp, err := client.Get(["https://localhost:8081"](https://localhost:8081/))

    if err != nil {
        fmt.Println("error:", err)
        return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    fmt.Println(string(body))
}

通過設(shè)置tls.Config的InsecureSkipVerify為true,client將不再對服務(wù)端的證書進(jìn)行校驗(yàn)。執(zhí)行后的結(jié)果 也證實(shí)了這一點(diǎn):

$go run client2.go
Hi, This is an example of http service in golang!

五、對服務(wù)端的證書進(jìn)行校驗(yàn)

多數(shù)時(shí)候,我們需要對服務(wù)端的證書進(jìn)行校驗(yàn),而不是像上面client2.go那樣忽略這個校驗(yàn)。我大腦中的這個產(chǎn)品需要服務(wù)端和客戶端雙向 校驗(yàn),我們先來看看如何能讓client端實(shí)現(xiàn)對Server端證書的校驗(yàn)?zāi)兀?/p>

client端校驗(yàn)證書的原理是什么呢?回想前面我們提到的瀏覽器內(nèi)置了知名CA的相關(guān)信息,用來校驗(yàn)服務(wù)端發(fā)送過來的數(shù)字證書。那么瀏覽器 存儲的到底是CA的什么信息呢?其實(shí)是CA自身的數(shù)字證書(包含CA自己的公鑰)。而且為了保證CA證書的真實(shí)性,瀏覽器是在出廠時(shí)就內(nèi)置了 這些CA證書的,而不是后期通過通信的方式獲取的。CA證書就是用來校驗(yàn)由該CA頒發(fā)的數(shù)字證書的。

那么如何使用CA證書校驗(yàn)Server證書的呢?這就涉及到數(shù)字證書到底是什么了!

我們可以通過瀏覽器中的"https/ssl證書管理"來查看證書的內(nèi)容,一般服務(wù)器證書都會包含諸如站點(diǎn)的名稱和主機(jī)名、公鑰、簽發(fā)機(jī)構(gòu) (CA)名稱和來自簽發(fā)機(jī)構(gòu)的簽名等。我們重點(diǎn)關(guān)注這個來自簽發(fā)機(jī)構(gòu)的簽名,因?yàn)閷τ谧C書的校驗(yàn),就是使用客戶端CA證書來驗(yàn)證服務(wù)端證書的簽名是否這 個CA簽的。

通過簽名驗(yàn)證我們可以來確認(rèn)兩件事:
1、服務(wù)端傳來的數(shù)字證書是由某個特定CA簽發(fā)的(如果是self-signed,也無妨),數(shù)字證書中的簽名類似于日常生活中的簽名,首先 驗(yàn)證這個簽名簽的是Tony Bai,而不是Tom Bai, Tony Blair等。
2、服務(wù)端傳來的數(shù)字證書沒有被中途篡改過。這類似于"Tony Bai"有無數(shù)種寫法,這里驗(yàn)證必須是我自己的那種寫法,而不是張三、李四寫的"Tony Bai"。

一旦簽名驗(yàn)證通過,我們因?yàn)樾湃芜@個CA,從而信任這個服務(wù)端證書。由此也可以看出,CA機(jī)構(gòu)的最大資本就是其信用度。

CA在為客戶簽發(fā)數(shù)字證書時(shí)是這樣在證書上簽名的:

數(shù)字證書由兩部分組成:
1、C:證書相關(guān)信息(對象名稱+過期時(shí)間+證書發(fā)布者+證書簽名算法….)
2、S:證書的數(shù)字簽名

其中的數(shù)字簽名是通過公式S = F(Digest(C))得到的。

Digest為摘要函數(shù),也就是 md5、sha-1或sha256等單向散列算法,用于將無限輸入值轉(zhuǎn)換為一個有限長度的“濃縮”輸出值。比如我們常用md5值來驗(yàn)證下載的大文件是否完 整。大文件的內(nèi)容就是一個無限輸入。大文件被放在網(wǎng)站上用于下載時(shí),網(wǎng)站會對大文件做一次md5計(jì)算,得出一個128bit的值作為大文件的 摘要一同放在網(wǎng)站上。用戶在下載文件后,對下載后的文件再進(jìn)行一次本地的md5計(jì)算,用得出的值與網(wǎng)站上的md5值進(jìn)行比較,如果一致,則大 文件下載完好,否則下載過程大文件內(nèi)容有損壞或源文件被篡改。

F為簽名函數(shù)。CA自己的私鑰是唯一標(biāo)識CA簽名的,因此CA用于生成數(shù)字證書的簽名函數(shù)一定要以自己的私鑰作為一個輸入?yún)?shù)。在RSA加密 系統(tǒng)中,發(fā)送端的解密函數(shù)就是一個以私鑰作 為參數(shù)的函數(shù),因此常常被用作簽名函數(shù)使用。簽名算法是與證書一并發(fā)送給接收 端的,比如apple的一個服務(wù)的證書中關(guān)于簽名算法的描述是“帶 RSA 加密的 SHA-256 ( 1.2.840.113549.1.1.11 )”。因此CA用私鑰解密函數(shù)作為F,對C的摘要進(jìn)行運(yùn)算得到了客戶數(shù)字證書的簽名,好比大學(xué)畢業(yè)證上的校長簽名,所有畢業(yè)證都是校長簽發(fā)的。

接收端接收服務(wù)端數(shù)字證書后,如何驗(yàn)證數(shù)字證書上攜帶的簽名是這個CA的簽名呢?接收端會運(yùn)用下面算法對數(shù)字證書的簽名進(jìn)行校驗(yàn):
F'(S) ?= Digest(C)

接收端進(jìn)行兩個計(jì)算,并將計(jì)算結(jié)果進(jìn)行比對:
1、首先通過Digest(C),接收端計(jì)算出證書內(nèi)容(除簽名之外)的摘要。
2、數(shù)字證書攜帶的簽名是CA通過CA密鑰加密摘要后的結(jié)果,因此接收端通過一個解密函數(shù)F'對S進(jìn)行“解密”。RSA系統(tǒng)中,接收端使用 CA公鑰對S進(jìn)行“解密”,這恰是CA用私鑰對S進(jìn)行“加密”的逆過程。

將上述兩個運(yùn)算的結(jié)果進(jìn)行比較,如果一致,說明簽名的確屬于該CA,該證書有效,否則要么證書不是該CA的,要么就是中途被人篡改了。

但對于self-signed(自簽發(fā))證書來說,接收端并沒有你這個self-CA的數(shù)字證書,也就是沒有CA公鑰,也就沒有辦法對數(shù)字證 書的簽名進(jìn)行驗(yàn)證。因此如果要編寫一個可以對self-signed證書進(jìn)行校驗(yàn)的接收端程序的話,首先我們要做的就是建立一個屬于自己的 CA,用該CA簽發(fā)我們的server端證書,并將該CA自身的數(shù)字證書隨客戶端一并發(fā)布。

這讓我想起了在《搭建自己的ngrok服務(wù)》一文中為ngrok服務(wù)端、客戶端生成證書的那幾個步驟,我們來重溫并分析一下每一步都在做什么。

(1)openssl genrsa -out rootCA.key 2048
(2)openssl req -x509 -new -nodes -key rootCA.key -subj "/CN=*.tunnel.tonybai.com" -days 5000 -out rootCA.pem

(3)openssl genrsa -out device.key 2048
(4)openssl req -new -key device.key -subj "/CN=*.tunnel.tonybai.com" -out device.csr
(5)openssl x509 -req -in device.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out device.crt -days 5000

(6)cp rootCA.pem assets/client/tls/ngrokroot.crt
(7)cp device.crt assets/server/tls/snakeoil.crt
(8)cp device.key assets/server/tls/snakeoil.key

自己搭建ngrok服務(wù),客戶端要驗(yàn)證服務(wù)端證書,我們需要自己做CA,因此步驟(1)和步驟(2)就是生成CA自己的相關(guān)信息。
步驟(1) ,生成CA自己的私鑰 rootCA.key
步驟(2),根據(jù)CA自己的私鑰生成自簽發(fā)的數(shù)字證書,該證書里包含CA自己的公鑰。

步驟(3)~(5)是用來生成ngrok服務(wù)端的私鑰和數(shù)字證書(由自CA簽發(fā))。
步驟(3),生成ngrok服務(wù)端私鑰。
步驟(4),生成Certificate Sign Request,CSR,證書簽名請求。
步驟(5),自CA用自己的CA私鑰對服務(wù)端提交的csr進(jìn)行簽名處理,得到服務(wù)端的數(shù)字證書device.crt。

步驟(6),將自CA的數(shù)字證書同客戶端一并發(fā)布,用于客戶端對服務(wù)端的數(shù)字證書進(jìn)行校驗(yàn)。
步驟(7)和步驟(8),將服務(wù)端的數(shù)字證書和私鑰同服務(wù)端一并發(fā)布。

接下來我們來驗(yàn)證一下客戶端對服務(wù)端數(shù)字證書進(jìn)行驗(yàn)證(gohttps/5-verify-server-cert)!

首先我們來建立我們自己的CA,需要生成一個CA私鑰和一個CA的數(shù)字證書:

$openssl genrsa -out ca.key 2048
Generating RSA private key, 2048 bit long modulus
……….+++
………………………….+++
e is 65537 (0×10001)

$openssl req -x509 -new -nodes -key ca.key -subj "/CN=tonybai.com" -days 5000 -out ca.crt

接下來,生成server端的私鑰,生成數(shù)字證書請求,并用我們的ca私鑰簽發(fā)server的數(shù)字證書:

openssl genrsa -out server.key 2048
Generating RSA private key, 2048 bit long modulus
….+++
…………………….+++
e is 65537 (0×10001)

$openssl req -new -key server.key -subj "/CN=localhost" -out server.csr

$openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 5000
Signature ok
subject=/CN=localhost
Getting CA Private Key

現(xiàn)在我們的工作目錄下有如下一些私鑰和證書文件:
CA:
私鑰文件 ca.key
數(shù)字證書 ca.crt

Server:
私鑰文件 server.key
數(shù)字證書 server.crt

接下來,我們就來完成我們的程序。

Server端的程序幾乎沒有變化:

// gohttps/5-verify-server-cert/server.go
package main

import (
    "fmt"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w,
        "Hi, This is an example of http service in golang!")
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServeTLS(":8081",
        "server.crt", "server.key", nil)
}

client端程序變化較大,由于client端需要驗(yàn)證server端的數(shù)字證書,因此client端需要預(yù)先加載ca.crt,以用于服務(wù)端數(shù)字證書的校驗(yàn):

// gohttps/5-verify-server-cert/client.go
package main

import (
    "crypto/tls"
    "crypto/x509"
    "fmt"
    "io/ioutil"
    "net/http"
)

func main() {
    pool := x509.NewCertPool()
    caCertPath := "ca.crt"

    caCrt, err := ioutil.ReadFile(caCertPath)
    if err != nil {
        fmt.Println("ReadFile err:", err)
        return
    }
    pool.AppendCertsFromPEM(caCrt)

    tr := &http.Transport{
        TLSClientConfig: &tls.Config{RootCAs: pool},
    }
    client := &http.Client{Transport: tr}
    resp, err := client.Get("https://localhost:8081")
    if err != nil {
        fmt.Println("Get error:", err)
        return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    fmt.Println(string(body))
}

運(yùn)行server和client:

$go run server.go

go run client.go
Hi, This is an example of http service in golang!

六、對客戶端的證書進(jìn)行校驗(yàn)(雙向證書校驗(yàn))

服務(wù)端可以要求對客戶端的證書進(jìn)行校驗(yàn),以更嚴(yán)格識別客戶端的身份,限制客戶端的訪問。

要對客戶端數(shù)字證書進(jìn)行校驗(yàn),首先客戶端需要先有自己的證書。我們以上面的例子為基礎(chǔ),生成客戶端的私鑰與證書。

$openssl genrsa -out client.key 2048
Generating RSA private key, 2048 bit long modulus
………………..+++
………………..+++
e is 65537 (0×10001)
$openssl req -new -key client.key -subj "/CN=tonybai_cn" -out client.csr
$openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 5000
Signature ok
subject=/CN=tonybai_cn
Getting CA Private Key

接下來我們來改造我們的程序,首先是server端。

首先server端需要要求校驗(yàn)client端的數(shù)字證書,并且加載用于校驗(yàn)數(shù)字證書的ca.crt,因此我們需要對server進(jìn)行更加靈活的控制:

// 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 := "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("server.crt", "server.key")
    if err != nil {
        fmt.Println("ListenAndServeTLS err:", err)
    }
}

可以看出代碼通過將tls.Config.ClientAuth賦值為tls.RequireAndVerifyClientCert來實(shí)現(xiàn)Server強(qiáng)制校驗(yàn)client端證書。ClientCAs是用來校驗(yàn)客戶端證書的ca certificate。

Client端變化也很大,需要加載client.key和client.crt用于server端連接時(shí)的證書校驗(yàn):

// 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 := "ca.crt"

    caCrt, err := ioutil.ReadFile(caCertPath)
    if err != nil {
        fmt.Println("ReadFile err:", err)
        return
    }
    pool.AppendCertsFromPEM(caCrt)

    cliCrt, err := tls.LoadX509KeyPair("client.crt", "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://localhost:8081")
    if err != nil {
        fmt.Println("Get error:", err)
        return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    fmt.Println(string(body))
}

好了,讓我們來試著運(yùn)行一下這兩個程序,結(jié)果如下:

$go run server.go
2015/04/30 22:13:33 http: TLS handshake error from 127.0.0.1:53542:
tls: client's certificate's extended key usage doesn't permit it to be
used for client authentication

$go run client.go
Get error: Get https://localhost:8081: remote error: handshake failure

失敗了!從server端的錯誤日志來看,似乎是client端的client.crt文件不滿足某些條件。

根據(jù)server端的錯誤日志,搜索了Golang的源碼,發(fā)現(xiàn)錯誤出自crypto/tls/handshake_server.go。

k := false
for _, ku := range certs[0].ExtKeyUsage {
    if ku == x509.ExtKeyUsageClientAuth {
        ok = true
        break
    }
}
if !ok {
    c.sendAlert(alertHandshakeFailure)
    return nil, errors.New("tls: client's certificate's extended key usage doesn't permit it to be used for client authentication")
}

大致判斷是證書中的ExtKeyUsage信息應(yīng)該包含clientAuth。翻看openssl的相關(guān)資料,了解到自CA簽名的數(shù)字證書中包含的都是一些basic的信息,根本沒有ExtKeyUsage的信息。我們可以用命令來查看一下當(dāng)前client.crt的內(nèi)容:

$ openssl x509 -text -in client.crt -noout
Certificate:
    Data:
        Version: 1 (0×0)
        Serial Number:
            d6:e3:f6:fa:ae:65:ed:df
        Signature Algorithm: sha1WithRSAEncryption
        Issuer: CN=tonybai.com
        Validity
            Not Before: Apr 30 14:11:34 2015 GMT
            Not After : Jan  6 14:11:34 2029 GMT
        Subject: CN=tonybai_cn
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
            RSA Public Key: (2048 bit)
                Modulus (2048 bit):
                    00:e4:12:22:50:75:ae:b2:8a:9e:56:d5:f3:7d:31:
                    7b:aa:75:5d:3f:90:05:4e:ff:ed:9a:0a:2a:75:15:
                    … …
                Exponent: 65537 (0×10001)
    Signature Algorithm: sha1WithRSAEncryption
        76:3b:31:3e:9d:b0:66:ad:c0:03:d4:19:c6:f2:1a:52:91:d6:
        13:31:3a:c5:d5:58:ea:42:1d:b7:33:b8:43:a8:a8:28:91:ac:
         … …

而偏偏golang的tls又要校驗(yàn)ExtKeyUsage,如此我們需要重新生成client.crt,并在生成時(shí)指定extKeyUsage。經(jīng)過摸索,可以用如下方法重新生成client.crt:

1、創(chuàng)建文件client.ext
內(nèi)容:
extendedKeyUsage=clientAuth

2、重建client.crt

$openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial **-extfile client.ext** -out client.crt -days 5000
Signature ok
subject=/CN=tonybai_cn
Getting CA Private Key

再通過命令查看一下新client.crt:

看到輸出的文本中多了這么幾行:
X509v3 extensions:
X509v3 Extended Key Usage:
TLS Web Client Authentication

這說明client.crt的extended key usage已經(jīng)添加成功了。我們再來執(zhí)行一下server和client:

$ go run client.go
Hi, This is an example of http service in golang!

client端證書驗(yàn)證成功,也就是說雙向證書驗(yàn)證均ok了。

七、小結(jié)

通過上面的例子可以看出,使用golang開發(fā)https相關(guān)程序十分便利,Golang標(biāo)準(zhǔn)庫已經(jīng)實(shí)現(xiàn)了TLS 1.2版本協(xié)議。上述所有example代碼均放在我的github上的experiments/gohttps中。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容