Go代碼打通HTTPs

TL;DR 手工創(chuàng)建CA證書(shū)鏈,手寫(xiě)代碼打通HTTPs的兩端

HTTPs最近是一個(gè)重要的話(huà)題,同時(shí)也是一個(gè)有點(diǎn)難懂的話(huà)題。所以網(wǎng)上有大量的HTTPs/TLS/SSL的教程。關(guān)于這些的原理,這里不做講解,有興趣的可以自行搜索。

本文介紹一個(gè)自己創(chuàng)建證書(shū),并編寫(xiě) Go 代碼實(shí)現(xiàn) client/server 兩端的過(guò)程。從實(shí)踐的角度幫助理解。

構(gòu)建 CA 證書(shū)鏈

我們首先要?jiǎng)?chuàng)建 client/server 使用的證書(shū)。創(chuàng)建證書(shū)的方法有很多種:有不怕麻煩,直接通過(guò) openssl
創(chuàng)建的,有通過(guò) cfssl 創(chuàng)建的。這里要介紹的是我認(rèn)為最簡(jiǎn)單的一種:tls-gen

tls-gen是一個(gè)用 Python 編寫(xiě)的、非常易用的工具。它定義了三種 profile。這里我們選擇最簡(jiǎn)單的一種:一個(gè)根證書(shū)和一組證書(shū)、私鑰對(duì)。

在 shell 里面執(zhí)行一下的命令:

  1. git clone https://github.com/michaelklishin/tls-gen
  2. cd tls-gen/basic
  3. make CN=www.mytestdomain.io

就這樣,我們就為域名 www.mytestdomain.io 創(chuàng)建了一套證書(shū)。觀(guān)察一下當(dāng)前路徑的內(nèi)容,我們會(huì)發(fā)現(xiàn)兩個(gè)新的目錄:testcaserver。前者里面存放了剛剛創(chuàng)建的根證書(shū) (root CA),后者里面存放了我們之后的服務(wù)程序要用的的證書(shū)和私鑰。

testca/
  cacert.pem

server/
  cert.pem
  key.pem

編寫(xiě)服務(wù)

接下來(lái)開(kāi)始寫(xiě)代碼。Go 對(duì) TLS 的支持還是比較完備的,也比較簡(jiǎn)單。以下是服務(wù)器端的代碼 (server.go):

func HelloServer(w http.ResponseWriter, req *http.Request) {
    w.Header().Set("Content-Type", "text/plain")
    w.Write([]byte("This is an example server.\n"))
}

func main() {
    http.HandleFunc("/hello", HelloServer)
    err := http.ListenAndServeTLS(":1443", "server/cert.pem", "server/key.pem", nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

可以看到我們創(chuàng)建了一個(gè) HTTP 服務(wù),這個(gè)服務(wù)監(jiān)聽(tīng) 1443 端口并且只處理一個(gè)路徑 /hello。然后調(diào)用了下面這個(gè)函數(shù)來(lái)監(jiān)聽(tīng) 1443 端口。注意我們給出了之前創(chuàng)建的服務(wù)的證書(shū)和私鑰 - 這樣就保證了HTTP會(huì)用加密的方式來(lái)傳輸。

ListenAndServeTLS(addr, certFile, keyFile string, handler Handler)

運(yùn)行服務(wù)程序:

go run server.go

訪(fǎng)問(wèn)HTTPs服務(wù)

假定我們的服務(wù)程序是運(yùn)行在本地的。我們先改一下 /etc/hosts 來(lái)配置域名解析:

# echo 127.0.0.1 www.mytestdomain.io >> /etc/hosts

我們用以下的代碼 (client.go) 來(lái)訪(fǎng)問(wèn)服務(wù):

func main() {
    client := &http.Client{}
    resp, err := client.Get("https://www.mytestdomain.io:1443/hello")
    if err != nil {
        panic("failed to connect: " + err.Error())
    }
    content, _ := ioutil.ReadAll(resp.Body)
    s := strings.TrimSpace(string(content))

    fmt.Println(s)
}

運(yùn)行 go run client.go,只能得到這樣的錯(cuò)誤:

panic: failed to connect: Get https://www.mytestdomain.io:1443/hello: x509: certificate signed by unknown authorit

這是因?yàn)橄到y(tǒng)不知道如何來(lái)處理這個(gè) self signed 證書(shū)。

各個(gè) OS 添加根證書(shū)的方法是不同的。對(duì)于 Linux 系統(tǒng) (以 Ubuntu 為例) 來(lái)說(shuō),把證書(shū)文件放到相應(yīng)的目錄即可:

# sudo cp testca/cacert.pem /etc/ssl/certs

如果是 macOS,可以用一下的命令:

# sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain  testca/cacert.pem

上面的方法會(huì)把我們手工創(chuàng)建的 root CA
添加到系統(tǒng)所已知的列表里面。這樣一來(lái),所有用該 root CA
創(chuàng)建的證書(shū)都可以被認(rèn)證了。

現(xiàn)在我們?cè)俅芜\(yùn)行剛才那個(gè)程就會(huì)成功的獲得服務(wù)端的響應(yīng)了:

This is an example server.

另一種訪(fǎng)問(wèn)方法

假如只是一個(gè)普通的用戶(hù),沒(méi)有 root/sudo 權(quán)限,不就無(wú)法做上面的操作了嗎?這種情況下還有另外一種做法: 把 root CA 放置在代碼里面。

在上面的 client.go 里面添加這么幾行代碼:

func main() {
    roots := x509.NewCertPool()
    ok := roots.AppendCertsFromPEM([]byte(rootPEM))
    if !ok {
        panic("failed to parse root certificate")
    }

    tr := &http.Transport{
        TLSClientConfig: &tls.Config{RootCAs: roots},
    }

    client := &http.Client{Transport: tr}
    // ...

其中的 rootPEM 就是 testca/cacert.pem 的內(nèi)容

var rootPEM = `
-----BEGIN CERTIFICATE-----
MIIDAjCCAeqgAwIBAgIJAL2faqa73yLvMA0GCSqGSIb3DQEBCwUAMDExIDAeBgNV
BAMMF1RMU0dlblNlbGZTaWduZWR0Um9vdENBMQ0wCwYDVQQHDAQkJCQkMB4XDTE4
MDIwNTA5Mzc0NVoXDTI4MDIwMzA5Mzc0NVowMTEgMB4GA1UEAwwXVExTR2VuU2Vs
ZlNpZ25lZHRSb290Q0ExDTALBgNVBAcMBCQkJCQwggEiMA0GCSqGSIb3DQEBAQUA
A4IBDwAwggEKAoIBAQC9eO6Tam4XFDUbK9FAStAg29teYeKtt8WEJvKGB50xMfXO
2pD0StsXhKrspXBYck0FwKIBsTLr97w7dSqa64z3U2V2BorogFzoEE4JH2sydYGA
QqNAqezGx8VZnQVRyZEBifRPebR4WVD5GtXYe+MnSkHPIgsG0QG0SaiSfMl05dSJ
HoE9T9Kly9fH6yED88++OYjZZRGKOf2THpQlXJjF3iwCDLkwz9Z/kjmpK/rR0SEh
tanf7bOgGs3OoFmX4DvmFJXoriVUC9jcj0Z4oX3Ld81XXyd4FJkpKvdKDhYkqcug
FgERqdBeRDM+MA38YooKHZh0klL2EThNXJxM0r1vAgMBAAGjHTAbMAwGA1UdEwQF
MAMBAf8wCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBCwUAA4IBAQBEqp0ON1A/pCKF
ztfKuzdW+9pauE8dl6Ij3++dt6AqW5QYFLOEFQwoMBOkGChGQDxHkakyaA0DfGe5
JntMH0yYyZnr4kfs+AcY6P+2PfgrgVBqadhR6uAGOBaXDW7dlllqIJJ8NRInA/fT
DYXMxBJbFrcj2cGIYVPvAbrosZ5L/YdAdVM76V8uuk8Hmmy5zRQj+gWt/jDkYWFr
p0b6k3FBXvM7+nhqAIdyMjLioAdYwFpPglGj3xHXS5neWjyUDlAYISNe+PKMERSe
DrptyDE+ljzl77hvvfZD9OPhXbDkAeVU/NaDwHG/G5HDVdNbg/FZ6ueevF34Xuze
jm3lrdJm
-----END CERTIFICATE-----`

也就是說(shuō),我們用準(zhǔn)備好的 root CA 的內(nèi)容產(chǎn)生了一個(gè)新的 http transport。

運(yùn)行一下 go run client.go。成功!

This is an example server.

總結(jié)

一對(duì) HTTPs client/server 程序中需要一個(gè)共同的 root CA。服務(wù)器端需要該 root CA
創(chuàng)建的 CA/私鑰對(duì)。

這里用的是 Go 語(yǔ)言來(lái)實(shí)現(xiàn),其它的語(yǔ)言過(guò)程也類(lèi)似。

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

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

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