diff --git a/certs/ca-key.pem b/certs/ca-key.pem new file mode 100644 index 0000000..579b973 --- /dev/null +++ b/certs/ca-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIMaSy7CGp7A69DLcmREodmwU8Z06eJw5KxVMByIkVw+xoAoGCCqGSM49 +AwEHoUQDQgAEMlHIUMNtdUeoT2k+s0qGuEKcn5/MxTQsvTHn2UtV6llpKzEJwt8P +Xq9wJgKY92KJIVunncJfWH03Pt46w3rsXQ== +-----END EC PRIVATE KEY----- diff --git a/certs/ca.pem b/certs/ca.pem new file mode 100644 index 0000000..b6aff21 --- /dev/null +++ b/certs/ca.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBnDCCAUGgAwIBAgIINzq1sRDfD4UwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ +bG9jYWxob3N0MB4XDTIzMDUwMzA1MTc1NFoXDTMzMDQzMDA1MjI1NFowFDESMBAG +A1UEAxMJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMlHIUMNt +dUeoT2k+s0qGuEKcn5/MxTQsvTHn2UtV6llpKzEJwt8PXq9wJgKY92KJIVunncJf +WH03Pt46w3rsXaN9MHswDgYDVR0PAQH/BAQDAgGmMB0GA1UdJQQWMBQGCCsGAQUF +BwMBBggrBgEFBQcDAjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQvGKzbHLBJ +gQRkitLTkbuzl8Yd7DAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwCgYIKoZI +zj0EAwIDSQAwRgIhAOVxPVGEjsRZ70QMfNsD2gfjrPguOVZZ6sgzKsVB38BuAiEA +0l2us3+vY1c5TARSVTk6OMmTCJrNYlBjYQP7BbWy6HQ= +-----END CERTIFICATE----- diff --git a/certs/cfssl.json b/certs/cfssl.json new file mode 100644 index 0000000..62ac8be --- /dev/null +++ b/certs/cfssl.json @@ -0,0 +1,42 @@ +{ + "signing": { + "default": { + "expiry": "87600h" + }, + "profiles": { + "rootca": { + "usages": [ + "signing", + "digital signature", + "key encipherment", + "cert sign", + "crl sign", + "server auth", + "client auth" + ], + "ca_constraint": { + "is_ca": true + }, + "expiry": "87600h" + }, + "server": { + "usages": [ + "signing", + "digital signing", + "key encipherment", + "server auth" + ], + "expiry": "87600h" + }, + "client": { + "usages": [ + "signing", + "digital signature", + "key encipherment", + "client auth" + ], + "expiry": "87600h" + } + } + } +} diff --git a/certs/client-key.pem b/certs/client-key.pem new file mode 100644 index 0000000..9a1811d --- /dev/null +++ b/certs/client-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEILI/RrEZWQ1sOKQSpISjVjzjfnys4SbD53TKVOjCxgNZoAoGCCqGSM49 +AwEHoUQDQgAEZDh+LpU9TmChPAocLxlfgCUwa4TBZnsf/NcwOyo2HupLj4Pha63V +kvbg2DSLFF+Fe4HoSiTWpq4Kwd8dnBxrkA== +-----END EC PRIVATE KEY----- diff --git a/certs/client.pem b/certs/client.pem new file mode 100644 index 0000000..44305c7 --- /dev/null +++ b/certs/client.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBmTCCAUCgAwIBAgIUYF9Sr1vgTtG1L5BBlj6yGrPpI0kwCgYIKoZIzj0EAwIw +FDESMBAGA1UEAxMJbG9jYWxob3N0MB4XDTIzMDUwMzA1MTgwMFoXDTMzMDQzMDA1 +MTgwMFowFDESMBAGA1UEAxMJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0D +AQcDQgAEZDh+LpU9TmChPAocLxlfgCUwa4TBZnsf/NcwOyo2HupLj4Pha63Vkvbg +2DSLFF+Fe4HoSiTWpq4Kwd8dnBxrkKNwMG4wDgYDVR0PAQH/BAQDAgWgMBMGA1Ud +JQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFAMk3Lo4DM5j +EubUazT8r2BuMjs6MBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATAKBggqhkjO +PQQDAgNHADBEAiAGxGpa2fdAC/2uchVei6AZNJhPTTEhgTVQ9F51kqrjuAIgD6UA +2puaVsOnIzKIUxZFY7zhOgZYU7O7sdNCfZX8ic0= +-----END CERTIFICATE----- diff --git a/certs/csr.json b/certs/csr.json new file mode 100644 index 0000000..2b84fda --- /dev/null +++ b/certs/csr.json @@ -0,0 +1,9 @@ +{ + "hosts": ["localhost", "127.0.0.1"], + "key": { + "algo": "ecdsa", + "size": 256 + }, + "CN": "localhost", + "names": [] +} diff --git a/certs/generate.sh b/certs/generate.sh new file mode 100755 index 0000000..a6b22f5 --- /dev/null +++ b/certs/generate.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# https://github.com/cloudflare/cfssl +# https://rob-blackbourn.medium.com/how-to-use-cfssl-to-create-self-signed-certificates-d55f76ba5781 +cfssl selfsign -config cfssl.json --profile rootca "Root CA" csr.json | cfssljson -bare ca +cfssl genkey csr.json | cfssljson -bare server +cfssl genkey csr.json | cfssljson -bare client +cfssl sign -ca ca.pem -ca-key ca-key.pem -config cfssl.json -profile server server.csr | cfssljson -bare server +cfssl sign -ca ca.pem -ca-key ca-key.pem -config cfssl.json -profile client client.csr | cfssljson -bare client +rm ./*.csr diff --git a/certs/server-key.pem b/certs/server-key.pem new file mode 100644 index 0000000..dc5e3b8 --- /dev/null +++ b/certs/server-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIBMUH4C8W0a0V4C7TQ0PowlIEPyz/j3AaffAOu6UmHNloAoGCCqGSM49 +AwEHoUQDQgAESCljhOQJWwvupZtt2HGPty6XJvuyqf7RpxheHTx5rWmEnLeI7DYK +DhKlIzDeH7Opf4rQrqOECxknfxf355/08A== +-----END EC PRIVATE KEY----- diff --git a/certs/server.pem b/certs/server.pem new file mode 100644 index 0000000..ca8347a --- /dev/null +++ b/certs/server.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBmjCCAUCgAwIBAgIUfqrcRuTGIZsXgJguUp2R/dw0dzgwCgYIKoZIzj0EAwIw +FDESMBAGA1UEAxMJbG9jYWxob3N0MB4XDTIzMDUwMzA1MTgwMFoXDTMzMDQzMDA1 +MTgwMFowFDESMBAGA1UEAxMJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0D +AQcDQgAESCljhOQJWwvupZtt2HGPty6XJvuyqf7RpxheHTx5rWmEnLeI7DYKDhKl +IzDeH7Opf4rQrqOECxknfxf355/08KNwMG4wDgYDVR0PAQH/BAQDAgWgMBMGA1Ud +JQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFJL7liSFyGC6 +f85d6ic2cgsqBLJSMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATAKBggqhkjO +PQQDAgNIADBFAiEAq3vKTlnGRw3EysUyNzAaYATDkspyKFnoQ+ItjGq4ixACICpN +tsAyZ4rKQNy0ZGbbUIncM7MY7S+zDAMz8P1Y9YtT +-----END CERTIFICATE----- diff --git a/cmd/client/main.go b/cmd/client/main.go index 5e8134c..0751b0a 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -2,6 +2,8 @@ package main import ( "context" + "crypto/tls" + "crypto/x509" "errors" "flag" "fmt" @@ -18,7 +20,7 @@ import ( "github.com/google/uuid" "go.uber.org/zap" "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/credentials" ) const defaultAddress = "localhost:50001" @@ -37,14 +39,7 @@ func main() { _ = logger.Sync() }() - example := "" - flag.StringVar( - &example, - "example", - exampleUnary, - fmt.Sprintf("%s or %s (default: %s)", exampleUnary, exampleBidirectional, exampleUnary), - ) - flag.Parse() + example := parseExample() conn, err := newConn() if err != nil { @@ -70,18 +65,57 @@ func main() { case exampleBidirectional: sayHelloBidirectional(ctx, client, logger) default: - logger.Fatal("example doesn't exist", zap.String("example", example)) + logger.Error("example doesn't exist", zap.String("example", example)) } } +func parseExample() string { + example := "" + flag.StringVar( + &example, + "example", + exampleUnary, + fmt.Sprintf("%s or %s (default: %s)", exampleUnary, exampleBidirectional, exampleUnary), + ) + flag.Parse() + return example +} + func newConn() (*grpc.ClientConn, error) { + transport, err := newTransportCredentials() + if err != nil { + return nil, err + } + return grpc.Dial( "localhost:50001", - grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithTransportCredentials(transport), grpc.WithBlock(), ) } +func newTransportCredentials() (credentials.TransportCredentials, error) { + cert, err := tls.LoadX509KeyPair("./certs/client.pem", "./certs/client-key.pem") + if err != nil { + return nil, fmt.Errorf("couldn't load client cert: %w", err) + } + + data, err := os.ReadFile("./certs/ca.pem") + if err != nil { + return nil, fmt.Errorf("couldn't load CA file: %w", err) + } + + capool := x509.NewCertPool() + if !capool.AppendCertsFromPEM(data) { + return nil, errors.New("couldn't add ca cart to cert pool") + } + + return credentials.NewTLS(&tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: capool, + }), nil +} + func sayHello(ctx context.Context, client proto.GreeterClient, logger *zap.Logger) { reply, err := client.SayHello(ctx, &proto.HelloRequest{Name: uuid.NewString()}) if err != nil { @@ -91,6 +125,7 @@ func sayHello(ctx context.Context, client proto.GreeterClient, logger *zap.Logge } func sayHelloBidirectional(ctx context.Context, client proto.GreeterClient, logger *zap.Logger) { + // ctx is used in the 2nd goroutine to close the stream client //nolint:contextcheck streamClient, err := client.SayHelloBidirectionalStream(context.Background()) if err != nil { @@ -122,9 +157,7 @@ func sayHelloBidirectional(ctx context.Context, client proto.GreeterClient, logg defer wg.Done() ticker := time.NewTicker(time.Second) - defer func() { - ticker.Stop() - }() + defer ticker.Stop() for { select { diff --git a/cmd/server/main.go b/cmd/server/main.go index 948710a..78dc0bd 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -2,6 +2,8 @@ package main import ( "context" + "crypto/tls" + "crypto/x509" "errors" "fmt" "io" @@ -19,10 +21,11 @@ import ( "go.uber.org/zap" "google.golang.org/grpc" "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" "google.golang.org/grpc/status" ) -const defaultAddress = "localhost:50001" +const defaultAddress = ":50001" func main() { logger, err := internal.NewLogger() @@ -54,10 +57,7 @@ func main() { srv.GracefulStop() }(srv, logger) - logger.Info( - "listening and serving", - zap.String("address", srv.lis.Addr().String()), - ) + logger.Info("listening and serving", zap.String("address", srv.lis.Addr().String())) if err = srv.Serve(); err != nil { logger.Fatal("something went wrong while serving", zap.Error(err)) @@ -72,6 +72,11 @@ type server struct { } func newServer(address string, logger *zap.Logger) (*server, error) { + transport, err := newTransportCredentials() + if err != nil { + return nil, err + } + lis, err := net.Listen("tcp", address) if err != nil { return nil, err @@ -80,6 +85,7 @@ func newServer(address string, logger *zap.Logger) (*server, error) { grpc_zap.ReplaceGrpcLoggerV2(logger) srv := grpc.NewServer( + grpc.Creds(transport), grpc.ChainUnaryInterceptor( grpc_recovery.UnaryServerInterceptor(grpc_recovery.WithRecoveryHandler(createPanicHandler(logger))), grpc_ctxtags.UnaryServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)), @@ -109,6 +115,29 @@ func (s *server) GracefulStop() { s.grpcSrv.GracefulStop() } +func newTransportCredentials() (credentials.TransportCredentials, error) { + cert, err := tls.LoadX509KeyPair("./certs/server.pem", "./certs/server-key.pem") + if err != nil { + return nil, fmt.Errorf("couldn't load server cert: %w", err) + } + + data, err := os.ReadFile("./certs/ca.pem") + if err != nil { + return nil, fmt.Errorf("couldn't load CA file: %w", err) + } + + capool := x509.NewCertPool() + if !capool.AppendCertsFromPEM(data) { + return nil, errors.New("couldn't add ca cart to cert pool") + } + + return credentials.NewTLS(&tls.Config{ + ClientAuth: tls.RequireAndVerifyClientCert, + Certificates: []tls.Certificate{cert}, + ClientCAs: capool, + }), nil +} + func createPanicHandler(logger *zap.Logger) grpc_recovery.RecoveryHandlerFunc { return func(p interface{}) error { logger.Panic(fmt.Sprintf("%v", p), zap.Stack("stack")) @@ -120,8 +149,6 @@ type greeterServer struct { proto.UnimplementedGreeterServer } -var _ proto.GreeterServer = (*greeterServer)(nil) - func (g *greeterServer) SayHello(_ context.Context, r *proto.HelloRequest) (*proto.HelloReply, error) { return &proto.HelloReply{ Message: buildHelloMsg(r.GetName()),