Golang中x509 mTLS实现:客户端配置InsecureSkipVerify为false且服务端要求验证客户端证书(使用不同证书密钥对)

Golang中x509 mTLS实现:客户端配置InsecureSkipVerify为false且服务端要求验证客户端证书(使用不同证书密钥对) 我正在尝试为客户端和服务器端的 tls.Config 找到正确的配置,以满足以下约束条件:

  1. 我们从一个 CA 生成独立的证书——一个用于客户端,另一个完全不同的用于服务器。
  2. 证书是使用 golang 的 x509.CreateCertificate 生成的——而不是 openssl
  3. 客户端的 tls.Config 必须 指定 InsecureSkipVerify 为 false。
  4. 服务器的 tls.Config 必须 指定 ClientAuth 为 RequireAndVerifyClientCert。

我一直无法生成满足这组约束条件并允许建立连接的证书。我最接近成功的情况是,当我在客户端和服务器端都使用 CA 证书时——只有在这种情况下,客户端和服务器之间的一切才能正常工作。

如果我生成一个 CA 证书,并用它来创建并签名服务器证书,然后生成一个由该 CA 证书签名的客户端证书——我会遇到以下错误:

连接失败:x509: 证书由未知机构签名(可能因为尝试验证候选机构证书“localhost”时出现“crypto/rsa: 验证错误”)。

如果客户端使用 CA 证书连接到服务器(当服务器使用由 CA 生成并签名的证书时),也会出现同样的错误。

当我让服务器使用 CA 证书作为服务器证书,而客户端使用由 CA 生成并签名的证书时,在 Dial 之后,当我尝试在连接上读取数据时会失败,错误信息是:

连接读取错误:远程错误:tls: 错误的证书

只有当服务器和客户端使用完全相同的证书时,客户端和服务器才能连接并交换数据。

欢迎提出任何想法和建议。如果有人想深入研究并看一看,我可以发布一个 git 仓库。提前说明——我这样做是为了演示使用动态 tls.Config 进行动态证书生成,以及使用自定义验证器通过 GetConfigForClientVerifyPeerCertificate 进行验证。


更多关于Golang中x509 mTLS实现:客户端配置InsecureSkipVerify为false且服务端要求验证客户端证书(使用不同证书密钥对)的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

你是否已将自定义的CA证书添加到RootCAs(在tls.Config中)或系统信任存储中?

rabarar: 使用自定义验证器的 VerifyPeerCertificate

请注意,这仅在常规验证(主机名、信任链等)成功后才会被调用。

更多关于Golang中x509 mTLS实现:客户端配置InsecureSkipVerify为false且服务端要求验证客户端证书(使用不同证书密钥对)的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


感谢您的回复。我随后对 crypto/x509/verify.go 文件进行了一些研究,并发现了问题所在——我认为使用 NewCertPool 而非 SystemCertPool 存在一个问题,即无论服务器提供什么证书,使用前者都会构建一个零长度的证书链……在我看来,我会将其归类为一个错误——但也许我错了。

要实现满足你所有约束条件的mTLS配置,需要正确配置证书链和信任关系。以下是完整的示例代码:

服务端配置

package main

import (
    "crypto/rand"
    "crypto/rsa"
    "crypto/tls"
    "crypto/x509"
    "crypto/x509/pkix"
    "encoding/pem"
    "fmt"
    "log"
    "math/big"
    "net"
    "net/http"
    "time"
)

// 生成CA证书
func generateCA() (*x509.Certificate, *rsa.PrivateKey, error) {
    caKey, err := rsa.GenerateKey(rand.Reader, 2048)
    if err != nil {
        return nil, nil, err
    }

    caTemplate := &x509.Certificate{
        SerialNumber: big.NewInt(1),
        Subject: pkix.Name{
            Organization: []string{"My CA"},
            CommonName:   "My Root CA",
        },
        NotBefore:             time.Now(),
        NotAfter:              time.Now().AddDate(10, 0, 0),
        KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
        BasicConstraintsValid: true,
        IsCA:                  true,
        MaxPathLen:            2,
    }

    caCertDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey)
    if err != nil {
        return nil, nil, err
    }

    caCert, err := x509.ParseCertificate(caCertDER)
    if err != nil {
        return nil, nil, err
    }

    return caCert, caKey, nil
}

// 使用CA签发证书
func signCertificate(caCert *x509.Certificate, caKey *rsa.PrivateKey, template *x509.Certificate) (*x509.Certificate, *rsa.PrivateKey, error) {
    key, err := rsa.GenerateKey(rand.Reader, 2048)
    if err != nil {
        return nil, nil, err
    }

    certDER, err := x509.CreateCertificate(rand.Reader, template, caCert, &key.PublicKey, caKey)
    if err != nil {
        return nil, nil, err
    }

    cert, err := x509.ParseCertificate(certDER)
    if err != nil {
        return nil, nil, err
    }

    return cert, key, nil
}

func startServer() {
    // 生成CA
    caCert, caKey, err := generateCA()
    if err != nil {
        log.Fatal(err)
    }

    // 生成服务器证书
    serverTemplate := &x509.Certificate{
        SerialNumber: big.NewInt(2),
        Subject: pkix.Name{
            Organization: []string{"My Server"},
            CommonName:   "server.example.com",
        },
        DNSNames:    []string{"server.example.com", "localhost"},
        IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1)},
        NotBefore:   time.Now(),
        NotAfter:    time.Now().AddDate(1, 0, 0),
        KeyUsage:    x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
        ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
    }

    serverCert, serverKey, err := signCertificate(caCert, caKey, serverTemplate)
    if err != nil {
        log.Fatal(err)
    }

    // 生成客户端证书
    clientTemplate := &x509.Certificate{
        SerialNumber: big.NewInt(3),
        Subject: pkix.Name{
            Organization: []string{"My Client"},
            CommonName:   "client.example.com",
        },
        DNSNames:    []string{"client.example.com"},
        NotBefore:    time.Now(),
        NotAfter:     time.Now().AddDate(1, 0, 0),
        KeyUsage:     x509.KeyUsageDigitalSignature,
        ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
    }

    clientCert, _, err := signCertificate(caCert, caKey, clientTemplate)
    if err != nil {
        log.Fatal(err)
    }

    // 创建证书池并添加CA证书
    caCertPool := x509.NewCertPool()
    caCertPool.AddCert(caCert)

    // 创建服务器TLS配置
    serverTLSConfig := &tls.Config{
        Certificates: []tls.Certificate{
            {
                Certificate: [][]byte{serverCert.Raw},
                PrivateKey:  serverKey,
            },
        },
        ClientAuth: tls.RequireAndVerifyClientCert,
        ClientCAs:  caCertPool,
        MinVersion: tls.VersionTLS12,
    }

    // 创建HTTP处理器
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
            fmt.Fprintf(w, "Hello, %s! Your certificate was verified.\n", 
                r.TLS.PeerCertificates[0].Subject.CommonName)
        } else {
            fmt.Fprintln(w, "Hello, anonymous!")
        }
    })

    // 启动服务器
    server := &http.Server{
        Addr:      ":8443",
        Handler:   handler,
        TLSConfig: serverTLSConfig,
    }

    fmt.Println("Server starting on :8443")
    log.Fatal(server.ListenAndServeTLS("", ""))
}

func main() {
    startServer()
}

客户端配置

package main

import (
    "crypto/rand"
    "crypto/rsa"
    "crypto/tls"
    "crypto/x509"
    "crypto/x509/pkix"
    "fmt"
    "io/ioutil"
    "log"
    "math/big"
    "net/http"
    "time"
)

func startClient() {
    // 注意:在实际应用中,客户端应该从安全存储中获取CA证书
    // 这里为了演示,我们重新生成相同的CA
    caCert, caKey, err := generateCA()
    if err != nil {
        log.Fatal(err)
    }

    // 生成客户端证书(与服务器端生成的相同)
    clientTemplate := &x509.Certificate{
        SerialNumber: big.NewInt(3),
        Subject: pkix.Name{
            Organization: []string{"My Client"},
            CommonName:   "client.example.com",
        },
        DNSNames:    []string{"client.example.com"},
        NotBefore:    time.Now(),
        NotAfter:     time.Now().AddDate(1, 0, 0),
        KeyUsage:     x509.KeyUsageDigitalSignature,
        ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
    }

    clientCert, clientKey, err := signCertificate(caCert, caKey, clientTemplate)
    if err != nil {
        log.Fatal(err)
    }

    // 创建证书池并添加CA证书
    caCertPool := x509.NewCertPool()
    caCertPool.AddCert(caCert)

    // 创建客户端TLS配置
    clientTLSConfig := &tls.Config{
        Certificates: []tls.Certificate{
            {
                Certificate: [][]byte{clientCert.Raw},
                PrivateKey:  clientKey,
            },
        },
        RootCAs:            caCertPool,  // 关键:必须设置RootCAs
        InsecureSkipVerify: false,       // 必须为false
        ServerName:         "server.example.com", // 必须匹配服务器证书的CN或SAN
    }

    // 创建HTTP客户端
    client := &http.Client{
        Transport: &http.Transport{
            TLSClientConfig: clientTLSConfig,
        },
    }

    // 发起请求
    resp, err := client.Get("https://localhost:8443/")
    if err != nil {
        log.Fatal("请求失败:", err)
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatal("读取响应失败:", err)
    }

    fmt.Println("服务器响应:", string(body))
}

func main() {
    startClient()
}

关键点说明

  1. 证书链配置

    • 服务器需要配置ClientCAs包含CA证书
    • 客户端需要配置RootCAs包含相同的CA证书
  2. 证书扩展用途

    • 服务器证书必须包含x509.ExtKeyUsageServerAuth
    • 客户端证书必须包含x509.ExtKeyUsageClientAuth
  3. 服务器名称验证

    • 客户端必须设置ServerName匹配服务器证书的CN或SAN
    • 服务器证书的SAN必须包含客户端连接使用的地址
  4. 证书序列化(如果需要保存到文件):

// 保存证书到PEM文件
func saveCertToFile(cert *x509.Certificate, filename string) error {
    certOut, err := os.Create(filename)
    if err != nil {
        return err
    }
    defer certOut.Close()
    
    if err := pem.Encode(certOut, &pem.Block{
        Type:  "CERTIFICATE",
        Bytes: cert.Raw,
    }); err != nil {
        return err
    }
    return nil
}

// 保存私钥到PEM文件
func saveKeyToFile(key *rsa.PrivateKey, filename string) error {
    keyOut, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
    if err != nil {
        return err
    }
    defer keyOut.Close()
    
    if err := pem.Encode(keyOut, &pem.Block{
        Type:  "RSA PRIVATE KEY",
        Bytes: x509.MarshalPKCS1PrivateKey(key),
    }); err != nil {
        return err
    }
    return nil
}

这个配置满足你所有的约束条件:使用不同的证书密钥对、InsecureSkipVerify为false、ClientAuthRequireAndVerifyClientCert,并且完全使用Go的x509库生成证书。

回到顶部