From 8d2c5ed4ed8f5a1406c875cec69f6da27a61d485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?eV=20=28=E3=8B=8E=29?= Date: Mon, 12 Nov 2018 19:01:51 +0000 Subject: [PATCH] add offline create wallet flow, fix transaction submit expiration bug (#48) --- bin/vault-create-wallet/main.go | 162 +++++++++++++++++++++++---- settlement/README.md | 34 +++++- settlement/settlement.go | 28 ++++- wallet/provider/uphold/httpsigned.go | 25 +++-- wallet/provider/uphold/uphold.go | 136 ++++++++++++++++++---- 5 files changed, 322 insertions(+), 63 deletions(-) diff --git a/bin/vault-create-wallet/main.go b/bin/vault-create-wallet/main.go index 574e1fd1c..039538437 100644 --- a/bin/vault-create-wallet/main.go +++ b/bin/vault-create-wallet/main.go @@ -1,70 +1,186 @@ package main import ( + "encoding/hex" + "encoding/json" "flag" "fmt" - "log" "os" "github.com/brave-intl/bat-go/utils/altcurrency" + "github.com/brave-intl/bat-go/utils/formatters" + "github.com/brave-intl/bat-go/utils/httpsignature" "github.com/brave-intl/bat-go/utils/vaultsigner" "github.com/brave-intl/bat-go/wallet" "github.com/brave-intl/bat-go/wallet/provider/uphold" + "github.com/hashicorp/vault/api" + log "github.com/sirupsen/logrus" + "golang.org/x/crypto/ed25519" ) +var flags = flag.NewFlagSet("", flag.ExitOnError) +var verbose = flags.Bool("v", false, "verbose output") +var offline = flags.Bool("offline", false, "operate in multi-step offline mode") + +// State contains the current state of the registration +type State struct { + WalletInfo wallet.Info `json:"walletInfo"` + Registration string `json:"registration"` +} + func main() { - log.SetFlags(0) + log.SetFormatter(&formatters.CliFormatter{}) - flag.Usage = func() { + flags.Usage = func() { log.Printf("Create a new wallet backed by vault.\n\n") log.Printf("Usage:\n\n") log.Printf(" %s WALLET_NAME\n\n", os.Args[0]) log.Printf(" If a vault keypair exists with name WALLET_NAME, it will be used.\n") log.Printf(" Otherwise a new vault keypair with that name will be generated.\n\n") + flags.PrintDefaults() + } + err := flags.Parse(os.Args[1:]) + if err != nil { + log.Fatalln(err) + } + + if *verbose { + log.SetLevel(log.DebugLevel) } - flag.Parse() - args := flag.Args() + args := flags.Args() if len(args) != 1 { log.Printf("ERROR: Must pass a single argument to name generated wallet / keypair\n\n") - flag.Usage() + flags.Usage() os.Exit(1) } name := args[0] + logFile := name + "-registration.json" + + var state State + var enc *json.Encoder - var info wallet.Info - info.Provider = "uphold" - info.ProviderID = "" - { - tmp := altcurrency.BAT - info.AltCurrency = &tmp + if *offline { + f, err := os.OpenFile(logFile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0600) + if err != nil { + log.Fatalln(err) + } + + dec := json.NewDecoder(f) + + for dec.More() { + err := dec.Decode(&state) + if err != nil { + log.Fatalln(err) + } + } + + enc = json.NewEncoder(f) } - client, err := vaultsigner.Connect() - if err != nil { - log.Fatalln(err) + if len(state.WalletInfo.PublicKey) == 0 || len(state.Registration) == 0 { + var info wallet.Info + info.Provider = "uphold" + info.ProviderID = "" + { + tmp := altcurrency.BAT + info.AltCurrency = &tmp + } + state.WalletInfo = info + + client, err := vaultsigner.Connect() + if err != nil { + log.Fatalln(err) + } + + signer, err := vaultsigner.New(client, name) + if err != nil { + log.Fatalln(err) + } + + fmt.Printf("Keypair with public key: %s\n", signer) + + state.WalletInfo.PublicKey = signer.String() + + wallet := &uphold.Wallet{Info: state.WalletInfo, PrivKey: signer, PubKey: signer} + + reg, err := wallet.PrepareRegistration(name) + if err != nil { + log.Fatalln(err) + } + state.Registration = reg + + if *offline { + err = enc.Encode(state) + if err != nil { + log.Fatalln(err) + } + + fmt.Printf("Success, signed registration for wallet \"%s\"\n", name) + fmt.Printf("Please copy %s to the online machine and re-run.\n", logFile) + os.Exit(1) + } } - signer, err := vaultsigner.New(client, name) + if len(state.WalletInfo.ProviderID) == 0 { + var publicKey httpsignature.Ed25519PubKey + publicKey, err := hex.DecodeString(state.WalletInfo.PublicKey) + if err != nil { + log.Fatalln(err) + } + wallet := uphold.Wallet{Info: state.WalletInfo, PrivKey: ed25519.PrivateKey{}, PubKey: publicKey} + + err = wallet.SubmitRegistration(state.Registration) + if err != nil { + log.Fatalln(err) + } + + fmt.Printf("Success, registered new keypair and wallet \"%s\"\n", name) + fmt.Printf("Uphold card ID %s\n", wallet.Info.ProviderID) + state.WalletInfo.ProviderID = wallet.Info.ProviderID + + depositAddr, err := wallet.CreateCardAddress("ethereum") + if err != nil { + log.Fatalln(err) + } + fmt.Printf("ETH deposit addr: %s\n", depositAddr) + + if *offline { + err = enc.Encode(state) + if err != nil { + log.Fatalln(err) + } + + fmt.Printf("Please copy %s to the offline machine and re-run.\n", logFile) + os.Exit(1) + } + } + + client, err := vaultsigner.Connect() if err != nil { log.Fatalln(err) } - fmt.Printf("Generated keypair with public key: %s\n", signer) - - wallet := &uphold.Wallet{Info: info, PrivKey: signer, PubKey: signer} - err = wallet.Register(name) + mounts, err := client.Sys().ListMounts() if err != nil { log.Fatalln(err) } + if _, ok := mounts["wallets/"]; !ok { + // Mount kv secret backend if not already mounted + if err = client.Sys().Mount("wallets", &api.MountInput{ + Type: "kv", + }); err != nil { + log.Fatalln(err) + } + } - fmt.Printf("Success, registered new keypair and wallet \"%s\"\n", name) - fmt.Printf("Uphold card ID %s", wallet.Info.ProviderID) _, err = client.Logical().Write("wallets/"+name, map[string]interface{}{ - "providerId": wallet.Info.ProviderID, + "providerId": state.WalletInfo.ProviderID, }) if err != nil { log.Fatalln(err) } + + fmt.Printf("Wallet setup complete!\n") } diff --git a/settlement/README.md b/settlement/README.md index fc7de0554..87bd7fc04 100644 --- a/settlement/README.md +++ b/settlement/README.md @@ -17,7 +17,7 @@ export VAULT_ADDR=http://127.0.0.1:8200 gpg -d SHARE.GPG | ./vault-unseal ``` -## Running settlement +## Bringing up vault On the offline computer, in one window run: ``` @@ -29,7 +29,10 @@ In another run: gpg -d SHARE.GPG | ./vault-unseal ``` -You are now ready to transact +## Running settlement + +First bring up vault as described above. + ``` ./vault-sign-settlement -in ``` @@ -56,3 +59,30 @@ allow restoring from errors and to avoid duplicate payouts. Finally upload the "-finished" output file to eyeshade to account for payout transactions that were made. + +## Creating a new offline wallet + +On the offline machine, first bring up vault as described above. + +Run vault-create-wallet, this will sign the registration and store it into +a local file: +``` +vault-create-wallet -offline name-of-new-wallet +``` + +Copy the created `name-of-new-wallet-registration.json` file to the online +machine. + +Re-run vault-create-wallet, this will submit the pre-signed registration: +``` +export UPHOLD_ENVIRONMENT= +export UPHOLD_HTTP_PROXY= +export UPHOLD_ACCESS_TOKEN= +vault-create-wallet -offline name-of-new-wallet +``` + +Finally copy `name-of-new-wallet-registration.json` back to the offline +machine and run vault-create-wallet to record the provider ID in vault: +``` +vault-create-wallet -offline name-of-new-wallet +``` diff --git a/settlement/settlement.go b/settlement/settlement.go index e4f7b6448..3737354ce 100644 --- a/settlement/settlement.go +++ b/settlement/settlement.go @@ -129,13 +129,29 @@ func SubmitPreparedTransaction(settlementWallet *uphold.Wallet, settlement *Tran return nil } - if len(settlement.ProviderID) > 0 && time.Now().Before(settlement.ValidUntil) { - fmt.Printf("already submitted, skipping submit for channel %s\n", settlement.Channel) - return nil - } - if len(settlement.ProviderID) > 0 { - fmt.Printf("already submitted, but quote has expired for channel %s\n", settlement.Channel) + // first check if the transaction has already been confirmed + upholdInfo, err := settlementWallet.GetTransaction(settlement.ProviderID) + if err == nil { + settlement.Status = upholdInfo.Status + settlement.Currency = upholdInfo.DestCurrency + settlement.Amount = upholdInfo.DestAmount + settlement.TransferFee = upholdInfo.TransferFee + settlement.ExchangeFee = upholdInfo.ExchangeFee + + if settlement.IsComplete() { + fmt.Printf("transaction already complete for channel %s\n", settlement.Channel) + return nil + } + } else if wallet.IsNotFound(err) { // unconfirmed transactions appear as "not found" + if time.Now().Before(settlement.ValidUntil) { + return nil + } + + fmt.Printf("already submitted, but quote has expired for channel %s\n", settlement.Channel) + } else { + return err + } } // post the settlement to uphold but do not confirm it diff --git a/wallet/provider/uphold/httpsigned.go b/wallet/provider/uphold/httpsigned.go index 549ede31c..e3b610119 100644 --- a/wallet/provider/uphold/httpsigned.go +++ b/wallet/provider/uphold/httpsigned.go @@ -18,33 +18,40 @@ type HTTPSignedRequest struct { Body string `json:"octets" valid:"json"` } -// extract an HTTP request from the encapsulated signed request -func (sr *HTTPSignedRequest) extract() (*httpsignature.Signature, *http.Request, error) { +// extract from the encapsulated signed request +// into the provided HTTP request +// NOTE it intentionally does not set the URL +func (sr *HTTPSignedRequest) extract(r *http.Request) (*httpsignature.Signature, error) { + if r == nil { + return nil, errors.New("r was nil") + } + var s httpsignature.Signature err := s.UnmarshalText([]byte(sr.Headers["signature"])) if err != nil { - return nil, nil, err + return nil, err } - var r http.Request r.Body = ioutil.NopCloser(bytes.NewBufferString(sr.Body)) - r.Header = http.Header{} + if r.Header == nil { + r.Header = http.Header{} + } for k, v := range sr.Headers { if !httplex.ValidHeaderFieldName(k) { - return nil, nil, errors.New("invalid encapsulated header name") + return nil, errors.New("invalid encapsulated header name") } if !httplex.ValidHeaderFieldValue(v) { - return nil, nil, errors.New("invalid encapsulated header value") + return nil, errors.New("invalid encapsulated header value") } if k == httpsignature.RequestTarget { // TODO implement pseudo-header - return nil, nil, fmt.Errorf("%s pseudo-header not implemented", httpsignature.RequestTarget) + return nil, fmt.Errorf("%s pseudo-header not implemented", httpsignature.RequestTarget) } r.Header.Set(k, v) } - return &s, &r, nil + return &s, nil } // encapsulate a signed HTTP request diff --git a/wallet/provider/uphold/uphold.go b/wallet/provider/uphold/uphold.go index 719c1da3b..81c47daaf 100644 --- a/wallet/provider/uphold/uphold.go +++ b/wallet/provider/uphold/uphold.go @@ -191,17 +191,17 @@ type createCardRequest struct { PublicKey string `json:"publicKey"` } -// Register a wallet with Uphold with label -func (w *Wallet) Register(label string) error { +// sign registration for this wallet with Uphold with label +func (w *Wallet) signRegistration(label string) (*http.Request, error) { reqPayload := createCardRequest{Label: label, AltCurrency: w.Info.AltCurrency, PublicKey: w.PubKey.String()} payload, err := json.Marshal(reqPayload) if err != nil { - return err + return nil, err } req, err := newRequest("POST", "/v0/me/cards", bytes.NewBuffer(payload)) if err != nil { - return err + return nil, err } var s httpsignature.Signature @@ -216,6 +216,16 @@ func (w *Wallet) Register(label string) error { req.Header.Add("Digest", d.String()) err = s.Sign(w.PrivKey, crypto.Hash(0), req) + if err != nil { + return nil, err + } + + return req, nil +} + +// Register a wallet with Uphold with label +func (w *Wallet) Register(label string) error { + req, err := w.signRegistration(label) if err != nil { return err } @@ -234,6 +244,63 @@ func (w *Wallet) Register(label string) error { return nil } +// SubmitRegistration from a b64 encoded signed string +func (w *Wallet) SubmitRegistration(registrationB64 string) error { + b, err := base64.StdEncoding.DecodeString(registrationB64) + if err != nil { + return err + } + + var signedTx HTTPSignedRequest + err = json.Unmarshal(b, &signedTx) + if err != nil { + return err + } + + req, err := newRequest("POST", "/v0/me/cards", nil) + if err != nil { + return err + } + + _, err = signedTx.extract(req) + if err != nil { + return err + } + + body, _, err := submit(req) + if err != nil { + return err + } + + var details CardDetails + err = json.Unmarshal(body, &details) + if err != nil { + return err + } + w.Info.ProviderID = details.ID.String() + return nil +} + +// PrepareRegistration returns a b64 encoded serialized signed registration suitable for SubmitRegistration +func (w *Wallet) PrepareRegistration(label string) (string, error) { + req, err := w.signRegistration(label) + if err != nil { + return "", err + } + + httpSignedReq, err := encapsulate(req) + if err != nil { + return "", err + } + + b, err := json.Marshal(&httpSignedReq) + if err != nil { + return "", err + } + + return base64.StdEncoding.EncodeToString(b), nil +} + // CardSettings contains settings corresponding to the Uphold card type CardSettings struct { Protected bool `json:"protected,omitempty"` @@ -386,7 +453,8 @@ func (w *Wallet) decodeTransaction(transactionB64 string) (*transactionRequest, return nil, errors.New("The digest header does not match the included body") } - sig, req, err := signedTx.extract() + var req http.Request + sig, err := signedTx.extract(&req) if err != nil { return nil, err } @@ -401,7 +469,7 @@ func (w *Wallet) decodeTransaction(transactionB64 string) (*transactionRequest, return nil, errors.New("A transaction signature must cover the request body via digest") } - valid, err := sig.Verify(w.PubKey, crypto.Hash(0), req) + valid, err := sig.Verify(w.PubKey, crypto.Hash(0), &req) if err != nil { return nil, err } @@ -551,33 +619,20 @@ func (w *Wallet) SubmitTransaction(transactionB64 string, confirm bool) (*wallet return nil, err } - var headers http.Header - var body io.ReadCloser - { - var req *http.Request - _, req, err = signedTx.extract() - if err != nil { - return nil, err - } - headers = req.Header - body = req.Body - } - url := "/v0/me/cards/" + w.ProviderID + "/transactions" if confirm { url = url + "?commit=true" } + req, err := newRequest("POST", url, nil) if err != nil { return nil, err } - // Copy headers added from newRequest - for k := range req.Header { - headers.Set(k, req.Header.Get(k)) + _, err = signedTx.extract(req) + if err != nil { + return nil, err } - req.Header = headers - req.Body = body respBody, _, err := submit(req) if err != nil { @@ -721,3 +776,38 @@ func (w *Wallet) GetBalance(refresh bool) (*wallet.Balance, error) { return &balance, nil } + +type createCardAddressRequest struct { + Network string `json:"network"` +} + +type createCardAddressResponse struct { + ID string `json:"id"` +} + +// CreateCardAddress on network, returning the address +func (w *Wallet) CreateCardAddress(network string) (string, error) { + reqPayload := createCardAddressRequest{Network: network} + payload, err := json.Marshal(reqPayload) + if err != nil { + return "", err + } + + req, err := newRequest("POST", fmt.Sprintf("/v0/me/cards/%s/addresses", w.ProviderID), bytes.NewBuffer(payload)) + if err != nil { + return "", err + } + + body, _, err := submit(req) + if err != nil { + return "", err + } + + var details createCardAddressResponse + err = json.Unmarshal(body, &details) + if err != nil { + return "", err + } + return details.ID, nil + +}