This is the updated version of what I already have here. I had to publish this version just because Golang v1.15 breaks the way certificates are handled. You will get the error below.


transport: authentication handshake failed: x509: certificate relies on legacy Common Name field, use SANs or temporarily enable Common Name matching with GODEBUG=x509ignoreCN=0

In order to address this error, we need to change the way we create the certificates. While doing this, I might as well use a completely new example from scratch. In short our client and server applications will use TLS to communicate with each other.


Structure


├── Makefile
├── cert
│   ├── cert.conf
│   ├── server.crt
│   └── server.key
├── client
│   └── main.go
├── go.mod
├── hello.pb.go
├── hello.proto
├── message
│   ├── client.go
│   └── server.go
└── server
└── main.go

Certificates


Content of the cert.conf file is shown below.


[ req ]
default_bits = 2048
prompt = no
default_md = sha256
req_extensions = req_ext
distinguished_name = dn

[ dn ]
C = UK
ST = London
L = London
O = Inanzzz Ltd.
OU = Information Technologies
emailAddress = email@email.com
CN = localhost

[ req_ext ]
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = localhost
DNS.2 = inanzzz.com
DNS.3 = www.inanzzz.com

Let's create private key and the certificate.


$ openssl genrsa -out cert/server.key 2048

$ openssl req -nodes -new -x509 -sha256 -days 1825 -config cert/cert.conf -extensions 'req_ext' -key cert/server.key -out cert/server.crt

The important point here is that the subjectAltName property. Golang v1.15 needs SNA which is directly related to this property. Let's confirm the details.


$ openssl x509 -in cert/server.crt -text
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
59:b3:15:04:02:f9:04:0f:77:b5:0c:c6:dd:d9:65:0f:06:2d:e5:cd
Signature Algorithm: sha256WithRSAEncryption
Issuer: C = UK, ST = London, L = London, O = Inanzzz Ltd., OU = Information Technologies, emailAddress = email@email.com, CN = localhost
Validity
Not Before: Sep 13 19:13:52 2020 GMT
Not After : Sep 12 19:13:52 2025 GMT
Subject: C = UK, ST = London, L = London, O = Inanzzz Ltd., OU = Information Technologies, emailAddress = email@email.com, CN = localhost
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (4096 bit)
Modulus:
00:a7:90:4e:47:5b:3a:87:20:55:c8:36:8d:a9:00:
............................................:
e9:56:3f
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:localhost, DNS:inanzzz.com, DNS:www.inanzzz.com
Signature Algorithm: sha256WithRSAEncryption
9a:40:93:5a:20:6e:20:9f:14:70:c5:5b:96:ee:da:ac:22:98:
.....................................................:
44:fe:3d:31:03:53:b7:e1
-----BEGIN CERTIFICATE-----
MIIGADCCA+igAwIBAgIUWbMVBAL5BA93tQzG3dllDwYt5c0wDQYJKoZIhvcNAQEL
................................................................
mai+5Eck8n8uEJzQnIB4oDDb4eucoL26GLDa4YUGONZdeijOcpluTgXt/uNE/j0x
A1O34Q==
-----END CERTIFICATE-----

If you didn't use the subjectAltName property, your output above wouldn't contain X509v3 Subject Alternative Name extension.


Files


Run make compile command to generate hello.pb.go file.


Makefile


.PHONY: compile
compile:
protoc --go_out=plugins=grpc:. --go_opt=paths=source_relative hello.proto

.PHONY: client
client:
go run --race client/main.go

.PHONY: server
server:
go run --race server/main.go

hello.proto


syntax = "proto3";

package message;

option go_package = "github.com/inanzzz/hello";

message MessageRequest {
string text = 1;
}

message MessageResponse {
bool ok = 1;
}

service MessageService {
rpc SendMessage (MessageRequest) returns (MessageResponse) {}
}

go.mod


module github.com/inanzzz/hello

go 1.15

require (
github.com/golang/protobuf v1.4.2
golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f // indirect
golang.org/x/text v0.3.3 // indirect
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d // indirect
google.golang.org/grpc v1.31.1
google.golang.org/protobuf v1.25.0
)

server/main.go


package main

import (
"log"
"net"

"github.com/inanzzz/hello"
"github.com/inanzzz/hello/message"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)

func main() {
log.Println("server")

listener, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalln(err)
}

creds, err := credentials.NewServerTLSFromFile("cert/server.crt", "cert/server.key")
if err != nil {
log.Fatalln(err)
}

grpcServer := grpc.NewServer(grpc.Creds(creds))

messageServer := message.NewServer()

client.RegisterMessageServiceServer(grpcServer, messageServer)

log.Fatalln(grpcServer.Serve(listener))
}

message/server.go


package message

import (
"context"
"log"

"github.com/inanzzz/hello"
)

type Server struct {
client.UnimplementedMessageServiceServer
}

func NewServer() Server {
return Server{}
}

func (s Server) SendMessage(ctx context.Context, req *client.MessageRequest) (*client.MessageResponse, error) {
log.Println(req)

return &client.MessageResponse{Ok:true}, nil
}

client/main.go


This is version 1.


package main

import (
"context"
"log"
"time"

"github.com/inanzzz/hello/message"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)

func main() {
log.Println("client")

creds, err := credentials.NewClientTLSFromFile("cert/server.crt", "localhost")
if err != nil {
log.Fatalln(err)
}

opts := []grpc.DialOption{
grpc.WithTransportCredentials(creds),
}

conn, err := grpc.Dial(":50051", opts...)
if err != nil {
log.Fatalln(err)
}
defer conn.Close()

messageClient := message.NewClient(conn, time.Second)
res, err := messageClient.SendMessage(context.Background(), "Hello")

log.Println("RES:", res)
log.Println("ERR:", err)
}

This is version 2.


package main

import (
"context"
"crypto/tls"
"crypto/x509"
"io/ioutil"
"log"
"time"

"github.com/inanzzz/hello/message"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)

func main() {
log.Println("client")

caCert, err := ioutil.ReadFile("cert/server.crt")
if err != nil {
log.Fatalln(err)
}

rootCAs := x509.NewCertPool()
rootCAs.AppendCertsFromPEM(caCert)

tlsConf := &tls.Config{
RootCAs: rootCAs,
InsecureSkipVerify: false,
MinVersion: tls.VersionTLS12,
ServerName: "localhost",
}

opts := []grpc.DialOption{
grpc.WithTransportCredentials(credentials.NewTLS(tlsConf)),
}

conn, err := grpc.Dial(":50051", opts...)
if err != nil {
log.Fatalln(err)
}
defer conn.Close()

messageClient := message.NewClient(conn, time.Second)
res, err := messageClient.SendMessage(context.Background(), "Hello")

log.Println("RES:", res)
log.Println("ERR:", err)
}

message/client.go


package message

import (
"context"
"time"

"github.com/inanzzz/hello"
"google.golang.org/grpc"
)

type Client struct {
messageClient client.MessageServiceClient
timeout time.Duration
}

func NewClient(conn grpc.ClientConnInterface, timeout time.Duration) Client {
return Client{
messageClient: client.NewMessageServiceClient(conn),
timeout: timeout,
}
}

func (c Client) SendMessage(ctx context.Context, message string) (*client.MessageResponse, error) {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()

return c.messageClient.SendMessage(ctx, &client.MessageRequest{Text: message})
}

Test


$ make server
go run --race server/main.go
2020/09/13 20:39:17 server

# This shall output as soon as you run client
2020/09/13 20:39:22 text:"Hello"

$ make client
go run --race client/main.go
2020/09/13 20:39:22 client
2020/09/13 20:39:22 RES: ok:true
2020/09/13 20:39:22 ERR: nil