Secure Communication with gRPC: From SSL/TLS Certification to SAN Certification

Pasted image 20240512192344
gRPC is a high-performance RPC framework developed by Google, which by default includes two authentication methods:

  • SSL/TLS Authentication
  • Token-based Authentication
    Without the activation of the certificate, gRPC service and clients communicate in plaintext, leaving the information at risk of being intercepted by any third party. To ensure gRPC communication is not intercepted, altered or counterfeited by a third party, the server can activate TLS encryption features.

This article is first published in the medium MPP plan. If you are a medium user, please follow me in medium. Thank you very much.

Starting from go 1.15 version, depreciation of CommonName began, therefore, it is advised to use SAN certificates. If keys, CSR, and certificates are generated in the previous way through OpenSSL, the following error occurs:

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

What is SAN

SAN (Subject Alternative Name) is defined as an extension in the SSL standard x509. An SSL certificate with the SAN field can expand the domain names it supports, allowing a single certificate to support multiple different domain name resolutions.

Put simply, a SAN certificate can contain multiple complete CN (CommonName), so with a single certificate purchase, you can use it on multiple URLs. For example, the certificate of skype.com, it has many SANs.

Create a SAN certificate locally

Next, we will use an example to generate a client & server bilateral SAN certificate locally.

Assume the hostname of the gRPC server is localhost, and it is required to configure tls bilateral authentication encryption for the communication between the gRPC server and clients.

  1. Create openssl.conf to store relevant information
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

[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

The content is similar to when creating a ca earlier.

  1. Generate ca root certificate
1
2
3

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

-nodes is to ignore the password, making it convenient to use, but please note, this may reduce the security of the private key, as anyone can read the unencrypted private key.

  1. Generate server certificate
1
2
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
  1. Generate client certificate
1
2
3
4

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

The final generated result is as follows

1
2
3
4

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

Testing

We define the simplest grpc interface.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

// 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;

}

Server implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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"
"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()
}

We directly run the server go run main.go

Client

First, we use a request without a certificate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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())
}
}

Output

1
2
3

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

Service is unavailable
Now, let’s try a request carrying the certificate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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("localhost:50051", opts...)
if err != nil {
log.Fatalf("Connect to %s failed", "localhost:50051")
}
defer conn.Close()

client := api.NewGreeterClient(conn)

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())
}
}

Output

1
2
=== RUN Test_server_SayHello
2024/05/12 19:20:17 Greeting: Hello Hello

Conclusion

  1. We can use tls to implement gRPC encryption communication,
  2. Starting from go1.15, the use of CA is not recommended, instead SAN certificates are utilized.