From a0d26a7ba76faba40aed20dae22fb5567ec0a891 Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Thu, 7 Jan 2021 19:55:09 +0000 Subject: [PATCH] rpcserver: Allow TLS client cert authentication This adds an alternative method for TLS clients to authenticate to the server by presenting a client certificate in the TLS handshake. In order for dcrd to trust this client, the certificate authority must be added as a trusted root (by default, in clients.pem in dcrd's application data directory) as well as setting the config value authtype=clientcerts. HTTP POST and websocket clients need not provide HTTP Basic authentication when client certificates are used. In future changes, TLS client authentication may be used to limit access to particular RPCs by various clients, which is possible now that each client can present their own cryptographic identity. However, at the moment, enabling TLS client authentication requires it at the TLS listener level, and therefore breaks the --rpclimituser/rpclimitpass mechanism to provide non-admin client authentication. Additional changes will be required to provide the client identity through the request context in order to bring back limited access for clients. Users depending on the limited user settings should avoid using client certificates in the meantime. --- config.go | 55 ++++++++++++++++++++++++++++++--- internal/rpcserver/rpcserver.go | 9 +++++- server.go | 14 +++++++++ 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/config.go b/config.go index 07c117d2..cc026d88 100644 --- a/config.go +++ b/config.go @@ -79,6 +79,10 @@ const ( defaultTxIndex = false defaultAddrIndex = false defaultNoExistsAddrIndex = false + + // Authorization types. + authTypeBasic = "basic" + authTypeClientCert = "clientcert" ) var ( @@ -90,8 +94,10 @@ var ( knownDbTypes = database.SupportedDrivers() // Constructed defaults for RPC server options and policy. - defaultRPCKeyFile = filepath.Join(defaultHomeDir, "rpc.key") - defaultRPCCertFile = filepath.Join(defaultHomeDir, "rpc.cert") + defaultRPCKeyFile = filepath.Join(defaultHomeDir, "rpc.key") + defaultRPCCertFile = filepath.Join(defaultHomeDir, "rpc.cert") + defaultRPCAuthType = authTypeBasic + defaultRPCClientCAs = filepath.Join(defaultHomeDir, "clients.pem") ) // runServiceCommand is only set to a real function on Windows. It is used @@ -133,6 +139,8 @@ type config struct { RPCListeners []string `long:"rpclisten" description:"Add an interface/port to listen for RPC connections (default port: 9109, testnet: 19109)"` RPCUser string `short:"u" long:"rpcuser" description:"Username for RPC connections"` RPCPass string `short:"P" long:"rpcpass" default-mask:"-" description:"Password for RPC connections"` + RPCAuthType string `long:"authtype" description:"Method for RPC client authentication (basic or clientcert)"` + RPCClientCAs string `long:"clientcafile" description:"File containing Certificate Authorities to verify TLS client certificates; requires authtype=clientcert"` RPCLimitUser string `long:"rpclimituser" description:"Username for limited RPC connections"` RPCLimitPass string `long:"rpclimitpass" default-mask:"-" description:"Password for limited RPC connections"` RPCCert string `long:"rpccert" description:"File containing the certificate file"` @@ -563,6 +571,8 @@ func loadConfig(appName string) (*config, []string, error) { // RPC server options and policy. RPCCert: defaultRPCCertFile, RPCKey: defaultRPCKeyFile, + RPCAuthType: defaultRPCAuthType, + RPCClientCAs: defaultRPCClientCAs, TLSCurve: defaultTLSCurve, RPCMaxClients: defaultMaxRPCClients, RPCMaxWebsockets: defaultMaxRPCWebsockets, @@ -673,6 +683,11 @@ func loadConfig(appName string) (*config, []string, error) { } else { cfg.RPCCert = preCfg.RPCCert } + if preCfg.RPCClientCAs == defaultRPCClientCAs { + cfg.RPCClientCAs = filepath.Join(cfg.HomeDir, "clients.pem") + } else { + cfg.RPCClientCAs = preCfg.RPCClientCAs + } if preCfg.LogDir == defaultLogDir { cfg.LogDir = filepath.Join(cfg.HomeDir, defaultLogDirname) } else { @@ -950,12 +965,27 @@ func loadConfig(appName string) (*config, []string, error) { return nil, nil, err } - // The RPC server is disabled if no username or password is provided. - if (cfg.RPCUser == "" || cfg.RPCPass == "") && + // The RPC server is disabled if no username or password is provided + // under basic user/pass authentication. + if cfg.RPCAuthType == authTypeBasic && + (cfg.RPCUser == "" || cfg.RPCPass == "") && (cfg.RPCLimitUser == "" || cfg.RPCLimitPass == "") { cfg.DisableRPC = true } + // Check to make sure RPC usernames and passwords are not provided under + // client cert authentiation. + if cfg.RPCAuthType == authTypeClientCert { + switch { + case cfg.RPCUser != "", cfg.RPCPass != "", + cfg.RPCLimitUser != "", cfg.RPCLimitPass != "": + str := "%s: RPC usernames and passwords are not allowed " + + "with --authtype=clientcert" + err := fmt.Errorf(str, funcName) + return nil, nil, err + } + } + // Default RPC to listen on localhost only. if !cfg.DisableRPC && len(cfg.RPCListeners) == 0 { addrs, err := net.LookupHost("localhost") @@ -1084,8 +1114,17 @@ func loadConfig(appName string) (*config, []string, error) { cfg.RPCListeners = normalizeAddresses(cfg.RPCListeners, cfg.params.rpcPort) + // The authtype config must be one of "basic" or "clientcert". + switch cfg.RPCAuthType { + case authTypeBasic, authTypeClientCert: + default: + err := fmt.Errorf("%s: invalid authtype option %q", + funcName, cfg.RPCAuthType) + return nil, nil, err + } + // Only allow TLS to be disabled if the RPC is bound to localhost - // addresses. + // addresses, and when client cert auth is not used. if !cfg.DisableRPC && cfg.DisableTLS { allowedTLSListeners := map[string]struct{}{ "localhost": {}, @@ -1108,6 +1147,12 @@ func loadConfig(appName string) (*config, []string, error) { return nil, nil, err } } + + if cfg.RPCAuthType == authTypeClientCert { + err := fmt.Errorf("%s: TLS may not be disabled with "+ + "authtype=clientcert", funcName) + return nil, nil, err + } } // Add default port to all added peer addresses if needed and remove diff --git a/internal/rpcserver/rpcserver.go b/internal/rpcserver/rpcserver.go index 8fd6885b..3a3cb47a 100644 --- a/internal/rpcserver/rpcserver.go +++ b/internal/rpcserver/rpcserver.go @@ -5532,8 +5532,15 @@ func (s *Server) authMAC(dst, auth []byte) []byte { // of the server (true) or whether the user is limited (false). The second is // always false if the first is. func (s *Server) checkAuth(r *http.Request, require bool) (bool, bool, error) { + // If admin-level RPC user and pass options are not set, this always + // succeeds. This will be the case when TLS client certificates are + // being used for authentication. + if s.authsha == ([32]byte{}) { + return true, true, nil + } + authhdr := r.Header["Authorization"] - if len(authhdr) <= 0 { + if len(authhdr) == 0 { if require { log.Warnf("RPC authentication failure from %s", r.RemoteAddr) diff --git a/server.go b/server.go index 1edb380d..c6c79484 100644 --- a/server.go +++ b/server.go @@ -10,6 +10,7 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/tls" + "crypto/x509" "encoding/binary" "errors" "fmt" @@ -3288,6 +3289,19 @@ func setupRPCListeners() ([]net.Listener, error) { MinVersion: tls.VersionTLS12, } + if cfg.RPCAuthType == authTypeClientCert { + pemCerts, err := ioutil.ReadFile(cfg.RPCClientCAs) + if err != nil { + return nil, err + } + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + tlsConfig.ClientCAs = x509.NewCertPool() + if !tlsConfig.ClientCAs.AppendCertsFromPEM(pemCerts) { + return nil, fmt.Errorf("no certificates found in %q", + cfg.RPCClientCAs) + } + } + // Change the standard net.Listen function to the tls one. listenFunc = func(net string, laddr string) (net.Listener, error) { return tls.Listen(net, laddr, &tlsConfig)