使用自签名证书SAN为gRPC建立TLS 连接
gRPC 是Google开发的一个高性能RPC框架,gRPC 默认内置了两种认证方式:
- SSL/TLS 认证方式
- 基于 Token 的认证方式
没有启用证书的gRPC服务和客户端进行的是明文通信,信息面临被任何第三方监听的风险。为了保证gRPC通信不被第三方监听、篡改或伪造,可以对服务器启动TLS加密特性。
从 go 1.15 版本开始废弃 CommonName,因此推荐使用SAN证书。如果按照之前的步骤通过 OpenSSL 来生成密钥、CSR、证书,会出现这样的错误:
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. 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双向认证加密。
- 新建
openssl.conf来放相关信息
[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根证书
openssl req -x509 -newkey rsa:4096 -keyout ca.key -out ca.crt -subj "/CN=localhost" -days 3650 -nodes
-nodes 是忽略密码,方便使用,但是请注意,这可能会降低私钥的安全性,因为任何人都可以读取未加密的私钥。
3. 生成服务端证书
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
- 生成客户端证书
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
最终生成的结果如下
➜ 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接口
// 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;
}
服务端实现
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
客户端
首先我们使用一个不带证书的请求
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())
}
输出
2024/05/12 19:18:51 Failed to greet, error: rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: EOF"
服务不可用
我们再使用一个携带证书的请请求
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())
}
输出
=== RUN Test_server_SayHello
2024/05/12 19:20:17 Greeting: Hello Hello
总结
- 我们可以使用tls实现gRPC 的加密通信,
- 从go1.15 开始,go不建议使用CA而是使用SAN证书