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)