> ## Content Index
> Fetch the complete content index at: https://huizhou92.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# 使用自签名证书SAN为gRPC建立TLS 连接
- URL: https://huizhou92.com/shi-yong-zi-qian-ming-zheng-shu-sanwei-grpcjian-li-tls-lian-jie/
- Published: 2024-06-29T13:41:50.000Z
- Updated: 2026-09-06T11:40:27.000Z
- Author: huizhou92
- Tags: tech, golang, #lang-zh-cn

[gRPC](https://grpc.io/?ref=huizhou92.com) 是Google开发的一个高性能RPC框架，gRPC 默认内置了两种认证方式：

- SSL/TLS 认证方式
- 基于 Token 的认证方式  
没有启用证书的`gRPC`服务和客户端进行的是明文通信，信息面临被任何第三方监听的风险。为了保证`gRPC`通信不被第三方监听、篡改或伪造，可以对服务器启动`TLS`加密特性。  
从 go 1.15 版本开始[废弃 CommonName](https://golang.org/doc/go1.15?ref=huizhou92.com#commonname)，因此推荐使用 `SAN` 证书。如果按照之前的步骤通过 OpenSSL 来生成密钥、CSR、证书，会出现这样的错误：

```shell
rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: certificate relies on legacy Common Name field, use SANs instead"|

```

> This article was first published in the Medium MPP plan. If you are a Medium user, please follow me on [Medium](https://medium.huizhou92.com/?ref=huizhou92.com). Thank you very much.

## 什么是`SAN`

**SAN(Subject Alternative Name)** 是 `SSL` 标准 `x509` 中定义的一个扩展。使用了 `SAN` 字段的 `SSL` 证书，可以扩展此证书支持的域名，使得一个证书可以支持多个不同域名的解析。  
通俗点就是，在 SAN 证书中，可以有多个**完整**的 CN（CommonName），这样只需要购买一个证书就可以用在多个 URL。比如 skype.com 的证书，它就有很多 SAN。

## 在本地创建SAN 证书

下面 我们将用一个例子在本地生成 客户端&服务端双向SAN 证书。  
假设gRPC服务端的主机名为`localhost`，需要为gRPC服务端和客户端之间的通信配置tls双向认证加密。

1. 新建 `openssl.conf` 来放相关信息

```shell
[req]  
req_extensions = v3_req  
distinguished_name = req_distinguished_name  
prompt = no  
  
[req_distinguished_name]  
countryName = CN  
stateOrProvinceName = state  
localityName = city  
organizationName = huizhou92  
commonName = hello-world  
  
[v3_req]  
subjectAltName = @alt_names  
  
[alt_names]  
DNS.1 = localhost

```

其中内容 跟 以前创建ca的时候差不多。  
2\. 生成ca根证书

```shell
openssl req -x509 -newkey rsa:4096 -keyout ca.key -out ca.crt -subj "/CN=localhost" -days 3650  -nodes

```

\-nodes 是忽略密码,方便使用，但是请注意，这可能会降低私钥的安全性，因为任何人都可以读取未加密的私钥。  
3\. 生成服务端证书

```shell
 openssl req -newkey rsa:2048 -nodes -keyout server.key -out server.csr -subj "/CN=localhost" -config openssl.cnf 
 openssl x509 -req -in server.csr -out server.crt -CA ca.crt -CAkey ca.key -CAcreateserial -days 365 -extensions v3_req -extfile openssl.cnf

```

1. 生成客户端证书

```shell
openssl req -newkey rsa:2048 -nodes -keyout client.key -out client.csr -subj "/CN=localhost" -config openssl.cnf

openssl x509 -req -in client.csr -out client.crt -CA ca.crt -CAkey ca.key -CAcreateserial -days 365 -extensions v3_req -extfile openssl.cnf

```

最终生成的结果如下

```shell
➜  keys git:(day1) ✗ ls 
ca.crt      ca.key      ca.srl      client.crt  client.csr  client.key  openssl.cnf server.crt  server.csr  server.key

```

## 测试

我们定义一个最简单的grpc接口

```proto
// helloworld.proto  
syntax = "proto3";  
option go_package = "./api;api";  
package api;  
  
// The greeting service definition.  
service Greeter {  
  // Sends a greeting  
  rpc SayHello (HelloRequest) returns (HelloReply) {}  
}  
  
// The request message containing the user's name.  
message HelloRequest {  
  string name = 1;  
}  
  
// The response message containing the greetings  
message HelloReply {  
  string message = 1;  
}

```

### 服务端实现

```go
package main  
  
import (  
    "context"  
    "crypto/tls"    
    "crypto/x509"    
    "fmt"    
    "google.golang.org/genproto/googleapis/rpc/errdetails"    
    "google.golang.org/grpc"    
    "google.golang.org/grpc/codes"    
    "google.golang.org/grpc/credentials"    
    "google.golang.org/grpc/status"    
    "hello-world/api"    
    "io"    
    "log"    
    "net"    
    "os"    
    "time"
)  
  
type server struct {  
    api.UnimplementedGreeterServer  
}  
  
func (s *server) SayHello(ctx context.Context, in *api.HelloRequest) (*api.HelloReply, error) {  
    log.Printf("Received: %v", in.GetName())  
    select {  
    case <-ctx.Done():  
       log.Println("client timeout return")  
       return nil, ErrorWithDetails()  
    case <-time.After(3 * time.Second):  
       return &api.HelloReply{Message: "Hello " + in.GetName()}, nil  
    }  
}  

func main() {  
  
    certificate, err := tls.LoadX509KeyPair("./keys/server.crt", "./keys/server.key")  
    if err != nil {  
       log.Fatalf("Failed to load key pair: %v", err)  
    }  
    // 通过CA创建证书池  
    certPool := x509.NewCertPool()  
    ca, err := os.ReadFile("./keys/ca.crt")  
    if err != nil {  
       log.Fatalf("Failed to read ca: %v", err)  
    }  
  
    // 将来自CA的客户端证书附加到证书池  
    if ok := certPool.AppendCertsFromPEM(ca); !ok {  
       log.Fatalf("Failed to append ca certificate")  
    }  
  
    opts := []grpc.ServerOption{  
       grpc.Creds( // 为所有传入的连接启用TLS  
          credentials.NewTLS(&tls.Config{  
             ClientAuth:   tls.RequireAndVerifyClientCert,  
             Certificates: []tls.Certificate{certificate},  
             ClientCAs:    certPool,  
          },  
          )),  
    }  
  
    listen, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", 50051))  
    if err != nil {  
       log.Fatalf("failed to listen %d port", 50051)  
    }  
    // 通过传入的TLS服务器凭证创建新的gRPC服务实例  
    s := grpc.NewServer(opts...)  
    api.RegisterGreeterServer(s, &server{})  
    log.Printf("server listening at %v", listen.Addr())  
    if err := s.Serve(listen); err != nil {  
       log.Fatalf("Failed to serve: %v", err)  
    }  
  
}  
  
func ErrorWithDetails() error {  
    st := status.Newf(codes.Internal, fmt.Sprintf("something went wrong: %v", "api.Getter"))  
    v := &errdetails.PreconditionFailure_Violation{ //errDetails  
       Type:        "test",  
       Subject:     "12",  
       Description: "32",  
    }  
    br := &errdetails.PreconditionFailure{}  
    br.Violations = append(br.Violations, v)  
    st, _ = st.WithDetails(br)  
    return st.Err()  
}

```

我们直接运行服务端 `go run main.go`

### 客户端

首先我们使用一个不带证书的请求

```go
func Test_server_SayHello_No_Cert(t *testing.T) {  
    conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))  
    if err != nil {  
       log.Fatalf("Connect to %s failed", "localhost:50051")  
    }  
    defer conn.Close()  
  
    client := api.NewGreeterClient(conn)  
    // 创建带有超时时间的上下文, cancel可以取消上下文  
    ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)  
    defer cancel()  
    // 业务代码处理部分 ...    r, err := client.SayHello(ctx, &api.HelloRequest{Name: "Hello"})  
    if err != nil {  
       log.Printf("Failed to greet, error: %v", err)  
    } else {  
       log.Printf("Greeting: %v", r.GetMessage())  
    }  
    // Set up a connection to the server.  
    log.Printf("Greeting: %s", r.GetMessage())  
}

```

输出

```shell
2024/05/12 19:18:51 Failed to greet, error: rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: EOF"

```

服务不可用

我们再使用一个携带证书的请请求

```go
func Test_server_SayHello(t *testing.T) {  
    certificate, err := tls.LoadX509KeyPair("./keys/client.crt", "./keys/client.key")  
    if err != nil {  
       log.Fatalf("Failed to load client key pair, %v", err)  
    }  
  
    certPool := x509.NewCertPool()  
    ca, err := os.ReadFile("./keys/ca.crt")  
    if err != nil {  
       log.Fatalf("Failed to read %s, error: %v", "./keys/ca.crt", err)  
    }  
  
    if ok := certPool.AppendCertsFromPEM(ca); !ok {  
       log.Fatalf("Failed to append ca certs")  
    }  
  
    opts := []grpc.DialOption{  
       grpc.WithTransportCredentials(credentials.NewTLS(  
          &tls.Config{  
             ServerName:   "localhost",  
             Certificates: []tls.Certificate{certificate},  
             RootCAs:      certPool,  
          })),  
    }  
  
    // conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))  
    conn, err := grpc.Dial("localhost:50051", opts...)  
    if err != nil {  
       log.Fatalf("Connect to %s failed", "localhost:50051")  
    }  
    defer conn.Close()  
  
    client := api.NewGreeterClient(conn)  
    // 创建带有超时时间的上下文, cancel可以取消上下文  
    ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)  
    defer cancel()  
    // 业务代码处理部分 ...    r, err := client.SayHello(ctx, &api.HelloRequest{Name: "Hello"})  
    if err != nil {  
       log.Printf("Failed to greet, error: %v", err)  
    } else {  
       log.Printf("Greeting: %v", r.GetMessage())  
    }  
    // Set up a connection to the server.  
    log.Printf("Greeting: %s", r.GetMessage())  
}

```

输出

```shell
=== RUN   Test_server_SayHello
2024/05/12 19:20:17 Greeting: Hello Hello

```

## 总结

1. 我们可以使用tls实现gRPC 的加密通信，
2. 从go1.15 开始，go不建议使用CA而是使用SAN证书