From 6f57aba94f439c9a1d8c068ba76020cced175c0b Mon Sep 17 00:00:00 2001 From: Alex Akselrod Date: Fri, 16 Dec 2022 15:15:22 -0800 Subject: [PATCH] vault: implement key import --- README.md | 28 +- go.mod | 9 +- go.sum | 12 +- vault/aezeed.go | 2154 +++++++++++++++++++++++++++++++++++++++++ vault/backend.go | 81 +- vault/backend_test.go | 209 ++++ vault/errors.go | 18 + vault/keys.go | 45 + vault/paths.go | 366 +++---- 9 files changed, 2724 insertions(+), 198 deletions(-) create mode 100644 vault/aezeed.go create mode 100644 vault/backend_test.go create mode 100644 vault/errors.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 b2c63b0..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,8 +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 ) @@ -21,6 +25,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,17 +60,19 @@ 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 + 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 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..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= @@ -270,21 +273,26 @@ 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= 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= @@ -342,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= @@ -417,6 +426,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/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 e2a5c09..3a77a4a 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"` @@ -33,6 +37,30 @@ type listedAccount struct { WatchOnly bool `json:"watch_only"` } +type backend struct { + *framework.Backend +} + +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 + } + defer zero(seed) + + return b.newNode(ctx, req.Storage, seed, strNet, strNode) +} + func (b *backend) listAccounts(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { @@ -453,16 +481,16 @@ func (b *backend) getNode(ctx context.Context, storage logical.Storage, return nil, nil, errors.New("node not found") } - if len(entry.Value) <= hdkeychain.RecommendedSeedLen { + if len(entry.Value) <= seedLen { return nil, nil, errors.New("got invalid seed from storage") } - 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, @@ -502,20 +530,13 @@ 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( - hdkeychain.RecommendedSeedLen, - ) + seed, err = hdkeychain.GenerateSeed(seedLen) } if err != nil { b.Logger().Error("Failed to generate new LND seed", @@ -523,6 +544,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), @@ -538,27 +572,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{}{ @@ -585,7 +632,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/backend_test.go b/vault/backend_test.go new file mode 100644 index 0000000..5fcc024 --- /dev/null +++ b/vault/backend_test.go @@ -0,0 +1,209 @@ +package vault + +import ( + "context" + "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" +) + +func TestBackend(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "vault-plugin-lndsigner") + require.NoError(t, err) + + defer os.RemoveAll(tmpDir) + + logger := hclog.Default() + + pStorage, err := filestore.NewFileBackend( + map[string]string{"path": tmpDir}, + logger, + ) + + storage := logical.NewLogicalStorage(pStorage) + + ctx := context.Background() + + b, err := Factory(ctx, &logical.BackendConfig{ + StorageView: storage, + Logger: logger, + }) + require.NoError(t, err) + + backEnd := b.(*backend) + + testCases := []struct { + name string + path *framework.Path + op logical.Operation + data *framework.FieldData + resp *logical.Response + err error + }{ + { + name: ErrSeedPhraseWrongLength.Error(), + path: backEnd.importPath(), + op: logical.CreateOperation, + data: &framework.FieldData{}, + resp: nil, + err: ErrSeedPhraseWrongLength, + }, + { + name: ErrSeedPhraseNotBIP39.Error(), + path: backEnd.importPath(), + op: logical.CreateOperation, + data: &framework.FieldData{ + Raw: 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", + }, + }, + resp: nil, + err: ErrSeedPhraseNotBIP39, + }, + { + name: ErrBadCipherSeedVer.Error(), + path: backEnd.importPath(), + op: logical.CreateOperation, + data: &framework.FieldData{ + Raw: 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", + }, + }, + resp: nil, + err: ErrBadCipherSeedVer, + }, + { + name: ErrChecksumMismatch.Error(), + path: backEnd.importPath(), + op: logical.CreateOperation, + data: &framework.FieldData{ + Raw: 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", + }, + }, + resp: nil, + err: ErrChecksumMismatch, + }, + { + name: "import without passphrase", + path: backEnd.importPath(), + op: logical.CreateOperation, + data: &framework.FieldData{ + Raw: 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": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + }, + }, + resp: &logical.Response{ + Data: map[string]interface{}{ + "node": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + }, + }, + err: nil, + }, + { + name: "import with passphrase", + path: backEnd.importPath(), + op: logical.CreateOperation, + data: &framework.FieldData{ + Raw: 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": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + }, + }, + resp: &logical.Response{ + Data: map[string]interface{}{ + "node": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + }, + }, + err: nil, + }, + { + name: ErrNodeAlreadyExists.Error(), + path: backEnd.importPath(), + op: logical.CreateOperation, + data: &framework.FieldData{ + Raw: 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", + }, + }, + resp: nil, + err: ErrNodeAlreadyExists, + }, + { + name: ErrInvalidPassphrase.Error(), + path: backEnd.importPath(), + op: logical.CreateOperation, + data: &framework.FieldData{ + Raw: 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", + }, + }, + resp: nil, + err: ErrInvalidPassphrase, + }, + { + name: ErrNodePubkeyMismatch.Error(), + path: backEnd.importPath(), + op: logical.CreateOperation, + data: &framework.FieldData{ + Raw: 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", + }, + }, + resp: nil, + err: ErrNodePubkeyMismatch, + }, + { + name: ErrInvalidNetwork.Error(), + path: backEnd.importPath(), + op: logical.CreateOperation, + data: &framework.FieldData{ + Raw: 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", + }, + }, + resp: nil, + err: ErrInvalidNetwork, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + testCase.data.Schema = testCase.path.Fields + + resp, err := testCase.path.Callbacks[testCase.op]( + ctx, &logical.Request{Storage: storage}, testCase.data, + ) + require.Equal(t, testCase.err, err) + + if err != nil { + return + } + + require.Equal(t, testCase.resp, resp) + + return + }) + } +} diff --git a/vault/errors.go b/vault/errors.go new file mode 100644 index 0000000..2749208 --- /dev/null +++ b/vault/errors.go @@ -0,0 +1,18 @@ +package vault + +import ( + "errors" +) + +var ( + ErrSeedPhraseWrongLength = errors.New("seed phrase must be 24 words") + ErrNodeAlreadyExists = errors.New("node already exists") + ErrInvalidPassphrase = errors.New("invalid passphrase") + ErrNodePubkeyMismatch = errors.New("node pubkey mismatch") + ErrInvalidNetwork = errors.New("invalid network") + 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") +) diff --git a/vault/keys.go b/vault/keys.go index ffbbf53..ecc0e23 100644 --- a/vault/keys.go +++ b/vault/keys.go @@ -18,6 +18,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 21a620d..8461a07 100644 --- a/vault/paths.go +++ b/vault/paths.go @@ -10,216 +10,228 @@ import ( "github.com/hashicorp/vault/sdk/logical" ) -const ( - // MaxAcctID is the number of accounts/key families to create on - // initialization. - MaxAcctID = 255 +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: ` - Bip0043purpose = 1017 - NodeKeyAcct = 6 -) +GET - list all node pubkeys and coin types for HD derivations +POST - generate a new node seed and store it indexed by node pubkey -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 +`, + Fields: map[string]*framework.FieldSchema{ + "network": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Network, one of " + + "'mainnet', 'testnet', " + + "'simnet', 'signet', or " + + "'regtest'", + Default: "regtest", }, }, } -) - -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) importPath() *framework.Path { + return &framework.Path{ + Pattern: "lnd-nodes/import/?", + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.importNode, + logical.CreateOperation: b.importNode, + }, + HelpSynopsis: "Import existing LND node into vault", + 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 +POST - import existing LND node into vault with seedphrase and passphrase `, - 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{ + "node": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "optional: node pubkey, " + + "must be 66 hex characters", + Default: "", }, - }, - &framework.Path{ - Pattern: "lnd-nodes/accounts/?", - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ReadOperation: b.listAccounts, + "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: "", }, - HelpSynopsis: "List accounts for import into LND " + - "watch-only node", - HelpDescription: ` + "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/?", + 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: "", }, - HelpSynopsis: "Derive pubkeys and sign with privkeys", - HelpDescription: ` + "peer": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "pubkey for ECDH peer, " + + "must be 66 hex characters", + Default: "", + }, + }, + } +} + +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.importPath(), + b.accountsPath(), + b.ecdhPath(), + b.signPath(), + } +}