From 08364101a999a1e23dd7b18a8cf9d27d5afae1d5 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Wed, 26 Oct 2022 15:12:32 -0700 Subject: [PATCH 01/42] lndsigner: initial commit --- .gitignore | 18 + LICENSE | 22 + README.md | 116 +++++- config.go | 538 +++++++++++++++++++++++++ doc.go | 6 + go.mod | 89 +++++ go.sum | 463 ++++++++++++++++++++++ keyring/keyring.go | 793 +++++++++++++++++++++++++++++++++++++ keyring/log.go | 29 ++ lndsigner.go | 417 +++++++++++++++++++ log.go | 39 ++ macaroons.go | 183 +++++++++ proto/gen_protos.sh | 37 ++ proto/lightning.pb.go | 390 ++++++++++++++++++ proto/lightning.proto | 76 ++++ proto/lightning_grpc.pb.go | 109 +++++ proto/signer.pb.go | 462 +++++++++++++++++++++ proto/signer.proto | 99 +++++ proto/signer_grpc.pb.go | 167 ++++++++ proto/walletkit.pb.go | 234 +++++++++++ proto/walletkit.proto | 45 +++ proto/walletkit_grpc.pb.go | 125 ++++++ rpcserver.go | 179 +++++++++ signer_server.go | 224 +++++++++++ vault/backend.go | 616 ++++++++++++++++++++++++++++ vault/keys.go | 231 +++++++++++ vault/paths.go | 225 +++++++++++ walletkit_server.go | 103 +++++ 28 files changed, 6034 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 config.go create mode 100644 doc.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 keyring/keyring.go create mode 100644 keyring/log.go create mode 100644 lndsigner.go create mode 100644 log.go create mode 100644 macaroons.go create mode 100755 proto/gen_protos.sh create mode 100644 proto/lightning.pb.go create mode 100644 proto/lightning.proto create mode 100644 proto/lightning_grpc.pb.go create mode 100644 proto/signer.pb.go create mode 100644 proto/signer.proto create mode 100644 proto/signer_grpc.pb.go create mode 100644 proto/walletkit.pb.go create mode 100644 proto/walletkit.proto create mode 100644 proto/walletkit_grpc.pb.go create mode 100644 rpcserver.go create mode 100644 signer_server.go create mode 100644 vault/backend.go create mode 100644 vault/keys.go create mode 100644 vault/paths.go create mode 100644 walletkit_server.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4df5f8d --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +vault-plugin-lndsigner +lndsignerd + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..96c6989 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +Copyright (C) 2013-2017 The btcsuite developers +Copyright (C) 2015-2016 The Decred developers +Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index f33dd9f..04e71f5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,116 @@ # lndsigner -lndsigner +`lndsigner` is a [remote signer](https://github.com/lightningnetwork/lnd/blob/master/docs/remote-signing.md) for [lnd](https://github.com/lightningnetwork/lnd). Currently, it can do the following: +- [x] store seeds for multiple nodes in [Hashicorp Vault](https://github.com/hashicorp/vault/) +- [x] perform derivation and signing operations in a Vault plugin +- [x] import macaroon root key from environment variable +- [x] export account list and macaroon for watch-only lnd instances on startup +- [x] sign messages for network announcements +- [x] derive shared keys for peer connections +- [x] sign PSBTs for on-chain transactions, channel openings/closes, HTLC updates, etc. +- [x] verify macaroons on grpc request +- [ ] perform musig2 ops +- [ ] add and verify macaroon caveats (like expiration or ip address restriction) +- [ ] export account list and macaroon for watch-only lnd instances when node is created in vault or some other way without restarting the signer +- [ ] track on-chain wallet state and enforce policy for on-chain transactions +- [ ] track channel state and enforce policy for channel updates +- [ ] allow preauthorizations for on-chain transactions, channel opens/closes, and channel updates +- [ ] allow an interceptor to determine whether or not to sign +- [ ] run unit tests and itests, do automated/reproducible builds +- [ ] log and gather metrics coherently +- [ ] enforce custom SELinux policy to harden plugin execution environment + +## Usage + +Ensure you have `bitcoind`, `lnd`, and `vault` installed. Build `signer` using Go 1.18+ from this directory: + +``` +$ go install ./cmd/... +``` + +Create a directory `~/vault_plugins` and then move the `vault-plugin-lndsigner` binary to it. + +Start Vault from your home directory: + +``` +~$ vault server -dev -dev-root-token-id=root -dev-plugin-dir=./vault_plugins -log-level=trace +``` + +Enable the signer plugin: + +``` +$ VAULT_ADDR=http://127.0.0.1:8200 VAULT_TOKEN=root vault secrets enable --path=lndsigner vault-plugin-lndsigner +``` + +Create a new node: + +``` +$ VAULT_ADDR=http://127.0.0.1:8200 VAULT_TOKEN=root vault write lndsigner/lnd-nodes network=regtest + +``` + +Note that this should return a pubkey for the new node: + +``` +Key Value +--- ----- +node 03dc60dce282bb96abb4328c3e19640aa4f87defc400458322b80f0b73c2b14263 +``` + +You can also list the nodes as follows: + +``` +$ VAULT_ADDR=http://127.0.0.1:8200 VAULT_TOKEN=root vault read lndsigner/lnd-nodes +Key Value +--- ----- +03dc60dce282bb96abb4328c3e19640aa4f87defc400458322b80f0b73c2b14263 1 +``` + +The value is the HDCoinType used for the wallet, derived from the network specified above. Note that the plugin and signer support multiple nodes, so you can add more nodes by writing as above. + +Create a directory `~/.lndsigner` with a `signer.conf` similar to: + +``` +rpclisten=tcp://127.0.0.1:10021 +regtest=true +``` + +Run the signer binary as follows, with the macaroon root key in `SIGNER_MAC_ROOT_KEY`: + +``` +~/.lndsigner$ VAULT_ADDR=http://127.0.0.1:8200 VAULT_TOKEN=root \ + SIGNER_MAC_ROOT_KEY=6666666666555555555544444444443333333333222222222211111111114321 \ + lndsignerd --outputmacaroon=signer.custom.macaroon --outputaccounts=accounts.json --debuglevel=trace +``` + +You'll notice some new files created, such as `tls.key` and `tls.cert` for the signer's GRPC interface. You'll also notice a file called `accounts.json.*pubkey*` and another called `signer.custom.macaroon.*pubkey*`, both of which you'll need to pass to `lnd` in future steps. If you created multiple nodes, you'll notice an `accounts.json` and a `signer.custom.macaroon` for each one, with the node's pubkey appended to the filename. + +Ensure you have a `bitcoind` instance running locally on regtest. Then, create a directory `~/.lnd-watchonly` with a `lnd.conf` similar to: + +``` +[bitcoin] +bitcoin.active=true +bitcoin.regtest=true +bitcoin.node=bitcoind + +[remotesigner] +remotesigner.enable=true +remotesigner.rpchost=127.0.0.1:10021 +remotesigner.tlscertpath=/home/*user*/.lndsigner/tls.cert +remotesigner.macaroonpath=/home/*user*/.lndsigner/signer.custom.macaroon.*pubkey* +``` + +Note that `lnd` will need the macaroon file to authenticate itself to the signer. + +Now, run `lnd` in watch-only mode: + +``` +~/.lnd-watchonly$ lnd --lnddir=. +``` + +Create the watch-only wallet using the accounts exported by the signer: + +``` +~$ lncli createwatchonly .lndsigner/accounts.json.*pubkey* +``` + +Now you can use your node as usual. Note that MuSig2 isn't supported yet. If you created multiple nodes in the vault, you can create a separate directory for each watch-only node and start it as above. diff --git a/config.go b/config.go new file mode 100644 index 0000000..84b6221 --- /dev/null +++ b/config.go @@ -0,0 +1,538 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package lndsigner + +import ( + "encoding/hex" + "fmt" + "net" + "os" + "os/user" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + flags "github.com/jessevdk/go-flags" +) + +const ( + defaultConfigFilename = "signer.conf" + defaultTLSCertFilename = "tls.cert" + defaultTLSKeyFilename = "tls.key" + defaultLogLevel = "info" + defaultLogDirname = "logs" + defaultLogFilename = "signer.log" + defaultRPCPort = 10009 + defaultRPCHost = "localhost" + + defaultMaxLogFiles = 3 + defaultMaxLogFileSize = 10 + + // DefaultAutogenValidity is the default validity of a self-signed + // certificate. The value corresponds to 14 months + // (14 months * 30 days * 24 hours). + defaultTLSCertDuration = 14 * 30 * 24 * time.Hour + + // Set defaults for a health check which ensures that the TLS certificate + // is not expired. Although this check is off by default (not all setups + // require it), we still set the other default values so that the health + // check can be easily enabled with sane defaults. + defaultTLSInterval = time.Minute + defaultTLSTimeout = time.Second * 5 + defaultTLSBackoff = time.Minute + defaultTLSAttempts = 0 +) + +var ( + // DefaultSignerDir is the default directory where lnd tries to find its + // configuration file and store its data. This is a directory in the + // user's application data, for example: + // C:\Users\\AppData\Local\Lndsigner on Windows + // ~/.lndsigner on Linux + // ~/Library/Application Support/Lndsigner on MacOS + DefaultSignerDir = btcutil.AppDataDir("lndsigner", false) + + // DefaultConfigFile is the default full path of lnd's configuration + // file. + DefaultConfigFile = filepath.Join(DefaultSignerDir, defaultConfigFilename) + + defaultLogDir = filepath.Join(DefaultSignerDir, defaultLogDirname) + + defaultTLSCertPath = filepath.Join(DefaultSignerDir, defaultTLSCertFilename) + defaultTLSKeyPath = filepath.Join(DefaultSignerDir, defaultTLSKeyFilename) +) + +// Config defines the configuration options for lnd. +// +// See LoadConfig for further details regarding the configuration +// loading+parsing process. +type Config struct { + SignerDir string `long:"signerdir" description:"The base directory that contains signer's data, logs, configuration file, etc."` + ConfigFile string `short:"C" long:"configfile" description:"Path to configuration file"` + + TLSCertPath string `long:"tlscertpath" description:"Path to write the TLS certificate for lnd's RPC services"` + TLSKeyPath string `long:"tlskeypath" description:"Path to write the TLS private key for lnd's RPC services"` + TLSExtraIPs []string `long:"tlsextraip" description:"Adds an extra ip to the generated certificate"` + TLSExtraDomains []string `long:"tlsextradomain" description:"Adds an extra domain to the generated certificate"` + TLSAutoRefresh bool `long:"tlsautorefresh" description:"Re-generate TLS certificate and key if the IPs or domains are changed"` + TLSDisableAutofill bool `long:"tlsdisableautofill" description:"Do not include the interface IPs or the system hostname in TLS certificate, use first --tlsextradomain as Common Name instead, if set"` + TLSCertDuration time.Duration `long:"tlscertduration" description:"The duration for which the auto-generated TLS certificate will be valid for"` + + OutputMacaroon string `long:"outputmacaroon" description:"Path to write a signer macaroon for the watch-only node"` + OutputAccounts string `long:"outputaccounts" description:"Path to write a JSON file with xpubs for the watch-only node"` + + LogDir string `long:"logdir" description:"Directory to log output."` + MaxLogFiles int `long:"maxlogfiles" description:"Maximum logfiles to keep (0 for no rotation)"` + MaxLogFileSize int `long:"maxlogfilesize" description:"Maximum logfile size in MB"` + + // We'll parse these 'raw' string arguments into real net.Addrs in the + // loadConfig function. We need to expose the 'raw' strings so the + // command line library can access them. + // Only the parsed net.Addrs should be used! + RawRPCListeners []string `long:"rpclisten" description:"Add an interface/port/socket to listen for RPC connections"` + RPCListeners []net.Addr + + DebugLevel string `short:"d" long:"debuglevel" description:"Logging level for all subsystems {trace, debug, info, warn, error, critical}"` + + // MainNet bool `long:"mainnet" description:"NOT RECOMMENDED: Use the main network"` + TestNet3 bool `long:"testnet" description:"Use the test network"` + SimNet bool `long:"simnet" description:"Use the simulation test network"` + RegTest bool `long:"regtest" description:"Use the regression test network"` + SigNet bool `long:"signet" description:"Use the signet test network"` + + // ActiveNetParams contains parameters of the target chain. + ActiveNetParams chaincfg.Params + + // seed contains the 32-byte wallet seed. + seed [32]byte + + // macRootKey contains the 32-byte macaroon root key. + macRootKey [32]byte +} + +// DefaultConfig returns all default values for the Config struct. +func DefaultConfig() Config { + return Config{ + SignerDir: DefaultSignerDir, + ConfigFile: DefaultConfigFile, + DebugLevel: defaultLogLevel, + TLSCertPath: defaultTLSCertPath, + TLSKeyPath: defaultTLSKeyPath, + TLSCertDuration: defaultTLSCertDuration, + LogDir: defaultLogDir, + MaxLogFiles: defaultMaxLogFiles, + MaxLogFileSize: defaultMaxLogFileSize, + ActiveNetParams: chaincfg.RegressionNetParams, + } +} + +// LoadConfig initializes and parses the config using a config file and command +// line options. +// +// The configuration proceeds as follows: +// 1. Start with a default config with sane settings +// 2. Pre-parse the command line to check for an alternative config file +// 3. Load configuration file overwriting defaults with any specified options +// 4. Parse CLI options and overwrite/add any specified options +func LoadConfig() (*Config, error) { + // Pre-parse the command line options to pick up an alternative config + // file. + preCfg := DefaultConfig() + if _, err := flags.Parse(&preCfg); err != nil { + return nil, err + } + + // Show the version and exit if the version flag was specified. + appName := filepath.Base(os.Args[0]) + appName = strings.TrimSuffix(appName, filepath.Ext(appName)) + usageMessage := fmt.Sprintf("Use %s -h to show usage", appName) + + // If the config file path has not been modified by the user, then we'll + // use the default config file path. However, if the user has modified + // their lnddir, then we should assume they intend to use the config + // file within it. + configFileDir := CleanAndExpandPath(preCfg.SignerDir) + configFilePath := CleanAndExpandPath(preCfg.ConfigFile) + switch { + // User specified --lnddir but no --configfile. Update the config file + // path to the lnd config directory, but don't require it to exist. + case configFileDir != DefaultSignerDir && + configFilePath == DefaultConfigFile: + + configFilePath = filepath.Join( + configFileDir, defaultConfigFilename, + ) + + // User did specify an explicit --configfile, so we check that it does + // exist under that path to avoid surprises. + case configFilePath != DefaultConfigFile: + if !fileExists(configFilePath) { + return nil, fmt.Errorf("specified config file does "+ + "not exist in %s", configFilePath) + } + } + + // Next, load any additional configuration options from the file. + var configFileError error + cfg := preCfg + fileParser := flags.NewParser(&cfg, flags.Default) + err := flags.NewIniParser(fileParser).ParseFile(configFilePath) + if err != nil { + // If it's a parsing related error, then we'll return + // immediately, otherwise we can proceed as possibly the config + // file doesn't exist which is OK. + if _, ok := err.(*flags.IniError); ok { + return nil, err + } + + configFileError = err + } + + // Finally, parse the remaining command line options again to ensure + // they take precedence. + flagParser := flags.NewParser(&cfg, flags.Default) + if _, err := flagParser.Parse(); err != nil { + return nil, err + } + + // Make sure everything we just loaded makes sense. + cleanCfg, err := ValidateConfig( + cfg, fileParser, flagParser, + ) + if usageErr, ok := err.(*usageError); ok { + // The logging system might not yet be initialized, so we also + // write to stderr to make sure the error appears somewhere. + _, _ = fmt.Fprintln(os.Stderr, usageMessage) + signerLog.Warnf("Incorrect usage: %v", usageMessage) + + // The log subsystem might not yet be initialized. But we still + // try to log the error there since some packaging solutions + // might only look at the log and not stdout/stderr. + signerLog.Warnf("Error validating config: %v", usageErr.err) + + return nil, usageErr.err + } + if err != nil { + // The log subsystem might not yet be initialized. But we still + // try to log the error there since some packaging solutions + // might only look at the log and not stdout/stderr. + signerLog.Warnf("Error validating config: %v", err) + + return nil, err + } + + // Warn about missing config file only after all other configuration is + // done. This prevents the warning on help messages and invalid options. + // Note this should go directly before the return. + if configFileError != nil { + signerLog.Warnf("%v", configFileError) + } + + return cleanCfg, nil +} + +// usageError is an error type that signals a problem with the supplied flags. +type usageError struct { + err error +} + +// Error returns the error string. +// +// NOTE: This is part of the error interface. +func (u *usageError) Error() string { + return u.err.Error() +} + +// ValidateConfig check the given configuration to be sane. This makes sure no +// illegal values or combination of values are set. All file system paths are +// normalized. The cleaned up config is returned on success. +func ValidateConfig(cfg Config, fileParser, flagParser *flags.Parser) ( + *Config, error) { + + // If the provided lnd directory is not the default, we'll modify the + // path to all of the files and directories that will live within it. + signerDir := CleanAndExpandPath(cfg.SignerDir) + if signerDir != DefaultSignerDir { + cfg.TLSCertPath = filepath.Join(signerDir, defaultTLSCertFilename) + cfg.TLSKeyPath = filepath.Join(signerDir, defaultTLSKeyFilename) + cfg.LogDir = filepath.Join(signerDir, defaultLogDirname) + } + + funcName := "ValidateConfig" + mkErr := func(format string, args ...interface{}) error { + return fmt.Errorf(funcName+": "+format, args...) + } + makeDirectory := func(dir string) error { + err := os.MkdirAll(dir, 0700) + if err != nil { + // Show a nicer error message if it's because a symlink + // is linked to a directory that does not exist + // (probably because it's not mounted). + if e, ok := err.(*os.PathError); ok && os.IsExist(err) { + link, lerr := os.Readlink(e.Path) + if lerr == nil { + str := "is symlink %s -> %s mounted?" + err = fmt.Errorf(str, e.Path, link) + } + } + + str := "Failed to create lnd directory '%s': %v" + return mkErr(str, dir, err) + } + + return nil + } + + // As soon as we're done parsing configuration options, ensure all paths + // to directories and files are cleaned and expanded before attempting + // to use them later on. + cfg.TLSCertPath = CleanAndExpandPath(cfg.TLSCertPath) + cfg.TLSKeyPath = CleanAndExpandPath(cfg.TLSKeyPath) + cfg.LogDir = CleanAndExpandPath(cfg.LogDir) + + // Multiple networks can't be selected simultaneously. Count + // number of network flags passed; assign active network params + // while we're at it. + numNets := 0 + /*if cfg.MainNet { + numNets++ + cfg.ActiveNetParams = chaincfg.MainNetParams + }*/ + if cfg.TestNet3 { + numNets++ + cfg.ActiveNetParams = chaincfg.TestNet3Params + } + if cfg.RegTest { + numNets++ + cfg.ActiveNetParams = chaincfg.RegressionNetParams + } + if cfg.SimNet { + numNets++ + cfg.ActiveNetParams = chaincfg.SimNetParams + } + if cfg.SigNet { + numNets++ + cfg.ActiveNetParams = chaincfg.SigNetParams + } + if numNets > 1 { + str := "The mainnet, testnet, regtest, and simnet " + + "params can't be used together -- choose one " + + "of the four" + return nil, mkErr(str) + } + + // The target network must be provided, otherwise, we won't + // know how to initialize the daemon. + if numNets == 0 { + str := "either --bitcoin.mainnet, or bitcoin.testnet," + + "bitcoin.simnet, or bitcoin.regtest " + + "must be specified" + return nil, mkErr(str) + } + + // Create the lnd directory and all other sub-directories if they don't + // already exist. This makes sure that directory trees are also created + // for files that point to outside the lnddir. + dirs := []string{ + signerDir, filepath.Dir(cfg.TLSCertPath), + filepath.Dir(cfg.TLSKeyPath), filepath.Dir(cfg.OutputMacaroon), + } + for _, dir := range dirs { + if err := makeDirectory(dir); err != nil { + return nil, err + } + } + + err := setLogLevel(cfg.DebugLevel) + if err != nil { + return nil, mkErr("error setting debug level: %v", err) + } + + // At least one RPCListener is required. So listen on localhost per + // default. + if len(cfg.RawRPCListeners) == 0 { + addr := fmt.Sprintf("localhost:%d", defaultRPCPort) + cfg.RawRPCListeners = append(cfg.RawRPCListeners, addr) + } + + // Add default port to all RPC listener addresses if needed and remove + // duplicate addresses. + cfg.RPCListeners, err = NormalizeAddresses( + cfg.RawRPCListeners, strconv.Itoa(defaultRPCPort), + net.ResolveTCPAddr, + ) + if err != nil { + return nil, mkErr("error normalizing RPC listen addrs: %v", err) + } + + cfg.OutputAccounts = CleanAndExpandPath(cfg.OutputAccounts) + + // Get the macaroon root key from the environment. + cfg.macRootKey, err = get32BytesFromEnv("SIGNER_MAC_ROOT_KEY") + if err != nil { + return nil, err + } + + cfg.OutputMacaroon = CleanAndExpandPath(cfg.OutputMacaroon) + + // All good, return the sanitized result. + return &cfg, nil +} + +// CleanAndExpandPath expands environment variables and leading ~ in the +// passed path, cleans the result, and returns it. +// This function is taken from https://github.com/btcsuite/btcd +func CleanAndExpandPath(path string) string { + if path == "" { + return "" + } + + // Expand initial ~ to OS specific home directory. + if strings.HasPrefix(path, "~") { + var homeDir string + u, err := user.Current() + if err == nil { + homeDir = u.HomeDir + } else { + homeDir = os.Getenv("HOME") + } + + path = strings.Replace(path, "~", homeDir, 1) + } + + // NOTE: The os.ExpandEnv doesn't work with Windows-style %VARIABLE%, + // but the variables can still be expanded via POSIX-style $VARIABLE. + return filepath.Clean(os.ExpandEnv(path)) +} + +// get32BytesFromEnv gets a 64-byte hex string, encoding a 32-byte value, from +// an environment variable. +func get32BytesFromEnv(envKey string) ([32]byte, error) { + strHex, ok := os.LookupEnv(envKey) + if !ok { + return [32]byte{}, fmt.Errorf("env var %s not found: ", envKey) + } + + keyBytes, err := hex.DecodeString(strHex) + if err != nil { + return [32]byte{}, err + } + + if len(keyBytes) != 32 { + return [32]byte{}, fmt.Errorf("key length %d instead of 32", + len(keyBytes)) + } + + var key [32]byte + copy(key[:], keyBytes) + + return key, nil +} + +// TCPResolver is a function signature that resolves an address on a given +// network. +type TCPResolver = func(network, addr string) (*net.TCPAddr, error) + +// NormalizeAddresses returns a new slice with all the passed addresses +// normalized with the given default port and all duplicates removed. +func NormalizeAddresses(addrs []string, defaultPort string, + tcpResolver TCPResolver) ([]net.Addr, error) { + + result := make([]net.Addr, 0, len(addrs)) + seen := map[string]struct{}{} + + for _, addr := range addrs { + parsedAddr, err := ParseAddressString( + addr, defaultPort, tcpResolver, + ) + if err != nil { + return nil, fmt.Errorf("parse address %s failed: %w", + addr, err) + } + + if _, ok := seen[parsedAddr.String()]; !ok { + result = append(result, parsedAddr) + seen[parsedAddr.String()] = struct{}{} + } + } + + return result, nil +} + +// verifyPort makes sure that an address string has both a host and a port. If +// there is no port found, the default port is appended. If the address is just +// a port, then we'll assume that the user is using the short cut to specify a +// localhost:port address. +func verifyPort(address string, defaultPort string) string { + host, port, err := net.SplitHostPort(address) + if err != nil { + // If the address itself is just an integer, then we'll assume + // that we're mapping this directly to a localhost:port pair. + // This ensures we maintain the legacy behavior. + if _, err := strconv.Atoi(address); err == nil { + return net.JoinHostPort("localhost", address) + } + + // Otherwise, we'll assume that the address just failed to + // attach its own port, so we'll use the default port. In the + // case of IPv6 addresses, if the host is already surrounded by + // brackets, then we'll avoid using the JoinHostPort function, + // since it will always add a pair of brackets. + if strings.HasPrefix(address, "[") { + return address + ":" + defaultPort + } + return net.JoinHostPort(address, defaultPort) + } + + // In the case that both the host and port are empty, we'll use the + // default port. + if host == "" && port == "" { + return ":" + defaultPort + } + + return address +} + +// ParseAddressString converts an address in string format to a net.Addr that is +// compatible with lnd. UDP is not supported because lnd needs reliable +// connections. We accept a custom function to resolve any TCP addresses so +// that caller is able control exactly how resolution is performed. +func ParseAddressString(strAddress string, defaultPort string, + tcpResolver TCPResolver) (net.Addr, error) { + + var parsedNetwork, parsedAddr string + + // Addresses can either be in network://address:port format, + // network:address:port, address:port, or just port. We want to support + // all possible types. + if strings.Contains(strAddress, "://") { + parts := strings.Split(strAddress, "://") + parsedNetwork, parsedAddr = parts[0], parts[1] + } else if strings.Contains(strAddress, ":") { + parts := strings.Split(strAddress, ":") + parsedNetwork = parts[0] + parsedAddr = strings.Join(parts[1:], ":") + } + + // Only TCP and Unix socket addresses are valid. We can't use IP or + // UDP only connections for anything we do in lnd. + switch parsedNetwork { + case "unix", "unixpacket": + return net.ResolveUnixAddr(parsedNetwork, parsedAddr) + + case "tcp", "tcp4", "tcp6": + return tcpResolver( + parsedNetwork, verifyPort(parsedAddr, defaultPort), + ) + + default: + return nil, fmt.Errorf("only TCP or unix socket "+ + "addresses are supported: %s", parsedAddr) + } +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..feb52bb --- /dev/null +++ b/doc.go @@ -0,0 +1,6 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package lndsigner diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..83b2275 --- /dev/null +++ b/go.mod @@ -0,0 +1,89 @@ +module github.com/bottlepay/lndsigner + +require ( + github.com/btcsuite/btcd v0.23.1 + github.com/btcsuite/btcd/btcec/v2 v2.2.1 + github.com/btcsuite/btcd/btcutil v1.1.2 + github.com/btcsuite/btcd/btcutil/psbt v1.1.5 + github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 + github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f + github.com/hashicorp/go-hclog v1.3.1 + github.com/hashicorp/vault/api v1.8.0 + github.com/hashicorp/vault/sdk v0.6.0 + github.com/jessevdk/go-flags v1.4.0 + github.com/lightningnetwork/lnd/cert v1.1.1 + github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 + google.golang.org/grpc v1.47.0 + google.golang.org/protobuf v1.28.0 + gopkg.in/macaroon-bakery.v2 v2.2.0 + gopkg.in/macaroon.v2 v2.1.0 +) + +require ( + github.com/armon/go-metrics v0.3.9 // indirect + github.com/armon/go-radix v1.0.0 // indirect + github.com/cenkalti/backoff/v3 v3.0.0 // indirect + github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/evanphx/json-patch/v5 v5.5.0 // indirect + github.com/fatih/color v1.13.0 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-kms-wrapping/entropy/v2 v2.0.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-plugin v1.4.5 // indirect + github.com/hashicorp/go-retryablehttp v0.6.6 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect + github.com/hashicorp/go-sockaddr v1.0.2 // indirect + github.com/hashicorp/go-uuid v1.0.2 // indirect + github.com/hashicorp/go-version v1.2.0 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect + github.com/kr/pretty v0.3.0 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mitchellh/copystructure v1.0.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-testing-interface v1.0.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.0 // indirect + github.com/oklog/run v1.0.0 // indirect + github.com/pierrec/lz4 v2.5.2+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rogpeppe/fastuuid v1.2.0 // indirect + github.com/rogpeppe/go-internal v1.8.0 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect + golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect + golang.org/x/text v0.3.7 // indirect + golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect + google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0 // indirect + gopkg.in/errgo.v1 v1.0.1 // indirect + gopkg.in/square/go-jose.v2 v2.5.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) + +// This replace is for https://github.com/advisories/GHSA-w73w-5m7g-f7qc +replace github.com/dgrijalva/jwt-go => github.com/golang-jwt/jwt v3.2.1+incompatible + +// This replace is for https://github.com/advisories/GHSA-25xm-hr59-7c27 +replace github.com/ulikunitz/xz => github.com/ulikunitz/xz v0.5.8 + +// This replace is for +// https://deps.dev/advisory/OSV/GO-2021-0053?from=%2Fgo%2Fgithub.com%252Fgogo%252Fprotobuf%2Fv1.3.1 +replace github.com/gogo/protobuf => github.com/gogo/protobuf v1.3.2 + +// If you change this please also update .github/pull_request_template.md and +// docs/INSTALL.md. +go 1.18 + +retract v0.0.2 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..367e0e6 --- /dev/null +++ b/go.sum @@ -0,0 +1,463 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/go-metrics v0.3.9 h1:O2sNqxBdvq8Eq5xmzljcYzAORli6RWCvEym4cJf9m18= +github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= +github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= +github.com/btcsuite/btcd v0.23.1 h1:IB8cVQcC2X5mHbnfirLG5IZnkWYNTPlLZVrxUYSotbE= +github.com/btcsuite/btcd v0.23.1/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= +github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= +github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcec/v2 v2.2.1 h1:xP60mv8fvp+0khmrN0zTdPC3cNm24rfeE6lh2R/Yv3E= +github.com/btcsuite/btcd/btcec/v2 v2.2.1/go.mod h1:9/CSmJxmuvqzX9Wh2fXMWToLOHhPd11lSPuIupwTkI8= +github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= +github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= +github.com/btcsuite/btcd/btcutil v1.1.2 h1:XLMbX8JQEiwMcYft2EGi8zPUkoa0abKIU6/BJSRsjzQ= +github.com/btcsuite/btcd/btcutil v1.1.2/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0= +github.com/btcsuite/btcd/btcutil/psbt v1.1.5 h1:x0ZRrYY8j75ThV6xBz86CkYAG82F5bzay4H5D1c8b/U= +github.com/btcsuite/btcd/btcutil/psbt v1.1.5/go.mod h1:kA6FLH/JfUx++j9pYU0pyu+Z8XGBQuuTmuKYUf6q7/U= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= +github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch/v5 v5.5.0 h1:bAmFiUJ+o0o2B4OiTFeE3MqCOtyo+jjPP9iZ0VRxYUc= +github.com/evanphx/json-patch/v5 v5.5.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= +github.com/frankban/quicktest v1.1.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= +github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= +github.com/frankban/quicktest v1.7.3/go.mod h1:V1d2J5pfxYH6EjBAgSK7YNXcXlTWxUHdE1sVDXkjnig= +github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v1.3.1 h1:vDwF1DFNZhntP4DAjuTpOw3uEgMUpXh1pB5fW9DqHpo= +github.com/hashicorp/go-hclog v1.3.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-kms-wrapping/entropy/v2 v2.0.0 h1:pSjQfW3vPtrOTcasTUKgCTQT7OGPPTTMVRrOfU6FJD8= +github.com/hashicorp/go-kms-wrapping/entropy/v2 v2.0.0/go.mod h1:xvb32K2keAc+R8DSFG2IwDcydK9DBQE+fGA5fsw6hSk= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.4.5 h1:oTE/oQR4eghggRg8VY7PAz3dr++VwDNBGCcOfIvHpBo= +github.com/hashicorp/go-plugin v1.4.5/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM= +github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 h1:cCRo8gK7oq6A2L6LICkUZ+/a5rLiRXFMf1Qd4xSwxTc= +github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/vault/api v1.8.0 h1:7765sW1XBt+qf4XKIYE4ebY9qc/yi9V2/egzGSUNMZU= +github.com/hashicorp/vault/api v1.8.0/go.mod h1:uJrw6D3y9Rv7hhmS17JQC50jbPDAZdjZoTtrCCxxs7E= +github.com/hashicorp/vault/sdk v0.6.0 h1:6Z+In5DXHiUfZvIZdMx7e2loL1PPyDjA4bVh9ZTIAhs= +github.com/hashicorp/vault/sdk v0.6.0/go.mod h1:+DRpzoXIdMvKc88R4qxr+edwy/RvH5QK8itmxLiDHLc= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/juju/mgotest v1.0.1/go.mod h1:vTaDufYul+Ps8D7bgseHjq87X8eu0ivlKLp9mVc/Bfc= +github.com/juju/postgrestest v1.1.0/go.mod h1:/n17Y2T6iFozzXwSCO0JYJ5gSiz2caEtSwAjh/uLXDM= +github.com/juju/qthttptest v0.0.1/go.mod h1://LCf/Ls22/rPw2u1yWukUJvYtfPY4nYpWUl2uZhryo= +github.com/juju/schema v1.0.0/go.mod h1:Y+ThzXpUJ0E7NYYocAbuvJ7vTivXfrof/IfRPq/0abI= +github.com/juju/webbrowser v0.0.0-20160309143629-54b8c57083b4/go.mod h1:G6PCelgkM6cuvyD10iYJsjLBsSadVXtJ+nBxFAxE2BU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lightningnetwork/lnd/cert v1.1.1 h1:Nsav0RlIDRbOnzz2Yu69SQlK939IKya3Q2S0mDviIN8= +github.com/lightningnetwork/lnd/cert v1.1.1/go.mod h1:1P46svkkd73oSoeI4zjkVKgZNwGq8bkGuPR8z+5vQUs= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI= +github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 h1:tcJ6OjwOMvExLlzrAVZute09ocAGa7KqOON60++Gz4E= +github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S068ZqfrO6S8HsoJq2bF3ETfTL+kt4tInY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38= +golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20150829230318-ea47fc708ee3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181008205924-a2b3f7f249e9/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0 h1:5Tbluzus3QxoAJx4IefGt1W0HQZW4nuMrVk684jI74Q= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v1 v1.0.0/go.mod h1:CxwszS/Xz1C49Ucd2i6Zil5UToP1EmyrFhKaMVbg1mk= +gopkg.in/errgo.v1 v1.0.1 h1:oQFRXzZ7CkBGdm1XZm/EbQYaYNNEElNBOd09M6cqNso= +gopkg.in/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/httprequest.v1 v1.2.0/go.mod h1:T61ZUaJLpMnzvoJDO03ZD8yRXD4nZzBeDoW5e9sffjg= +gopkg.in/juju/environschema.v1 v1.0.0/go.mod h1:WTgU3KXKCVoO9bMmG/4KHzoaRvLeoxfjArpgd1MGWFA= +gopkg.in/macaroon-bakery.v2 v2.2.0 h1:tgib3W6Nz8GhYfF83vp0FEZmncj6UE1ubIG7a09flkc= +gopkg.in/macaroon-bakery.v2 v2.2.0/go.mod h1:XyHjEinGUBsCK60Qv+bBejOQD/WklvntpSVGja9utaU= +gopkg.in/macaroon.v2 v2.1.0 h1:HZcsjBCzq9t0eBPMKqTN/uSN6JOm78ZJ2INbqcBQOUI= +gopkg.in/macaroon.v2 v2.1.0/go.mod h1:OUb+TQP/OP0WOerC2Jp/3CwhIKyIa9kQjuc7H24e6/o= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/keyring/keyring.go b/keyring/keyring.go new file mode 100644 index 0000000..9af32d6 --- /dev/null +++ b/keyring/keyring.go @@ -0,0 +1,793 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package keyring + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + + "github.com/bottlepay/lndsigner/vault" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcutil/hdkeychain" + "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/hashicorp/vault/api" +) + +// signMethod defines the different ways a signer can sign, given a specific +// input. +type signMethod uint8 + +const ( + // WitnessV0SignMethod denotes that a SegWit v0 (p2wkh, np2wkh, p2wsh) + // input script should be signed. + witnessV0SignMethod signMethod = 0 + + // TaprootKeySpendBIP0086SignMethod denotes that a SegWit v1 (p2tr) + // input should be signed by using the BIP0086 method (commit to + // internal key only). + taprootKeySpendBIP0086SignMethod signMethod = 1 + + // TaprootKeySpendSignMethod denotes that a SegWit v1 (p2tr) + // input should be signed by using a given taproot hash to commit to in + // addition to the internal key. + taprootKeySpendSignMethod signMethod = 2 + + // TaprootScriptSpendSignMethod denotes that a SegWit v1 (p2tr) input + // should be spent using the script path and that a specific leaf script + // should be signed for. + taprootScriptSpendSignMethod signMethod = 3 +) + +var ( + // psbtKeyTypeInputSignatureTweakSingle is a custom/proprietary PSBT key + // for an input that specifies what single tweak should be applied to + // the key before signing the input. The value 51 is leet speak for + // "si", short for "single". + psbtKeyTypeInputSignatureTweakSingle = []byte{0x51} + + // psbtKeyTypeInputSignatureTweakDouble is a custom/proprietary PSBT key + // for an input that specifies what double tweak should be applied to + // the key before signing the input. The value d0 is leet speak for + // "do", short for "double". + psbtKeyTypeInputSignatureTweakDouble = []byte{0xd0} +) + +type KeyLocator struct { + // Family is the family of key being identified. + Family uint32 + + // Index is the precise index of the key being identified. + Index uint32 +} + +type KeyDescriptor struct { + // KeyLocator is the internal KeyLocator of the descriptor. + KeyLocator + + // PubKey is an optional public key that fully describes a target key. + // If this is nil, the KeyLocator MUST NOT be empty. + PubKey *btcec.PublicKey +} + +// KeyRing is an HD keyring backed by pre-derived in-memory account keys from +// which index keys can be quickly derived on demand. +type KeyRing struct { + client *api.Logical + node string + coin int +} + +// NewKeyRing returns a vault-backed key ring. +func NewKeyRing(client *api.Logical, node string, coin int) *KeyRing { + return &KeyRing{ + client: client, + node: node, + coin: coin, + } +} + +// ECDH performs a scalar multiplication (ECDH-like operation) between the +// target key descriptor and remote public key. The output returned will be +// the sha256 of the resulting shared point serialized in compressed format. If +// k is our private key, and P is the public key, we perform the following +// operation: +// +// sx := k*P s := sha256(sx.SerializeCompressed()) +func (k *KeyRing) ECDH(keyDesc KeyDescriptor, pub *btcec.PublicKey) ([32]byte, + error) { + + reqData := map[string]interface{}{ + "node": k.node, + "path": []int{ + int(vault.Bip0043purpose + + hdkeychain.HardenedKeyStart), + int(k.coin + hdkeychain.HardenedKeyStart), + int(keyDesc.Family + hdkeychain.HardenedKeyStart), + 0, // Only external branch in LN purpose. + int(keyDesc.Index), + }, + "peer": hex.EncodeToString(pub.SerializeCompressed()), + } + + if keyDesc.PubKey != nil { + reqData["pubkey"] = hex.EncodeToString( + keyDesc.PubKey.SerializeCompressed(), + ) + } + + log.Tracef("Sending data %+v for signing request", reqData) + + sharedKeyResp, err := k.client.Write( + "lndsigner/lnd-nodes/ecdh", + reqData, + ) + if err != nil { + return [32]byte{}, err + } + + log.Tracef("Got data %+v in signing response", sharedKeyResp.Data) + + sharedKeyHex, ok := sharedKeyResp.Data["sharedkey"].(string) + if !ok { + return [32]byte{}, errors.New("vault returned no shared key") + } + + sharedKeyBytes, err := hex.DecodeString(sharedKeyHex) + if err != nil { + return [32]byte{}, err + } + + if len(sharedKeyBytes) != 32 { + return [32]byte{}, errors.New("vault returned bad shared key") + } + + var sharedKeyByteArray [32]byte + + copy(sharedKeyByteArray[:], sharedKeyBytes) + + return sharedKeyByteArray, nil +} + +// SignMessage signs the given message, single or double SHA256 hashing it +// first, with the private key described in the key locator. +func (k *KeyRing) SignMessage(keyLoc KeyLocator, msg []byte, doubleHash bool, + compact bool) ([]byte, error) { + + var digest []byte + if doubleHash { + digest = chainhash.DoubleHashB(msg) + } else { + digest = chainhash.HashB(msg) + } + + reqData := map[string]interface{}{ + "node": k.node, + "path": []int{ + int(vault.Bip0043purpose + hdkeychain.HardenedKeyStart), + int(k.coin + hdkeychain.HardenedKeyStart), + int(keyLoc.Family + hdkeychain.HardenedKeyStart), + 0, // Only external branch in LN purpose. + int(keyLoc.Index), + }, + "method": "ecdsa", + "digest": hex.EncodeToString(digest), + } + + if compact { + reqData["method"] = "ecdsa-compact" + } + + log.Tracef("Sending data %+v for signing request", reqData) + + signResp, err := k.client.Write( + "lndsigner/lnd-nodes/sign", + reqData, + ) + if err != nil { + return nil, err + } + + log.Tracef("Got data %+v in signing response", signResp.Data) + + signatureHex, ok := signResp.Data["signature"].(string) + if !ok { + return nil, errors.New("vault returned no signature") + } + + return hex.DecodeString(signatureHex) +} + +// SignMessageSchnorr signs the given message, single or double SHA256 +// hashing it first, with the private key described in the key locator +// and the optional Taproot tweak applied to the private key. +func (k *KeyRing) SignMessageSchnorr(keyLoc KeyLocator, msg []byte, + doubleHash bool, taprootTweak []byte) (*schnorr.Signature, error) { + + var digest []byte + if doubleHash { + digest = chainhash.DoubleHashB(msg) + } else { + digest = chainhash.HashB(msg) + } + + reqData := map[string]interface{}{ + "node": k.node, + "path": []int{ + int(vault.Bip0043purpose + hdkeychain.HardenedKeyStart), + int(k.coin + hdkeychain.HardenedKeyStart), + int(keyLoc.Family + hdkeychain.HardenedKeyStart), + 0, // Only external branch in LN purpose. + int(keyLoc.Index), + }, + "method": "schnorr", + "digest": hex.EncodeToString(digest), + } + + if len(taprootTweak) > 0 { + reqData["taptweak"] = hex.EncodeToString(taprootTweak) + } + + log.Tracef("Sending data %+v for signing request", reqData) + + signResp, err := k.client.Write( + "lndsigner/lnd-nodes/sign", + reqData, + ) + if err != nil { + return nil, err + } + + log.Tracef("Got data %+v in signing response", signResp.Data) + + signatureHex, ok := signResp.Data["signature"].(string) + if !ok { + return nil, errors.New("vault returned no signature") + } + + signatureBytes, err := hex.DecodeString(signatureHex) + if err != nil { + return nil, err + } + + return schnorr.ParseSignature(signatureBytes) +} + +// SignPsbt signs all inputs in the PSBT that can be signed by our keyring. +// We have no state information, so we only attempt to derive the appropriate +// keys for each input and sign if we get a match. +func (k *KeyRing) SignPsbt(packet *psbt.Packet) ([]uint32, error) { + // In signedInputs we return the indices of psbt inputs that were signed + // by our wallet. This way the caller can check if any inputs were signed. + var signedInputs []uint32 + + // Let's check that this is actually something we can and want to sign. + // We need at least one input and one output. + err := psbt.VerifyInputOutputLen(packet, true, true) + if err != nil { + return nil, err + } + + // Go through each input that doesn't have final witness data attached + // to it already and try to sign it. If there is nothing more to sign or + // there are inputs that we don't know how to sign, we won't return any + // error. So it's possible we're not the final signer. We expect all + // required UTXO data as part of the PSBT packet as we have no state. + tx := packet.UnsignedTx + prevOutputFetcher := psbtPrevOutputFetcher(packet) + sigHashes := txscript.NewTxSigHashes(tx, prevOutputFetcher) + for idx := range tx.TxIn { + in := &packet.Inputs[idx] + + // We can only sign if we have UTXO information available. Since + // we don't finalize, we just skip over any input that we know + // we can't do anything with. Since we only support signing + // witness inputs, we only look at the witness UTXO being set. + if in.WitnessUtxo == nil { + continue + } + + // Skip this input if it's got final witness data attached. + if len(in.FinalScriptWitness) > 0 { + continue + } + + // Skip this input if there is no BIP32 derivation info + // available. + if len(in.Bip32Derivation) == 0 { + continue + } + + /*// Let's try and derive the key now. This method will decide if + // it's a BIP49/84 key for normal on-chain funds or a key of the + // custom purpose 1017 key scope. + derivationInfo := in.Bip32Derivation[0] + privKey, err := k.deriveKeyByBIP32Path(derivationInfo.Bip32Path) + if err != nil { + log.Warnf("SignPsbt: Skipping input %d, error "+ + "deriving signing key: %v", idx, err) + continue + } + + // We need to make sure we actually derived the key that was + // expected to be derived. + pubKeysEqual := bytes.Equal( + derivationInfo.PubKey, + privKey.PubKey().SerializeCompressed(), + ) + if !pubKeysEqual { + log.Warnf("SignPsbt: Skipping input %d, derived "+ + "public key %x does not match bip32 "+ + "derivation info public key %x", idx, + privKey.PubKey().SerializeCompressed(), + derivationInfo.PubKey) + continue + } + + // Do we need to tweak anything? Single or double tweaks are + // sent as custom/proprietary fields in the PSBT input section. + privKey = maybeTweakPrivKeyPsbt(in.Unknowns, privKey) + */ + + // What kind of signature is expected from us and do we have all + // information we need? + signMethod, err := validateSigningMethod(in) + if err != nil { + return nil, err + } + + switch signMethod { + // For p2wkh, np2wkh and p2wsh. + case witnessV0SignMethod: + err = k.signSegWitV0(in, tx, sigHashes, idx) + + // For p2tr BIP0086 key spend only. + case taprootKeySpendBIP0086SignMethod: + rootHash := make([]byte, 0) + err = k.signSegWitV1KeySpend( + in, tx, sigHashes, idx, rootHash, + ) + + // For p2tr with script commitment key spend path. + case taprootKeySpendSignMethod: + rootHash := in.TaprootMerkleRoot + err = k.signSegWitV1KeySpend( + in, tx, sigHashes, idx, rootHash, + ) + + // For p2tr script spend path. + case taprootScriptSpendSignMethod: + leafScript := in.TaprootLeafScript[0] + leaf := txscript.TapLeaf{ + LeafVersion: leafScript.LeafVersion, + Script: leafScript.Script, + } + err = k.signSegWitV1ScriptSpend( + in, tx, sigHashes, idx, leaf, + ) + + default: + err = fmt.Errorf("unsupported signing method for "+ + "PSBT signing: %v", signMethod) + } + if err != nil { + return nil, err + } + + signedInputs = append(signedInputs, uint32(idx)) + + /* newTx := packet.UnsignedTx.Copy() + newTx.TxIn[0].SignatureScript = in.RedeemScript + newTx.TxIn[0].Witness = wire.TxWitness{ + in.PartialSigs[0].Signature, + in.PartialSigs[0].PubKey, + } + + log.Infof("Executing engine on tx input %+v (from %+v)", + newTx.TxIn[0], in) + + engine, err := txscript.NewEngine( + in.WitnessUtxo.PkScript, + newTx, + idx, + txscript.StandardVerifyFlags, + txscript.NewSigCache(10), + sigHashes, + in.WitnessUtxo.Value, + prevOutputFetcher, + ) + if err != nil { + log.Errorf("Error creating engine: %v", err) + continue + } + + err = engine.Execute() + if err != nil { + log.Errorf("Error executing engine: %v", err) + continue + } + + log.Infof("Succeeded executing engine") */ + } + + return signedInputs, nil +} + +// signSegWitV0 attempts to generate a signature for a SegWit version 0 input +// and stores it in the PartialSigs (and FinalScriptSig for np2wkh addresses) +// field. +func (k *KeyRing) signSegWitV0(in *psbt.PInput, tx *wire.MsgTx, + sigHashes *txscript.TxSigHashes, idx int) error { + + if len(in.Bip32Derivation) == 0 { + return nil + } + + // Extract the correct witness and/or legacy scripts now, depending on + // the type of input we sign. The txscript package has the peculiar + // requirement that the PkScript of a P2PKH must be given as the witness + // script in order for it to arrive at the correct sighash. That's why + // we call it subScript here instead of witness script. + subScript := prepareScriptsV0(in) + + // We have everything we need to calculate the digest for signing. + digest, err := txscript.CalcWitnessSigHash(subScript, sigHashes, + in.SighashType, tx, idx, in.WitnessUtxo.Value) + if err != nil { + return fmt.Errorf("error getting sighash for input %d: %v", + idx, err) + } + + log.Tracef("Got input %+v for signing with unknowns %+v", in, + in.Unknowns) + + reqData := map[string]interface{}{ + "node": k.node, + "path": sliceUint32ToInt(in.Bip32Derivation[0].Bip32Path), + "method": "ecdsa", + "digest": hex.EncodeToString(digest), + } + + getTweakParams(in.Unknowns, reqData) + + log.Tracef("Sending data %+v for signing request", reqData) + + signResp, err := k.client.Write( + "lndsigner/lnd-nodes/sign", + reqData, + ) + if err != nil { + return err + } + + log.Tracef("Got data %+v in signing response", signResp.Data) + + signatureHex, ok := signResp.Data["signature"].(string) + if !ok { + return errors.New("vault returned no signature") + } + + signatureBytes, err := hex.DecodeString(signatureHex) + if err != nil { + return err + } + + pubKeyHex, ok := signResp.Data["pubkey"].(string) + if !ok { + return errors.New("vault returned no pubkey") + } + + pubKeyBytes, err := hex.DecodeString(pubKeyHex) + if err != nil { + return err + } + + in.PartialSigs = append(in.PartialSigs, &psbt.PartialSig{ + PubKey: pubKeyBytes, + Signature: append(signatureBytes, byte(in.SighashType)), + }) + + return nil +} + +// signSegWitV1KeySpend attempts to generate a signature for a SegWit version 1 +// (p2tr) input and stores it in the TaprootKeySpendSig field. +func (k *KeyRing) signSegWitV1KeySpend(in *psbt.PInput, tx *wire.MsgTx, + sigHashes *txscript.TxSigHashes, idx int, + tapscriptRootHash []byte) error { + + if len(in.Bip32Derivation) == 0 { + return nil + } + + digest, err := txscript.CalcTaprootSignatureHash( + sigHashes, in.SighashType, tx, idx, + txscript.NewCannedPrevOutputFetcher( + in.WitnessUtxo.PkScript, + in.WitnessUtxo.Value, + ), + ) + if err != nil { + return err + } + + reqData := map[string]interface{}{ + "node": k.node, + "path": sliceUint32ToInt(in.Bip32Derivation[0].Bip32Path), + "method": "schnorr", + "digest": hex.EncodeToString(digest), + "taptweak": hex.EncodeToString(tapscriptRootHash), + } + + getTweakParams(in.Unknowns, reqData) + + log.Tracef("Sending data %+v for signing request", reqData) + + signResp, err := k.client.Write( + "lndsigner/lnd-nodes/sign", + reqData, + ) + if err != nil { + return err + } + + signatureHex, ok := signResp.Data["signature"].(string) + if !ok { + return errors.New("vault returned no signature") + } + + signatureBytes, err := hex.DecodeString(signatureHex) + if err != nil { + return err + } + + in.TaprootKeySpendSig = append(signatureBytes, byte(in.SighashType)) + + return nil +} + +// signSegWitV1ScriptSpend attempts to generate a signature for a SegWit version +// 1 (p2tr) input and stores it in the TaprootScriptSpendSig field. +func (k *KeyRing) signSegWitV1ScriptSpend(in *psbt.PInput, tx *wire.MsgTx, + sigHashes *txscript.TxSigHashes, idx int, leaf txscript.TapLeaf) error { + + if len(in.Bip32Derivation) == 0 { + return nil + } + + digest, err := txscript.CalcTapscriptSignaturehash( + sigHashes, in.SighashType, tx, idx, + txscript.NewCannedPrevOutputFetcher( + in.WitnessUtxo.PkScript, + in.WitnessUtxo.Value, + ), + leaf, + ) + if err != nil { + return err + } + + reqData := map[string]interface{}{ + "node": k.node, + "path": sliceUint32ToInt(in.Bip32Derivation[0].Bip32Path), + "method": "schnorr", + "digest": hex.EncodeToString(digest), + } + + getTweakParams(in.Unknowns, reqData) + + log.Tracef("Sending data %+v for signing request", reqData) + + signResp, err := k.client.Write( + "lndsigner/lnd-nodes/sign", + reqData, + ) + if err != nil { + return err + } + + signatureHex, ok := signResp.Data["signature"].(string) + if !ok { + return errors.New("vault returned no signature") + } + + signatureBytes, err := hex.DecodeString(signatureHex) + if err != nil { + return err + } + + leafHash := leaf.TapHash() + in.TaprootScriptSpendSig = append( + in.TaprootScriptSpendSig, &psbt.TaprootScriptSpendSig{ + XOnlyPubKey: in.TaprootBip32Derivation[0].XOnlyPubKey, + LeafHash: leafHash[:], + Signature: signatureBytes, + SigHash: in.SighashType, + }, + ) + + return nil +} + +// prepareScriptsV0 returns the appropriate witness v0 and/or legacy scripts, +// depending on the type of input that should be signed. +func prepareScriptsV0(in *psbt.PInput) []byte { + switch { + // It's a NP2WKH input: + //case len(in.RedeemScript) > 0: + // return in.RedeemScript + + // It's a P2WSH input: + case len(in.WitnessScript) > 0: + return in.WitnessScript + + // It's a P2WKH input: + default: + return in.WitnessUtxo.PkScript + } +} + +// getTweakParams examines if there are any tweak parameters given in the +// custom/proprietary PSBT fields and adds them to a vault request's data +// to use them if populated. +func getTweakParams(unknowns []*psbt.Unknown, reqData map[string]interface{}) { + // There can be other custom/unknown keys in a PSBT that we just ignore. + // Key tweaking is optional and only one tweak (single _or_ double) can + // ever be applied (at least for any use cases described in the BOLT + // spec). + for _, u := range unknowns { + if bytes.Equal(u.Key, psbtKeyTypeInputSignatureTweakSingle) { + reqData["ln1tweak"] = hex.EncodeToString(u.Value) + return + } + + if bytes.Equal(u.Key, psbtKeyTypeInputSignatureTweakDouble) { + reqData["ln2tweak"] = hex.EncodeToString(u.Value) + return + } + } + + return +} + +// validateSigningMethod attempts to detect the signing method that is required +// to sign for the given PSBT input and makes sure all information is available +// to do so. +func validateSigningMethod(in *psbt.PInput) (signMethod, error) { + script, err := txscript.ParsePkScript(in.WitnessUtxo.PkScript) + if err != nil { + return 0, fmt.Errorf("error detecting signing method, "+ + "couldn't parse pkScript: %v", err) + } + + switch script.Class() { + case txscript.WitnessV0PubKeyHashTy, txscript.ScriptHashTy, + txscript.WitnessV0ScriptHashTy: + + return witnessV0SignMethod, nil + + case txscript.WitnessV1TaprootTy: + if len(in.TaprootBip32Derivation) == 0 { + return 0, fmt.Errorf("cannot sign for taproot input " + + "without taproot BIP0032 derivation info") + } + + // Currently, we only support creating one signature per input. + if len(in.TaprootBip32Derivation) > 1 { + return 0, fmt.Errorf("unsupported multiple taproot " + + "BIP0032 derivation info found, can only " + + "sign for one at a time") + } + + derivation := in.TaprootBip32Derivation[0] + switch { + // No leaf hashes means this is the internal key we're signing + // with, so it's a key spend. And no merkle root means this is + // a BIP0086 output we're signing for. + case len(derivation.LeafHashes) == 0 && + len(in.TaprootMerkleRoot) == 0: + + return taprootKeySpendBIP0086SignMethod, nil + + // A non-empty merkle root means we committed to a taproot hash + // that we need to use in the tap tweak. + case len(derivation.LeafHashes) == 0: + // Getting here means the merkle root isn't empty, but + // is it exactly the length we need? + if len(in.TaprootMerkleRoot) != sha256.Size { + return 0, fmt.Errorf("invalid taproot merkle "+ + "root length, got %d expected %d", + len(in.TaprootMerkleRoot), sha256.Size) + } + + return taprootKeySpendSignMethod, nil + + // Currently, we only support signing for one leaf at a time. + case len(derivation.LeafHashes) == 1: + // If we're supposed to be signing for a leaf hash, we + // also expect the leaf script that hashes to that hash + // in the appropriate field. + if len(in.TaprootLeafScript) != 1 { + return 0, fmt.Errorf("specified leaf hash in " + + "taproot BIP0032 derivation but " + + "missing taproot leaf script") + } + + leafScript := in.TaprootLeafScript[0] + leaf := txscript.TapLeaf{ + LeafVersion: leafScript.LeafVersion, + Script: leafScript.Script, + } + leafHash := leaf.TapHash() + if !bytes.Equal(leafHash[:], derivation.LeafHashes[0]) { + return 0, fmt.Errorf("specified leaf hash in" + + "taproot BIP0032 derivation but " + + "corresponding taproot leaf script " + + "was not found") + } + + return taprootScriptSpendSignMethod, nil + + default: + return 0, fmt.Errorf("unsupported number of leaf " + + "hashes in taproot BIP0032 derivation info, " + + "can only sign for one at a time") + } + + default: + return 0, fmt.Errorf("unsupported script class for signing "+ + "PSBT: %v", script.Class()) + } +} + +// psbtPrevOutputFetcher returns a txscript.PrevOutFetcher built from the UTXO +// information in a PSBT packet. +func psbtPrevOutputFetcher(packet *psbt.Packet) *txscript.MultiPrevOutFetcher { + fetcher := txscript.NewMultiPrevOutFetcher(nil) + for idx, txIn := range packet.UnsignedTx.TxIn { + in := packet.Inputs[idx] + + // Skip any input that has no UTXO. + if in.WitnessUtxo == nil && in.NonWitnessUtxo == nil { + continue + } + + if in.NonWitnessUtxo != nil { + prevIndex := txIn.PreviousOutPoint.Index + fetcher.AddPrevOut( + txIn.PreviousOutPoint, + in.NonWitnessUtxo.TxOut[prevIndex], + ) + + continue + } + + // Fall back to witness UTXO only for older wallets. + if in.WitnessUtxo != nil { + fetcher.AddPrevOut( + txIn.PreviousOutPoint, in.WitnessUtxo, + ) + } + } + + return fetcher +} + +func sliceUint32ToInt(uints []uint32) []int { + ints := make([]int, len(uints)) + + for idx, element := range uints { + ints[idx] = int(element) + } + + return ints +} diff --git a/keyring/log.go b/keyring/log.go new file mode 100644 index 0000000..b7aa7b4 --- /dev/null +++ b/keyring/log.go @@ -0,0 +1,29 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package keyring + +import "github.com/btcsuite/btclog" + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + DisableLog() +} + +// DisableLog disables all library log output. Logging output is disabled +// by default until UseLogger is called. +func DisableLog() { + log = btclog.Disabled +} + +// UseLogger uses a specified Logger to output package logging info. +func UseLogger(logger btclog.Logger) { + log = logger +} diff --git a/lndsigner.go b/lndsigner.go new file mode 100644 index 0000000..447cdc6 --- /dev/null +++ b/lndsigner.go @@ -0,0 +1,417 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package lndsigner + +import ( + "context" + "encoding/json" + "fmt" + "net" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/hashicorp/vault/api" + "github.com/lightningnetwork/lnd/cert" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "gopkg.in/macaroon-bakery.v2/bakery" + "gopkg.in/macaroon-bakery.v2/bakery/checkers" +) + +const ( + // outputFilePermissions is the file permission that is used for + // creating the signer macaroon file and the accounts list file. + // + // Why 640 is safe: + // Assuming a reasonably secure Linux system, it will have a + // separate group for each user. E.g. a new user lnd gets assigned group + // lnd which nothing else belongs to. A system that does not do this is + // inherently broken already. + // + // Since there is no other user in the group, no other user can read + // admin macaroon unless the administrator explicitly allowed it. Thus + // there's no harm allowing group read. + outputFilePermissions = 0640 +) + +// ListenerWithSignal is a net.Listener that has an additional Ready channel +// that will be closed when a server starts listening. +type ListenerWithSignal struct { + net.Listener + + // Ready will be closed by the server listening on Listener. + Ready chan struct{} + + // MacChan is an optional way to pass the admin macaroon to the program + // that started lnd. The channel should be buffered to avoid lnd being + // blocked on sending to the channel. + MacChan chan []byte +} + +// ListenerCfg is a wrapper around custom listeners that can be passed to lnd +// when calling its main method. +type ListenerCfg struct { + // RPCListeners can be set to the listeners to use for the RPC server. + // If empty a regular network listener will be created. + RPCListeners []*ListenerWithSignal +} + +// Main is the true entry point for lnd. It accepts a fully populated and +// validated main configuration struct and an optional listener config struct. +// This function starts all main system components then blocks until a signal +// is received on the shutdownChan at which point everything is shut down again. +func Main(cfg *Config, lisCfg ListenerCfg) error { + // mkErr makes it easy to return logged errors. + mkErr := func(format string, args ...interface{}) error { + signerLog.Errorf("Shutting down because error in main "+ + "method: "+format, args...) + return fmt.Errorf(format, args...) + } + + var network string + switch { + /*case cfg.MainNet: + network = "mainnet" + */ + case cfg.TestNet3: + network = "testnet" + + case cfg.SimNet: + network = "simnet" + + case cfg.RegTest: + network = "regtest" + + case cfg.SigNet: + network = "signet" + } + + signerLog.Infof("Active chain: %v (network=%v)", "bitcoin", network) + + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + // Use defaults for vault client, including getting config from env. + vaultClient, err := api.NewClient(nil) + if err != nil { + return mkErr("error creating vault client: %v", err) + } + + signerClient := vaultClient.Logical() + + nodeListResp, err := signerClient.Read("lndsigner/lnd-nodes") + if err != nil { + return mkErr("error getting list of lnd nodes: %v", err) + } + + // If we're asked to output a watch-only account list, do it here. + if cfg.OutputAccounts != "" { + for node := range nodeListResp.Data { + listAcctsResp, err := signerClient.ReadWithData( + "lndsigner/lnd-nodes/accounts", + map[string][]string{ + "node": []string{node}, + }, + ) + if err != nil { + return mkErr("error listing accounts for "+ + "node %s: %v", node, err) + } + + acctList, ok := listAcctsResp.Data["acctList"] + if !ok { + return mkErr("accounts not returned for "+ + "node %s", node) + } + + err = os.WriteFile( + cfg.OutputAccounts+"."+node, + []byte(acctList.(string)), + outputFilePermissions, + ) + if err != nil { + return mkErr("error writing account list: %v", + err) + } + } + } + + // Create a new macaroon service. + rootKeyStore := &assignedRootKeyStore{ + key: cfg.macRootKey[:], + } + + // Check that we have a valid caveat, we only accept 3 formats. + checker := &caveatChecker{} + + bakeryParams := bakery.BakeryParams{ + RootKeyStore: rootKeyStore, + Location: "lnd", + Checker: checker, + } + + bkry := bakery.New(bakeryParams) + + // If we're asked to output a macaroon file, do it here. + if cfg.OutputMacaroon != "" { + for node, coin := range nodeListResp.Data { + caveats := []checkers.Caveat{ + checkers.Caveat{ + Condition: checkers.Condition( + "node", + node, + ), + }, + checkers.Caveat{ + Condition: checkers.Condition( + "coin", + coin.(json.Number).String(), + ), + }, + } + + mac, err := bkry.Oven.NewMacaroon( + ctx, + bakery.LatestVersion, + caveats, + nodePermissions..., + ) + if err != nil { + return mkErr("error baking macaroon: %v", err) + } + + macBytes, err := mac.M().MarshalBinary() + if err != nil { + return mkErr("error marshaling macaroon "+ + "binary: %v", err) + } + + err = os.WriteFile( + cfg.OutputMacaroon+"."+node, + macBytes, + outputFilePermissions, + ) + if err != nil { + return mkErr("error writing account list: %v", + err) + } + } + } + + serverOpts, err := getTLSConfig(cfg) + if err != nil { + return mkErr("unable to load TLS credentials: %v", err) + } + + // If we have chosen to start with a dedicated listener for the + // rpc server, we set it directly. + grpcListeners := append([]*ListenerWithSignal{}, lisCfg.RPCListeners...) + if len(grpcListeners) == 0 { + // Otherwise we create listeners from the RPCListeners defined + // in the config. + for _, grpcEndpoint := range cfg.RPCListeners { + // Start a gRPC server listening for HTTP/2 + // connections. + lis, err := ListenOnAddress(grpcEndpoint) + if err != nil { + return mkErr("unable to listen on %s: %v", + grpcEndpoint, err) + } + defer lis.Close() + + grpcListeners = append( + grpcListeners, &ListenerWithSignal{ + Listener: lis, + Ready: make(chan struct{}), + }, + ) + } + } + + // Initialize the rpcServer and add its interceptor to the server + // options. + rpcServer := newRPCServer(cfg, signerClient, bkry.Checker) + serverOpts = append( + serverOpts, + grpc.ChainUnaryInterceptor(rpcServer.intercept), + ) + + // Create the GRPC server with the TLS and interceptor configuration. + grpcServer := grpc.NewServer(serverOpts...) + defer grpcServer.Stop() + + // Register our implementation of the gRPC interface exported by the + // rpcServer. + err = rpcServer.RegisterWithGrpcServer(grpcServer) + if err != nil { + return mkErr("error registering gRPC server: %v", err) + } + + // Now that both the WalletUnlocker and LightningService have been + // registered with the GRPC server, we can start listening. + err = startGrpcListen(cfg, grpcServer, grpcListeners) + if err != nil { + return mkErr("error starting gRPC listener: %v", err) + } + + // Wait for shutdown signal from the interrupt handler. + signerLog.Infof("Press ctrl-c to exit") + + sigint := make(chan os.Signal, 1) + signal.Notify(sigint, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + <-sigint + + return nil +} + +// getTLSConfig returns a TLS configuration for the gRPC server. +func getTLSConfig(cfg *Config) ([]grpc.ServerOption, error) { + + // Ensure we create TLS key and certificate if they don't exist. + if !fileExists(cfg.TLSCertPath) && !fileExists(cfg.TLSKeyPath) { + signerLog.Infof("Generating TLS certificates...") + err := cert.GenCertPair( + "signer autogenerated cert", cfg.TLSCertPath, + cfg.TLSKeyPath, cfg.TLSExtraIPs, cfg.TLSExtraDomains, + cfg.TLSDisableAutofill, cfg.TLSCertDuration, + ) + if err != nil { + return nil, err + } + signerLog.Infof("Done generating TLS certificates") + } + + certData, parsedCert, err := cert.LoadCert( + cfg.TLSCertPath, cfg.TLSKeyPath, + ) + if err != nil { + return nil, err + } + + // We check whether the certificate we have on disk match the IPs and + // domains specified by the config. If the extra IPs or domains have + // changed from when the certificate was created, we will refresh the + // certificate if auto refresh is active. + refresh := false + if cfg.TLSAutoRefresh { + refresh, err = cert.IsOutdated( + parsedCert, cfg.TLSExtraIPs, + cfg.TLSExtraDomains, cfg.TLSDisableAutofill, + ) + if err != nil { + return nil, err + } + } + + // If the certificate expired or it was outdated, delete it and the TLS + // key and generate a new pair. + if time.Now().After(parsedCert.NotAfter) || refresh { + signerLog.Info("TLS certificate is expired or outdated, " + + "generating a new one") + + err := os.Remove(cfg.TLSCertPath) + if err != nil { + return nil, err + } + + err = os.Remove(cfg.TLSKeyPath) + if err != nil { + return nil, err + } + + signerLog.Infof("Renewing TLS certificates...") + err = cert.GenCertPair( + "signer autogenerated cert", cfg.TLSCertPath, + cfg.TLSKeyPath, cfg.TLSExtraIPs, cfg.TLSExtraDomains, + cfg.TLSDisableAutofill, cfg.TLSCertDuration, + ) + if err != nil { + return nil, err + } + signerLog.Infof("Done renewing TLS certificates") + + // Reload the certificate data. + certData, _, err = cert.LoadCert( + cfg.TLSCertPath, cfg.TLSKeyPath, + ) + if err != nil { + return nil, err + } + } + + tlsCfg := cert.TLSConfFromCert(certData) + + serverCreds := credentials.NewTLS(tlsCfg) + serverOpts := []grpc.ServerOption{grpc.Creds(serverCreds)} + + return serverOpts, nil +} + +// fileExists reports whether the named file or directory exists. +// This function is taken from https://github.com/btcsuite/btcd +func fileExists(name string) bool { + if _, err := os.Stat(name); err != nil { + if os.IsNotExist(err) { + return false + } + } + return true +} + +// startGrpcListen starts the GRPC server on the passed listeners. +func startGrpcListen(cfg *Config, grpcServer *grpc.Server, + listeners []*ListenerWithSignal) error { + + // Use a WaitGroup so we can be sure the instructions on how to input the + // password is the last thing to be printed to the console. + var wg sync.WaitGroup + + for _, lis := range listeners { + wg.Add(1) + go func(lis *ListenerWithSignal) { + signerLog.Infof("RPC server listening on %s", lis.Addr()) + + // Close the ready chan to indicate we are listening. + close(lis.Ready) + + wg.Done() + _ = grpcServer.Serve(lis) + }(lis) + } + + // Wait for gRPC servers to be up running. + wg.Wait() + + return nil +} + +// parseNetwork parses the network type of the given address. +func parseNetwork(addr net.Addr) string { + switch addr := addr.(type) { + // TCP addresses resolved through net.ResolveTCPAddr give a default + // network of "tcp", so we'll map back the correct network for the given + // address. This ensures that we can listen on the correct interface + // (IPv4 vs IPv6). + case *net.TCPAddr: + if addr.IP.To4() != nil { + return "tcp4" + } + return "tcp6" + + default: + return addr.Network() + } +} + +// ListenOnAddress creates a listener that listens on the given address. +func ListenOnAddress(addr net.Addr) (net.Listener, error) { + return net.Listen(parseNetwork(addr), addr.String()) +} diff --git a/log.go b/log.go new file mode 100644 index 0000000..575ba43 --- /dev/null +++ b/log.go @@ -0,0 +1,39 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package lndsigner + +import ( + "errors" + "os" + + "github.com/bottlepay/lndsigner/keyring" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btclog" +) + +var ( + backend = btclog.NewBackend(os.Stdout) + signerLog = backend.Logger("SIGNER") + txscriptLog = backend.Logger("TXSCRIPT") + keyringLog = backend.Logger("KEYRING") +) + +func setLogLevel(level string) error { + logLevel, ok := btclog.LevelFromString(level) + if !ok { + return errors.New("invalid log level: " + level) + } + + signerLog.SetLevel(logLevel) + + txscriptLog.SetLevel(logLevel) + txscript.UseLogger(txscriptLog) + + keyringLog.SetLevel(logLevel) + keyring.UseLogger(keyringLog) + + return nil +} diff --git a/macaroons.go b/macaroons.go new file mode 100644 index 0000000..e57e7dc --- /dev/null +++ b/macaroons.go @@ -0,0 +1,183 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package lndsigner + +import ( + "context" + "encoding/hex" + "errors" + "fmt" + "strings" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "gopkg.in/macaroon-bakery.v2/bakery/checkers" + "gopkg.in/macaroon.v2" +) + +var defaultRootKeyID = []byte("0") + +type assignedRootKeyStore struct { + key []byte +} + +func (s *assignedRootKeyStore) Get(ctx context.Context, id []byte) ([]byte, + error) { + + return s.key, nil +} + +func (s *assignedRootKeyStore) RootKey(ctx context.Context) ([]byte, []byte, + error) { + + return s.key, defaultRootKeyID, nil +} + +type caveatChecker struct{} + +func (c *caveatChecker) CheckFirstPartyCaveat(ctx context.Context, + caveat string) error { + switch { + case caveat == "coin 0": + return nil + + case caveat == "coin 1": + return nil + + case len(caveat) == 71 && strings.HasPrefix( + caveat, + "node ", + ): + _, err := hex.DecodeString(caveat[5:]) + return err + + default: + return fmt.Errorf("invalid caveat: %s", caveat) + } +} + +func (c *caveatChecker) Namespace() *checkers.Namespace { + return nil +} + +func (r *rpcServer) checkMac(ctx context.Context, method string) (string, int, + error) { + + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + signerLog.Warnf("request for %v without metadata", method) + return "", 0, status.Error(codes.Unauthenticated, "no metadata") + } + + macaroonHex, ok := md["macaroon"] + if !ok { + signerLog.Warnf("request for %v without macaroons", method) + return "", 0, status.Error(codes.Unauthenticated, "no macaroons") + } + + var macSlice macaroon.Slice + + var ( + node string + coin int + coinSet bool + + check = func(caveat string) error { + signerLog.Tracef("checking caveat: %s", caveat) + + switch { + case strings.HasPrefix(caveat, "node "): + if node != "" { + return errors.New("node already set") + } + + // Caveat should be 5 bytes of "node " prefix + // plus 66 bytes of pubkey hex digits. + if len(caveat) != 71 { + return errors.New("invalid node pubkey") + } + + node = caveat[5:] + + case caveat == "coin 0": + if coinSet { + return errors.New("coin already set") + } + + coin = 0 + coinSet = true + + case caveat == "coin 1": + if coinSet { + return errors.New("coin already set") + } + + coin = 1 + coinSet = true + + default: + return errors.New("invalid caveat") + + } + + return nil + } + ) + + for _, macHex := range macaroonHex { + macBytes, err := hex.DecodeString(macHex) + if err != nil { + signerLog.Warnf("failed to decode macaroon hex "+ + "for %v: %v", method, err) + continue + } + + mac := &macaroon.Macaroon{} + err = mac.UnmarshalBinary(macBytes) + if err != nil { + signerLog.Warnf("failed to unmarshal macaroon bytes "+ + "for %v: %v", method, err) + continue + } + + err = mac.Verify(r.cfg.macRootKey[:], check, nil) + if err != nil { + signerLog.Warnf("failed to verify macaroon "+ + "for %v: %v", method, err) + continue + } + + macSlice = append(macSlice, mac) + } + + if len(macSlice) == 0 { + signerLog.Warnf("macaroon authentication failure for %v", + method) + return "", 0, status.Error(codes.Unauthenticated, + "macaroon authentication failure") + } + + if !(len(node) == 66 && coinSet) { + signerLog.Warn("macaroon doesn't specify both node and coin") + return "", 0, status.Error(codes.Unauthenticated, + "macaroon authentication failure") + } + + authChecker := r.checker.Auth(macSlice) + authInfo, err := authChecker.Allow(ctx, r.perms[method]...) + if err != nil { + signerLog.Warnf("macaroon authorization failure for %v: %v", + method, err) + return "", 0, status.Error(codes.PermissionDenied, + "macaroon authorization failure") + } + + signerLog.Debugf("successfully authorized request to %v", method) + signerLog.Tracef("auth info for %v: %+v", method, authInfo) + + return node, coin, nil +} diff --git a/proto/gen_protos.sh b/proto/gen_protos.sh new file mode 100755 index 0000000..68f4ec9 --- /dev/null +++ b/proto/gen_protos.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# +# Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +# Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +set -e + +# generate compiles the *.pb.go stubs from the *.proto files. +function generate() { + echo "Generating root gRPC server protos" + + PROTOS="lightning.proto signer.proto walletkit.proto" + + # For each of the sub-servers, we then generate their protos, but a restricted + # set as they don't yet require REST proxies, or swagger docs. + for file in $PROTOS; do + DIRECTORY=$(dirname "${file}") + echo "Generating protos from ${file}, into ${DIRECTORY}" + + # Generate the protos. + protoc -I/usr/local/include -I. \ + --go_out . --go_opt paths=source_relative \ + --go-grpc_out . --go-grpc_opt paths=source_relative \ + "${file}" + done +} + +# format formats the *.proto files with the clang-format utility. +function format() { + find . -name "*.proto" -print0 | xargs -0 clang-format --style=file -i +} + +# Compile and format the lnrpc package. +pushd proto +format +generate +popd diff --git a/proto/lightning.pb.go b/proto/lightning.pb.go new file mode 100644 index 0000000..5ef4c95 --- /dev/null +++ b/proto/lightning.pb.go @@ -0,0 +1,390 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0-devel +// protoc v3.14.0 +// source: lightning.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SignMessageRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The message to be signed. When using REST, this field must be encoded as + //base64. + Msg []byte `protobuf:"bytes,1,opt,name=msg,proto3" json:"msg,omitempty"` + // + //Instead of the default double-SHA256 hashing of the message before signing, + //only use one round of hashing instead. + SingleHash bool `protobuf:"varint,2,opt,name=single_hash,json=singleHash,proto3" json:"single_hash,omitempty"` +} + +func (x *SignMessageRequest) Reset() { + *x = SignMessageRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_lightning_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SignMessageRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignMessageRequest) ProtoMessage() {} + +func (x *SignMessageRequest) ProtoReflect() protoreflect.Message { + mi := &file_lightning_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignMessageRequest.ProtoReflect.Descriptor instead. +func (*SignMessageRequest) Descriptor() ([]byte, []int) { + return file_lightning_proto_rawDescGZIP(), []int{0} +} + +func (x *SignMessageRequest) GetMsg() []byte { + if x != nil { + return x.Msg + } + return nil +} + +func (x *SignMessageRequest) GetSingleHash() bool { + if x != nil { + return x.SingleHash + } + return false +} + +type SignMessageResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The signature for the given message + Signature string `protobuf:"bytes,1,opt,name=signature,proto3" json:"signature,omitempty"` +} + +func (x *SignMessageResponse) Reset() { + *x = SignMessageResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_lightning_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SignMessageResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignMessageResponse) ProtoMessage() {} + +func (x *SignMessageResponse) ProtoReflect() protoreflect.Message { + mi := &file_lightning_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignMessageResponse.ProtoReflect.Descriptor instead. +func (*SignMessageResponse) Descriptor() ([]byte, []int) { + return file_lightning_proto_rawDescGZIP(), []int{1} +} + +func (x *SignMessageResponse) GetSignature() string { + if x != nil { + return x.Signature + } + return "" +} + +type KeyLocator struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The family of key being identified. + KeyFamily int32 `protobuf:"varint,1,opt,name=key_family,json=keyFamily,proto3" json:"key_family,omitempty"` + // The precise index of the key being identified. + KeyIndex int32 `protobuf:"varint,2,opt,name=key_index,json=keyIndex,proto3" json:"key_index,omitempty"` +} + +func (x *KeyLocator) Reset() { + *x = KeyLocator{} + if protoimpl.UnsafeEnabled { + mi := &file_lightning_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *KeyLocator) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*KeyLocator) ProtoMessage() {} + +func (x *KeyLocator) ProtoReflect() protoreflect.Message { + mi := &file_lightning_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use KeyLocator.ProtoReflect.Descriptor instead. +func (*KeyLocator) Descriptor() ([]byte, []int) { + return file_lightning_proto_rawDescGZIP(), []int{2} +} + +func (x *KeyLocator) GetKeyFamily() int32 { + if x != nil { + return x.KeyFamily + } + return 0 +} + +func (x *KeyLocator) GetKeyIndex() int32 { + if x != nil { + return x.KeyIndex + } + return 0 +} + +type KeyDescriptor struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The raw bytes of the key being identified. + RawKeyBytes []byte `protobuf:"bytes,1,opt,name=raw_key_bytes,json=rawKeyBytes,proto3" json:"raw_key_bytes,omitempty"` + // + //The key locator that identifies which key to use for signing. + KeyLoc *KeyLocator `protobuf:"bytes,2,opt,name=key_loc,json=keyLoc,proto3" json:"key_loc,omitempty"` +} + +func (x *KeyDescriptor) Reset() { + *x = KeyDescriptor{} + if protoimpl.UnsafeEnabled { + mi := &file_lightning_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *KeyDescriptor) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*KeyDescriptor) ProtoMessage() {} + +func (x *KeyDescriptor) ProtoReflect() protoreflect.Message { + mi := &file_lightning_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use KeyDescriptor.ProtoReflect.Descriptor instead. +func (*KeyDescriptor) Descriptor() ([]byte, []int) { + return file_lightning_proto_rawDescGZIP(), []int{3} +} + +func (x *KeyDescriptor) GetRawKeyBytes() []byte { + if x != nil { + return x.RawKeyBytes + } + return nil +} + +func (x *KeyDescriptor) GetKeyLoc() *KeyLocator { + if x != nil { + return x.KeyLoc + } + return nil +} + +var File_lightning_proto protoreflect.FileDescriptor + +var file_lightning_proto_rawDesc = []byte{ + 0x0a, 0x0f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x47, 0x0a, 0x12, 0x53, 0x69, 0x67, 0x6e, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, + 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6d, 0x73, 0x67, + 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x48, 0x61, 0x73, + 0x68, 0x22, 0x33, 0x0a, 0x13, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, + 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x69, 0x67, + 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x48, 0x0a, 0x0a, 0x4b, 0x65, 0x79, 0x4c, 0x6f, 0x63, + 0x61, 0x74, 0x6f, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x6b, 0x65, 0x79, 0x5f, 0x66, 0x61, 0x6d, 0x69, + 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x6b, 0x65, 0x79, 0x46, 0x61, 0x6d, + 0x69, 0x6c, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x6b, 0x65, 0x79, 0x49, 0x6e, 0x64, 0x65, 0x78, + 0x22, 0x5f, 0x0a, 0x0d, 0x4b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, + 0x72, 0x12, 0x22, 0x0a, 0x0d, 0x72, 0x61, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x62, 0x79, 0x74, + 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x72, 0x61, 0x77, 0x4b, 0x65, 0x79, + 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x2a, 0x0a, 0x07, 0x6b, 0x65, 0x79, 0x5f, 0x6c, 0x6f, 0x63, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4b, + 0x65, 0x79, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x06, 0x6b, 0x65, 0x79, 0x4c, 0x6f, + 0x63, 0x32, 0x51, 0x0a, 0x09, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x12, 0x44, + 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x19, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x62, 0x6f, 0x74, 0x74, 0x6c, 0x65, 0x70, 0x61, 0x79, 0x2f, 0x6c, 0x6e, 0x64, + 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_lightning_proto_rawDescOnce sync.Once + file_lightning_proto_rawDescData = file_lightning_proto_rawDesc +) + +func file_lightning_proto_rawDescGZIP() []byte { + file_lightning_proto_rawDescOnce.Do(func() { + file_lightning_proto_rawDescData = protoimpl.X.CompressGZIP(file_lightning_proto_rawDescData) + }) + return file_lightning_proto_rawDescData +} + +var file_lightning_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_lightning_proto_goTypes = []interface{}{ + (*SignMessageRequest)(nil), // 0: proto.SignMessageRequest + (*SignMessageResponse)(nil), // 1: proto.SignMessageResponse + (*KeyLocator)(nil), // 2: proto.KeyLocator + (*KeyDescriptor)(nil), // 3: proto.KeyDescriptor +} +var file_lightning_proto_depIdxs = []int32{ + 2, // 0: proto.KeyDescriptor.key_loc:type_name -> proto.KeyLocator + 0, // 1: proto.Lightning.SignMessage:input_type -> proto.SignMessageRequest + 1, // 2: proto.Lightning.SignMessage:output_type -> proto.SignMessageResponse + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_lightning_proto_init() } +func file_lightning_proto_init() { + if File_lightning_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_lightning_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SignMessageRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lightning_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SignMessageResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lightning_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*KeyLocator); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lightning_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*KeyDescriptor); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_lightning_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_lightning_proto_goTypes, + DependencyIndexes: file_lightning_proto_depIdxs, + MessageInfos: file_lightning_proto_msgTypes, + }.Build() + File_lightning_proto = out.File + file_lightning_proto_rawDesc = nil + file_lightning_proto_goTypes = nil + file_lightning_proto_depIdxs = nil +} diff --git a/proto/lightning.proto b/proto/lightning.proto new file mode 100644 index 0000000..69fb450 --- /dev/null +++ b/proto/lightning.proto @@ -0,0 +1,76 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +syntax = "proto3"; + +package proto; + +option go_package = "github.com/bottlepay/lndsigner/proto"; + +/* + * Comments in this file will be directly parsed into the API + * Documentation as descriptions of the associated method, message, or field. + * These descriptions should go right above the definition of the object, and + * can be in either block or // comment format. + * + * An RPC method can be matched to an lncli command by placing a line in the + * beginning of the description in exactly the following format: + * lncli: `methodname` + * + * Failure to specify the exact name of the command will cause documentation + * generation to fail. + * + * More information on how exactly the gRPC documentation is generated from + * this proto file can be found here: + * https://github.com/lightninglabs/lightning-api + */ + +// Lightning is the main RPC server of the daemon. +service Lightning { + /* lncli: `signmessage` + SignMessage signs a message with this node's private key. The returned + signature string is `zbase32` encoded and pubkey recoverable, meaning that + only the message digest and signature are needed for verification. + */ + rpc SignMessage(SignMessageRequest) returns (SignMessageResponse); +} + +message SignMessageRequest { + /* + The message to be signed. When using REST, this field must be encoded as + base64. + */ + bytes msg = 1; + + /* + Instead of the default double-SHA256 hashing of the message before signing, + only use one round of hashing instead. + */ + bool single_hash = 2; +} +message SignMessageResponse { + // The signature for the given message + string signature = 1; +} + +message KeyLocator { + // The family of key being identified. + int32 key_family = 1; + + // The precise index of the key being identified. + int32 key_index = 2; +} + +message KeyDescriptor { + /* + The raw bytes of the key being identified. + */ + bytes raw_key_bytes = 1; + + /* + The key locator that identifies which key to use for signing. + */ + KeyLocator key_loc = 2; +} diff --git a/proto/lightning_grpc.pb.go b/proto/lightning_grpc.pb.go new file mode 100644 index 0000000..ac6f148 --- /dev/null +++ b/proto/lightning_grpc.pb.go @@ -0,0 +1,109 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package proto + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// LightningClient is the client API for Lightning service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type LightningClient interface { + // lncli: `signmessage` + //SignMessage signs a message with this node's private key. The returned + //signature string is `zbase32` encoded and pubkey recoverable, meaning that + //only the message digest and signature are needed for verification. + SignMessage(ctx context.Context, in *SignMessageRequest, opts ...grpc.CallOption) (*SignMessageResponse, error) +} + +type lightningClient struct { + cc grpc.ClientConnInterface +} + +func NewLightningClient(cc grpc.ClientConnInterface) LightningClient { + return &lightningClient{cc} +} + +func (c *lightningClient) SignMessage(ctx context.Context, in *SignMessageRequest, opts ...grpc.CallOption) (*SignMessageResponse, error) { + out := new(SignMessageResponse) + err := c.cc.Invoke(ctx, "/proto.Lightning/SignMessage", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// LightningServer is the server API for Lightning service. +// All implementations must embed UnimplementedLightningServer +// for forward compatibility +type LightningServer interface { + // lncli: `signmessage` + //SignMessage signs a message with this node's private key. The returned + //signature string is `zbase32` encoded and pubkey recoverable, meaning that + //only the message digest and signature are needed for verification. + SignMessage(context.Context, *SignMessageRequest) (*SignMessageResponse, error) + mustEmbedUnimplementedLightningServer() +} + +// UnimplementedLightningServer must be embedded to have forward compatible implementations. +type UnimplementedLightningServer struct { +} + +func (UnimplementedLightningServer) SignMessage(context.Context, *SignMessageRequest) (*SignMessageResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SignMessage not implemented") +} +func (UnimplementedLightningServer) mustEmbedUnimplementedLightningServer() {} + +// UnsafeLightningServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to LightningServer will +// result in compilation errors. +type UnsafeLightningServer interface { + mustEmbedUnimplementedLightningServer() +} + +func RegisterLightningServer(s grpc.ServiceRegistrar, srv LightningServer) { + s.RegisterService(&Lightning_ServiceDesc, srv) +} + +func _Lightning_SignMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SignMessageRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LightningServer).SignMessage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.Lightning/SignMessage", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LightningServer).SignMessage(ctx, req.(*SignMessageRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Lightning_ServiceDesc is the grpc.ServiceDesc for Lightning service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Lightning_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "proto.Lightning", + HandlerType: (*LightningServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SignMessage", + Handler: _Lightning_SignMessage_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "lightning.proto", +} diff --git a/proto/signer.pb.go b/proto/signer.pb.go new file mode 100644 index 0000000..183a113 --- /dev/null +++ b/proto/signer.pb.go @@ -0,0 +1,462 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0-devel +// protoc v3.14.0 +// source: signer.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SignMessageReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The message to be signed. When using REST, this field must be encoded as + //base64. + Msg []byte `protobuf:"bytes,1,opt,name=msg,proto3" json:"msg,omitempty"` + // The key locator that identifies which key to use for signing. + KeyLoc *KeyLocator `protobuf:"bytes,2,opt,name=key_loc,json=keyLoc,proto3" json:"key_loc,omitempty"` + // Double-SHA256 hash instead of just the default single round. + DoubleHash bool `protobuf:"varint,3,opt,name=double_hash,json=doubleHash,proto3" json:"double_hash,omitempty"` + // + //Use the compact (pubkey recoverable) format instead of the raw lnwire + //format. This option cannot be used with Schnorr signatures. + CompactSig bool `protobuf:"varint,4,opt,name=compact_sig,json=compactSig,proto3" json:"compact_sig,omitempty"` + // + //Use Schnorr signature. This option cannot be used with compact format. + SchnorrSig bool `protobuf:"varint,5,opt,name=schnorr_sig,json=schnorrSig,proto3" json:"schnorr_sig,omitempty"` + // + //The optional Taproot tweak bytes to apply to the private key before creating + //a Schnorr signature. The private key is tweaked as described in BIP-341: + //privKey + h_tapTweak(internalKey || tapTweak) + SchnorrSigTapTweak []byte `protobuf:"bytes,6,opt,name=schnorr_sig_tap_tweak,json=schnorrSigTapTweak,proto3" json:"schnorr_sig_tap_tweak,omitempty"` +} + +func (x *SignMessageReq) Reset() { + *x = SignMessageReq{} + if protoimpl.UnsafeEnabled { + mi := &file_signer_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SignMessageReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignMessageReq) ProtoMessage() {} + +func (x *SignMessageReq) ProtoReflect() protoreflect.Message { + mi := &file_signer_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignMessageReq.ProtoReflect.Descriptor instead. +func (*SignMessageReq) Descriptor() ([]byte, []int) { + return file_signer_proto_rawDescGZIP(), []int{0} +} + +func (x *SignMessageReq) GetMsg() []byte { + if x != nil { + return x.Msg + } + return nil +} + +func (x *SignMessageReq) GetKeyLoc() *KeyLocator { + if x != nil { + return x.KeyLoc + } + return nil +} + +func (x *SignMessageReq) GetDoubleHash() bool { + if x != nil { + return x.DoubleHash + } + return false +} + +func (x *SignMessageReq) GetCompactSig() bool { + if x != nil { + return x.CompactSig + } + return false +} + +func (x *SignMessageReq) GetSchnorrSig() bool { + if x != nil { + return x.SchnorrSig + } + return false +} + +func (x *SignMessageReq) GetSchnorrSigTapTweak() []byte { + if x != nil { + return x.SchnorrSigTapTweak + } + return nil +} + +type SignMessageResp struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The signature for the given message in the fixed-size LN wire format. + Signature []byte `protobuf:"bytes,1,opt,name=signature,proto3" json:"signature,omitempty"` +} + +func (x *SignMessageResp) Reset() { + *x = SignMessageResp{} + if protoimpl.UnsafeEnabled { + mi := &file_signer_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SignMessageResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignMessageResp) ProtoMessage() {} + +func (x *SignMessageResp) ProtoReflect() protoreflect.Message { + mi := &file_signer_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignMessageResp.ProtoReflect.Descriptor instead. +func (*SignMessageResp) Descriptor() ([]byte, []int) { + return file_signer_proto_rawDescGZIP(), []int{1} +} + +func (x *SignMessageResp) GetSignature() []byte { + if x != nil { + return x.Signature + } + return nil +} + +type SharedKeyRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The ephemeral public key to use for the DH key derivation. + EphemeralPubkey []byte `protobuf:"bytes,1,opt,name=ephemeral_pubkey,json=ephemeralPubkey,proto3" json:"ephemeral_pubkey,omitempty"` + // + //Deprecated. The optional key locator of the local key that should be used. + //If this parameter is not set then the node's identity private key will be + //used. + // + // Deprecated: Do not use. + KeyLoc *KeyLocator `protobuf:"bytes,2,opt,name=key_loc,json=keyLoc,proto3" json:"key_loc,omitempty"` + // + //A key descriptor describes the key used for performing ECDH. Either a key + //locator or a raw public key is expected, if neither is supplied, defaults to + //the node's identity private key. + KeyDesc *KeyDescriptor `protobuf:"bytes,3,opt,name=key_desc,json=keyDesc,proto3" json:"key_desc,omitempty"` +} + +func (x *SharedKeyRequest) Reset() { + *x = SharedKeyRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_signer_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SharedKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SharedKeyRequest) ProtoMessage() {} + +func (x *SharedKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_signer_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SharedKeyRequest.ProtoReflect.Descriptor instead. +func (*SharedKeyRequest) Descriptor() ([]byte, []int) { + return file_signer_proto_rawDescGZIP(), []int{2} +} + +func (x *SharedKeyRequest) GetEphemeralPubkey() []byte { + if x != nil { + return x.EphemeralPubkey + } + return nil +} + +// Deprecated: Do not use. +func (x *SharedKeyRequest) GetKeyLoc() *KeyLocator { + if x != nil { + return x.KeyLoc + } + return nil +} + +func (x *SharedKeyRequest) GetKeyDesc() *KeyDescriptor { + if x != nil { + return x.KeyDesc + } + return nil +} + +type SharedKeyResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The shared public key, hashed with sha256. + SharedKey []byte `protobuf:"bytes,1,opt,name=shared_key,json=sharedKey,proto3" json:"shared_key,omitempty"` +} + +func (x *SharedKeyResponse) Reset() { + *x = SharedKeyResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_signer_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SharedKeyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SharedKeyResponse) ProtoMessage() {} + +func (x *SharedKeyResponse) ProtoReflect() protoreflect.Message { + mi := &file_signer_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SharedKeyResponse.ProtoReflect.Descriptor instead. +func (*SharedKeyResponse) Descriptor() ([]byte, []int) { + return file_signer_proto_rawDescGZIP(), []int{3} +} + +func (x *SharedKeyResponse) GetSharedKey() []byte { + if x != nil { + return x.SharedKey + } + return nil +} + +var File_signer_proto protoreflect.FileDescriptor + +var file_signer_proto_rawDesc = []byte{ + 0x0a, 0x0c, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe4, 0x01, 0x0a, 0x0e, 0x53, 0x69, 0x67, 0x6e, 0x4d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x73, 0x67, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x12, 0x2a, 0x0a, 0x07, 0x6b, + 0x65, 0x79, 0x5f, 0x6c, 0x6f, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4b, 0x65, 0x79, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x52, + 0x06, 0x6b, 0x65, 0x79, 0x4c, 0x6f, 0x63, 0x12, 0x1f, 0x0a, 0x0b, 0x64, 0x6f, 0x75, 0x62, 0x6c, + 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x6f, + 0x75, 0x62, 0x6c, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x70, + 0x61, 0x63, 0x74, 0x5f, 0x73, 0x69, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x63, + 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x53, 0x69, 0x67, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x63, 0x68, + 0x6e, 0x6f, 0x72, 0x72, 0x5f, 0x73, 0x69, 0x67, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, + 0x73, 0x63, 0x68, 0x6e, 0x6f, 0x72, 0x72, 0x53, 0x69, 0x67, 0x12, 0x31, 0x0a, 0x15, 0x73, 0x63, + 0x68, 0x6e, 0x6f, 0x72, 0x72, 0x5f, 0x73, 0x69, 0x67, 0x5f, 0x74, 0x61, 0x70, 0x5f, 0x74, 0x77, + 0x65, 0x61, 0x6b, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x12, 0x73, 0x63, 0x68, 0x6e, 0x6f, + 0x72, 0x72, 0x53, 0x69, 0x67, 0x54, 0x61, 0x70, 0x54, 0x77, 0x65, 0x61, 0x6b, 0x22, 0x2f, 0x0a, + 0x0f, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x9e, + 0x01, 0x0a, 0x10, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, + 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x65, + 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x50, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x12, 0x2e, + 0x0a, 0x07, 0x6b, 0x65, 0x79, 0x5f, 0x6c, 0x6f, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4b, 0x65, 0x79, 0x4c, 0x6f, 0x63, 0x61, 0x74, + 0x6f, 0x72, 0x42, 0x02, 0x18, 0x01, 0x52, 0x06, 0x6b, 0x65, 0x79, 0x4c, 0x6f, 0x63, 0x12, 0x2f, + 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x22, + 0x32, 0x0a, 0x11, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x5f, 0x6b, + 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, + 0x4b, 0x65, 0x79, 0x32, 0x8c, 0x01, 0x0a, 0x06, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x12, 0x3c, + 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x15, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x52, 0x65, 0x71, 0x1a, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x69, 0x67, + 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x12, 0x44, 0x0a, 0x0f, + 0x44, 0x65, 0x72, 0x69, 0x76, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, + 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, + 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x62, 0x6f, 0x74, 0x74, 0x6c, 0x65, 0x70, 0x61, 0x79, 0x2f, 0x6c, 0x6e, 0x64, 0x73, 0x69, + 0x67, 0x6e, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_signer_proto_rawDescOnce sync.Once + file_signer_proto_rawDescData = file_signer_proto_rawDesc +) + +func file_signer_proto_rawDescGZIP() []byte { + file_signer_proto_rawDescOnce.Do(func() { + file_signer_proto_rawDescData = protoimpl.X.CompressGZIP(file_signer_proto_rawDescData) + }) + return file_signer_proto_rawDescData +} + +var file_signer_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_signer_proto_goTypes = []interface{}{ + (*SignMessageReq)(nil), // 0: proto.SignMessageReq + (*SignMessageResp)(nil), // 1: proto.SignMessageResp + (*SharedKeyRequest)(nil), // 2: proto.SharedKeyRequest + (*SharedKeyResponse)(nil), // 3: proto.SharedKeyResponse + (*KeyLocator)(nil), // 4: proto.KeyLocator + (*KeyDescriptor)(nil), // 5: proto.KeyDescriptor +} +var file_signer_proto_depIdxs = []int32{ + 4, // 0: proto.SignMessageReq.key_loc:type_name -> proto.KeyLocator + 4, // 1: proto.SharedKeyRequest.key_loc:type_name -> proto.KeyLocator + 5, // 2: proto.SharedKeyRequest.key_desc:type_name -> proto.KeyDescriptor + 0, // 3: proto.Signer.SignMessage:input_type -> proto.SignMessageReq + 2, // 4: proto.Signer.DeriveSharedKey:input_type -> proto.SharedKeyRequest + 1, // 5: proto.Signer.SignMessage:output_type -> proto.SignMessageResp + 3, // 6: proto.Signer.DeriveSharedKey:output_type -> proto.SharedKeyResponse + 5, // [5:7] is the sub-list for method output_type + 3, // [3:5] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_signer_proto_init() } +func file_signer_proto_init() { + if File_signer_proto != nil { + return + } + file_lightning_proto_init() + if !protoimpl.UnsafeEnabled { + file_signer_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SignMessageReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_signer_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SignMessageResp); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_signer_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SharedKeyRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_signer_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SharedKeyResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_signer_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_signer_proto_goTypes, + DependencyIndexes: file_signer_proto_depIdxs, + MessageInfos: file_signer_proto_msgTypes, + }.Build() + File_signer_proto = out.File + file_signer_proto_rawDesc = nil + file_signer_proto_goTypes = nil + file_signer_proto_depIdxs = nil +} diff --git a/proto/signer.proto b/proto/signer.proto new file mode 100644 index 0000000..9eed825 --- /dev/null +++ b/proto/signer.proto @@ -0,0 +1,99 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +syntax = "proto3"; + +import "lightning.proto"; + +package proto; + +option go_package = "github.com/bottlepay/lndsigner/proto"; + +// Signer is a service that gives access to the signing functionality of the +// daemon's wallet. +service Signer { + /* + SignMessage signs a message with the key specified in the key locator. The + returned signature is fixed-size LN wire format encoded. + + The main difference to SignMessage in the main RPC is that a specific key is + used to sign the message instead of the node identity private key. + */ + rpc SignMessage(SignMessageReq) returns (SignMessageResp); + + /* + DeriveSharedKey returns a shared secret key by performing Diffie-Hellman key + derivation between the ephemeral public key in the request and the node's + key specified in the key_desc parameter. Either a key locator or a raw + public key is expected in the key_desc, if neither is supplied, defaults to + the node's identity private key: + P_shared = privKeyNode * ephemeralPubkey + The resulting shared public key is serialized in the compressed format and + hashed with sha256, resulting in the final key length of 256bit. + */ + rpc DeriveSharedKey(SharedKeyRequest) returns (SharedKeyResponse); +} + +message SignMessageReq { + /* + The message to be signed. When using REST, this field must be encoded as + base64. + */ + bytes msg = 1; + + // The key locator that identifies which key to use for signing. + KeyLocator key_loc = 2; + + // Double-SHA256 hash instead of just the default single round. + bool double_hash = 3; + + /* + Use the compact (pubkey recoverable) format instead of the raw lnwire + format. This option cannot be used with Schnorr signatures. + */ + bool compact_sig = 4; + + /* + Use Schnorr signature. This option cannot be used with compact format. + */ + bool schnorr_sig = 5; + + /* + The optional Taproot tweak bytes to apply to the private key before creating + a Schnorr signature. The private key is tweaked as described in BIP-341: + privKey + h_tapTweak(internalKey || tapTweak) + */ + bytes schnorr_sig_tap_tweak = 6; +} +message SignMessageResp { + /* + The signature for the given message in the fixed-size LN wire format. + */ + bytes signature = 1; +} + +message SharedKeyRequest { + // The ephemeral public key to use for the DH key derivation. + bytes ephemeral_pubkey = 1; + + /* + Deprecated. The optional key locator of the local key that should be used. + If this parameter is not set then the node's identity private key will be + used. + */ + KeyLocator key_loc = 2 [ deprecated = true ]; + + /* + A key descriptor describes the key used for performing ECDH. Either a key + locator or a raw public key is expected, if neither is supplied, defaults to + the node's identity private key. + */ + KeyDescriptor key_desc = 3; +} + +message SharedKeyResponse { + // The shared public key, hashed with sha256. + bytes shared_key = 1; +} diff --git a/proto/signer_grpc.pb.go b/proto/signer_grpc.pb.go new file mode 100644 index 0000000..1480ddf --- /dev/null +++ b/proto/signer_grpc.pb.go @@ -0,0 +1,167 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package proto + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// SignerClient is the client API for Signer service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type SignerClient interface { + // + //SignMessage signs a message with the key specified in the key locator. The + //returned signature is fixed-size LN wire format encoded. + // + //The main difference to SignMessage in the main RPC is that a specific key is + //used to sign the message instead of the node identity private key. + SignMessage(ctx context.Context, in *SignMessageReq, opts ...grpc.CallOption) (*SignMessageResp, error) + // + //DeriveSharedKey returns a shared secret key by performing Diffie-Hellman key + //derivation between the ephemeral public key in the request and the node's + //key specified in the key_desc parameter. Either a key locator or a raw + //public key is expected in the key_desc, if neither is supplied, defaults to + //the node's identity private key: + //P_shared = privKeyNode * ephemeralPubkey + //The resulting shared public key is serialized in the compressed format and + //hashed with sha256, resulting in the final key length of 256bit. + DeriveSharedKey(ctx context.Context, in *SharedKeyRequest, opts ...grpc.CallOption) (*SharedKeyResponse, error) +} + +type signerClient struct { + cc grpc.ClientConnInterface +} + +func NewSignerClient(cc grpc.ClientConnInterface) SignerClient { + return &signerClient{cc} +} + +func (c *signerClient) SignMessage(ctx context.Context, in *SignMessageReq, opts ...grpc.CallOption) (*SignMessageResp, error) { + out := new(SignMessageResp) + err := c.cc.Invoke(ctx, "/proto.Signer/SignMessage", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *signerClient) DeriveSharedKey(ctx context.Context, in *SharedKeyRequest, opts ...grpc.CallOption) (*SharedKeyResponse, error) { + out := new(SharedKeyResponse) + err := c.cc.Invoke(ctx, "/proto.Signer/DeriveSharedKey", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// SignerServer is the server API for Signer service. +// All implementations must embed UnimplementedSignerServer +// for forward compatibility +type SignerServer interface { + // + //SignMessage signs a message with the key specified in the key locator. The + //returned signature is fixed-size LN wire format encoded. + // + //The main difference to SignMessage in the main RPC is that a specific key is + //used to sign the message instead of the node identity private key. + SignMessage(context.Context, *SignMessageReq) (*SignMessageResp, error) + // + //DeriveSharedKey returns a shared secret key by performing Diffie-Hellman key + //derivation between the ephemeral public key in the request and the node's + //key specified in the key_desc parameter. Either a key locator or a raw + //public key is expected in the key_desc, if neither is supplied, defaults to + //the node's identity private key: + //P_shared = privKeyNode * ephemeralPubkey + //The resulting shared public key is serialized in the compressed format and + //hashed with sha256, resulting in the final key length of 256bit. + DeriveSharedKey(context.Context, *SharedKeyRequest) (*SharedKeyResponse, error) + mustEmbedUnimplementedSignerServer() +} + +// UnimplementedSignerServer must be embedded to have forward compatible implementations. +type UnimplementedSignerServer struct { +} + +func (UnimplementedSignerServer) SignMessage(context.Context, *SignMessageReq) (*SignMessageResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method SignMessage not implemented") +} +func (UnimplementedSignerServer) DeriveSharedKey(context.Context, *SharedKeyRequest) (*SharedKeyResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeriveSharedKey not implemented") +} +func (UnimplementedSignerServer) mustEmbedUnimplementedSignerServer() {} + +// UnsafeSignerServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to SignerServer will +// result in compilation errors. +type UnsafeSignerServer interface { + mustEmbedUnimplementedSignerServer() +} + +func RegisterSignerServer(s grpc.ServiceRegistrar, srv SignerServer) { + s.RegisterService(&Signer_ServiceDesc, srv) +} + +func _Signer_SignMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SignMessageReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SignerServer).SignMessage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.Signer/SignMessage", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SignerServer).SignMessage(ctx, req.(*SignMessageReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Signer_DeriveSharedKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SharedKeyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SignerServer).DeriveSharedKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.Signer/DeriveSharedKey", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SignerServer).DeriveSharedKey(ctx, req.(*SharedKeyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Signer_ServiceDesc is the grpc.ServiceDesc for Signer service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Signer_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "proto.Signer", + HandlerType: (*SignerServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SignMessage", + Handler: _Signer_SignMessage_Handler, + }, + { + MethodName: "DeriveSharedKey", + Handler: _Signer_DeriveSharedKey_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "signer.proto", +} diff --git a/proto/walletkit.pb.go b/proto/walletkit.pb.go new file mode 100644 index 0000000..3aea118 --- /dev/null +++ b/proto/walletkit.pb.go @@ -0,0 +1,234 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0-devel +// protoc v3.14.0 +// source: walletkit.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SignPsbtRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The PSBT that should be signed. The PSBT must contain all required inputs, + //outputs, UTXO data and custom fields required to identify the signing key. + FundedPsbt []byte `protobuf:"bytes,1,opt,name=funded_psbt,json=fundedPsbt,proto3" json:"funded_psbt,omitempty"` +} + +func (x *SignPsbtRequest) Reset() { + *x = SignPsbtRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_walletkit_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SignPsbtRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignPsbtRequest) ProtoMessage() {} + +func (x *SignPsbtRequest) ProtoReflect() protoreflect.Message { + mi := &file_walletkit_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignPsbtRequest.ProtoReflect.Descriptor instead. +func (*SignPsbtRequest) Descriptor() ([]byte, []int) { + return file_walletkit_proto_rawDescGZIP(), []int{0} +} + +func (x *SignPsbtRequest) GetFundedPsbt() []byte { + if x != nil { + return x.FundedPsbt + } + return nil +} + +type SignPsbtResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The signed transaction in PSBT format. + SignedPsbt []byte `protobuf:"bytes,1,opt,name=signed_psbt,json=signedPsbt,proto3" json:"signed_psbt,omitempty"` + // The indices of signed inputs. + SignedInputs []uint32 `protobuf:"varint,2,rep,packed,name=signed_inputs,json=signedInputs,proto3" json:"signed_inputs,omitempty"` +} + +func (x *SignPsbtResponse) Reset() { + *x = SignPsbtResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_walletkit_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SignPsbtResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignPsbtResponse) ProtoMessage() {} + +func (x *SignPsbtResponse) ProtoReflect() protoreflect.Message { + mi := &file_walletkit_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignPsbtResponse.ProtoReflect.Descriptor instead. +func (*SignPsbtResponse) Descriptor() ([]byte, []int) { + return file_walletkit_proto_rawDescGZIP(), []int{1} +} + +func (x *SignPsbtResponse) GetSignedPsbt() []byte { + if x != nil { + return x.SignedPsbt + } + return nil +} + +func (x *SignPsbtResponse) GetSignedInputs() []uint32 { + if x != nil { + return x.SignedInputs + } + return nil +} + +var File_walletkit_proto protoreflect.FileDescriptor + +var file_walletkit_proto_rawDesc = []byte{ + 0x0a, 0x0f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x6b, 0x69, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x32, 0x0a, 0x0f, 0x53, 0x69, 0x67, 0x6e, + 0x50, 0x73, 0x62, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x66, + 0x75, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x70, 0x73, 0x62, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x0a, 0x66, 0x75, 0x6e, 0x64, 0x65, 0x64, 0x50, 0x73, 0x62, 0x74, 0x22, 0x58, 0x0a, 0x10, + 0x53, 0x69, 0x67, 0x6e, 0x50, 0x73, 0x62, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x70, 0x73, 0x62, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x50, 0x73, 0x62, + 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x69, 0x6e, 0x70, 0x75, + 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x0c, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, + 0x49, 0x6e, 0x70, 0x75, 0x74, 0x73, 0x32, 0x48, 0x0a, 0x09, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, + 0x4b, 0x69, 0x74, 0x12, 0x3b, 0x0a, 0x08, 0x53, 0x69, 0x67, 0x6e, 0x50, 0x73, 0x62, 0x74, 0x12, + 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x50, 0x73, 0x62, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x53, 0x69, 0x67, 0x6e, 0x50, 0x73, 0x62, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, + 0x6f, 0x74, 0x74, 0x6c, 0x65, 0x70, 0x61, 0x79, 0x2f, 0x6c, 0x6e, 0x64, 0x73, 0x69, 0x67, 0x6e, + 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_walletkit_proto_rawDescOnce sync.Once + file_walletkit_proto_rawDescData = file_walletkit_proto_rawDesc +) + +func file_walletkit_proto_rawDescGZIP() []byte { + file_walletkit_proto_rawDescOnce.Do(func() { + file_walletkit_proto_rawDescData = protoimpl.X.CompressGZIP(file_walletkit_proto_rawDescData) + }) + return file_walletkit_proto_rawDescData +} + +var file_walletkit_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_walletkit_proto_goTypes = []interface{}{ + (*SignPsbtRequest)(nil), // 0: proto.SignPsbtRequest + (*SignPsbtResponse)(nil), // 1: proto.SignPsbtResponse +} +var file_walletkit_proto_depIdxs = []int32{ + 0, // 0: proto.WalletKit.SignPsbt:input_type -> proto.SignPsbtRequest + 1, // 1: proto.WalletKit.SignPsbt:output_type -> proto.SignPsbtResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_walletkit_proto_init() } +func file_walletkit_proto_init() { + if File_walletkit_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_walletkit_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SignPsbtRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_walletkit_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SignPsbtResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_walletkit_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_walletkit_proto_goTypes, + DependencyIndexes: file_walletkit_proto_depIdxs, + MessageInfos: file_walletkit_proto_msgTypes, + }.Build() + File_walletkit_proto = out.File + file_walletkit_proto_rawDesc = nil + file_walletkit_proto_goTypes = nil + file_walletkit_proto_depIdxs = nil +} diff --git a/proto/walletkit.proto b/proto/walletkit.proto new file mode 100644 index 0000000..4cde64a --- /dev/null +++ b/proto/walletkit.proto @@ -0,0 +1,45 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +syntax = "proto3"; + +package proto; + +option go_package = "github.com/bottlepay/lndsigner/proto"; + +// WalletKit is a service that gives access to the core functionalities of the +// daemon's wallet. +service WalletKit { + /* + SignPsbt expects a partial transaction with all inputs and outputs fully + declared and tries to sign all unsigned inputs that have all required fields + (UTXO information, BIP32 derivation information, witness or sig scripts) + set. + If no error is returned, the PSBT is ready to be given to the next signer or + to be finalized if lnd was the last signer. + + NOTE: This RPC only signs inputs (and only those it can sign), it does not + perform any other tasks (such as coin selection, UTXO locking or + input/output/fee value validation, PSBT finalization). Any input that is + incomplete will be skipped. + */ + rpc SignPsbt(SignPsbtRequest) returns (SignPsbtResponse); +} + +message SignPsbtRequest { + /* + The PSBT that should be signed. The PSBT must contain all required inputs, + outputs, UTXO data and custom fields required to identify the signing key. + */ + bytes funded_psbt = 1; +} + +message SignPsbtResponse { + // The signed transaction in PSBT format. + bytes signed_psbt = 1; + + // The indices of signed inputs. + repeated uint32 signed_inputs = 2; +} diff --git a/proto/walletkit_grpc.pb.go b/proto/walletkit_grpc.pb.go new file mode 100644 index 0000000..3ff8382 --- /dev/null +++ b/proto/walletkit_grpc.pb.go @@ -0,0 +1,125 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package proto + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// WalletKitClient is the client API for WalletKit service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type WalletKitClient interface { + // + //SignPsbt expects a partial transaction with all inputs and outputs fully + //declared and tries to sign all unsigned inputs that have all required fields + //(UTXO information, BIP32 derivation information, witness or sig scripts) + //set. + //If no error is returned, the PSBT is ready to be given to the next signer or + //to be finalized if lnd was the last signer. + // + //NOTE: This RPC only signs inputs (and only those it can sign), it does not + //perform any other tasks (such as coin selection, UTXO locking or + //input/output/fee value validation, PSBT finalization). Any input that is + //incomplete will be skipped. + SignPsbt(ctx context.Context, in *SignPsbtRequest, opts ...grpc.CallOption) (*SignPsbtResponse, error) +} + +type walletKitClient struct { + cc grpc.ClientConnInterface +} + +func NewWalletKitClient(cc grpc.ClientConnInterface) WalletKitClient { + return &walletKitClient{cc} +} + +func (c *walletKitClient) SignPsbt(ctx context.Context, in *SignPsbtRequest, opts ...grpc.CallOption) (*SignPsbtResponse, error) { + out := new(SignPsbtResponse) + err := c.cc.Invoke(ctx, "/proto.WalletKit/SignPsbt", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// WalletKitServer is the server API for WalletKit service. +// All implementations must embed UnimplementedWalletKitServer +// for forward compatibility +type WalletKitServer interface { + // + //SignPsbt expects a partial transaction with all inputs and outputs fully + //declared and tries to sign all unsigned inputs that have all required fields + //(UTXO information, BIP32 derivation information, witness or sig scripts) + //set. + //If no error is returned, the PSBT is ready to be given to the next signer or + //to be finalized if lnd was the last signer. + // + //NOTE: This RPC only signs inputs (and only those it can sign), it does not + //perform any other tasks (such as coin selection, UTXO locking or + //input/output/fee value validation, PSBT finalization). Any input that is + //incomplete will be skipped. + SignPsbt(context.Context, *SignPsbtRequest) (*SignPsbtResponse, error) + mustEmbedUnimplementedWalletKitServer() +} + +// UnimplementedWalletKitServer must be embedded to have forward compatible implementations. +type UnimplementedWalletKitServer struct { +} + +func (UnimplementedWalletKitServer) SignPsbt(context.Context, *SignPsbtRequest) (*SignPsbtResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SignPsbt not implemented") +} +func (UnimplementedWalletKitServer) mustEmbedUnimplementedWalletKitServer() {} + +// UnsafeWalletKitServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to WalletKitServer will +// result in compilation errors. +type UnsafeWalletKitServer interface { + mustEmbedUnimplementedWalletKitServer() +} + +func RegisterWalletKitServer(s grpc.ServiceRegistrar, srv WalletKitServer) { + s.RegisterService(&WalletKit_ServiceDesc, srv) +} + +func _WalletKit_SignPsbt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SignPsbtRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WalletKitServer).SignPsbt(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.WalletKit/SignPsbt", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WalletKitServer).SignPsbt(ctx, req.(*SignPsbtRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// WalletKit_ServiceDesc is the grpc.ServiceDesc for WalletKit service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var WalletKit_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "proto.WalletKit", + HandlerType: (*WalletKitServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SignPsbt", + Handler: _WalletKit_SignPsbt_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "walletkit.proto", +} diff --git a/rpcserver.go b/rpcserver.go new file mode 100644 index 0000000..aceee6d --- /dev/null +++ b/rpcserver.go @@ -0,0 +1,179 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package lndsigner + +import ( + "context" + "fmt" + + "github.com/bottlepay/lndsigner/keyring" + "github.com/bottlepay/lndsigner/proto" + "github.com/hashicorp/vault/api" + "github.com/tv42/zbase32" + "google.golang.org/grpc" + "gopkg.in/macaroon-bakery.v2/bakery" +) + +// keyRingKeyStruct is a struct used to look up a keyring passed in a context. +type keyRingKeyStruct struct{} + +var ( + keyRingKey = keyRingKeyStruct{} + + // nodePermissions is a slice of all entities for using signing + // permissions for authorization purposes, all lowercase. + nodePermissions = []bakery.Op{ + { + Entity: "onchain", + Action: "write", + }, + { + Entity: "message", + Action: "write", + }, + { + Entity: "signer", + Action: "generate", + }, + } + + // mainRPCServerPermissions is a mapping of the main RPC server calls + // to the permissions they require. + mainRPCServerPermissions = map[string][]bakery.Op{ + "/proto.Lightning/SignMessage": {{ + Entity: "message", + Action: "write", + }}, + } +) + +// rpcServer is a gRPC front end to the signer daemon. +type rpcServer struct { + // Required by the grpc-gateway/v2 library for forward compatibility. + // Must be after the atomically used variables to not break struct + // alignment. + proto.UnimplementedLightningServer + + perms map[string][]bakery.Op + + client *api.Logical + + checker *bakery.Checker + + cfg *Config +} + +// A compile time check to ensure that rpcServer fully implements the +// LightningServer gRPC service. +var _ proto.LightningServer = (*rpcServer)(nil) + +// newRPCServer creates and returns a new instance of the rpcServer. Before +// dependencies are added, this will be an non-functioning RPC server only to +// be used to register the LightningService with the gRPC server. +func newRPCServer(cfg *Config, c *api.Logical, + checker *bakery.Checker) *rpcServer { + + return &rpcServer{ + cfg: cfg, + client: c, + checker: checker, + perms: make(map[string][]bakery.Op), + } +} + +// intercept allows the RPC server to intercept requests to ensure that they're +// authorized by a macaroon signed by the macaroon root key. +func (r *rpcServer) intercept(ctx context.Context, req interface{}, + info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) ( + interface{}, error) { + + node, coin, err := r.checkMac(ctx, info.FullMethod) + if err != nil { + return nil, err + } + + keyRing := keyring.NewKeyRing(r.client, node, coin) + + return handler( + context.WithValue(ctx, keyRingKey, keyRing), + req, + ) +} + +// RegisterWithGrpcServer registers the rpcServer and any subservers with the +// root gRPC server. +func (r *rpcServer) RegisterWithGrpcServer(grpcServer *grpc.Server) error { + // Register the main RPC server. + for k, v := range mainRPCServerPermissions { + r.perms[k] = v + } + lnDesc := proto.Lightning_ServiceDesc + lnDesc.ServiceName = "lnrpc.Lightning" + grpcServer.RegisterService(&lnDesc, r) + + // Register the wallet subserver. + for k, v := range walletPermissions { + r.perms[k] = v + } + walletDesc := proto.WalletKit_ServiceDesc + walletDesc.ServiceName = "walletrpc.WalletKit" + grpcServer.RegisterService(&walletDesc, &walletKit{ + server: r, + }) + + // Register the signer subserver. + for k, v := range signerPermissions { + r.perms[k] = v + } + signerDesc := proto.Signer_ServiceDesc + signerDesc.ServiceName = "signrpc.Signer" + grpcServer.RegisterService(&signerDesc, &signerServer{ + server: r, + }) + + return nil +} + +var ( + // signedMsgPrefix is a special prefix that we'll prepend to any + // messages we sign/verify. We do this to ensure that we don't + // accidentally sign a sighash, or other sensitive material. By + // prepending this fragment, we mind message signing to our particular + // context. + signedMsgPrefix = []byte("Lightning Signed Message:") +) + +// SignMessage signs a message with the resident node's private key. The +// returned signature string is zbase32 encoded and pubkey recoverable, meaning +// that only the message digest and signature are needed for verification. +func (r *rpcServer) SignMessage(ctx context.Context, + in *proto.SignMessageRequest) (*proto.SignMessageResponse, error) { + + if in.Msg == nil { + return nil, fmt.Errorf("need a message to sign") + } + + in.Msg = append(signedMsgPrefix, in.Msg...) + keyLoc := keyring.KeyLocator{ + Family: 6, + Index: 0, + } + + keyRing := ctx.Value(keyRingKey).(*keyring.KeyRing) + if keyRing == nil { + return nil, fmt.Errorf("no node/coin from macaroon") + } + + sig, err := keyRing.SignMessage( + keyLoc, in.Msg, !in.SingleHash, true, + ) + if err != nil { + return nil, err + } + + sigStr := zbase32.EncodeToString(sig) + return &proto.SignMessageResponse{Signature: sigStr}, nil +} diff --git a/signer_server.go b/signer_server.go new file mode 100644 index 0000000..4ea2dae --- /dev/null +++ b/signer_server.go @@ -0,0 +1,224 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package lndsigner + +import ( + "context" + "fmt" + + "github.com/bottlepay/lndsigner/keyring" + "github.com/bottlepay/lndsigner/proto" + "github.com/bottlepay/lndsigner/vault" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "gopkg.in/macaroon-bakery.v2/bakery" +) + +var ( + // signerPermissions maps RPC calls to the permissions they require. + signerPermissions = map[string][]bakery.Op{ + "/proto.Signer/SignMessage": {{ + Entity: "signer", + Action: "generate", + }}, + "/proto.Signer/DeriveSharedKey": {{ + Entity: "signer", + Action: "generate", + }}, + } +) + +// Server is a sub-server of the main RPC server: the signer RPC. This sub RPC +// server allows external callers to access the full signing capabilities of +// lnd. This allows callers to create custom protocols, external to lnd, even +// backed by multiple distinct lnd across independent failure domains. +type signerServer struct { + // Required by the grpc-gateway/v2 library for forward compatibility. + proto.UnimplementedSignerServer + + server *rpcServer +} + +// A compile time check to ensure that Server fully implements the SignerServer +// gRPC service. +var _ proto.SignerServer = (*signerServer)(nil) + +// SignMessage signs a message with the key specified in the key locator. The +// returned signature is fixed-size LN wire format encoded. +func (s *signerServer) SignMessage(ctx context.Context, + in *proto.SignMessageReq) (*proto.SignMessageResp, error) { + + if in.Msg == nil { + return nil, fmt.Errorf("a message to sign MUST be passed in") + } + if in.KeyLoc == nil { + return nil, fmt.Errorf("a key locator MUST be passed in") + } + if in.SchnorrSig && in.CompactSig { + return nil, fmt.Errorf("compact format can not be used for " + + "Schnorr signatures") + } + + // Describe the private key we'll be using for signing. + keyLocator := keyring.KeyLocator{ + Family: uint32(in.KeyLoc.KeyFamily), + Index: uint32(in.KeyLoc.KeyIndex), + } + + keyRing := ctx.Value(keyRingKey).(*keyring.KeyRing) + if keyRing == nil { + return nil, fmt.Errorf("no node/coin from macaroon") + } + + // Use the schnorr signature algorithm to sign the message. + if in.SchnorrSig { + sig, err := keyRing.SignMessageSchnorr( + keyLocator, in.Msg, in.DoubleHash, + in.SchnorrSigTapTweak, + ) + if err != nil { + return nil, fmt.Errorf("can't sign the hash: %v", err) + } + + sigParsed, err := schnorr.ParseSignature(sig.Serialize()) + if err != nil { + return nil, fmt.Errorf("can't parse Schnorr "+ + "signature: %v", err) + } + + return &proto.SignMessageResp{ + Signature: sigParsed.Serialize(), + }, nil + } + + // Create the raw ECDSA signature first and convert it to the final wire + // format after. + sig, err := keyRing.SignMessage( + keyLocator, in.Msg, in.DoubleHash, in.CompactSig, + ) + if err != nil { + return nil, fmt.Errorf("can't sign the hash: %v", err) + } + return &proto.SignMessageResp{ + Signature: sig, + }, nil +} + +// DeriveSharedKey returns a shared secret key by performing Diffie-Hellman key +// derivation between the ephemeral public key in the request and the node's +// key specified in the key_desc parameter. Either a key locator or a raw public +// key is expected in the key_desc, if neither is supplied, defaults to the +// node's identity private key. The old key_loc parameter in the request +// shouldn't be used anymore. +// The resulting shared public key is serialized in the compressed format and +// hashed with sha256, resulting in the final key length of 256bit. +func (s *signerServer) DeriveSharedKey(ctx context.Context, + in *proto.SharedKeyRequest) (*proto.SharedKeyResponse, error) { + + // Check that EphemeralPubkey is valid. + ephemeralPubkey, err := parseRawKeyBytes(in.EphemeralPubkey) + if err != nil { + return nil, fmt.Errorf("error in ephemeral pubkey: %v", err) + } + if ephemeralPubkey == nil { + return nil, fmt.Errorf("must provide ephemeral pubkey") + } + + // Check for backward compatibility. The caller either specifies the old + // key_loc field, or the new key_desc field, but not both. + if in.KeyDesc != nil && in.KeyLoc != nil { + return nil, fmt.Errorf("use either key_desc or key_loc") + } + + // When key_desc is used, the key_desc.key_loc is expected as the caller + // needs to specify the KeyFamily. + if in.KeyDesc != nil && in.KeyDesc.KeyLoc == nil { + return nil, fmt.Errorf("when setting key_desc the field " + + "key_desc.key_loc must also be set") + } + + // We extract two params, rawKeyBytes and keyLoc. Notice their initial + // values will be overwritten if not using the deprecated RPC param. + var rawKeyBytes []byte + keyLoc := in.KeyLoc + if in.KeyDesc != nil { + keyLoc = in.KeyDesc.GetKeyLoc() + rawKeyBytes = in.KeyDesc.GetRawKeyBytes() + } + + // When no keyLoc is supplied, defaults to the node's identity private + // key. + if keyLoc == nil { + keyLoc = &proto.KeyLocator{ + KeyFamily: int32(vault.NodeKeyAcct), + KeyIndex: 0, + } + } + + // Check the caller is using either the key index or the raw public key + // to perform the ECDH, we can't have both. + if rawKeyBytes != nil && keyLoc.KeyIndex != 0 { + return nil, fmt.Errorf("use either raw_key_bytes or key_index") + } + + // Check the raw public key is valid. Notice that if the rawKeyBytes is + // empty, the parseRawKeyBytes won't return an error, a nil + // *btcec.PublicKey is returned instead. + pk, err := parseRawKeyBytes(rawKeyBytes) + if err != nil { + return nil, fmt.Errorf("error in raw pubkey: %v", err) + } + + // Create a key descriptor. When the KeyIndex is not specified, it uses + // the empty value 0, and when the raw public key is not specified, the + // pk is nil. + keyDescriptor := keyring.KeyDescriptor{ + KeyLocator: keyring.KeyLocator{ + Family: uint32(keyLoc.KeyFamily), + Index: uint32(keyLoc.KeyIndex), + }, + PubKey: pk, + } + + keyRing := ctx.Value(keyRingKey).(*keyring.KeyRing) + if keyRing == nil { + return nil, fmt.Errorf("no node/coin from macaroon") + } + + // Derive the shared key using ECDH and hashing the serialized + // compressed shared point. + sharedKeyHash, err := keyRing.ECDH(keyDescriptor, ephemeralPubkey) + if err != nil { + err := fmt.Errorf("unable to derive shared key: %v", err) + signerLog.Error(err) + return nil, err + } + + return &proto.SharedKeyResponse{SharedKey: sharedKeyHash[:]}, nil +} + +// parseRawKeyBytes checks that the provided raw public key is valid and returns +// the public key. A nil public key is returned if the length of the rawKeyBytes +// is zero. +func parseRawKeyBytes(rawKeyBytes []byte) (*btcec.PublicKey, error) { + switch { + case len(rawKeyBytes) == 33: + // If a proper raw key was provided, then we'll attempt + // to decode and parse it. + return btcec.ParsePubKey(rawKeyBytes) + + case len(rawKeyBytes) == 0: + // No key is provided, return nil. + return nil, nil + + default: + // If the user provided a raw key, but it's of the + // wrong length, then we'll return with an error. + return nil, fmt.Errorf("pubkey must be " + + "serialized in compressed format if " + + "specified") + } +} diff --git a/vault/backend.go b/vault/backend.go new file mode 100644 index 0000000..040ac26 --- /dev/null +++ b/vault/backend.go @@ -0,0 +1,616 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package vault + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcutil/hdkeychain" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/logical" +) + +func (b *backend) listAccounts(ctx context.Context, req *logical.Request, + data *framework.FieldData) (*logical.Response, error) { + + strNode := data.Get("node").(string) + + seed, net, err := b.getNode(ctx, req.Storage, strNode) + if err != nil { + b.Logger().Error("Failed to retrieve node info", + "node", strNode, "error", err) + return nil, err + } + defer zero(seed) + + rootKey, err := hdkeychain.NewMaster(seed, net) + if err != nil { + return nil, err + } + defer rootKey.Zero() + + acctList := "{\n \"accounts\": [\n" + + listAccount := func(purpose, coin, act uint32, addrType string, + version []byte) (string, error) { + + strListing := "" + + // Derive purpose. + purposeKey, err := rootKey.DeriveNonStandard( + purpose + hdkeychain.HardenedKeyStart, + ) + if err != nil { + return "", err + } + defer purposeKey.Zero() + + // Derive coin. + coinKey, err := purposeKey.DeriveNonStandard( + coin + hdkeychain.HardenedKeyStart, + ) + if err != nil { + return "", err + } + defer coinKey.Zero() + + // Derive account. + actKey, err := coinKey.DeriveNonStandard( + act + hdkeychain.HardenedKeyStart, + ) + if err != nil { + return "", err + } + defer actKey.Zero() + + // Get account watch-only pubkey. + xPub, err := actKey.Neuter() + if err != nil { + return "", err + } + + // Ensure we get the right HDVersion for the account key. + if version != nil { + xPub, err = xPub.CloneWithVersion(version) + if err != nil { + return "", err + } + } + + strPurpose := fmt.Sprintf("%d", purpose) + strCoin := fmt.Sprintf("%d", coin) + strAct := fmt.Sprintf("%d", act) + + strListing += " {\n" + + strListing += " \"name\": \"" + if act == 0 { + strListing += "default" + } else { + strListing += "act:" + strAct + } + strListing += "\",\n" + + strListing += " \"address_type\": \"" + addrType + + "\",\n" + + strListing += " \"extended_public_key\": \"" + + xPub.String() + "\",\n" + + strListing += " \"master_key_fingerprint\": null,\n" + + strListing += " \"derivation_path\": \"m/" + + strPurpose + "'/" + strCoin + "'/" + strAct + "'\",\n" + + strListing += " \"external_key_count\": 0,\n" + + strListing += " \"internal_key_count\": 0,\n" + + strListing += " \"watch_only\": false\n" + + strListing += " }" + + return strListing, nil + } + + for _, acctInfo := range defaultPurposes { + strListing, err := listAccount( + acctInfo.purpose, + 0, + 0, + acctInfo.addrType, + acctInfo.hdVersion[net.HDCoinType][:], + ) + if err != nil { + b.Logger().Error("Failed to derive default account", + "node", strNode, "err", err) + return nil, err + } + + acctList += strListing + ",\n" + } + + for act := uint32(0); act <= MaxAcctID; act++ { + strListing, err := listAccount( + Bip0043purpose, + net.HDCoinType, + act, + "WITNESS_PUBKEY_HASH", + nil, + ) + if err != nil { + b.Logger().Error("Failed to derive Lightning account", + "node", strNode, "err", err) + return nil, err + } + + acctList += strListing + + if act < MaxAcctID { + acctList += "," + } + + acctList += "\n" + } + + acctList += " ]\n}" + + return &logical.Response{ + Data: map[string]interface{}{ + "acctList": acctList, + }, + }, nil +} + +func (b *backend) ecdh(ctx context.Context, req *logical.Request, + data *framework.FieldData) (*logical.Response, error) { + + peerPubHex := data.Get("peer").(string) + if len(peerPubHex) != 2*btcec.PubKeyBytesLenCompressed { + b.Logger().Error("Peer pubkey is wrong length", + "peer", peerPubHex) + return nil, errors.New("invalid peer pubkey") + } + + peerPubBytes, err := hex.DecodeString(peerPubHex) + if err != nil { + b.Logger().Error("Failed to decode peer pubkey hex", + "error", err) + return nil, err + } + + peerPubKey, err := btcec.ParsePubKey(peerPubBytes) + if err != nil { + b.Logger().Error("Failed to parse peer pubkey", + "error", err) + return nil, err + } + + var ( + pubJacobian btcec.JacobianPoint + s btcec.JacobianPoint + ) + peerPubKey.AsJacobian(&pubJacobian) + + strNode := data.Get("node").(string) + + seed, net, err := b.getNode(ctx, req.Storage, strNode) + if err != nil { + b.Logger().Error("Failed to retrieve node info", + "node", strNode, "error", err) + return nil, err + } + defer zero(seed) + + privKey, err := derivePrivKey(seed, net, data.Get("path").([]int)) + if err != nil { + b.Logger().Error("Failed to derive privkey", + "node", strNode, "error", err) + return nil, err + } + defer privKey.Zero() + + err = checkRequiredPubKey(privKey, data.Get("pubkey").(string)) + if err != nil { + // We log here as warning because there's no case when we + // should be using ECDH with a mismatching own key. + b.Logger().Warn("Pubkey mismatch", + "node", strNode, "error", err) + return nil, err + } + + ecPrivKey, err := privKey.ECPrivKey() + if err != nil { + b.Logger().Error("Failed to derive valid ECDSA privkey", + "node", strNode, "error", err) + return nil, err + } + defer ecPrivKey.Zero() + + btcec.ScalarMultNonConst(&ecPrivKey.Key, &pubJacobian, &s) + s.ToAffine() + sPubKey := btcec.NewPublicKey(&s.X, &s.Y) + h := sha256.Sum256(sPubKey.SerializeCompressed()) + + return &logical.Response{ + Data: map[string]interface{}{ + "sharedkey": hex.EncodeToString(h[:]), + }, + }, nil +} + +func (b *backend) derivePubKey(ctx context.Context, req *logical.Request, + data *framework.FieldData) (*logical.Response, error) { + + strNode := data.Get("node").(string) + + seed, net, err := b.getNode(ctx, req.Storage, strNode) + if err != nil { + b.Logger().Error("Failed to retrieve node info", + "node", strNode, "error", err) + return nil, err + } + defer zero(seed) + + pubKey, err := derivePubKey(seed, net, data.Get("path").([]int)) + if err != nil { + b.Logger().Error("Failed to derive pubkey", + "node", strNode, "error", err) + return nil, err + } + + pubKeyBytes, err := extKeyToPubBytes(pubKey) + if err != nil { + b.Logger().Error("derivePubKey: Failed to get pubkey bytes", + "node", strNode, "error", err) + return nil, err + } + + return &logical.Response{ + Data: map[string]interface{}{ + "pubkey": hex.EncodeToString(pubKeyBytes), + }, + }, nil +} + +func (b *backend) deriveAndSign(ctx context.Context, req *logical.Request, + data *framework.FieldData) (*logical.Response, error) { + + tapTweakHex := data.Get("taptweak").(string) + singleTweakHex := data.Get("ln1tweak").(string) + doubleTweakHex := data.Get("ln2tweak").(string) + + numTweaks := int(0) + + if len(singleTweakHex) > 0 { + numTweaks++ + } + if len(doubleTweakHex) > 0 { + numTweaks++ + } + + if numTweaks > 1 { + b.Logger().Error("Both single and double tweak specified") + return nil, errors.New("both single and double tweak specified") + } + + strNode := data.Get("node").(string) + + seed, net, err := b.getNode(ctx, req.Storage, strNode) + if err != nil { + b.Logger().Error("Failed to retrieve node info", + "node", strNode, "error", err) + return nil, err + } + defer zero(seed) + + privKey, err := derivePrivKey(seed, net, data.Get("path").([]int)) + if err != nil { + b.Logger().Error("Failed to derive privkey", + "node", strNode, "error", err) + return nil, err + } + defer privKey.Zero() + + err = checkRequiredPubKey(privKey, data.Get("pubkey").(string)) + if err != nil { + // We log here as info because this is expected when signing + // a PSBT. + b.Logger().Info("Pubkey mismatch", + "node", strNode, "error", err) + return nil, err + } + + ecPrivKey, err := privKey.ECPrivKey() + if err != nil { + b.Logger().Error("Failed to derive valid ECDSA privkey", + "node", strNode, "error", err) + return nil, err + } + defer ecPrivKey.Zero() + + signMethod := data.Get("method").(string) + + // Taproot tweak. + var tapTweakBytes []byte + + if len(tapTweakHex) > 0 { + tapTweakBytes, err = hex.DecodeString(tapTweakHex) + if err != nil { + b.Logger().Error("Couldn't decode taptweak hex", + "error", err) + return nil, err + } + } + + if signMethod == "schnorr" { + ecPrivKey = txscript.TweakTaprootPrivKey( + ecPrivKey, + tapTweakBytes, + ) + } + + switch { + // Single commitment tweak as used by SignPsbt. + case len(singleTweakHex) > 0: + singleTweakBytes, err := hex.DecodeString(singleTweakHex) + if err != nil { + b.Logger().Error("Couldn't decode ln1tweak hex", + "error", err) + return nil, err + } + + ecPrivKey = tweakPrivKey( + ecPrivKey, + singleTweakBytes, + ) + + // Double revocation tweak as used by SignPsbt. + case len(doubleTweakHex) > 0: + doubleTweakBytes, err := hex.DecodeString(doubleTweakHex) + if err != nil { + b.Logger().Error("Couldn't decode ln2tweak hex", + "error", err) + return nil, err + } + + doubleTweakKey, _ := btcec.PrivKeyFromBytes(doubleTweakBytes) + ecPrivKey = deriveRevocationPrivKey(ecPrivKey, doubleTweakKey) + } + + digest := data.Get("digest").(string) + if len(digest) != 64 { + b.Logger().Error("Digest is not hex-encoded 32-byte value") + return nil, errors.New("invalid digest") + } + + digestBytes, err := hex.DecodeString(digest) + if err != nil { + b.Logger().Error("Failed to decode digest from hex", + "error", err) + return nil, err + } + + var sigBytes []byte + + // TODO(aakselrod): check derivation paths are sane for the type of + // signature we're requesting. + switch signMethod { + case "ecdsa": + sigBytes = ecdsa.Sign(ecPrivKey, digestBytes).Serialize() + case "ecdsa-compact": + sigBytes, _ = ecdsa.SignCompact(ecPrivKey, digestBytes, true) + case "schnorr": + sig, err := schnorr.Sign(ecPrivKey, digestBytes) + if err != nil { + b.Logger().Error("Failed to sign digest using Schnorr", + "node", strNode, "error", err) + return nil, err + } + + sigBytes = sig.Serialize() + default: + b.Logger().Info("Requested invalid signing method", + "method", signMethod) + return nil, errors.New("invalid signing method") + } + + // We return the pre-tweak pubkey for populating PSBTs and other uses. + pubKeyBytes, err := extKeyToPubBytes(privKey) + if err != nil { + b.Logger().Error("derivePubKey: Failed to get pubkey bytes", + "node", strNode, "error", err) + return nil, err + } + + return &logical.Response{ + Data: map[string]interface{}{ + "signature": hex.EncodeToString(sigBytes), + "pubkey": hex.EncodeToString(pubKeyBytes), + }, + }, nil +} + +func (b *backend) getNode(ctx context.Context, storage logical.Storage, + id string) ([]byte, *chaincfg.Params, error) { + + if len(id) != 2*btcec.PubKeyBytesLenCompressed { + return nil, nil, errors.New("invalid node id") + } + + nodePath := "lnd-nodes/" + id + entry, err := storage.Get(ctx, nodePath) + if err != nil { + return nil, nil, err + } + + if entry == nil { + return nil, nil, errors.New("node not found") + } + + if len(entry.Value) <= hdkeychain.RecommendedSeedLen { + return nil, nil, errors.New("got invalid seed from storage") + } + + net, err := getNet(string(entry.Value[hdkeychain.RecommendedSeedLen:])) + if err != nil { + return nil, nil, err + } + + return entry.Value[:hdkeychain.RecommendedSeedLen], net, nil +} + +func (b *backend) listNodes(ctx context.Context, req *logical.Request, + data *framework.FieldData) (*logical.Response, error) { + + nodes, err := req.Storage.List(ctx, "lnd-nodes/") + if err != nil { + b.Logger().Error("Failed to retrieve the list of nodes", + "error", err) + return nil, err + } + + respData := make(map[string]interface{}) + for _, node := range nodes { + seed, net, err := b.getNode(ctx, req.Storage, node) + if err != nil { + b.Logger().Error("Failed to retrieve node info", + "node", node, "error", err) + return nil, err + } + defer zero(seed) + + respData[node] = int(net.HDCoinType) + } + + return &logical.Response{ + Data: respData, + }, nil +} + +func (b *backend) createNode(ctx context.Context, req *logical.Request, + data *framework.FieldData) (*logical.Response, error) { + + strNet := data.Get("network").(string) + net, err := getNet(strNet) + if err != nil { + b.Logger().Error("Failed to parse network", "error", err) + return nil, err + } + + var seed []byte + defer zero(seed) + + err = hdkeychain.ErrUnusableSeed + for err == hdkeychain.ErrUnusableSeed { + seed, err = hdkeychain.GenerateSeed( + hdkeychain.RecommendedSeedLen, + ) + } + if err != nil { + b.Logger().Error("Failed to generate new LND seed", + "error", err) + return nil, err + } + + nodePubKey, err := derivePubKey(seed, net, []int{ + int(Bip0043purpose + hdkeychain.HardenedKeyStart), + int(net.HDCoinType + hdkeychain.HardenedKeyStart), + int(NodeKeyAcct + hdkeychain.HardenedKeyStart), + 0, + 0, + }) + if err != nil { + b.Logger().Error("Failed to derive node pubkey from LND seed", + "error", err) + return nil, err + } + + pubKeyBytes, err := extKeyToPubBytes(nodePubKey) + if err != nil { + b.Logger().Error("createNode: Failed to get pubkey bytes", + "error", err) + return nil, err + } + + strPubKey := hex.EncodeToString(pubKeyBytes) + nodePath := "lnd-nodes/" + strPubKey + + seed = append(seed, []byte(strNet)...) + err = req.Storage.Put(ctx, &logical.StorageEntry{ + Key: nodePath, + Value: seed, + SealWrap: true, + }) + if err != nil { + b.Logger().Error("Failed to save seed for node", + "error", err) + return nil, err + } + + b.Logger().Info("Wrote new LND node seed", "pubkey", strPubKey) + + return &logical.Response{ + Data: map[string]interface{}{ + "node": strPubKey, + }, + }, nil +} + +func getNet(strNet string) (*chaincfg.Params, error) { + switch strNet { + /*case "mainnet": + return &chaincfg.MainNetParams, nil + */ + case "testnet": + return &chaincfg.TestNet3Params, nil + + case "simnet": + return &chaincfg.SimNetParams, nil + + case "signet": + return &chaincfg.SigNetParams, nil + + case "regtest": + return &chaincfg.RegressionNetParams, nil + + default: + return nil, errors.New("invalid network specified: " + strNet) + } +} + +func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, + error) { + + var b backend + b.Backend = &framework.Backend{ + Help: "", + Paths: framework.PathAppend(b.paths()), + PathsSpecial: &logical.Paths{ + SealWrapStorage: []string{ + "lnd-nodes/", + }, + }, + Secrets: []*framework.Secret{}, + BackendType: logical.TypeLogical, + } + + err := b.Setup(ctx, conf) + if err != nil { + return nil, err + } + + return &b, nil +} diff --git a/vault/keys.go b/vault/keys.go new file mode 100644 index 0000000..26a40df --- /dev/null +++ b/vault/keys.go @@ -0,0 +1,231 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package vault + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "math" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil/hdkeychain" + "github.com/btcsuite/btcd/chaincfg" +) + +func extKeyToPubBytes(key *hdkeychain.ExtendedKey) ([]byte, error) { + ecPubKey, err := key.ECPubKey() + if err != nil { + return nil, err + } + + return ecPubKey.SerializeCompressed(), nil +} + +func checkRequiredPubKey(derived *hdkeychain.ExtendedKey, + required string) error { + + if required == "" { + return nil + } + + pubKeyBytes, err := extKeyToPubBytes(derived) + if err != nil { + return err + } + + requiredBytes, err := hex.DecodeString(required) + if err != nil { + return err + } + + if !bytes.Equal(requiredBytes, pubKeyBytes) { + return errors.New("pubkey mismatch") + } + + return nil +} + +func derivePrivKey(seed []byte, net *chaincfg.Params, + derivationPath []int) (*hdkeychain.ExtendedKey, error) { + + if len(derivationPath) != 5 { + return nil, errors.New("derivation path not 5 elements") + } + + derPath := make([]uint32, 5) + + for idx, element := range derivationPath { + if element < 0 { + return nil, errors.New("negative derivation path " + + "element") + } + + if element > math.MaxUint32 { + return nil, errors.New("derivation path element > " + + "MaxUint32") + } + + if idx < 3 && element < hdkeychain.HardenedKeyStart { + return nil, fmt.Errorf("element at index %d is not "+ + "hardened", idx) + } + + derPath[idx] = uint32(element) + } + + rootKey, err := hdkeychain.NewMaster(seed, net) + if err != nil { + return nil, err + } + defer rootKey.Zero() + + // Derive purpose. + purposeKey, err := rootKey.DeriveNonStandard( + derPath[0], + ) + if err != nil { + return nil, errors.New("error deriving purpose") + } + defer purposeKey.Zero() + + // Derive coin type. + coinTypeKey, err := purposeKey.DeriveNonStandard( + derPath[1], + ) + if err != nil { + return nil, errors.New("error deriving coin type") + } + defer coinTypeKey.Zero() + + // Derive account. + accountKey, err := coinTypeKey.DeriveNonStandard( + derPath[2], + ) + if err != nil { + return nil, errors.New("error deriving account") + } + defer accountKey.Zero() + + // Derive branch. + branchKey, err := accountKey.DeriveNonStandard(derPath[3]) + if err != nil { + return nil, errors.New("error deriving branch") + } + defer branchKey.Zero() + + // Derive index. + indexKey, err := branchKey.DeriveNonStandard(derPath[4]) + if err != nil { + return nil, errors.New("error deriving index") + } + + return indexKey, nil +} + +func derivePubKey(seed []byte, net *chaincfg.Params, derivationPath []int) ( + *hdkeychain.ExtendedKey, error) { + + privKey, err := derivePrivKey(seed, net, derivationPath) + if err != nil { + return nil, err + } + + return privKey.Neuter() +} + +// zero sets all bytes in the passed slice to zero. This is used to +// explicitly clear private key material from memory. +func zero(b []byte) { + lenb := len(b) + for i := 0; i < lenb; i++ { + b[i] = 0 + } +} + +// tweakPrivKey tweaks the private key of a public base point given a per +// commitment point. The per commitment secret is the revealed revocation +// secret for the commitment state in question. This private key will only need +// to be generated in the case that a channel counter party broadcasts a +// revoked state. Precisely, the following operation is used to derive a +// tweaked private key: +// +// - tweakPriv := basePriv + sha256(commitment || basePub) mod N +// +// Where N is the order of the sub-group. +func tweakPrivKey(basePriv *btcec.PrivateKey, + commitTweak []byte) *btcec.PrivateKey { + + // tweakInt := sha256(commitPoint || basePub) + tweakScalar := new(btcec.ModNScalar) + tweakScalar.SetByteSlice(commitTweak) + + tweakScalar.Add(&basePriv.Key) + + return &btcec.PrivateKey{Key: *tweakScalar} +} + +// singleTweakBytes computes set of bytes we call the single tweak. The purpose +// of the single tweak is to randomize all regular delay and payment base +// points. To do this, we generate a hash that binds the commitment point to +// the pay/delay base point. The end end results is that the basePoint is +// tweaked as follows: +// +// - key = basePoint + sha256(commitPoint || basePoint)*G +func singleTweakBytes(commitPoint, basePoint *btcec.PublicKey) []byte { + h := sha256.New() + h.Write(commitPoint.SerializeCompressed()) + h.Write(basePoint.SerializeCompressed()) + return h.Sum(nil) +} + +// deriveRevocationPrivKey derives the revocation private key given a node's +// commitment private key, and the preimage to a previously seen revocation +// hash. Using this derived private key, a node is able to claim the output +// within the commitment transaction of a node in the case that they broadcast +// a previously revoked commitment transaction. +// +// The private key is derived as follows: +// +// revokePriv := (revokeBasePriv * sha256(revocationBase || commitPoint)) + +// (commitSecret * sha256(commitPoint || revocationBase)) mod N +// +// Where N is the order of the sub-group. +func deriveRevocationPrivKey(revokeBasePriv *btcec.PrivateKey, + commitSecret *btcec.PrivateKey) *btcec.PrivateKey { + + // r = sha256(revokeBasePub || commitPoint) + revokeTweakBytes := singleTweakBytes( + revokeBasePriv.PubKey(), commitSecret.PubKey(), + ) + revokeTweakScalar := new(btcec.ModNScalar) + revokeTweakScalar.SetByteSlice(revokeTweakBytes) + + // c = sha256(commitPoint || revokeBasePub) + commitTweakBytes := singleTweakBytes( + commitSecret.PubKey(), revokeBasePriv.PubKey(), + ) + commitTweakScalar := new(btcec.ModNScalar) + commitTweakScalar.SetByteSlice(commitTweakBytes) + + // Finally to derive the revocation secret key we'll perform the + // following operation: + // + // k = (revocationPriv * r) + (commitSecret * c) mod N + // + // This works since: + // P = (G*a)*b + (G*c)*d + // P = G*(a*b) + G*(c*d) + // P = G*(a*b + c*d) + revokeHalfPriv := revokeTweakScalar.Mul(&revokeBasePriv.Key) + commitHalfPriv := commitTweakScalar.Mul(&commitSecret.Key) + + revocationPriv := revokeHalfPriv.Add(commitHalfPriv) + + return &btcec.PrivateKey{Key: *revocationPriv} +} diff --git a/vault/paths.go b/vault/paths.go new file mode 100644 index 0000000..21a620d --- /dev/null +++ b/vault/paths.go @@ -0,0 +1,225 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package vault + +import ( + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/logical" +) + +const ( + // MaxAcctID is the number of accounts/key families to create on + // initialization. + MaxAcctID = 255 + + Bip0043purpose = 1017 + NodeKeyAcct = 6 +) + +var ( + // defaultPurposes is a list of non-LN(1017) purposes for which we + // should create a m/purpose'/0'/0' account as well as their default + // address types. + defaultPurposes = []struct { + purpose uint32 + addrType string + hdVersion [2][4]byte + }{ + { + purpose: 49, + addrType: "HYBRID_NESTED_WITNESS_PUBKEY_HASH", + hdVersion: [2][4]byte{ + [4]byte{0x04, 0x9d, 0x7c, 0xb2}, // ypub + [4]byte{0x04, 0x4a, 0x52, 0x62}, // upub + }, + }, + { + purpose: 84, + addrType: "WITNESS_PUBKEY_HASH", + hdVersion: [2][4]byte{ + [4]byte{0x04, 0xb2, 0x47, 0x46}, // zpub + [4]byte{0x04, 0x5f, 0x1c, 0xf6}, // vpub + }, + }, + { + purpose: 86, + addrType: "TAPROOT_PUBKEY", + hdVersion: [2][4]byte{ + [4]byte{0x04, 0x88, 0xb2, 0x1e}, // xpub + [4]byte{0x04, 0x35, 0x87, 0xcf}, // tpub + }, + }, + } +) + +type backend struct { + *framework.Backend +} + +func (b *backend) paths() []*framework.Path { + return []*framework.Path{ + &framework.Path{ + Pattern: "lnd-nodes/?", + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.listNodes, + logical.UpdateOperation: b.createNode, + logical.CreateOperation: b.createNode, + }, + HelpSynopsis: "Create and list LND nodes", + HelpDescription: ` + +GET - list all node pubkeys and coin types for HD derivations +POST - generate a new node seed and store it indexed by node pubkey + +`, + Fields: map[string]*framework.FieldSchema{ + "network": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Network, one of " + + "'mainnet', 'testnet', " + + "'simnet', 'signet', or " + + "'regtest'", + Default: 1, + }, + }, + }, + &framework.Path{ + Pattern: "lnd-nodes/accounts/?", + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.listAccounts, + }, + HelpSynopsis: "List accounts for import into LND " + + "watch-only node", + HelpDescription: ` + +GET - list all node accounts in JSON format suitable for import into watch- +only LND + +`, + Fields: map[string]*framework.FieldSchema{ + "node": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "node pubkey, must be " + + "66 hex characters", + Default: "", + }, + }, + }, + &framework.Path{ + Pattern: "lnd-nodes/ecdh/?", + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.ecdh, + logical.CreateOperation: b.ecdh, + }, + HelpSynopsis: "ECDH derived privkey with peer pubkey", + HelpDescription: ` + +POST - ECDH the privkey derived with the submitted path with the specified +peer pubkey + +`, + Fields: map[string]*framework.FieldSchema{ + "node": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "node pubkey, must be " + + "66 hex characters", + Default: "", + }, + "path": &framework.FieldSchema{ + Type: framework.TypeCommaIntSlice, + Description: "derivation path, with " + + "the first 3 elements " + + "being hardened", + Default: []int{}, + }, + "pubkey": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "optional: pubkey for " + + "which to do ECDH, checked " + + "against derived pubkey to " + + "ensure a match", + Default: "", + }, + "peer": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "pubkey for ECDH peer, " + + "must be 66 hex characters", + Default: "", + }, + }, + }, + &framework.Path{ + Pattern: "lnd-nodes/sign/?", + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.derivePubKey, + logical.UpdateOperation: b.deriveAndSign, + logical.CreateOperation: b.deriveAndSign, + }, + HelpSynopsis: "Derive pubkeys and sign with privkeys", + HelpDescription: ` + +GET - return the pubkey derived with the submitted path +POST - sign a digest with the method specified using the privkey derived with +the submitted path + +`, + Fields: map[string]*framework.FieldSchema{ + "node": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "node pubkey, must be " + + "66 hex characters", + Default: "", + }, + "path": &framework.FieldSchema{ + Type: framework.TypeCommaIntSlice, + Description: "derivation path, with " + + "the first 3 elements " + + "being hardened", + Default: []int{}, + }, + "digest": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "digest to sign, must " + + "be hex-encoded 32 bytes", + Default: "", + }, + "method": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "signing method: " + + "one of: ecdsa, " + + "ecdsa-compact, or schnorr", + Default: "", + }, + "pubkey": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "optional: pubkey for " + + "which to sign, checked " + + "against derived pubkey to " + + "ensure a match", + Default: "", + }, + "taptweak": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "optional: hex-encoded " + + "taproot tweak", + Default: "", + }, + "ln1tweak": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "optional: hex-encoded " + + "LN single commit tweak", + Default: "", + }, + "ln2tweak": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "optional: hex-encoded " + + "LN double revocation tweak", + Default: "", + }, + }, + }, + } +} diff --git a/walletkit_server.go b/walletkit_server.go new file mode 100644 index 0000000..a7f8472 --- /dev/null +++ b/walletkit_server.go @@ -0,0 +1,103 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package lndsigner + +import ( + "bytes" + "context" + "fmt" + + "github.com/bottlepay/lndsigner/keyring" + "github.com/bottlepay/lndsigner/proto" + "github.com/btcsuite/btcd/btcutil/psbt" + "gopkg.in/macaroon-bakery.v2/bakery" +) + +var ( + // macPermissions maps RPC calls to the permissions they require. + walletPermissions = map[string][]bakery.Op{ + "/proto.WalletKit/SignPsbt": {{ + Entity: "onchain", + Action: "write", + }}, + } +) + +// walletKit is a sub-RPC server that exposes a tool kit which allows clients +// to execute common wallet operations. This includes requesting new addresses, +// keys (for contracts!), and publishing transactions. +type walletKit struct { + // Required by the grpc-gateway/v2 library for forward compatibility. + proto.UnimplementedWalletKitServer + + server *rpcServer +} + +// A compile time check to ensure that walletKit fully implements the +// proto.WalletKitServer gRPC service. +var _ proto.WalletKitServer = (*walletKit)(nil) + +// SignPsbt expects a partial transaction with all inputs and outputs fully +// declared and tries to sign all unsigned inputs that have all required fields +// (UTXO information, BIP32 derivation information, witness or sig scripts) +// set. +// If no error is returned, the PSBT is ready to be given to the next signer or +// to be finalized if lnd was the last signer. +// +// NOTE: This RPC only signs inputs (and only those it can sign), it does not +// perform any other tasks (such as coin selection, UTXO locking or +// input/output/fee value validation, PSBT finalization). Any input that is +// incomplete will be skipped. +func (w *walletKit) SignPsbt(ctx context.Context, req *proto.SignPsbtRequest) ( + *proto.SignPsbtResponse, error) { + + packet, err := psbt.NewFromRawBytes( + bytes.NewReader(req.FundedPsbt), false, + ) + if err != nil { + signerLog.Debugf("Error parsing PSBT: %v, raw input: %x", err, + req.FundedPsbt) + return nil, fmt.Errorf("error parsing PSBT: %v", err) + } + + // Before we attempt to sign the packet, ensure that every input either + // has a witness UTXO, or a non witness UTXO. + for idx := range packet.UnsignedTx.TxIn { + in := packet.Inputs[idx] + + // Doesn't have either a witness or non witness UTXO so we need + // to exit here as otherwise signing will fail. + if in.WitnessUtxo == nil && in.NonWitnessUtxo == nil { + return nil, fmt.Errorf("input (index=%v) doesn't "+ + "specify any UTXO info", idx) + } + } + + // Let the wallet do the heavy lifting. This will sign all inputs that + // we have the UTXO for. If some inputs can't be signed and don't have + // witness data attached, they will just be skipped. + keyRing := ctx.Value(keyRingKey).(*keyring.KeyRing) + if keyRing == nil { + return nil, fmt.Errorf("no node/coin from macaroon") + } + + signedInputs, err := keyRing.SignPsbt(packet) + if err != nil { + return nil, fmt.Errorf("error signing PSBT: %v", err) + } + + // Serialize the signed PSBT in both the packet and wire format. + var signedPsbtBytes bytes.Buffer + err = packet.Serialize(&signedPsbtBytes) + if err != nil { + return nil, fmt.Errorf("error serializing PSBT: %v", err) + } + + return &proto.SignPsbtResponse{ + SignedPsbt: signedPsbtBytes.Bytes(), + SignedInputs: signedInputs, + }, nil +} From f2bc48aa8b266622aab03fc9da5d3597a47d0cd7 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Wed, 16 Nov 2022 08:51:58 -0800 Subject: [PATCH 02/42] fix cmd dir and gitignore --- .gitignore | 3 --- cmd/lndsignerd/main.go | 35 ++++++++++++++++++++++++++++++ cmd/vault-plugin-lndsigner/main.go | 31 ++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 cmd/lndsignerd/main.go create mode 100644 cmd/vault-plugin-lndsigner/main.go diff --git a/.gitignore b/.gitignore index 4df5f8d..66fd13c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,6 @@ *.so *.dylib -vault-plugin-lndsigner -lndsignerd - # Test binary, built with `go test -c` *.test diff --git a/cmd/lndsignerd/main.go b/cmd/lndsignerd/main.go new file mode 100644 index 0000000..db5a7c6 --- /dev/null +++ b/cmd/lndsignerd/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "os" + + "github.com/bottlepay/lndsigner" + "github.com/jessevdk/go-flags" +) + +func main() { + // Load the configuration, and parse any command line options. This + // function will also set up logging properly. + loadedConfig, err := lndsigner.LoadConfig() + if err != nil { + if e, ok := err.(*flags.Error); !ok || e.Type != flags.ErrHelp { + // Print error if not due to help request. + err = fmt.Errorf("failed to load config: %w", err) + _, _ = fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + // Help was requested, exit normally. + os.Exit(0) + } + + // Call the "real" main in a nested manner so the defers will properly + // be executed in the case of a graceful shutdown. + if err = lndsigner.Main( + loadedConfig, lndsigner.ListenerCfg{}, + ); err != nil { + _, _ = fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/cmd/vault-plugin-lndsigner/main.go b/cmd/vault-plugin-lndsigner/main.go new file mode 100644 index 0000000..feda564 --- /dev/null +++ b/cmd/vault-plugin-lndsigner/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "os" + + "github.com/bottlepay/lndsigner/vault" + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/sdk/plugin" +) + +func main() { + apiClientMeta := &api.PluginAPIClientMeta{} + flags := apiClientMeta.FlagSet() + flags.Parse(os.Args[1:]) + + tlsConfig := apiClientMeta.GetTLSConfig() + tlsProviderFunc := api.VaultPluginTLSProvider(tlsConfig) + + logger := hclog.New(&hclog.LoggerOptions{}) + + err := plugin.Serve(&plugin.ServeOpts{ + BackendFactoryFunc: vault.Factory, + TLSProviderFunc: tlsProviderFunc, + Logger: logger, + }) + if err != nil { + logger.Error("plugin shutting down", "error", err) + os.Exit(1) + } +} From f91237795e9749801b87be5859b96d4d9eb51668 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Tue, 22 Nov 2022 14:46:48 -0800 Subject: [PATCH 03/42] multi: change lndsignerd from multi-tenant to sidecar --- README.md | 56 ++++++---- config.go | 193 ++++++++--------------------------- go.mod | 10 +- go.sum | 43 ++------ keyring/keyring.go | 26 ++--- keyring/log.go | 19 +--- lndsigner.go | 231 ++++-------------------------------------- log.go | 36 ++----- macaroons.go | 183 --------------------------------- proto/signer.pb.go | 87 +++++++--------- proto/signer.proto | 7 -- proto/walletkit.proto | 2 +- rpcserver.go | 75 +++----------- signer_server.go | 60 +++-------- vault/backend.go | 102 +++++++++---------- vault/keys.go | 3 +- walletkit_server.go | 23 +---- 17 files changed, 257 insertions(+), 899 deletions(-) delete mode 100644 macaroons.go diff --git a/README.md b/README.md index 04e71f5..655359c 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,11 @@ `lndsigner` is a [remote signer](https://github.com/lightningnetwork/lnd/blob/master/docs/remote-signing.md) for [lnd](https://github.com/lightningnetwork/lnd). Currently, it can do the following: - [x] store seeds for multiple nodes in [Hashicorp Vault](https://github.com/hashicorp/vault/) - [x] perform derivation and signing operations in a Vault plugin -- [x] import macaroon root key from environment variable -- [x] export account list and macaroon for watch-only lnd instances on startup +- [x] export account list for watch-only lnd instance on startup - [x] sign messages for network announcements - [x] derive shared keys for peer connections - [x] sign PSBTs for on-chain transactions, channel openings/closes, HTLC updates, etc. -- [x] verify macaroons on grpc request - [ ] perform musig2 ops -- [ ] add and verify macaroon caveats (like expiration or ip address restriction) -- [ ] export account list and macaroon for watch-only lnd instances when node is created in vault or some other way without restarting the signer - [ ] track on-chain wallet state and enforce policy for on-chain transactions - [ ] track channel state and enforce policy for channel updates - [ ] allow preauthorizations for on-chain transactions, channel opens/closes, and channel updates @@ -62,28 +58,34 @@ You can also list the nodes as follows: $ VAULT_ADDR=http://127.0.0.1:8200 VAULT_TOKEN=root vault read lndsigner/lnd-nodes Key Value --- ----- -03dc60dce282bb96abb4328c3e19640aa4f87defc400458322b80f0b73c2b14263 1 +03dc60dce282bb96abb4328c3e19640aa4f87defc400458322b80f0b73c2b14263 regtest ``` -The value is the HDCoinType used for the wallet, derived from the network specified above. Note that the plugin and signer support multiple nodes, so you can add more nodes by writing as above. +The value is the network specified above. Note that the Vault plugin is multi-tenant (supports multiple nodes), so you can add more nodes by writing as above. -Create a directory `~/.lndsigner` with a `signer.conf` similar to: +Create a directory `~/.lndsigner` (Linux) with a `signer.conf` similar to: ``` rpclisten=tcp://127.0.0.1:10021 -regtest=true +network=regtest +nodepubkey=*pubkey* ``` -Run the signer binary as follows, with the macaroon root key in `SIGNER_MAC_ROOT_KEY`: +Use the pubkey from the node you created above. Note that on other platforms, the lndsigner directory you need to create may be different, such as: + +- `C:\Users\\AppData\Local\Lndsigner` on Windows +- `~/Library/Application Support/Lndsigner` on MacOS + +The rest of this README assumes you're working on Linux. Additional documentation for other platforms welcome. + +You'll need to provide a `tls.key` and `tls.cert` for the daemon to accept TLS connections from `lnd`. For testing purposes, you can grab some that are auto-generated by a regtest instance of `lnd`. For deploy, you'll want your infrastructure to create these. + +Run the signer binary as follows: ``` -~/.lndsigner$ VAULT_ADDR=http://127.0.0.1:8200 VAULT_TOKEN=root \ - SIGNER_MAC_ROOT_KEY=6666666666555555555544444444443333333333222222222211111111114321 \ - lndsignerd --outputmacaroon=signer.custom.macaroon --outputaccounts=accounts.json --debuglevel=trace +~/.lndsigner$ VAULT_ADDR=http://127.0.0.1:8200 VAULT_TOKEN=root lndsignerd ``` -You'll notice some new files created, such as `tls.key` and `tls.cert` for the signer's GRPC interface. You'll also notice a file called `accounts.json.*pubkey*` and another called `signer.custom.macaroon.*pubkey*`, both of which you'll need to pass to `lnd` in future steps. If you created multiple nodes, you'll notice an `accounts.json` and a `signer.custom.macaroon` for each one, with the node's pubkey appended to the filename. - Ensure you have a `bitcoind` instance running locally on regtest. Then, create a directory `~/.lnd-watchonly` with a `lnd.conf` similar to: ``` @@ -96,10 +98,26 @@ bitcoin.node=bitcoind remotesigner.enable=true remotesigner.rpchost=127.0.0.1:10021 remotesigner.tlscertpath=/home/*user*/.lndsigner/tls.cert -remotesigner.macaroonpath=/home/*user*/.lndsigner/signer.custom.macaroon.*pubkey* +remotesigner.macaroonpath=any.macaroon +``` + +Note that `lnd` checks that the macaroon file deserializes correctly but lndsigner ignores the macaroon. + +Next, get the account list for the node: + +``` +~/.lnd-watchonly$ VAULT_ADDR=http://127.0.0.1:8200 VAULT_TOKEN=root vault read lndsigner/lnd-nodes/accounts node=*pubkey* > accounts.json +``` + +You'll get an `accounts.json` file that starts like: + +``` +Key Value +--- ----- +acctList {"accounts":[{"name":"default","address_type":"HYBRID_NESTED_WITNESS_PUBKEY_HASH","extended_public_key":"... ``` -Note that `lnd` will need the macaroon file to authenticate itself to the signer. +Remove everything, including `acctList`, to the first `{` from the top of the file to get the actual JSON you'll need to import into `lnd` when you create the watch-only instance below. Your JSON file should start with `{"accounts":[{`. You can run it through `jq` to ensure the syntax is correct. Now, run `lnd` in watch-only mode: @@ -110,7 +128,7 @@ Now, run `lnd` in watch-only mode: Create the watch-only wallet using the accounts exported by the signer: ``` -~$ lncli createwatchonly .lndsigner/accounts.json.*pubkey* +~$ lncli createwatchonly .lndsigner/accounts.json ``` -Now you can use your node as usual. Note that MuSig2 isn't supported yet. If you created multiple nodes in the vault, you can create a separate directory for each watch-only node and start it as above. +Now you can use your node as usual. Note that MuSig2 isn't supported yet. If you created multiple nodes in the vault, you can create a separate directory for each signer instance and each watch-only node and start it as above. diff --git a/config.go b/config.go index 84b6221..2f758ba 100644 --- a/config.go +++ b/config.go @@ -14,8 +14,8 @@ import ( "path/filepath" "strconv" "strings" - "time" + "github.com/bottlepay/lndsigner/vault" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" flags "github.com/jessevdk/go-flags" @@ -25,50 +25,28 @@ const ( defaultConfigFilename = "signer.conf" defaultTLSCertFilename = "tls.cert" defaultTLSKeyFilename = "tls.key" - defaultLogLevel = "info" - defaultLogDirname = "logs" - defaultLogFilename = "signer.log" defaultRPCPort = 10009 defaultRPCHost = "localhost" - - defaultMaxLogFiles = 3 - defaultMaxLogFileSize = 10 - - // DefaultAutogenValidity is the default validity of a self-signed - // certificate. The value corresponds to 14 months - // (14 months * 30 days * 24 hours). - defaultTLSCertDuration = 14 * 30 * 24 * time.Hour - - // Set defaults for a health check which ensures that the TLS certificate - // is not expired. Although this check is off by default (not all setups - // require it), we still set the other default values so that the health - // check can be easily enabled with sane defaults. - defaultTLSInterval = time.Minute - defaultTLSTimeout = time.Second * 5 - defaultTLSBackoff = time.Minute - defaultTLSAttempts = 0 ) var ( - // DefaultSignerDir is the default directory where lnd tries to find its - // configuration file and store its data. This is a directory in the - // user's application data, for example: + // DefaultSignerDir is the default directory where lndsignerd tries to + // find its configuration file and store its data. This is a directory + // in the user's application data, for example: // C:\Users\\AppData\Local\Lndsigner on Windows // ~/.lndsigner on Linux // ~/Library/Application Support/Lndsigner on MacOS DefaultSignerDir = btcutil.AppDataDir("lndsigner", false) - // DefaultConfigFile is the default full path of lnd's configuration - // file. + // DefaultConfigFile is the default full path of lndsignerd's + // configuration file. DefaultConfigFile = filepath.Join(DefaultSignerDir, defaultConfigFilename) - defaultLogDir = filepath.Join(DefaultSignerDir, defaultLogDirname) - defaultTLSCertPath = filepath.Join(DefaultSignerDir, defaultTLSCertFilename) defaultTLSKeyPath = filepath.Join(DefaultSignerDir, defaultTLSKeyFilename) ) -// Config defines the configuration options for lnd. +// Config defines the configuration options for lndsignerd. // // See LoadConfig for further details regarding the configuration // loading+parsing process. @@ -76,20 +54,8 @@ type Config struct { SignerDir string `long:"signerdir" description:"The base directory that contains signer's data, logs, configuration file, etc."` ConfigFile string `short:"C" long:"configfile" description:"Path to configuration file"` - TLSCertPath string `long:"tlscertpath" description:"Path to write the TLS certificate for lnd's RPC services"` - TLSKeyPath string `long:"tlskeypath" description:"Path to write the TLS private key for lnd's RPC services"` - TLSExtraIPs []string `long:"tlsextraip" description:"Adds an extra ip to the generated certificate"` - TLSExtraDomains []string `long:"tlsextradomain" description:"Adds an extra domain to the generated certificate"` - TLSAutoRefresh bool `long:"tlsautorefresh" description:"Re-generate TLS certificate and key if the IPs or domains are changed"` - TLSDisableAutofill bool `long:"tlsdisableautofill" description:"Do not include the interface IPs or the system hostname in TLS certificate, use first --tlsextradomain as Common Name instead, if set"` - TLSCertDuration time.Duration `long:"tlscertduration" description:"The duration for which the auto-generated TLS certificate will be valid for"` - - OutputMacaroon string `long:"outputmacaroon" description:"Path to write a signer macaroon for the watch-only node"` - OutputAccounts string `long:"outputaccounts" description:"Path to write a JSON file with xpubs for the watch-only node"` - - LogDir string `long:"logdir" description:"Directory to log output."` - MaxLogFiles int `long:"maxlogfiles" description:"Maximum logfiles to keep (0 for no rotation)"` - MaxLogFileSize int `long:"maxlogfilesize" description:"Maximum logfile size in MB"` + TLSCertPath string `long:"tlscertpath" description:"Path to write the TLS certificate for lndsignerd's RPC services"` + TLSKeyPath string `long:"tlskeypath" description:"Path to write the TLS private key for lndsignerd's RPC services"` // We'll parse these 'raw' string arguments into real net.Addrs in the // loadConfig function. We need to expose the 'raw' strings so the @@ -98,37 +64,23 @@ type Config struct { RawRPCListeners []string `long:"rpclisten" description:"Add an interface/port/socket to listen for RPC connections"` RPCListeners []net.Addr - DebugLevel string `short:"d" long:"debuglevel" description:"Logging level for all subsystems {trace, debug, info, warn, error, critical}"` - - // MainNet bool `long:"mainnet" description:"NOT RECOMMENDED: Use the main network"` - TestNet3 bool `long:"testnet" description:"Use the test network"` - SimNet bool `long:"simnet" description:"Use the simulation test network"` - RegTest bool `long:"regtest" description:"Use the regression test network"` - SigNet bool `long:"signet" description:"Use the signet test network"` + Network string `long:"network" description:"The network for which the node was created in the vault. One of: 'testnet', 'simnet', 'regtest', 'signet'"` // ActiveNetParams contains parameters of the target chain. ActiveNetParams chaincfg.Params - // seed contains the 32-byte wallet seed. - seed [32]byte - - // macRootKey contains the 32-byte macaroon root key. - macRootKey [32]byte + // Node contains the node ID as a 66-character hex string. + NodePubKey string `long:"nodepubkey" description:"Node pubkey hex"` } // DefaultConfig returns all default values for the Config struct. func DefaultConfig() Config { return Config{ - SignerDir: DefaultSignerDir, - ConfigFile: DefaultConfigFile, - DebugLevel: defaultLogLevel, - TLSCertPath: defaultTLSCertPath, - TLSKeyPath: defaultTLSKeyPath, - TLSCertDuration: defaultTLSCertDuration, - LogDir: defaultLogDir, - MaxLogFiles: defaultMaxLogFiles, - MaxLogFileSize: defaultMaxLogFileSize, - ActiveNetParams: chaincfg.RegressionNetParams, + SignerDir: DefaultSignerDir, + ConfigFile: DefaultConfigFile, + TLSCertPath: defaultTLSCertPath, + TLSKeyPath: defaultTLSKeyPath, + Network: "regtest", } } @@ -155,13 +107,14 @@ func LoadConfig() (*Config, error) { // If the config file path has not been modified by the user, then we'll // use the default config file path. However, if the user has modified - // their lnddir, then we should assume they intend to use the config + // their signerdir, then we should assume they intend to use the config // file within it. configFileDir := CleanAndExpandPath(preCfg.SignerDir) configFilePath := CleanAndExpandPath(preCfg.ConfigFile) switch { - // User specified --lnddir but no --configfile. Update the config file - // path to the lnd config directory, but don't require it to exist. + // User specified --signerdir but no --configfile. Update the config + // file path to the lndsignerd config directory, but don't require it + // to exist. case configFileDir != DefaultSignerDir && configFilePath == DefaultConfigFile: @@ -255,13 +208,13 @@ func (u *usageError) Error() string { func ValidateConfig(cfg Config, fileParser, flagParser *flags.Parser) ( *Config, error) { - // If the provided lnd directory is not the default, we'll modify the - // path to all of the files and directories that will live within it. + // If the provided lndsignerd directory is not the default, we'll + // modify the path to all of the files and directories that will live + // within it. signerDir := CleanAndExpandPath(cfg.SignerDir) if signerDir != DefaultSignerDir { cfg.TLSCertPath = filepath.Join(signerDir, defaultTLSCertFilename) cfg.TLSKeyPath = filepath.Join(signerDir, defaultTLSKeyFilename) - cfg.LogDir = filepath.Join(signerDir, defaultLogDirname) } funcName := "ValidateConfig" @@ -282,7 +235,7 @@ func ValidateConfig(cfg Config, fileParser, flagParser *flags.Parser) ( } } - str := "Failed to create lnd directory '%s': %v" + str := "Failed to create lndsigner directory '%s': %v" return mkErr(str, dir, err) } @@ -294,54 +247,19 @@ func ValidateConfig(cfg Config, fileParser, flagParser *flags.Parser) ( // to use them later on. cfg.TLSCertPath = CleanAndExpandPath(cfg.TLSCertPath) cfg.TLSKeyPath = CleanAndExpandPath(cfg.TLSKeyPath) - cfg.LogDir = CleanAndExpandPath(cfg.LogDir) - - // Multiple networks can't be selected simultaneously. Count - // number of network flags passed; assign active network params - // while we're at it. - numNets := 0 - /*if cfg.MainNet { - numNets++ - cfg.ActiveNetParams = chaincfg.MainNetParams - }*/ - if cfg.TestNet3 { - numNets++ - cfg.ActiveNetParams = chaincfg.TestNet3Params - } - if cfg.RegTest { - numNets++ - cfg.ActiveNetParams = chaincfg.RegressionNetParams - } - if cfg.SimNet { - numNets++ - cfg.ActiveNetParams = chaincfg.SimNetParams - } - if cfg.SigNet { - numNets++ - cfg.ActiveNetParams = chaincfg.SigNetParams - } - if numNets > 1 { - str := "The mainnet, testnet, regtest, and simnet " + - "params can't be used together -- choose one " + - "of the four" - return nil, mkErr(str) - } - // The target network must be provided, otherwise, we won't - // know how to initialize the daemon. - if numNets == 0 { - str := "either --bitcoin.mainnet, or bitcoin.testnet," + - "bitcoin.simnet, or bitcoin.regtest " + - "must be specified" - return nil, mkErr(str) + params, err := vault.GetNet(cfg.Network) + if err != nil { + return nil, err } + cfg.ActiveNetParams = *params - // Create the lnd directory and all other sub-directories if they don't - // already exist. This makes sure that directory trees are also created - // for files that point to outside the lnddir. + // Create the lndsignerd directory and all other sub-directories if + // they don't already exist. This makes sure that directory trees are + // also created for files that point to outside the signerdir. dirs := []string{ signerDir, filepath.Dir(cfg.TLSCertPath), - filepath.Dir(cfg.TLSKeyPath), filepath.Dir(cfg.OutputMacaroon), + filepath.Dir(cfg.TLSKeyPath), } for _, dir := range dirs { if err := makeDirectory(dir); err != nil { @@ -349,11 +267,6 @@ func ValidateConfig(cfg Config, fileParser, flagParser *flags.Parser) ( } } - err := setLogLevel(cfg.DebugLevel) - if err != nil { - return nil, mkErr("error setting debug level: %v", err) - } - // At least one RPCListener is required. So listen on localhost per // default. if len(cfg.RawRPCListeners) == 0 { @@ -364,23 +277,11 @@ func ValidateConfig(cfg Config, fileParser, flagParser *flags.Parser) ( // Add default port to all RPC listener addresses if needed and remove // duplicate addresses. cfg.RPCListeners, err = NormalizeAddresses( - cfg.RawRPCListeners, strconv.Itoa(defaultRPCPort), - net.ResolveTCPAddr, - ) + cfg.RawRPCListeners, strconv.Itoa(defaultRPCPort)) if err != nil { return nil, mkErr("error normalizing RPC listen addrs: %v", err) } - cfg.OutputAccounts = CleanAndExpandPath(cfg.OutputAccounts) - - // Get the macaroon root key from the environment. - cfg.macRootKey, err = get32BytesFromEnv("SIGNER_MAC_ROOT_KEY") - if err != nil { - return nil, err - } - - cfg.OutputMacaroon = CleanAndExpandPath(cfg.OutputMacaroon) - // All good, return the sanitized result. return &cfg, nil } @@ -435,22 +336,16 @@ func get32BytesFromEnv(envKey string) ([32]byte, error) { return key, nil } -// TCPResolver is a function signature that resolves an address on a given -// network. -type TCPResolver = func(network, addr string) (*net.TCPAddr, error) - // NormalizeAddresses returns a new slice with all the passed addresses // normalized with the given default port and all duplicates removed. -func NormalizeAddresses(addrs []string, defaultPort string, - tcpResolver TCPResolver) ([]net.Addr, error) { +func NormalizeAddresses(addrs []string, defaultPort string) ([]net.Addr, + error) { result := make([]net.Addr, 0, len(addrs)) seen := map[string]struct{}{} for _, addr := range addrs { - parsedAddr, err := ParseAddressString( - addr, defaultPort, tcpResolver, - ) + parsedAddr, err := ParseAddressString(addr, defaultPort) if err != nil { return nil, fmt.Errorf("parse address %s failed: %w", addr, err) @@ -500,11 +395,9 @@ func verifyPort(address string, defaultPort string) string { } // ParseAddressString converts an address in string format to a net.Addr that is -// compatible with lnd. UDP is not supported because lnd needs reliable -// connections. We accept a custom function to resolve any TCP addresses so -// that caller is able control exactly how resolution is performed. -func ParseAddressString(strAddress string, defaultPort string, - tcpResolver TCPResolver) (net.Addr, error) { +// compatible with lndsignerd. +func ParseAddressString(strAddress string, defaultPort string) (net.Addr, + error) { var parsedNetwork, parsedAddr string @@ -521,13 +414,13 @@ func ParseAddressString(strAddress string, defaultPort string, } // Only TCP and Unix socket addresses are valid. We can't use IP or - // UDP only connections for anything we do in lnd. + // UDP only connections for anything we do here. switch parsedNetwork { case "unix", "unixpacket": return net.ResolveUnixAddr(parsedNetwork, parsedAddr) - case "tcp", "tcp4", "tcp6": - return tcpResolver( + case "", "tcp", "tcp4", "tcp6": + return net.ResolveTCPAddr( parsedNetwork, verifyPort(parsedAddr, defaultPort), ) diff --git a/go.mod b/go.mod index 83b2275..b2c63b0 100644 --- a/go.mod +++ b/go.mod @@ -6,22 +6,20 @@ require ( github.com/btcsuite/btcd/btcutil v1.1.2 github.com/btcsuite/btcd/btcutil/psbt v1.1.5 github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 - github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f github.com/hashicorp/go-hclog v1.3.1 github.com/hashicorp/vault/api v1.8.0 github.com/hashicorp/vault/sdk v0.6.0 github.com/jessevdk/go-flags v1.4.0 - github.com/lightningnetwork/lnd/cert v1.1.1 github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 + go.uber.org/zap v1.23.0 google.golang.org/grpc v1.47.0 google.golang.org/protobuf v1.28.0 - gopkg.in/macaroon-bakery.v2 v2.2.0 - gopkg.in/macaroon.v2 v2.1.0 ) require ( github.com/armon/go-metrics v0.3.9 // indirect github.com/armon/go-radix v1.0.0 // indirect + github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect github.com/cenkalti/backoff/v3 v3.0.0 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect @@ -57,19 +55,17 @@ require ( github.com/oklog/run v1.0.0 // indirect github.com/pierrec/lz4 v2.5.2+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/rogpeppe/fastuuid v1.2.0 // indirect github.com/rogpeppe/go-internal v1.8.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.6.0 // indirect golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0 // indirect - gopkg.in/errgo.v1 v1.0.1 // indirect gopkg.in/square/go-jose.v2 v2.5.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect ) // This replace is for https://github.com/advisories/GHSA-w73w-5m7g-f7qc diff --git a/go.sum b/go.sum index 367e0e6..328f75c 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,7 @@ github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -84,10 +85,6 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= -github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= -github.com/frankban/quicktest v1.1.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= -github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= -github.com/frankban/quicktest v1.7.3/go.mod h1:V1d2J5pfxYH6EjBAgSK7YNXcXlTWxUHdE1sVDXkjnig= github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= @@ -119,7 +116,6 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -188,11 +184,6 @@ github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/juju/mgotest v1.0.1/go.mod h1:vTaDufYul+Ps8D7bgseHjq87X8eu0ivlKLp9mVc/Bfc= -github.com/juju/postgrestest v1.1.0/go.mod h1:/n17Y2T6iFozzXwSCO0JYJ5gSiz2caEtSwAjh/uLXDM= -github.com/juju/qthttptest v0.0.1/go.mod h1://LCf/Ls22/rPw2u1yWukUJvYtfPY4nYpWUl2uZhryo= -github.com/juju/schema v1.0.0/go.mod h1:Y+ThzXpUJ0E7NYYocAbuvJ7vTivXfrof/IfRPq/0abI= -github.com/juju/webbrowser v0.0.0-20160309143629-54b8c57083b4/go.mod h1:G6PCelgkM6cuvyD10iYJsjLBsSadVXtJ+nBxFAxE2BU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -200,17 +191,12 @@ github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lightningnetwork/lnd/cert v1.1.1 h1:Nsav0RlIDRbOnzz2Yu69SQlK939IKya3Q2S0mDviIN8= -github.com/lightningnetwork/lnd/cert v1.1.1/go.mod h1:1P46svkkd73oSoeI4zjkVKgZNwGq8bkGuPR8z+5vQUs= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= @@ -273,8 +259,6 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= @@ -293,8 +277,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 h1:tcJ6OjwOMvExLlzrAVZute09ocAGa7KqOON60++Gz4E= @@ -302,13 +286,17 @@ github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S0 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= +go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38= @@ -319,7 +307,6 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20150829230318-ea47fc708ee3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -353,7 +340,6 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -385,7 +371,6 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181008205924-a2b3f7f249e9/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -433,18 +418,8 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v1 v1.0.0/go.mod h1:CxwszS/Xz1C49Ucd2i6Zil5UToP1EmyrFhKaMVbg1mk= -gopkg.in/errgo.v1 v1.0.1 h1:oQFRXzZ7CkBGdm1XZm/EbQYaYNNEElNBOd09M6cqNso= -gopkg.in/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/httprequest.v1 v1.2.0/go.mod h1:T61ZUaJLpMnzvoJDO03ZD8yRXD4nZzBeDoW5e9sffjg= -gopkg.in/juju/environschema.v1 v1.0.0/go.mod h1:WTgU3KXKCVoO9bMmG/4KHzoaRvLeoxfjArpgd1MGWFA= -gopkg.in/macaroon-bakery.v2 v2.2.0 h1:tgib3W6Nz8GhYfF83vp0FEZmncj6UE1ubIG7a09flkc= -gopkg.in/macaroon-bakery.v2 v2.2.0/go.mod h1:XyHjEinGUBsCK60Qv+bBejOQD/WklvntpSVGja9utaU= -gopkg.in/macaroon.v2 v2.1.0 h1:HZcsjBCzq9t0eBPMKqTN/uSN6JOm78ZJ2INbqcBQOUI= -gopkg.in/macaroon.v2 v2.1.0/go.mod h1:OUb+TQP/OP0WOerC2Jp/3CwhIKyIa9kQjuc7H24e6/o= -gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= @@ -454,8 +429,6 @@ gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/keyring/keyring.go b/keyring/keyring.go index 9af32d6..840b680 100644 --- a/keyring/keyring.go +++ b/keyring/keyring.go @@ -84,11 +84,11 @@ type KeyDescriptor struct { type KeyRing struct { client *api.Logical node string - coin int + coin uint32 } // NewKeyRing returns a vault-backed key ring. -func NewKeyRing(client *api.Logical, node string, coin int) *KeyRing { +func NewKeyRing(client *api.Logical, node string, coin uint32) *KeyRing { return &KeyRing{ client: client, node: node, @@ -125,7 +125,7 @@ func (k *KeyRing) ECDH(keyDesc KeyDescriptor, pub *btcec.PublicKey) ([32]byte, ) } - log.Tracef("Sending data %+v for signing request", reqData) + log.Debugf("Sending data %+v for shared key request", reqData) sharedKeyResp, err := k.client.Write( "lndsigner/lnd-nodes/ecdh", @@ -135,7 +135,7 @@ func (k *KeyRing) ECDH(keyDesc KeyDescriptor, pub *btcec.PublicKey) ([32]byte, return [32]byte{}, err } - log.Tracef("Got data %+v in signing response", sharedKeyResp.Data) + log.Debugf("Got data %+v in shared key response", sharedKeyResp.Data) sharedKeyHex, ok := sharedKeyResp.Data["sharedkey"].(string) if !ok { @@ -187,7 +187,7 @@ func (k *KeyRing) SignMessage(keyLoc KeyLocator, msg []byte, doubleHash bool, reqData["method"] = "ecdsa-compact" } - log.Tracef("Sending data %+v for signing request", reqData) + log.Debugf("Sending data %+v for signing request", reqData) signResp, err := k.client.Write( "lndsigner/lnd-nodes/sign", @@ -197,7 +197,7 @@ func (k *KeyRing) SignMessage(keyLoc KeyLocator, msg []byte, doubleHash bool, return nil, err } - log.Tracef("Got data %+v in signing response", signResp.Data) + log.Debugf("Got data %+v in signing response", signResp.Data) signatureHex, ok := signResp.Data["signature"].(string) if !ok { @@ -237,7 +237,7 @@ func (k *KeyRing) SignMessageSchnorr(keyLoc KeyLocator, msg []byte, reqData["taptweak"] = hex.EncodeToString(taprootTweak) } - log.Tracef("Sending data %+v for signing request", reqData) + log.Debugf("Sending data %+v for signing request", reqData) signResp, err := k.client.Write( "lndsigner/lnd-nodes/sign", @@ -247,7 +247,7 @@ func (k *KeyRing) SignMessageSchnorr(keyLoc KeyLocator, msg []byte, return nil, err } - log.Tracef("Got data %+v in signing response", signResp.Data) + log.Debugf("Got data %+v in signing response", signResp.Data) signatureHex, ok := signResp.Data["signature"].(string) if !ok { @@ -447,7 +447,7 @@ func (k *KeyRing) signSegWitV0(in *psbt.PInput, tx *wire.MsgTx, idx, err) } - log.Tracef("Got input %+v for signing with unknowns %+v", in, + log.Debugf("Got input %+v for signing with unknowns %+v", in, in.Unknowns) reqData := map[string]interface{}{ @@ -459,7 +459,7 @@ func (k *KeyRing) signSegWitV0(in *psbt.PInput, tx *wire.MsgTx, getTweakParams(in.Unknowns, reqData) - log.Tracef("Sending data %+v for signing request", reqData) + log.Debugf("Sending data %+v for signing request", reqData) signResp, err := k.client.Write( "lndsigner/lnd-nodes/sign", @@ -469,7 +469,7 @@ func (k *KeyRing) signSegWitV0(in *psbt.PInput, tx *wire.MsgTx, return err } - log.Tracef("Got data %+v in signing response", signResp.Data) + log.Debugf("Got data %+v in signing response", signResp.Data) signatureHex, ok := signResp.Data["signature"].(string) if !ok { @@ -530,7 +530,7 @@ func (k *KeyRing) signSegWitV1KeySpend(in *psbt.PInput, tx *wire.MsgTx, getTweakParams(in.Unknowns, reqData) - log.Tracef("Sending data %+v for signing request", reqData) + log.Debugf("Sending data %+v for signing request", reqData) signResp, err := k.client.Write( "lndsigner/lnd-nodes/sign", @@ -585,7 +585,7 @@ func (k *KeyRing) signSegWitV1ScriptSpend(in *psbt.PInput, tx *wire.MsgTx, getTweakParams(in.Unknowns, reqData) - log.Tracef("Sending data %+v for signing request", reqData) + log.Debugf("Sending data %+v for signing request", reqData) signResp, err := k.client.Write( "lndsigner/lnd-nodes/sign", diff --git a/keyring/log.go b/keyring/log.go index b7aa7b4..931e554 100644 --- a/keyring/log.go +++ b/keyring/log.go @@ -5,25 +5,16 @@ package keyring -import "github.com/btcsuite/btclog" +import ( + "go.uber.org/zap" +) // log is a logger that is initialized with no output filters. This // means the package will not perform any logging by default until the caller // requests it. -var log btclog.Logger - -// The default amount of logging is none. -func init() { - DisableLog() -} - -// DisableLog disables all library log output. Logging output is disabled -// by default until UseLogger is called. -func DisableLog() { - log = btclog.Disabled -} +var log *zap.SugaredLogger = zap.NewNop().Sugar() // UseLogger uses a specified Logger to output package logging info. -func UseLogger(logger btclog.Logger) { +func UseLogger(logger *zap.SugaredLogger) { log = logger } diff --git a/lndsigner.go b/lndsigner.go index 447cdc6..7ccd5e0 100644 --- a/lndsigner.go +++ b/lndsigner.go @@ -7,37 +7,18 @@ package lndsigner import ( "context" - "encoding/json" + "crypto/tls" + "crypto/x509" "fmt" "net" "os" "os/signal" "sync" "syscall" - "time" "github.com/hashicorp/vault/api" - "github.com/lightningnetwork/lnd/cert" "google.golang.org/grpc" "google.golang.org/grpc/credentials" - "gopkg.in/macaroon-bakery.v2/bakery" - "gopkg.in/macaroon-bakery.v2/bakery/checkers" -) - -const ( - // outputFilePermissions is the file permission that is used for - // creating the signer macaroon file and the accounts list file. - // - // Why 640 is safe: - // Assuming a reasonably secure Linux system, it will have a - // separate group for each user. E.g. a new user lnd gets assigned group - // lnd which nothing else belongs to. A system that does not do this is - // inherently broken already. - // - // Since there is no other user in the group, no other user can read - // admin macaroon unless the administrator explicitly allowed it. Thus - // there's no harm allowing group read. - outputFilePermissions = 0640 ) // ListenerWithSignal is a net.Listener that has an additional Ready channel @@ -47,11 +28,6 @@ type ListenerWithSignal struct { // Ready will be closed by the server listening on Listener. Ready chan struct{} - - // MacChan is an optional way to pass the admin macaroon to the program - // that started lnd. The channel should be buffered to avoid lnd being - // blocked on sending to the channel. - MacChan chan []byte } // ListenerCfg is a wrapper around custom listeners that can be passed to lnd @@ -74,25 +50,7 @@ func Main(cfg *Config, lisCfg ListenerCfg) error { return fmt.Errorf(format, args...) } - var network string - switch { - /*case cfg.MainNet: - network = "mainnet" - */ - case cfg.TestNet3: - network = "testnet" - - case cfg.SimNet: - network = "simnet" - - case cfg.RegTest: - network = "regtest" - - case cfg.SigNet: - network = "signet" - } - - signerLog.Infof("Active chain: %v (network=%v)", "bitcoin", network) + signerLog.Infof("Active Bitcoin network: %v)", cfg.ActiveNetParams.Name) ctx := context.Background() ctx, cancel := context.WithCancel(ctx) @@ -106,105 +64,6 @@ func Main(cfg *Config, lisCfg ListenerCfg) error { signerClient := vaultClient.Logical() - nodeListResp, err := signerClient.Read("lndsigner/lnd-nodes") - if err != nil { - return mkErr("error getting list of lnd nodes: %v", err) - } - - // If we're asked to output a watch-only account list, do it here. - if cfg.OutputAccounts != "" { - for node := range nodeListResp.Data { - listAcctsResp, err := signerClient.ReadWithData( - "lndsigner/lnd-nodes/accounts", - map[string][]string{ - "node": []string{node}, - }, - ) - if err != nil { - return mkErr("error listing accounts for "+ - "node %s: %v", node, err) - } - - acctList, ok := listAcctsResp.Data["acctList"] - if !ok { - return mkErr("accounts not returned for "+ - "node %s", node) - } - - err = os.WriteFile( - cfg.OutputAccounts+"."+node, - []byte(acctList.(string)), - outputFilePermissions, - ) - if err != nil { - return mkErr("error writing account list: %v", - err) - } - } - } - - // Create a new macaroon service. - rootKeyStore := &assignedRootKeyStore{ - key: cfg.macRootKey[:], - } - - // Check that we have a valid caveat, we only accept 3 formats. - checker := &caveatChecker{} - - bakeryParams := bakery.BakeryParams{ - RootKeyStore: rootKeyStore, - Location: "lnd", - Checker: checker, - } - - bkry := bakery.New(bakeryParams) - - // If we're asked to output a macaroon file, do it here. - if cfg.OutputMacaroon != "" { - for node, coin := range nodeListResp.Data { - caveats := []checkers.Caveat{ - checkers.Caveat{ - Condition: checkers.Condition( - "node", - node, - ), - }, - checkers.Caveat{ - Condition: checkers.Condition( - "coin", - coin.(json.Number).String(), - ), - }, - } - - mac, err := bkry.Oven.NewMacaroon( - ctx, - bakery.LatestVersion, - caveats, - nodePermissions..., - ) - if err != nil { - return mkErr("error baking macaroon: %v", err) - } - - macBytes, err := mac.M().MarshalBinary() - if err != nil { - return mkErr("error marshaling macaroon "+ - "binary: %v", err) - } - - err = os.WriteFile( - cfg.OutputMacaroon+"."+node, - macBytes, - outputFilePermissions, - ) - if err != nil { - return mkErr("error writing account list: %v", - err) - } - } - } - serverOpts, err := getTLSConfig(cfg) if err != nil { return mkErr("unable to load TLS credentials: %v", err) @@ -237,7 +96,7 @@ func Main(cfg *Config, lisCfg ListenerCfg) error { // Initialize the rpcServer and add its interceptor to the server // options. - rpcServer := newRPCServer(cfg, signerClient, bkry.Checker) + rpcServer := newRPCServer(cfg, signerClient) serverOpts = append( serverOpts, grpc.ChainUnaryInterceptor(rpcServer.intercept), @@ -275,80 +134,34 @@ func Main(cfg *Config, lisCfg ListenerCfg) error { // getTLSConfig returns a TLS configuration for the gRPC server. func getTLSConfig(cfg *Config) ([]grpc.ServerOption, error) { - // Ensure we create TLS key and certificate if they don't exist. - if !fileExists(cfg.TLSCertPath) && !fileExists(cfg.TLSKeyPath) { - signerLog.Infof("Generating TLS certificates...") - err := cert.GenCertPair( - "signer autogenerated cert", cfg.TLSCertPath, - cfg.TLSKeyPath, cfg.TLSExtraIPs, cfg.TLSExtraDomains, - cfg.TLSDisableAutofill, cfg.TLSCertDuration, - ) - if err != nil { - return nil, err - } - signerLog.Infof("Done generating TLS certificates") - } - - certData, parsedCert, err := cert.LoadCert( + // The certData returned here is just a wrapper around the PEM blocks + // loaded from the file. The PEM is not yet fully parsed but a basic + // check is performed that the certificate and private key actually + // belong together. + certData, err := tls.LoadX509KeyPair( cfg.TLSCertPath, cfg.TLSKeyPath, ) if err != nil { return nil, err } - // We check whether the certificate we have on disk match the IPs and - // domains specified by the config. If the extra IPs or domains have - // changed from when the certificate was created, we will refresh the - // certificate if auto refresh is active. - refresh := false - if cfg.TLSAutoRefresh { - refresh, err = cert.IsOutdated( - parsedCert, cfg.TLSExtraIPs, - cfg.TLSExtraDomains, cfg.TLSDisableAutofill, - ) - if err != nil { - return nil, err - } + // Now parse the the PEM block of the certificate. + _, err = x509.ParseCertificate(certData.Certificate[0]) + if err != nil { + return nil, err } - // If the certificate expired or it was outdated, delete it and the TLS - // key and generate a new pair. - if time.Now().After(parsedCert.NotAfter) || refresh { - signerLog.Info("TLS certificate is expired or outdated, " + - "generating a new one") - - err := os.Remove(cfg.TLSCertPath) - if err != nil { - return nil, err - } - - err = os.Remove(cfg.TLSKeyPath) - if err != nil { - return nil, err - } - - signerLog.Infof("Renewing TLS certificates...") - err = cert.GenCertPair( - "signer autogenerated cert", cfg.TLSCertPath, - cfg.TLSKeyPath, cfg.TLSExtraIPs, cfg.TLSExtraDomains, - cfg.TLSDisableAutofill, cfg.TLSCertDuration, - ) - if err != nil { - return nil, err - } - signerLog.Infof("Done renewing TLS certificates") - - // Reload the certificate data. - certData, _, err = cert.LoadCert( - cfg.TLSCertPath, cfg.TLSKeyPath, - ) - if err != nil { - return nil, err - } + tlsCfg := &tls.Config{ + Certificates: []tls.Certificate{certData}, + MinVersion: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + }, } - tlsCfg := cert.TLSConfFromCert(certData) - serverCreds := credentials.NewTLS(tlsCfg) serverOpts := []grpc.ServerOption{grpc.Creds(serverCreds)} diff --git a/log.go b/log.go index 575ba43..8c49994 100644 --- a/log.go +++ b/log.go @@ -6,34 +6,18 @@ package lndsigner import ( - "errors" - "os" - "github.com/bottlepay/lndsigner/keyring" - "github.com/btcsuite/btcd/txscript" - "github.com/btcsuite/btclog" -) - -var ( - backend = btclog.NewBackend(os.Stdout) - signerLog = backend.Logger("SIGNER") - txscriptLog = backend.Logger("TXSCRIPT") - keyringLog = backend.Logger("KEYRING") + "go.uber.org/zap" + "go.uber.org/zap/zapcore" ) -func setLogLevel(level string) error { - logLevel, ok := btclog.LevelFromString(level) - if !ok { - return errors.New("invalid log level: " + level) - } - - signerLog.SetLevel(logLevel) - - txscriptLog.SetLevel(logLevel) - txscript.UseLogger(txscriptLog) - - keyringLog.SetLevel(logLevel) - keyring.UseLogger(keyringLog) +var signerLog *zap.SugaredLogger - return nil +func init() { + config := zap.NewDevelopmentConfig() + config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder + config.EncoderConfig.EncodeCaller = nil + rawLog := zap.Must(config.Build()) + signerLog = rawLog.Sugar() + keyring.UseLogger(signerLog.With(zap.Any("pkg", "keyring"))) } diff --git a/macaroons.go b/macaroons.go deleted file mode 100644 index e57e7dc..0000000 --- a/macaroons.go +++ /dev/null @@ -1,183 +0,0 @@ -// Copyright (C) 2013-2017 The btcsuite developers -// Copyright (C) 2015-2016 The Decred developers -// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers -// Copyright (C) 2022 Bottlepay and The Lightning Network Developers - -package lndsigner - -import ( - "context" - "encoding/hex" - "errors" - "fmt" - "strings" - - "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" - "gopkg.in/macaroon-bakery.v2/bakery/checkers" - "gopkg.in/macaroon.v2" -) - -var defaultRootKeyID = []byte("0") - -type assignedRootKeyStore struct { - key []byte -} - -func (s *assignedRootKeyStore) Get(ctx context.Context, id []byte) ([]byte, - error) { - - return s.key, nil -} - -func (s *assignedRootKeyStore) RootKey(ctx context.Context) ([]byte, []byte, - error) { - - return s.key, defaultRootKeyID, nil -} - -type caveatChecker struct{} - -func (c *caveatChecker) CheckFirstPartyCaveat(ctx context.Context, - caveat string) error { - switch { - case caveat == "coin 0": - return nil - - case caveat == "coin 1": - return nil - - case len(caveat) == 71 && strings.HasPrefix( - caveat, - "node ", - ): - _, err := hex.DecodeString(caveat[5:]) - return err - - default: - return fmt.Errorf("invalid caveat: %s", caveat) - } -} - -func (c *caveatChecker) Namespace() *checkers.Namespace { - return nil -} - -func (r *rpcServer) checkMac(ctx context.Context, method string) (string, int, - error) { - - md, ok := metadata.FromIncomingContext(ctx) - if !ok { - signerLog.Warnf("request for %v without metadata", method) - return "", 0, status.Error(codes.Unauthenticated, "no metadata") - } - - macaroonHex, ok := md["macaroon"] - if !ok { - signerLog.Warnf("request for %v without macaroons", method) - return "", 0, status.Error(codes.Unauthenticated, "no macaroons") - } - - var macSlice macaroon.Slice - - var ( - node string - coin int - coinSet bool - - check = func(caveat string) error { - signerLog.Tracef("checking caveat: %s", caveat) - - switch { - case strings.HasPrefix(caveat, "node "): - if node != "" { - return errors.New("node already set") - } - - // Caveat should be 5 bytes of "node " prefix - // plus 66 bytes of pubkey hex digits. - if len(caveat) != 71 { - return errors.New("invalid node pubkey") - } - - node = caveat[5:] - - case caveat == "coin 0": - if coinSet { - return errors.New("coin already set") - } - - coin = 0 - coinSet = true - - case caveat == "coin 1": - if coinSet { - return errors.New("coin already set") - } - - coin = 1 - coinSet = true - - default: - return errors.New("invalid caveat") - - } - - return nil - } - ) - - for _, macHex := range macaroonHex { - macBytes, err := hex.DecodeString(macHex) - if err != nil { - signerLog.Warnf("failed to decode macaroon hex "+ - "for %v: %v", method, err) - continue - } - - mac := &macaroon.Macaroon{} - err = mac.UnmarshalBinary(macBytes) - if err != nil { - signerLog.Warnf("failed to unmarshal macaroon bytes "+ - "for %v: %v", method, err) - continue - } - - err = mac.Verify(r.cfg.macRootKey[:], check, nil) - if err != nil { - signerLog.Warnf("failed to verify macaroon "+ - "for %v: %v", method, err) - continue - } - - macSlice = append(macSlice, mac) - } - - if len(macSlice) == 0 { - signerLog.Warnf("macaroon authentication failure for %v", - method) - return "", 0, status.Error(codes.Unauthenticated, - "macaroon authentication failure") - } - - if !(len(node) == 66 && coinSet) { - signerLog.Warn("macaroon doesn't specify both node and coin") - return "", 0, status.Error(codes.Unauthenticated, - "macaroon authentication failure") - } - - authChecker := r.checker.Auth(macSlice) - authInfo, err := authChecker.Allow(ctx, r.perms[method]...) - if err != nil { - signerLog.Warnf("macaroon authorization failure for %v: %v", - method, err) - return "", 0, status.Error(codes.PermissionDenied, - "macaroon authorization failure") - } - - signerLog.Debugf("successfully authorized request to %v", method) - signerLog.Tracef("auth info for %v: %+v", method, authInfo) - - return node, coin, nil -} diff --git a/proto/signer.pb.go b/proto/signer.pb.go index 183a113..96b38f0 100644 --- a/proto/signer.pb.go +++ b/proto/signer.pb.go @@ -183,13 +183,6 @@ type SharedKeyRequest struct { // The ephemeral public key to use for the DH key derivation. EphemeralPubkey []byte `protobuf:"bytes,1,opt,name=ephemeral_pubkey,json=ephemeralPubkey,proto3" json:"ephemeral_pubkey,omitempty"` // - //Deprecated. The optional key locator of the local key that should be used. - //If this parameter is not set then the node's identity private key will be - //used. - // - // Deprecated: Do not use. - KeyLoc *KeyLocator `protobuf:"bytes,2,opt,name=key_loc,json=keyLoc,proto3" json:"key_loc,omitempty"` - // //A key descriptor describes the key used for performing ECDH. Either a key //locator or a raw public key is expected, if neither is supplied, defaults to //the node's identity private key. @@ -235,14 +228,6 @@ func (x *SharedKeyRequest) GetEphemeralPubkey() []byte { return nil } -// Deprecated: Do not use. -func (x *SharedKeyRequest) GetKeyLoc() *KeyLocator { - if x != nil { - return x.KeyLoc - } - return nil -} - func (x *SharedKeyRequest) GetKeyDesc() *KeyDescriptor { if x != nil { return x.KeyDesc @@ -320,33 +305,30 @@ var file_signer_proto_rawDesc = []byte{ 0x72, 0x72, 0x53, 0x69, 0x67, 0x54, 0x61, 0x70, 0x54, 0x77, 0x65, 0x61, 0x6b, 0x22, 0x2f, 0x0a, 0x0f, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x9e, - 0x01, 0x0a, 0x10, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, - 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x65, - 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x50, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x12, 0x2e, - 0x0a, 0x07, 0x6b, 0x65, 0x79, 0x5f, 0x6c, 0x6f, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4b, 0x65, 0x79, 0x4c, 0x6f, 0x63, 0x61, 0x74, - 0x6f, 0x72, 0x42, 0x02, 0x18, 0x01, 0x52, 0x06, 0x6b, 0x65, 0x79, 0x4c, 0x6f, 0x63, 0x12, 0x2f, - 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x22, - 0x32, 0x0a, 0x11, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x5f, 0x6b, - 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, - 0x4b, 0x65, 0x79, 0x32, 0x8c, 0x01, 0x0a, 0x06, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x12, 0x3c, - 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x15, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x52, 0x65, 0x71, 0x1a, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x69, 0x67, - 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x12, 0x44, 0x0a, 0x0f, - 0x44, 0x65, 0x72, 0x69, 0x76, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, - 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, - 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x62, 0x6f, 0x74, 0x74, 0x6c, 0x65, 0x70, 0x61, 0x79, 0x2f, 0x6c, 0x6e, 0x64, 0x73, 0x69, - 0x67, 0x6e, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x6e, + 0x0a, 0x10, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x5f, + 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x65, 0x70, + 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x50, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x12, 0x2f, 0x0a, + 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x22, 0x32, + 0x0a, 0x11, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x5f, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, + 0x65, 0x79, 0x32, 0x8c, 0x01, 0x0a, 0x06, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x12, 0x3c, 0x0a, + 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x15, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x52, 0x65, 0x71, 0x1a, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x69, 0x67, 0x6e, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x12, 0x44, 0x0a, 0x0f, 0x44, + 0x65, 0x72, 0x69, 0x76, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x17, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x62, 0x6f, 0x74, 0x74, 0x6c, 0x65, 0x70, 0x61, 0x79, 0x2f, 0x6c, 0x6e, 0x64, 0x73, 0x69, 0x67, + 0x6e, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, } var ( @@ -372,17 +354,16 @@ var file_signer_proto_goTypes = []interface{}{ } var file_signer_proto_depIdxs = []int32{ 4, // 0: proto.SignMessageReq.key_loc:type_name -> proto.KeyLocator - 4, // 1: proto.SharedKeyRequest.key_loc:type_name -> proto.KeyLocator - 5, // 2: proto.SharedKeyRequest.key_desc:type_name -> proto.KeyDescriptor - 0, // 3: proto.Signer.SignMessage:input_type -> proto.SignMessageReq - 2, // 4: proto.Signer.DeriveSharedKey:input_type -> proto.SharedKeyRequest - 1, // 5: proto.Signer.SignMessage:output_type -> proto.SignMessageResp - 3, // 6: proto.Signer.DeriveSharedKey:output_type -> proto.SharedKeyResponse - 5, // [5:7] is the sub-list for method output_type - 3, // [3:5] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name + 5, // 1: proto.SharedKeyRequest.key_desc:type_name -> proto.KeyDescriptor + 0, // 2: proto.Signer.SignMessage:input_type -> proto.SignMessageReq + 2, // 3: proto.Signer.DeriveSharedKey:input_type -> proto.SharedKeyRequest + 1, // 4: proto.Signer.SignMessage:output_type -> proto.SignMessageResp + 3, // 5: proto.Signer.DeriveSharedKey:output_type -> proto.SharedKeyResponse + 4, // [4:6] is the sub-list for method output_type + 2, // [2:4] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name } func init() { file_signer_proto_init() } diff --git a/proto/signer.proto b/proto/signer.proto index 9eed825..3630c7c 100644 --- a/proto/signer.proto +++ b/proto/signer.proto @@ -78,13 +78,6 @@ message SharedKeyRequest { // The ephemeral public key to use for the DH key derivation. bytes ephemeral_pubkey = 1; - /* - Deprecated. The optional key locator of the local key that should be used. - If this parameter is not set then the node's identity private key will be - used. - */ - KeyLocator key_loc = 2 [ deprecated = true ]; - /* A key descriptor describes the key used for performing ECDH. Either a key locator or a raw public key is expected, if neither is supplied, defaults to diff --git a/proto/walletkit.proto b/proto/walletkit.proto index 4cde64a..a0ab4eb 100644 --- a/proto/walletkit.proto +++ b/proto/walletkit.proto @@ -18,7 +18,7 @@ service WalletKit { (UTXO information, BIP32 derivation information, witness or sig scripts) set. If no error is returned, the PSBT is ready to be given to the next signer or - to be finalized if lnd was the last signer. + to be finalized if we were the last signer. NOTE: This RPC only signs inputs (and only those it can sign), it does not perform any other tasks (such as coin selection, UTXO locking or diff --git a/rpcserver.go b/rpcserver.go index aceee6d..56496a1 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -14,7 +14,6 @@ import ( "github.com/hashicorp/vault/api" "github.com/tv42/zbase32" "google.golang.org/grpc" - "gopkg.in/macaroon-bakery.v2/bakery" ) // keyRingKeyStruct is a struct used to look up a keyring passed in a context. @@ -22,32 +21,6 @@ type keyRingKeyStruct struct{} var ( keyRingKey = keyRingKeyStruct{} - - // nodePermissions is a slice of all entities for using signing - // permissions for authorization purposes, all lowercase. - nodePermissions = []bakery.Op{ - { - Entity: "onchain", - Action: "write", - }, - { - Entity: "message", - Action: "write", - }, - { - Entity: "signer", - Action: "generate", - }, - } - - // mainRPCServerPermissions is a mapping of the main RPC server calls - // to the permissions they require. - mainRPCServerPermissions = map[string][]bakery.Op{ - "/proto.Lightning/SignMessage": {{ - Entity: "message", - Action: "write", - }}, - } ) // rpcServer is a gRPC front end to the signer daemon. @@ -57,13 +30,11 @@ type rpcServer struct { // alignment. proto.UnimplementedLightningServer - perms map[string][]bakery.Op - client *api.Logical - checker *bakery.Checker - cfg *Config + + keyRing *keyring.KeyRing } // A compile time check to ensure that rpcServer fully implements the @@ -73,14 +44,15 @@ var _ proto.LightningServer = (*rpcServer)(nil) // newRPCServer creates and returns a new instance of the rpcServer. Before // dependencies are added, this will be an non-functioning RPC server only to // be used to register the LightningService with the gRPC server. -func newRPCServer(cfg *Config, c *api.Logical, - checker *bakery.Checker) *rpcServer { - +func newRPCServer(cfg *Config, c *api.Logical) *rpcServer { return &rpcServer{ - cfg: cfg, - client: c, - checker: checker, - perms: make(map[string][]bakery.Op), + cfg: cfg, + client: c, + keyRing: keyring.NewKeyRing( + c, + cfg.NodePubKey, + cfg.ActiveNetParams.HDCoinType, + ), } } @@ -90,15 +62,8 @@ func (r *rpcServer) intercept(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) ( interface{}, error) { - node, coin, err := r.checkMac(ctx, info.FullMethod) - if err != nil { - return nil, err - } - - keyRing := keyring.NewKeyRing(r.client, node, coin) - return handler( - context.WithValue(ctx, keyRingKey, keyRing), + context.WithValue(ctx, keyRingKey, r.keyRing), req, ) } @@ -107,17 +72,11 @@ func (r *rpcServer) intercept(ctx context.Context, req interface{}, // root gRPC server. func (r *rpcServer) RegisterWithGrpcServer(grpcServer *grpc.Server) error { // Register the main RPC server. - for k, v := range mainRPCServerPermissions { - r.perms[k] = v - } lnDesc := proto.Lightning_ServiceDesc lnDesc.ServiceName = "lnrpc.Lightning" grpcServer.RegisterService(&lnDesc, r) // Register the wallet subserver. - for k, v := range walletPermissions { - r.perms[k] = v - } walletDesc := proto.WalletKit_ServiceDesc walletDesc.ServiceName = "walletrpc.WalletKit" grpcServer.RegisterService(&walletDesc, &walletKit{ @@ -125,9 +84,6 @@ func (r *rpcServer) RegisterWithGrpcServer(grpcServer *grpc.Server) error { }) // Register the signer subserver. - for k, v := range signerPermissions { - r.perms[k] = v - } signerDesc := proto.Signer_ServiceDesc signerDesc.ServiceName = "signrpc.Signer" grpcServer.RegisterService(&signerDesc, &signerServer{ @@ -149,7 +105,7 @@ var ( // SignMessage signs a message with the resident node's private key. The // returned signature string is zbase32 encoded and pubkey recoverable, meaning // that only the message digest and signature are needed for verification. -func (r *rpcServer) SignMessage(ctx context.Context, +func (r *rpcServer) SignMessage(_ context.Context, in *proto.SignMessageRequest) (*proto.SignMessageResponse, error) { if in.Msg == nil { @@ -162,12 +118,7 @@ func (r *rpcServer) SignMessage(ctx context.Context, Index: 0, } - keyRing := ctx.Value(keyRingKey).(*keyring.KeyRing) - if keyRing == nil { - return nil, fmt.Errorf("no node/coin from macaroon") - } - - sig, err := keyRing.SignMessage( + sig, err := r.keyRing.SignMessage( keyLoc, in.Msg, !in.SingleHash, true, ) if err != nil { diff --git a/signer_server.go b/signer_server.go index 4ea2dae..7d14e55 100644 --- a/signer_server.go +++ b/signer_server.go @@ -14,27 +14,13 @@ import ( "github.com/bottlepay/lndsigner/vault" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" - "gopkg.in/macaroon-bakery.v2/bakery" -) - -var ( - // signerPermissions maps RPC calls to the permissions they require. - signerPermissions = map[string][]bakery.Op{ - "/proto.Signer/SignMessage": {{ - Entity: "signer", - Action: "generate", - }}, - "/proto.Signer/DeriveSharedKey": {{ - Entity: "signer", - Action: "generate", - }}, - } ) // Server is a sub-server of the main RPC server: the signer RPC. This sub RPC // server allows external callers to access the full signing capabilities of -// lnd. This allows callers to create custom protocols, external to lnd, even -// backed by multiple distinct lnd across independent failure domains. +// lndsignerd. This allows callers to create custom protocols, external to the +// signer itself, even backed by multiple distinct signers across independent +// failure domains. type signerServer struct { // Required by the grpc-gateway/v2 library for forward compatibility. proto.UnimplementedSignerServer @@ -48,7 +34,7 @@ var _ proto.SignerServer = (*signerServer)(nil) // SignMessage signs a message with the key specified in the key locator. The // returned signature is fixed-size LN wire format encoded. -func (s *signerServer) SignMessage(ctx context.Context, +func (s *signerServer) SignMessage(_ context.Context, in *proto.SignMessageReq) (*proto.SignMessageResp, error) { if in.Msg == nil { @@ -68,14 +54,9 @@ func (s *signerServer) SignMessage(ctx context.Context, Index: uint32(in.KeyLoc.KeyIndex), } - keyRing := ctx.Value(keyRingKey).(*keyring.KeyRing) - if keyRing == nil { - return nil, fmt.Errorf("no node/coin from macaroon") - } - // Use the schnorr signature algorithm to sign the message. if in.SchnorrSig { - sig, err := keyRing.SignMessageSchnorr( + sig, err := s.server.keyRing.SignMessageSchnorr( keyLocator, in.Msg, in.DoubleHash, in.SchnorrSigTapTweak, ) @@ -96,7 +77,7 @@ func (s *signerServer) SignMessage(ctx context.Context, // Create the raw ECDSA signature first and convert it to the final wire // format after. - sig, err := keyRing.SignMessage( + sig, err := s.server.keyRing.SignMessage( keyLocator, in.Msg, in.DoubleHash, in.CompactSig, ) if err != nil { @@ -127,12 +108,6 @@ func (s *signerServer) DeriveSharedKey(ctx context.Context, return nil, fmt.Errorf("must provide ephemeral pubkey") } - // Check for backward compatibility. The caller either specifies the old - // key_loc field, or the new key_desc field, but not both. - if in.KeyDesc != nil && in.KeyLoc != nil { - return nil, fmt.Errorf("use either key_desc or key_loc") - } - // When key_desc is used, the key_desc.key_loc is expected as the caller // needs to specify the KeyFamily. if in.KeyDesc != nil && in.KeyDesc.KeyLoc == nil { @@ -140,14 +115,9 @@ func (s *signerServer) DeriveSharedKey(ctx context.Context, "key_desc.key_loc must also be set") } - // We extract two params, rawKeyBytes and keyLoc. Notice their initial - // values will be overwritten if not using the deprecated RPC param. - var rawKeyBytes []byte - keyLoc := in.KeyLoc - if in.KeyDesc != nil { - keyLoc = in.KeyDesc.GetKeyLoc() - rawKeyBytes = in.KeyDesc.GetRawKeyBytes() - } + // We extract two params, rawKeyBytes and keyLoc. + rawKeyBytes := in.KeyDesc.GetRawKeyBytes() + keyLoc := in.KeyDesc.GetKeyLoc() // When no keyLoc is supplied, defaults to the node's identity private // key. @@ -183,17 +153,13 @@ func (s *signerServer) DeriveSharedKey(ctx context.Context, PubKey: pk, } - keyRing := ctx.Value(keyRingKey).(*keyring.KeyRing) - if keyRing == nil { - return nil, fmt.Errorf("no node/coin from macaroon") - } - // Derive the shared key using ECDH and hashing the serialized // compressed shared point. - sharedKeyHash, err := keyRing.ECDH(keyDescriptor, ephemeralPubkey) + sharedKeyHash, err := s.server.keyRing.ECDH( + keyDescriptor, ephemeralPubkey, + ) if err != nil { - err := fmt.Errorf("unable to derive shared key: %v", err) - signerLog.Error(err) + signerLog.Errorf("unable to derive shared key: %+v", err) return nil, err } diff --git a/vault/backend.go b/vault/backend.go index 040ac26..e2a5c09 100644 --- a/vault/backend.go +++ b/vault/backend.go @@ -19,9 +19,20 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/txscript" "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/helper/jsonutil" "github.com/hashicorp/vault/sdk/logical" ) +type listedAccount struct { + Name string `json:"name"` + AddressType string `json:"address_type"` + XPub string `json:"extended_public_key"` + DerivationPath string `json:"derivation_path"` + ExternalKeyCount int `json:"external_key_count"` + InternalKeyCount int `json:"internal_key_count"` + WatchOnly bool `json:"watch_only"` +} + func (b *backend) listAccounts(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { @@ -41,19 +52,17 @@ func (b *backend) listAccounts(ctx context.Context, req *logical.Request, } defer rootKey.Zero() - acctList := "{\n \"accounts\": [\n" + acctList := make([]*listedAccount, 0, 260) listAccount := func(purpose, coin, act uint32, addrType string, - version []byte) (string, error) { - - strListing := "" + version []byte) (*listedAccount, error) { // Derive purpose. purposeKey, err := rootKey.DeriveNonStandard( purpose + hdkeychain.HardenedKeyStart, ) if err != nil { - return "", err + return nil, err } defer purposeKey.Zero() @@ -62,7 +71,7 @@ func (b *backend) listAccounts(ctx context.Context, req *logical.Request, coin + hdkeychain.HardenedKeyStart, ) if err != nil { - return "", err + return nil, err } defer coinKey.Zero() @@ -71,21 +80,21 @@ func (b *backend) listAccounts(ctx context.Context, req *logical.Request, act + hdkeychain.HardenedKeyStart, ) if err != nil { - return "", err + return nil, err } defer actKey.Zero() // Get account watch-only pubkey. xPub, err := actKey.Neuter() if err != nil { - return "", err + return nil, err } // Ensure we get the right HDVersion for the account key. if version != nil { xPub, err = xPub.CloneWithVersion(version) if err != nil { - return "", err + return nil, err } } @@ -93,40 +102,23 @@ func (b *backend) listAccounts(ctx context.Context, req *logical.Request, strCoin := fmt.Sprintf("%d", coin) strAct := fmt.Sprintf("%d", act) - strListing += " {\n" + listing := &listedAccount{ + Name: "act:" + strAct, + AddressType: addrType, + XPub: xPub.String(), + DerivationPath: "m/" + strPurpose + "'/" + strCoin + + "'/" + strAct + "'", + } - strListing += " \"name\": \"" if act == 0 { - strListing += "default" - } else { - strListing += "act:" + strAct + listing.Name = "default" } - strListing += "\",\n" - - strListing += " \"address_type\": \"" + addrType + - "\",\n" - - strListing += " \"extended_public_key\": \"" + - xPub.String() + "\",\n" - - strListing += " \"master_key_fingerprint\": null,\n" - - strListing += " \"derivation_path\": \"m/" + - strPurpose + "'/" + strCoin + "'/" + strAct + "'\",\n" - - strListing += " \"external_key_count\": 0,\n" - strListing += " \"internal_key_count\": 0,\n" - - strListing += " \"watch_only\": false\n" - - strListing += " }" - - return strListing, nil + return listing, nil } for _, acctInfo := range defaultPurposes { - strListing, err := listAccount( + listing, err := listAccount( acctInfo.purpose, 0, 0, @@ -139,11 +131,11 @@ func (b *backend) listAccounts(ctx context.Context, req *logical.Request, return nil, err } - acctList += strListing + ",\n" + acctList = append(acctList, listing) } for act := uint32(0); act <= MaxAcctID; act++ { - strListing, err := listAccount( + listing, err := listAccount( Bip0043purpose, net.HDCoinType, act, @@ -156,20 +148,21 @@ func (b *backend) listAccounts(ctx context.Context, req *logical.Request, return nil, err } - acctList += strListing - - if act < MaxAcctID { - acctList += "," - } - - acctList += "\n" + acctList = append(acctList, listing) } - acctList += " ]\n}" + resp, err := jsonutil.EncodeJSON(struct { + Accounts []*listedAccount `json:"accounts"` + }{ + Accounts: acctList, + }) + if err != nil { + return nil, err + } return &logical.Response{ Data: map[string]interface{}{ - "acctList": acctList, + "acctList": string(resp), }, }, nil } @@ -464,7 +457,7 @@ func (b *backend) getNode(ctx context.Context, storage logical.Storage, return nil, nil, errors.New("got invalid seed from storage") } - net, err := getNet(string(entry.Value[hdkeychain.RecommendedSeedLen:])) + net, err := GetNet(string(entry.Value[hdkeychain.RecommendedSeedLen:])) if err != nil { return nil, nil, err } @@ -492,7 +485,12 @@ func (b *backend) listNodes(ctx context.Context, req *logical.Request, } defer zero(seed) - respData[node] = int(net.HDCoinType) + netName := net.Name + if netName == "testnet3" { + netName = "testnet" + } + + respData[node] = netName } return &logical.Response{ @@ -504,7 +502,7 @@ func (b *backend) createNode(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { strNet := data.Get("network").(string) - net, err := getNet(strNet) + net, err := GetNet(strNet) if err != nil { b.Logger().Error("Failed to parse network", "error", err) return nil, err @@ -569,12 +567,12 @@ func (b *backend) createNode(ctx context.Context, req *logical.Request, }, nil } -func getNet(strNet string) (*chaincfg.Params, error) { +func GetNet(strNet string) (*chaincfg.Params, error) { switch strNet { /*case "mainnet": return &chaincfg.MainNetParams, nil */ - case "testnet": + case "testnet", "testnet3": return &chaincfg.TestNet3Params, nil case "simnet": diff --git a/vault/keys.go b/vault/keys.go index 26a40df..ffbbf53 100644 --- a/vault/keys.go +++ b/vault/keys.go @@ -45,7 +45,8 @@ func checkRequiredPubKey(derived *hdkeychain.ExtendedKey, } if !bytes.Equal(requiredBytes, pubKeyBytes) { - return errors.New("pubkey mismatch") + return fmt.Errorf("pubkey mismatch: wanted %x, got %x", + requiredBytes, pubKeyBytes) } return nil diff --git a/walletkit_server.go b/walletkit_server.go index a7f8472..4a45796 100644 --- a/walletkit_server.go +++ b/walletkit_server.go @@ -10,20 +10,8 @@ import ( "context" "fmt" - "github.com/bottlepay/lndsigner/keyring" "github.com/bottlepay/lndsigner/proto" "github.com/btcsuite/btcd/btcutil/psbt" - "gopkg.in/macaroon-bakery.v2/bakery" -) - -var ( - // macPermissions maps RPC calls to the permissions they require. - walletPermissions = map[string][]bakery.Op{ - "/proto.WalletKit/SignPsbt": {{ - Entity: "onchain", - Action: "write", - }}, - } ) // walletKit is a sub-RPC server that exposes a tool kit which allows clients @@ -45,13 +33,13 @@ var _ proto.WalletKitServer = (*walletKit)(nil) // (UTXO information, BIP32 derivation information, witness or sig scripts) // set. // If no error is returned, the PSBT is ready to be given to the next signer or -// to be finalized if lnd was the last signer. +// to be finalized if lndsignerd was the last signer. // // NOTE: This RPC only signs inputs (and only those it can sign), it does not // perform any other tasks (such as coin selection, UTXO locking or // input/output/fee value validation, PSBT finalization). Any input that is // incomplete will be skipped. -func (w *walletKit) SignPsbt(ctx context.Context, req *proto.SignPsbtRequest) ( +func (w *walletKit) SignPsbt(_ context.Context, req *proto.SignPsbtRequest) ( *proto.SignPsbtResponse, error) { packet, err := psbt.NewFromRawBytes( @@ -79,12 +67,7 @@ func (w *walletKit) SignPsbt(ctx context.Context, req *proto.SignPsbtRequest) ( // Let the wallet do the heavy lifting. This will sign all inputs that // we have the UTXO for. If some inputs can't be signed and don't have // witness data attached, they will just be skipped. - keyRing := ctx.Value(keyRingKey).(*keyring.KeyRing) - if keyRing == nil { - return nil, fmt.Errorf("no node/coin from macaroon") - } - - signedInputs, err := keyRing.SignPsbt(packet) + signedInputs, err := w.server.keyRing.SignPsbt(packet) if err != nil { return nil, fmt.Errorf("error signing PSBT: %v", err) } From 73ba024207360267ac42b939ebebc4896c5170ba Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Thu, 8 Dec 2022 15:22:42 -0800 Subject: [PATCH 04/42] README: fix command line to generate accounts.json --- README.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 655359c..d117eed 100644 --- a/README.md +++ b/README.md @@ -103,22 +103,25 @@ remotesigner.macaroonpath=any.macaroon Note that `lnd` checks that the macaroon file deserializes correctly but lndsigner ignores the macaroon. -Next, get the account list for the node: +Next, get the account list for the node (this works on Linux with `jq` installed): ``` -~/.lnd-watchonly$ VAULT_ADDR=http://127.0.0.1:8200 VAULT_TOKEN=root vault read lndsigner/lnd-nodes/accounts node=*pubkey* > accounts.json +~/.lnd-watchonly$ VAULT_ADDR=http://127.0.0.1:8200 VAULT_TOKEN=root \ + vault read lndsigner/lnd-nodes/accounts node=*pubkey* | \ + tail -n 1 | sed s/acctList\\s*// | jq > accounts.json ``` You'll get an `accounts.json` file that starts like: ``` -Key Value ---- ----- -acctList {"accounts":[{"name":"default","address_type":"HYBRID_NESTED_WITNESS_PUBKEY_HASH","extended_public_key":"... +{ + "accounts": [ + { + "name": "default", + "address_type": "HYBRID_NESTED_WITNESS_PUBKEY_HASH", + "extended_public_key": "upub... ``` -Remove everything, including `acctList`, to the first `{` from the top of the file to get the actual JSON you'll need to import into `lnd` when you create the watch-only instance below. Your JSON file should start with `{"accounts":[{`. You can run it through `jq` to ensure the syntax is correct. - Now, run `lnd` in watch-only mode: ``` From 2c61fd1acfc5c8eb2ee66c876a35d732299045fd Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Thu, 8 Dec 2022 15:26:09 -0800 Subject: [PATCH 05/42] README: fix phrasing regarding TLS auth b/w lnd and lndsignerd --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d117eed..6c6015f 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ Use the pubkey from the node you created above. Note that on other platforms, th The rest of this README assumes you're working on Linux. Additional documentation for other platforms welcome. -You'll need to provide a `tls.key` and `tls.cert` for the daemon to accept TLS connections from `lnd`. For testing purposes, you can grab some that are auto-generated by a regtest instance of `lnd`. For deploy, you'll want your infrastructure to create these. +You'll need to provide a `tls.key` and `tls.cert` for the daemon. This allows it to accept TLS connections and lets `lnd` to authenticate that it's connecting to the correct signer, as configured below. For testing purposes, you can grab some that are auto-generated by a regtest instance of `lnd`. For deploy, you'll want your infrastructure to create these. Run the signer binary as follows: From e46fee8ea6c5e416704fdc5781aa926e9e844810 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Thu, 8 Dec 2022 15:29:02 -0800 Subject: [PATCH 06/42] README: clarify directories when deploying multiple signers/nodes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c6015f..7f9fa90 100644 --- a/README.md +++ b/README.md @@ -134,4 +134,4 @@ Create the watch-only wallet using the accounts exported by the signer: ~$ lncli createwatchonly .lndsigner/accounts.json ``` -Now you can use your node as usual. Note that MuSig2 isn't supported yet. If you created multiple nodes in the vault, you can create a separate directory for each signer instance and each watch-only node and start it as above. +Now you can use your node as usual. Note that MuSig2 isn't supported yet. If you created multiple nodes in the vault, you can create a separate directory for each signer instance (`.lndsigner`) and each watch-only node (`.lnd`) and start each as above. From f13bb16e2888f48bd8caee18deab94c35a570f35 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Wed, 1 Feb 2023 15:24:19 -0800 Subject: [PATCH 07/42] vault: filter lint failures on DeriveNonStandard --- vault/backend.go | 11 +++++++---- vault/keys.go | 18 ++++++++++++------ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/vault/backend.go b/vault/backend.go index e2a5c09..c1ff7fe 100644 --- a/vault/backend.go +++ b/vault/backend.go @@ -57,8 +57,11 @@ func (b *backend) listAccounts(ctx context.Context, req *logical.Request, listAccount := func(purpose, coin, act uint32, addrType string, version []byte) (*listedAccount, error) { - // Derive purpose. - purposeKey, err := rootKey.DeriveNonStandard( + // Derive purpose. We do these derivations with + // DeriveNonStandard to match btcwallet's (and thus lnd's) + // usage as shown here: + // https://github.com/btcsuite/btcwallet/blob/c314de6995500686c93716037f2279128cc1e9e8/waddrmgr/manager.go#L1459 + purposeKey, err := rootKey.DeriveNonStandard( // nolint:staticcheck purpose + hdkeychain.HardenedKeyStart, ) if err != nil { @@ -67,7 +70,7 @@ func (b *backend) listAccounts(ctx context.Context, req *logical.Request, defer purposeKey.Zero() // Derive coin. - coinKey, err := purposeKey.DeriveNonStandard( + coinKey, err := purposeKey.DeriveNonStandard( // nolint:staticcheck coin + hdkeychain.HardenedKeyStart, ) if err != nil { @@ -76,7 +79,7 @@ func (b *backend) listAccounts(ctx context.Context, req *logical.Request, defer coinKey.Zero() // Derive account. - actKey, err := coinKey.DeriveNonStandard( + actKey, err := coinKey.DeriveNonStandard( // nolint:staticcheck act + hdkeychain.HardenedKeyStart, ) if err != nil { diff --git a/vault/keys.go b/vault/keys.go index ffbbf53..8dd64d1 100644 --- a/vault/keys.go +++ b/vault/keys.go @@ -86,8 +86,10 @@ func derivePrivKey(seed []byte, net *chaincfg.Params, } defer rootKey.Zero() - // Derive purpose. - purposeKey, err := rootKey.DeriveNonStandard( + // Derive purpose. We do these derivations with DeriveNonStandard to + // match btcwallet's (and thus lnd's) usage as shown here: + // https://github.com/btcsuite/btcwallet/blob/c314de6995500686c93716037f2279128cc1e9e8/waddrmgr/manager.go#L1459 + purposeKey, err := rootKey.DeriveNonStandard( // nolint:staticcheck derPath[0], ) if err != nil { @@ -96,7 +98,7 @@ func derivePrivKey(seed []byte, net *chaincfg.Params, defer purposeKey.Zero() // Derive coin type. - coinTypeKey, err := purposeKey.DeriveNonStandard( + coinTypeKey, err := purposeKey.DeriveNonStandard( // nolint:staticcheck derPath[1], ) if err != nil { @@ -105,7 +107,7 @@ func derivePrivKey(seed []byte, net *chaincfg.Params, defer coinTypeKey.Zero() // Derive account. - accountKey, err := coinTypeKey.DeriveNonStandard( + accountKey, err := coinTypeKey.DeriveNonStandard( // nolint:staticcheck derPath[2], ) if err != nil { @@ -114,14 +116,18 @@ func derivePrivKey(seed []byte, net *chaincfg.Params, defer accountKey.Zero() // Derive branch. - branchKey, err := accountKey.DeriveNonStandard(derPath[3]) + branchKey, err := accountKey.DeriveNonStandard( // nolint:staticcheck + derPath[3], + ) if err != nil { return nil, errors.New("error deriving branch") } defer branchKey.Zero() // Derive index. - indexKey, err := branchKey.DeriveNonStandard(derPath[4]) + indexKey, err := branchKey.DeriveNonStandard( // nolint:staticcheck + derPath[4], + ) if err != nil { return nil, errors.New("error deriving index") } From cd5a131c84eeaf43555ec7d1ff7528bf59be7a07 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Wed, 1 Feb 2023 15:39:15 -0800 Subject: [PATCH 08/42] config: remove unused function --- config.go | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/config.go b/config.go index 2f758ba..10e8df3 100644 --- a/config.go +++ b/config.go @@ -6,7 +6,6 @@ package lndsigner import ( - "encoding/hex" "fmt" "net" "os" @@ -312,30 +311,6 @@ func CleanAndExpandPath(path string) string { return filepath.Clean(os.ExpandEnv(path)) } -// get32BytesFromEnv gets a 64-byte hex string, encoding a 32-byte value, from -// an environment variable. -func get32BytesFromEnv(envKey string) ([32]byte, error) { - strHex, ok := os.LookupEnv(envKey) - if !ok { - return [32]byte{}, fmt.Errorf("env var %s not found: ", envKey) - } - - keyBytes, err := hex.DecodeString(strHex) - if err != nil { - return [32]byte{}, err - } - - if len(keyBytes) != 32 { - return [32]byte{}, fmt.Errorf("key length %d instead of 32", - len(keyBytes)) - } - - var key [32]byte - copy(key[:], keyBytes) - - return key, nil -} - // NormalizeAddresses returns a new slice with all the passed addresses // normalized with the given default port and all duplicates removed. func NormalizeAddresses(addrs []string, defaultPort string) ([]net.Addr, From c3465e408051020c8f058002226ca9bc70ac4ac6 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Wed, 1 Feb 2023 16:18:47 -0800 Subject: [PATCH 09/42] keyring: remove redundant return --- keyring/keyring.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/keyring/keyring.go b/keyring/keyring.go index 840b680..e472552 100644 --- a/keyring/keyring.go +++ b/keyring/keyring.go @@ -655,8 +655,6 @@ func getTweakParams(unknowns []*psbt.Unknown, reqData map[string]interface{}) { return } } - - return } // validateSigningMethod attempts to detect the signing method that is required From e25caeebb0a07a934e6d6d63fe248255875bf262 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Wed, 1 Feb 2023 16:22:07 -0800 Subject: [PATCH 10/42] vault-plugin-lndsigner: check error when parsing vault API flags --- cmd/vault-plugin-lndsigner/main.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/cmd/vault-plugin-lndsigner/main.go b/cmd/vault-plugin-lndsigner/main.go index feda564..52a7207 100644 --- a/cmd/vault-plugin-lndsigner/main.go +++ b/cmd/vault-plugin-lndsigner/main.go @@ -10,16 +10,20 @@ import ( ) func main() { + logger := hclog.New(&hclog.LoggerOptions{}) + apiClientMeta := &api.PluginAPIClientMeta{} flags := apiClientMeta.FlagSet() - flags.Parse(os.Args[1:]) + err := flags.Parse(os.Args[1:]) + if err != nil { + logger.Error("error parsing vault API client flags") + os.Exit(1) + } tlsConfig := apiClientMeta.GetTLSConfig() tlsProviderFunc := api.VaultPluginTLSProvider(tlsConfig) - logger := hclog.New(&hclog.LoggerOptions{}) - - err := plugin.Serve(&plugin.ServeOpts{ + err = plugin.Serve(&plugin.ServeOpts{ BackendFactoryFunc: vault.Factory, TLSProviderFunc: tlsProviderFunc, Logger: logger, From a90815d8f310a532a754fb520873caa04581080b Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Wed, 1 Feb 2023 16:26:08 -0800 Subject: [PATCH 11/42] lndsigner: remove unused context --- lndsigner.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lndsigner.go b/lndsigner.go index 7ccd5e0..cba8a1f 100644 --- a/lndsigner.go +++ b/lndsigner.go @@ -6,7 +6,6 @@ package lndsigner import ( - "context" "crypto/tls" "crypto/x509" "fmt" @@ -52,10 +51,6 @@ func Main(cfg *Config, lisCfg ListenerCfg) error { signerLog.Infof("Active Bitcoin network: %v)", cfg.ActiveNetParams.Name) - ctx := context.Background() - ctx, cancel := context.WithCancel(ctx) - defer cancel() - // Use defaults for vault client, including getting config from env. vaultClient, err := api.NewClient(nil) if err != nil { From 1ad7df7ffe9d543b4c35c1b90d69330694cf0983 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Thu, 2 Feb 2023 10:33:12 -0800 Subject: [PATCH 12/42] lint: enable golangci-lint on repo using github action --- .github/workflows/golangci-lint.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/golangci-lint.yml diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..3ed5ea5 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,26 @@ +name: golangci-lint + +on: + push: + tags: [ "v*" ] + branches: [ "master", "main" ] + pull_request: + branches: [ "**" ] + + +permissions: + contents: read + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v3 + with: + go-version: 1.19.5 + - uses: actions/checkout@v3 + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: latest From 3e0ae0a4ec5bd812d3c52b4f158d93b757ea8746 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Thu, 19 Jan 2023 16:22:31 -0800 Subject: [PATCH 13/42] vault: replace errors with exported error variables --- vault/backend.go | 10 +++++----- vault/errors.go | 18 ++++++++++++++++++ vault/keys.go | 26 ++++++++++---------------- 3 files changed, 33 insertions(+), 21 deletions(-) create mode 100644 vault/errors.go diff --git a/vault/backend.go b/vault/backend.go index c1ff7fe..3d394ac 100644 --- a/vault/backend.go +++ b/vault/backend.go @@ -177,7 +177,7 @@ func (b *backend) ecdh(ctx context.Context, req *logical.Request, if len(peerPubHex) != 2*btcec.PubKeyBytesLenCompressed { b.Logger().Error("Peer pubkey is wrong length", "peer", peerPubHex) - return nil, errors.New("invalid peer pubkey") + return nil, ErrInvalidPeerPubkey } peerPubBytes, err := hex.DecodeString(peerPubHex) @@ -299,7 +299,7 @@ func (b *backend) deriveAndSign(ctx context.Context, req *logical.Request, if numTweaks > 1 { b.Logger().Error("Both single and double tweak specified") - return nil, errors.New("both single and double tweak specified") + return nil, ErrTooManyTweaks } strNode := data.Get("node").(string) @@ -443,7 +443,7 @@ func (b *backend) getNode(ctx context.Context, storage logical.Storage, id string) ([]byte, *chaincfg.Params, error) { if len(id) != 2*btcec.PubKeyBytesLenCompressed { - return nil, nil, errors.New("invalid node id") + return nil, nil, ErrInvalidNodeID } nodePath := "lnd-nodes/" + id @@ -453,11 +453,11 @@ func (b *backend) getNode(ctx context.Context, storage logical.Storage, } if entry == nil { - return nil, nil, errors.New("node not found") + return nil, nil, ErrNodeNotFound } if len(entry.Value) <= hdkeychain.RecommendedSeedLen { - return nil, nil, errors.New("got invalid seed from storage") + return nil, nil, ErrInvalidSeedFromStorage } net, err := GetNet(string(entry.Value[hdkeychain.RecommendedSeedLen:])) diff --git a/vault/errors.go b/vault/errors.go new file mode 100644 index 0000000..9c11679 --- /dev/null +++ b/vault/errors.go @@ -0,0 +1,18 @@ +package vault + +import ( + "errors" +) + +var ( + ErrInvalidPeerPubkey = errors.New("invalid peer pubkey") + ErrInvalidNodeID = errors.New("invalid node id") + ErrNodeNotFound = errors.New("node not found") + ErrInvalidSeedFromStorage = errors.New("invalid seed from storage") + ErrElementNotHardened = errors.New("derivation path element not hardened") + ErrNegativeElement = errors.New("negative derivation path element") + ErrWrongLengthDerivationPath = errors.New("derivation path not 5 elements") + ErrElementOverflow = errors.New("derivation path element > MaxUint32") + ErrPubkeyMismatch = errors.New("pubkey mismatch") + ErrTooManyTweaks = errors.New("both single and double tweak specified") +) diff --git a/vault/keys.go b/vault/keys.go index 8dd64d1..be086c6 100644 --- a/vault/keys.go +++ b/vault/keys.go @@ -9,8 +9,6 @@ import ( "bytes" "crypto/sha256" "encoding/hex" - "errors" - "fmt" "math" "github.com/btcsuite/btcd/btcec/v2" @@ -45,8 +43,7 @@ func checkRequiredPubKey(derived *hdkeychain.ExtendedKey, } if !bytes.Equal(requiredBytes, pubKeyBytes) { - return fmt.Errorf("pubkey mismatch: wanted %x, got %x", - requiredBytes, pubKeyBytes) + return ErrPubkeyMismatch } return nil @@ -56,25 +53,22 @@ func derivePrivKey(seed []byte, net *chaincfg.Params, derivationPath []int) (*hdkeychain.ExtendedKey, error) { if len(derivationPath) != 5 { - return nil, errors.New("derivation path not 5 elements") + return nil, ErrWrongLengthDerivationPath } derPath := make([]uint32, 5) for idx, element := range derivationPath { if element < 0 { - return nil, errors.New("negative derivation path " + - "element") + return nil, ErrNegativeElement } if element > math.MaxUint32 { - return nil, errors.New("derivation path element > " + - "MaxUint32") + return nil, ErrElementOverflow } if idx < 3 && element < hdkeychain.HardenedKeyStart { - return nil, fmt.Errorf("element at index %d is not "+ - "hardened", idx) + return nil, ErrElementNotHardened } derPath[idx] = uint32(element) @@ -93,7 +87,7 @@ func derivePrivKey(seed []byte, net *chaincfg.Params, derPath[0], ) if err != nil { - return nil, errors.New("error deriving purpose") + return nil, err } defer purposeKey.Zero() @@ -102,7 +96,7 @@ func derivePrivKey(seed []byte, net *chaincfg.Params, derPath[1], ) if err != nil { - return nil, errors.New("error deriving coin type") + return nil, err } defer coinTypeKey.Zero() @@ -111,7 +105,7 @@ func derivePrivKey(seed []byte, net *chaincfg.Params, derPath[2], ) if err != nil { - return nil, errors.New("error deriving account") + return nil, err } defer accountKey.Zero() @@ -120,7 +114,7 @@ func derivePrivKey(seed []byte, net *chaincfg.Params, derPath[3], ) if err != nil { - return nil, errors.New("error deriving branch") + return nil, err } defer branchKey.Zero() @@ -129,7 +123,7 @@ func derivePrivKey(seed []byte, net *chaincfg.Params, derPath[4], ) if err != nil { - return nil, errors.New("error deriving index") + return nil, err } return indexKey, nil From d842748f5e854a20cd3f2dd399bdec7e2248b6c6 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Thu, 19 Jan 2023 15:49:41 -0800 Subject: [PATCH 14/42] vault: break up paths function into separate functions for testing --- vault/paths.go | 282 ++++++++++++++++++++++++++----------------------- 1 file changed, 149 insertions(+), 133 deletions(-) diff --git a/vault/paths.go b/vault/paths.go index 21a620d..41a5d58 100644 --- a/vault/paths.go +++ b/vault/paths.go @@ -59,167 +59,183 @@ type backend struct { *framework.Backend } -func (b *backend) paths() []*framework.Path { - return []*framework.Path{ - &framework.Path{ - Pattern: "lnd-nodes/?", - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ReadOperation: b.listNodes, - logical.UpdateOperation: b.createNode, - logical.CreateOperation: b.createNode, - }, - HelpSynopsis: "Create and list LND nodes", - HelpDescription: ` +func (b *backend) basePath() *framework.Path { + return &framework.Path{ + Pattern: "lnd-nodes/?", + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.listNodes, + logical.UpdateOperation: b.createNode, + logical.CreateOperation: b.createNode, + }, + HelpSynopsis: "Create and list LND nodes", + HelpDescription: ` GET - list all node pubkeys and coin types for HD derivations POST - generate a new node seed and store it indexed by node pubkey `, - Fields: map[string]*framework.FieldSchema{ - "network": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "Network, one of " + - "'mainnet', 'testnet', " + - "'simnet', 'signet', or " + - "'regtest'", - Default: 1, - }, + Fields: map[string]*framework.FieldSchema{ + "network": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Network, one of " + + "'mainnet', 'testnet', " + + "'simnet', 'signet', or " + + "'regtest'", + Default: "regtest", }, }, - &framework.Path{ - Pattern: "lnd-nodes/accounts/?", - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ReadOperation: b.listAccounts, - }, - HelpSynopsis: "List accounts for import into LND " + - "watch-only node", - HelpDescription: ` + } +} + +func (b *backend) accountsPath() *framework.Path { + return &framework.Path{ + Pattern: "lnd-nodes/accounts/?", + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.listAccounts, + }, + HelpSynopsis: "List accounts for import into LND " + + "watch-only node", + HelpDescription: ` GET - list all node accounts in JSON format suitable for import into watch- only LND `, - Fields: map[string]*framework.FieldSchema{ - "node": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "node pubkey, must be " + - "66 hex characters", - Default: "", - }, + Fields: map[string]*framework.FieldSchema{ + "node": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "node pubkey, must be " + + "66 hex characters", + Default: "", }, }, - &framework.Path{ - Pattern: "lnd-nodes/ecdh/?", - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.UpdateOperation: b.ecdh, - logical.CreateOperation: b.ecdh, - }, - HelpSynopsis: "ECDH derived privkey with peer pubkey", - HelpDescription: ` + } +} + +func (b *backend) ecdhPath() *framework.Path { + return &framework.Path{ + Pattern: "lnd-nodes/ecdh/?", + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.ecdh, + logical.CreateOperation: b.ecdh, + }, + HelpSynopsis: "ECDH derived privkey with peer pubkey", + HelpDescription: ` POST - ECDH the privkey derived with the submitted path with the specified peer pubkey `, - Fields: map[string]*framework.FieldSchema{ - "node": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "node pubkey, must be " + - "66 hex characters", - Default: "", - }, - "path": &framework.FieldSchema{ - Type: framework.TypeCommaIntSlice, - Description: "derivation path, with " + - "the first 3 elements " + - "being hardened", - Default: []int{}, - }, - "pubkey": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "optional: pubkey for " + - "which to do ECDH, checked " + - "against derived pubkey to " + - "ensure a match", - Default: "", - }, - "peer": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "pubkey for ECDH peer, " + - "must be 66 hex characters", - Default: "", - }, + Fields: map[string]*framework.FieldSchema{ + "node": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "node pubkey, must be " + + "66 hex characters", + Default: "", }, - }, - &framework.Path{ - Pattern: "lnd-nodes/sign/?", - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ReadOperation: b.derivePubKey, - logical.UpdateOperation: b.deriveAndSign, - logical.CreateOperation: b.deriveAndSign, + "path": &framework.FieldSchema{ + Type: framework.TypeCommaIntSlice, + Description: "derivation path, with " + + "the first 3 elements " + + "being hardened", + Default: []int{}, + }, + "pubkey": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "optional: pubkey for " + + "which to do ECDH, checked " + + "against derived pubkey to " + + "ensure a match", + Default: "", + }, + "peer": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "pubkey for ECDH peer, " + + "must be 66 hex characters", + Default: "", }, - HelpSynopsis: "Derive pubkeys and sign with privkeys", - HelpDescription: ` + }, + } +} + +func (b *backend) signPath() *framework.Path { + return &framework.Path{ + Pattern: "lnd-nodes/sign/?", + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.derivePubKey, + logical.UpdateOperation: b.deriveAndSign, + logical.CreateOperation: b.deriveAndSign, + }, + HelpSynopsis: "Derive pubkeys and sign with privkeys", + HelpDescription: ` GET - return the pubkey derived with the submitted path POST - sign a digest with the method specified using the privkey derived with the submitted path `, - Fields: map[string]*framework.FieldSchema{ - "node": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "node pubkey, must be " + - "66 hex characters", - Default: "", - }, - "path": &framework.FieldSchema{ - Type: framework.TypeCommaIntSlice, - Description: "derivation path, with " + - "the first 3 elements " + - "being hardened", - Default: []int{}, - }, - "digest": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "digest to sign, must " + - "be hex-encoded 32 bytes", - Default: "", - }, - "method": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "signing method: " + - "one of: ecdsa, " + - "ecdsa-compact, or schnorr", - Default: "", - }, - "pubkey": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "optional: pubkey for " + - "which to sign, checked " + - "against derived pubkey to " + - "ensure a match", - Default: "", - }, - "taptweak": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "optional: hex-encoded " + - "taproot tweak", - Default: "", - }, - "ln1tweak": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "optional: hex-encoded " + - "LN single commit tweak", - Default: "", - }, - "ln2tweak": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "optional: hex-encoded " + - "LN double revocation tweak", - Default: "", - }, + Fields: map[string]*framework.FieldSchema{ + "node": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "node pubkey, must be " + + "66 hex characters", + Default: "", + }, + "path": &framework.FieldSchema{ + Type: framework.TypeCommaIntSlice, + Description: "derivation path, with " + + "the first 3 elements " + + "being hardened", + Default: []int{}, + }, + "digest": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "digest to sign, must " + + "be hex-encoded 32 bytes", + Default: "", + }, + "method": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "signing method: " + + "one of: ecdsa, " + + "ecdsa-compact, or schnorr", + Default: "", + }, + "pubkey": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "optional: pubkey for " + + "which to sign, checked " + + "against derived pubkey to " + + "ensure a match", + Default: "", + }, + "taptweak": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "optional: hex-encoded " + + "taproot tweak", + Default: "", + }, + "ln1tweak": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "optional: hex-encoded " + + "LN single commit tweak", + Default: "", + }, + "ln2tweak": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "optional: hex-encoded " + + "LN double revocation tweak", + Default: "", }, }, } } + +func (b *backend) paths() []*framework.Path { + return []*framework.Path{ + b.basePath(), + b.accountsPath(), + b.ecdhPath(), + b.signPath(), + } +} From 08c8015450069a7eddff33e47908665a044fc78f Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Tue, 24 Jan 2023 16:46:53 -0800 Subject: [PATCH 15/42] vault: remove redundant CreateOperation from paths --- vault/paths.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/vault/paths.go b/vault/paths.go index 41a5d58..9bc246e 100644 --- a/vault/paths.go +++ b/vault/paths.go @@ -65,7 +65,6 @@ func (b *backend) basePath() *framework.Path { Callbacks: map[logical.Operation]framework.OperationFunc{ logical.ReadOperation: b.listNodes, logical.UpdateOperation: b.createNode, - logical.CreateOperation: b.createNode, }, HelpSynopsis: "Create and list LND nodes", HelpDescription: ` @@ -117,7 +116,6 @@ func (b *backend) ecdhPath() *framework.Path { Pattern: "lnd-nodes/ecdh/?", Callbacks: map[logical.Operation]framework.OperationFunc{ logical.UpdateOperation: b.ecdh, - logical.CreateOperation: b.ecdh, }, HelpSynopsis: "ECDH derived privkey with peer pubkey", HelpDescription: ` @@ -164,7 +162,6 @@ func (b *backend) signPath() *framework.Path { Callbacks: map[logical.Operation]framework.OperationFunc{ logical.ReadOperation: b.derivePubKey, logical.UpdateOperation: b.deriveAndSign, - logical.CreateOperation: b.deriveAndSign, }, HelpSynopsis: "Derive pubkeys and sign with privkeys", HelpDescription: ` From a848ab8b90d9a6f208a7926e88d43614c1fcf01e Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Thu, 2 Feb 2023 16:17:45 -0800 Subject: [PATCH 16/42] vault: replace deprecated Callbacks with Operations in paths --- vault/paths.go | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/vault/paths.go b/vault/paths.go index 9bc246e..368d3b4 100644 --- a/vault/paths.go +++ b/vault/paths.go @@ -59,12 +59,20 @@ type backend struct { *framework.Backend } +// TODO(aakselrod): expand text documentation throughout this file where +// fields are available, in order to auto-generate docs. +func wrapOp(f framework.OperationFunc) framework.OperationHandler { + return &framework.PathOperation{ + Callback: f, + } +} + func (b *backend) basePath() *framework.Path { return &framework.Path{ Pattern: "lnd-nodes/?", - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ReadOperation: b.listNodes, - logical.UpdateOperation: b.createNode, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: wrapOp(b.listNodes), + logical.UpdateOperation: wrapOp(b.createNode), }, HelpSynopsis: "Create and list LND nodes", HelpDescription: ` @@ -89,8 +97,8 @@ POST - generate a new node seed and store it indexed by node pubkey func (b *backend) accountsPath() *framework.Path { return &framework.Path{ Pattern: "lnd-nodes/accounts/?", - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ReadOperation: b.listAccounts, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: wrapOp(b.listAccounts), }, HelpSynopsis: "List accounts for import into LND " + "watch-only node", @@ -114,8 +122,8 @@ only LND func (b *backend) ecdhPath() *framework.Path { return &framework.Path{ Pattern: "lnd-nodes/ecdh/?", - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.UpdateOperation: b.ecdh, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.UpdateOperation: wrapOp(b.ecdh), }, HelpSynopsis: "ECDH derived privkey with peer pubkey", HelpDescription: ` @@ -159,9 +167,9 @@ peer pubkey func (b *backend) signPath() *framework.Path { return &framework.Path{ Pattern: "lnd-nodes/sign/?", - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ReadOperation: b.derivePubKey, - logical.UpdateOperation: b.deriveAndSign, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: wrapOp(b.derivePubKey), + logical.UpdateOperation: wrapOp(b.deriveAndSign), }, HelpSynopsis: "Derive pubkeys and sign with privkeys", HelpDescription: ` From 3975ce2822b342e805c7226da952dd7c9fe59990 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Mon, 23 Jan 2023 11:30:03 -0800 Subject: [PATCH 17/42] vault: add initial plugin backend tests --- go.mod | 4 + go.sum | 6 +- vault/backend_test.go | 362 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 371 insertions(+), 1 deletion(-) create mode 100644 vault/backend_test.go diff --git a/go.mod b/go.mod index b2c63b0..c4c1b5f 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/hashicorp/vault/api v1.8.0 github.com/hashicorp/vault/sdk v0.6.0 github.com/jessevdk/go-flags v1.4.0 + github.com/stretchr/testify v1.8.0 github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 go.uber.org/zap v1.23.0 google.golang.org/grpc v1.47.0 @@ -21,6 +22,7 @@ require ( github.com/armon/go-radix v1.0.0 // indirect github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect github.com/cenkalti/backoff/v3 v3.0.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/evanphx/json-patch/v5 v5.5.0 // indirect @@ -55,6 +57,7 @@ require ( github.com/oklog/run v1.0.0 // indirect github.com/pierrec/lz4 v2.5.2+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.8.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect go.uber.org/atomic v1.9.0 // indirect @@ -66,6 +69,7 @@ require ( golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0 // indirect gopkg.in/square/go-jose.v2 v2.5.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) // This replace is for https://github.com/advisories/GHSA-w73w-5m7g-f7qc diff --git a/go.sum b/go.sum index 328f75c..e8ab065 100644 --- a/go.sum +++ b/go.sum @@ -270,15 +270,18 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 h1:tcJ6OjwOMvExLlzrAVZute09ocAGa7KqOON60++Gz4E= @@ -417,6 +420,7 @@ google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= diff --git a/vault/backend_test.go b/vault/backend_test.go new file mode 100644 index 0000000..5b44818 --- /dev/null +++ b/vault/backend_test.go @@ -0,0 +1,362 @@ +package vault + +import ( + "context" + "encoding/hex" + "os" + "testing" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/logical" + filestore "github.com/hashicorp/vault/sdk/physical/file" + "github.com/stretchr/testify/require" +) + +// testContext controls a vault plugin with storage in a temporary directory. +type testContext struct { + // Basic test context info + t *testing.T + cancel context.CancelFunc + + // tmpDir tracks where we created a temp directory to delete at the end. + tmpDir string + + // storage tracks the logical storage object for requests. + storage logical.Storage + + // Plugin back end to test against. + backEnd *backend +} + +// newTestContext creates a new test context from the test environment. +func newTestContext(t *testing.T) *testContext { + t.Helper() + + ctx, cancel := context.WithCancel(context.Background()) + + tmpDir, err := os.MkdirTemp("", "vault-plugin-lndsigner") + require.NoError(t, err) + + logger := hclog.Default() + + // Create storage in a temp directory. When we use in-memory storage, + // some request and response data is passed by reference and gets + // zeroed out inappropriately. This doesn't happen when copies are + // made by the storage backend. + pStorage, err := filestore.NewFileBackend( + map[string]string{"path": tmpDir}, + logger, + ) + require.NoError(t, err) + + storage := logical.NewLogicalStorage(pStorage) + + b, err := Factory(ctx, &logical.BackendConfig{ + StorageView: storage, + Logger: logger, + }) + require.NoError(t, err) + + return &testContext{ + t: t, + cancel: cancel, + tmpDir: tmpDir, + storage: storage, + backEnd: b.(*backend), + } +} + +// Close cancels the test context's inner context and deletes the temporary +// directory. +func (tctx *testContext) Close() { + tctx.t.Helper() + + tctx.cancel() + + require.NoError(tctx.t, os.RemoveAll(tctx.tmpDir)) +} + +// call sends a request to perform an operation to the plugin backend, and +// returns the response. +func (tctx *testContext) call(path *framework.Path, op logical.Operation, + data map[string]interface{}) (*logical.Response, error) { + + tctx.t.Helper() + + return path.Operations[op].Handler()(context.Background(), + &logical.Request{Storage: tctx.storage}, + &framework.FieldData{ + Schema: path.Fields, + Raw: data, + }, + ) +} + +// update sends an update call to the plugin backend on the specified path, and +// returns the response. +func (tctx *testContext) update(path *framework.Path, + data map[string]interface{}) (*logical.Response, error) { + + tctx.t.Helper() + + return tctx.call(path, logical.UpdateOperation, data) +} + +// read sends a read call to the plugin backend on the specified path, and +// returns the response. +func (tctx *testContext) read(path *framework.Path, + data map[string]interface{}) (*logical.Response, error) { + + tctx.t.Helper() + + return tctx.call(path, logical.ReadOperation, data) +} + +// ecdh calls the ecdh endpoint on the plugin backend, and returns the shared +// key. +func (tctx *testContext) ecdh(data map[string]interface{}) (*logical.Response, + error) { + + tctx.t.Helper() + + return tctx.update(tctx.backEnd.ecdhPath(), data) +} + +// createNode creates a node on the plugin backend with the specified network, +// and returns the node ID. +func (tctx *testContext) createNode(data map[string]interface{}) ( + *logical.Response, error) { + + tctx.t.Helper() + + return tctx.update(tctx.backEnd.basePath(), data) +} + +// listNodes lists all of the nodes stored in the plugin backend's storage and +// the network name for each. +func (tctx *testContext) listNodes() (*logical.Response, error) { + tctx.t.Helper() + + return tctx.read(tctx.backEnd.basePath(), map[string]interface{}{}) +} + +// derivePubkey requests a pubkey given a node ID and derivation path, and +// returns the derived pubkey. +func (tctx *testContext) derivePubkey(data map[string]interface{}) ( + *logical.Response, error) { + + tctx.t.Helper() + + return tctx.read(tctx.backEnd.signPath(), data) +} + +// sign requests a signature given a node ID, derivation path, algorithm, +// optional tweaks, and returns the signature and derived pubkey. +func (tctx *testContext) sign(data map[string]interface{}) ( + *logical.Response, error) { + + tctx.t.Helper() + + return tctx.update(tctx.backEnd.signPath(), data) +} + +// TestECDH tests the ECDH endpoint. It's not fully tested because we don't +// have the ability to do deterministic tests without key import. +func TestECDH(t *testing.T) { + t.Parallel() + + tctx := newTestContext(t) + defer tctx.Close() + + // Check parsing of peer pubkey. + _, err := tctx.ecdh(map[string]interface{}{ + "peer": "abcdef", + }) + require.ErrorIs(t, err, ErrInvalidPeerPubkey) + + // Check parsing of node pubkey. + _, err = tctx.ecdh(map[string]interface{}{ + "peer": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + "node": "abcdef", + }) + require.ErrorIs(t, err, ErrInvalidNodeID) + + // Check that a request for a nonexistent node returns the correct error. + _, err = tctx.ecdh(map[string]interface{}{ + "peer": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + "node": "02252bb0fdf7f6e7c055c5419c6fa1c9799cf348b480603b9c0af61dbdea29149e", + }) + require.ErrorIs(t, err, ErrNodeNotFound) + + // Create a node for more ECDH checks. + resp, err := tctx.createNode(map[string]interface{}{ + "network": "regtest", + }) + require.NoError(t, err) + + // Get the new node's pubkey. + createdNode := resp.Data["node"].(string) + require.Equal(t, 66, len(createdNode)) + + // Check that a request for the wrong pubkey returns the correct error. + _, err = tctx.ecdh(map[string]interface{}{ + "node": createdNode, + "pubkey": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25ef", + "peer": "02252bb0fdf7f6e7c055c5419c6fa1c9799cf348b480603b9c0af61dbdea29149e", + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.ErrorIs(t, err, ErrPubkeyMismatch) + +} + +// TestListNodes checks that it's possible to list all of the nodes ever +// created in storage. +func TestListNodes(t *testing.T) { + t.Parallel() + + tctx := newTestContext(t) + defer tctx.Close() + + // Check that the node list is empty. + resp, err := tctx.listNodes() + require.NoError(t, err) + require.Equal(t, resp, &logical.Response{ + Data: map[string]interface{}{}, + }) + + // Create a node. + resp, err = tctx.createNode(map[string]interface{}{ + "network": "regtest", + }) + require.NoError(t, err) + + // Get the new node's pubkey. + createdNode := resp.Data["node"].(string) + require.Equal(t, 66, len(createdNode)) + + // Check that our new node is in the list. + resp, err = tctx.listNodes() + require.NoError(t, err) + require.Equal(t, resp, &logical.Response{ + Data: map[string]interface{}{ + createdNode: "regtest", + }, + }) +} + +// TestDerivePubkey checks that public keys are derived correctly. +func TestDerivePubkey(t *testing.T) { + t.Parallel() + + tctx := newTestContext(t) + defer tctx.Close() + + // Create a node. + resp, err := tctx.createNode(map[string]interface{}{ + "network": "regtest", + }) + require.NoError(t, err) + + // Get the new node's pubkey. + createdNode := resp.Data["node"].(string) + require.Equal(t, 66, len(createdNode)) + + // Check that our new node derives its node pubkey correctly. + resp, err = tctx.derivePubkey(map[string]interface{}{ + "node": createdNode, + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.NoError(t, err) + require.Equal(t, resp, &logical.Response{ + Data: map[string]interface{}{ + "pubkey": createdNode, + }, + }) + + // Check for ErrWrongLengthDerivationPath + resp, err = tctx.derivePubkey(map[string]interface{}{ + "node": createdNode, + "path": []int{2147484665, 2147483649, 2147483654, 0, 0, 0}, + }) + require.ErrorIs(t, err, ErrWrongLengthDerivationPath) + require.Nil(t, resp) + + // Check for ErrNegativeElement + resp, err = tctx.derivePubkey(map[string]interface{}{ + "node": createdNode, + "path": []int{-1, 2147483649, 2147483654, 0, 0}, + }) + require.ErrorIs(t, err, ErrNegativeElement) + require.Nil(t, resp) + + // Check for ErrElementOverflow + resp, err = tctx.derivePubkey(map[string]interface{}{ + "node": createdNode, + "path": []int{22147484665, 2147483649, 2147483654, 0, 0}, + }) + require.ErrorIs(t, err, ErrElementOverflow) + require.Nil(t, resp) + + // Check for ErrElementNotHardened + resp, err = tctx.derivePubkey(map[string]interface{}{ + "node": createdNode, + "path": []int{1017, 2147483649, 2147483654, 0, 0}, + }) + require.ErrorIs(t, err, ErrElementNotHardened) + require.Nil(t, resp) +} + +// TestSign checks that the plugin backend signs digests properly. It's not +// yet fully tested because we don't have deterministic tests without key +// import. +func TestSign(t *testing.T) { + t.Parallel() + + tctx := newTestContext(t) + defer tctx.Close() + + // Create a node. + resp, err := tctx.createNode(map[string]interface{}{ + "network": "regtest", + }) + require.NoError(t, err) + + // Get the new node's pubkey. + createdNode := resp.Data["node"].(string) + require.Equal(t, 66, len(createdNode)) + + // Check for ErrTooManyTweaks. + resp, err = tctx.sign(map[string]interface{}{ + "node": createdNode, + "digest": "9d3d4b1c81f2554200ccc05635f01c008f1be1fe7164bf39c1dd83a6a1eec7df", + "ln1tweak": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "ln2tweak": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "method": "ecdsa", + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.ErrorIs(t, err, ErrTooManyTweaks) + require.Nil(t, resp) + + // Check for invalid hex in ln1tweak. + resp, err = tctx.sign(map[string]interface{}{ + "node": createdNode, + "digest": "9d3d4b1c81f2554200ccc05635f01c008f1be1fe7164bf39c1dd83a6a1eec7df", + "ln1tweak": "g123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "method": "ecdsa", + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.ErrorIs(t, err, hex.InvalidByteError(0x67)) + require.Nil(t, resp) + + // Check for invalid hex in ln2tweak. + resp, err = tctx.sign(map[string]interface{}{ + "node": createdNode, + "digest": "9d3d4b1c81f2554200ccc05635f01c008f1be1fe7164bf39c1dd83a6a1eec7df", + "ln2tweak": "g123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "method": "ecdsa", + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.ErrorIs(t, err, hex.InvalidByteError(0x67)) + require.Nil(t, resp) +} From 350e9c040a10139a4348e0e6f4f0a4bcb7557fb3 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Thu, 19 Jan 2023 17:01:43 -0800 Subject: [PATCH 18/42] vault: change seed len to 16 to match lnd in prep for key import --- vault/backend.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/vault/backend.go b/vault/backend.go index 3d394ac..720cbf8 100644 --- a/vault/backend.go +++ b/vault/backend.go @@ -23,6 +23,10 @@ import ( "github.com/hashicorp/vault/sdk/logical" ) +const ( + seedLen = 16 // Matches LND usage +) + type listedAccount struct { Name string `json:"name"` AddressType string `json:"address_type"` @@ -456,16 +460,16 @@ func (b *backend) getNode(ctx context.Context, storage logical.Storage, return nil, nil, ErrNodeNotFound } - if len(entry.Value) <= hdkeychain.RecommendedSeedLen { + if len(entry.Value) <= seedLen { return nil, nil, ErrInvalidSeedFromStorage } - net, err := GetNet(string(entry.Value[hdkeychain.RecommendedSeedLen:])) + net, err := GetNet(string(entry.Value[seedLen:])) if err != nil { return nil, nil, err } - return entry.Value[:hdkeychain.RecommendedSeedLen], net, nil + return entry.Value[:seedLen], net, nil } func (b *backend) listNodes(ctx context.Context, req *logical.Request, @@ -516,9 +520,7 @@ func (b *backend) createNode(ctx context.Context, req *logical.Request, err = hdkeychain.ErrUnusableSeed for err == hdkeychain.ErrUnusableSeed { - seed, err = hdkeychain.GenerateSeed( - hdkeychain.RecommendedSeedLen, - ) + seed, err = hdkeychain.GenerateSeed(seedLen) } if err != nil { b.Logger().Error("Failed to generate new LND seed", From fd313b04936d26ef90e5078d6dade09c75eb541d Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Thu, 19 Jan 2023 14:36:20 -0800 Subject: [PATCH 19/42] vault: clean up paths.go: move defs to backend.go and keys.go --- vault/backend.go | 4 ++++ vault/keys.go | 45 ++++++++++++++++++++++++++++++++++++++++++++ vault/paths.go | 49 ------------------------------------------------ 3 files changed, 49 insertions(+), 49 deletions(-) diff --git a/vault/backend.go b/vault/backend.go index 720cbf8..d3ac258 100644 --- a/vault/backend.go +++ b/vault/backend.go @@ -27,6 +27,10 @@ const ( seedLen = 16 // Matches LND usage ) +type backend struct { + *framework.Backend +} + type listedAccount struct { Name string `json:"name"` AddressType string `json:"address_type"` diff --git a/vault/keys.go b/vault/keys.go index be086c6..39fc339 100644 --- a/vault/keys.go +++ b/vault/keys.go @@ -16,6 +16,51 @@ import ( "github.com/btcsuite/btcd/chaincfg" ) +const ( + // MaxAcctID is the number of accounts/key families to create on + // initialization. + MaxAcctID = 255 + + Bip0043purpose = 1017 + NodeKeyAcct = 6 +) + +var ( + // defaultPurposes is a list of non-LN(1017) purposes for which we + // should create a m/purpose'/0'/0' account as well as their default + // address types. + defaultPurposes = []struct { + purpose uint32 + addrType string + hdVersion [2][4]byte + }{ + { + purpose: 49, + addrType: "HYBRID_NESTED_WITNESS_PUBKEY_HASH", + hdVersion: [2][4]byte{ + [4]byte{0x04, 0x9d, 0x7c, 0xb2}, // ypub + [4]byte{0x04, 0x4a, 0x52, 0x62}, // upub + }, + }, + { + purpose: 84, + addrType: "WITNESS_PUBKEY_HASH", + hdVersion: [2][4]byte{ + [4]byte{0x04, 0xb2, 0x47, 0x46}, // zpub + [4]byte{0x04, 0x5f, 0x1c, 0xf6}, // vpub + }, + }, + { + purpose: 86, + addrType: "TAPROOT_PUBKEY", + hdVersion: [2][4]byte{ + [4]byte{0x04, 0x88, 0xb2, 0x1e}, // xpub + [4]byte{0x04, 0x35, 0x87, 0xcf}, // tpub + }, + }, + } +) + func extKeyToPubBytes(key *hdkeychain.ExtendedKey) ([]byte, error) { ecPubKey, err := key.ECPubKey() if err != nil { diff --git a/vault/paths.go b/vault/paths.go index 368d3b4..fdc47bd 100644 --- a/vault/paths.go +++ b/vault/paths.go @@ -10,55 +10,6 @@ import ( "github.com/hashicorp/vault/sdk/logical" ) -const ( - // MaxAcctID is the number of accounts/key families to create on - // initialization. - MaxAcctID = 255 - - Bip0043purpose = 1017 - NodeKeyAcct = 6 -) - -var ( - // defaultPurposes is a list of non-LN(1017) purposes for which we - // should create a m/purpose'/0'/0' account as well as their default - // address types. - defaultPurposes = []struct { - purpose uint32 - addrType string - hdVersion [2][4]byte - }{ - { - purpose: 49, - addrType: "HYBRID_NESTED_WITNESS_PUBKEY_HASH", - hdVersion: [2][4]byte{ - [4]byte{0x04, 0x9d, 0x7c, 0xb2}, // ypub - [4]byte{0x04, 0x4a, 0x52, 0x62}, // upub - }, - }, - { - purpose: 84, - addrType: "WITNESS_PUBKEY_HASH", - hdVersion: [2][4]byte{ - [4]byte{0x04, 0xb2, 0x47, 0x46}, // zpub - [4]byte{0x04, 0x5f, 0x1c, 0xf6}, // vpub - }, - }, - { - purpose: 86, - addrType: "TAPROOT_PUBKEY", - hdVersion: [2][4]byte{ - [4]byte{0x04, 0x88, 0xb2, 0x1e}, // xpub - [4]byte{0x04, 0x35, 0x87, 0xcf}, // tpub - }, - }, - } -) - -type backend struct { - *framework.Backend -} - // TODO(aakselrod): expand text documentation throughout this file where // fields are available, in order to auto-generate docs. func wrapOp(f framework.OperationFunc) framework.OperationHandler { From e17f5fb3b469278632a6ae6d261fadfad80ac961 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Fri, 20 Jan 2023 11:14:50 -0800 Subject: [PATCH 20/42] vault: factor newNode out from createNode --- vault/backend.go | 43 ++++++++++++++++++++++++++++++++----------- vault/errors.go | 3 +++ 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/vault/backend.go b/vault/backend.go index d3ac258..c299d7d 100644 --- a/vault/backend.go +++ b/vault/backend.go @@ -513,16 +513,11 @@ func (b *backend) createNode(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { strNet := data.Get("network").(string) - net, err := GetNet(strNet) - if err != nil { - b.Logger().Error("Failed to parse network", "error", err) - return nil, err - } var seed []byte defer zero(seed) - err = hdkeychain.ErrUnusableSeed + err := hdkeychain.ErrUnusableSeed for err == hdkeychain.ErrUnusableSeed { seed, err = hdkeychain.GenerateSeed(seedLen) } @@ -532,6 +527,19 @@ func (b *backend) createNode(ctx context.Context, req *logical.Request, return nil, err } + return b.newNode(ctx, req.Storage, seed, strNet, "") +} + +func (b *backend) newNode(ctx context.Context, storage logical.Storage, + seed []byte, strNet, reqKey string) (*logical.Response, error) { + + net, err := GetNet(strNet) + if err != nil { + b.Logger().Error("Failed to parse network", "error", err, + "network", strNet) + return nil, err + } + nodePubKey, err := derivePubKey(seed, net, []int{ int(Bip0043purpose + hdkeychain.HardenedKeyStart), int(net.HDCoinType + hdkeychain.HardenedKeyStart), @@ -547,27 +555,40 @@ func (b *backend) createNode(ctx context.Context, req *logical.Request, pubKeyBytes, err := extKeyToPubBytes(nodePubKey) if err != nil { - b.Logger().Error("createNode: Failed to get pubkey bytes", + b.Logger().Error("newNode: Failed to get pubkey bytes", "error", err) return nil, err } strPubKey := hex.EncodeToString(pubKeyBytes) + + if reqKey != "" && strPubKey != reqKey { + b.Logger().Error("newNode: node pubkey mismatch") + return nil, ErrNodePubkeyMismatch + } + nodePath := "lnd-nodes/" + strPubKey + obj, err := storage.Get(ctx, nodePath) + if err == nil && obj != nil { + b.Logger().Error("newNode: node already exists", + "node", strPubKey) + return nil, ErrNodeAlreadyExists + } + seed = append(seed, []byte(strNet)...) - err = req.Storage.Put(ctx, &logical.StorageEntry{ + err = storage.Put(ctx, &logical.StorageEntry{ Key: nodePath, Value: seed, SealWrap: true, }) if err != nil { b.Logger().Error("Failed to save seed for node", - "error", err) + "node", strPubKey, "error", err) return nil, err } - b.Logger().Info("Wrote new LND node seed", "pubkey", strPubKey) + b.Logger().Info("Wrote new LND seed", "node", strPubKey) return &logical.Response{ Data: map[string]interface{}{ @@ -594,7 +615,7 @@ func GetNet(strNet string) (*chaincfg.Params, error) { return &chaincfg.RegressionNetParams, nil default: - return nil, errors.New("invalid network specified: " + strNet) + return nil, ErrInvalidNetwork } } diff --git a/vault/errors.go b/vault/errors.go index 9c11679..8085728 100644 --- a/vault/errors.go +++ b/vault/errors.go @@ -5,6 +5,9 @@ import ( ) var ( + ErrNodeAlreadyExists = errors.New("node already exists") + ErrNodePubkeyMismatch = errors.New("node pubkey mismatch") + ErrInvalidNetwork = errors.New("invalid network") ErrInvalidPeerPubkey = errors.New("invalid peer pubkey") ErrInvalidNodeID = errors.New("invalid node id") ErrNodeNotFound = errors.New("node not found") From 4ebc3517892a6cbdc83432c053ac1e33d87df4ea Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Tue, 24 Jan 2023 14:29:44 -0800 Subject: [PATCH 21/42] vault: implement key import --- README.md | 28 +- go.mod | 5 +- go.sum | 6 + vault/aezeed.go | 2154 +++++++++++++++++++++++++++++++++++++++++ vault/backend.go | 24 +- vault/backend_test.go | 336 ++++++- vault/errors.go | 7 + vault/paths.go | 44 + 8 files changed, 2591 insertions(+), 13 deletions(-) create mode 100644 vault/aezeed.go diff --git a/README.md b/README.md index 7f9fa90..7e0594b 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,22 @@ # lndsigner `lndsigner` is a [remote signer](https://github.com/lightningnetwork/lnd/blob/master/docs/remote-signing.md) for [lnd](https://github.com/lightningnetwork/lnd). Currently, it can do the following: - [x] store seeds for multiple nodes in [Hashicorp Vault](https://github.com/hashicorp/vault/) +- [x] securely generate new node seeds in vault +- [x] import seed/pass phrases +- [x] run unit tests - [x] perform derivation and signing operations in a Vault plugin -- [x] export account list for watch-only lnd instance on startup +- [x] export account list as JSON from vault - [x] sign messages for network announcements - [x] derive shared keys for peer connections - [x] sign PSBTs for on-chain transactions, channel openings/closes, HTLC updates, etc. +- [ ] run itests +- [ ] do automated builds +- [ ] do reproducible builds - [ ] perform musig2 ops - [ ] track on-chain wallet state and enforce policy for on-chain transactions - [ ] track channel state and enforce policy for channel updates - [ ] allow preauthorizations for on-chain transactions, channel opens/closes, and channel updates - [ ] allow an interceptor to determine whether or not to sign -- [ ] run unit tests and itests, do automated/reproducible builds - [ ] log and gather metrics coherently - [ ] enforce custom SELinux policy to harden plugin execution environment @@ -135,3 +140,22 @@ Create the watch-only wallet using the accounts exported by the signer: ``` Now you can use your node as usual. Note that MuSig2 isn't supported yet. If you created multiple nodes in the vault, you can create a separate directory for each signer instance (`.lndsigner`) and each watch-only node (`.lnd`) and start each as above. + +You can also import a seedphrase, optionally protected by a passphrase, into the vault if you have a backup from an existing LND installation: +``` +~$ vault write lndsigner/lnd-nodes/import \ + seedphrase="abstract inch live custom just tray hockey enroll upon friend mass author filter desert parrot network finger uniform alley artefact path palace chicken diet" \ + passphrase=weks1234 \ + network=regtest \ + node=03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf +``` + +Note that the `node` parameter is optional and used to check that the correct node pubkey is derived from the seed and network passed to the vault. You should get output like this if the command succeeds: + +``` +Key Value +--- ----- +node 03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf +``` + +Now you can use the imported key as before. diff --git a/go.mod b/go.mod index c4c1b5f..6930d44 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,7 @@ module github.com/bottlepay/lndsigner require ( + github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 github.com/btcsuite/btcd v0.23.1 github.com/btcsuite/btcd/btcec/v2 v2.2.1 github.com/btcsuite/btcd/btcutil v1.1.2 @@ -10,9 +11,11 @@ require ( github.com/hashicorp/vault/api v1.8.0 github.com/hashicorp/vault/sdk v0.6.0 github.com/jessevdk/go-flags v1.4.0 + github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 github.com/stretchr/testify v1.8.0 github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 go.uber.org/zap v1.23.0 + golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd google.golang.org/grpc v1.47.0 google.golang.org/protobuf v1.28.0 ) @@ -60,9 +63,9 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.8.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect + gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.6.0 // indirect - golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect golang.org/x/text v0.3.7 // indirect diff --git a/go.sum b/go.sum index e8ab065..841c6fd 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok= +github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -187,6 +189,7 @@ github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 h1:FOOIBWrEkLgmlgGfMuZT83xIwfPDxEI2OHu6xUmJMFE= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -288,6 +291,8 @@ github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 h1:tcJ6OjwOMvExLlzrAV github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S068ZqfrO6S8HsoJq2bF3ETfTL+kt4tInY= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo= +gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= @@ -345,6 +350,7 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/vault/aezeed.go b/vault/aezeed.go new file mode 100644 index 0000000..8ee0361 --- /dev/null +++ b/vault/aezeed.go @@ -0,0 +1,2154 @@ +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package vault + +import ( + "encoding/binary" + "hash/crc32" + "strings" + + "github.com/Yawning/aez" + "github.com/kkdai/bstream" + "golang.org/x/crypto/scrypt" +) + +var ( + // reverseWordMap maps a word to its position within the default word list. + reverseWordMap map[string]int +) + +func seedFromSeedAndPassPhrases(seedPhrase, passPhrase string) ([]byte, error) { + if passPhrase == "" { + passPhrase = "aezeed" + } + + mnemonic := strings.Split( + strings.ToLower(strings.TrimSpace(seedPhrase)), " ", + ) + + if len(mnemonic) != 24 { + return nil, ErrSeedPhraseWrongLength + } + + cipherBits := bstream.NewBStreamWriter(33) + + for _, word := range mnemonic { + idx, ok := reverseWordMap[word] + if !ok { + return nil, ErrSeedPhraseNotBIP39 + } + + cipherBits.WriteBits(uint64(idx), 11) + } + + cipherText := cipherBits.Bytes() + + if cipherText[0] != byte(0) { + return nil, ErrBadCipherSeedVer + } + + salt := cipherText[24:29] + + checksum := cipherText[29:] + if len(checksum) != 4 { + return nil, ErrWrongLengthChecksum + } + + freshChecksum := crc32.Checksum( + cipherText[:29], crc32.MakeTable(crc32.Castagnoli), + ) + if freshChecksum != binary.BigEndian.Uint32(checksum) { + return nil, ErrChecksumMismatch + } + + cipherSeed := cipherText[1:24] + + key, err := scrypt.Key([]byte(passPhrase), salt, 32768, 8, 1, 32) + if err != nil { + return nil, err + } + + ad := make([]byte, 6) + ad[0] = cipherText[0] + copy(ad[1:], salt) + + plainSeedBytes, ok := aez.Decrypt( + key, nil, [][]byte{ad[:]}, 4, cipherSeed, nil, + ) + if !ok { + return nil, ErrInvalidPassphrase + } + + if plainSeedBytes[0] != byte(1) && plainSeedBytes[0] != byte(0) { + return nil, ErrWrongInternalVersion + } + + entropy := make([]byte, 16) + copy(entropy[:], plainSeedBytes[3:]) + + return entropy, nil +} + +func init() { + reverseWordMap = make(map[string]int) + for i, v := range defaultWordList { + reverseWordMap[v] = i + } +} + +// defaultWordList is a slice of the current default word list that's used to +// encode the enciphered seed into a human readable set of words. +var defaultWordList = strings.Split(englishWordList, "\n") + +// englishWordList is an English wordlist that's used as part of version 0 of +// the cipherseed scheme. This is the *same* word list that's recommend for use +// with BIP0039. +var englishWordList = `abandon +ability +able +about +above +absent +absorb +abstract +absurd +abuse +access +accident +account +accuse +achieve +acid +acoustic +acquire +across +act +action +actor +actress +actual +adapt +add +addict +address +adjust +admit +adult +advance +advice +aerobic +affair +afford +afraid +again +age +agent +agree +ahead +aim +air +airport +aisle +alarm +album +alcohol +alert +alien +all +alley +allow +almost +alone +alpha +already +also +alter +always +amateur +amazing +among +amount +amused +analyst +anchor +ancient +anger +angle +angry +animal +ankle +announce +annual +another +answer +antenna +antique +anxiety +any +apart +apology +appear +apple +approve +april +arch +arctic +area +arena +argue +arm +armed +armor +army +around +arrange +arrest +arrive +arrow +art +artefact +artist +artwork +ask +aspect +assault +asset +assist +assume +asthma +athlete +atom +attack +attend +attitude +attract +auction +audit +august +aunt +author +auto +autumn +average +avocado +avoid +awake +aware +away +awesome +awful +awkward +axis +baby +bachelor +bacon +badge +bag +balance +balcony +ball +bamboo +banana +banner +bar +barely +bargain +barrel +base +basic +basket +battle +beach +bean +beauty +because +become +beef +before +begin +behave +behind +believe +below +belt +bench +benefit +best +betray +better +between +beyond +bicycle +bid +bike +bind +biology +bird +birth +bitter +black +blade +blame +blanket +blast +bleak +bless +blind +blood +blossom +blouse +blue +blur +blush +board +boat +body +boil +bomb +bone +bonus +book +boost +border +boring +borrow +boss +bottom +bounce +box +boy +bracket +brain +brand +brass +brave +bread +breeze +brick +bridge +brief +bright +bring +brisk +broccoli +broken +bronze +broom +brother +brown +brush +bubble +buddy +budget +buffalo +build +bulb +bulk +bullet +bundle +bunker +burden +burger +burst +bus +business +busy +butter +buyer +buzz +cabbage +cabin +cable +cactus +cage +cake +call +calm +camera +camp +can +canal +cancel +candy +cannon +canoe +canvas +canyon +capable +capital +captain +car +carbon +card +cargo +carpet +carry +cart +case +cash +casino +castle +casual +cat +catalog +catch +category +cattle +caught +cause +caution +cave +ceiling +celery +cement +census +century +cereal +certain +chair +chalk +champion +change +chaos +chapter +charge +chase +chat +cheap +check +cheese +chef +cherry +chest +chicken +chief +child +chimney +choice +choose +chronic +chuckle +chunk +churn +cigar +cinnamon +circle +citizen +city +civil +claim +clap +clarify +claw +clay +clean +clerk +clever +click +client +cliff +climb +clinic +clip +clock +clog +close +cloth +cloud +clown +club +clump +cluster +clutch +coach +coast +coconut +code +coffee +coil +coin +collect +color +column +combine +come +comfort +comic +common +company +concert +conduct +confirm +congress +connect +consider +control +convince +cook +cool +copper +copy +coral +core +corn +correct +cost +cotton +couch +country +couple +course +cousin +cover +coyote +crack +cradle +craft +cram +crane +crash +crater +crawl +crazy +cream +credit +creek +crew +cricket +crime +crisp +critic +crop +cross +crouch +crowd +crucial +cruel +cruise +crumble +crunch +crush +cry +crystal +cube +culture +cup +cupboard +curious +current +curtain +curve +cushion +custom +cute +cycle +dad +damage +damp +dance +danger +daring +dash +daughter +dawn +day +deal +debate +debris +decade +december +decide +decline +decorate +decrease +deer +defense +define +defy +degree +delay +deliver +demand +demise +denial +dentist +deny +depart +depend +deposit +depth +deputy +derive +describe +desert +design +desk +despair +destroy +detail +detect +develop +device +devote +diagram +dial +diamond +diary +dice +diesel +diet +differ +digital +dignity +dilemma +dinner +dinosaur +direct +dirt +disagree +discover +disease +dish +dismiss +disorder +display +distance +divert +divide +divorce +dizzy +doctor +document +dog +doll +dolphin +domain +donate +donkey +donor +door +dose +double +dove +draft +dragon +drama +drastic +draw +dream +dress +drift +drill +drink +drip +drive +drop +drum +dry +duck +dumb +dune +during +dust +dutch +duty +dwarf +dynamic +eager +eagle +early +earn +earth +easily +east +easy +echo +ecology +economy +edge +edit +educate +effort +egg +eight +either +elbow +elder +electric +elegant +element +elephant +elevator +elite +else +embark +embody +embrace +emerge +emotion +employ +empower +empty +enable +enact +end +endless +endorse +enemy +energy +enforce +engage +engine +enhance +enjoy +enlist +enough +enrich +enroll +ensure +enter +entire +entry +envelope +episode +equal +equip +era +erase +erode +erosion +error +erupt +escape +essay +essence +estate +eternal +ethics +evidence +evil +evoke +evolve +exact +example +excess +exchange +excite +exclude +excuse +execute +exercise +exhaust +exhibit +exile +exist +exit +exotic +expand +expect +expire +explain +expose +express +extend +extra +eye +eyebrow +fabric +face +faculty +fade +faint +faith +fall +false +fame +family +famous +fan +fancy +fantasy +farm +fashion +fat +fatal +father +fatigue +fault +favorite +feature +february +federal +fee +feed +feel +female +fence +festival +fetch +fever +few +fiber +fiction +field +figure +file +film +filter +final +find +fine +finger +finish +fire +firm +first +fiscal +fish +fit +fitness +fix +flag +flame +flash +flat +flavor +flee +flight +flip +float +flock +floor +flower +fluid +flush +fly +foam +focus +fog +foil +fold +follow +food +foot +force +forest +forget +fork +fortune +forum +forward +fossil +foster +found +fox +fragile +frame +frequent +fresh +friend +fringe +frog +front +frost +frown +frozen +fruit +fuel +fun +funny +furnace +fury +future +gadget +gain +galaxy +gallery +game +gap +garage +garbage +garden +garlic +garment +gas +gasp +gate +gather +gauge +gaze +general +genius +genre +gentle +genuine +gesture +ghost +giant +gift +giggle +ginger +giraffe +girl +give +glad +glance +glare +glass +glide +glimpse +globe +gloom +glory +glove +glow +glue +goat +goddess +gold +good +goose +gorilla +gospel +gossip +govern +gown +grab +grace +grain +grant +grape +grass +gravity +great +green +grid +grief +grit +grocery +group +grow +grunt +guard +guess +guide +guilt +guitar +gun +gym +habit +hair +half +hammer +hamster +hand +happy +harbor +hard +harsh +harvest +hat +have +hawk +hazard +head +health +heart +heavy +hedgehog +height +hello +helmet +help +hen +hero +hidden +high +hill +hint +hip +hire +history +hobby +hockey +hold +hole +holiday +hollow +home +honey +hood +hope +horn +horror +horse +hospital +host +hotel +hour +hover +hub +huge +human +humble +humor +hundred +hungry +hunt +hurdle +hurry +hurt +husband +hybrid +ice +icon +idea +identify +idle +ignore +ill +illegal +illness +image +imitate +immense +immune +impact +impose +improve +impulse +inch +include +income +increase +index +indicate +indoor +industry +infant +inflict +inform +inhale +inherit +initial +inject +injury +inmate +inner +innocent +input +inquiry +insane +insect +inside +inspire +install +intact +interest +into +invest +invite +involve +iron +island +isolate +issue +item +ivory +jacket +jaguar +jar +jazz +jealous +jeans +jelly +jewel +job +join +joke +journey +joy +judge +juice +jump +jungle +junior +junk +just +kangaroo +keen +keep +ketchup +key +kick +kid +kidney +kind +kingdom +kiss +kit +kitchen +kite +kitten +kiwi +knee +knife +knock +know +lab +label +labor +ladder +lady +lake +lamp +language +laptop +large +later +latin +laugh +laundry +lava +law +lawn +lawsuit +layer +lazy +leader +leaf +learn +leave +lecture +left +leg +legal +legend +leisure +lemon +lend +length +lens +leopard +lesson +letter +level +liar +liberty +library +license +life +lift +light +like +limb +limit +link +lion +liquid +list +little +live +lizard +load +loan +lobster +local +lock +logic +lonely +long +loop +lottery +loud +lounge +love +loyal +lucky +luggage +lumber +lunar +lunch +luxury +lyrics +machine +mad +magic +magnet +maid +mail +main +major +make +mammal +man +manage +mandate +mango +mansion +manual +maple +marble +march +margin +marine +market +marriage +mask +mass +master +match +material +math +matrix +matter +maximum +maze +meadow +mean +measure +meat +mechanic +medal +media +melody +melt +member +memory +mention +menu +mercy +merge +merit +merry +mesh +message +metal +method +middle +midnight +milk +million +mimic +mind +minimum +minor +minute +miracle +mirror +misery +miss +mistake +mix +mixed +mixture +mobile +model +modify +mom +moment +monitor +monkey +monster +month +moon +moral +more +morning +mosquito +mother +motion +motor +mountain +mouse +move +movie +much +muffin +mule +multiply +muscle +museum +mushroom +music +must +mutual +myself +mystery +myth +naive +name +napkin +narrow +nasty +nation +nature +near +neck +need +negative +neglect +neither +nephew +nerve +nest +net +network +neutral +never +news +next +nice +night +noble +noise +nominee +noodle +normal +north +nose +notable +note +nothing +notice +novel +now +nuclear +number +nurse +nut +oak +obey +object +oblige +obscure +observe +obtain +obvious +occur +ocean +october +odor +off +offer +office +often +oil +okay +old +olive +olympic +omit +once +one +onion +online +only +open +opera +opinion +oppose +option +orange +orbit +orchard +order +ordinary +organ +orient +original +orphan +ostrich +other +outdoor +outer +output +outside +oval +oven +over +own +owner +oxygen +oyster +ozone +pact +paddle +page +pair +palace +palm +panda +panel +panic +panther +paper +parade +parent +park +parrot +party +pass +patch +path +patient +patrol +pattern +pause +pave +payment +peace +peanut +pear +peasant +pelican +pen +penalty +pencil +people +pepper +perfect +permit +person +pet +phone +photo +phrase +physical +piano +picnic +picture +piece +pig +pigeon +pill +pilot +pink +pioneer +pipe +pistol +pitch +pizza +place +planet +plastic +plate +play +please +pledge +pluck +plug +plunge +poem +poet +point +polar +pole +police +pond +pony +pool +popular +portion +position +possible +post +potato +pottery +poverty +powder +power +practice +praise +predict +prefer +prepare +present +pretty +prevent +price +pride +primary +print +priority +prison +private +prize +problem +process +produce +profit +program +project +promote +proof +property +prosper +protect +proud +provide +public +pudding +pull +pulp +pulse +pumpkin +punch +pupil +puppy +purchase +purity +purpose +purse +push +put +puzzle +pyramid +quality +quantum +quarter +question +quick +quit +quiz +quote +rabbit +raccoon +race +rack +radar +radio +rail +rain +raise +rally +ramp +ranch +random +range +rapid +rare +rate +rather +raven +raw +razor +ready +real +reason +rebel +rebuild +recall +receive +recipe +record +recycle +reduce +reflect +reform +refuse +region +regret +regular +reject +relax +release +relief +rely +remain +remember +remind +remove +render +renew +rent +reopen +repair +repeat +replace +report +require +rescue +resemble +resist +resource +response +result +retire +retreat +return +reunion +reveal +review +reward +rhythm +rib +ribbon +rice +rich +ride +ridge +rifle +right +rigid +ring +riot +ripple +risk +ritual +rival +river +road +roast +robot +robust +rocket +romance +roof +rookie +room +rose +rotate +rough +round +route +royal +rubber +rude +rug +rule +run +runway +rural +sad +saddle +sadness +safe +sail +salad +salmon +salon +salt +salute +same +sample +sand +satisfy +satoshi +sauce +sausage +save +say +scale +scan +scare +scatter +scene +scheme +school +science +scissors +scorpion +scout +scrap +screen +script +scrub +sea +search +season +seat +second +secret +section +security +seed +seek +segment +select +sell +seminar +senior +sense +sentence +series +service +session +settle +setup +seven +shadow +shaft +shallow +share +shed +shell +sheriff +shield +shift +shine +ship +shiver +shock +shoe +shoot +shop +short +shoulder +shove +shrimp +shrug +shuffle +shy +sibling +sick +side +siege +sight +sign +silent +silk +silly +silver +similar +simple +since +sing +siren +sister +situate +six +size +skate +sketch +ski +skill +skin +skirt +skull +slab +slam +sleep +slender +slice +slide +slight +slim +slogan +slot +slow +slush +small +smart +smile +smoke +smooth +snack +snake +snap +sniff +snow +soap +soccer +social +sock +soda +soft +solar +soldier +solid +solution +solve +someone +song +soon +sorry +sort +soul +sound +soup +source +south +space +spare +spatial +spawn +speak +special +speed +spell +spend +sphere +spice +spider +spike +spin +spirit +split +spoil +sponsor +spoon +sport +spot +spray +spread +spring +spy +square +squeeze +squirrel +stable +stadium +staff +stage +stairs +stamp +stand +start +state +stay +steak +steel +stem +step +stereo +stick +still +sting +stock +stomach +stone +stool +story +stove +strategy +street +strike +strong +struggle +student +stuff +stumble +style +subject +submit +subway +success +such +sudden +suffer +sugar +suggest +suit +summer +sun +sunny +sunset +super +supply +supreme +sure +surface +surge +surprise +surround +survey +suspect +sustain +swallow +swamp +swap +swarm +swear +sweet +swift +swim +swing +switch +sword +symbol +symptom +syrup +system +table +tackle +tag +tail +talent +talk +tank +tape +target +task +taste +tattoo +taxi +teach +team +tell +ten +tenant +tennis +tent +term +test +text +thank +that +theme +then +theory +there +they +thing +this +thought +three +thrive +throw +thumb +thunder +ticket +tide +tiger +tilt +timber +time +tiny +tip +tired +tissue +title +toast +tobacco +today +toddler +toe +together +toilet +token +tomato +tomorrow +tone +tongue +tonight +tool +tooth +top +topic +topple +torch +tornado +tortoise +toss +total +tourist +toward +tower +town +toy +track +trade +traffic +tragic +train +transfer +trap +trash +travel +tray +treat +tree +trend +trial +tribe +trick +trigger +trim +trip +trophy +trouble +truck +true +truly +trumpet +trust +truth +try +tube +tuition +tumble +tuna +tunnel +turkey +turn +turtle +twelve +twenty +twice +twin +twist +two +type +typical +ugly +umbrella +unable +unaware +uncle +uncover +under +undo +unfair +unfold +unhappy +uniform +unique +unit +universe +unknown +unlock +until +unusual +unveil +update +upgrade +uphold +upon +upper +upset +urban +urge +usage +use +used +useful +useless +usual +utility +vacant +vacuum +vague +valid +valley +valve +van +vanish +vapor +various +vast +vault +vehicle +velvet +vendor +venture +venue +verb +verify +version +very +vessel +veteran +viable +vibrant +vicious +victory +video +view +village +vintage +violin +virtual +virus +visa +visit +visual +vital +vivid +vocal +voice +void +volcano +volume +vote +voyage +wage +wagon +wait +walk +wall +walnut +want +warfare +warm +warrior +wash +wasp +waste +water +wave +way +wealth +weapon +wear +weasel +weather +web +wedding +weekend +weird +welcome +west +wet +whale +what +wheat +wheel +when +where +whip +whisper +wide +width +wife +wild +will +win +window +wine +wing +wink +winner +winter +wire +wisdom +wise +wish +witness +wolf +woman +wonder +wood +wool +word +work +world +worry +worth +wrap +wreck +wrestle +wrist +write +wrong +yard +year +yellow +you +young +youth +zebra +zero +zone +zoo` diff --git a/vault/backend.go b/vault/backend.go index c299d7d..485e79e 100644 --- a/vault/backend.go +++ b/vault/backend.go @@ -509,6 +509,25 @@ func (b *backend) listNodes(ctx context.Context, req *logical.Request, }, nil } +func (b *backend) importNode(ctx context.Context, req *logical.Request, + data *framework.FieldData) (*logical.Response, error) { + + strNode := data.Get("node").(string) + strNet := data.Get("network").(string) + + seed, err := seedFromSeedAndPassPhrases( + data.Get("seedphrase").(string), + data.Get("passphrase").(string), + ) + if err != nil { + b.Logger().Error("Failed to get seed from seed and "+ + "pass phrases", "error", err) + return nil, err + } + + return b.newNode(ctx, req.Storage, seed, strNet, strNode) +} + func (b *backend) createNode(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { @@ -570,7 +589,10 @@ func (b *backend) newNode(ctx context.Context, storage logical.Storage, nodePath := "lnd-nodes/" + strPubKey obj, err := storage.Get(ctx, nodePath) - if err == nil && obj != nil { + if err != nil { + return nil, err + } + if obj != nil { b.Logger().Error("newNode: node already exists", "node", strPubKey) return nil, ErrNodeAlreadyExists diff --git a/vault/backend_test.go b/vault/backend_test.go index 5b44818..b7e331a 100644 --- a/vault/backend_test.go +++ b/vault/backend_test.go @@ -2,6 +2,7 @@ package vault import ( "context" + "crypto/sha256" "encoding/hex" "os" "testing" @@ -133,6 +134,26 @@ func (tctx *testContext) createNode(data map[string]interface{}) ( return tctx.update(tctx.backEnd.basePath(), data) } +// importNode imports a node to the plugin backend given the specified network, +// seed phrase, and optional passphrase, and returns the node ID. +func (tctx *testContext) importNode(data map[string]interface{}) ( + *logical.Response, error) { + + tctx.t.Helper() + + return tctx.update(tctx.backEnd.importPath(), data) +} + +// listAccounts returns a JSON account list compatible with lnd's +// `lncli createwatchonly` command given a node ID. +func (tctx *testContext) listAccounts(data map[string]interface{}) ( + *logical.Response, error) { + + tctx.t.Helper() + + return tctx.read(tctx.backEnd.accountsPath(), data) +} + // listNodes lists all of the nodes stored in the plugin backend's storage and // the network name for each. func (tctx *testContext) listNodes() (*logical.Response, error) { @@ -208,6 +229,31 @@ func TestECDH(t *testing.T) { }) require.ErrorIs(t, err, ErrPubkeyMismatch) + // Import with passphrase. + resp, err = tctx.importNode(map[string]interface{}{ + "network": "regtest", + "seedphrase": "abstract inch live custom just tray hockey enroll upon friend mass author filter desert parrot network finger uniform alley artefact path palace chicken diet", + "passphrase": "weks1234", + "node": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + }) + require.NoError(t, err) + require.Equal(t, + resp.Data["node"].(string), + "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + ) + + // Check correct derivation of shared key. + resp, err = tctx.ecdh(map[string]interface{}{ + "node": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + "pubkey": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + "peer": "02252bb0fdf7f6e7c055c5419c6fa1c9799cf348b480603b9c0af61dbdea29149e", + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.NoError(t, err) + require.Equal(t, + resp.Data["sharedkey"].(string), + "7895c217d4f1a33265c0122ce66dd16bcd0b86976198f1128e6dbaef86a2f327", + ) } // TestListNodes checks that it's possible to list all of the nodes ever @@ -243,6 +289,43 @@ func TestListNodes(t *testing.T) { createdNode: "regtest", }, }) + + // Import without passphrase. + resp, err = tctx.importNode(map[string]interface{}{ + "network": "regtest", + "seedphrase": "absent walnut slam olive squeeze cluster blame express asthma gym force warfare physical stuff unusual tiny endless patient again sound deny identify fall guard", + "node": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + }) + require.NoError(t, err) + require.Equal(t, + resp.Data["node"].(string), + "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + ) + + // Import with passphrase. + resp, err = tctx.importNode(map[string]interface{}{ + "network": "regtest", + "seedphrase": "abstract inch live custom just tray hockey enroll upon friend mass author filter desert parrot network finger uniform alley artefact path palace chicken diet", + "passphrase": "weks1234", + "node": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + }) + require.NoError(t, err) + require.Equal(t, + resp.Data["node"].(string), + "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + ) + + // Check that all our nodes are in the list. + resp, err = tctx.listNodes() + require.NoError(t, err) + require.Equal(t, resp, &logical.Response{ + Data: map[string]interface{}{ + createdNode: "regtest", + "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf": "regtest", + "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6": "regtest", + }, + }) + } // TestDerivePubkey checks that public keys are derived correctly. @@ -268,13 +351,9 @@ func TestDerivePubkey(t *testing.T) { "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, }) require.NoError(t, err) - require.Equal(t, resp, &logical.Response{ - Data: map[string]interface{}{ - "pubkey": createdNode, - }, - }) + require.Equal(t, resp.Data["pubkey"].(string), createdNode) - // Check for ErrWrongLengthDerivationPath + // Check for ErrWrongLengthDerivationPath. resp, err = tctx.derivePubkey(map[string]interface{}{ "node": createdNode, "path": []int{2147484665, 2147483649, 2147483654, 0, 0, 0}, @@ -282,7 +361,7 @@ func TestDerivePubkey(t *testing.T) { require.ErrorIs(t, err, ErrWrongLengthDerivationPath) require.Nil(t, resp) - // Check for ErrNegativeElement + // Check for ErrNegativeElement. resp, err = tctx.derivePubkey(map[string]interface{}{ "node": createdNode, "path": []int{-1, 2147483649, 2147483654, 0, 0}, @@ -290,7 +369,7 @@ func TestDerivePubkey(t *testing.T) { require.ErrorIs(t, err, ErrNegativeElement) require.Nil(t, resp) - // Check for ErrElementOverflow + // Check for ErrElementOverflow. resp, err = tctx.derivePubkey(map[string]interface{}{ "node": createdNode, "path": []int{22147484665, 2147483649, 2147483654, 0, 0}, @@ -298,7 +377,7 @@ func TestDerivePubkey(t *testing.T) { require.ErrorIs(t, err, ErrElementOverflow) require.Nil(t, resp) - // Check for ErrElementNotHardened + // Check for ErrElementNotHardened. resp, err = tctx.derivePubkey(map[string]interface{}{ "node": createdNode, "path": []int{1017, 2147483649, 2147483654, 0, 0}, @@ -359,4 +438,243 @@ func TestSign(t *testing.T) { }) require.ErrorIs(t, err, hex.InvalidByteError(0x67)) require.Nil(t, resp) + + // Import without passphrase. + resp, err = tctx.importNode(map[string]interface{}{ + "network": "regtest", + "seedphrase": "absent walnut slam olive squeeze cluster blame express asthma gym force warfare physical stuff unusual tiny endless patient again sound deny identify fall guard", + "node": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + }) + require.NoError(t, err) + require.Equal(t, resp, &logical.Response{Data: map[string]interface{}{ + "node": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + }}) + + // Sign ECDSA. + resp, err = tctx.sign(map[string]interface{}{ + "node": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "digest": "9d3d4b1c81f2554200ccc05635f01c008f1be1fe7164bf39c1dd83a6a1eec7df", + "method": "ecdsa", + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.NoError(t, err) + require.Equal(t, resp, &logical.Response{Data: map[string]interface{}{ + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "signature": "3045022100d5e9e57012d5bcce055e17a1b467a7b00c9c29e33bcca2aaa23a991452f3d10b0220219595c988f0e3c3acccb4ccdd856c662a9f462ae02d82243306fb0f316ea872", + }}) + + // Sign ECDSA ignoring taptweak. + resp, err = tctx.sign(map[string]interface{}{ + "node": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "digest": "9d3d4b1c81f2554200ccc05635f01c008f1be1fe7164bf39c1dd83a6a1eec7df", + "taptweak": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "method": "ecdsa", + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.NoError(t, err) + require.Equal(t, resp, &logical.Response{Data: map[string]interface{}{ + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "signature": "3045022100d5e9e57012d5bcce055e17a1b467a7b00c9c29e33bcca2aaa23a991452f3d10b0220219595c988f0e3c3acccb4ccdd856c662a9f462ae02d82243306fb0f316ea872", + }}) + + // Sign ECDSA with single tweak. + resp, err = tctx.sign(map[string]interface{}{ + "node": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "digest": "9d3d4b1c81f2554200ccc05635f01c008f1be1fe7164bf39c1dd83a6a1eec7df", + "ln1tweak": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "method": "ecdsa", + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.NoError(t, err) + require.Equal(t, resp, &logical.Response{Data: map[string]interface{}{ + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "signature": "3045022100ac182be53be9ce5a94565bf21fffd640e56dc10631fbe6f7d75e1ef03f7e23ff022010f917056b002695f33281c6f569de0e2934be966f7d9c36669d93a56530ca9b", + }}) + + // Sign ECDSA with double tweak. + resp, err = tctx.sign(map[string]interface{}{ + "node": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "digest": "9d3d4b1c81f2554200ccc05635f01c008f1be1fe7164bf39c1dd83a6a1eec7df", + "ln2tweak": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "method": "ecdsa", + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.NoError(t, err) + require.Equal(t, resp, &logical.Response{Data: map[string]interface{}{ + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "signature": "3044022067e3a4a3b40592e10dc08e8b585e1b2a00c3f3e906f8d7959642102ebc977d4302202527213f7f795e2d45849c8a147cf39ed8f6246141c6f092f51b0bde53eb3d49", + }}) + + // Sign ECDSA compact. + resp, err = tctx.sign(map[string]interface{}{ + "node": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "digest": "9d3d4b1c81f2554200ccc05635f01c008f1be1fe7164bf39c1dd83a6a1eec7df", + "method": "ecdsa-compact", + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.NoError(t, err) + require.Equal(t, resp, &logical.Response{Data: map[string]interface{}{ + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "signature": "20d5e9e57012d5bcce055e17a1b467a7b00c9c29e33bcca2aaa23a991452f3d10b219595c988f0e3c3acccb4ccdd856c662a9f462ae02d82243306fb0f316ea872", + }}) + + // Sign Schnorr. + resp, err = tctx.sign(map[string]interface{}{ + "node": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "digest": "9d3d4b1c81f2554200ccc05635f01c008f1be1fe7164bf39c1dd83a6a1eec7df", + "method": "schnorr", + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.NoError(t, err) + require.Equal(t, resp, &logical.Response{Data: map[string]interface{}{ + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "signature": "71b77d9c8a0badfa7c4eca3fbef5da2a552bf032f56b85fbc5c2f3500498fc20d5ab8505ae9733b1b756da7a5dba41dbe069dd0d86793618829c3077df0cd759", + }}) + + // Sign Schnorr with taptweak. + resp, err = tctx.sign(map[string]interface{}{ + "node": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "digest": "9d3d4b1c81f2554200ccc05635f01c008f1be1fe7164bf39c1dd83a6a1eec7df", + "taptweak": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "method": "schnorr", + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.NoError(t, err) + require.Equal(t, resp, &logical.Response{Data: map[string]interface{}{ + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "signature": "e4112ae8f73f1d13a6128ddbde38f8bae00fbe9d6e1c3c330b5856e1587c593d9ed050c5f502ea80ab5bcc1a4ebcd4b3e0bfbbb5312591427d582613982c42a5", + }}) +} + +func TestImportNode(t *testing.T) { + t.Parallel() + + tctx := newTestContext(t) + defer tctx.Close() + + // Check for ErrSeedPhraseWrongLength. + _, err := tctx.importNode(map[string]interface{}{}) + require.ErrorIs(t, err, ErrSeedPhraseWrongLength) + + // Check for ErrSeedPhraseNotBIP39. + _, err = tctx.importNode(map[string]interface{}{ + "seedphrase": "absent weks slam olive squeeze cluster blame express asthma gym force warfare physical stuff unusual tiny endless patient again sound deny identify fall guard", + }) + require.ErrorIs(t, err, ErrSeedPhraseNotBIP39) + + // Check for ErrBadCipherSeedVer. + _, err = tctx.importNode(map[string]interface{}{ + "seedphrase": "walnut absent slam olive squeeze cluster blame express asthma gym force warfare physical stuff unusual tiny endless patient again sound deny identify fall guard", + }) + require.ErrorIs(t, err, ErrBadCipherSeedVer) + + // Check for ErrChecksumMismatch. + _, err = tctx.importNode(map[string]interface{}{ + "seedphrase": "absent walnut slam olive squeeze cluster blame express asthma gym force warfare physical stuff unusual tiny endless patient again sound deny identify fall fall", + }) + require.ErrorIs(t, err, ErrChecksumMismatch) + + // Check for ErrInvalidPassphrase. + _, err = tctx.importNode(map[string]interface{}{ + "network": "regtest", + "seedphrase": "abstract inch live custom just tray hockey enroll upon friend mass author filter desert parrot network finger uniform alley artefact path palace chicken diet", + "node": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + }) + require.ErrorIs(t, err, ErrInvalidPassphrase) + + // Check for ErrNodePubkeyMismatch. + _, err = tctx.importNode(map[string]interface{}{ + "network": "regtest", + "seedphrase": "abstract inch live custom just tray hockey enroll upon friend mass author filter desert parrot network finger uniform alley artefact path palace chicken diet", + "passphrase": "weks1234", + "node": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25ab", + }) + require.ErrorIs(t, err, ErrNodePubkeyMismatch) + + // Check for ErrInvalidNetwork. + _, err = tctx.importNode(map[string]interface{}{ + "network": "mainnet", // TODO(aakselrod): change this before going live on mainnet + "seedphrase": "abstract inch live custom just tray hockey enroll upon friend mass author filter desert parrot network finger uniform alley artefact path palace chicken diet", + "passphrase": "weks1234", + "node": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25ab", + }) + require.ErrorIs(t, err, ErrInvalidNetwork) + + // Import without passphrase. + resp, err := tctx.importNode(map[string]interface{}{ + "network": "regtest", + "seedphrase": "absent walnut slam olive squeeze cluster blame express asthma gym force warfare physical stuff unusual tiny endless patient again sound deny identify fall guard", + "node": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + }) + require.NoError(t, err) + require.Equal(t, + resp.Data["node"].(string), + "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + ) + + // Import with passphrase. + resp, err = tctx.importNode(map[string]interface{}{ + "network": "regtest", + "seedphrase": "abstract inch live custom just tray hockey enroll upon friend mass author filter desert parrot network finger uniform alley artefact path palace chicken diet", + "passphrase": "weks1234", + "node": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + }) + require.NoError(t, err) + require.Equal(t, + resp.Data["node"].(string), + "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + ) + + // Import over an existing node should fail. + resp, err = tctx.importNode(map[string]interface{}{ + "network": "regtest", + "seedphrase": "abstract inch live custom just tray hockey enroll upon friend mass author filter desert parrot network finger uniform alley artefact path palace chicken diet", + "passphrase": "weks1234", + "node": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + }) + require.ErrorIs(t, err, ErrNodeAlreadyExists) + require.Nil(t, resp) +} + +func TestListAccounts(t *testing.T) { + t.Parallel() + + tctx := newTestContext(t) + defer tctx.Close() + + // Import with passphrase. + resp, err := tctx.importNode(map[string]interface{}{ + "network": "regtest", + "seedphrase": "abstract inch live custom just tray hockey enroll upon friend mass author filter desert parrot network finger uniform alley artefact path palace chicken diet", + "passphrase": "weks1234", + "node": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + }) + require.NoError(t, err) + require.Equal(t, + resp.Data["node"].(string), + "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + ) + + // Get account list. + resp, err = tctx.listAccounts(map[string]interface{}{ + "node": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + }) + require.NoError(t, err) + + // Check against expected digest. + acctList, ok := resp.Data["acctList"].(string) + require.True(t, ok) + digest := sha256.Sum256([]byte(acctList)) + digestHex := hex.EncodeToString(digest[:]) + require.Equal(t, + digestHex, + "223b82c397cbccce80c5c5e33c993e332909e093bc5ca3398266f7a5e0f48806", + ) } diff --git a/vault/errors.go b/vault/errors.go index 8085728..47551cb 100644 --- a/vault/errors.go +++ b/vault/errors.go @@ -5,6 +5,13 @@ import ( ) var ( + ErrSeedPhraseWrongLength = errors.New("seed phrase must be 24 words") + ErrInvalidPassphrase = errors.New("invalid passphrase") + ErrSeedPhraseNotBIP39 = errors.New("seed phrase must use BIP39 word list") + ErrBadCipherSeedVer = errors.New("cipher seed version not recognized") + ErrWrongLengthChecksum = errors.New("wrong length checksum") + ErrChecksumMismatch = errors.New("checksum mismatch") + ErrWrongInternalVersion = errors.New("wrong internal version") ErrNodeAlreadyExists = errors.New("node already exists") ErrNodePubkeyMismatch = errors.New("node pubkey mismatch") ErrInvalidNetwork = errors.New("invalid network") diff --git a/vault/paths.go b/vault/paths.go index fdc47bd..5610728 100644 --- a/vault/paths.go +++ b/vault/paths.go @@ -45,6 +45,49 @@ POST - generate a new node seed and store it indexed by node pubkey } } +func (b *backend) importPath() *framework.Path { + return &framework.Path{ + Pattern: "lnd-nodes/import/?", + Operations: map[logical.Operation]framework.OperationHandler{ + logical.UpdateOperation: wrapOp(b.importNode), + }, + HelpSynopsis: "Import existing LND node into vault", + HelpDescription: ` + +POST - import existing LND node into vault with seedphrase and passphrase + +`, + Fields: map[string]*framework.FieldSchema{ + "node": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "optional: node pubkey, " + + "must be 66 hex characters", + Default: "", + }, + "network": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Network, one of " + + "'mainnet', 'testnet', " + + "'simnet', 'signet', or " + + "'regtest'", + Default: "regtest", + }, + "seedphrase": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "seed phrase to import, " + + "use instead of seed", + Default: "", + }, + "passphrase": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "optional: passphrase, " + + "use only with seed phrase", + Default: "", + }, + }, + } +} + func (b *backend) accountsPath() *framework.Path { return &framework.Path{ Pattern: "lnd-nodes/accounts/?", @@ -190,6 +233,7 @@ the submitted path func (b *backend) paths() []*framework.Path { return []*framework.Path{ b.basePath(), + b.importPath(), b.accountsPath(), b.ecdhPath(), b.signPath(), From be69864efe39ea8cefb64bc9ac7dc50f728788fa Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Thu, 19 Jan 2023 17:24:17 -0800 Subject: [PATCH 22/42] keyring: add package error variables, improve logs --- keyring/errors.go | 12 ++++++ keyring/keyring.go | 98 +++++++++++----------------------------------- 2 files changed, 35 insertions(+), 75 deletions(-) create mode 100644 keyring/errors.go diff --git a/keyring/errors.go b/keyring/errors.go new file mode 100644 index 0000000..3c1bf16 --- /dev/null +++ b/keyring/errors.go @@ -0,0 +1,12 @@ +// Copyright (C) 2022-2023 Bottlepay and The Lightning Network Developers + +package keyring + +import "errors" + +var ( + ErrNoSharedKeyReturned = errors.New("vault returned no shared key") + ErrBadSharedKey = errors.New("vault returned bad shared key") + ErrNoSignatureReturned = errors.New("vault returned no signature") + ErrNoPubkeyReturned = errors.New("vault returned no pubkey") +) diff --git a/keyring/keyring.go b/keyring/keyring.go index e472552..f5767f6 100644 --- a/keyring/keyring.go +++ b/keyring/keyring.go @@ -9,7 +9,6 @@ import ( "bytes" "crypto/sha256" "encoding/hex" - "errors" "fmt" "github.com/bottlepay/lndsigner/vault" @@ -79,16 +78,22 @@ type KeyDescriptor struct { PubKey *btcec.PublicKey } +// logicalWriter is an interface that specifies the relevant methods of the +// vault/api.Logical struct for mocking in tests. +type logicalWriter interface { + Write(string, map[string]interface{}) (*api.Secret, error) +} + // KeyRing is an HD keyring backed by pre-derived in-memory account keys from // which index keys can be quickly derived on demand. type KeyRing struct { - client *api.Logical + client logicalWriter node string coin uint32 } // NewKeyRing returns a vault-backed key ring. -func NewKeyRing(client *api.Logical, node string, coin uint32) *KeyRing { +func NewKeyRing(client logicalWriter, node string, coin uint32) *KeyRing { return &KeyRing{ client: client, node: node, @@ -139,7 +144,7 @@ func (k *KeyRing) ECDH(keyDesc KeyDescriptor, pub *btcec.PublicKey) ([32]byte, sharedKeyHex, ok := sharedKeyResp.Data["sharedkey"].(string) if !ok { - return [32]byte{}, errors.New("vault returned no shared key") + return [32]byte{}, ErrNoSharedKeyReturned } sharedKeyBytes, err := hex.DecodeString(sharedKeyHex) @@ -148,7 +153,7 @@ func (k *KeyRing) ECDH(keyDesc KeyDescriptor, pub *btcec.PublicKey) ([32]byte, } if len(sharedKeyBytes) != 32 { - return [32]byte{}, errors.New("vault returned bad shared key") + return [32]byte{}, ErrBadSharedKey } var sharedKeyByteArray [32]byte @@ -201,7 +206,7 @@ func (k *KeyRing) SignMessage(keyLoc KeyLocator, msg []byte, doubleHash bool, signatureHex, ok := signResp.Data["signature"].(string) if !ok { - return nil, errors.New("vault returned no signature") + return nil, ErrNoSignatureReturned } return hex.DecodeString(signatureHex) @@ -251,7 +256,7 @@ func (k *KeyRing) SignMessageSchnorr(keyLoc KeyLocator, msg []byte, signatureHex, ok := signResp.Data["signature"].(string) if !ok { - return nil, errors.New("vault returned no signature") + return nil, ErrNoSignatureReturned } signatureBytes, err := hex.DecodeString(signatureHex) @@ -277,6 +282,9 @@ func (k *KeyRing) SignPsbt(packet *psbt.Packet) ([]uint32, error) { return nil, err } + strPacket, _ := packet.B64Encode() + log.Debugf("Got PSBT to sign: %s", strPacket) + // Go through each input that doesn't have final witness data attached // to it already and try to sign it. If there is nothing more to sign or // there are inputs that we don't know how to sign, we won't return any @@ -307,37 +315,6 @@ func (k *KeyRing) SignPsbt(packet *psbt.Packet) ([]uint32, error) { continue } - /*// Let's try and derive the key now. This method will decide if - // it's a BIP49/84 key for normal on-chain funds or a key of the - // custom purpose 1017 key scope. - derivationInfo := in.Bip32Derivation[0] - privKey, err := k.deriveKeyByBIP32Path(derivationInfo.Bip32Path) - if err != nil { - log.Warnf("SignPsbt: Skipping input %d, error "+ - "deriving signing key: %v", idx, err) - continue - } - - // We need to make sure we actually derived the key that was - // expected to be derived. - pubKeysEqual := bytes.Equal( - derivationInfo.PubKey, - privKey.PubKey().SerializeCompressed(), - ) - if !pubKeysEqual { - log.Warnf("SignPsbt: Skipping input %d, derived "+ - "public key %x does not match bip32 "+ - "derivation info public key %x", idx, - privKey.PubKey().SerializeCompressed(), - derivationInfo.PubKey) - continue - } - - // Do we need to tweak anything? Single or double tweaks are - // sent as custom/proprietary fields in the PSBT input section. - privKey = maybeTweakPrivKeyPsbt(in.Unknowns, privKey) - */ - // What kind of signature is expected from us and do we have all // information we need? signMethod, err := validateSigningMethod(in) @@ -384,39 +361,6 @@ func (k *KeyRing) SignPsbt(packet *psbt.Packet) ([]uint32, error) { } signedInputs = append(signedInputs, uint32(idx)) - - /* newTx := packet.UnsignedTx.Copy() - newTx.TxIn[0].SignatureScript = in.RedeemScript - newTx.TxIn[0].Witness = wire.TxWitness{ - in.PartialSigs[0].Signature, - in.PartialSigs[0].PubKey, - } - - log.Infof("Executing engine on tx input %+v (from %+v)", - newTx.TxIn[0], in) - - engine, err := txscript.NewEngine( - in.WitnessUtxo.PkScript, - newTx, - idx, - txscript.StandardVerifyFlags, - txscript.NewSigCache(10), - sigHashes, - in.WitnessUtxo.Value, - prevOutputFetcher, - ) - if err != nil { - log.Errorf("Error creating engine: %v", err) - continue - } - - err = engine.Execute() - if err != nil { - log.Errorf("Error executing engine: %v", err) - continue - } - - log.Infof("Succeeded executing engine") */ } return signedInputs, nil @@ -473,7 +417,7 @@ func (k *KeyRing) signSegWitV0(in *psbt.PInput, tx *wire.MsgTx, signatureHex, ok := signResp.Data["signature"].(string) if !ok { - return errors.New("vault returned no signature") + return ErrNoSignatureReturned } signatureBytes, err := hex.DecodeString(signatureHex) @@ -483,7 +427,7 @@ func (k *KeyRing) signSegWitV0(in *psbt.PInput, tx *wire.MsgTx, pubKeyHex, ok := signResp.Data["pubkey"].(string) if !ok { - return errors.New("vault returned no pubkey") + return ErrNoPubkeyReturned } pubKeyBytes, err := hex.DecodeString(pubKeyHex) @@ -542,9 +486,11 @@ func (k *KeyRing) signSegWitV1KeySpend(in *psbt.PInput, tx *wire.MsgTx, signatureHex, ok := signResp.Data["signature"].(string) if !ok { - return errors.New("vault returned no signature") + return ErrNoSignatureReturned } + log.Debugf("Got data %+v in signing response", signResp.Data) + signatureBytes, err := hex.DecodeString(signatureHex) if err != nil { return err @@ -595,9 +541,11 @@ func (k *KeyRing) signSegWitV1ScriptSpend(in *psbt.PInput, tx *wire.MsgTx, return err } + log.Debugf("Got data %+v in signing response", signResp.Data) + signatureHex, ok := signResp.Data["signature"].(string) if !ok { - return errors.New("vault returned no signature") + return ErrNoSignatureReturned } signatureBytes, err := hex.DecodeString(signatureHex) From c2bef3cdd3a34df20e734e9b1135b63e2dfa5b8d Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Thu, 19 Jan 2023 17:20:59 -0800 Subject: [PATCH 23/42] keyring: add unit tests --- go.mod | 2 +- keyring/keyring_test.go | 678 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 679 insertions(+), 1 deletion(-) create mode 100644 keyring/keyring_test.go diff --git a/go.mod b/go.mod index 6930d44..8d35150 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/btcsuite/btcd/btcutil v1.1.2 github.com/btcsuite/btcd/btcutil/psbt v1.1.5 github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 github.com/hashicorp/go-hclog v1.3.1 github.com/hashicorp/vault/api v1.8.0 github.com/hashicorp/vault/sdk v0.6.0 @@ -27,7 +28,6 @@ require ( github.com/cenkalti/backoff/v3 v3.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/evanphx/json-patch/v5 v5.5.0 // indirect github.com/fatih/color v1.13.0 // indirect github.com/golang/protobuf v1.5.2 // indirect diff --git a/keyring/keyring_test.go b/keyring/keyring_test.go new file mode 100644 index 0000000..917f5f0 --- /dev/null +++ b/keyring/keyring_test.go @@ -0,0 +1,678 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package keyring + +import ( + "bytes" + "encoding/hex" + "errors" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/decred/dcrd/dcrec/secp256k1/v4/schnorr" + "github.com/hashicorp/vault/api" + "github.com/stretchr/testify/require" +) + +var ( + ourPub = mustParsePubKey("03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf") + + message = []byte("happy hanukkah") + + keyLoc = KeyLocator{ + Family: 6, + Index: 0, + } + + schnorrSigHex = "71b77d9c8a0badfa7c4eca3fbef5da2a552bf032f56b85fbc5c2f3500498fc20d5ab8505ae9733b1b756da7a5dba41dbe069dd0d86793618829c3077df0cd759" + schnorrSig, _ = hex.DecodeString(schnorrSigHex) + + requestError = errors.New("error on request") +) + +type mockClient struct { + writeFunc func(string, map[string]interface{}) (*api.Secret, error) +} + +func (m *mockClient) Write(path string, data map[string]interface{}) (*api.Secret, error) { + return m.writeFunc(path, data) +} + +func newTestKeyRing() *KeyRing { + return NewKeyRing( + &mockClient{}, + "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + 1, + ) +} + +func mustParsePubKey(keyHex string) *btcec.PublicKey { + keyBytes, _ := hex.DecodeString(keyHex) + key, _ := btcec.ParsePubKey(keyBytes) + return key +} + +func TestECDH(t *testing.T) { + t.Parallel() + + keyRing := newTestKeyRing() + client := keyRing.client.(*mockClient) + + peerPubHex := "02252bb0fdf7f6e7c055c5419c6fa1c9799cf348b480603b9c0af61dbdea29149e" + peerPub := mustParsePubKey(peerPubHex) + + keyBytes, _ := hex.DecodeString( + "7895c217d4f1a33265c0122ce66dd16bcd0b86976198f1128e6dbaef86a2f327", + ) + var sharedKey [32]byte + copy(sharedKey[:], keyBytes) + + keyDesc := KeyDescriptor{ + KeyLocator: keyLoc, + PubKey: ourPub, + } + + testCases := []struct { + name string + respData map[string]interface{} + respErr error + key [32]byte + err error + }{ + { + name: "ecdh", + respData: map[string]interface{}{ + "sharedkey": "7895c217d4f1a33265c0122ce66dd16bcd0b86976198f1128e6dbaef86a2f327", + }, + key: sharedKey, + }, + { + name: ErrNoSharedKeyReturned.Error(), + respData: map[string]interface{}{}, + err: ErrNoSharedKeyReturned, + }, + { + name: ErrBadSharedKey.Error(), + respData: map[string]interface{}{ + "sharedkey": "7895", + }, + err: ErrBadSharedKey, + }, + { + name: "shared key not hex", + respData: map[string]interface{}{ + "sharedkey": "g", + }, + err: hex.InvalidByteError(0x67), + }, + { + name: "error on request", + respErr: errors.New("request error"), + err: errors.New("request error"), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + client.writeFunc = func(path string, + data map[string]interface{}) (*api.Secret, + error) { + + require.Equal(t, "lndsigner/lnd-nodes/ecdh", + path) + + require.Equal(t, map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "peer": peerPubHex, + "pubkey": keyRing.node, + }, data) + + return &api.Secret{Data: testCase.respData}, + testCase.respErr + } + + key, err := keyRing.ECDH(keyDesc, peerPub) + require.Equal(t, testCase.err, err) + + if err != nil { + return + } + + require.Equal(t, testCase.key, key) + }) + } +} + +func TestSignMessage(t *testing.T) { + t.Parallel() + + keyRing := newTestKeyRing() + client := keyRing.client.(*mockClient) + + testCases := []struct { + name string + doubleHash bool + compact bool + reqData map[string]interface{} + respData map[string]interface{} + respErr error + sig []byte + err error + }{ + { + name: "sign single", + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "ecdsa", + "digest": "4eacd1f26fe18294c9671e427240e6762e6f021f1e35793cab2850cbec7320f3", + }, + respData: map[string]interface{}{ + "signature": "abcdef", + }, + sig: []byte{0xab, 0xcd, 0xef}, + }, + { + name: "sign double", + doubleHash: true, + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "ecdsa", + "digest": "2b81484875960ba2eaea16ae0ecfc2848c2d40944b5c034c609ce95542151f14", + }, + respData: map[string]interface{}{ + "signature": "abcdef", + }, + sig: []byte{0xab, 0xcd, 0xef}, + }, + { + name: "sign single compact", + compact: true, + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "ecdsa-compact", + "digest": "4eacd1f26fe18294c9671e427240e6762e6f021f1e35793cab2850cbec7320f3", + }, + respData: map[string]interface{}{ + "signature": "abcdef", + }, + sig: []byte{0xab, 0xcd, 0xef}, + }, + { + name: "sign double compact", + doubleHash: true, + compact: true, + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "ecdsa-compact", + "digest": "2b81484875960ba2eaea16ae0ecfc2848c2d40944b5c034c609ce95542151f14", + }, + respData: map[string]interface{}{ + "signature": "abcdef", + }, + sig: []byte{0xab, 0xcd, 0xef}, + }, + { + name: "error on request", + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "ecdsa", + "digest": "4eacd1f26fe18294c9671e427240e6762e6f021f1e35793cab2850cbec7320f3", + }, + respErr: requestError, + err: requestError, + }, + { + name: ErrNoSignatureReturned.Error(), + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "ecdsa", + "digest": "4eacd1f26fe18294c9671e427240e6762e6f021f1e35793cab2850cbec7320f3", + }, + err: ErrNoSignatureReturned, + }, + { + name: "signature not hex", + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "ecdsa", + "digest": "4eacd1f26fe18294c9671e427240e6762e6f021f1e35793cab2850cbec7320f3", + }, + respData: map[string]interface{}{ + "signature": "g", + }, + err: hex.InvalidByteError(0x67), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + client.writeFunc = func(path string, + data map[string]interface{}) (*api.Secret, + error) { + + require.Equal(t, "lndsigner/lnd-nodes/sign", + path) + + require.Equal(t, testCase.reqData, data) + + return &api.Secret{Data: testCase.respData}, + testCase.respErr + } + + sig, err := keyRing.SignMessage(keyLoc, message, + testCase.doubleHash, testCase.compact) + require.Equal(t, testCase.err, err) + + if err != nil { + return + } + + require.Equal(t, testCase.sig, sig) + }) + } +} + +func TestSignMessageSchnorr(t *testing.T) { + t.Parallel() + + keyRing := newTestKeyRing() + client := keyRing.client.(*mockClient) + + testCases := []struct { + name string + doubleHash bool + tapTweak []byte + reqData map[string]interface{} + respData map[string]interface{} + respErr error + sig []byte + err error + }{ + { + name: "sign single", + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "schnorr", + "digest": "4eacd1f26fe18294c9671e427240e6762e6f021f1e35793cab2850cbec7320f3", + }, + respData: map[string]interface{}{ + "signature": schnorrSigHex, + }, + sig: schnorrSig, + }, + { + name: "sign double", + doubleHash: true, + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "schnorr", + "digest": "2b81484875960ba2eaea16ae0ecfc2848c2d40944b5c034c609ce95542151f14", + }, + respData: map[string]interface{}{ + "signature": schnorrSigHex, + }, + sig: schnorrSig, + }, + { + name: "sign single tweaked", + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "schnorr", + "digest": "4eacd1f26fe18294c9671e427240e6762e6f021f1e35793cab2850cbec7320f3", + }, + respData: map[string]interface{}{ + "signature": schnorrSigHex, + }, + sig: schnorrSig, + }, + { + name: "sign double tweaked", + doubleHash: true, + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "schnorr", + "digest": "2b81484875960ba2eaea16ae0ecfc2848c2d40944b5c034c609ce95542151f14", + }, + respData: map[string]interface{}{ + "signature": schnorrSigHex, + }, + sig: schnorrSig, + }, + { + name: "error on request", + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "schnorr", + "digest": "4eacd1f26fe18294c9671e427240e6762e6f021f1e35793cab2850cbec7320f3", + }, + respErr: requestError, + err: requestError, + }, + { + name: ErrNoSignatureReturned.Error(), + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "schnorr", + "digest": "4eacd1f26fe18294c9671e427240e6762e6f021f1e35793cab2850cbec7320f3", + }, + err: ErrNoSignatureReturned, + }, + { + name: "signature not hex", + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "schnorr", + "digest": "4eacd1f26fe18294c9671e427240e6762e6f021f1e35793cab2850cbec7320f3", + }, + respData: map[string]interface{}{ + "signature": "g", + }, + err: hex.InvalidByteError(0x67), + }, + { + name: schnorr.ErrSigTooShort.Error(), + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "schnorr", + "digest": "4eacd1f26fe18294c9671e427240e6762e6f021f1e35793cab2850cbec7320f3", + }, + respData: map[string]interface{}{ + "signature": "abcdef", + }, + err: schnorr.Error{ + Err: schnorr.ErrSigTooShort, + Description: "malformed signature: too short: 3 < 64", + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + client.writeFunc = func(path string, + data map[string]interface{}) (*api.Secret, + error) { + + require.Equal(t, "lndsigner/lnd-nodes/sign", + path) + + require.Equal(t, testCase.reqData, data) + + return &api.Secret{Data: testCase.respData}, + testCase.respErr + } + + sig, err := keyRing.SignMessageSchnorr(keyLoc, message, + testCase.doubleHash, testCase.tapTweak) + require.Equal(t, testCase.err, err) + + if err != nil { + return + } + + require.Equal(t, testCase.sig, sig.Serialize()) + }) + } +} + +func TestSignPsbt(t *testing.T) { + t.Parallel() + + keyRing := newTestKeyRing() + client := keyRing.client.(*mockClient) + + testCases := []struct { + name string + packet *psbt.Packet + reqData map[string]interface{} + respData map[string]interface{} + respErr error + inputs []uint32 + err error + }{ + { + name: "nil PSBT", + err: errors.New("PSBT packet cannot be nil"), + }, + { + name: "p2tr spend", + packet: p2trPsbt, + reqData: map[string]interface{}{ + "node": keyRing.node, + "digest": "6a14f55652583393923a9f6909c9be3ada3e5bd724c324d8a554b823388491ad", + "path": []int{2147483734, 2147483648, 2147483648, 0, 0}, + "method": "schnorr", + "taptweak": "", + }, + respData: map[string]interface{}{ + "signature": schnorrSigHex, + }, + inputs: []uint32{0}, + }, + { + name: "p2wkh spend", + packet: p2wkhPsbt, + reqData: map[string]interface{}{ + "node": keyRing.node, + "digest": "2046c479fa1d00033ff7239086071cb4abadc2c99e2dd14e6e1af7ed8060f3ca", + "path": []int{2147483732, 2147483648, 2147483648, 0, 0}, + "method": "ecdsa", + }, + respData: map[string]interface{}{ + "pubkey": "abcdef", + "signature": "abcdef", + }, + inputs: []uint32{0}, + }, + { + name: "np2wkh spend", + packet: np2wkhPsbt, + reqData: map[string]interface{}{ + "node": keyRing.node, + "digest": "5f5c0b1eb31d60a15c0b607ee037b97bbb2ef375d127be13c4718ddc5b67dc70", + "path": []int{2147483697, 2147483648, 2147483648, 0, 0}, + "method": "ecdsa", + }, + respData: map[string]interface{}{ + "pubkey": "abcdef", + "signature": "abcdef", + }, + inputs: []uint32{0}, + }, + { + name: "ln1tweak spend", + packet: tweak1Psbt, + reqData: map[string]interface{}{ + "node": keyRing.node, + "digest": "acf17dc76b84ab1b061f274ccea1b680e7195d34f138c92bec64f66a6ed11b7c", + "ln1tweak": "cf374dcf99541cff08176226b16e1848eee7f00430da428a74ddc671224bbe8f", + "path": []int{2147484665, 2147483649, 2147483650, 0, 0}, + "method": "ecdsa", + }, + respData: map[string]interface{}{ + "pubkey": "abcdef", + "signature": "abcdef", + }, + inputs: []uint32{0}, + }, + { + name: ErrNoPubkeyReturned.Error(), + packet: np2wkhPsbt, + reqData: map[string]interface{}{ + "node": keyRing.node, + "digest": "5f5c0b1eb31d60a15c0b607ee037b97bbb2ef375d127be13c4718ddc5b67dc70", + "path": []int{2147483697, 2147483648, 2147483648, 0, 0}, + "method": "ecdsa", + }, + respData: map[string]interface{}{ + "signature": "abcdef", + }, + err: ErrNoPubkeyReturned, + }, + { + name: "pubkey not hex", + packet: np2wkhPsbt, + reqData: map[string]interface{}{ + "node": keyRing.node, + "digest": "5f5c0b1eb31d60a15c0b607ee037b97bbb2ef375d127be13c4718ddc5b67dc70", + "path": []int{2147483697, 2147483648, 2147483648, 0, 0}, + "method": "ecdsa", + }, + respData: map[string]interface{}{ + "pubkey": "g", + "signature": "abcdef", + }, + err: hex.InvalidByteError(0x67), + }, + { + name: "p2wkh and np2wkh error on request", + packet: np2wkhPsbt, + reqData: map[string]interface{}{ + "node": keyRing.node, + "digest": "5f5c0b1eb31d60a15c0b607ee037b97bbb2ef375d127be13c4718ddc5b67dc70", + "path": []int{2147483697, 2147483648, 2147483648, 0, 0}, + "method": "ecdsa", + }, + respErr: requestError, + err: requestError, + }, + { + name: "np2wkh and p2wkh " + ErrNoSignatureReturned.Error(), + packet: np2wkhPsbt, + reqData: map[string]interface{}{ + "node": keyRing.node, + "digest": "5f5c0b1eb31d60a15c0b607ee037b97bbb2ef375d127be13c4718ddc5b67dc70", + "path": []int{2147483697, 2147483648, 2147483648, 0, 0}, + "method": "ecdsa", + }, + err: ErrNoSignatureReturned, + }, + { + name: "np2wkh and p2wkh signature not hex", + packet: np2wkhPsbt, + reqData: map[string]interface{}{ + "node": keyRing.node, + "digest": "5f5c0b1eb31d60a15c0b607ee037b97bbb2ef375d127be13c4718ddc5b67dc70", + "path": []int{2147483697, 2147483648, 2147483648, 0, 0}, + "method": "ecdsa", + }, + respData: map[string]interface{}{ + "signature": "g", + }, + err: hex.InvalidByteError(0x67), + }, + { + name: "p2wkh and np2wkh error on request", + packet: p2trPsbt, + reqData: map[string]interface{}{ + "node": keyRing.node, + "digest": "6a14f55652583393923a9f6909c9be3ada3e5bd724c324d8a554b823388491ad", + "path": []int{2147483734, 2147483648, 2147483648, 0, 0}, + "method": "schnorr", + "taptweak": "", + }, + respErr: requestError, + err: requestError, + }, + { + name: "p2tr " + ErrNoSignatureReturned.Error(), + packet: p2trPsbt, + reqData: map[string]interface{}{ + "node": keyRing.node, + "digest": "6a14f55652583393923a9f6909c9be3ada3e5bd724c324d8a554b823388491ad", + "path": []int{2147483734, 2147483648, 2147483648, 0, 0}, + "method": "schnorr", + "taptweak": "", + }, + err: ErrNoSignatureReturned, + }, + { + name: "p2tr signature not hex", + packet: p2trPsbt, + reqData: map[string]interface{}{ + "node": keyRing.node, + "digest": "6a14f55652583393923a9f6909c9be3ada3e5bd724c324d8a554b823388491ad", + "path": []int{2147483734, 2147483648, 2147483648, 0, 0}, + "method": "schnorr", + "taptweak": "", + }, + respData: map[string]interface{}{ + "signature": "g", + }, + err: hex.InvalidByteError(0x67), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + client.writeFunc = func(path string, + data map[string]interface{}) (*api.Secret, + error) { + + require.Equal(t, "lndsigner/lnd-nodes/sign", + path) + + require.Equal(t, testCase.reqData, data) + + return &api.Secret{Data: testCase.respData}, + testCase.respErr + } + + signed, err := keyRing.SignPsbt(testCase.packet) + require.Equal(t, testCase.err, err) + + if err != nil { + return + } + + require.Equal(t, testCase.inputs, signed) + }) + } +} + +func mustGetPacket(pb64 string) *psbt.Packet { + packet, _ := psbt.NewFromRawBytes(bytes.NewBuffer([]byte(pb64)), true) + return packet +} + +var ( + p2trPsbt = mustGetPacket("cHNidP8BAF4CAAAAAReb7pdpQYTQ2CNhvICbAlIE+1c/c+mDcoMwoc8rinenAAAAAAAAAAAAAUbL9QUAAAAAIlEgtfY28ZYCnRSLF8t3TlIu/whJ6214cNnzljDTT1efhbXlCwAAAAEA9gIAAAAAAQFJMZAOE1PK+T6HTes4eGZLMIDd0bzYEZ1oFJVYK/ZiCwAAAAAA/f///wIA4fUFAAAAACJRILX2NvGWAp0UixfLd05SLv8ISetteHDZ85Yw009Xn4W1nR0NjwAAAAAiUSDDFLNZISF+rpRN7aK0J7JM0mT5fqTEKvFKaUMBEw6ZIAJHMEQCIHHymwJU7u3YG6DRJHEBXU7WM7iKpfEyi2RirHkkG3qgAiBlFw4ueMJ7R9UiX7X4kqOkrtnPPbjRFAcV0N2dlwMsqQEhAlwtWnotMY/75da30kBFv6MAP1cQ9+JAtFPHOys8EcVx5AsAAAEBKwDh9QUAAAAAIlEgtfY28ZYCnRSLF8t3TlIu/whJ6214cNnzljDTT1efhbUiBgPNayRhLVP0LyNSLfBQl/5mzCRF1JJj0XfEHSRXn2ocYBgAAAAAVgAAgAAAAIAAAACAAAAAAAAAAAAhFs1rJGEtU/QvI1It8FCX/mbMJEXUkmPRd8QdJFefahxgGQAAAAAAVgAAgAAAAIAAAACAAAAAAAAAAAAAAA==") + p2wkhPsbt = mustGetPacket("cHNidP8BAFICAAAAAY5F4ge3IcjdMnXPFvSlcsJtrWsucxdANZMwuxiBAn9mAAAAAAAAAAAAAXWi9QUAAAAAFgAU9Pc1yrjv1bubTE9iLVshd6CQcBPnCwAAAAEAlgIAAAAAAQFFycXtfec9d3aq5MGiwljDzXdT1b27wxA35E5bQ+fjlgAAAAAAAAAAAAHkt/UFAAAAABYAFPT3Ncq479W7m0xPYi1bIXegkHATAUB2zaC2OeERmiGxi78tR1hE7aDCWUJCMhcSvcs4e7kxarTGXy2IcLzCGuwsJkWiYmcunFp6VQSbpcO23KQNMDkz5gsAAAEBH+S39QUAAAAAFgAU9Pc1yrjv1bubTE9iLVshd6CQcBMBAwQBAAAAAQUWABT09zXKuO/Vu5tMT2ItWyF3oJBwEyIGAwEIoLvhEXJvhEZtTcYV/qv1mWWy6gvKBE+dVfZTNiRDGAAAAABUAACAAAAAgAAAAIAAAAAAAAAAAAAA") + np2wkhPsbt = mustGetPacket("cHNidP8BAFMCAAAAAd5zNp3onh0tGqfG/uWxkO492bU81losB9X4NeyTzD+/AAAAAAAAAAAAAV1w9QUAAAAAF6kUraCbn6aZpvU7PJsguxGajtIBNxOH6QsAAAABAMECAAAAAAEBZd+rb8GaP6GGqU8L9mbFsIQhZg5Tu74ydSOG3Co97GQAAAAAAAAAAAAB1Iz1BQAAAAAXqRStoJufppmm9Ts8myC7EZqO0gE3E4cCSDBFAiEAnd6DdDpgIBspLqYb4c4UxA0OfHH5U6v8MdFqYNGr3qsCICz6j4z50tyrlb6udEg19obEMvxZYapfiTm8b1u+yY7WASEDAQigu+ERcm+ERm1NxhX+q/WZZbLqC8oET51V9lM2JEPoCwAAAQEg1Iz1BQAAAAAXqRStoJufppmm9Ts8myC7EZqO0gE3E4cBAwQBAAAAAQQXFgAUZHoOpcJIIpm9ZZqp4+yUqUbVxlABBRYAFGR6DqXCSCKZvWWaqePslKlG1cZQIgYCjY4A5/M2NnFkZxRH38ob8I0kLvCMqAaLpjV2L4W2DV4YAAAAADEAAIAAAACAAAAAgAAAAAAAAAAAAAA=") + tweak1Psbt = mustGetPacket("cHNidP8BAF4CAAAAAefBDVMh9KyBaUQgDsjV6nUylfgrye5+zKxHOWe2WppIAgAAAAABAAAAAYgTAAAAAAAAIgAgnt22lI7xTfZhaVvG98+/a2fgGpKGx4If+tDif9TNGtIbDAAAAAEBK4gTAAAAAAAAIgAgh3mqXWpXMnilHq2jlNLT1RHiQblrGAcXso10qnIuHo4BAwSDAAAAAQWIdqkUSL7Vs9GdmmyMm/jAw3NWT1cavGSHY6xnIQM9249K7owaTOry2WiTfm5kYbSiKpopzl0v4REiL4tjk3yCASCHZHVSfCEDCrYgRP5lJXfOS1ZXS110yioIRjDn5OEr6yCLWXhajChSrmepFL5fYOxnPqaCwyQdgixZKou3K8d1iKxoUbJ1aCIGA0fj8run5YT4j5ZwAwgD7jOWCKVLiFP7xPUeSQVQotAvGAAAAAD5AwCAAQAAgAIAAIAAAAAAAAAAAAFRIM83Tc+ZVBz/CBdiJrFuGEju5/AEMNpCinTdxnEiS76PAAA=") +) From 1ec988b245e6729f8d20c6fe6d5ba7e5c7280031 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Fri, 27 Jan 2023 00:56:27 -0800 Subject: [PATCH 24/42] main: typo fix and opportunistic upgrade to structured logging --- lndsigner.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lndsigner.go b/lndsigner.go index cba8a1f..e0841ac 100644 --- a/lndsigner.go +++ b/lndsigner.go @@ -49,7 +49,8 @@ func Main(cfg *Config, lisCfg ListenerCfg) error { return fmt.Errorf(format, args...) } - signerLog.Infof("Active Bitcoin network: %v)", cfg.ActiveNetParams.Name) + signerLog.Infow("Active Bitcoin network: ", "net", + cfg.ActiveNetParams.Name) // Use defaults for vault client, including getting config from env. vaultClient, err := api.NewClient(nil) From 7f0b6441a875f352552f4352ce90d9b6c0c54a9c Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Fri, 23 Dec 2022 14:46:56 -0800 Subject: [PATCH 25/42] lndsigner: add integration tests --- .github/workflows/docker-test.yml | 18 ++ Dockerfile | 29 ++ Makefile | 50 ++++ README.md | 19 +- go.mod | 1 + go.sum | 1 + itest/gen_protos.sh | 37 +++ itest/itest_context_test.go | 397 ++++++++++++++++++++++++++++ itest/itest_lndharness_test.go | 397 ++++++++++++++++++++++++++++ itest/lndsigner_test.go | 198 ++++++++++++++ itest/walletunlocker.pb.go | 423 ++++++++++++++++++++++++++++++ itest/walletunlocker.proto | 112 ++++++++ itest/walletunlocker_grpc.pb.go | 127 +++++++++ lndsigner.go | 53 ++++ 14 files changed, 1861 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/docker-test.yml create mode 100644 Dockerfile create mode 100644 Makefile create mode 100755 itest/gen_protos.sh create mode 100644 itest/itest_context_test.go create mode 100644 itest/itest_lndharness_test.go create mode 100644 itest/lndsigner_test.go create mode 100644 itest/walletunlocker.pb.go create mode 100644 itest/walletunlocker.proto create mode 100644 itest/walletunlocker_grpc.pb.go diff --git a/.github/workflows/docker-test.yml b/.github/workflows/docker-test.yml new file mode 100644 index 0000000..5b7cdc1 --- /dev/null +++ b/.github/workflows/docker-test.yml @@ -0,0 +1,18 @@ +name: Dockerized Tests + +on: + push: + branches: [ "master", "main" ] + pull_request: + branches: [ "**" ] + +jobs: + all-tests: + runs-on: ubuntu-latest + + steps: + - name: Check out repository code + uses: actions/checkout@v3 + + - name: Run tests in docker container + run: make docker-test-all diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2632450 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +ARG gover=1.19.5 + +FROM golang:$gover + +ARG goplatform=amd64 +ARG cppplatform=x86_64 +ARG lnd=v0.15.5-beta +ARG bitcoind=24.0.1 +ARG vault=1.12.2 + +RUN apt update && apt-get install -y zip + +RUN cd /root && \ + wget https://bitcoincore.org/bin/bitcoin-core-$bitcoind/bitcoin-${bitcoind}-${cppplatform}-linux-gnu.tar.gz && \ + tar xfz bitcoin-$bitcoind-$cppplatform-linux-gnu.tar.gz && \ + mv bitcoin-$bitcoind/bin/* /usr/local/bin/ && \ + wget https://github.com/lightningnetwork/lnd/releases/download/$lnd/lnd-linux-$goplatform-$lnd.tar.gz && \ + tar xfz lnd-linux-$goplatform-$lnd.tar.gz && \ + mv lnd-linux-$goplatform-$lnd/* /usr/local/bin/ && \ + wget https://releases.hashicorp.com/vault/$vault/vault_${vault}_linux_${goplatform}.zip && \ + unzip vault_${vault}_linux_${goplatform}.zip && \ + mv vault /usr/local/bin/ && \ + go install github.com/go-delve/delve/cmd/dlv@latest && \ + git config --global --add safe.directory /app && \ + echo "export PATH='$PATH:/usr/local/go/bin:/root/go/bin'" >> .bashrc + +VOLUME [ "/app" ] + +WORKDIR /app diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2a2d217 --- /dev/null +++ b/Makefile @@ -0,0 +1,50 @@ +.PHONY: docker docker-itest docker-test docker-test-all docker-check docker-shell itest test test-all + +IMG_NAME := lndsigner-builder + +GOVER := 1.19.5 +GOPLATFORM := amd64 +CPPPLATFORM := x86_64 +LND := v0.15.5-beta +BITCOIND := 24.0.1 +VAULT := 1.12.2 + +# docker just tags the latest image to the builderstamp, in case the +# dependencies have been changed and a new image was built. +docker: + docker build -t $(IMG_NAME):latest . + +# docker-itest runs itests in a docker container, then removes the container. +docker-itest: docker + docker run -t --rm \ + --mount type=bind,source=$(CURDIR),target=/app $(IMG_NAME):latest \ + make itest + +# docker-test runs unit tests in a docker container, then removes the container. +docker-test: docker + docker run -t --rm \ + --mount type=bind,source=$(CURDIR),target=/app $(IMG_NAME):latest \ + make test + +# docker-test-all runs unit and integration tests in a docker container, then +# removes the container. +docker-test-all: docker + docker run -t --rm \ + --mount type=bind,source=$(CURDIR),target=/app $(IMG_NAME):latest \ + make test-all + +# docker-shell opens a shell to a dockerized environment with all dependencies +# and also dlv installed for easy debugging, then removes the container. +docker-shell: docker + docker run -it --rm \ + --mount type=bind,source=$(CURDIR),target=/app $(IMG_NAME):latest \ + bash -l + +itest: + go install -race -buildvcs=false ./cmd/... && go test -v -count=1 -race -tags=itest -cover ./itest + +test: + go test -v -count=1 -race -cover ./... + +test-all: + go install -race -buildvcs=false ./cmd/... && go test -v -count=1 -race -tags=itest -cover ./... diff --git a/README.md b/README.md index 7e0594b..a9b8374 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ - [x] sign messages for network announcements - [x] derive shared keys for peer connections - [x] sign PSBTs for on-chain transactions, channel openings/closes, HTLC updates, etc. -- [ ] run itests +- [x] run itests - [ ] do automated builds - [ ] do reproducible builds - [ ] perform musig2 ops @@ -159,3 +159,20 @@ node 03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf ``` Now you can use the imported key as before. + +## Testing +You can run unit tests and integration tests, together or separately, in Docker or on your host system. To run tests inside Docker, from the project directory, run one of: + + * `$ make docker-test` for unit tests + * `$ make docker-itest` for integration tests + * `$ make docker-test-all` for integration and unit tests + +To run tests directly on your development machine, you can use: + + * `$ make test` for unit tests + * `$ make itest` for integration tests + * `$ make test-all` for integration and unit tests + +Before running integration tests on your development machine, ensure you have all the required binaries (bitcoind, bitcoin-cli, lnd, lncli, vault). + +To get a shell on a container that can run tests, you can use `make docker-shell`. Then, you can `make test`, `make itest`, or `make test-all` inside the container, just like you would directly on the host system. diff --git a/go.mod b/go.mod index 8d35150..78ed3d7 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( require ( github.com/armon/go-metrics v0.3.9 // indirect github.com/armon/go-radix v1.0.0 // indirect + github.com/benbjohnson/clock v1.1.0 // indirect github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect github.com/cenkalti/backoff/v3 v3.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index 841c6fd..4a21b90 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,7 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= diff --git a/itest/gen_protos.sh b/itest/gen_protos.sh new file mode 100755 index 0000000..101ca2a --- /dev/null +++ b/itest/gen_protos.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# +# Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +# Copyright (C) 2022-2023 Bottlepay and The Lightning Network Developers + +set -e + +# generate compiles the *.pb.go stubs from the *.proto files. +function generate() { + echo "Generating root gRPC server protos" + + PROTOS="walletunlocker.proto" + + # For each of the sub-servers, we then generate their protos, but a restricted + # set as they don't yet require REST proxies, or swagger docs. + for file in $PROTOS; do + DIRECTORY=$(dirname "${file}") + echo "Generating protos from ${file}, into ${DIRECTORY}" + + # Generate the protos. + protoc -I/usr/local/include -I. \ + --go_out . --go_opt paths=source_relative \ + --go-grpc_out . --go-grpc_opt paths=source_relative \ + "${file}" + done +} + +# format formats the *.proto files with the clang-format utility. +function format() { + find . -name "*.proto" -print0 | xargs -0 clang-format --style=file -i +} + +# Compile and format the itest package. +pushd itest +format +generate +popd diff --git a/itest/itest_context_test.go b/itest/itest_context_test.go new file mode 100644 index 0000000..79419ed --- /dev/null +++ b/itest/itest_context_test.go @@ -0,0 +1,397 @@ +//go:build itest +// +build itest + +package itest_test + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io/fs" + "net" + "os" + "os/exec" + "path" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/hashicorp/vault/api" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "go.uber.org/zap/zaptest" +) + +// testContext manages the test environment. +type testContext struct { + t *testing.T + log *zap.SugaredLogger + cancel context.CancelFunc + + tmpRoot string + + vaultPort string + vaultCmd *exec.Cmd + vaultClient *api.Logical + + bitcoinDir string + bitcoinRPC string + bitcoinZB *net.TCPAddr + bitcoinZT *net.TCPAddr + bitcoindCmd *exec.Cmd + bitcoindClient *api.Logical + blocksMined uint32 + + lndPath string + lndSignerPath string + lncliPath string + bitcoincliPath string + + lnds []*lndHarness +} + +//newTestContext creates a new test context. +func newTestContext(t *testing.T) *testContext { + t.Helper() + + tctx := &testContext{ + t: t, + log: zaptest.NewLogger(t).Sugar(), + lnds: make([]*lndHarness, 0, 3), + } + + ctx, cancel := context.WithCancel(context.Background()) + tctx.cancel = cancel + + // Create temp directory for test context. + tmpRoot, err := os.MkdirTemp("", "lndsigner-itest") + require.NoError(t, err) + tctx.tmpRoot = tmpRoot + + // Get binary paths + bitcoindPath, err := exec.LookPath("bitcoind") + require.NoError(t, err) + + tctx.lndPath, err = exec.LookPath("lnd") + require.NoError(tctx.t, err) + + tctx.lndSignerPath, err = exec.LookPath("lndsignerd") + require.NoError(tctx.t, err) + + tctx.lncliPath, err = exec.LookPath("lncli") + require.NoError(tctx.t, err) + + tctx.bitcoincliPath, err = exec.LookPath("bitcoin-cli") + require.NoError(tctx.t, err) + + // Start bitcoind + tctx.bitcoinDir = path.Join(tctx.tmpRoot, "bitcoin") + err = os.Mkdir(tctx.bitcoinDir, fs.ModeDir|0700) + require.NoError(t, err) + + tctx.bitcoinRPC = newPortString() + tctx.bitcoinZB = newPort() + tctx.bitcoinZT = newPort() + + tctx.bitcoindCmd = exec.CommandContext(ctx, bitcoindPath, "-server=1", + "-datadir="+tctx.bitcoinDir, "-listen=0", "-txindex=1", + "-regtest=1", "-rpcuser=user", "-rpcpassword=password", + "-rpcport="+tctx.bitcoinRPC, + "-zmqpubrawblock=tcp://"+tctx.bitcoinZB.String(), + "-zmqpubrawtx=tcp://"+tctx.bitcoinZT.String()) + + go waitProc(tctx.bitcoindCmd) + + // Wait for bitcoind to start. Log file is ~6300 bytes when regtest + // bitcoind with our options is started. + waitFile( + t, + path.Join(tctx.bitcoinDir, "/regtest/debug.log"), + "init message: Done loading", + ) + + // Mine blocks to give us funds and activate soft forks. + go func() { + tctx.bitcoinCli("createwallet", "default") + tctx.mine(1000) + }() + + // Start vault. + vaultPath, err := exec.LookPath("vault") + require.NoError(t, err) + + pluginPath, err := exec.LookPath("vault-plugin-lndsigner") + require.NoError(t, err) + + pluginDir := path.Join(tmpRoot, "vault_plugins") + err = os.Mkdir(pluginDir, fs.ModeDir|0700) + require.NoError(t, err) + + mustCopyFile(pluginPath, path.Join(pluginDir, "vault-plugin-lndsigner"), + 0700) + + tctx.vaultPort = newPortString() + tctx.vaultCmd = exec.CommandContext(ctx, vaultPath, "server", "-dev", + "-dev-root-token-id=root", "-dev-plugin-dir="+pluginDir, + "-dev-listen-address=127.0.0.1:"+tctx.vaultPort) + + go waitProc(tctx.vaultCmd) + + vaultClientConf := api.DefaultConfig() + vaultClientConf.Address = "http://127.0.0.1:" + tctx.vaultPort + + vaultClient, err := api.NewClient(vaultClientConf) + require.NoError(t, err) + + vaultClient.SetToken("root") + + tctx.vaultClient = vaultClient.Logical() + + vaultSys := vaultClient.Sys() + err = vaultSys.Mount("lndsigner", &api.MountInput{ + Type: "vault-plugin-lndsigner", + }) + require.NoError(t, err) + + return tctx +} + +// bitcoinCli sends a command to the test context's bitcoind. +func (tctx *testContext) bitcoinCli(args ...string) map[string]interface{} { + tctx.t.Helper() + + bitcoinCliCmd := exec.CommandContext(context.Background(), + tctx.bitcoincliPath, + append([]string{"-datadir=" + tctx.bitcoinDir, + "-rpcport=" + tctx.bitcoinRPC, "-rpcuser=user", + "-rpcpassword=password", "-rpcwaittimeout=5"}, + args...)...) + + stdErrBuf := bytes.NewBuffer(make([]byte, 0)) + bitcoinCliCmd.Stderr = stdErrBuf + + stdOutBuf := bytes.NewBuffer(make([]byte, 0)) + bitcoinCliCmd.Stdout = stdOutBuf + + err := bitcoinCliCmd.Start() + require.NoError(tctx.t, err) + + // If there's an error on exit, show stderr. + err = bitcoinCliCmd.Wait() + require.NoError(tctx.t, err, string(stdErrBuf.Bytes())) + + stdout := string(stdOutBuf.Bytes()) + + // sendtoaddress only returns a txid on success. In this case, the + // first argument is "-named". + if len(args) > 1 && args[1] == "sendtoaddress" { + return map[string]interface{}{ + "txid": stdout[:64], + } + } + + // If we're stopping, we won't get JSON back. + if args[0] == "stop" { + return nil + } + + // If there's an error parsing the JSON, show stdout to see the issue. + resp := make(map[string]interface{}) + err = json.Unmarshal([]byte(stdout), &resp) + require.NoError(tctx.t, err, stdout) + + return resp +} + +// Close cleans up the test context. +func (tctx *testContext) Close() { + tctx.t.Helper() + + for _, lnd := range tctx.lnds { + lnd.Close() + } + + _ = tctx.bitcoinCli("stop") + _ = tctx.vaultCmd.Process.Signal(os.Interrupt) + + tctx.cancel() + + os.RemoveAll(tctx.tmpRoot) +} + +// addNode adds a new LND node to the test context, complete with its own +// lndsignerd. reqPath can be used to specify create or import, reqData must +// have a network and optional seed/passphrase, and unixSocket may be used to +// specify that a UNIX socket should be used to communicate between LND and +// lndsignerd. +func (tctx *testContext) addNode(reqPath string, + reqData map[string]interface{}, unixSocket bool) string { + + tctx.t.Helper() + + resp, err := tctx.vaultClient.Write(reqPath, reqData) + require.NoError(tctx.t, err) + + pubKey, ok := resp.Data["node"].(string) + require.True(tctx.t, ok) + require.Equal(tctx.t, 66, len(pubKey)) + + lnd := &lndHarness{ + tctx: tctx, + idPubKey: pubKey, + unixSocket: unixSocket, + } + + lnd.Start() + + tctx.lnds = append(tctx.lnds, lnd) + + return pubKey +} + +// mine mines the specified number of blocks, and ensures `getblockchaininfo` +// returns the correct number. +func (tctx *testContext) mine(blocks int) { + tctx.t.Helper() + + // Ensure all TXs are accepted to mempool. + time.Sleep(mineDelay) + + require.Equal(tctx.t, blocks, + len(tctx.bitcoinCli("-generate", fmt.Sprintf( + "%d", blocks))["blocks"].([]interface{}))) + + reqMined := int(atomic.AddUint32(&tctx.blocksMined, uint32(blocks))) + + var ( + mined int + resp map[string]interface{} + ) + + for mined < reqMined || mined < 1000 { + time.Sleep(waitDelay) + + resp = tctx.bitcoinCli("getblockchaininfo") + mined = int(resp["blocks"].(float64)) + } +} + +// waitForSync ensures that each LND has caught up to the blocks that have been +// mined on bitcoind. +func (tctx *testContext) waitForSync() { + tctx.t.Helper() + + // Ensure everyone has time to catch up. + time.Sleep(mineDelay) + + blocks := int(atomic.LoadUint32(&tctx.blocksMined)) + + var resp map[string]interface{} + for _, lnd := range tctx.lnds { + synced := 0 + + for synced != blocks { + time.Sleep(waitDelay) + + resp = lnd.Lncli("getinfo") + require.Equal(tctx.t, resp["identity_pubkey"].(string), + lnd.idPubKey) + + synced = int(resp["block_height"].(float64)) + } + } +} + +// waitForGraphSync ensures that each LND has a synchronized graph. +func (tctx *testContext) waitForGraphSync() { + tctx.t.Helper() + + var ( + nodes, chans int + synced bool + resp map[string]interface{} + ) + + for !synced { + synced = true + + for _, lnd := range tctx.lnds { + + resp = lnd.Lncli("getnetworkinfo") + + gotNodes := int(resp["num_nodes"].(float64)) + if gotNodes != nodes { + synced = false + + if gotNodes > nodes { + nodes = gotNodes + } + } + + gotChans := int(resp["num_channels"].(float64)) + if gotChans != chans { + synced = false + + if gotChans > chans { + chans = gotChans + } + } + } + + time.Sleep(mineDelay) + } + + // One final round of `describegraph` to ensure we've updated our + // routing information. + for _, lnd := range tctx.lnds { + lnd.Lncli("describegraph") + } +} + +// testEach runs a function for each LND instance, in parallel. +func (tctx *testContext) testEach(test func(lnd *lndHarness)) { + tctx.t.Helper() + + var wg sync.WaitGroup + for _, lnd := range tctx.lnds { + innerLnd := lnd + + wg.Add(1) + go func() { + defer wg.Done() + + test(innerLnd) + }() + } + + wg.Wait() +} + +// testEachPair runs a function for each pair of LNDs in parallel, avoiding +// testing an LND instance with itself. +func (tctx *testContext) testEachPair(test func(lnd1, lnd2 *lndHarness)) { + tctx.t.Helper() + + var wg sync.WaitGroup + for i, lnd1 := range tctx.lnds { + for j, lnd2 := range tctx.lnds { + innerLnd1 := lnd1 + innerLnd2 := lnd2 + + if i == j { + continue + } + + wg.Add(1) + go func() { + defer wg.Done() + + test(innerLnd1, innerLnd2) + }() + } + } + + wg.Wait() +} diff --git a/itest/itest_lndharness_test.go b/itest/itest_lndharness_test.go new file mode 100644 index 0000000..228dd2b --- /dev/null +++ b/itest/itest_lndharness_test.go @@ -0,0 +1,397 @@ +//go:build itest +// +build itest + +package itest_test + +import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/json" + "encoding/pem" + "fmt" + "github.com/bottlepay/lndsigner" + "github.com/bottlepay/lndsigner/itest" + "io/fs" + "math/big" + "net" + "os" + "os/exec" + "path" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +// lndHarness manages a single lndsignerd-backed instance of LND. +type lndHarness struct { + tctx *testContext + idPubKey string + + unixSocket bool + + cancel context.CancelFunc + + lndSignerCmd *exec.Cmd + + lndDir string + lncliPath string + rpc string + p2p string + lndCmd *exec.Cmd + + startChan chan struct{} +} + +// Start takes the initial configuration (tctx, idPubKey, and unixSocket) and +// starts lndsignerd and LND. +func (l *lndHarness) Start() { + l.tctx.t.Helper() + + ctx, cancel := context.WithCancel(context.Background()) + l.cancel = cancel + + // Make a channel. We'll close this channel after the node is fully + // started to signal clients it's safe to make calls. + l.startChan = make(chan struct{}) + + // Start lndsignerd. + l.lndDir = path.Join(l.tctx.tmpRoot, fmt.Sprintf("lnd%s", l.idPubKey)) + err := os.Mkdir(l.lndDir, fs.ModeDir|0700) + require.NoError(l.tctx.t, err) + + keyPath := path.Join(l.lndDir, "signer.key") + certPath := path.Join(l.lndDir, "signer.cert") + + mustGenCertPair(l.tctx.t, certPath, keyPath) + + macPath := path.Join(l.lndDir, "dummy.macaroon") + + mustGenMacaroon(l.tctx.t, macPath) + + signerAddr := "127.0.0.1:" + newPortString() + fullSignerAddr := "tcp://" + signerAddr + + if l.unixSocket { + signerAddr = "unix://" + path.Join(l.tctx.tmpRoot, l.idPubKey+".socket") + fullSignerAddr = signerAddr + } + + l.lndSignerCmd = exec.CommandContext(ctx, l.tctx.lndSignerPath, + "--rpclisten="+fullSignerAddr, "--nodepubkey="+l.idPubKey, + "--tlscertpath="+certPath, "--tlskeypath="+keyPath, + "--network=regtest", + ) + + l.lndSignerCmd.Env = append(l.lndSignerCmd.Env, + "VAULT_ADDR=http://127.0.0.1:"+l.tctx.vaultPort, + "VAULT_TOKEN=root", + ) + + go waitProc(l.lndSignerCmd) + + // Start lnd. + acctsResp, err := l.tctx.vaultClient.ReadWithData( + "lndsigner/lnd-nodes/accounts", + map[string][]string{ + "node": []string{l.idPubKey}, + }, + ) + require.NoError(l.tctx.t, err) + + acctList, ok := acctsResp.Data["acctList"].(string) + require.True(l.tctx.t, ok) + + accounts, err := lndsigner.GetAccounts(acctList) + require.NoError(l.tctx.t, err) + + grpcAccounts := make([]*itest.WatchOnlyAccount, 0, + len(accounts)) + + for derPath, xPub := range accounts { + grpcAccounts = append(grpcAccounts, + &itest.WatchOnlyAccount{ + Purpose: derPath[0], + CoinType: derPath[1], + Account: derPath[2], + Xpub: xPub, + }) + } + + l.rpc = newPortString() + l.p2p = newPortString() + + l.lndCmd = exec.CommandContext(ctx, l.tctx.lndPath, + "--lnddir="+l.lndDir, "--norest", "--listen="+l.p2p, + "--rpclisten="+l.rpc, "--trickledelay=1", "--bitcoin.active", + "--bitcoin.regtest", "--bitcoin.node=bitcoind", + "--bitcoind.rpcuser=user", "--bitcoind.rpcpass=password", + "--bitcoind.rpchost=127.0.0.1:"+l.tctx.bitcoinRPC, + "--bitcoind.zmqpubrawblock=tcp://"+l.tctx.bitcoinZB.String(), + "--bitcoind.zmqpubrawtx=tcp://"+l.tctx.bitcoinZT.String(), + "--remotesigner.enable", + "--remotesigner.rpchost="+signerAddr, + "--remotesigner.tlscertpath="+certPath, + "--remotesigner.macaroonpath="+macPath, + ) + + go waitProc(l.lndCmd) + + go func() { + // Ensure we wait until lnd has started its wallet unlocker + // server. + waitFile( + l.tctx.t, + path.Join(l.lndDir, "/logs/bitcoin/regtest/lnd.log"), + "Waiting for wallet encryption password", + ) + + // Initialize with the accounts information. We use gRPC for this + // because lncli doesn't run non-interactively, so we have to send a + // wallet password over gRPC. + tlsCreds, err := credentials.NewClientTLSFromFile( + path.Join(l.lndDir, "tls.cert"), "") + require.NoError(l.tctx.t, err) + + tlsCredsOption := grpc.WithTransportCredentials(tlsCreds) + unlockerConn, err := grpc.Dial("127.0.0.1:"+l.rpc, tlsCredsOption) + require.NoError(l.tctx.t, err) + + unlocker := itest.NewWalletUnlockerClient(unlockerConn) + _, err = unlocker.InitWallet( + ctx, + &itest.InitWalletRequest{ + WalletPassword: []byte("weks1234"), + WatchOnly: &itest.WatchOnly{ + Accounts: grpcAccounts, + }, + }, + ) + require.NoError(l.tctx.t, err) + + // Wait for lnd to start the main gRPC server. Log file is + // ~7300 bytes when the RPC server is started. + // TODO(aakselrod): maybe check log file for + // "Auto peer bootstrapping" instead? + waitFile( + l.tctx.t, + path.Join(l.lndDir, "/logs/bitcoin/regtest/lnd.log"), + "Auto peer bootstrapping", + ) + + // Signal any waiting clients that lnd should be initialized + close(l.startChan) + }() +} + +// Close cleans up LND and lndsignerd. +func (l *lndHarness) Close() { + l.tctx.t.Helper() + + _ = l.Lncli("stop") + _ = l.lndSignerCmd.Process.Signal(os.Interrupt) + + l.cancel() +} + +// LnCli calls lncli against the harness' LND instance. +func (l *lndHarness) Lncli(args ...string) map[string]interface{} { + l.tctx.t.Helper() + + <-l.startChan + + lnCliCmd := exec.CommandContext(context.Background(), l.tctx.lncliPath, + append([]string{"--lnddir=" + l.lndDir, + "--rpcserver=127.0.0.1:" + l.rpc, + "--network=regtest", + "--tlscertpath=./testdata/tls.cert"}, args...)...) + + outBuf := bytes.NewBuffer(make([]byte, 0)) + lnCliCmd.Stdout = outBuf + + errBuf := bytes.NewBuffer(make([]byte, 0)) + lnCliCmd.Stderr = errBuf + + err := lnCliCmd.Start() + require.NoError(l.tctx.t, err) + + err = lnCliCmd.Wait() + require.NoError(l.tctx.t, err, + fmt.Sprintf("lncli (args %+v) failed:\n%s\n%s", args, + errBuf.Bytes(), outBuf.Bytes())) + + stdout := string(outBuf.Bytes()) + + // If we're stopping, we won't get JSON back. + if args[0] == "stop" { + return nil + } + + resp := make(map[string]interface{}) + err = json.Unmarshal([]byte(stdout), &resp) + require.NoError(l.tctx.t, err) + + return resp +} + +// waitFile waits for a log file to contain the requested string. +func waitFile(t *testing.T, file, waitStr string) { + var ( + err error + logBytes []byte + ) + + for { + time.Sleep(waitDelay) + + logBytes, err = os.ReadFile(file) + if err != nil { + require.True(t, os.IsNotExist(err), err) + } + + if bytes.Contains(logBytes, []byte(waitStr)) { + break + } + } +} + +// waitProc launches a goroutine to wait for a long-running program, such as +// vault, bitcoind, lndsignerd, or lnd, to stop. If the program returns an +// exit error, the program's entire stderr and stdout are logged. +func waitProc(cmd *exec.Cmd) { + output, err := cmd.CombinedOutput() + if err != nil && err.Error() != "signal: killed" { + config := zap.NewDevelopmentConfig() + config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder + config.EncoderConfig.EncodeCaller = nil + logger := zap.Must(config.Build()).Sugar() + logger.Warnw( + "WARNING: Service exited with error", + "cmd", cmd.Path, + "err", err, + "stdout/stderr", string(output), + ) + } +} + +// newPort finds an open TCP port to listen on. +func newPort() *net.TCPAddr { + lis, err := net.Listen("tcp", "127.0.0.1:") + if err != nil { + panic(err) + } + defer lis.Close() + return lis.Addr().(*net.TCPAddr) +} + +// newPortString finds an open TCP port to listen on and returns the port +// number as a string. +func newPortString() string { + return fmt.Sprintf("%d", newPort().Port) +} + +// mustCopyFile copies a file and panics on error. +func mustCopyFile(src, dst string, mode os.FileMode) { + fileBytes, err := os.ReadFile(src) + if err != nil { + panic(err) + } + + err = os.WriteFile(dst, fileBytes, mode) + if err != nil { + panic(err) + } +} + +func mustGenCertPair(t *testing.T, certFile, keyFile string) { + now := time.Now() + + // Generate a random serial number. + serialNumber, err := rand.Int(rand.Reader, + new(big.Int).Lsh(big.NewInt(1), 128)) + require.NoError(t, err) + + // Generate a private key for the certificate. + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + // Construct the certificate template. + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"test"}, + CommonName: "localhost", + }, + NotBefore: now.Add(-time.Hour * 24), + NotAfter: now.Add(365 * 24 * time.Hour), + + KeyUsage: x509.KeyUsageKeyEncipherment | + x509.KeyUsageDigitalSignature | + x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, + }, + IsCA: true, // so can sign self. + BasicConstraintsValid: true, + + DNSNames: []string{"localhost"}, + IPAddresses: []net.IP{net.IPv6loopback, net.IP{127, 0, 0, 1}}, + } + + derBytes, err := x509.CreateCertificate( + rand.Reader, &template, + &template, &priv.PublicKey, priv, + ) + require.NoError(t, err) + + certBuf := &bytes.Buffer{} + require.NoError(t, pem.Encode( + certBuf, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}, + )) + + keybytes, err := x509.MarshalECPrivateKey(priv) + require.NoError(t, err) + + keyBuf := &bytes.Buffer{} + require.NoError(t, pem.Encode( + keyBuf, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keybytes}, + )) + + require.NoError(t, os.WriteFile(certFile, certBuf.Bytes(), 0644)) + + require.NoError(t, os.WriteFile(keyFile, keyBuf.Bytes(), 0600)) +} + +func mustGenMacaroon(t *testing.T, macPath string) { + var macData []byte + + macData = append(macData, 2) // version + macData = append(macData, 1) // field type loc + macData = append(macData, 1) // length + macData = append(macData, 65) // loc ("A") + macData = append(macData, 2) // field type id + macData = append(macData, 1) // length + macData = append(macData, 65) // id ("A") + macData = append(macData, 0) // end of seq + macData = append(macData, 0) // end of seq + macData = append(macData, 6) // field type sig + macData = append(macData, 32) // length + macData = append(macData, // sig (32 * "A") + 65, 65, 65, 65, 65, 65, 65, 65, + 65, 65, 65, 65, 65, 65, 65, 65, + 65, 65, 65, 65, 65, 65, 65, 65, + 65, 65, 65, 65, 65, 65, 65, 65, + ) + macData = append(macData, 0) // end of seq + + require.NoError(t, os.WriteFile(macPath, macData, 0644)) +} diff --git a/itest/lndsigner_test.go b/itest/lndsigner_test.go new file mode 100644 index 0000000..937b3f8 --- /dev/null +++ b/itest/lndsigner_test.go @@ -0,0 +1,198 @@ +//go:build itest +// +build itest + +package itest_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +const ( + lndCreatePath = "lndsigner/lnd-nodes" + lndImportPath = "lndsigner/lnd-nodes/import" + + mineDelay = 500 * time.Millisecond + waitDelay = 100 * time.Millisecond +) + +// TestIntegration function runs end-to-end tests using all of the required +// binaries. +// +// This assumes we've got `lnd`, `lncli`, `vault`, `bitcoind`, `bitcoin-cli`, +// and the binaries produced by this package installed and available in the +// executable path. These are installed in CI by the GitHub workflow, but +// for now need to be installed manually in the dev environment. +// +// TODO(aakselrod): add Dockerfile to dockerize itests locally. +func TestIntegration(t *testing.T) { + tctx := newTestContext(t) + defer tctx.Close() + + // Create a randomly-initialized node for which nobody's ever seen the + // keys. + + _ = tctx.addNode(lndCreatePath, map[string]interface{}{ + "network": "regtest", + }, true) + + // Import node without passphrase. + lnd2PK := "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6" + require.Equal(t, tctx.addNode(lndImportPath, map[string]interface{}{ + "network": "regtest", + "seedphrase": "absent walnut slam olive squeeze cluster blame express asthma gym force warfare physical stuff unusual tiny endless patient again sound deny identify fall guard", + "passphrase": "", + "node": lnd2PK, + }, true), lnd2PK) + + // Import node with passphrase. + lnd3PK := "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf" + require.Equal(t, tctx.addNode(lndImportPath, map[string]interface{}{ + "network": "testnet", + "seedphrase": "abstract inch live custom just tray hockey enroll upon friend mass author filter desert parrot network finger uniform alley artefact path palace chicken diet", + "passphrase": "weks1234", + "node": lnd3PK, + }, false), lnd3PK) + + tctx.waitForSync() + + t.Run("fund each lnd with a p2tr address", tctx.testFundLnds) + + tctx.mine(1) + tctx.waitForSync() + + t.Run("sweep p2tr to p2wkh address", tctx.testSweepToP2WKH) + + tctx.mine(1) + tctx.waitForSync() + + t.Run("sweep p2wkh to np2wkh address", tctx.testSweepToNP2WKH) + + tctx.mine(1) + tctx.waitForSync() + + t.Run("sweep np2wkh to p2tr address", tctx.testSweepToP2TR) + + tctx.mine(1) + tctx.waitForSync() + + t.Run("open channel lnd1 to lnd2", func(t *testing.T) { + _ = tctx.lnds[0].Lncli("connect", + tctx.lnds[1].idPubKey+"@127.0.0.1:"+tctx.lnds[1].p2p) + + resp := tctx.lnds[0].Lncli("openchannel", tctx.lnds[1].idPubKey, + "10000000", "5000000") + require.Equal(t, 64, len(resp["funding_txid"].(string))) + }) + + t.Run("open channel lnd2 to lnd3", func(t *testing.T) { + _ = tctx.lnds[1].Lncli("connect", + tctx.lnds[2].idPubKey+"@127.0.0.1:"+tctx.lnds[2].p2p) + + resp := tctx.lnds[1].Lncli("openchannel", tctx.lnds[2].idPubKey, + "10000000", "5000000") + require.Equal(t, 64, len(resp["funding_txid"].(string))) + }) + + // Confirm our channels. + tctx.mine(5) + tctx.waitForSync() + tctx.mine(5) + tctx.waitForSync() + tctx.waitForGraphSync() + + t.Run("sign and verify messages", tctx.testEachSignVerifyEachOther) + + t.Run("each lnd pays every other lnd", tctx.testEachPaysEachOther) +} + +// testFundLnds funds each lnd instance in the test context with 1 BTC into +// a new P2TR address. +func (tctx *testContext) testFundLnds(t *testing.T) { + tctx.testEach(func(lnd *lndHarness) { + resp := lnd.Lncli("newaddress", "p2tr") + address := resp["address"].(string) + + tctx.bitcoinCli("-named", "sendtoaddress", + "address="+address, "amount=1", "fee_rate=25") + }) +} + +// testSweepToP2WKH sweeps all of the nodes' on-chain funds into P2WKH +// addresses +func (tctx *testContext) testSweepToP2WKH(t *testing.T) { + tctx.testEach(func(lnd *lndHarness) { + resp := lnd.Lncli("newaddress", "p2wkh") + address := resp["address"].(string) + + resp = lnd.Lncli("sendcoins", "--sweepall", + address) + require.Equal(t, 64, len(resp["txid"].(string))) + + tctx.log.Infow("swept", "node", lnd.idPubKey) + }) +} + +// testSweepToNP2WKH sweeps all of the nodes' on-chain funds into NP2WKH +// addresses +func (tctx *testContext) testSweepToNP2WKH(t *testing.T) { + tctx.testEach(func(lnd *lndHarness) { + resp := lnd.Lncli("newaddress", "np2wkh") + address := resp["address"].(string) + + resp = lnd.Lncli("sendcoins", "--sweepall", + address) + require.Equal(t, 64, len(resp["txid"].(string))) + + tctx.log.Infow("swept", "node", lnd.idPubKey) + }) +} + +// testSweepToP2TR sweeps all of the nodes' on-chain funds into P2TR +// addresses +func (tctx *testContext) testSweepToP2TR(t *testing.T) { + tctx.testEach(func(lnd *lndHarness) { + resp := lnd.Lncli("newaddress", "p2tr") + address := resp["address"].(string) + + resp = lnd.Lncli("sendcoins", "--sweepall", + address) + require.Equal(t, 64, len(resp["txid"].(string))) + + tctx.log.Infow("swept", "node", lnd.idPubKey) + }) +} + +// testEachPaysEachOther sends LN payments from each LND to each other LND, +// testing both direct and chained payments. +func (tctx *testContext) testEachPaysEachOther(t *testing.T) { + tctx.testEachPair(func(lnd1, lnd2 *lndHarness) { + resp := lnd1.Lncli("addinvoice", "5000") + invoice := resp["payment_request"].(string) + + resp = lnd2.Lncli("payinvoice", "--timeout=10s", "--json", + "-f", invoice) + require.Equal(t, resp["status"].(string), "SUCCEEDED") + + tctx.log.Infow("payment", "src", lnd2.idPubKey, + "dst", lnd1.idPubKey) + }) +} + +// testEachSignVerifyEachOther signs a message from each LND to each other LND, +// verifying the message on the second LND. +func (tctx *testContext) testEachSignVerifyEachOther(t *testing.T) { + tctx.testEachPair(func(lnd1, lnd2 *lndHarness) { + message := lnd1.idPubKey + " to " + lnd2.idPubKey + + resp := lnd1.Lncli("signmessage", message) + sig := resp["signature"].(string) + + resp = lnd2.Lncli("verifymessage", message, sig) + require.True(t, resp["valid"].(bool)) + + tctx.log.Info(message) + }) +} diff --git a/itest/walletunlocker.pb.go b/itest/walletunlocker.pb.go new file mode 100644 index 0000000..0202fb9 --- /dev/null +++ b/itest/walletunlocker.pb.go @@ -0,0 +1,423 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0-devel +// protoc v3.14.0 +// source: walletunlocker.proto + +package itest + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type InitWalletRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //wallet_password is the passphrase that should be used to encrypt the + //wallet. This MUST be at least 8 chars in length. After creation, this + //password is required to unlock the daemon. When using REST, this field + //must be encoded as base64. + WalletPassword []byte `protobuf:"bytes,1,opt,name=wallet_password,json=walletPassword,proto3" json:"wallet_password,omitempty"` + // + //watch_only is the third option of initializing a wallet: by importing + //account xpubs only and therefore creating a watch-only wallet that does not + //contain any private keys. That means the wallet won't be able to sign for + //any of the keys and _needs_ to be run with a remote signer that has the + //corresponding private keys and can serve signing RPC requests. + WatchOnly *WatchOnly `protobuf:"bytes,9,opt,name=watch_only,json=watchOnly,proto3" json:"watch_only,omitempty"` +} + +func (x *InitWalletRequest) Reset() { + *x = InitWalletRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_walletunlocker_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *InitWalletRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InitWalletRequest) ProtoMessage() {} + +func (x *InitWalletRequest) ProtoReflect() protoreflect.Message { + mi := &file_walletunlocker_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InitWalletRequest.ProtoReflect.Descriptor instead. +func (*InitWalletRequest) Descriptor() ([]byte, []int) { + return file_walletunlocker_proto_rawDescGZIP(), []int{0} +} + +func (x *InitWalletRequest) GetWalletPassword() []byte { + if x != nil { + return x.WalletPassword + } + return nil +} + +func (x *InitWalletRequest) GetWatchOnly() *WatchOnly { + if x != nil { + return x.WatchOnly + } + return nil +} + +type InitWalletResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The binary serialized admin macaroon that can be used to access the daemon + //after creating the wallet. If the stateless_init parameter was set to true, + //this is the ONLY copy of the macaroon and MUST be stored safely by the + //caller. Otherwise a copy of this macaroon is also persisted on disk by the + //daemon, together with other macaroon files. + AdminMacaroon []byte `protobuf:"bytes,1,opt,name=admin_macaroon,json=adminMacaroon,proto3" json:"admin_macaroon,omitempty"` +} + +func (x *InitWalletResponse) Reset() { + *x = InitWalletResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_walletunlocker_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *InitWalletResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InitWalletResponse) ProtoMessage() {} + +func (x *InitWalletResponse) ProtoReflect() protoreflect.Message { + mi := &file_walletunlocker_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InitWalletResponse.ProtoReflect.Descriptor instead. +func (*InitWalletResponse) Descriptor() ([]byte, []int) { + return file_walletunlocker_proto_rawDescGZIP(), []int{1} +} + +func (x *InitWalletResponse) GetAdminMacaroon() []byte { + if x != nil { + return x.AdminMacaroon + } + return nil +} + +type WatchOnly struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The list of accounts to import. There _must_ be an account for all of lnd's + //main key scopes: BIP49/BIP84 (m/49'/0'/0', m/84'/0'/0', note that the + //coin type is always 0, even for testnet/regtest) and lnd's internal key + //scope (m/1017'/'/'), where account is the key family as + //defined in `keychain/derivation.go` (currently indices 0 to 9). + Accounts []*WatchOnlyAccount `protobuf:"bytes,3,rep,name=accounts,proto3" json:"accounts,omitempty"` +} + +func (x *WatchOnly) Reset() { + *x = WatchOnly{} + if protoimpl.UnsafeEnabled { + mi := &file_walletunlocker_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WatchOnly) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WatchOnly) ProtoMessage() {} + +func (x *WatchOnly) ProtoReflect() protoreflect.Message { + mi := &file_walletunlocker_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WatchOnly.ProtoReflect.Descriptor instead. +func (*WatchOnly) Descriptor() ([]byte, []int) { + return file_walletunlocker_proto_rawDescGZIP(), []int{2} +} + +func (x *WatchOnly) GetAccounts() []*WatchOnlyAccount { + if x != nil { + return x.Accounts + } + return nil +} + +type WatchOnlyAccount struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //Purpose is the first number in the derivation path, must be either 49, 84 + //or 1017. + Purpose uint32 `protobuf:"varint,1,opt,name=purpose,proto3" json:"purpose,omitempty"` + // + //Coin type is the second number in the derivation path, this is _always_ 0 + //for purposes 49 and 84. It only needs to be set to 1 for purpose 1017 on + //testnet or regtest. + CoinType uint32 `protobuf:"varint,2,opt,name=coin_type,json=coinType,proto3" json:"coin_type,omitempty"` + // + //Account is the third number in the derivation path. For purposes 49 and 84 + //at least the default account (index 0) needs to be created but optional + //additional accounts are allowed. For purpose 1017 there needs to be exactly + //one account for each of the key families defined in `keychain/derivation.go` + //(currently indices 0 to 9) + Account uint32 `protobuf:"varint,3,opt,name=account,proto3" json:"account,omitempty"` + // + //The extended public key at depth 3 for the given account. + Xpub string `protobuf:"bytes,4,opt,name=xpub,proto3" json:"xpub,omitempty"` +} + +func (x *WatchOnlyAccount) Reset() { + *x = WatchOnlyAccount{} + if protoimpl.UnsafeEnabled { + mi := &file_walletunlocker_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WatchOnlyAccount) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WatchOnlyAccount) ProtoMessage() {} + +func (x *WatchOnlyAccount) ProtoReflect() protoreflect.Message { + mi := &file_walletunlocker_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WatchOnlyAccount.ProtoReflect.Descriptor instead. +func (*WatchOnlyAccount) Descriptor() ([]byte, []int) { + return file_walletunlocker_proto_rawDescGZIP(), []int{3} +} + +func (x *WatchOnlyAccount) GetPurpose() uint32 { + if x != nil { + return x.Purpose + } + return 0 +} + +func (x *WatchOnlyAccount) GetCoinType() uint32 { + if x != nil { + return x.CoinType + } + return 0 +} + +func (x *WatchOnlyAccount) GetAccount() uint32 { + if x != nil { + return x.Account + } + return 0 +} + +func (x *WatchOnlyAccount) GetXpub() string { + if x != nil { + return x.Xpub + } + return "" +} + +var File_walletunlocker_proto protoreflect.FileDescriptor + +var file_walletunlocker_proto_rawDesc = []byte{ + 0x0a, 0x14, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x75, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x72, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x22, 0x6d, 0x0a, + 0x11, 0x49, 0x6e, 0x69, 0x74, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x5f, 0x70, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x77, 0x61, 0x6c, + 0x6c, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x2f, 0x0a, 0x0a, 0x77, + 0x61, 0x74, 0x63, 0x68, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x10, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x4f, 0x6e, 0x6c, + 0x79, 0x52, 0x09, 0x77, 0x61, 0x74, 0x63, 0x68, 0x4f, 0x6e, 0x6c, 0x79, 0x22, 0x3b, 0x0a, 0x12, + 0x49, 0x6e, 0x69, 0x74, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x5f, 0x6d, 0x61, 0x63, 0x61, + 0x72, 0x6f, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x61, 0x64, 0x6d, 0x69, + 0x6e, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x22, 0x40, 0x0a, 0x09, 0x57, 0x61, 0x74, + 0x63, 0x68, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x33, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, + 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x4f, 0x6e, 0x6c, 0x79, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x52, 0x08, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x22, 0x77, 0x0a, 0x10, 0x57, + 0x61, 0x74, 0x63, 0x68, 0x4f, 0x6e, 0x6c, 0x79, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, + 0x18, 0x0a, 0x07, 0x70, 0x75, 0x72, 0x70, 0x6f, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x07, 0x70, 0x75, 0x72, 0x70, 0x6f, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x69, + 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x63, 0x6f, + 0x69, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x12, 0x12, 0x0a, 0x04, 0x78, 0x70, 0x75, 0x62, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x78, 0x70, 0x75, 0x62, 0x32, 0x53, 0x0a, 0x0e, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x55, 0x6e, + 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x12, 0x41, 0x0a, 0x0a, 0x49, 0x6e, 0x69, 0x74, 0x57, 0x61, + 0x6c, 0x6c, 0x65, 0x74, 0x12, 0x18, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x69, + 0x74, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, + 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x57, 0x61, 0x6c, 0x6c, 0x65, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x6f, 0x74, 0x74, 0x6c, 0x65, 0x70, 0x61, + 0x79, 0x2f, 0x6c, 0x6e, 0x64, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x2f, 0x69, 0x74, 0x65, 0x73, + 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_walletunlocker_proto_rawDescOnce sync.Once + file_walletunlocker_proto_rawDescData = file_walletunlocker_proto_rawDesc +) + +func file_walletunlocker_proto_rawDescGZIP() []byte { + file_walletunlocker_proto_rawDescOnce.Do(func() { + file_walletunlocker_proto_rawDescData = protoimpl.X.CompressGZIP(file_walletunlocker_proto_rawDescData) + }) + return file_walletunlocker_proto_rawDescData +} + +var file_walletunlocker_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_walletunlocker_proto_goTypes = []interface{}{ + (*InitWalletRequest)(nil), // 0: lnrpc.InitWalletRequest + (*InitWalletResponse)(nil), // 1: lnrpc.InitWalletResponse + (*WatchOnly)(nil), // 2: lnrpc.WatchOnly + (*WatchOnlyAccount)(nil), // 3: lnrpc.WatchOnlyAccount +} +var file_walletunlocker_proto_depIdxs = []int32{ + 2, // 0: lnrpc.InitWalletRequest.watch_only:type_name -> lnrpc.WatchOnly + 3, // 1: lnrpc.WatchOnly.accounts:type_name -> lnrpc.WatchOnlyAccount + 0, // 2: lnrpc.WalletUnlocker.InitWallet:input_type -> lnrpc.InitWalletRequest + 1, // 3: lnrpc.WalletUnlocker.InitWallet:output_type -> lnrpc.InitWalletResponse + 3, // [3:4] is the sub-list for method output_type + 2, // [2:3] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_walletunlocker_proto_init() } +func file_walletunlocker_proto_init() { + if File_walletunlocker_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_walletunlocker_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*InitWalletRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_walletunlocker_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*InitWalletResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_walletunlocker_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WatchOnly); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_walletunlocker_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WatchOnlyAccount); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_walletunlocker_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_walletunlocker_proto_goTypes, + DependencyIndexes: file_walletunlocker_proto_depIdxs, + MessageInfos: file_walletunlocker_proto_msgTypes, + }.Build() + File_walletunlocker_proto = out.File + file_walletunlocker_proto_rawDesc = nil + file_walletunlocker_proto_goTypes = nil + file_walletunlocker_proto_depIdxs = nil +} diff --git a/itest/walletunlocker.proto b/itest/walletunlocker.proto new file mode 100644 index 0000000..eeb86af --- /dev/null +++ b/itest/walletunlocker.proto @@ -0,0 +1,112 @@ +syntax = "proto3"; + +package lnrpc; + +option go_package = "github.com/bottlepay/lndsigner/itest"; + +/* + * Comments in this file will be directly parsed into the API + * Documentation as descriptions of the associated method, message, or field. + * These descriptions should go right above the definition of the object, and + * can be in either block or // comment format. + * + * An RPC method can be matched to an lncli command by placing a line in the + * beginning of the description in exactly the following format: + * lncli: `methodname` + * + * Failure to specify the exact name of the command will cause documentation + * generation to fail. + * + * More information on how exactly the gRPC documentation is generated from + * this proto file can be found here: + * https://github.com/lightninglabs/lightning-api + */ + +// WalletUnlocker is a service that is used to set up a wallet password for +// lnd at first startup, and unlock a previously set up wallet. +service WalletUnlocker { + /* + InitWallet is used when lnd is starting up for the first time to fully + initialize the daemon and its internal wallet. At the very least a wallet + password must be provided. This will be used to encrypt sensitive material + on disk. + + In the case of a recovery scenario, the user can also specify their aezeed + mnemonic and passphrase. If set, then the daemon will use this prior state + to initialize its internal wallet. + + Alternatively, this can be used along with the GenSeed RPC to obtain a + seed, then present it to the user. Once it has been verified by the user, + the seed can be fed into this RPC in order to commit the new wallet. + */ + rpc InitWallet(InitWalletRequest) returns (InitWalletResponse); +} + +message InitWalletRequest { + /* + wallet_password is the passphrase that should be used to encrypt the + wallet. This MUST be at least 8 chars in length. After creation, this + password is required to unlock the daemon. When using REST, this field + must be encoded as base64. + */ + bytes wallet_password = 1; + + /* + watch_only is the third option of initializing a wallet: by importing + account xpubs only and therefore creating a watch-only wallet that does not + contain any private keys. That means the wallet won't be able to sign for + any of the keys and _needs_ to be run with a remote signer that has the + corresponding private keys and can serve signing RPC requests. + */ + WatchOnly watch_only = 9; +} +message InitWalletResponse { + /* + The binary serialized admin macaroon that can be used to access the daemon + after creating the wallet. If the stateless_init parameter was set to true, + this is the ONLY copy of the macaroon and MUST be stored safely by the + caller. Otherwise a copy of this macaroon is also persisted on disk by the + daemon, together with other macaroon files. + */ + bytes admin_macaroon = 1; +} + +message WatchOnly { + /* + The list of accounts to import. There _must_ be an account for all of lnd's + main key scopes: BIP49/BIP84 (m/49'/0'/0', m/84'/0'/0', note that the + coin type is always 0, even for testnet/regtest) and lnd's internal key + scope (m/1017'/'/'), where account is the key family as + defined in `keychain/derivation.go` (currently indices 0 to 9). + */ + repeated WatchOnlyAccount accounts = 3; +} + +message WatchOnlyAccount { + /* + Purpose is the first number in the derivation path, must be either 49, 84 + or 1017. + */ + uint32 purpose = 1; + + /* + Coin type is the second number in the derivation path, this is _always_ 0 + for purposes 49 and 84. It only needs to be set to 1 for purpose 1017 on + testnet or regtest. + */ + uint32 coin_type = 2; + + /* + Account is the third number in the derivation path. For purposes 49 and 84 + at least the default account (index 0) needs to be created but optional + additional accounts are allowed. For purpose 1017 there needs to be exactly + one account for each of the key families defined in `keychain/derivation.go` + (currently indices 0 to 9) + */ + uint32 account = 3; + + /* + The extended public key at depth 3 for the given account. + */ + string xpub = 4; +} diff --git a/itest/walletunlocker_grpc.pb.go b/itest/walletunlocker_grpc.pb.go new file mode 100644 index 0000000..da38cfc --- /dev/null +++ b/itest/walletunlocker_grpc.pb.go @@ -0,0 +1,127 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package itest + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// WalletUnlockerClient is the client API for WalletUnlocker service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type WalletUnlockerClient interface { + // + //InitWallet is used when lnd is starting up for the first time to fully + //initialize the daemon and its internal wallet. At the very least a wallet + //password must be provided. This will be used to encrypt sensitive material + //on disk. + // + //In the case of a recovery scenario, the user can also specify their aezeed + //mnemonic and passphrase. If set, then the daemon will use this prior state + //to initialize its internal wallet. + // + //Alternatively, this can be used along with the GenSeed RPC to obtain a + //seed, then present it to the user. Once it has been verified by the user, + //the seed can be fed into this RPC in order to commit the new wallet. + InitWallet(ctx context.Context, in *InitWalletRequest, opts ...grpc.CallOption) (*InitWalletResponse, error) +} + +type walletUnlockerClient struct { + cc grpc.ClientConnInterface +} + +func NewWalletUnlockerClient(cc grpc.ClientConnInterface) WalletUnlockerClient { + return &walletUnlockerClient{cc} +} + +func (c *walletUnlockerClient) InitWallet(ctx context.Context, in *InitWalletRequest, opts ...grpc.CallOption) (*InitWalletResponse, error) { + out := new(InitWalletResponse) + err := c.cc.Invoke(ctx, "/lnrpc.WalletUnlocker/InitWallet", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// WalletUnlockerServer is the server API for WalletUnlocker service. +// All implementations must embed UnimplementedWalletUnlockerServer +// for forward compatibility +type WalletUnlockerServer interface { + // + //InitWallet is used when lnd is starting up for the first time to fully + //initialize the daemon and its internal wallet. At the very least a wallet + //password must be provided. This will be used to encrypt sensitive material + //on disk. + // + //In the case of a recovery scenario, the user can also specify their aezeed + //mnemonic and passphrase. If set, then the daemon will use this prior state + //to initialize its internal wallet. + // + //Alternatively, this can be used along with the GenSeed RPC to obtain a + //seed, then present it to the user. Once it has been verified by the user, + //the seed can be fed into this RPC in order to commit the new wallet. + InitWallet(context.Context, *InitWalletRequest) (*InitWalletResponse, error) + mustEmbedUnimplementedWalletUnlockerServer() +} + +// UnimplementedWalletUnlockerServer must be embedded to have forward compatible implementations. +type UnimplementedWalletUnlockerServer struct { +} + +func (UnimplementedWalletUnlockerServer) InitWallet(context.Context, *InitWalletRequest) (*InitWalletResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method InitWallet not implemented") +} +func (UnimplementedWalletUnlockerServer) mustEmbedUnimplementedWalletUnlockerServer() {} + +// UnsafeWalletUnlockerServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to WalletUnlockerServer will +// result in compilation errors. +type UnsafeWalletUnlockerServer interface { + mustEmbedUnimplementedWalletUnlockerServer() +} + +func RegisterWalletUnlockerServer(s grpc.ServiceRegistrar, srv WalletUnlockerServer) { + s.RegisterService(&WalletUnlocker_ServiceDesc, srv) +} + +func _WalletUnlocker_InitWallet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(InitWalletRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WalletUnlockerServer).InitWallet(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/lnrpc.WalletUnlocker/InitWallet", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WalletUnlockerServer).InitWallet(ctx, req.(*InitWalletRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// WalletUnlocker_ServiceDesc is the grpc.ServiceDesc for WalletUnlocker service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var WalletUnlocker_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "lnrpc.WalletUnlocker", + HandlerType: (*WalletUnlockerServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "InitWallet", + Handler: _WalletUnlocker_InitWallet_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "walletunlocker.proto", +} diff --git a/lndsigner.go b/lndsigner.go index e0841ac..e2ddc7e 100644 --- a/lndsigner.go +++ b/lndsigner.go @@ -8,13 +8,17 @@ package lndsigner import ( "crypto/tls" "crypto/x509" + "encoding/json" "fmt" "net" "os" "os/signal" + "strconv" + "strings" "sync" "syscall" + "github.com/bottlepay/lndsigner/vault" "github.com/hashicorp/vault/api" "google.golang.org/grpc" "google.golang.org/grpc/credentials" @@ -224,3 +228,52 @@ func parseNetwork(addr net.Addr) string { func ListenOnAddress(addr net.Addr) (net.Listener, error) { return net.Listen(parseNetwork(addr), addr.String()) } + +type jsonAcctEl struct { + Xpub string `json:"extended_public_key"` + Path string `json:"derivation_path"` +} + +// GetAccounts is currently used in integration testing, but will soon also +// be used in policy enforcement. For current status, see the branch at +// https://github.com/bottlepay/lndsigner/tree/offchain-ratelimiting +func GetAccounts(acctList string) (map[[3]uint32]string, error) { + accounts := make(map[[3]uint32]string) + + elements := make(map[string][]*jsonAcctEl) + + err := json.Unmarshal([]byte(acctList), &elements) + if err != nil { + return nil, err + } + + acctElements, ok := elements["accounts"] + if !ok { + return nil, fmt.Errorf("no accounts returned in JSON") + } + + for _, acctEl := range acctElements { + pathEls := strings.Split(acctEl.Path, "/") + if len(pathEls) != 4 || pathEls[0] != "m" { + return nil, fmt.Errorf("invalid derivation path") + } + + var derPath [3]uint32 + for idx, el := range pathEls[1:] { + if !strings.HasSuffix(el, "'") { + return nil, vault.ErrElementNotHardened + } + + intEl, err := strconv.ParseUint(el[:len(el)-1], 10, 32) + if err != nil { + return nil, err + } + + derPath[idx] = uint32(intEl) + } + + accounts[derPath] = acctEl.Xpub + } + + return accounts, nil +} From 7d671824ee88474b72333b7100ee66872701ea83 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Tue, 7 Feb 2023 12:04:40 -0800 Subject: [PATCH 26/42] README: replace unchecked TODO items with link to issues --- README.md | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a9b8374..d30dc9d 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,8 @@ - [x] derive shared keys for peer connections - [x] sign PSBTs for on-chain transactions, channel openings/closes, HTLC updates, etc. - [x] run itests -- [ ] do automated builds -- [ ] do reproducible builds -- [ ] perform musig2 ops -- [ ] track on-chain wallet state and enforce policy for on-chain transactions -- [ ] track channel state and enforce policy for channel updates -- [ ] allow preauthorizations for on-chain transactions, channel opens/closes, and channel updates -- [ ] allow an interceptor to determine whether or not to sign -- [ ] log and gather metrics coherently -- [ ] enforce custom SELinux policy to harden plugin execution environment + +There is a [list of issues](https://github.com/bottlepay/lndsigner/issues?q=is%3Aissue+is%3Aopen+milestone%3Amainnet-ready) that tracks TODO items needed for a mainnet release. ## Usage From 3463f335ac385f69fa1da3d7ce91167a023188dc Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Mon, 13 Feb 2023 08:15:04 -0800 Subject: [PATCH 27/42] docker/make: detect arch and pass build args from makefile --- Dockerfile | 14 +++++++------- Makefile | 25 ++++++++++++++++++++----- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2632450..2d69481 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,17 +2,17 @@ ARG gover=1.19.5 FROM golang:$gover -ARG goplatform=amd64 -ARG cppplatform=x86_64 -ARG lnd=v0.15.5-beta -ARG bitcoind=24.0.1 -ARG vault=1.12.2 +ARG goplatform +ARG cplatform +ARG lnd +ARG bitcoind +ARG vault RUN apt update && apt-get install -y zip RUN cd /root && \ - wget https://bitcoincore.org/bin/bitcoin-core-$bitcoind/bitcoin-${bitcoind}-${cppplatform}-linux-gnu.tar.gz && \ - tar xfz bitcoin-$bitcoind-$cppplatform-linux-gnu.tar.gz && \ + wget https://bitcoincore.org/bin/bitcoin-core-$bitcoind/bitcoin-${bitcoind}-${cplatform}-linux-gnu.tar.gz && \ + tar xfz bitcoin-$bitcoind-$cplatform-linux-gnu.tar.gz && \ mv bitcoin-$bitcoind/bin/* /usr/local/bin/ && \ wget https://github.com/lightningnetwork/lnd/releases/download/$lnd/lnd-linux-$goplatform-$lnd.tar.gz && \ tar xfz lnd-linux-$goplatform-$lnd.tar.gz && \ diff --git a/Makefile b/Makefile index 2a2d217..67c33b6 100644 --- a/Makefile +++ b/Makefile @@ -2,17 +2,32 @@ IMG_NAME := lndsigner-builder +CPLATFORM := $(shell uname -m) + +ifeq ($(CPLATFORM), x86_64) + GOPLATFORM := amd64 +endif + +ifeq ($(CPLATFORM), aarch64) + GOPLATFORM := arm64 +endif + +ifeq ($(CPLATFORM), arm64) + GOPLATFORM := arm64 + CPLATFORM := aarch64 +endif + GOVER := 1.19.5 -GOPLATFORM := amd64 -CPPPLATFORM := x86_64 LND := v0.15.5-beta BITCOIND := 24.0.1 VAULT := 1.12.2 -# docker just tags the latest image to the builderstamp, in case the -# dependencies have been changed and a new image was built. +# docker builds a builder image for the host platform if one isn't cached. docker: - docker build -t $(IMG_NAME):latest . + docker build -t $(IMG_NAME):latest --build-arg cplatform=$(CPLATFORM) \ + --build-arg goplatform=$(GOPLATFORM) --build-arg gover=$(GOVER) \ + --build-arg lnd=$(LND) --build-arg bitcoind=$(BITCOIND) \ + --build-arg vault=$(VAULT) . # docker-itest runs itests in a docker container, then removes the container. docker-itest: docker From b70a5928fcdb1770e892967463f2123d9efb3c15 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Thu, 16 Feb 2023 10:05:44 -0800 Subject: [PATCH 28/42] make/docker: rename dev/test dockerfile --- Dockerfile => Dockerfile.dev | 0 Makefile | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename Dockerfile => Dockerfile.dev (100%) diff --git a/Dockerfile b/Dockerfile.dev similarity index 100% rename from Dockerfile rename to Dockerfile.dev diff --git a/Makefile b/Makefile index 67c33b6..40e645f 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ docker: docker build -t $(IMG_NAME):latest --build-arg cplatform=$(CPLATFORM) \ --build-arg goplatform=$(GOPLATFORM) --build-arg gover=$(GOVER) \ --build-arg lnd=$(LND) --build-arg bitcoind=$(BITCOIND) \ - --build-arg vault=$(VAULT) . + --build-arg vault=$(VAULT) -f Dockerfile.dev . # docker-itest runs itests in a docker container, then removes the container. docker-itest: docker From 59c48ca81a459f5f63708f8632344a344f789b08 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Mon, 13 Feb 2023 13:54:44 -0800 Subject: [PATCH 29/42] build: build lndsignerd sidecar container and vault-plugin-lndsigner binary --- .github/workflows/docker-build.yml | 106 +++++++++++++++++++++++++++++ Dockerfile | 45 ++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 .github/workflows/docker-build.yml create mode 100644 Dockerfile diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..f12e88b --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,106 @@ +name: Release + +on: + push: + tags: + - v* + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + GO_VERSION: 1.19.5 + +jobs: + plugin: + strategy: + matrix: + os: [ "linux" ] + arch: [ "amd64", "arm64" ] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: '${{ env.GO_VERSION }}' + + - name: Build plugin + run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} go build -buildvcs=false -o . ./cmd/vault-plugin-lndsigner/ + + - name: Create plugin archive + run: tar cfz vault-plugin-lndsigner-${{ matrix.os }}-${{ matrix.arch }}-${{ github.ref_name }}.tgz vault-plugin-lndsigner + + - name: Upload plugin archive to release + uses: softprops/action-gh-release@v1 + with: + files: vault-plugin-lndsigner-${{ matrix.os }}-${{ matrix.arch }}-${{ github.ref_name }}.tgz + + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to GHCR + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build the Docker image + uses: docker/build-push-action@v3 + with: + context: . + target: release-builder + platforms: linux/amd64,linux/arm64 + + - name: "Debian: Extract metadata (tags, labels) for Docker" + id: debian_meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=tag + type=sha + flavor: | + latest=auto + + - name: "Debian: Push Docker image" + uses: docker/build-push-action@v3 + with: + context: . + push: true + tags: ${{ steps.debian_meta.outputs.tags }} + labels: ${{ steps.debian_meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + target: debian + + - name: "Alpine: Extract metadata (tags, labels) for Docker" + id: alpine_meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=tag + type=sha + flavor: | + suffix=-alpine + + - name: "Alpine: Push Docker image" + uses: docker/build-push-action@v3 + with: + context: . + push: true + tags: ${{ steps.alpine_meta.outputs.tags }} + labels: ${{ steps.alpine_meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + target: alpine diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a547441 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,45 @@ +ARG gover=1.19.5 + +# Build a release binary + +FROM golang:$gover AS release-builder + +COPY . /go/src/github.com/bottlepay/lndsigner + +RUN cd /go/src/github.com/bottlepay/lndsigner \ + && CGO_ENABLED=0 go install -buildvcs=false \ + github.com/bottlepay/lndsigner/cmd/... + +### Build an Alpine image +FROM alpine:3.16 as alpine + +# Update CA certs +RUN apk add --no-cache ca-certificates && rm -rf /var/cache/apk/* + +# Copy over app binary +COPY --from=release-builder /go/bin/lndsignerd /usr/bin/lndsignerd + +# Add a user +RUN mkdir -p /app && adduser -D lndsignerd && chown -R lndsignerd /app +USER lndsignerd + +WORKDIR /app/ + +CMD [ "/usr/bin/lndsignerd" ] + +### Build a Debian image +FROM debian:bullseye-slim as debian + +# Update CA certs +RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* + +# Copy over app binary +COPY --from=release-builder /go/bin/lndsignerd /usr/bin/lndsignerd + +# Add a user +RUN mkdir -p /app && adduser --disabled-login lndsignerd && chown -R lndsignerd /app +USER lndsignerd + +WORKDIR /app + +CMD [ "/usr/bin/lndsignerd" ] From abbaea6b23015b1088b7153422d1b6e30bb577be Mon Sep 17 00:00:00 2001 From: Joshua Welsh <99905207+jwelsh-nydig@users.noreply.github.com> Date: Fri, 17 Feb 2023 16:54:14 +0000 Subject: [PATCH 30/42] Add Vault image build to release workflow (#41) Add Vault image build to release workflow --- .github/workflows/docker-build.yml | 106 --------------- .github/workflows/golangci-lint.yml | 4 +- .github/workflows/release-build.yml | 203 ++++++++++++++++++++++++++++ Dockerfile.vault | 11 ++ 4 files changed, 216 insertions(+), 108 deletions(-) delete mode 100644 .github/workflows/docker-build.yml create mode 100644 .github/workflows/release-build.yml create mode 100644 Dockerfile.vault diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml deleted file mode 100644 index f12e88b..0000000 --- a/.github/workflows/docker-build.yml +++ /dev/null @@ -1,106 +0,0 @@ -name: Release - -on: - push: - tags: - - v* - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - GO_VERSION: 1.19.5 - -jobs: - plugin: - strategy: - matrix: - os: [ "linux" ] - arch: [ "amd64", "arm64" ] - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Set up Go - uses: actions/setup-go@v3 - with: - go-version: '${{ env.GO_VERSION }}' - - - name: Build plugin - run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} go build -buildvcs=false -o . ./cmd/vault-plugin-lndsigner/ - - - name: Create plugin archive - run: tar cfz vault-plugin-lndsigner-${{ matrix.os }}-${{ matrix.arch }}-${{ github.ref_name }}.tgz vault-plugin-lndsigner - - - name: Upload plugin archive to release - uses: softprops/action-gh-release@v1 - with: - files: vault-plugin-lndsigner-${{ matrix.os }}-${{ matrix.arch }}-${{ github.ref_name }}.tgz - - docker: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Login to GHCR - uses: docker/login-action@v2 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build the Docker image - uses: docker/build-push-action@v3 - with: - context: . - target: release-builder - platforms: linux/amd64,linux/arm64 - - - name: "Debian: Extract metadata (tags, labels) for Docker" - id: debian_meta - uses: docker/metadata-action@v4 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=tag - type=sha - flavor: | - latest=auto - - - name: "Debian: Push Docker image" - uses: docker/build-push-action@v3 - with: - context: . - push: true - tags: ${{ steps.debian_meta.outputs.tags }} - labels: ${{ steps.debian_meta.outputs.labels }} - platforms: linux/amd64,linux/arm64 - target: debian - - - name: "Alpine: Extract metadata (tags, labels) for Docker" - id: alpine_meta - uses: docker/metadata-action@v4 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=tag - type=sha - flavor: | - suffix=-alpine - - - name: "Alpine: Push Docker image" - uses: docker/build-push-action@v3 - with: - context: . - push: true - tags: ${{ steps.alpine_meta.outputs.tags }} - labels: ${{ steps.alpine_meta.outputs.labels }} - platforms: linux/amd64,linux/arm64 - target: alpine diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 3ed5ea5..f7a5477 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -6,7 +6,7 @@ on: branches: [ "master", "main" ] pull_request: branches: [ "**" ] - + permissions: contents: read @@ -22,5 +22,5 @@ jobs: - uses: actions/checkout@v3 - name: golangci-lint uses: golangci/golangci-lint-action@v3 - with: + with: version: latest diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml new file mode 100644 index 0000000..60c97dc --- /dev/null +++ b/.github/workflows/release-build.yml @@ -0,0 +1,203 @@ +name: Build Release Artifacts + +on: + push: + tags: + - v* + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + VAULT_IMAGE_NAME: bottlepay/vault-plugin-lndsigner + GO_VERSION: 1.19.5 + +jobs: + # This job fetches the latest minor revision for each currently supported vault version. + # The output of this job is used to construct the version matrix in the build-docker-vault job. + vault_latest_versions: + runs-on: ubuntu-latest + outputs: + versions: ${{ steps.generate.outputs.versions }} + steps: + - name: "Generate matrix" + id: generate + run: | + VAULT_VERSIONS=`curl https://raw.githubusercontent.com/docker-library/official-images/master/library/vault | grep -Eo '1\.(9|1[0123])\.[0-9]+' | jq --raw-input --raw-output --slurp 'split("\n") | del(.[] | select(. == "")) | tojson'` + echo "versions=$VAULT_VERSIONS" >> "$GITHUB_OUTPUT" + + # This job builds the Vault plugin, and pushes it to the GitHub release + build-vault-plugin: + strategy: + matrix: + os: [ "linux" ] + arch: [ "amd64", "arm64" ] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: '${{ env.GO_VERSION }}' + + - name: Build plugin + run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} CGO_ENABLED=0 go build -buildvcs=false -o "vault-plugin-lndsigner-${{ matrix.os }}-${{ matrix.arch }}" ./cmd/vault-plugin-lndsigner/ + + - name: Create the SHA256 checksum file + run: shasum -a 256 "vault-plugin-lndsigner-${{ matrix.os }}-${{ matrix.arch }}" | cut -d " " -f1 > "vault-plugin-lndsigner-${{ matrix.os }}-${{ matrix.arch }}.SHA256SUM" + + - name: Upload plugin assets to release + uses: softprops/action-gh-release@v1 + with: + files: | + vault-plugin-lndsigner-${{ matrix.os }}-${{ matrix.arch }} + vault-plugin-lndsigner-${{ matrix.os }}-${{ matrix.arch }}.SHA256SUM + + body: | + ## Docker Images + All images are built for both `linux/amd64` and `linux/arm64` architectures. + + ### lndsignerd + Pre-built docker images for the `lndsignerd` server are available in multiple flavours: + + - Debian (Bullseye): `${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}` + - Alpine Linux (3.17): `${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-alpine` + + ### vault-plugin-lndsigner + Pre-built extensions of the latest [base Vault images](https://hub.docker.com/_/vault) with `vault-plugin-lndsigner` pre-installed are available here: + + [${{ env.REGISTRY }}/${{ env.VAULT_IMAGE_NAME }}](https://github.com/bottlepay/lndsigner/pkgs/container/vault-plugin-lndsigner) + + Note that the images within this package are provided for testing purposes only. Running a Vault image from an untrusted source in production is not recommended. + + generate_release_notes: true + prerelease: true + + # This job extends the base Vault docker image by pre-installing the lndsigner plugin created + # by the build-vault-plugin job + build-docker-vault: + runs-on: ubuntu-latest + needs: + - vault_latest_versions + - build-vault-plugin + strategy: + matrix: + vault_version: ${{ fromJSON(needs.vault_latest_versions.outputs.versions) }} + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to GHCR + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: "Extract metadata (tags, labels) for Docker" + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.VAULT_IMAGE_NAME }} + tags: | + type=ref,event=tag + flavor: | + prefix=${{ matrix.vault_version }}-lndsigner- + latest=${{ startsWith(matrix.vault_version, '1.12') }} + labels: | + org.opencontainers.image.title=Vault (with lndsigner) + org.opencontainers.image.description=The base Hashicorp Vault image (library/vault), with the lndsigner plugin pre-installed. + org.opencontainers.image.vendor=Hashicorp (Vault), Bottlepay (lndsigner) + org.opencontainers.image.source=https://github.com/bottlepay/lndsigner + org.opencontainers.image.version=${{ github.ref_name }} + org.opencontainers.image.licenses=(MPL-2.0 AND MIT) + org.opencontainers.image.base.name=docker.io/library/vault:${{ matrix.vault_version }} + + - name: Build the Docker image + uses: docker/build-push-action@v3 + with: + context: . + file: Dockerfile.vault + platforms: linux/amd64,linux/arm64 + build-args: | + VAULT_VER=${{ matrix.vault_version }} + VAULT_SIGNER_PLUGIN_VER=${{ github.ref_name }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + push: true + + # This job builds the lndsigner server image + build-docker-lndsigner: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to GHCR + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build the Docker image + uses: docker/build-push-action@v3 + with: + context: . + target: release-builder + platforms: linux/amd64,linux/arm64 + + - name: "Debian: Extract metadata (tags, labels) for Docker" + id: debian_meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=tag + type=sha + flavor: | + latest=auto + + - name: "Debian: Push Docker image" + uses: docker/build-push-action@v3 + with: + context: . + push: true + tags: ${{ steps.debian_meta.outputs.tags }} + labels: ${{ steps.debian_meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + target: debian + + - name: "Alpine: Extract metadata (tags, labels) for Docker" + id: alpine_meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=tag + type=sha + flavor: | + suffix=-alpine + + - name: "Alpine: Push Docker image" + uses: docker/build-push-action@v3 + with: + context: . + push: true + tags: ${{ steps.alpine_meta.outputs.tags }} + labels: ${{ steps.alpine_meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + target: alpine diff --git a/Dockerfile.vault b/Dockerfile.vault new file mode 100644 index 0000000..c4388a5 --- /dev/null +++ b/Dockerfile.vault @@ -0,0 +1,11 @@ +ARG VAULT_VER=1.11.7 + +FROM library/vault:${VAULT_VER} +ARG TARGETARCH +ARG VAULT_SIGNER_PLUGIN_VER + +ADD https://github.com/bottlepay/lndsigner/releases/download/${VAULT_SIGNER_PLUGIN_VER}/vault-plugin-lndsigner-linux-${TARGETARCH} /vault/plugins/vault-plugin-lndsigner +ADD https://github.com/bottlepay/lndsigner/releases/download/${VAULT_SIGNER_PLUGIN_VER}/vault-plugin-lndsigner-linux-${TARGETARCH}.SHA256SUM /vault/plugins/vault-plugin-lndsigner.SHA256SUM +ADD LICENSE /vault/plugins/vault-plugin-lndsigner.LICENSE + +RUN setcap cap_ipc_lock=+ep /vault/plugins/vault-plugin-lndsigner From 648e1c6ed1325f37c78708b5904657a9fe9224e3 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Mon, 3 Apr 2023 11:42:10 -0700 Subject: [PATCH 31/42] multi: rename from bottlepay/lndsigner to nydig/lndsigner --- Dockerfile | 6 +++--- Dockerfile.vault | 4 ++-- README.md | 2 +- cmd/lndsignerd/main.go | 2 +- cmd/vault-plugin-lndsigner/main.go | 2 +- config.go | 2 +- go.mod | 2 +- itest/itest_lndharness_test.go | 4 ++-- itest/walletunlocker.proto | 2 +- keyring/keyring.go | 2 +- lndsigner.go | 4 ++-- log.go | 2 +- proto/lightning.proto | 2 +- proto/signer.proto | 2 +- proto/walletkit.proto | 2 +- rpcserver.go | 4 ++-- signer_server.go | 6 +++--- walletkit_server.go | 2 +- 18 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Dockerfile b/Dockerfile index a547441..56cff99 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,11 +4,11 @@ ARG gover=1.19.5 FROM golang:$gover AS release-builder -COPY . /go/src/github.com/bottlepay/lndsigner +COPY . /go/src/github.com/nydig/lndsigner -RUN cd /go/src/github.com/bottlepay/lndsigner \ +RUN cd /go/src/github.com/nydig/lndsigner \ && CGO_ENABLED=0 go install -buildvcs=false \ - github.com/bottlepay/lndsigner/cmd/... + github.com/nydig/lndsigner/cmd/... ### Build an Alpine image FROM alpine:3.16 as alpine diff --git a/Dockerfile.vault b/Dockerfile.vault index c4388a5..a0b0503 100644 --- a/Dockerfile.vault +++ b/Dockerfile.vault @@ -4,8 +4,8 @@ FROM library/vault:${VAULT_VER} ARG TARGETARCH ARG VAULT_SIGNER_PLUGIN_VER -ADD https://github.com/bottlepay/lndsigner/releases/download/${VAULT_SIGNER_PLUGIN_VER}/vault-plugin-lndsigner-linux-${TARGETARCH} /vault/plugins/vault-plugin-lndsigner -ADD https://github.com/bottlepay/lndsigner/releases/download/${VAULT_SIGNER_PLUGIN_VER}/vault-plugin-lndsigner-linux-${TARGETARCH}.SHA256SUM /vault/plugins/vault-plugin-lndsigner.SHA256SUM +ADD https://github.com/nydig/lndsigner/releases/download/${VAULT_SIGNER_PLUGIN_VER}/vault-plugin-lndsigner-linux-${TARGETARCH} /vault/plugins/vault-plugin-lndsigner +ADD https://github.com/nydig/lndsigner/releases/download/${VAULT_SIGNER_PLUGIN_VER}/vault-plugin-lndsigner-linux-${TARGETARCH}.SHA256SUM /vault/plugins/vault-plugin-lndsigner.SHA256SUM ADD LICENSE /vault/plugins/vault-plugin-lndsigner.LICENSE RUN setcap cap_ipc_lock=+ep /vault/plugins/vault-plugin-lndsigner diff --git a/README.md b/README.md index d30dc9d..2c5d740 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ - [x] sign PSBTs for on-chain transactions, channel openings/closes, HTLC updates, etc. - [x] run itests -There is a [list of issues](https://github.com/bottlepay/lndsigner/issues?q=is%3Aissue+is%3Aopen+milestone%3Amainnet-ready) that tracks TODO items needed for a mainnet release. +There is a [list of issues](https://github.com/nydig/lndsigner/issues?q=is%3Aissue+is%3Aopen+milestone%3Amainnet-ready) that tracks TODO items needed for a mainnet release. ## Usage diff --git a/cmd/lndsignerd/main.go b/cmd/lndsignerd/main.go index db5a7c6..e7d3e9e 100644 --- a/cmd/lndsignerd/main.go +++ b/cmd/lndsignerd/main.go @@ -4,8 +4,8 @@ import ( "fmt" "os" - "github.com/bottlepay/lndsigner" "github.com/jessevdk/go-flags" + "github.com/nydig/lndsigner" ) func main() { diff --git a/cmd/vault-plugin-lndsigner/main.go b/cmd/vault-plugin-lndsigner/main.go index 52a7207..1875aed 100644 --- a/cmd/vault-plugin-lndsigner/main.go +++ b/cmd/vault-plugin-lndsigner/main.go @@ -3,10 +3,10 @@ package main import ( "os" - "github.com/bottlepay/lndsigner/vault" "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/sdk/plugin" + "github.com/nydig/lndsigner/vault" ) func main() { diff --git a/config.go b/config.go index 10e8df3..f003f4d 100644 --- a/config.go +++ b/config.go @@ -14,10 +14,10 @@ import ( "strconv" "strings" - "github.com/bottlepay/lndsigner/vault" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" flags "github.com/jessevdk/go-flags" + "github.com/nydig/lndsigner/vault" ) const ( diff --git a/go.mod b/go.mod index 78ed3d7..2ccc517 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/bottlepay/lndsigner +module github.com/nydig/lndsigner require ( github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 diff --git a/itest/itest_lndharness_test.go b/itest/itest_lndharness_test.go index 228dd2b..022d7ba 100644 --- a/itest/itest_lndharness_test.go +++ b/itest/itest_lndharness_test.go @@ -14,8 +14,8 @@ import ( "encoding/json" "encoding/pem" "fmt" - "github.com/bottlepay/lndsigner" - "github.com/bottlepay/lndsigner/itest" + "github.com/nydig/lndsigner" + "github.com/nydig/lndsigner/itest" "io/fs" "math/big" "net" diff --git a/itest/walletunlocker.proto b/itest/walletunlocker.proto index eeb86af..8a6cf80 100644 --- a/itest/walletunlocker.proto +++ b/itest/walletunlocker.proto @@ -2,7 +2,7 @@ syntax = "proto3"; package lnrpc; -option go_package = "github.com/bottlepay/lndsigner/itest"; +option go_package = "github.com/nydig/lndsigner/itest"; /* * Comments in this file will be directly parsed into the API diff --git a/keyring/keyring.go b/keyring/keyring.go index f5767f6..a84865f 100644 --- a/keyring/keyring.go +++ b/keyring/keyring.go @@ -11,7 +11,6 @@ import ( "encoding/hex" "fmt" - "github.com/bottlepay/lndsigner/vault" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil/hdkeychain" @@ -20,6 +19,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/hashicorp/vault/api" + "github.com/nydig/lndsigner/vault" ) // signMethod defines the different ways a signer can sign, given a specific diff --git a/lndsigner.go b/lndsigner.go index e2ddc7e..01db15b 100644 --- a/lndsigner.go +++ b/lndsigner.go @@ -18,8 +18,8 @@ import ( "sync" "syscall" - "github.com/bottlepay/lndsigner/vault" "github.com/hashicorp/vault/api" + "github.com/nydig/lndsigner/vault" "google.golang.org/grpc" "google.golang.org/grpc/credentials" ) @@ -236,7 +236,7 @@ type jsonAcctEl struct { // GetAccounts is currently used in integration testing, but will soon also // be used in policy enforcement. For current status, see the branch at -// https://github.com/bottlepay/lndsigner/tree/offchain-ratelimiting +// https://github.com/aakselrod/lndsigner/tree/offchain-ratelimiting func GetAccounts(acctList string) (map[[3]uint32]string, error) { accounts := make(map[[3]uint32]string) diff --git a/log.go b/log.go index 8c49994..f83bd53 100644 --- a/log.go +++ b/log.go @@ -6,7 +6,7 @@ package lndsigner import ( - "github.com/bottlepay/lndsigner/keyring" + "github.com/nydig/lndsigner/keyring" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) diff --git a/proto/lightning.proto b/proto/lightning.proto index 69fb450..8be1716 100644 --- a/proto/lightning.proto +++ b/proto/lightning.proto @@ -7,7 +7,7 @@ syntax = "proto3"; package proto; -option go_package = "github.com/bottlepay/lndsigner/proto"; +option go_package = "github.com/nydig/lndsigner/proto"; /* * Comments in this file will be directly parsed into the API diff --git a/proto/signer.proto b/proto/signer.proto index 3630c7c..342a2db 100644 --- a/proto/signer.proto +++ b/proto/signer.proto @@ -9,7 +9,7 @@ import "lightning.proto"; package proto; -option go_package = "github.com/bottlepay/lndsigner/proto"; +option go_package = "github.com/nydig/lndsigner/proto"; // Signer is a service that gives access to the signing functionality of the // daemon's wallet. diff --git a/proto/walletkit.proto b/proto/walletkit.proto index a0ab4eb..4c307f3 100644 --- a/proto/walletkit.proto +++ b/proto/walletkit.proto @@ -7,7 +7,7 @@ syntax = "proto3"; package proto; -option go_package = "github.com/bottlepay/lndsigner/proto"; +option go_package = "github.com/nydig/lndsigner/proto"; // WalletKit is a service that gives access to the core functionalities of the // daemon's wallet. diff --git a/rpcserver.go b/rpcserver.go index 56496a1..020ed3d 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -9,9 +9,9 @@ import ( "context" "fmt" - "github.com/bottlepay/lndsigner/keyring" - "github.com/bottlepay/lndsigner/proto" "github.com/hashicorp/vault/api" + "github.com/nydig/lndsigner/keyring" + "github.com/nydig/lndsigner/proto" "github.com/tv42/zbase32" "google.golang.org/grpc" ) diff --git a/signer_server.go b/signer_server.go index 7d14e55..3ea65e4 100644 --- a/signer_server.go +++ b/signer_server.go @@ -9,11 +9,11 @@ import ( "context" "fmt" - "github.com/bottlepay/lndsigner/keyring" - "github.com/bottlepay/lndsigner/proto" - "github.com/bottlepay/lndsigner/vault" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/nydig/lndsigner/keyring" + "github.com/nydig/lndsigner/proto" + "github.com/nydig/lndsigner/vault" ) // Server is a sub-server of the main RPC server: the signer RPC. This sub RPC diff --git a/walletkit_server.go b/walletkit_server.go index 4a45796..8425e3f 100644 --- a/walletkit_server.go +++ b/walletkit_server.go @@ -10,8 +10,8 @@ import ( "context" "fmt" - "github.com/bottlepay/lndsigner/proto" "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/nydig/lndsigner/proto" ) // walletKit is a sub-RPC server that exposes a tool kit which allows clients From c755aa918fe9f655ba6a1c3d1b954a07e3f5aa5c Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Mon, 3 Apr 2023 20:17:30 -0700 Subject: [PATCH 32/42] build: fix golangci-lint github action --- .github/workflows/golangci-lint.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index f7a5477..bc1e8ee 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -7,10 +7,6 @@ on: pull_request: branches: [ "**" ] - -permissions: - contents: read - jobs: golangci: name: lint @@ -20,7 +16,7 @@ jobs: with: go-version: 1.19.5 - uses: actions/checkout@v3 - - name: golangci-lint - uses: golangci/golangci-lint-action@v3 - with: - version: latest + - name: install golangci-lint + run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + - name: run golangci-lint + run: golangci-lint run From 3c51042909254cee979dd70a1ab4ec992894fea2 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Tue, 4 Apr 2023 12:58:24 -0700 Subject: [PATCH 33/42] build: generate release with permitted github actions --- .github/workflows/release-build.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 60c97dc..52b00dd 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -8,7 +8,7 @@ on: env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} - VAULT_IMAGE_NAME: bottlepay/vault-plugin-lndsigner + VAULT_IMAGE_NAME: nydig/vault-plugin-lndsigner GO_VERSION: 1.19.5 jobs: @@ -48,9 +48,9 @@ jobs: run: shasum -a 256 "vault-plugin-lndsigner-${{ matrix.os }}-${{ matrix.arch }}" | cut -d " " -f1 > "vault-plugin-lndsigner-${{ matrix.os }}-${{ matrix.arch }}.SHA256SUM" - name: Upload plugin assets to release - uses: softprops/action-gh-release@v1 + uses: ncipollo/release-action@v1 with: - files: | + artifacts: | vault-plugin-lndsigner-${{ matrix.os }}-${{ matrix.arch }} vault-plugin-lndsigner-${{ matrix.os }}-${{ matrix.arch }}.SHA256SUM @@ -67,12 +67,13 @@ jobs: ### vault-plugin-lndsigner Pre-built extensions of the latest [base Vault images](https://hub.docker.com/_/vault) with `vault-plugin-lndsigner` pre-installed are available here: - [${{ env.REGISTRY }}/${{ env.VAULT_IMAGE_NAME }}](https://github.com/bottlepay/lndsigner/pkgs/container/vault-plugin-lndsigner) + [${{ env.REGISTRY }}/${{ env.VAULT_IMAGE_NAME }}](https://github.com/nydig/lndsigner/pkgs/container/vault-plugin-lndsigner) Note that the images within this package are provided for testing purposes only. Running a Vault image from an untrusted source in production is not recommended. - generate_release_notes: true + generateReleaseNotes: true prerelease: true + allowUpdates: true # This job extends the base Vault docker image by pre-installing the lndsigner plugin created # by the build-vault-plugin job @@ -114,8 +115,8 @@ jobs: labels: | org.opencontainers.image.title=Vault (with lndsigner) org.opencontainers.image.description=The base Hashicorp Vault image (library/vault), with the lndsigner plugin pre-installed. - org.opencontainers.image.vendor=Hashicorp (Vault), Bottlepay (lndsigner) - org.opencontainers.image.source=https://github.com/bottlepay/lndsigner + org.opencontainers.image.vendor=Hashicorp (Vault), NYDIG (lndsigner) + org.opencontainers.image.source=https://github.com/nydig/lndsigner org.opencontainers.image.version=${{ github.ref_name }} org.opencontainers.image.licenses=(MPL-2.0 AND MIT) org.opencontainers.image.base.name=docker.io/library/vault:${{ matrix.vault_version }} From 779628a4842775799c3175657887f0382911ba14 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 19:32:27 +0000 Subject: [PATCH 34/42] build(deps): bump golang.org/x/net Bumps [golang.org/x/net](https://github.com/golang/net) from 0.0.0-20220722155237-a158d28d115b to 0.7.0. - [Release notes](https://github.com/golang/net/releases) - [Commits](https://github.com/golang/net/commits/v0.7.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 2ccc517..1a5c4b0 100644 --- a/go.mod +++ b/go.mod @@ -67,9 +67,9 @@ require ( gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.6.0 // indirect - golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect - golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect - golang.org/x/text v0.3.7 // indirect + golang.org/x/net v0.7.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0 // indirect gopkg.in/square/go-jose.v2 v2.5.1 // indirect diff --git a/go.sum b/go.sum index 4a21b90..5ffa6c5 100644 --- a/go.sum +++ b/go.sum @@ -333,8 +333,8 @@ golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -369,15 +369,15 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From 24672c7f13d9c12d7a1f15e7b0d384403d88c47f Mon Sep 17 00:00:00 2001 From: Joshua Welsh Date: Fri, 10 Mar 2023 09:18:42 +0000 Subject: [PATCH 35/42] Bump go to 1.19.7, and fix permissions in the vault image --- .github/workflows/golangci-lint.yml | 2 +- .github/workflows/release-build.yml | 2 +- Dockerfile | 4 ++-- Dockerfile.dev | 2 +- Dockerfile.vault | 5 ++++- Makefile | 2 +- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index bc1e8ee..13d46ee 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/setup-go@v3 with: - go-version: 1.19.5 + go-version: 1.19.7 - uses: actions/checkout@v3 - name: install golangci-lint run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 52b00dd..dddf2ef 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -9,7 +9,7 @@ env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} VAULT_IMAGE_NAME: nydig/vault-plugin-lndsigner - GO_VERSION: 1.19.5 + GO_VERSION: 1.19.7 jobs: # This job fetches the latest minor revision for each currently supported vault version. diff --git a/Dockerfile b/Dockerfile index 56cff99..2b2b2b6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG gover=1.19.5 +ARG gover=1.19.7 # Build a release binary @@ -11,7 +11,7 @@ RUN cd /go/src/github.com/nydig/lndsigner \ github.com/nydig/lndsigner/cmd/... ### Build an Alpine image -FROM alpine:3.16 as alpine +FROM alpine:3.17 as alpine # Update CA certs RUN apk add --no-cache ca-certificates && rm -rf /var/cache/apk/* diff --git a/Dockerfile.dev b/Dockerfile.dev index 2d69481..ff0276c 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,4 +1,4 @@ -ARG gover=1.19.5 +ARG gover=1.19.7 FROM golang:$gover diff --git a/Dockerfile.vault b/Dockerfile.vault index a0b0503..edc65a6 100644 --- a/Dockerfile.vault +++ b/Dockerfile.vault @@ -8,4 +8,7 @@ ADD https://github.com/nydig/lndsigner/releases/download/${VAULT_SIGNER_PLUGIN_V ADD https://github.com/nydig/lndsigner/releases/download/${VAULT_SIGNER_PLUGIN_VER}/vault-plugin-lndsigner-linux-${TARGETARCH}.SHA256SUM /vault/plugins/vault-plugin-lndsigner.SHA256SUM ADD LICENSE /vault/plugins/vault-plugin-lndsigner.LICENSE -RUN setcap cap_ipc_lock=+ep /vault/plugins/vault-plugin-lndsigner +RUN \ + chown -R vault:vault /vault/plugins \ + && chmod +x /vault/plugins/vault-plugin-lndsigner \ + && setcap cap_ipc_lock=+ep /vault/plugins/vault-plugin-lndsigner diff --git a/Makefile b/Makefile index 40e645f..0dc64f9 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ ifeq ($(CPLATFORM), arm64) CPLATFORM := aarch64 endif -GOVER := 1.19.5 +GOVER := 1.19.7 LND := v0.15.5-beta BITCOIND := 24.0.1 VAULT := 1.12.2 From 3cafd8a9549536d947ae7443d26b63a1f24b5168 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Wed, 29 Mar 2023 11:10:28 -0700 Subject: [PATCH 36/42] gitignore: ignore vim swapfiles --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 66fd13c..c4f3cb7 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ # Dependency directories (remove the comment below to include it) # vendor/ + +# vim +*.swp From 826797e788798752d3326d9e04195a8c94dd574c Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Wed, 29 Mar 2023 11:11:13 -0700 Subject: [PATCH 37/42] Makefile: update lnd version for itests to 0.16.0-beta --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 0dc64f9..3c11fe7 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ ifeq ($(CPLATFORM), arm64) endif GOVER := 1.19.7 -LND := v0.15.5-beta +LND := v0.16.0-beta BITCOIND := 24.0.1 VAULT := 1.12.2 From 45ba6b2f75d51794029494e45589d9c5df51b07b Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Wed, 5 Apr 2023 12:15:48 -0700 Subject: [PATCH 38/42] deps: update to psbt 1.1.8 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1a5c4b0..224590e 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ require ( github.com/btcsuite/btcd v0.23.1 github.com/btcsuite/btcd/btcec/v2 v2.2.1 github.com/btcsuite/btcd/btcutil v1.1.2 - github.com/btcsuite/btcd/btcutil/psbt v1.1.5 + github.com/btcsuite/btcd/btcutil/psbt v1.1.8 github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 github.com/hashicorp/go-hclog v1.3.1 diff --git a/go.sum b/go.sum index 5ffa6c5..513dfec 100644 --- a/go.sum +++ b/go.sum @@ -35,8 +35,8 @@ github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9Ur github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= github.com/btcsuite/btcd/btcutil v1.1.2 h1:XLMbX8JQEiwMcYft2EGi8zPUkoa0abKIU6/BJSRsjzQ= github.com/btcsuite/btcd/btcutil v1.1.2/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0= -github.com/btcsuite/btcd/btcutil/psbt v1.1.5 h1:x0ZRrYY8j75ThV6xBz86CkYAG82F5bzay4H5D1c8b/U= -github.com/btcsuite/btcd/btcutil/psbt v1.1.5/go.mod h1:kA6FLH/JfUx++j9pYU0pyu+Z8XGBQuuTmuKYUf6q7/U= +github.com/btcsuite/btcd/btcutil/psbt v1.1.8 h1:4voqtT8UppT7nmKQkXV+T9K8UyQjKOn2z/ycpmJK8wg= +github.com/btcsuite/btcd/btcutil/psbt v1.1.8/go.mod h1:kA6FLH/JfUx++j9pYU0pyu+Z8XGBQuuTmuKYUf6q7/U= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= From c93b7c088ea65d5c6d81e82c378041ee39536908 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Tue, 2 May 2023 17:04:19 -0700 Subject: [PATCH 39/42] build: update to go 1.20.3 --- .github/workflows/golangci-lint.yml | 2 +- .github/workflows/release-build.yml | 2 +- Dockerfile | 2 +- Dockerfile.dev | 2 +- Makefile | 2 +- go.mod | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 13d46ee..2e2f883 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/setup-go@v3 with: - go-version: 1.19.7 + go-version: 1.20.3 - uses: actions/checkout@v3 - name: install golangci-lint run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index dddf2ef..46ef00e 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -9,7 +9,7 @@ env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} VAULT_IMAGE_NAME: nydig/vault-plugin-lndsigner - GO_VERSION: 1.19.7 + GO_VERSION: 1.20.3 jobs: # This job fetches the latest minor revision for each currently supported vault version. diff --git a/Dockerfile b/Dockerfile index 2b2b2b6..b06f622 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG gover=1.19.7 +ARG gover=1.20.3 # Build a release binary diff --git a/Dockerfile.dev b/Dockerfile.dev index ff0276c..b47e022 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,4 +1,4 @@ -ARG gover=1.19.7 +ARG gover=1.20.3 FROM golang:$gover diff --git a/Makefile b/Makefile index 3c11fe7..0bb3fd2 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ ifeq ($(CPLATFORM), arm64) CPLATFORM := aarch64 endif -GOVER := 1.19.7 +GOVER := 1.20.3 LND := v0.16.0-beta BITCOIND := 24.0.1 VAULT := 1.12.2 diff --git a/go.mod b/go.mod index 224590e..7cfaa93 100644 --- a/go.mod +++ b/go.mod @@ -88,6 +88,6 @@ replace github.com/gogo/protobuf => github.com/gogo/protobuf v1.3.2 // If you change this please also update .github/pull_request_template.md and // docs/INSTALL.md. -go 1.18 +go 1.20 retract v0.0.2 From 185625ec83111bf8b3d2cbfe465dac1a8a36f2e8 Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Wed, 3 May 2023 15:35:35 -0700 Subject: [PATCH 40/42] itest: remove redundant interrupt signal to lndsignerd --- itest/itest_lndharness_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/itest/itest_lndharness_test.go b/itest/itest_lndharness_test.go index 022d7ba..080f9be 100644 --- a/itest/itest_lndharness_test.go +++ b/itest/itest_lndharness_test.go @@ -198,7 +198,6 @@ func (l *lndHarness) Close() { l.tctx.t.Helper() _ = l.Lncli("stop") - _ = l.lndSignerCmd.Process.Signal(os.Interrupt) l.cancel() } From 83890b93778eb64f738a6e0ea2af01970a2ceafa Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Wed, 3 May 2023 15:48:12 -0700 Subject: [PATCH 41/42] build: use lnd 0.16.2 for itests --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 0bb3fd2..30e91d0 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ ifeq ($(CPLATFORM), arm64) endif GOVER := 1.20.3 -LND := v0.16.0-beta +LND := v0.16.2-beta BITCOIND := 24.0.1 VAULT := 1.12.2 From e62444492dc4a44a886c5e9b15a3ed3947d75e1a Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Wed, 10 May 2023 13:30:26 -0700 Subject: [PATCH 42/42] multi: move from nydig to nydig-oss --- .github/workflows/release-build.yml | 6 +++--- Dockerfile | 6 +++--- Dockerfile.vault | 4 ++-- README.md | 2 +- cmd/lndsignerd/main.go | 2 +- cmd/vault-plugin-lndsigner/main.go | 2 +- config.go | 2 +- go.mod | 2 +- itest/itest_lndharness_test.go | 4 ++-- itest/walletunlocker.proto | 2 +- keyring/keyring.go | 2 +- lndsigner.go | 2 +- log.go | 2 +- proto/lightning.proto | 2 +- proto/signer.proto | 2 +- proto/walletkit.proto | 2 +- rpcserver.go | 4 ++-- signer_server.go | 6 +++--- walletkit_server.go | 2 +- 19 files changed, 28 insertions(+), 28 deletions(-) diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 46ef00e..2562312 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -8,7 +8,7 @@ on: env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} - VAULT_IMAGE_NAME: nydig/vault-plugin-lndsigner + VAULT_IMAGE_NAME: nydig-oss/vault-plugin-lndsigner GO_VERSION: 1.20.3 jobs: @@ -67,7 +67,7 @@ jobs: ### vault-plugin-lndsigner Pre-built extensions of the latest [base Vault images](https://hub.docker.com/_/vault) with `vault-plugin-lndsigner` pre-installed are available here: - [${{ env.REGISTRY }}/${{ env.VAULT_IMAGE_NAME }}](https://github.com/nydig/lndsigner/pkgs/container/vault-plugin-lndsigner) + [${{ env.REGISTRY }}/${{ env.VAULT_IMAGE_NAME }}](https://github.com/nydig-oss/lndsigner/pkgs/container/vault-plugin-lndsigner) Note that the images within this package are provided for testing purposes only. Running a Vault image from an untrusted source in production is not recommended. @@ -116,7 +116,7 @@ jobs: org.opencontainers.image.title=Vault (with lndsigner) org.opencontainers.image.description=The base Hashicorp Vault image (library/vault), with the lndsigner plugin pre-installed. org.opencontainers.image.vendor=Hashicorp (Vault), NYDIG (lndsigner) - org.opencontainers.image.source=https://github.com/nydig/lndsigner + org.opencontainers.image.source=https://github.com/nydig-oss/lndsigner org.opencontainers.image.version=${{ github.ref_name }} org.opencontainers.image.licenses=(MPL-2.0 AND MIT) org.opencontainers.image.base.name=docker.io/library/vault:${{ matrix.vault_version }} diff --git a/Dockerfile b/Dockerfile index b06f622..560ef3c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,11 +4,11 @@ ARG gover=1.20.3 FROM golang:$gover AS release-builder -COPY . /go/src/github.com/nydig/lndsigner +COPY . /go/src/github.com/nydig-oss/lndsigner -RUN cd /go/src/github.com/nydig/lndsigner \ +RUN cd /go/src/github.com/nydig-oss/lndsigner \ && CGO_ENABLED=0 go install -buildvcs=false \ - github.com/nydig/lndsigner/cmd/... + github.com/nydig-oss/lndsigner/cmd/... ### Build an Alpine image FROM alpine:3.17 as alpine diff --git a/Dockerfile.vault b/Dockerfile.vault index edc65a6..4649b54 100644 --- a/Dockerfile.vault +++ b/Dockerfile.vault @@ -4,8 +4,8 @@ FROM library/vault:${VAULT_VER} ARG TARGETARCH ARG VAULT_SIGNER_PLUGIN_VER -ADD https://github.com/nydig/lndsigner/releases/download/${VAULT_SIGNER_PLUGIN_VER}/vault-plugin-lndsigner-linux-${TARGETARCH} /vault/plugins/vault-plugin-lndsigner -ADD https://github.com/nydig/lndsigner/releases/download/${VAULT_SIGNER_PLUGIN_VER}/vault-plugin-lndsigner-linux-${TARGETARCH}.SHA256SUM /vault/plugins/vault-plugin-lndsigner.SHA256SUM +ADD https://github.com/nydig-oss/lndsigner/releases/download/${VAULT_SIGNER_PLUGIN_VER}/vault-plugin-lndsigner-linux-${TARGETARCH} /vault/plugins/vault-plugin-lndsigner +ADD https://github.com/nydig-oss/lndsigner/releases/download/${VAULT_SIGNER_PLUGIN_VER}/vault-plugin-lndsigner-linux-${TARGETARCH}.SHA256SUM /vault/plugins/vault-plugin-lndsigner.SHA256SUM ADD LICENSE /vault/plugins/vault-plugin-lndsigner.LICENSE RUN \ diff --git a/README.md b/README.md index 2c5d740..21400eb 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ - [x] sign PSBTs for on-chain transactions, channel openings/closes, HTLC updates, etc. - [x] run itests -There is a [list of issues](https://github.com/nydig/lndsigner/issues?q=is%3Aissue+is%3Aopen+milestone%3Amainnet-ready) that tracks TODO items needed for a mainnet release. +There is a [list of issues](https://github.com/nydig-oss/lndsigner/issues?q=is%3Aissue+is%3Aopen+milestone%3Amainnet-ready) that tracks TODO items needed for a mainnet release. ## Usage diff --git a/cmd/lndsignerd/main.go b/cmd/lndsignerd/main.go index e7d3e9e..0c029bf 100644 --- a/cmd/lndsignerd/main.go +++ b/cmd/lndsignerd/main.go @@ -5,7 +5,7 @@ import ( "os" "github.com/jessevdk/go-flags" - "github.com/nydig/lndsigner" + "github.com/nydig-oss/lndsigner" ) func main() { diff --git a/cmd/vault-plugin-lndsigner/main.go b/cmd/vault-plugin-lndsigner/main.go index 1875aed..1c9b5a0 100644 --- a/cmd/vault-plugin-lndsigner/main.go +++ b/cmd/vault-plugin-lndsigner/main.go @@ -6,7 +6,7 @@ import ( "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/sdk/plugin" - "github.com/nydig/lndsigner/vault" + "github.com/nydig-oss/lndsigner/vault" ) func main() { diff --git a/config.go b/config.go index f003f4d..0476c73 100644 --- a/config.go +++ b/config.go @@ -17,7 +17,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" flags "github.com/jessevdk/go-flags" - "github.com/nydig/lndsigner/vault" + "github.com/nydig-oss/lndsigner/vault" ) const ( diff --git a/go.mod b/go.mod index 7cfaa93..d9511a7 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/nydig/lndsigner +module github.com/nydig-oss/lndsigner require ( github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 diff --git a/itest/itest_lndharness_test.go b/itest/itest_lndharness_test.go index 080f9be..16c40f7 100644 --- a/itest/itest_lndharness_test.go +++ b/itest/itest_lndharness_test.go @@ -14,8 +14,8 @@ import ( "encoding/json" "encoding/pem" "fmt" - "github.com/nydig/lndsigner" - "github.com/nydig/lndsigner/itest" + "github.com/nydig-oss/lndsigner" + "github.com/nydig-oss/lndsigner/itest" "io/fs" "math/big" "net" diff --git a/itest/walletunlocker.proto b/itest/walletunlocker.proto index 8a6cf80..36ad7a0 100644 --- a/itest/walletunlocker.proto +++ b/itest/walletunlocker.proto @@ -2,7 +2,7 @@ syntax = "proto3"; package lnrpc; -option go_package = "github.com/nydig/lndsigner/itest"; +option go_package = "github.com/nydig-oss/lndsigner/itest"; /* * Comments in this file will be directly parsed into the API diff --git a/keyring/keyring.go b/keyring/keyring.go index a84865f..0a783c4 100644 --- a/keyring/keyring.go +++ b/keyring/keyring.go @@ -19,7 +19,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/hashicorp/vault/api" - "github.com/nydig/lndsigner/vault" + "github.com/nydig-oss/lndsigner/vault" ) // signMethod defines the different ways a signer can sign, given a specific diff --git a/lndsigner.go b/lndsigner.go index 01db15b..fdb07ec 100644 --- a/lndsigner.go +++ b/lndsigner.go @@ -19,7 +19,7 @@ import ( "syscall" "github.com/hashicorp/vault/api" - "github.com/nydig/lndsigner/vault" + "github.com/nydig-oss/lndsigner/vault" "google.golang.org/grpc" "google.golang.org/grpc/credentials" ) diff --git a/log.go b/log.go index f83bd53..a5407ec 100644 --- a/log.go +++ b/log.go @@ -6,7 +6,7 @@ package lndsigner import ( - "github.com/nydig/lndsigner/keyring" + "github.com/nydig-oss/lndsigner/keyring" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) diff --git a/proto/lightning.proto b/proto/lightning.proto index 8be1716..dd38452 100644 --- a/proto/lightning.proto +++ b/proto/lightning.proto @@ -7,7 +7,7 @@ syntax = "proto3"; package proto; -option go_package = "github.com/nydig/lndsigner/proto"; +option go_package = "github.com/nydig-oss/lndsigner/proto"; /* * Comments in this file will be directly parsed into the API diff --git a/proto/signer.proto b/proto/signer.proto index 342a2db..e995a75 100644 --- a/proto/signer.proto +++ b/proto/signer.proto @@ -9,7 +9,7 @@ import "lightning.proto"; package proto; -option go_package = "github.com/nydig/lndsigner/proto"; +option go_package = "github.com/nydig-oss/lndsigner/proto"; // Signer is a service that gives access to the signing functionality of the // daemon's wallet. diff --git a/proto/walletkit.proto b/proto/walletkit.proto index 4c307f3..22fb2b7 100644 --- a/proto/walletkit.proto +++ b/proto/walletkit.proto @@ -7,7 +7,7 @@ syntax = "proto3"; package proto; -option go_package = "github.com/nydig/lndsigner/proto"; +option go_package = "github.com/nydig-oss/lndsigner/proto"; // WalletKit is a service that gives access to the core functionalities of the // daemon's wallet. diff --git a/rpcserver.go b/rpcserver.go index 020ed3d..e973a86 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -10,8 +10,8 @@ import ( "fmt" "github.com/hashicorp/vault/api" - "github.com/nydig/lndsigner/keyring" - "github.com/nydig/lndsigner/proto" + "github.com/nydig-oss/lndsigner/keyring" + "github.com/nydig-oss/lndsigner/proto" "github.com/tv42/zbase32" "google.golang.org/grpc" ) diff --git a/signer_server.go b/signer_server.go index 3ea65e4..cd5029b 100644 --- a/signer_server.go +++ b/signer_server.go @@ -11,9 +11,9 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" - "github.com/nydig/lndsigner/keyring" - "github.com/nydig/lndsigner/proto" - "github.com/nydig/lndsigner/vault" + "github.com/nydig-oss/lndsigner/keyring" + "github.com/nydig-oss/lndsigner/proto" + "github.com/nydig-oss/lndsigner/vault" ) // Server is a sub-server of the main RPC server: the signer RPC. This sub RPC diff --git a/walletkit_server.go b/walletkit_server.go index 8425e3f..83df2ae 100644 --- a/walletkit_server.go +++ b/walletkit_server.go @@ -11,7 +11,7 @@ import ( "fmt" "github.com/btcsuite/btcd/btcutil/psbt" - "github.com/nydig/lndsigner/proto" + "github.com/nydig-oss/lndsigner/proto" ) // walletKit is a sub-RPC server that exposes a tool kit which allows clients