You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

gRPC双向证书认证客户端出现无效证书错误求助

问题描述

我的代码在非安全模式和服务端认证模式下均可正常运行,查遍资料确认配置无误,但双向认证模式始终无法正常工作,恳请协助解决。

错误信息

alert bad certificate:ssl/record/rec_layer_s3.c:908:SSL alert number 42

证书相关说明

证书、密钥和根CA均在Mac上通过mkcert生成,已存入文件并通过配置文件引用。

证书验证测试结果

openssl verify -CAfile rootCA.crt localhost.crt
localhost.crt: OK
openssl x509 -noout -modulus -in localhost.crt | openssl md5
MD5(stdin)= 2458874611c7bad14f86a76c4b28a332
openssl rsa -noout -modulus -in ../localhost-private.key | openssl md5
MD5(stdin)= 2458874611c7bad14f86a76c4b28a332

注:loadConfig代码未附上,该功能运行正常。

客户端配置

grpc_host: "localhost"
grpc_port: 50051
grpc_secure:
  mutual: true
grpc_tls_info:
  tls_certificate_fqn: "/Volumes/development-share/.keys/mac-localhost/tls/certs/client-localhost.crt"
  tls_private_key_fqn: "/Volumes/development-share/.keys/mac-localhost/tls/client-localhost.key"
  tls_ca_bundle_fqn: "/Volumes/development-share/.keys/mac-localhost/tls/certs/rootCA.crt"

服务端配置

grpc_host: "localhost"
grpc_port: 50051
grpc_secure:
  mutual: true
grpc_tls_info:
  tls_certificate_fqn: "/Volumes/development-share/.keys/mac-localhost/tls/certs/localhost.crt"
  tls_private_key_fqn: "/Volumes/development-share/.keys/mac-localhost/tls/localhost-private.key"
  tls_ca_bundle_fqn: "/Volumes/development-share/.keys/mac-localhost/tls/certs/rootCA.crt"

服务端GRPC核心代码

func NewGRPCService(
    extensionName string,
    config GRPCConfiguration,
) (gRPCServicePtr *GRPCService, errorInfo errs.ErrorInfo) {

    var (
        tCertificate          tls.Certificate
        tCACertPool           *x509.CertPool
        tGRPCListener         net.Listener
        tTLSConfig            *tls.Config
        tTransportCredentials credentials.TransportCredentials
    )

    gRPCServicePtr = &GRPCService{
        host: config.GRPCHost,
    }
    if tGRPCListener, errorInfo.Error = net.Listen(ctv.VAL_TCP, fmt.Sprintf("%s:%d", ctv.VAL_LOCAL_HOST, config.GRPCPort)); errorInfo.Error != nil {
        errorInfo = errs.NewErrorInfo(errorInfo.Error, errs.BuildLabelValue(ctv.LBL_GRPC_LISTENER, ctv.TXT_FAILED))
        return
    }
    gRPCServicePtr.GRPCListenerPtr = &tGRPCListener

    tTLSConfig = &tls.Config{}
    switch {
    case config.GRPCSecure.ServerSide:
        if tCertificate, errorInfo = LoadTLSCredentialsWithKey(config.GRPCTLSInfo); errorInfo.Error != nil {
            return
        }

        tTLSConfig.Certificates = []tls.Certificate{tCertificate}
        tTLSConfig.ClientAuth = tls.NoClientCert
        tTransportCredentials = credentials.NewTLS(tTLSConfig)

        gRPCServicePtr.GRPCServerPtr = grpc.NewServer(grpc.Creds(tTransportCredentials))
        if hlps.CheckPointerNotNil(gRPCServicePtr.GRPCServerPtr, errs.ErrPointerMissing, ctv.FN_GRPC_SERVER_POINTER); errorInfo.Error != nil {
            return
        }
    case config.GRPCSecure.Mutual:
        if tCertificate, errorInfo = LoadTLSCredentialsWithKey(config.GRPCTLSInfo); errorInfo.Error != nil {
            return
        }

        if tCACertPool, errorInfo = LoadTLSCACertificate(config.GRPCTLSInfo); errorInfo.Error != nil {
            return
        }

        tTLSConfig.Certificates = []tls.Certificate{tCertificate}
        tTLSConfig.RootCAs = tCACertPool
        tTLSConfig.ClientCAs = tCACertPool
        tTLSConfig.ClientAuth = tls.RequireAndVerifyClientCert
        tTransportCredentials = credentials.NewTLS(tTLSConfig)
        gRPCServicePtr.GRPCServerPtr = grpc.NewServer(grpc.Creds(tTransportCredentials))
    default:
        // 若server side和mutual都设为false,默认使用该安全模式
        tTLSConfig.ClientAuth = tls.NoClientCert
        gRPCServicePtr.GRPCServerPtr = grpc.NewServer()
    }

    return
}

func LoadTLSCACertificate(tlsConfig jwts.TLSInfo) (caCertPoolPtr *x509.CertPool, errorInfo errs.ErrorInfo) {

    var (
        tCACertificateFile []byte
    )

    if tCACertificateFile, errorInfo.Error = os.ReadFile(tlsConfig.TLSCABundleFQN); errorInfo.Error != nil {
        errorInfo = errs.NewErrorInfo(errorInfo.Error, errs.BuildLabelValue(ctv.LBL_TLS_CA_BUNDLE_FILENAME, tlsConfig.TLSCABundleFQN))
        return
    }

    caCertPoolPtr = x509.NewCertPool()
    if ok := caCertPoolPtr.AppendCertsFromPEM(tCACertificateFile); !ok {
        errorInfo = errs.NewErrorInfo(errorInfo.Error, errs.BuildLabelValue(ctv.LBL_TLS_CA_CERT_POOL, ctv.TXT_FAILED))
    }

    return
}
func LoadTLSCredentialsWithKey(tlsConfig jwts.TLSInfo) (certificate tls.Certificate, errorInfo errs.ErrorInfo) {

    if certificate, errorInfo.Error = tls.LoadX509KeyPair(tlsConfig.TLSCertFQN, tlsConfig.TLSPrivateKeyFQN); errorInfo.Error != nil {
        errorInfo = errs.NewErrorInfo(
            errorInfo.Error, fmt.Sprintf(
                "%s, %s", errs.BuildLabelValue(ctv.LBL_TLS_CERTIFICATE_FILENAME, tlsConfig.TLSCertFQN), errs.BuildLabelValue(
                    ctv.LBL_TLS_PRIVATE_KEY_FILENAME,
                    tlsConfig.TLSPrivateKey,
                ),
            ),
        )
    }

    return
}

服务端Main.go

func main() {

    var (
        errorInfo       errs.ErrorInfo
        tConfig         grpcs.GRPCConfiguration
        tGRPCServicePtr *grpcs.GRPCService
        tSecurityLevel  string
    )

    if tConfig, errorInfo = loadConfig(configFilename); errorInfo.Error != nil {
        log.Fatalf("服务启动失败: %v", errorInfo.Error)
    }

    tGRPCServicePtr, errorInfo = grpcs.NewGRPCService(utilityName, tConfig)
    pb.RegisterGRPCTestServicesServer(tGRPCServicePtr.GRPCServerPtr, &Server{})

    switch {
    case tConfig.GRPCSecure.Mutual:
        tSecurityLevel = "双向认证"
    case tConfig.GRPCSecure.ServerSide:
        tSecurityLevel = "服务端认证"
    default:
        tSecurityLevel = "非安全模式"
    }

    log.Printf("gRPC服务启动于端口 %v / 安全等级: %s", tConfig.GRPCPort, tSecurityLevel)
    if err := tGRPCServicePtr.GRPCServerPtr.Serve(*tGRPCServicePtr.GRPCListenerPtr); err != nil {
        log.Fatalf("服务启动失败: %v", err)
    }

    fmt.Println(tGRPCServicePtr, errorInfo)
}

客户端Main.go

func main() {

    var (
        errorInfo             errs.ErrorInfo
        tCACertPoolPtr        *x509.CertPool
        tClientCertificate    tls.Certificate
        tConfig               grpcs.GRPCConfiguration
        tConn                 *grpc.ClientConn
        tPongReply            *pb.Pong
        tTLSConfig            *tls.Config
        tTransportCredentials credentials.TransportCredentials
    )

    if tConfig, errorInfo = loadConfig(configFilename); errorInfo.Error != nil {
        log.Fatalf("配置文件加载失败: %v", errorInfo.Error)
    }

    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()

    if tClientCertificate, errorInfo = grpcs.LoadTLSCredentialsWithKey(tConfig.GRPCTLSInfo); errorInfo.Error != nil {
        log.Fatalf("TLS凭证加载失败: %v", errorInfo.Error)
    }

    if tCACertPoolPtr, errorInfo = grpcs.LoadTLSCACertificate(tConfig.GRPCTLSInfo); errorInfo.Error != nil {
        log.Fatalf("TLS根CA证书加载失败: %v", errorInfo.Error)
    }

    tTLSConfig = &tls.Config{
        RootCAs:      tCACertPoolPtr,
        ClientCAs:    tCACertPoolPtr,
        Certificates: []tls.Certificate{tClientCertificate},
    }
    tTLSConfig.ServerName = ctv.LOCAL_HOST
    tTLSConfig.InsecureSkipVerify = false
    tTransportCredentials = credentials.NewTLS(tTLSConfig)

    tConn, errorInfo.Error = grpc.NewClient(fmt.Sprintf("%s:%v", tConfig.GRPCHost, tConfig.GRPCPort), grpc.WithTransportCredentials(tTransportCredentials))
    if errorInfo.Error != nil {
        log.Fatalf("连接gRPC服务器失败 %s: %v", fmt.Sprintf("%s:%v", tConfig.GRPCHost, tConfig.GRPCPort), errorInfo.Error)
    }
    defer func(conn *grpc.ClientConn) {
        err := conn.Close()
        if err != nil {

        }
    }(tConn)

    c := pb.NewGRPCTestServicesClient(tConn)

    if tPongReply, errorInfo.Error = c.DKPing(
        ctx, &pb.Ping{
            Email: "scott@yackofamily.com",
        },
    ); errorInfo.Error != nil {
        errorInfo = errs.NewErrorInfo(errorInfo.Error, ctv.VAL_EMPTY)
    }

    log.Printf("Ping响应: %v", tPongReply)
    if errorInfo.Error != nil {
        log.Printf("错误: %v", errorInfo.Error.Error())
    }

}

问题排查与解决方案

SSL alert number 42对应bad certificate,说明服务端验证客户端证书时不认可,可从以下方向排查:

  1. 验证客户端证书有效性
    执行命令确认客户端证书由指定根CA签发:

    openssl verify -CAfile rootCA.crt client-localhost.crt
    

    若返回OK则证书本身有效,否则重新生成客户端证书。

  2. 检查客户端证书SAN字段
    执行命令查看客户端证书的主题备用名称:

    openssl x509 -in client-localhost.crt -text -noout
    

    确认X509v3 Subject Alternative Name包含DNS:localhost(或客户端连接使用的IP地址),若缺失则重新生成证书:

    mkcert client localhost 127.0.0.1 ::1
    
  3. 修正服务端监听器绑定地址
    当前服务端硬编码绑定ctv.VAL_LOCAL_HOST,改为绑定配置中的config.GRPCHost,避免地址不匹配:

    if tGRPCListener, errorInfo.Error = net.Listen(ctv.VAL_TCP, fmt.Sprintf("%s:%d", config.GRPCHost, config.GRPCPort)); errorInfo.Error != nil {
    
  4. 优化客户端TLS配置
    客户端无需设置ClientCAs(该字段用于验证其他客户端证书),仅保留RootCAs验证服务端证书:

    tTLSConfig = &tls.Config{
        RootCAs:      tCACertPoolPtr,
        Certificates: []tls.Certificate{tClientCertificate},
    }
    
  5. 开启GRPC调试日志
    启动服务端和客户端时设置环境变量,获取更详细的TLS握手日志:

    export GRPC_GO_LOG_SEVERITY_LEVEL=info
    export GRPC_GO_LOG_VERBOSITY_LEVEL=99
    

内容的提问来源于stack exchange,提问作者S. Yacko

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 16:35:56