Use TLS+auth for frontend connections.

This change is mostly a copy paste job from the TLS listeners and
autogenerated cert code from btcd.
This commit is contained in:
Josh Rickmar 2013-12-03 10:52:09 -05:00
parent af1438eecd
commit 3b04e3a4bc
3 changed files with 151 additions and 11 deletions

View file

@ -110,9 +110,6 @@ messages they originated from.
## TODO ## TODO
- Require authentication before wallet functionality can be accessed
- Serve frontend websocket connections over TLS
- Rescan the blockchain for missed transactions
- Documentation (specifically the websocket API additions) - Documentation (specifically the websocket API additions)
- Code cleanup - Code cleanup
- Optimize - Optimize

View file

@ -36,10 +36,12 @@ const (
) )
var ( var (
btcwalletHomeDir = btcutil.AppDataDir("btcwallet", false) btcwalletHomeDir = btcutil.AppDataDir("btcwallet", false)
defaultCAFile = filepath.Join(btcwalletHomeDir, defaultCAFilename) defaultCAFile = filepath.Join(btcwalletHomeDir, defaultCAFilename)
defaultConfigFile = filepath.Join(btcwalletHomeDir, defaultConfigFilename) defaultConfigFile = filepath.Join(btcwalletHomeDir, defaultConfigFilename)
defaultDataDir = btcwalletHomeDir defaultDataDir = btcwalletHomeDir
defaultRPCKeyFile = filepath.Join(btcwalletHomeDir, "rpc.key")
defaultRPCCertFile = filepath.Join(btcwalletHomeDir, "rpc.cert")
) )
type config struct { type config struct {
@ -52,6 +54,8 @@ type config struct {
DataDir string `short:"D" long:"datadir" description:"Directory to store wallets and transactions"` DataDir string `short:"D" long:"datadir" description:"Directory to store wallets and transactions"`
Username string `short:"u" long:"username" description:"Username for btcd authorization"` Username string `short:"u" long:"username" description:"Username for btcd authorization"`
Password string `short:"P" long:"password" description:"Password for btcd authorization"` Password string `short:"P" long:"password" description:"Password for btcd authorization"`
RPCCert string `long:"rpccert" description:"File containing the certificate file"`
RPCKey string `long:"rpckey" description:"File containing the certificate key"`
MainNet bool `long:"mainnet" description:"*DISABLED* Use the main Bitcoin network (default testnet3)"` MainNet bool `long:"mainnet" description:"*DISABLED* Use the main Bitcoin network (default testnet3)"`
Proxy string `long:"proxy" description:"Connect via SOCKS5 proxy (eg. 127.0.0.1:9050)"` Proxy string `long:"proxy" description:"Connect via SOCKS5 proxy (eg. 127.0.0.1:9050)"`
ProxyUser string `long:"proxyuser" description:"Username for proxy server"` ProxyUser string `long:"proxyuser" description:"Username for proxy server"`
@ -128,6 +132,8 @@ func loadConfig() (*config, []string, error) {
Connect: netParams(defaultBtcNet).connect, Connect: netParams(defaultBtcNet).connect,
SvrPort: netParams(defaultBtcNet).svrPort, SvrPort: netParams(defaultBtcNet).svrPort,
DataDir: defaultDataDir, DataDir: defaultDataDir,
RPCKey: defaultRPCKeyFile,
RPCCert: defaultRPCCertFile,
} }
// A config file in the current directory takes precedence. // A config file in the current directory takes precedence.

View file

@ -18,10 +18,16 @@ package main
import ( import (
"code.google.com/p/go.net/websocket" "code.google.com/p/go.net/websocket"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
_ "crypto/sha512" // for cert generation
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"encoding/pem"
"errors" "errors"
"fmt" "fmt"
"github.com/conformal/btcjson" "github.com/conformal/btcjson"
@ -29,9 +35,12 @@ import (
"github.com/conformal/btcwire" "github.com/conformal/btcwire"
"github.com/conformal/btcws" "github.com/conformal/btcws"
"github.com/conformal/go-socks" "github.com/conformal/go-socks"
"math/big"
"net" "net"
"net/http" "net/http"
"os"
"sync" "sync"
"time"
) )
var ( var (
@ -91,6 +100,8 @@ var (
// config, shutdown, etc.) // config, shutdown, etc.)
type server struct { type server struct {
port string port string
username string
password string
wg sync.WaitGroup wg sync.WaitGroup
listeners []net.Listener listeners []net.Listener
} }
@ -98,13 +109,32 @@ type server struct {
// newServer returns a new instance of the server struct. // newServer returns a new instance of the server struct.
func newServer() (*server, error) { func newServer() (*server, error) {
s := server{ s := server{
port: cfg.SvrPort, port: cfg.SvrPort,
username: cfg.Username,
password: cfg.Password,
}
// Check for existence of cert file and key file
if !fileExists(cfg.RPCKey) && !fileExists(cfg.RPCCert) {
// if both files do not exist, we generate them.
err := genKey(cfg.RPCKey, cfg.RPCCert)
if err != nil {
return nil, err
}
}
keypair, err := tls.LoadX509KeyPair(cfg.RPCCert, cfg.RPCKey)
if err != nil {
return nil, err
}
tlsConfig := tls.Config{
Certificates: []tls.Certificate{keypair},
} }
// IPv4 listener. // IPv4 listener.
var listeners []net.Listener var listeners []net.Listener
listenAddr4 := net.JoinHostPort("127.0.0.1", s.port) listenAddr4 := net.JoinHostPort("127.0.0.1", s.port)
listener4, err := net.Listen("tcp4", listenAddr4) listener4, err := tls.Listen("tcp4", listenAddr4, &tlsConfig)
if err != nil { if err != nil {
log.Errorf("RPCS: Couldn't create listener: %v", err) log.Errorf("RPCS: Couldn't create listener: %v", err)
return nil, err return nil, err
@ -113,7 +143,7 @@ func newServer() (*server, error) {
// IPv6 listener. // IPv6 listener.
listenAddr6 := net.JoinHostPort("::1", s.port) listenAddr6 := net.JoinHostPort("::1", s.port)
listener6, err := net.Listen("tcp6", listenAddr6) listener6, err := tls.Listen("tcp6", listenAddr6, &tlsConfig)
if err != nil { if err != nil {
log.Errorf("RPCS: Couldn't create listener: %v", err) log.Errorf("RPCS: Couldn't create listener: %v", err)
return nil, err return nil, err
@ -125,6 +155,97 @@ func newServer() (*server, error) {
return &s, nil return &s, nil
} }
// genkey generates a key/cert pair to the paths provided.
// TODO(oga) wrap errors with fmt.Errorf for more context?
func genKey(key, cert string) error {
log.Infof("Generating TLS certificates...")
priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
if err != nil {
return err
}
notBefore := time.Now()
notAfter := notBefore.Add(10 * 365 * 24 * time.Hour)
// end of ASN.1 time
endOfTime := time.Date(2049, 12, 31, 23, 59, 59, 0, time.UTC)
if notAfter.After(endOfTime) {
notAfter = endOfTime
}
template := x509.Certificate{
SerialNumber: new(big.Int).SetInt64(0),
Subject: pkix.Name{
Organization: []string{"btcwallet autogenerated cert"},
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
IsCA: true, // so can sign self.
BasicConstraintsValid: true,
}
host, err := os.Hostname()
if err != nil {
return err
}
template.DNSNames = append(template.DNSNames, host, "localhost")
needLocalhost := true
addrs, err := net.InterfaceAddrs()
if err != nil {
return err
}
for _, a := range addrs {
ip, _, err := net.ParseCIDR(a.String())
if err == nil {
if ip.String() == "127.0.0.1" {
needLocalhost = false
}
template.IPAddresses = append(template.IPAddresses, ip)
}
}
if needLocalhost {
localHost := net.ParseIP("127.0.0.1")
template.IPAddresses = append(template.IPAddresses, localHost)
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template,
&template, &priv.PublicKey, priv)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create certificate: %v\n", err)
os.Exit(-1)
}
certOut, err := os.Create(cert)
if err != nil {
return err
}
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
certOut.Close()
keyOut, err := os.OpenFile(key, os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
0600)
if err != nil {
os.Remove(cert)
return err
}
keybytes, err := x509.MarshalECPrivateKey(priv)
if err != nil {
os.Remove(key)
os.Remove(cert)
return err
}
pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keybytes})
keyOut.Close()
log.Info("Done generating TLS certificates")
return nil
}
// handleRPCRequest processes a JSON-RPC request from a frontend. // handleRPCRequest processes a JSON-RPC request from a frontend.
func (s *server) handleRPCRequest(w http.ResponseWriter, r *http.Request) { func (s *server) handleRPCRequest(w http.ResponseWriter, r *http.Request) {
frontend := make(chan []byte) frontend := make(chan []byte)
@ -535,6 +656,8 @@ func (s *server) Start() {
// Use a sync.Once to insure no extra duplicators run. // Use a sync.Once to insure no extra duplicators run.
go duplicateOnce.Do(frontendListenerDuplicator) go duplicateOnce.Do(frontendListenerDuplicator)
log.Trace("Starting RPC server")
// TODO(jrick): We need some sort of authentication before websocket // TODO(jrick): We need some sort of authentication before websocket
// connections are allowed, and perhaps TLS on the server as well. // connections are allowed, and perhaps TLS on the server as well.
serveMux := http.NewServeMux() serveMux := http.NewServeMux()
@ -542,7 +665,21 @@ func (s *server) Start() {
serveMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { serveMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
s.handleRPCRequest(w, r) s.handleRPCRequest(w, r)
}) })
serveMux.Handle("/frontend", websocket.Handler(frontendSendRecv)) wsServer := websocket.Server{
Handler: websocket.Handler(func(ws *websocket.Conn) {
frontendSendRecv(ws)
}),
Handshake: func(_ *websocket.Config, r *http.Request) error {
login := s.username + ":" + s.password
auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(login))
authhdr := r.Header["Authorization"]
if len(authhdr) <= 0 || authhdr[0] != auth {
return errors.New("auth failure")
}
return nil
},
}
serveMux.Handle("/frontend", wsServer)
for _, listener := range s.listeners { for _, listener := range s.listeners {
s.wg.Add(1) s.wg.Add(1)
go func(listener net.Listener) { go func(listener net.Listener) {