Go TLS 服務端綁定證書的幾種方式

隨着互聯網的發展,網站提供的服務類型和規模不斷擴大,同時也對 Web 服務的安全性提出了更高的要求。TLS(Transport Layer Security)[1] 已然成爲 Web 服務最重要的安全基礎設施之一。默認情況下,一個 TLS 服務器通常只綁定一個證書 [2],但當服務複雜度增加時,單一證書已然難以滿足需求。這時,服務端綁定多個 TLS 證書就成爲一個非常實用的功能。

Go 語言中的 net/http 包和 tls 包對 TLS 提供了強大的支持,在密碼學和安全專家 Filippo Valsorda[3] 的精心設計下,Go 提供了多種 TLS 服務端綁定證書的方式,本文將詳細探討服務端綁定 TLS 證書的幾種方式,包括綁定單個證書、多個證書、自定義證書綁定邏輯等。我會配合示例代碼,瞭解每種方式的使用場景、實現原理和優缺點。

注:本文假設讀者已熟悉基本的 TLS 使用方法 [4],並具備 Go 語言編程經驗。如果你不具備 Go 語言基礎知識,可以將學習我撰寫的極客時間專欄《Go 語言第一課》[5] 作爲你入門 Go 的起點。

  1. 熱身:製作證書

爲了後續示例說明方便,我們先來把示例所需的私鑰和證書都做出來,本文涉及的證書以及他們之間的簽發關係如下圖:

注:示例使用的自簽名根證書。

從圖中我們看到,我們證書分爲三個層次,最左邊是 CA 的根證書 (root certificate,比如 ca-cert.pem),之後是根 CA 簽發的中間 CA 證書 (intermediate certificate,比如 inter-cert.pem),從安全和管理角度出發,真正簽發服務器證書的都是這些中間 CA;最右側則是由中間 CA 簽發的葉子證書 (leaf certificate,比如 leaf-server-cert.pem),也就是服務器配置的服務端證書 (server certificate),我們爲三個不同域名創建了不同的服務器證書。

在這裏,我們製作上述證書沒有使用類似 openssl[6] 這樣的工具,而是通過 Go 代碼生成的,下面是生成上述證書的代碼片段:

// tls-certs-binding/make_certs/main.go

func main() {
 // 生成CA根證書密鑰對
 caKey, err := rsa.GenerateKey(rand.Reader, 2048)
 checkError(err)

 // 生成CA證書模板
 caTemplate := x509.Certificate{
  SerialNumber: big.NewInt(1),
  Subject: pkix.Name{
   Organization: []string{"Go CA"},
  },
  NotBefore:             time.Now(),
  NotAfter:              time.Now().Add(time.Hour * 24 * 365),
  KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
  BasicConstraintsValid: true,
  IsCA:                  true,
 }

 // 使用模板自簽名生成CA證書
 caCert, err := x509.CreateCertificate(rand.Reader, &caTemplate, &caTemplate, &caKey.PublicKey, caKey)
 checkError(err)

 // 生成中間CA密鑰對
 interKey, err := rsa.GenerateKey(rand.Reader, 2048)
 checkError(err)

 // 生成中間CA證書模板
 interTemplate := x509.Certificate{
  SerialNumber: big.NewInt(2),
  Subject: pkix.Name{
   Organization: []string{"Go Intermediate CA"},
  },
  NotBefore:             time.Now(),
  NotAfter:              time.Now().Add(time.Hour * 24 * 365),
  KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
  BasicConstraintsValid: true,
  IsCA:                  true,
 }

 // 用CA證書籤名生成中間CA證書
 interCert, err := x509.CreateCertificate(rand.Reader, &interTemplate, &caTemplate, &interKey.PublicKey, caKey)
 checkError(err)

 // 生成葉子證書密鑰對
 leafKey, err := rsa.GenerateKey(rand.Reader, 2048)
 checkError(err)

 // 生成葉子證書模板,CN爲server.com
 leafTemplate := x509.Certificate{
  SerialNumber: big.NewInt(3),
  Subject: pkix.Name{
   Organization: []string{"Go Server"},
   CommonName:   "server.com",
  },
  NotBefore:    time.Now(),
  NotAfter:     time.Now().Add(time.Hour * 24 * 365),
  KeyUsage:     x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
  ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
  IPAddresses:  []net.IP{net.ParseIP("127.0.0.1")},
  DNSNames:     []string{"server.com"},
  SubjectKeyId: []byte{1, 2, 3, 4},
 }

 // 用中間CA證書籤名生成葉子證書
 leafCert, err := x509.CreateCertificate(rand.Reader, &leafTemplate, &interTemplate, &leafKey.PublicKey, interKey)
 checkError(err)

 // 生成server1.com葉子證書
 leafKey1, _ := rsa.GenerateKey(rand.Reader, 2048)

 leafTemplate1 := x509.Certificate{
  SerialNumber: big.NewInt(4),
  Subject: pkix.Name{
   CommonName: "server1.com",
  },
  NotBefore: time.Now(),
  NotAfter:  time.Now().Add(time.Hour * 24 * 365),

  KeyUsage:    x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
  ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
  DNSNames:    []string{"server1.com"},
 }

 leafCert1, _ := x509.CreateCertificate(rand.Reader, &leafTemplate1, &interTemplate, &leafKey1.PublicKey, interKey)

 // 生成server2.com葉子證書
 leafKey2, _ := rsa.GenerateKey(rand.Reader, 2048)

 leafTemplate2 := x509.Certificate{
  SerialNumber: big.NewInt(5),
  Subject: pkix.Name{
   CommonName: "server2.com",
  },
  NotBefore: time.Now(),
  NotAfter:  time.Now().Add(time.Hour * 24 * 365),

  KeyUsage:    x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
  ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
  DNSNames:    []string{"server2.com"},
 }

 leafCert2, _ := x509.CreateCertificate(rand.Reader, &leafTemplate2, &interTemplate, &leafKey2.PublicKey, interKey)

 // 將證書和密鑰編碼爲PEM格式
 caCertPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert})
 caKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(caKey)})

 interCertPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: interCert})
 interKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(interKey)})

 leafCertPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert})
 leafKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(leafKey)})

 leafCertPEM1 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert1})
 leafKeyPEM1 := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(leafKey1)})

 leafCertPEM2 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert2})
 leafKeyPEM2 := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(leafKey2)})

 // 將PEM寫入文件
 writeDataToFile("ca-cert.pem", caCertPEM)
 writeDataToFile("ca-key.pem", caKeyPEM)

 writeDataToFile("inter-cert.pem", interCertPEM)
 writeDataToFile("inter-key.pem", interKeyPEM)

 writeDataToFile("leaf-server-cert.pem", leafCertPEM)
 writeDataToFile("leaf-server-key.pem", leafKeyPEM)

 writeDataToFile("leaf-server1-cert.pem", leafCertPEM1)
 writeDataToFile("leaf-server1-key.pem", leafKeyPEM1)

 writeDataToFile("leaf-server2-cert.pem", leafCertPEM2)
 writeDataToFile("leaf-server2-key.pem", leafKeyPEM2)
}

運行這個程序後,當前目錄下就會出現如下私鑰文件 (xx-key.pem) 和證書文件(xx-cert.pem):

$ls *pem
ca-cert.pem  inter-cert.pem  leaf-server-cert.pem leaf-server1-cert.pem leaf-server2-cert.pem
ca-key.pem  inter-key.pem  leaf-server-key.pem leaf-server1-key.pem leaf-server2-key.pem

製作完證書後,我們就來看看日常使用最多的綁定單一 TLS 證書的情況。

  1. 綁定單一 TLS 證書

做過 web 應用的讀者,想必對綁定單一 TLS 證書的實現方式並不陌生。服務端只需要加載一對服務端私鑰與公鑰證書即可對外提供基於 TLS 的安全網絡服務,這裏一個 echo 服務爲例,我們來看下服務端的代碼:

// tls-certs-binding/bind_single_cert/sever/main.go


// 服務端
func startServer(certFile, keyFile string) {
 // 讀取證書和密鑰
 cert, err := tls.LoadX509KeyPair(certFile, keyFile)
 if err != nil {
  log.Fatal(err)
 }

 // 創建TLS配置
 config := &tls.Config{
  Certificates: []tls.Certificate{cert},
 }

 // 啓動TLS服務器
 listener, err := tls.Listen("tcp"":8443", config)
 if err != nil {
  log.Fatal(err)
 }
 defer listener.Close()

 log.Println("Server started")

 for {
  conn, err := listener.Accept()
  if err != nil {
   log.Println(err)
   continue
  }
  handleConnection(conn)
 }
}

func handleConnection(conn net.Conn) {
 defer conn.Close()
 // 處理連接...
 // 循環讀取客戶端的數據
 for {
  buf := make([]byte, 1024)
  n, err := conn.Read(buf)
  if err != nil {
   // 讀取失敗則退出
   return
  }

  // 回顯數據給客戶端
  s := string(buf[:n])
  fmt.Printf("recv data: %s\n", s)
  conn.Write(buf[:n])
 }
}

func main() {
 // 啓動服務器
 startServer("leaf-server-cert.pem""leaf-server-key.pem")
}

根據 TLS 的原理,客戶端在與服務端的握手過程中,服務端會將服務端證書 (leaf-server-cert.pem) 發到客戶端供後者驗證,客戶端使用服務器公鑰證書校驗服務器身份。這一過程的實質是客戶端利用 CA 證書中的公鑰或中間 CA 證書中的公鑰對服務端證書中由 CA 私鑰或中間 CA 私鑰簽名的數據進行驗籤

// tls-certs-binding/bind_single_cert/client/main.go

func main() {
 caCert, err := ioutil.ReadFile("inter-cert.pem")
 if err != nil {
  log.Fatal(err)
 }

 caCertPool := x509.NewCertPool()
 caCertPool.AppendCertsFromPEM(caCert)

 config := &tls.Config{
  RootCAs: caCertPool,
 }

 conn, err := tls.Dial("tcp""server.com:8443", config)
 if err != nil {
  log.Fatal(err)
 }
 defer conn.Close()

 // 每秒發送信息
 ticker := time.NewTicker(time.Second)
 for range ticker.C {
  msg := "hello, tls"
  conn.Write([]byte(msg))

  // 讀取回復
  buf := make([]byte, len(msg))
  conn.Read(buf)
  log.Println(string(buf))
 }

}

這裏我們使用了簽發了 leaf-server-cert.pem 證書的中間 CA(inter-cert.pem)來驗證服務端證書 (leaf-server-cert.pem) 的合法性,毫無疑問這是會成功的!

// server
$go run main.go
2023/10/05 22:49:17 Server started


// client

$go run main.go
2023/10/05 22:49:22 hello, tls
2023/10/05 22:49:23 hello, tls
... ...

注:運行上述代碼之前,需修改 / etc/hosts 文件,添加 server.com 的 IP 爲 127.0.0.1。

不過要注意的是,在這裏用 CA 根證書 (ca-cert.pem) 直接驗證葉子證書 (leaf-server-cert.pem) 會失敗,因爲根證書不是葉子證書的直接簽發者,必須通過驗證證書鏈來建立根證書和葉子證書之間的信任鏈。

  1. 證書鏈

實際生產中,服務器實體證書和根證書分別只有一張,但中間證書可以有多張,這些中間證書在客戶端並不一定存在,這就可能導致客戶端與服務端的連接無法建立。通過 openssl 命令也可以印證這一點:

// 在make_certs目錄下

// CA根證書無法直接驗證葉子證書
$openssl verify -CAfile ca-cert.pem leaf-server-cert.pem
leaf-server-cert.pem: O = Go Server, CN = server.com
error 20 at 0 depth lookup:unable to get local issuer certificate


// 證書鏈不完整,也無法驗證
$openssl verify -CAfile inter-cert.pem leaf-server-cert.pem
leaf-server-cert.pem: O = Go Intermediate CA
error 2 at 1 depth lookup:unable to get issuer certificate

// 需要用完整證書鏈來驗證
$openssl verify -CAfile ca-cert.pem -untrusted inter-cert.pem leaf-server-cert.pem
leaf-server-cert.pem: OK

爲此在建連階段,服務端不僅要將服務器實體證書發給客戶端,還要發送完整的證書鏈 (如下圖所示)。

證書鏈的最頂端是 CA 根證書,它的簽名值是自己簽名的,驗證簽名的公鑰就包含在根證書中,根證書的簽發者(Issuer)與使用者(Subject)是相同的。除了根證書,每個證書的簽發者(Issuer)是它的上一級證書的使用者(Subject)。以上圖爲例,下列關係是成立的:

- ca-cert.pem的Issuer == ca-cert.pem的Subject
- inter1-cert.pem的Issuer == ca-cert.pem的Subject
- inter2-cert.pem的Issuer == inter1-cert.pem的Subject
... ...
- interN-cert.pem的Issuer == interN-1-cert.pem的Subject
- leaf-server-cert.pem的Issuer == interN-cert.pem的Subject

每張證書包含的重要信息是簽發者 (Issuer)、數字簽名算法、簽名值、使用者(Subject)域名、使用者公鑰。除了根證書,每個證書(比如 inter2-cert.pem 證書)被它的上一級證書(比如 inter1-cert.pem 證書)對應的私鑰簽名,簽名值包含在證書中,上一級證書包含的公鑰可以用來驗證該證書中的簽名值 (inter2-cert.pem 證書可以用來驗證 inter1-cert.pem 證書中的簽名值)。

那麼如何在服務端返回證書鏈呢?如何在客戶端接收並驗證證書鏈呢?我們來看下面示例。在這個示例中,客戶端僅部署了根證書 (ca-cert.pem),而服務端需要將服務證書與簽發服務證書的中間 CA 證書以證書鏈的形式返回給客戶端。

我們先來看服務端:

// tls-certs-binding/bind_single_cert/server-with-certs-chain/main.go

// 服務端
func startServer(certFile, keyFile string) {
 // 讀取證書和密鑰
 cert, err := tls.LoadX509KeyPair(certFile, keyFile)
 if err != nil {
  log.Fatal(err)
 }

 interCertBytes, err := os.ReadFile("inter-cert.pem")
 if err != nil {
  log.Fatal(err)
 }

 interCertblock, _ := pem.Decode(interCertBytes)

 // 將中間證書添加到證書鏈
 cert.Certificate = append(cert.Certificate, interCertblock.Bytes)

 // 創建TLS配置
 config := &tls.Config{
  Certificates: []tls.Certificate{cert},
 }

 // 啓動TLS服務器
 listener, err := tls.Listen("tcp"":8443", config)
 if err != nil {
  log.Fatal(err)
 }
 defer listener.Close()

 log.Println("Server started")

 for {
  conn, err := listener.Accept()
  if err != nil {
   log.Println(err)
   continue
  }
  handleConnection(conn)
 }
}

我們看到:服務端在加載完服務端證書後,又將中間 CA 證書 inter-cert.pem attach 到 cert.Certificate,這樣 cert.Certificate 中就構造出了一個證書鏈,而不單單是一個服務端證書了。

我們要注意證書鏈構造時的順序,這裏按照的是如下順序構造證書鏈的:

- 服務端證書 (leaf certificate)
- 中間CA證書N
- 中間CA證書N-1
... ...
- 中間CA證書2
- 中間CA證書1

如果客戶端沒有根 CA 證書 (root certificate),在服務端構造證書鏈時,需要將根 CA 證書作爲最後一個證書 attach 到證書鏈中。

下面則是客戶端驗證證書鏈的代碼:

// tls-certs-binding/bind_single_cert/client-verify-certs-chain/main.go

func main() {
 // 加載ca-cert.pem
 caCertBytes, err := os.ReadFile("ca-cert.pem")
 if err != nil {
  log.Fatal(err)
 }

 caCertblock, _ := pem.Decode(caCertBytes)
 caCert, err := x509.ParseCertificate(caCertblock.Bytes)
 if err != nil {
  log.Fatal(err)
 }

 // 創建TLS配置
 config := &tls.Config{
  InsecureSkipVerify: true, // trigger to call VerifyPeerCertificate

  // 設置證書驗證回調函數
  VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
   // 解析服務端返回的證書鏈(順序:server-cert.pem, inter-cert.pem,inter-cert.pem's issuer...)

   var issuer *x509.Certificate
   var cert *x509.Certificate
   var err error

   if len(rawCerts) == 0 {
    return errors.New("no server certificate found")
   }

   issuer = caCert

   for i := len(rawCerts) - 1; i >= 0; i-- {
    cert, err = x509.ParseCertificate(rawCerts[i])
    if err != nil {
     return err
    }

    if !verifyCert(issuer, cert) {
     return errors.New("verifyCert failed")
    }

    issuer = cert
   }
   return nil
  },
 }

 conn, err := tls.Dial("tcp""server.com:8443", config)
 if err != nil {
  log.Fatal(err)
 }
 defer conn.Close()

 // 每秒發送信息
 ticker := time.NewTicker(time.Second)
 for range ticker.C {
  msg := "hello, tls"
  conn.Write([]byte(msg))

  // 讀取回復
  buf := make([]byte, len(msg))
  conn.Read(buf)
  log.Println(string(buf))
 }

}

// 驗證cert是否是issuer的簽發
func verifyCert(issuer, cert *x509.Certificate) bool {

 // 驗證證書
 certPool := x509.NewCertPool()
 certPool.AddCert(issuer) // ok
 opts := x509.VerifyOptions{
  Roots: certPool,
 }
 _, err := cert.Verify(opts)
 return err == nil
}

從代碼可以看到,我們需要將 InsecureSkipVerify 設置爲 true 才能觸發證書鏈的自定義校驗邏輯 (VerifyPeerCertificate)。在 VerifyPeerCertificate 中,我們先用 ca 根證書校驗位於證書鏈最後的那個證書,驗證成功後,用驗證成功的證書驗證倒數第二個證書,依次類推,知道全部證書都校驗 ok,說明證書鏈是可信任的。

服務端綁定一個證書或一套證書鏈是最簡單的,也是最常見的方案,但在一些場景下,比如考慮支持多個域名、證書輪換等,TLS 服務端可能需要綁定多個證書以滿足要求。下面我們就來看看如何爲 TLS 服務端綁定多個證書。

  1. 綁定多個 TLS 證書

這個示例的證書綁定情況如下圖:

我們在服務端部署並綁定了三個證書,三個證書與域名的對應關係如下:

- 證書leaf-server-cert.pem 對應 server.com 
- 證書leaf-server1-cert.pem 對應 server1.com 
- 證書leaf-server2-cert.pem 對應 server2.com

注:在 / etc/hosts 中添加 server1.com 和 server2.com 對應的 ip 均爲 127.0.0.1。

// tls-certs-binding/bind_multi_certs/server/main.go

func main() {
 certFiles := []string{"leaf-server-cert.pem""leaf-server1-cert.pem""leaf-server2-cert.pem"}
 keyFiles := []string{"leaf-server-key.pem""leaf-server1-key.pem""leaf-server2-key.pem"}

 // 啓動服務器
 startServer(certFiles, keyFiles)
}

// 服務端
func startServer(certFiles, keyFiles []string) {
 // 讀取證書和密鑰

 var certs []tls.Certificate
 for i := 0; i < len(certFiles); i++ {
  cert, err := tls.LoadX509KeyPair(certFiles[i], keyFiles[i])
  if err != nil {
   log.Fatal(err)
  }
  certs = append(certs, cert)
 }

 // 創建TLS配置
 config := &tls.Config{
  Certificates: certs,
 }

 // 啓動TLS服務器
 listener, err := tls.Listen("tcp"":8443", config)
 if err != nil {
  log.Fatal(err)
 }
 defer listener.Close()

 log.Println("Server started")

 for {
  conn, err := listener.Accept()
  if err != nil {
   log.Println(err)
   continue
  }
  handleConnection(conn)
 }
}

我們看到,綁定多個證書與綁定一個證書的原理是完全一樣的,tls.Config 的 Certificates 字段原本就是一個切片,可以容納單個證書,也可以容納證書鏈,容納多個證書也不是問題。

客戶端代碼變化不大,我們僅是通過下面代碼輸出了服務端返回的證書的 Subject.CN:

// tls-certs-binding/bind_multi_certs/client/main.go

// 解析連接的服務器證書
certs := conn.ConnectionState().PeerCertificates
if len(certs) > 0 {
    log.Println("Server CN:", certs[0].Subject.CommonName)
}

接下來我們通過 client 連接不同的域名,得到如下執行結果:

// 服務端
$go run main.go
2023/10/06 10:22:38 Server started

// 客戶端
$go run main.go -server server.com:8443
2023/10/06 10:22:57 Server CN: server.com
2023/10/06 10:22:58 hello, tls

$go run main.go -server server1.com:8443
2023/10/06 10:23:02 Server CN: server1.com
2023/10/06 10:23:03 hello, tls
2023/10/06 10:23:04 hello, tls

$go run main.go -server server2.com:8443
2023/10/06 10:23:08 Server CN: server2.com
2023/10/06 10:23:09 hello, tls
... ...

我們看到,由於綁定多個域名對應的證書,程序可以支持訪問不同域名的請求,並根據請求的域名,返回對應域名的證書。

  1. 自定義證書選擇綁定邏輯

無論是單一 TLS 證書、證書鏈還是多 TLS 證書,他們都有一個共同特點,那就是證書的綁定是事先已知的,是一種 “靜態” 模式的綁定;有些場景下,服務端在初始化啓動後並不會綁定某個固定的證書,而是根據客戶端的連接需求以及特定規則在證書池中選擇某個匹配的證書。在這種情況下,我們需要使用 GetCertificate 回調從自定義的證書池中選擇匹配的證書,而不能在用上面示例中那種 “靜態” 模式了。

我們來看一個自定義證書選擇邏輯的示例,下面示意圖展示了客戶端和服務端的證書部署情況:

我們主要看一下服務端的代碼邏輯變動:

// tls-certs-binding/bind_custom_logic/server/main.go

func startServer(certsPath string) {

    // 創建TLS配置
    config := &tls.Config{
        GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
            // 根據clientHello信息選擇cert
            
            certFile := fmt.Sprintf("%s/leaf-%s-cert.pem", certsPath, info.ServerName[:len(info.ServerName)-4])
            keyFile := fmt.Sprintf("%s/leaf-%s-key.pem", certsPath, info.ServerName[:len(info.ServerName)-4])
            
            // 讀取證書和密鑰
            cert, err := tls.LoadX509KeyPair(certFile, keyFile)
            return &cert, err
        },  
    }   
 ... ...
}

我們看到: tls.Config 我們建立了一個匿名函數賦值給了 GetCertificate 字段,該函數的實現邏輯就是根據客戶端 clientHello 信息 (tls 握手時發送的信息) 按照規則從證書池目錄中查找並加載對應的證書與其私鑰信息。示例使用 ServerName 來查找帶有同名信息的證書。

例子的運行結果與上面的示例都差不多,這裏就不贅述了。

利用這種動態的證書選擇邏輯,我們還可以實現通過執行外部命令來獲取證書、從數據庫加載證書等。

  1. 小結

通過本文的介紹,我們全面瞭解了在 Go 服務端綁定單個、多個 TLS 證書的各種方式。我們首先介紹了生成自簽名證書的方法,這爲我們的示例程序奠定了基礎。然後我們詳細探討了綁定單證書、證書鏈、多證書、定製從證書池取特定證書的邏輯等不同機制的用法、優劣勢和適用場景。同時,在介紹每種用法時,我們都用代碼示例進一步解釋了這些綁定方式的具體實現流程。

單證書 TLS 簡單易理解,運行性能優異。多證書 TLS 在提高性能、安全性、便利管理等方面有着重要意義。而自定義證書選取邏輯則更加靈活。通過綜合運用各種綁定機制,可以使我們的 Go 語言服務器端更加強大和靈活。

本文示例所涉及的 Go 源碼可以在這裏 [7] 下載。

注:代碼倉庫中的證書和 key 文件有效期爲一年,大家如發現證書已經過期,可以在 make_certs 目錄下重新生成各種證書和私鑰並 copy 到對應的其他目錄中去。

  1. 參考資料


Gopher Daily(Gopher 每日新聞) - https://gopherdaily.tonybai.com

我的聯繫方式:

參考資料

[1] 

TLS(Transport Layer Security): https://tonybai.com/2023/01/13/go-and-tls13

[2] 

一個 TLS 服務器通常只綁定一個證書: https://tonybai.com/2015/04/30/go-and-https

[3] 

Filippo Valsorda: https://filippo.io/

[4] 

TLS 使用方法: https://tonybai.com/2015/04/30/go-and-https

[5] 

《Go 語言第一課》: http://gk.link/a/10AVZ

[6] 

openssl: https://www.openssl.org

[7] 

這裏: https://github.com/bigwhite/experiments/tree/master/tls-certs-binding

[8] 

《深入淺出 HTTPS:從原理到實戰》: https://book.douban.com/subject/30250772/

[9] 

Certificate chains and cross-certification: https://en.wikipedia.org/wiki/X.509#Certificate_chains_and_cross-certification

[10] 

“Gopher 部落” 知識星球: https://public.zsxq.com/groups/51284458844544

[11] 

鏈接地址: https://m.do.co/c/bff6eed92687

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/P0mbc0rqt6vYhK3YeTDBrQ