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.
This commit is contained in:
Josh Rickmar 2021-01-07 19:55:09 +00:00 committed by Dave Collins
parent 24ee6082cf
commit a0d26a7ba7
3 changed files with 72 additions and 6 deletions

View File

@ -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

View File

@ -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)

View File

@ -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)