TLS with Go

TLS with Go

原文見:https://ericchiang.github.io/post/go-tls/

雖然之前也接觸一些 openssl 的編程,但是對證書頒發,證書鏈的一些細節依然有些似懂非懂。完成這篇文章之後解決了之前的很多困惑,果然實踐動手是加固理論的最好方式。

下面的實踐是基於Go的TLS,但是搞懂之後別的語言也一樣的。

在閱讀本文之前,需要先了解以下知識:
數字簽名
證書鏈

公私鑰的加解密

先從最基本的公鑰和私鑰的加密開始,由私鑰加密的數據,只有公鑰可以解開。go 的 crypto/rsa 包裏直接提供了生成公鑰和私鑰的方法。

以下是生成一對公鑰和私鑰,其中公鑰存在 privKey.PublicKey 裏

// create a public/private keypair
// NOTE: Use crypto/rand not math/rand
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
    log.Fatalf("generating random key: %v", err)
}

接下來,我們使用生成的公鑰對一些數據進行加密

plainText := []byte("hello world")
// use the public key to encrypt the message
cipherText, err := rsa.EncryptPKCS1v15(rand.Reader, &privKey.PublicKey, plainText)
if err != nil {
    log.Fatalf("could not encrypt data: %v", err)
}
log.Printf("%s\n", strconv.Quote(string(cipherText)))

觀察輸出,加密後的數據已經是不可讀的了。接下來,用私鑰對加密數據進行解密

decryptedText, err := rsa.DecryptPKCS1v15(nil, privKey, cipherText)
if err != nil {
    log.Fatalf("error decrypting cipher text: %v", err)
}
log.Printf("%s\n", decryptedText)

可以看到,數據被正常的解密之後,就可以看到了明文了。

這是網絡中數據安全傳輸最簡單也最基礎的一步,通過加密數據,我們可以防止傳輸過程中數據明文被人獲取。

數字簽名

公私鑰除了加解密之外的另一個應用是,對給定的信息創建一個數字簽名。這些簽名可以保證被簽名文件的有效性,也就是沒有被修改過。

具體的做法,首先對傳輸的信息進行hash運算(這裏使用 SHA256),然後用私鑰在hash的結果上生成簽名。

//對明文進行hash運算
hash := sha256.Sum256(plainText)
fmt.Printf("The hash of my message is: %#x\n", hash)
// The hash of my message is: 0xe6a8502561b8e2328b856b4dbe6a9448d2bf76f02b7820e5d5d4907ed2e6db80

//用私鑰在 hash 結果上生成簽名
signature, err := rsa.SignPKCS1v15(rand.Reader, privKey, crypto.SHA256, hash[:])
if err != nil {
    log.Fatalf("error creating signature: %v", err)
}

接下來,使用公鑰對信息以及該信息對應的簽名進行認證,來確認信息沒有被僞造被修改。可以看到前2種情況,由於信息和簽名都對不上,認證失敗。

//用上文生成的公鑰對明文和簽名進行認證,以確認傳輸過程中信息沒有被修改
verify := func(pub *rsa.PublicKey, msg, signature []byte) error {
    hash := sha256.Sum256(msg)
    return rsa.VerifyPKCS1v15(pub, crypto.SHA256, hash[:], signature)
}

fmt.Println(verify(&privKey.PublicKey, plainText, []byte("a bad signature")))
// crypto/rsa: verification error
fmt.Println(verify(&privKey.PublicKey, []byte("a different plain text"), signature))
// crypto/rsa: verification error
fmt.Println(verify(&privKey.PublicKey, plainText, signature))
// <nil>

因此數字簽名可以用於保證我們收到的信息的正確性。

生成自簽名證書

crypto/x509 是用來生成數字證書的包。首先,要知道一個證書分爲兩部分: 公鑰+證書持有者信息。

第二部分信息包括序列號,有效期,採用的簽名算法等等。我們將這部分信息用通用的函數封裝起來,作爲一個證書模板。

//證書模板,通過該模板默認設置一些證書需要的字段,比如序列號,組織信息,有效期等等
func CertTemplate() (*x509.Certificate, error) {
    //生成隨機的序列號 (不同組織可以有不同的序列號生成方式)
    serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
    serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
    if err != nil {
        return nil, errors.New("failed to generate serial number: " + err.Error())
    }
    tmpl := x509.Certificate{
        SerialNumber:          serialNumber,
        Subject:               pkix.Name{Organization: []string{"Yhat, Inc."}},
        SignatureAlgorithm:    x509.SHA256WithRSA,
        NotBefore:             time.Now(),
        NotAfter:              time.Now().Add(time.Hour), //1小時的有效期
        BasicConstraintsValid: true,
    }
    return &tmpl, nil
}

接下來,我們創建一對新的公私鑰 rootKey ,以及一個證書模板 rootCertTmpl

//生成一對新的公私鑰
rootKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
    log.Fatalf("generating random key: %v", err)
}
rootCertTmpl, err := CertTemplate()
if err != nil {
    log.Fatalf("creating cert template: %v", err)
}
//在模板的基礎上增加一些新的證書信息
rootCertTmpl.IsCA = true   //是否是CA
rootCertTmpl.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature
rootCertTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}
rootCertTmpl.IPAddresses = []net.IP{net.ParseIP("127.0.0.1")}

接下來,萬事具備,我們生成一個自簽名的證書。一個證書必須由其父證書的私鑰簽名。當然,根證書是沒有父證書的,根證書的私鑰由CA機構自己保存。以下生成一個證書的過程:

x509.CreateCertificate 需要 4 個參數:

  • 證書申請者的證書模板
  • 父證書
  • 證書申請者的公鑰
  • 父證書對應的公私鑰對
func CreateCert(template, parent *x509.Certificate, pub interface{}, parentPriv interface{}) (
    cert *x509.Certificate, certPEM []byte, err error) {

    certDER, err := x509.CreateCertificate(rand.Reader, template, parent, pub, parentPriv)
    if err != nil {
        return
    }
    // parse the resulting certificate so we can use it again
    cert, err = x509.ParseCertificate(certDER)
    if err != nil {
        return
    }
    //將 certDER 用 pem 編碼,生成 certPEM 證書
    b := pem.Block{Type: "CERTIFICATE", Bytes: certDER}
    certPEM = pem.EncodeToMemory(&b)
    return
}

那麼沒有父證書的根證書如何生成呢?根證書的父證書就是自己

rootCert, rootCertPEM, err := CreateCert(rootCertTmpl, rootCertTmpl, &rootKey.PublicKey, rootKey)
if err != nil {
    log.Fatalf("error creating cert: %v", err)
}
fmt.Printf("%s\n", rootCertPEM)
fmt.Printf("%#x\n", rootCert.Signature) // 證書的簽名信息

以下是生成的證書信息

-----BEGIN CERTIFICATE-----
MIIDDDCCAfSgAwIBAgIRANeLpYEH+dj0bnlDxJF5sMYwDQYJKoZIhvcNAQELBQAw
FzEVMBMGA1UEChMMY2FzdGVyLCBJbmMuMB4XDTE4MDgwNjEyNDAwMFoXDTE4MDgw
NjEzNDAwMFowFzEVMBMGA1UEChMMY2FzdGVyLCBJbmMuMIIBIjANBgkqhkiG9w0B
AQEFAAOCAQ8AMIIBCgKCAQEAqDEF45u26kMg9tidYJWXbpCZhNwQgBGmYRjOvWtl
7ofQqmhBodUYTdy/fRvKqKOqY2fd00o/qF4AcdRvlVX2KgD2sw3owvd+RVcVoPX1
TtjS4jdPyrdpjwODRvmeGkXHkGC2PVc4eptQZcd9RmZOMnnZG+Up/KQzaJkZOv3G
YVw1K8ipxD3+u2H5INnlGf0LR5WXn3wQoablM/bNG9Plb3xxRVOMsWPMmvYnLXr1
5ElqkyrH2CAi+ECfPEHg8yJAl/IwBPi0DGsVVtF5W5S7/Yw95ym9s2mjWzjK/qWw
8DJVGecjkLpDYSOUF//66xyGooJylzsW8vrrB7GW/SeEpQIDAQABo1MwUTAOBgNV
HQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMA8GA1Ud
EwEB/wQFMAMBAf8wDwYDVR0RBAgwBocEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEA
Qjop8reUUe59TH4RYuR31iQ7iQUmKnq4kRK0kGTe4WLDOVO9ocbc1WyYU8M+so5S
w/ep2tAVQMZ5ofhRf2fgxRKLv1vqNLfYnDn2yBoxQ+r+hdz7dirEr40iTCk3exDT
zYPGZ2b6rUKOdWhDJ2YjuUFm6MX7Z7sNCH0RhCTaCxuEvIH24LzOtSfpoekjuSGF
jPAjmxv/rhB3fqdPT4/7SpB/1hqUkJSZv0QDf6etceG7LWUa7JdNmOhZDrZJNmK4
8c98edPM7uHqN6SolMpaRbQTj/F+9LDbi6gkCN3bTENuxw2Uy3PPNp0FHogyQPQa
4xOrRE3lEuoto5V/ZxLhAg==
-----END CERTIFICATE-----

但是,目前爲止的證書實際上只相當於一個公鑰,我們還不能把證書放在服務器上。爲了證明你是證書的真實擁有者,必須還要有配對的私鑰。生成私鑰:

// 將私鑰用 pem 編碼
rootKeyPEM := pem.EncodeToMemory(&pem.Block{
    Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(rootKey),
})
fmt.Printf("rootKeyPEM :\n%s\n", rootKeyPEM)
// 將證書和私鑰都 pem 編碼之後,結合起來生成最終的 TLS 證書
rootTLSCert, err := tls.X509KeyPair(rootCertPEM, rootKeyPEM)
if err != nil {
    log.Fatalf("invalid key pair: %v", err)
}

私鑰生成如下:

-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAuNTbfJcpwmi7zMFJGE3tfYljhVpSXEzmNC5TylGAbHGAO5EC
sM2ymT77n3FvcIMGjPV+4D94cRPF7uanaHpXo+3CT/s6XdFxggVipIZQirmWVhNa
KEP2G4ufFbBeWY+KnFwnfXQpHa/4iibz5263wYl6xfAe9JBgcilwFMtP5b+Sxi6p
lvqWutZsy5kR66HRl9xR/rr2Nf+Az98+Out0HYA4zM5izgL/uhLerLuUosFiFDsR
4VcH49Kvbwsfyv0/Agh+ylq3Y/ssj4aSiGiPOT9QyyF+iJ2uoOSBvXDvjjtmGWiD
NMsQ6lrXyTzEA2boDZ/+R5iurFcviqrYbtsgOwIDAQABAoIBACQsNFBr3RZZHPfz
k/SXu7Tn4HxGsvuxaRQpROjBjpqqk+gUdyxW9W8cbm5D6wVf/zYzDYOhqFapAgHB
Tl4aI3DHpVG13zRhOw+xMh700mpz68Iow2pB8rZtWtMJ000/1GbJekkJJMrUl5Wi
DfXrKzdLSqXWWpiOcPGmvnKzX42c3V3AX4vBEuXxHT7gk/apUU40t8bIYwDMxH/c
xuC0knUhcYA4uEYbyai7oL3ioUI4xh6M5/6vYCurNtBDKNG/U0zLbYAf/UJWFm7a
6aA4NgdDgvGDqONBtsUnZyBnpAQr0xTyhmCAgeLknb9TP5ogtHVbumd5gvDNrNkU
3slJSCECgYEA2s3ixrxl1XwVKMRl1LZtxE8aAyGwct1WSvruSiOOMR4eu+Txp673
nls9ZjdiBl4/Dh0qVwHC03dKRz/shZ/8d6RVIV3kLthZEZ4sa340FRcoVK+APIS+
+d500oX//NDsY3xPVOFOMyWxg7IzF4RP2kRgb6QtjeKMnfIFxvtHwSkCgYEA2ECD
05ot81zu8v4ul3rL3Ts53ZViI61LLOCcq301XAhdUuC2W+XrVIGT2rVkbkaLV8VY
iID70TRhE8QY87pZVoNKLDZsSVjoyUovT1XaxjqmK3ihAqVAByKEhDpM0Cxn+XTy
of2awf8h814dDk9rtc23KLOMIfrCbqbw/yAkzsMCgYAblXUPZNTZswjf2NKVnGH+
K5K17ltWP70POs8rnYvheVCak2Q7pX0mA46cAkNjViJQ3zBlQ52SFynQDaj9t4uW
cashx7pqhW/FHtGuw3xBZGf7NRzPhFSnH3pOyAHbl2MVr6g4pSa8n/XfCmoSfuWq
OJCHwoTTrEnZ55b+3NLQ8QKBgQCmhAb+SRpY6paURWVa/xM7pv9HwF9xWV8pj0sU
QbV0yHwT9TR2TvSGfcB8CHDs+SUS0ML7WVaOIOcfcUBFbJieJTpYERAQ6oVVeeo0
DMgJG+AYWSqh/tzuoYWoy7uaEJd/Xq32TnF8MBjUbQOyoTUvKNiAXsDo6U4OJj4s
NXQiQQKBgDSqI5NOSOZscSHV8P4j7Qo3l9Rzx7DjzrCx0PdhTKUsxxq20SPEixeU
h2OfWCsAziKutJssWg3+saNFlf5IOv3baGxmTV+BtBFyetkz8NrY+QdVJj+UuPWU
Z6vjEVw+Cfu3NTtO4Q+1CJTVye6r5logi512oj/TaiNYRaD+Sgwa
-----END RSA PRIVATE KEY-----

OK,至今爲止,TLS 證書準備完畢。下面啓動一個帶 TLS 認證的http服務器,然後 httptest 模擬發送請求

ok := func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("HI!")) }
s := httptest.NewUnstartedServer(http.HandlerFunc(ok))

//將上述生成的 TLS 證書配置在服務器上
s.TLS = &tls.Config{
    Certificates: []tls.Certificate{rootTLSCert},
}
s.StartTLS()
fmt.Println(s.URL)

//模擬瀏覽器向服務器發送一個請求
_, err = http.Get(s.URL)
s.Close()

//請求失敗,由於服務器返回的證書並不是操作系統或瀏覽器內置的受信證書
fmt.Println(err)
//Get https://127.0.0.1:47254: x509: certificate signed by unknown authority
//2018/08/07 10:15:49 http: TLS handshake error from 127.0.0.1:47255: remote error: tls: bad certificate

可以看到在握手階段就失敗了,由於服務端的證書不受客戶端信任,因此客戶端拒絕了連接。

默認的 net/http 會從操作系統加載所有受信任的證書,同時瀏覽器裏也會內置這些受信任的證書。現在的問題是,服務器提供的數字簽名證書,對於瀏覽器或客戶端來說,並不在受信任的證書列表中。

模擬CA頒發證書

在解決上述客戶端不信任服務器的問題之前,我們首先模擬一個真實場景,某個CA機構給某個組織頒發證書。假設上文中我們生成的 rootCert 是該 CA 的證書。那麼接下來使用該證書給第三方組織頒發證書。

首先需要第三方組織自己提供公鑰 + 證書模板(主要就是組織的信息等等)

//第三方組織先自己生成一對公私鑰
servKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
    log.Fatalf("generating random key: %v", err)
}

//第三方組織提供一個證書模板,包括自己公司的信息,ip 等等
servCertTmpl, err := CertTemplate()
if err != nil {
    log.Fatalf("creating cert template: %v", err)
}
servCertTmpl.KeyUsage = x509.KeyUsageDigitalSignature
servCertTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
servCertTmpl.IPAddresses = []net.IP{net.ParseIP("127.0.0.1")}

接下來,由CA機構來給第三方機構簽名,頒發證書。注意這一步與上面生成自簽名證書一致,只是其父證書變成了我們之前生成的 rootCert

//使用自簽名的CA證書給二級組織頒發證書
_, servCertPEM, err := CreateCert(servCertTmpl, rootCert, &servKey.PublicKey, rootKey)
if err != nil {
    log.Fatalf("error creating cert: %v", err)
}

現在我們有了CA頒發的證書,那麼如何將證書應用到服務器呢,結合相應的私鑰即可。

//先將上面生成的私鑰用pem編碼
servKeyPEM := pem.EncodeToMemory(&pem.Block{
    Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(servKey),
})
//將CA頒發的證書和本地的私鑰結合,生成服務器證書
servTLSCert, err := tls.X509KeyPair(servCertPEM, servKeyPEM)
if err != nil {
    log.Fatalf("invalid key pair: %v", err)
}
//用服務器證書來啓動服務器
s = httptest.NewUnstartedServer(http.HandlerFunc(ok))
s.TLS = &tls.Config{
    Certificates: []tls.Certificate{servTLSCert},
}

ok,一個CA頒發證書的簡單流程就結束了,第三方組織拿到了CA頒發的證書。當然,此時如果你發送一個請求,會發現依然會被客戶端 reject , 因爲上文中的 rootCert 只是我們自制的證書,CA 機構也是不存在的。

如何讓客戶端信任服務器

上面模擬了CA給第三方組織頒發證書的過程。那麼如何讓客戶端相信服務端自制的證書呢?我們需要將我們自簽名的CA證書加入到客戶端的受信證書池中。

//創建一個受信證書池,並將我們自制的CA的證書加入到證書池中
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(rootCertPEM)

//給客戶端配置這個證書池
client := &http.Client{
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{RootCAs: certPool},
    },
}

經過以上配置的客戶端,除了會信任操作系統或瀏覽器內置的證書,還會信任證書池中的證書。也就是我們模擬的CA的證書,有了CA的信任,那麼CA頒發給第三方組織的證書也可以信任了。具體操作,就是客戶端會同時用內置的證書和 certPool 裏的證書來校驗數字簽名。

接下來啓動服務器。發送請求。

s.StartTLS()
resp, err := client.Get(s.URL)
s.Close()
if err != nil {
    log.Fatalf("could not make GET request: %v", err)
}
dump, err := httputil.DumpResponse(resp, true)
if err != nil {
    log.Fatalf("could not dump response: %v", err)
}
fmt.Printf("%s\n", dump)

ok, 成功的看到了服務器的回覆

HTTP/1.1 200 OK
Content-Length: 3
Content-Type: text/plain; charset=utf-8
Date: Tue, 07 Aug 2018 03:14:29 GMT

HI!

雙向認證:讓服務器信任客戶端

大多數的web服務器並不關心客戶端的身份。換句話說,實際上對客戶端的身份認證和鑑權實際上是在應用層做的,比如 session tokens等,而不是在tcp 層。如果要開啓CS之間的雙向認證應該如何做呢?

開啓服務端的認證很簡單

//配置服務端對客戶端的認證,要求客戶端必須攜帶證書
s = httptest.NewUnstartedServer(http.HandlerFunc(ok))
s.TLS = &tls.Config{
    Certificates: []tls.Certificate{servTLSCert},
    ClientAuth:   tls.RequireAndVerifyClientCert, 
}

s.StartTLS()
_, err = client.Get(s.URL)
s.Close()
fmt.Println(err)
//2018/08/07 11:55:01 http: TLS handshake error from 127.0.0.1:56390: tls: client didn't provide a certificate

可以看到服務器開啓雙向認證之後,客戶端的連接就無法建立了,因爲客戶端沒有提供服務器要求的證書。

接下來是生成一個客戶端的證書,跟生成服務端證書類似,先生成公私鑰對和證書模板

//創建客戶端的公私鑰對
clientKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
    log.Fatalf("generating random key: %v", err)
}
//創建客戶端證書模板
clientCertTmpl, err := CertTemplate()
if err != nil {
    log.Fatalf("creating cert template: %v", err)
}
clientCertTmpl.KeyUsage = x509.KeyUsageDigitalSignature
clientCertTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}

同樣的,模擬CA機構給客戶端頒發證書

//使用 CA 的證書給客戶端的公鑰+證書信息簽名
_, clientCertPEM, err := CreateCert(clientCertTmpl, rootCert, &clientKey.PublicKey, rootKey)
if err != nil {
    log.Fatalf("error creating cert: %v", err)
}
//給客戶端的私鑰進行 pem 編碼
clientKeyPEM := pem.EncodeToMemory(&pem.Block{
    Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(clientKey),
})
//生成客戶端 TSL 證書
clientTLSCert, err := tls.X509KeyPair(clientCertPEM, clientKeyPEM)
if err != nil {
    log.Fatalf("invalid key pair: %v", err)
}

客戶端證書生成之後,需要配置向服務端提供自己的證書

authedClient := &http.Client{
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{
            RootCAs:      certPool,
            Certificates: []tls.Certificate{clientTLSCert}, //提供客戶端的證書
        },
    },
}

當然,這一切都是在客戶端做的,服務端依然不會信任客戶端。如果發送請求將會看到:

http: TLS handshake error from 127.0.0.1:59756: tls: failed to verify client's certificate: x509: certificate signed by unknown authority

最後一步,將客戶端用到的受信證書池也配置到服務端就可以了:

certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(rootCertPEM)

s.TLS = &tls.Config{
    Certificates: []tls.Certificate{servTLSCert}, //服務端證書
    ClientAuth:   tls.RequireAndVerifyClientCert,//開啓客戶端認證模式
    ClientCAs:    certPool,                     //與客戶端設置相同的受信證書池
}

這樣就可以實現客戶端和服務端的雙向認證了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章