certificates/autocert/examples/hello-mtls/go-grpc/client/client.go
2019-02-13 15:15:59 -08:00

164 lines
3.9 KiB
Go

package main
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"sync"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"github.com/smallstep/certificates/autocert/examples/hello-mtls/go-grpc/hello"
)
const (
autocertFile = "/var/run/autocert.step.sm/site.crt"
autocertKey = "/var/run/autocert.step.sm/site.key"
autocertRoot = "/var/run/autocert.step.sm/root.crt"
requestFrequency = 5 * time.Second
tickFrequency = 15 * time.Second
)
// Uses techniques from https://diogomonica.com/2017/01/11/hitless-tls-certificate-rotation-in-go/
// to automatically rotate certificates when they're renewed.
type rotator struct {
sync.RWMutex
certificate *tls.Certificate
}
func (r *rotator) getClientCertificate(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) {
r.RLock()
defer r.RUnlock()
return r.certificate, nil
}
func (r *rotator) loadCertificate(certFile, keyFile string) error {
r.Lock()
defer r.Unlock()
c, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return err
}
r.certificate = &c
return nil
}
func loadRootCertPool() (*x509.CertPool, error) {
root, err := ioutil.ReadFile(autocertRoot)
if err != nil {
return nil, err
}
pool := x509.NewCertPool()
if ok := pool.AppendCertsFromPEM(root); !ok {
return nil, errors.New("Missing or invalid root certificate")
}
return pool, nil
}
func sayHello(c hello.GreeterClient) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.SayHello(ctx, &hello.HelloRequest{Name: "world"})
if err != nil {
return err
}
log.Printf("Greeting: %s", r.Message)
return nil
}
func sayHelloAgain(c hello.GreeterClient) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.SayHelloAgain(ctx, &hello.HelloRequest{Name: "world"})
if err != nil {
return err
}
log.Printf("Greeting: %s", r.Message)
return nil
}
func main() {
// Read the root certificate for our CA from disk
roots, err := loadRootCertPool()
if err != nil {
log.Fatal(err)
}
// Load certificate
r := &rotator{}
if err := r.loadCertificate(autocertFile, autocertKey); err != nil {
log.Fatal("error loading certificate and key", err)
}
tlsConfig := &tls.Config{
RootCAs: roots,
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
},
// GetClientCertificate is called when a server requests a
// certificate from a client.
//
// In this example keep alives will cause the certificate to
// only be called once, but if we disable them,
// GetClientCertificate will be called on every request.
GetClientCertificate: r.getClientCertificate,
}
// Schedule periodic re-load of certificate
// A real implementation can use something like
// https://github.com/fsnotify/fsnotify
done := make(chan struct{})
go func() {
ticker := time.NewTicker(tickFrequency)
defer ticker.Stop()
for {
select {
case <-ticker.C:
fmt.Println("Checking for new certificate...")
err := r.loadCertificate(autocertFile, autocertKey)
if err != nil {
log.Println("Error loading certificate and key", err)
}
case <-done:
return
}
}
}()
defer close(done)
// Set up a connection to the server.
address := os.Getenv("HELLO_MTLS_URL")
conn, err := grpc.Dial(address, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
client := hello.NewGreeterClient(conn)
for {
if err := sayHello(client); err != nil {
log.Fatalf("could not greet: %v", err)
}
if err := sayHelloAgain(client); err != nil {
log.Fatalf("could not greet: %v", err)
}
time.Sleep(requestFrequency)
}
}