Skip to content

Commit

Permalink
Allow client certificate authentication to dcrd RPC
Browse files Browse the repository at this point in the history
When the new config option --dcrdauthtype=clientcert is set, a client
certificate and key (set by --dcrdclientcert and --dcrdclientkey) will be used
to authenticate the dcrd JSON-RPC connection instead of basic authentication
with a user and password.
  • Loading branch information
jrick committed Apr 17, 2024
1 parent ad140d2 commit 0c735a7
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 13 deletions.
13 changes: 12 additions & 1 deletion chain/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ type RPCOptions struct {
Pass string
Dial func(ctx context.Context, network, address string) (net.Conn, error)
CA []byte
ClientCert []byte
ClientKey []byte
Insecure bool
}

Expand Down Expand Up @@ -532,7 +534,9 @@ func (s *Syncer) Run(ctx context.Context) (err error) {
addr = "wss://" + addr + "/ws"
}
opts := make([]wsrpc.Option, 0, 5)
opts = append(opts, wsrpc.WithBasicAuth(s.opts.User, s.opts.Pass))
if s.opts.User != "" {
opts = append(opts, wsrpc.WithBasicAuth(s.opts.User, s.opts.Pass))
}
opts = append(opts, wsrpc.WithNotifier(s.notifier))
opts = append(opts, wsrpc.WithoutPongDeadline())
if s.opts.Dial != nil {
Expand All @@ -554,6 +558,13 @@ func (s *Syncer) Run(ctx context.Context) (err error) {
},
RootCAs: pool,
}
if len(s.opts.ClientCert) != 0 {
keypair, err := tls.X509KeyPair(s.opts.ClientCert, s.opts.ClientKey)
if err != nil {
return err
}
tc.Certificates = []tls.Certificate{keypair}
}
opts = append(opts, wsrpc.WithTLSConfig(tc))
}
client, err := wsrpc.Dial(ctx, addr, opts...)
Expand Down
76 changes: 66 additions & 10 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ import (
flags "github.com/jessevdk/go-flags"
)

const (
// Authorization types.
authTypeBasic = "basic"
authTypeClientCert = "clientcert"
)

const (
defaultCAFilename = "dcrd.cert"
defaultConfigFilename = "dcrwallet.conf"
Expand All @@ -43,7 +49,7 @@ const (
defaultLogSize = "10M"
defaultRPCMaxClients = 10
defaultRPCMaxWebsockets = 25
defaultAuthType = "basic"
defaultAuthType = authTypeBasic
defaultEnableTicketBuyer = false
defaultEnableVoting = false
defaultPurchaseAccount = "default"
Expand All @@ -67,13 +73,15 @@ const (
)

var (
dcrdDefaultCAFile = filepath.Join(dcrutil.AppDataDir("dcrd", false), "rpc.cert")
defaultAppDataDir = dcrutil.AppDataDir("dcrwallet", false)
defaultConfigFile = filepath.Join(defaultAppDataDir, defaultConfigFilename)
defaultRPCKeyFile = filepath.Join(defaultAppDataDir, "rpc.key")
defaultRPCCertFile = filepath.Join(defaultAppDataDir, "rpc.cert")
defaultRPCClientCAFile = filepath.Join(defaultAppDataDir, "clients.pem")
defaultLogDir = filepath.Join(defaultAppDataDir, defaultLogDirname)
dcrdDefaultCAFile = filepath.Join(dcrutil.AppDataDir("dcrd", false), "rpc.cert")
defaultAppDataDir = dcrutil.AppDataDir("dcrwallet", false)
defaultConfigFile = filepath.Join(defaultAppDataDir, defaultConfigFilename)
defaultRPCKeyFile = filepath.Join(defaultAppDataDir, "rpc.key")
defaultRPCCertFile = filepath.Join(defaultAppDataDir, "rpc.cert")
defaultDcrdClientCertFile = filepath.Join(defaultAppDataDir, "dcrd-client.cert")
defaultDcrdClientKeyFile = filepath.Join(defaultAppDataDir, "dcrd-client.key")
defaultRPCClientCAFile = filepath.Join(defaultAppDataDir, "clients.pem")
defaultLogDir = filepath.Join(defaultAppDataDir, defaultLogDirname)
)

type config struct {
Expand Down Expand Up @@ -122,6 +130,9 @@ type config struct {
DisableClientTLS bool `long:"noclienttls" description:"Disable TLS for dcrd RPC; only allowed when connecting to localhost"`
DcrdUsername string `long:"dcrdusername" description:"dcrd RPC username; overrides --username"`
DcrdPassword string `long:"dcrdpassword" default-mask:"-" description:"dcrd RPC password; overrides --password"`
DcrdClientCert *cfgutil.ExplicitString `long:"dcrdclientcert" description:"TLS client certificate to present to authenticate RPC connections to dcrd"`
DcrdClientKey *cfgutil.ExplicitString `long:"dcrdclientkey" description:"Key for dcrd RPC client certificate"`
DcrdAuthType string `long:"dcrdauthtype" description:"Method for dcrd JSON-RPC client authentication (basic or clientcert)"`

// Proxy and Tor settings
Proxy string `long:"proxy" description:"Establish network connections and DNS lookups through a SOCKS5 proxy (e.g. 127.0.0.1:9050)"`
Expand Down Expand Up @@ -350,6 +361,8 @@ func loadConfig(ctx context.Context) (*config, []string, error) {
WalletPass: wallet.InsecurePubPassphrase,
CAFile: cfgutil.NewExplicitString(""),
ClientCAFile: cfgutil.NewExplicitString(defaultRPCClientCAFile),
DcrdClientCert: cfgutil.NewExplicitString(defaultDcrdClientCertFile),
DcrdClientKey: cfgutil.NewExplicitString(defaultDcrdClientKeyFile),
dial: new(net.Dialer).DialContext,
lookup: net.LookupIP,
PromptPass: defaultPromptPass,
Expand All @@ -361,6 +374,7 @@ func loadConfig(ctx context.Context) (*config, []string, error) {
LegacyRPCMaxClients: defaultRPCMaxClients,
LegacyRPCMaxWebsockets: defaultRPCMaxWebsockets,
JSONRPCAuthType: defaultAuthType,
DcrdAuthType: defaultAuthType,
EnableTicketBuyer: defaultEnableTicketBuyer,
EnableVoting: defaultEnableVoting,
PurchaseAccount: defaultPurchaseAccount,
Expand Down Expand Up @@ -458,6 +472,12 @@ func loadConfig(ctx context.Context) (*config, []string, error) {
if !cfg.ClientCAFile.ExplicitlySet() {
cfg.ClientCAFile.Value = filepath.Join(cfg.AppDataDir.Value, "clients.pem")
}
if !cfg.DcrdClientCert.ExplicitlySet() {
cfg.DcrdClientCert.Value = filepath.Join(cfg.AppDataDir.Value, "dcrd-client.cert")
}
if !cfg.DcrdClientKey.ExplicitlySet() {
cfg.DcrdClientKey.Value = filepath.Join(cfg.AppDataDir.Value, "dcrd-client.key")
}
if !cfg.LogDir.ExplicitlySet() {
cfg.LogDir.Value = filepath.Join(cfg.AppDataDir.Value, defaultLogDirname)
}
Expand Down Expand Up @@ -1006,6 +1026,8 @@ func loadConfig(ctx context.Context) (*config, []string, error) {
cfg.CAFile.Value = cleanAndExpandPath(cfg.CAFile.Value)
cfg.RPCCert.Value = cleanAndExpandPath(cfg.RPCCert.Value)
cfg.RPCKey.Value = cleanAndExpandPath(cfg.RPCKey.Value)
cfg.DcrdClientCert.Value = cleanAndExpandPath(cfg.DcrdClientCert.Value)
cfg.DcrdClientKey.Value = cleanAndExpandPath(cfg.DcrdClientKey.Value)
cfg.ClientCAFile.Value = cleanAndExpandPath(cfg.ClientCAFile.Value)

// If the dcrd username or password are unset, use the same auth as for
Expand All @@ -1019,10 +1041,44 @@ func loadConfig(ctx context.Context) (*config, []string, error) {
cfg.DcrdPassword = cfg.Password
}

switch cfg.DcrdAuthType {
case authTypeBasic:
case authTypeClientCert:
if cfg.DisableClientTLS {
err := fmt.Errorf("dcrdauthtype=clientcert is " +
"incompatible with disableclienttls")
fmt.Fprintln(os.Stderr, err)
return loadConfigError(err)
}
dcrdClientCertExists, _ := cfgutil.FileExists(
cfg.DcrdClientCert.Value)
if !dcrdClientCertExists {
err := fmt.Errorf("dcrdclientcert %q is required "+
"by dcrdauthtype=clientcert but does not exist",
cfg.DcrdClientCert.Value)
fmt.Fprintln(os.Stderr, err)
return loadConfigError(err)
}
dcrdClientKeyExists, _ := cfgutil.FileExists(
cfg.DcrdClientKey.Value)
if !dcrdClientKeyExists {
err := fmt.Errorf("dcrdclientkey %q is required "+
"by dcrdauthtype=clientcert but does not exist",
cfg.DcrdClientKey.Value)
fmt.Fprintln(os.Stderr, err)
return loadConfigError(err)
}
default:
err := fmt.Errorf("unknown dcrd authtype %q", cfg.DcrdAuthType)
fmt.Fprintln(os.Stderr, err)
fmt.Fprintln(os.Stderr, usageMessage)
return loadConfigError(err)
}

switch cfg.JSONRPCAuthType {
case "basic", "clientcert":
case authTypeBasic, authTypeClientCert:
default:
err := fmt.Errorf("unknown authtype %q", cfg.JSONRPCAuthType)
err := fmt.Errorf("unknown JSON-RPC authtype %q", cfg.JSONRPCAuthType)
fmt.Fprintln(os.Stderr, err)
fmt.Fprintln(os.Stderr, usageMessage)
return loadConfigError(err)
Expand Down
29 changes: 27 additions & 2 deletions dcrwallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -567,20 +567,28 @@ func spvLoop(ctx context.Context, w *wallet.Wallet) {
// disassociated from the client and a new connection is attempmted.
func rpcSyncLoop(ctx context.Context, w *wallet.Wallet) {
certs := readCAFile()
clientCert, clientKey := readClientCertKey()
dial := cfg.dial
if cfg.NoDcrdProxy {
dial = new(net.Dialer).DialContext
}
for {
syncer := chain.NewSyncer(w, &chain.RPCOptions{
rpcOptions := &chain.RPCOptions{
Address: cfg.RPCConnect,
DefaultPort: activeNet.JSONRPCClientPort,
User: cfg.DcrdUsername,
Pass: cfg.DcrdPassword,
Dial: dial,
CA: certs,
Insecure: cfg.DisableClientTLS,
})
}
if len(clientCert) != 0 {
rpcOptions.User = ""
rpcOptions.Pass = ""
rpcOptions.ClientCert = clientCert
rpcOptions.ClientKey = clientKey
}
syncer := chain.NewSyncer(w, rpcOptions)
err := syncer.Run(ctx)
if err != nil {
loggers.SyncLog.Errorf("Wallet synchronization stopped: %v", err)
Expand Down Expand Up @@ -611,3 +619,20 @@ func readCAFile() []byte {

return certs
}

func readClientCertKey() ([]byte, []byte) {
if cfg.DcrdAuthType != authTypeClientCert {
return nil, nil
}
cert, err := os.ReadFile(cfg.DcrdClientCert.Value)
if err != nil {
log.Warnf("Cannot open dcrd RPC client certificate: %v", err)
cert = nil
}
key, err := os.ReadFile(cfg.DcrdClientKey.Value)
if err != nil {
log.Warnf("Cannot open dcrd RPC client key: %v", err)
key = nil
}
return cert, key
}

0 comments on commit 0c735a7

Please sign in to comment.