From 81ad318d075b7af0ee0760122897b71e0e756210 Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Tue, 7 Mar 2023 14:24:55 +0100 Subject: [PATCH] gobgp,gobgpd: implement TLS client authentication. --- cmd/gobgp/common.go | 107 +++++++++++++++++++++++++++++++++++++++++--- cmd/gobgp/root.go | 32 +++++++------ cmd/gobgpd/main.go | 24 +++++++++- 3 files changed, 143 insertions(+), 20 deletions(-) diff --git a/cmd/gobgp/common.go b/cmd/gobgp/common.go index 15e6c2fa9..55e49d725 100644 --- a/cmd/gobgp/common.go +++ b/cmd/gobgp/common.go @@ -18,7 +18,12 @@ package main import ( "bytes" "context" + "crypto" + "crypto/tls" + "crypto/x509" "encoding/json" + "encoding/pem" + "errors" "fmt" "net" "os" @@ -191,19 +196,111 @@ func extractReserved(args []string, keys map[string]int) (map[string][]string, e return m, nil } +func loadCertificatePEM(filePath string) (*x509.Certificate, error) { + content, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + + rest := content + var block *pem.Block + var cert *x509.Certificate + for len(rest) > 0 { + block, rest = pem.Decode(content) + if block == nil { + // no PEM data found, rest will not have been modified + break + } + content = rest + switch block.Type { + case "CERTIFICATE": + cert, err = x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, err + } + return cert, err + default: + // not the PEM block we're looking for + continue + } + } + return nil, errors.New("no certificate PEM block found") +} + +func loadKeyPEM(filePath string) (crypto.PrivateKey, error) { + content, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + + rest := content + var block *pem.Block + var key crypto.PrivateKey + for len(rest) > 0 { + block, rest = pem.Decode(content) + if block == nil { + // no PEM data found, rest will not have been modified + break + } + switch block.Type { + case "RSA PRIVATE KEY": + key, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + return key, err + case "PRIVATE KEY": + key, err = x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + return key, err + case "EC PRIVATE KEY": + key, err = x509.ParseECPrivateKey(block.Bytes) + if err != nil { + return nil, err + } + return key, err + default: + // not the PEM block we're looking for + continue + } + } + return nil, errors.New("no private key PEM block found") +} + func newClient(ctx context.Context) (api.GobgpApiClient, context.CancelFunc, error) { grpcOpts := []grpc.DialOption{grpc.WithBlock()} if globalOpts.TLS { var creds credentials.TransportCredentials - if globalOpts.CaFile == "" { - creds = credentials.NewClientTLSFromCert(nil, "") - } else { - var err error - creds, err = credentials.NewClientTLSFromFile(globalOpts.CaFile, "") + tlsConfig := new(tls.Config) + if len(globalOpts.CaFile) != 0 { + pemCerts, err := os.ReadFile(globalOpts.CaFile) if err != nil { exitWithError(err) } + tlsConfig.RootCAs = x509.NewCertPool() + if !tlsConfig.RootCAs.AppendCertsFromPEM(pemCerts) { + exitWithError(errors.New("no valid CA certificates to load")) + } + } + if len(globalOpts.ClientCertFile) != 0 && len(globalOpts.ClientKeyFile) != 0 { + cert, err := loadCertificatePEM(globalOpts.ClientCertFile) + if err != nil { + exitWithError(fmt.Errorf("failed to load client certificate: %w", err)) + } + key, err := loadKeyPEM(globalOpts.ClientKeyFile) + if err != nil { + exitWithError(fmt.Errorf("failed to load client key: %w", err)) + } + tlsConfig.Certificates = []tls.Certificate{ + { + Certificate: [][]byte{cert.Raw}, + PrivateKey: key, + }, + } } + creds = credentials.NewTLS(tlsConfig) grpcOpts = append(grpcOpts, grpc.WithTransportCredentials(creds)) } else { grpcOpts = append(grpcOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) diff --git a/cmd/gobgp/root.go b/cmd/gobgp/root.go index dfb260038..336209c24 100644 --- a/cmd/gobgp/root.go +++ b/cmd/gobgp/root.go @@ -26,21 +26,25 @@ import ( ) var globalOpts struct { - Host string - Port int - Target string - Debug bool - Quiet bool - Json bool - GenCmpl bool - BashCmplFile string - PprofPort int - TLS bool - CaFile string + Host string + Port int + Target string + Debug bool + Quiet bool + Json bool + GenCmpl bool + BashCmplFile string + PprofPort int + TLS bool + ClientCertFile string + ClientKeyFile string + CaFile string } -var client api.GobgpApiClient -var ctx context.Context +var ( + client api.GobgpApiClient + ctx context.Context +) func newRootCmd() *cobra.Command { cobra.EnablePrefixMatching = true @@ -92,6 +96,8 @@ func newRootCmd() *cobra.Command { rootCmd.PersistentFlags().StringVarP(&globalOpts.BashCmplFile, "bash-cmpl-file", "", "gobgp-completion.bash", "bash cmpl filename") rootCmd.PersistentFlags().IntVarP(&globalOpts.PprofPort, "pprof-port", "r", 0, "pprof port") rootCmd.PersistentFlags().BoolVarP(&globalOpts.TLS, "tls", "", false, "connection uses TLS if true, else plain TCP") + rootCmd.PersistentFlags().StringVarP(&globalOpts.ClientCertFile, "tls-client-cert-file", "", "", "Optional file path to TLS client certificate") + rootCmd.PersistentFlags().StringVarP(&globalOpts.ClientKeyFile, "tls-client-key-file", "", "", "Optional file path to TLS client key") rootCmd.PersistentFlags().StringVarP(&globalOpts.CaFile, "tls-ca-file", "", "", "The file containing the CA root cert file") globalCmd := newGlobalCmd() diff --git a/cmd/gobgpd/main.go b/cmd/gobgpd/main.go index a48e25510..2ef065c5e 100644 --- a/cmd/gobgpd/main.go +++ b/cmd/gobgpd/main.go @@ -17,6 +17,8 @@ package main import ( + "crypto/tls" + "crypto/x509" "fmt" "io" "net/http" @@ -63,6 +65,7 @@ func main() { TLS bool `long:"tls" description:"enable TLS authentication for gRPC API"` TLSCertFile string `long:"tls-cert-file" description:"The TLS cert file"` TLSKeyFile string `long:"tls-key-file" description:"The TLS key file"` + TLSClientCAFile string `long:"tls-client-ca-file" description:"Optional TLS client CA file to authenticate clients against"` Version bool `long:"version" description:"show version number"` } _, err := flags.Parse(&opts) @@ -142,10 +145,27 @@ func main() { maxSize := 256 << 20 grpcOpts := []grpc.ServerOption{grpc.MaxRecvMsgSize(maxSize), grpc.MaxSendMsgSize(maxSize)} if opts.TLS { - creds, err := credentials.NewServerTLSFromFile(opts.TLSCertFile, opts.TLSKeyFile) + // server cert/key + cert, err := tls.LoadX509KeyPair(opts.TLSCertFile, opts.TLSKeyFile) if err != nil { - logger.Fatalf("Failed to generate credentials: %v", err) + logger.Fatalf("Failed to load server certificate/keypair: %v", err) } + tlsConfig := &tls.Config{Certificates: []tls.Certificate{cert}} + + // client CA + if len(opts.TLSClientCAFile) != 0 { + tlsConfig.ClientCAs = x509.NewCertPool() + pemCerts, err := os.ReadFile(opts.TLSClientCAFile) + if err != nil { + logger.Fatalf("Failed to load client CA certificates from %q: %v", opts.TLSClientCAFile, err) + } + if ok := tlsConfig.ClientCAs.AppendCertsFromPEM(pemCerts); !ok { + logger.Fatalf("No valid client CA certificates in %q", opts.TLSClientCAFile) + } + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + } + + creds := credentials.NewTLS(tlsConfig) grpcOpts = append(grpcOpts, grpc.Creds(creds)) }