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

A step-by-step guide to creating and utilizing SAN Certifications for secure gRPC communication

分享
Secure Communication with gRPC: From SSL/TLS Certification to SAN Certification
generate by DALLE-3

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.

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:

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. A 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 has many SANs.

Create a SAN certificate locally

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

! You can see all the code here

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
​ 
 [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
​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
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

2. Generate client certificate

​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

​ 
 ➜ 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.

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

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

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

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

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

=== 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 go 1.15, the use of CA is not recommended, instead SAN certificates are utilized.