From ac488464c04f9333fcf8d8b3829b5cf14bea79a6 Mon Sep 17 00:00:00 2001 From: Jamie Holdstock Date: Tue, 26 May 2020 14:14:38 +0100 Subject: [PATCH] Rework client/server authentication. (#58) * Rework client/server authentication. - Remove Signature from all requests, and instead expect a signature in HTTP header "VSP-Client-Signature". - Remove CommitmentSignatures from the database. - Use a bool flag to indicate when a ticket is missing from the database rather than an error. This commit introduces a lot of duplication into each of the authenticated HTTP handlers. This should be removed in future work which moves the authentication to a dedicated middleware. * Introduce auth and rpc middleware. This removed the duplication added in the previous commit, and also removes the duplication of RPC client error handling. --- README.md | 5 +- database/database_test.go | 44 ++++++------ database/ticket.go | 30 ++++---- rpc/feewallet.go | 26 +++++++ webapi/getfeeaddress.go | 142 ++++++++++++-------------------------- webapi/helpers.go | 17 +++++ webapi/middleware.go | 124 +++++++++++++++++++++++++++++++++ webapi/payfee.go | 57 ++++++--------- webapi/setvotechoices.go | 73 +++++--------------- webapi/ticketstatus.go | 47 ++++--------- webapi/types.go | 3 - webapi/webapi.go | 28 +++++--- 12 files changed, 325 insertions(+), 271 deletions(-) create mode 100644 webapi/middleware.go diff --git a/README.md b/README.md index af6a5c6..f7a44eb 100644 --- a/README.md +++ b/README.md @@ -58,8 +58,11 @@ ticket details + fee to a VSP, and the VSP will take the fee and vote in return. - When dcrvsp is started for the first time, it generates a ed25519 keypair and stores it in the database. This key is used to sign all API responses, and the - signature is included in the response header `VSP-Signature`. Error responses + signature is included in the response header `VSP-Server-Signature`. Error responses are not signed. +- Every client request which references a ticket should include a HTTP header + `VSP-Client-Signature`. The value of this header must be a signature of the + request body, signed with the commitment address of the referenced ticket. - An xpub key is provided to dcrvsp via config. The first time dcrvsp starts, it imports this xpub to create a new wallet account. This account is used to derive addresses for fee payments. diff --git a/database/database_test.go b/database/database_test.go index e14b1dc..086d740 100644 --- a/database/database_test.go +++ b/database/database_test.go @@ -15,17 +15,16 @@ var ( func exampleTicket() Ticket { return Ticket{ - Hash: "Hash", - CommitmentAddress: "Address", - CommitmentSignature: "CommitmentSignature", - FeeAddress: "FeeAddress", - SDiff: 1, - BlockHeight: 2, - VoteChoices: map[string]string{"AgendaID": "Choice"}, - VotingKey: "VotingKey", - VSPFee: 0.1, - FeeExpiration: 4, - FeeTxHash: "", + Hash: "Hash", + CommitmentAddress: "Address", + FeeAddress: "FeeAddress", + SDiff: 1, + BlockHeight: 2, + VoteChoices: map[string]string{"AgendaID": "Choice"}, + VotingKey: "VotingKey", + VSPFee: 0.1, + FeeExpiration: 4, + FeeTxHash: "", } } @@ -95,15 +94,17 @@ func testGetTicketByHash(t *testing.T) { } // Retrieve ticket from database. - retrieved, err := db.GetTicketByHash(ticket.Hash) + retrieved, found, err := db.GetTicketByHash(ticket.Hash) if err != nil { t.Fatalf("error retrieving ticket by ticket hash: %v", err) } + if !found { + t.Fatal("expected found==true") + } // Check ticket fields match expected. if retrieved.Hash != ticket.Hash || retrieved.CommitmentAddress != ticket.CommitmentAddress || - retrieved.CommitmentSignature != ticket.CommitmentSignature || retrieved.FeeAddress != ticket.FeeAddress || retrieved.SDiff != ticket.SDiff || retrieved.BlockHeight != ticket.BlockHeight || @@ -115,10 +116,13 @@ func testGetTicketByHash(t *testing.T) { t.Fatal("retrieved ticket value didnt match expected") } - // Error if non-existent ticket requested. - _, err = db.GetTicketByHash("Not a real ticket hash") - if err == nil { - t.Fatal("expected an error while retrieving a non-existent ticket") + // Check found==false when requesting a non-existent ticket. + _, found, err = db.GetTicketByHash("Not a real ticket hash") + if err != nil { + t.Fatalf("error retrieving ticket by ticket hash: %v", err) + } + if found { + t.Fatal("expected found==false") } } @@ -141,7 +145,7 @@ func testSetTicketVotingKey(t *testing.T) { } // Retrieve ticket from database. - retrieved, err := db.GetTicketByHash(ticket.Hash) + retrieved, _, err := db.GetTicketByHash(ticket.Hash) if err != nil { t.Fatalf("error retrieving ticket by ticket hash: %v", err) } @@ -171,7 +175,7 @@ func testUpdateExpireAndFee(t *testing.T) { } // Get updated ticket - retrieved, err := db.GetTicketByHash(ticket.Hash) + retrieved, _, err := db.GetTicketByHash(ticket.Hash) if err != nil { t.Fatalf("error retrieving updated ticket: %v", err) } @@ -199,7 +203,7 @@ func testUpdateVoteChoices(t *testing.T) { } // Get updated ticket - retrieved, err := db.GetTicketByHash(ticket.Hash) + retrieved, _, err := db.GetTicketByHash(ticket.Hash) if err != nil { t.Fatalf("error retrieving updated ticket: %v", err) } diff --git a/database/ticket.go b/database/ticket.go index 4b9f9b6..8731ccb 100644 --- a/database/ticket.go +++ b/database/ticket.go @@ -10,17 +10,16 @@ import ( ) type Ticket struct { - Hash string `json:"hash"` - CommitmentSignature string `json:"commitmentsignature"` - CommitmentAddress string `json:"commitmentaddress"` - FeeAddress string `json:"feeaddress"` - SDiff float64 `json:"sdiff"` - BlockHeight int64 `json:"blockheight"` - VoteChoices map[string]string `json:"votechoices"` - VotingKey string `json:"votingkey"` - VSPFee float64 `json:"vspfee"` - FeeExpiration int64 `json:"feeexpiration"` - FeeTxHash string `json:"feetxhash"` + Hash string `json:"hash"` + CommitmentAddress string `json:"commitmentaddress"` + FeeAddress string `json:"feeaddress"` + SDiff float64 `json:"sdiff"` + BlockHeight int64 `json:"blockheight"` + VoteChoices map[string]string `json:"votechoices"` + VotingKey string `json:"votingkey"` + VSPFee float64 `json:"vspfee"` + FeeExpiration int64 `json:"feeexpiration"` + FeeTxHash string `json:"feetxhash"` } func (t *Ticket) FeeExpired() bool { @@ -82,14 +81,15 @@ func (vdb *VspDatabase) SetTicketVotingKey(ticketHash, votingKey string, voteCho }) } -func (vdb *VspDatabase) GetTicketByHash(ticketHash string) (Ticket, error) { +func (vdb *VspDatabase) GetTicketByHash(ticketHash string) (Ticket, bool, error) { var ticket Ticket + var found bool err := vdb.db.View(func(tx *bolt.Tx) error { ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK) ticketBytes := ticketBkt.Get([]byte(ticketHash)) if ticketBytes == nil { - return ErrNoTicketFound + return nil } err := json.Unmarshal(ticketBytes, &ticket) @@ -97,10 +97,12 @@ func (vdb *VspDatabase) GetTicketByHash(ticketHash string) (Ticket, error) { return fmt.Errorf("could not unmarshal ticket: %v", err) } + found = true + return nil }) - return ticket, err + return ticket, found, err } func (vdb *VspDatabase) UpdateVoteChoices(ticketHash string, voteChoices map[string]string) error { diff --git a/rpc/feewallet.go b/rpc/feewallet.go index b49de43..9300ebe 100644 --- a/rpc/feewallet.go +++ b/rpc/feewallet.go @@ -2,11 +2,15 @@ package rpc import ( "context" + "encoding/hex" "fmt" wallettypes "decred.org/dcrwallet/rpc/jsonrpc/types" + "github.com/decred/dcrd/blockchain/stake/v3" + "github.com/decred/dcrd/chaincfg/v3" "github.com/decred/dcrd/dcrutil/v3" dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v2" + "github.com/decred/dcrd/wire" ) const ( @@ -129,3 +133,25 @@ func (c *FeeWalletRPC) GetWalletFee() (dcrutil.Amount, error) { return amount, nil } + +func (c *FeeWalletRPC) GetTicketCommitmentAddress(ticketHash string, netParams *chaincfg.Params) (string, error) { + resp, err := c.GetRawTransaction(ticketHash) + if err != nil { + return "", err + } + + msgHex, err := hex.DecodeString(resp.Hex) + if err != nil { + return "", err + } + msgTx := wire.NewMsgTx() + if err = msgTx.FromBytes(msgHex); err != nil { + return "", err + } + addr, err := stake.AddrFromSStxPkScrCommitment(msgTx.TxOut[1].PkScript, netParams) + if err != nil { + return "", err + } + + return addr.Address(), nil +} diff --git a/webapi/getfeeaddress.go b/webapi/getfeeaddress.go index 49b33ed..e44de74 100644 --- a/webapi/getfeeaddress.go +++ b/webapi/getfeeaddress.go @@ -1,104 +1,72 @@ package webapi import ( - "encoding/base64" "encoding/hex" - "errors" - "fmt" "net/http" "time" "github.com/decred/dcrd/blockchain/stake/v3" - "github.com/decred/dcrd/chaincfg/chainhash" - "github.com/decred/dcrd/dcrutil/v3" "github.com/decred/dcrd/wire" "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" "github.com/jholdstock/dcrvsp/database" "github.com/jholdstock/dcrvsp/rpc" ) // feeAddress is the handler for "POST /feeaddress". func feeAddress(c *gin.Context) { + + // Get values which have been added to context by middleware. + rawRequest := c.MustGet("RawRequest").([]byte) + ticket := c.MustGet("Ticket").(database.Ticket) + knownTicket := c.MustGet("KnownTicket").(bool) + commitmentAddress := c.MustGet("CommitmentAddress").(string) + fWalletClient := c.MustGet("FeeWalletClient").(*rpc.FeeWalletRPC) + var feeAddressRequest FeeAddressRequest - if err := c.ShouldBindJSON(&feeAddressRequest); err != nil { + if err := binding.JSON.BindBody(rawRequest, &feeAddressRequest); err != nil { log.Warnf("Bad feeaddress request from %s: %v", c.ClientIP(), err) sendErrorResponse(err.Error(), http.StatusBadRequest, c) return } - // Validate TicketHash. - ticketHashStr := feeAddressRequest.TicketHash - txHash, err := chainhash.NewHashFromStr(ticketHashStr) - if err != nil { - log.Warnf("Invalid ticket hash from %s", c.ClientIP()) - sendErrorResponse("invalid ticket hash", http.StatusBadRequest, c) - return - } + // VSP already knows this ticket and has already issued it a fee address. + if knownTicket { + // If the expiry period has passed we need to issue a new fee. + now := time.Now() + expire := ticket.FeeExpiration + VSPFee := ticket.VSPFee + if now.After(time.Unix(ticket.FeeExpiration, 0)) { + expire = now.Add(cfg.FeeAddressExpiration).Unix() + VSPFee = cfg.VSPFee - // Validate Signature - sanity check signature is in base64 encoding. - signature := feeAddressRequest.Signature - if _, err = base64.StdEncoding.DecodeString(signature); err != nil { - log.Warnf("Invalid signature from %s: %v", c.ClientIP(), err) - sendErrorResponse("invalid signature", http.StatusBadRequest, c) - return - } - - // Check for existing response - ticket, err := db.GetTicketByHash(ticketHashStr) - if err != nil && !errors.Is(err, database.ErrNoTicketFound) { - log.Errorf("GetTicketByHash error: %v", err) - sendErrorResponse("database error", http.StatusInternalServerError, c) - return - } - if err == nil { - // Ticket already exists - if signature == ticket.CommitmentSignature { - now := time.Now() - expire := ticket.FeeExpiration - VSPFee := ticket.VSPFee - if ticket.FeeExpired() { - expire = now.Add(cfg.FeeAddressExpiration).Unix() - VSPFee = cfg.VSPFee - - err = db.UpdateExpireAndFee(ticketHashStr, expire, VSPFee) - if err != nil { - log.Errorf("UpdateExpireAndFee error: %v", err) - sendErrorResponse("database error", http.StatusInternalServerError, c) - return - } + err := db.UpdateExpireAndFee(ticket.Hash, expire, VSPFee) + if err != nil { + log.Errorf("UpdateExpireAndFee error: %v", err) + sendErrorResponse("database error", http.StatusInternalServerError, c) + return } - sendJSONResponse(feeAddressResponse{ - Timestamp: now.Unix(), - Request: feeAddressRequest, - FeeAddress: ticket.FeeAddress, - Fee: VSPFee, - Expiration: expire, - }, c) - - return } - log.Warnf("Invalid signature from %s", c.ClientIP()) - sendErrorResponse("invalid signature", http.StatusBadRequest, c) + sendJSONResponse(feeAddressResponse{ + Timestamp: now.Unix(), + Request: feeAddressRequest, + FeeAddress: ticket.FeeAddress, + Fee: VSPFee, + Expiration: expire, + }, c) + return } - fWalletConn, err := feeWalletConnect() - if err != nil { - log.Errorf("Fee wallet connection error: %v", err) - sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) - return - } - ctx := c.Request.Context() - fWalletClient, err := rpc.FeeWalletClient(ctx, fWalletConn) - if err != nil { - log.Errorf("Fee wallet client error: %v", err) - sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) - return - } + // Beyond this point we are processing a new ticket which the VSP has not + // seen before. - resp, err := fWalletClient.GetRawTransaction(txHash.String()) + ticketHash := feeAddressRequest.TicketHash + + // Ensure ticket exists and is mined. + resp, err := fWalletClient.GetRawTransaction(ticketHash) if err != nil { - log.Warnf("Could not retrieve tx %s for %s: %v", txHash, c.ClientIP(), err) + log.Warnf("Could not retrieve tx %s for %s: %v", ticketHash, c.ClientIP(), err) sendErrorResponse("unknown transaction", http.StatusBadRequest, c) return } @@ -137,23 +105,6 @@ func feeAddress(c *gin.Context) { return } - // Get commitment address - addr, err := stake.AddrFromSStxPkScrCommitment(msgTx.TxOut[1].PkScript, cfg.NetParams) - if err != nil { - log.Errorf("Failed to get commitment address: %v", err) - sendErrorResponse("failed to get commitment address", http.StatusInternalServerError, c) - return - } - - // verify message - message := fmt.Sprintf("vsp v3 getfeeaddress %s", msgTx.TxHash()) - err = dcrutil.VerifyMessage(addr.Address(), signature, message, cfg.NetParams) - if err != nil { - log.Warnf("Invalid signature from %s: %v", c.ClientIP(), err) - sendErrorResponse("invalid signature", http.StatusBadRequest, c) - return - } - // get blockheight and sdiff which is required by // txrules.StakePoolTicketFee, and store them in the database // for processing by payfee @@ -176,14 +127,13 @@ func feeAddress(c *gin.Context) { expire := now.Add(cfg.FeeAddressExpiration).Unix() dbTicket := database.Ticket{ - Hash: txHash.String(), - CommitmentSignature: signature, - CommitmentAddress: addr.Address(), - FeeAddress: newAddress, - SDiff: blockHeader.SBits, - BlockHeight: int64(blockHeader.Height), - VSPFee: cfg.VSPFee, - FeeExpiration: expire, + Hash: ticketHash, + CommitmentAddress: commitmentAddress, + FeeAddress: newAddress, + SDiff: blockHeader.SBits, + BlockHeight: int64(blockHeader.Height), + VSPFee: cfg.VSPFee, + FeeExpiration: expire, // VotingKey and VoteChoices: set during payfee } diff --git a/webapi/helpers.go b/webapi/helpers.go index c29da2c..f223534 100644 --- a/webapi/helpers.go +++ b/webapi/helpers.go @@ -1,9 +1,12 @@ package webapi import ( + "errors" "fmt" "github.com/decred/dcrd/chaincfg/v3" + "github.com/decred/dcrd/dcrutil/v3" + "github.com/gin-gonic/gin" ) func currentVoteVersion(params *chaincfg.Params) uint32 { @@ -41,3 +44,17 @@ agendaLoop: return nil } + +func validateSignature(reqBytes []byte, commitmentAddress string, c *gin.Context) error { + // Ensure a signature is provided. + signature := c.GetHeader("VSP-Client-Signature") + if signature == "" { + return errors.New("no VSP-Client-Signature header") + } + + err := dcrutil.VerifyMessage(commitmentAddress, signature, string(reqBytes), cfg.NetParams) + if err != nil { + return err + } + return nil +} diff --git a/webapi/middleware.go b/webapi/middleware.go new file mode 100644 index 0000000..0323fdf --- /dev/null +++ b/webapi/middleware.go @@ -0,0 +1,124 @@ +package webapi + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "github.com/jholdstock/dcrvsp/rpc" +) + +type ticketHashRequest struct { + TicketHash string `json:"tickethash" binding:"required"` +} + +// withFeeWalletClient middleware adds a fee wallet client to the request +// context for downstream handlers to make use of. +func withFeeWalletClient() gin.HandlerFunc { + return func(c *gin.Context) { + fWalletConn, err := feeWalletConnect() + if err != nil { + log.Errorf("Fee wallet connection error: %v", err) + sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) + return + } + fWalletClient, err := rpc.FeeWalletClient(c, fWalletConn) + if err != nil { + log.Errorf("Fee wallet client error: %v", err) + sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) + return + } + + c.Set("FeeWalletClient", fWalletClient) + } +} + +// withVotingWalletClient middleware adds a voting wallet client to the request +// context for downstream handlers to make use of. +func withVotingWalletClient() gin.HandlerFunc { + return func(c *gin.Context) { + vWalletConn, err := votingWalletConnect() + if err != nil { + log.Errorf("Voting wallet connection error: %v", err) + sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) + return + } + vWalletClient, err := rpc.VotingWalletClient(c, vWalletConn) + if err != nil { + log.Errorf("Voting wallet client error: %v", err) + sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) + return + } + + c.Set("VotingWalletClient", vWalletClient) + } +} + +// vspAuth middleware reads the request body and extracts the ticket hash. The +// commitment address for the ticket is retrieved from the database if it is +// known, or it is retrieved from the chain if not. +// The middleware errors out if the VSP-Client-Signature header of the request +// does not contain the request body signed with the commitment address. +// Ticket information is added to the request context for downstream handlers to +// use. +func vspAuth() gin.HandlerFunc { + return func(c *gin.Context) { + // Read request bytes. + reqBytes, err := c.GetRawData() + if err != nil { + log.Warnf("Error reading request from %s: %v", c.ClientIP(), err) + sendErrorResponse(err.Error(), http.StatusBadRequest, c) + return + } + + // Add raw request to context for downstream handlers to use. + c.Set("RawRequest", reqBytes) + + // Parse request and ensure there is a ticket hash included. + var request ticketHashRequest + if err := binding.JSON.BindBody(reqBytes, &request); err != nil { + log.Warnf("Bad request from %s: %v", c.ClientIP(), err) + sendErrorResponse(err.Error(), http.StatusBadRequest, c) + return + } + hash := request.TicketHash + + // Check if this ticket already appears in the database. + ticket, ticketFound, err := db.GetTicketByHash(hash) + if err != nil { + log.Errorf("GetTicketByHash error: %v", err) + sendErrorResponse("database error", http.StatusInternalServerError, c) + return + } + + // If the ticket was found in the database we already know its commitment + // address. Otherwise we need to get it from the chain. + var commitmentAddress string + if ticketFound { + commitmentAddress = ticket.CommitmentAddress + } else { + fWalletClient := c.MustGet("FeeWalletClient").(*rpc.FeeWalletRPC) + commitmentAddress, err = fWalletClient.GetTicketCommitmentAddress(hash, cfg.NetParams) + if err != nil { + log.Errorf("GetTicketCommitmentAddress error: %v", err) + sendErrorResponse("database error", http.StatusInternalServerError, c) + return + } + } + + // Validate request signature to ensure ticket ownership. + err = validateSignature(reqBytes, commitmentAddress, c) + if err != nil { + log.Warnf("Bad signature from %s: %v", c.ClientIP(), err) + sendErrorResponse("bad signature", http.StatusBadRequest, c) + return + } + + // Add ticket information to context so downstream handlers don't need + // to access the db for it. + c.Set("Ticket", ticket) + c.Set("KnownTicket", ticketFound) + c.Set("CommitmentAddress", commitmentAddress) + } + +} diff --git a/webapi/payfee.go b/webapi/payfee.go index fc7c389..68645ca 100644 --- a/webapi/payfee.go +++ b/webapi/payfee.go @@ -12,32 +12,43 @@ import ( "github.com/decred/dcrd/txscript/v3" "github.com/decred/dcrd/wire" "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "github.com/jholdstock/dcrvsp/database" "github.com/jholdstock/dcrvsp/rpc" ) // payFee is the handler for "POST /payfee". func payFee(c *gin.Context) { - var payFeeRequest PayFeeRequest - if err := c.ShouldBindJSON(&payFeeRequest); err != nil { - log.Warnf("Bad payfee request from %s: %v", c.ClientIP(), err) - sendErrorResponse(err.Error(), http.StatusBadRequest, c) - return - } - ticket, err := db.GetTicketByHash(payFeeRequest.TicketHash) - if err != nil { + // Get values which have been added to context by middleware. + rawRequest := c.MustGet("RawRequest").([]byte) + ticket := c.MustGet("Ticket").(database.Ticket) + knownTicket := c.MustGet("KnownTicket").(bool) + fWalletClient := c.MustGet("FeeWalletClient").(*rpc.FeeWalletRPC) + vWalletClient := c.MustGet("VotingWalletClient").(*rpc.VotingWalletRPC) + + if !knownTicket { log.Warnf("Invalid ticket from %s", c.ClientIP()) sendErrorResponse("invalid ticket", http.StatusBadRequest, c) return } - // Fee transaction has already been broadcast for this ticket. + var payFeeRequest PayFeeRequest + if err := binding.JSON.BindBody(rawRequest, &payFeeRequest); err != nil { + log.Warnf("Bad payfee request from %s: %v", c.ClientIP(), err) + sendErrorResponse(err.Error(), http.StatusBadRequest, c) + return + } + + // Respond early if fee transaction has already been broadcast for this + // ticket. if ticket.FeeTxHash != "" { sendJSONResponse(payFeeResponse{ Timestamp: time.Now().Unix(), TxHash: ticket.FeeTxHash, Request: payFeeRequest, }, c) + return } // Validate VotingKey. @@ -129,21 +140,6 @@ findAddress: sDiff := dcrutil.Amount(ticket.SDiff) - fWalletConn, err := feeWalletConnect() - if err != nil { - log.Errorf("Fee wallet connection error: %v", err) - sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) - return - } - ctx := c.Request.Context() - - fWalletClient, err := rpc.FeeWalletClient(ctx, fWalletConn) - if err != nil { - log.Errorf("Fee wallet client error: %v", err) - sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) - return - } - relayFee, err := fWalletClient.GetWalletFee() if err != nil { log.Errorf("GetWalletFee failed: %v", err) @@ -187,19 +183,6 @@ findAddress: return } - vWalletConn, err := votingWalletConnect() - if err != nil { - log.Errorf("Voting wallet connection error: %v", err) - sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) - return - } - vWalletClient, err := rpc.VotingWalletClient(ctx, vWalletConn) - if err != nil { - log.Errorf("Voting wallet client error: %v", err) - sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) - return - } - err = vWalletClient.AddTransaction(rawTicket.BlockHash, rawTicket.Hex) if err != nil { log.Errorf("AddTransaction failed: %v", err) diff --git a/webapi/setvotechoices.go b/webapi/setvotechoices.go index c45ed33..404eca3 100644 --- a/webapi/setvotechoices.go +++ b/webapi/setvotechoices.go @@ -1,85 +1,48 @@ package webapi import ( - "encoding/base64" - "fmt" "net/http" "time" - "github.com/decred/dcrd/chaincfg/chainhash" - "github.com/decred/dcrd/dcrutil/v3" "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "github.com/jholdstock/dcrvsp/database" "github.com/jholdstock/dcrvsp/rpc" ) // setVoteChoices is the handler for "POST /setvotechoices". func setVoteChoices(c *gin.Context) { + + // Get values which have been added to context by middleware. + rawRequest := c.MustGet("RawRequest").([]byte) + ticket := c.MustGet("Ticket").(database.Ticket) + knownTicket := c.MustGet("KnownTicket").(bool) + vWalletClient := c.MustGet("VotingWalletClient").(*rpc.VotingWalletRPC) + + if !knownTicket { + log.Warnf("Invalid ticket from %s", c.ClientIP()) + sendErrorResponse("invalid ticket", http.StatusBadRequest, c) + return + } + var setVoteChoicesRequest SetVoteChoicesRequest - if err := c.ShouldBindJSON(&setVoteChoicesRequest); err != nil { + if err := binding.JSON.BindBody(rawRequest, &setVoteChoicesRequest); err != nil { log.Warnf("Bad setvotechoices request from %s: %v", c.ClientIP(), err) sendErrorResponse(err.Error(), http.StatusBadRequest, c) return } - // Validate TicketHash. - ticketHashStr := setVoteChoicesRequest.TicketHash - txHash, err := chainhash.NewHashFromStr(ticketHashStr) - if err != nil { - log.Warnf("Invalid ticket hash from %s", c.ClientIP()) - sendErrorResponse("invalid ticket hash", http.StatusBadRequest, c) - return - } - - // Validate Signature - sanity check signature is in base64 encoding. - signature := setVoteChoicesRequest.Signature - if _, err = base64.StdEncoding.DecodeString(signature); err != nil { - log.Warnf("Invalid signature from %s: %v", c.ClientIP(), err) - sendErrorResponse("invalid signature", http.StatusBadRequest, c) - return - } - voteChoices := setVoteChoicesRequest.VoteChoices - err = isValidVoteChoices(cfg.NetParams, currentVoteVersion(cfg.NetParams), voteChoices) + err := isValidVoteChoices(cfg.NetParams, currentVoteVersion(cfg.NetParams), voteChoices) if err != nil { log.Warnf("Invalid votechoices from %s: %v", c.ClientIP(), err) sendErrorResponse(err.Error(), http.StatusBadRequest, c) return } - ticket, err := db.GetTicketByHash(txHash.String()) - if err != nil { - log.Warnf("Invalid ticket from %s", c.ClientIP()) - sendErrorResponse("invalid ticket", http.StatusBadRequest, c) - return - } - - // verify message - message := fmt.Sprintf("vsp v3 setvotechoices %d %s %v", setVoteChoicesRequest.Timestamp, txHash, voteChoices) - err = dcrutil.VerifyMessage(ticket.CommitmentAddress, signature, message, cfg.NetParams) - if err != nil { - log.Warnf("Failed to verify message from %s: %v", c.ClientIP(), err) - sendErrorResponse("message did not pass verification", http.StatusBadRequest, c) - return - } - - vWalletConn, err := votingWalletConnect() - if err != nil { - log.Errorf("Voting wallet connection error: %v", err) - sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) - return - } - - ctx := c.Request.Context() - vWalletClient, err := rpc.VotingWalletClient(ctx, vWalletConn) - if err != nil { - log.Errorf("Voting wallet client error: %v", err) - sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) - return - } - // Update VoteChoices in the database before updating the wallets. DB is // source of truth and is less likely to error. - err = db.UpdateVoteChoices(txHash.String(), voteChoices) + err = db.UpdateVoteChoices(ticket.Hash, voteChoices) if err != nil { log.Errorf("UpdateVoteChoices error: %v", err) sendErrorResponse("database error", http.StatusInternalServerError, c) diff --git a/webapi/ticketstatus.go b/webapi/ticketstatus.go index 07fcf74..7bd049d 100644 --- a/webapi/ticketstatus.go +++ b/webapi/ticketstatus.go @@ -1,62 +1,39 @@ package webapi import ( - "encoding/base64" - "fmt" "net/http" "time" - "github.com/decred/dcrd/chaincfg/chainhash" - "github.com/decred/dcrd/dcrutil/v3" "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "github.com/jholdstock/dcrvsp/database" ) // ticketStatus is the handler for "GET /ticketstatus". func ticketStatus(c *gin.Context) { - var ticketStatusRequest TicketStatusRequest - if err := c.ShouldBindJSON(&ticketStatusRequest); err != nil { - log.Warnf("Bad ticketstatus request from %s: %v", c.ClientIP(), err) - sendErrorResponse(err.Error(), http.StatusBadRequest, c) - return - } - // Validate TicketHash. - ticketHashStr := ticketStatusRequest.TicketHash - _, err := chainhash.NewHashFromStr(ticketHashStr) - if err != nil { - log.Warnf("Invalid ticket hash from %s", c.ClientIP()) - sendErrorResponse("invalid ticket hash", http.StatusBadRequest, c) - return - } + // Get values which have been added to context by middleware. + rawRequest := c.MustGet("RawRequest").([]byte) + ticket := c.MustGet("Ticket").(database.Ticket) + knownTicket := c.MustGet("KnownTicket").(bool) - // Validate Signature - sanity check signature is in base64 encoding. - signature := ticketStatusRequest.Signature - if _, err = base64.StdEncoding.DecodeString(signature); err != nil { - log.Warnf("Invalid signature from %s: %v", c.ClientIP(), err) - sendErrorResponse("invalid signature", http.StatusBadRequest, c) - return - } - - ticket, err := db.GetTicketByHash(ticketHashStr) - if err != nil { + if !knownTicket { log.Warnf("Invalid ticket from %s", c.ClientIP()) sendErrorResponse("invalid ticket", http.StatusBadRequest, c) return } - // verify message - message := fmt.Sprintf("vsp v3 ticketstatus %d %s", ticketStatusRequest.Timestamp, ticketHashStr) - err = dcrutil.VerifyMessage(ticket.CommitmentAddress, signature, message, cfg.NetParams) - if err != nil { - log.Warnf("Invalid signature from %s: %v", c.ClientIP(), err) - sendErrorResponse("invalid signature", http.StatusBadRequest, c) + var ticketStatusRequest TicketStatusRequest + if err := binding.JSON.BindBody(rawRequest, &ticketStatusRequest); err != nil { + log.Warnf("Bad ticketstatus request from %s: %v", c.ClientIP(), err) + sendErrorResponse(err.Error(), http.StatusBadRequest, c) return } sendJSONResponse(ticketStatusResponse{ Timestamp: time.Now().Unix(), Request: ticketStatusRequest, - Status: "active", // TODO - active, pending, expired (missed, revoked?) + Status: "active", VoteChoices: ticket.VoteChoices, }, c) } diff --git a/webapi/types.go b/webapi/types.go index 5c1028e..d7d026b 100644 --- a/webapi/types.go +++ b/webapi/types.go @@ -13,7 +13,6 @@ type feeResponse struct { type FeeAddressRequest struct { Timestamp int64 `json:"timestamp" binding:"required"` TicketHash string `json:"tickethash" binding:"required"` - Signature string `json:"signature" binding:"required"` } type feeAddressResponse struct { @@ -41,7 +40,6 @@ type payFeeResponse struct { type SetVoteChoicesRequest struct { Timestamp int64 `json:"timestamp" binding:"required"` TicketHash string `json:"tickethash" binding:"required"` - Signature string `json:"commitmentsignature" binding:"required"` VoteChoices map[string]string `json:"votechoices" binding:"required"` } @@ -54,7 +52,6 @@ type setVoteChoicesResponse struct { type TicketStatusRequest struct { Timestamp int64 `json:"timestamp" binding:"required"` TicketHash string `json:"tickethash" binding:"required"` - Signature string `json:"signature" binding:"required"` } type ticketStatusResponse struct { diff --git a/webapi/webapi.go b/webapi/webapi.go index 46830cc..3f9ab68 100644 --- a/webapi/webapi.go +++ b/webapi/webapi.go @@ -145,17 +145,25 @@ func router(debugMode bool) *gin.Engine { // Serve static web resources router.Static("/public", "webapi/public/") + // These routes have no extra middleware. They can be accessed by anybody. router.GET("/", homepage) + router.GET("/api/fee", fee) + router.GET("/api/pubkey", pubKey) - api := router.Group("/api") - { - api.GET("/fee", fee) - api.POST("/feeaddress", feeAddress) - api.GET("/pubkey", pubKey) - api.POST("/payfee", payFee) - api.POST("/setvotechoices", setVoteChoices) - api.GET("/ticketstatus", ticketStatus) - } + // These API routes access the fee wallet and they need authentication. + feeOnly := router.Group("/api").Use( + withFeeWalletClient(), vspAuth(), + ) + feeOnly.POST("/feeaddress", feeAddress) + feeOnly.GET("/ticketstatus", ticketStatus) + + // These API routes access the fee wallet and the voting wallets, and they + // need authentication. + both := router.Group("/api").Use( + withFeeWalletClient(), withVotingWalletClient(), vspAuth(), + ) + both.POST("/payfee", payFee) + both.POST("/setvotechoices", setVoteChoices) return router } @@ -188,7 +196,7 @@ func sendJSONResponse(resp interface{}, c *gin.Context) { } sig := ed25519.Sign(cfg.SignKey, dec) - c.Writer.Header().Set("VSP-Signature", hex.EncodeToString(sig)) + c.Writer.Header().Set("VSP-Server-Signature", hex.EncodeToString(sig)) c.AbortWithStatusJSON(http.StatusOK, resp) }