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.
This commit is contained in:
Jamie Holdstock 2020-05-26 14:14:38 +01:00 committed by GitHub
parent 1ff55f4b30
commit ac488464c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 325 additions and 271 deletions

View File

@ -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 - 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 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. 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 - 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 imports this xpub to create a new wallet account. This account is used to
derive addresses for fee payments. derive addresses for fee payments.

View File

@ -17,7 +17,6 @@ func exampleTicket() Ticket {
return Ticket{ return Ticket{
Hash: "Hash", Hash: "Hash",
CommitmentAddress: "Address", CommitmentAddress: "Address",
CommitmentSignature: "CommitmentSignature",
FeeAddress: "FeeAddress", FeeAddress: "FeeAddress",
SDiff: 1, SDiff: 1,
BlockHeight: 2, BlockHeight: 2,
@ -95,15 +94,17 @@ func testGetTicketByHash(t *testing.T) {
} }
// Retrieve ticket from database. // Retrieve ticket from database.
retrieved, err := db.GetTicketByHash(ticket.Hash) retrieved, found, err := db.GetTicketByHash(ticket.Hash)
if err != nil { if err != nil {
t.Fatalf("error retrieving ticket by ticket hash: %v", err) t.Fatalf("error retrieving ticket by ticket hash: %v", err)
} }
if !found {
t.Fatal("expected found==true")
}
// Check ticket fields match expected. // Check ticket fields match expected.
if retrieved.Hash != ticket.Hash || if retrieved.Hash != ticket.Hash ||
retrieved.CommitmentAddress != ticket.CommitmentAddress || retrieved.CommitmentAddress != ticket.CommitmentAddress ||
retrieved.CommitmentSignature != ticket.CommitmentSignature ||
retrieved.FeeAddress != ticket.FeeAddress || retrieved.FeeAddress != ticket.FeeAddress ||
retrieved.SDiff != ticket.SDiff || retrieved.SDiff != ticket.SDiff ||
retrieved.BlockHeight != ticket.BlockHeight || retrieved.BlockHeight != ticket.BlockHeight ||
@ -115,10 +116,13 @@ func testGetTicketByHash(t *testing.T) {
t.Fatal("retrieved ticket value didnt match expected") t.Fatal("retrieved ticket value didnt match expected")
} }
// Error if non-existent ticket requested. // Check found==false when requesting a non-existent ticket.
_, err = db.GetTicketByHash("Not a real ticket hash") _, found, err = db.GetTicketByHash("Not a real ticket hash")
if err == nil { if err != nil {
t.Fatal("expected an error while retrieving a non-existent ticket") 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. // Retrieve ticket from database.
retrieved, err := db.GetTicketByHash(ticket.Hash) retrieved, _, err := db.GetTicketByHash(ticket.Hash)
if err != nil { if err != nil {
t.Fatalf("error retrieving ticket by ticket hash: %v", err) t.Fatalf("error retrieving ticket by ticket hash: %v", err)
} }
@ -171,7 +175,7 @@ func testUpdateExpireAndFee(t *testing.T) {
} }
// Get updated ticket // Get updated ticket
retrieved, err := db.GetTicketByHash(ticket.Hash) retrieved, _, err := db.GetTicketByHash(ticket.Hash)
if err != nil { if err != nil {
t.Fatalf("error retrieving updated ticket: %v", err) t.Fatalf("error retrieving updated ticket: %v", err)
} }
@ -199,7 +203,7 @@ func testUpdateVoteChoices(t *testing.T) {
} }
// Get updated ticket // Get updated ticket
retrieved, err := db.GetTicketByHash(ticket.Hash) retrieved, _, err := db.GetTicketByHash(ticket.Hash)
if err != nil { if err != nil {
t.Fatalf("error retrieving updated ticket: %v", err) t.Fatalf("error retrieving updated ticket: %v", err)
} }

View File

@ -11,7 +11,6 @@ import (
type Ticket struct { type Ticket struct {
Hash string `json:"hash"` Hash string `json:"hash"`
CommitmentSignature string `json:"commitmentsignature"`
CommitmentAddress string `json:"commitmentaddress"` CommitmentAddress string `json:"commitmentaddress"`
FeeAddress string `json:"feeaddress"` FeeAddress string `json:"feeaddress"`
SDiff float64 `json:"sdiff"` SDiff float64 `json:"sdiff"`
@ -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 ticket Ticket
var found bool
err := vdb.db.View(func(tx *bolt.Tx) error { err := vdb.db.View(func(tx *bolt.Tx) error {
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK) ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)
ticketBytes := ticketBkt.Get([]byte(ticketHash)) ticketBytes := ticketBkt.Get([]byte(ticketHash))
if ticketBytes == nil { if ticketBytes == nil {
return ErrNoTicketFound return nil
} }
err := json.Unmarshal(ticketBytes, &ticket) 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) return fmt.Errorf("could not unmarshal ticket: %v", err)
} }
found = true
return nil return nil
}) })
return ticket, err return ticket, found, err
} }
func (vdb *VspDatabase) UpdateVoteChoices(ticketHash string, voteChoices map[string]string) error { func (vdb *VspDatabase) UpdateVoteChoices(ticketHash string, voteChoices map[string]string) error {

View File

@ -2,11 +2,15 @@ package rpc
import ( import (
"context" "context"
"encoding/hex"
"fmt" "fmt"
wallettypes "decred.org/dcrwallet/rpc/jsonrpc/types" 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" "github.com/decred/dcrd/dcrutil/v3"
dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v2" dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v2"
"github.com/decred/dcrd/wire"
) )
const ( const (
@ -129,3 +133,25 @@ func (c *FeeWalletRPC) GetWalletFee() (dcrutil.Amount, error) {
return amount, nil 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
}

View File

@ -1,66 +1,46 @@
package webapi package webapi
import ( import (
"encoding/base64"
"encoding/hex" "encoding/hex"
"errors"
"fmt"
"net/http" "net/http"
"time" "time"
"github.com/decred/dcrd/blockchain/stake/v3" "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/decred/dcrd/wire"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/jholdstock/dcrvsp/database" "github.com/jholdstock/dcrvsp/database"
"github.com/jholdstock/dcrvsp/rpc" "github.com/jholdstock/dcrvsp/rpc"
) )
// feeAddress is the handler for "POST /feeaddress". // feeAddress is the handler for "POST /feeaddress".
func feeAddress(c *gin.Context) { 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 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) log.Warnf("Bad feeaddress request from %s: %v", c.ClientIP(), err)
sendErrorResponse(err.Error(), http.StatusBadRequest, c) sendErrorResponse(err.Error(), http.StatusBadRequest, c)
return return
} }
// Validate TicketHash. // VSP already knows this ticket and has already issued it a fee address.
ticketHashStr := feeAddressRequest.TicketHash if knownTicket {
txHash, err := chainhash.NewHashFromStr(ticketHashStr) // If the expiry period has passed we need to issue a new fee.
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 := 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() now := time.Now()
expire := ticket.FeeExpiration expire := ticket.FeeExpiration
VSPFee := ticket.VSPFee VSPFee := ticket.VSPFee
if ticket.FeeExpired() { if now.After(time.Unix(ticket.FeeExpiration, 0)) {
expire = now.Add(cfg.FeeAddressExpiration).Unix() expire = now.Add(cfg.FeeAddressExpiration).Unix()
VSPFee = cfg.VSPFee VSPFee = cfg.VSPFee
err = db.UpdateExpireAndFee(ticketHashStr, expire, VSPFee) err := db.UpdateExpireAndFee(ticket.Hash, expire, VSPFee)
if err != nil { if err != nil {
log.Errorf("UpdateExpireAndFee error: %v", err) log.Errorf("UpdateExpireAndFee error: %v", err)
sendErrorResponse("database error", http.StatusInternalServerError, c) sendErrorResponse("database error", http.StatusInternalServerError, c)
@ -77,28 +57,16 @@ func feeAddress(c *gin.Context) {
return return
} }
log.Warnf("Invalid signature from %s", c.ClientIP())
sendErrorResponse("invalid signature", http.StatusBadRequest, c)
return
}
fWalletConn, err := feeWalletConnect() // Beyond this point we are processing a new ticket which the VSP has not
if err != nil { // seen before.
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
}
resp, err := fWalletClient.GetRawTransaction(txHash.String()) ticketHash := feeAddressRequest.TicketHash
// Ensure ticket exists and is mined.
resp, err := fWalletClient.GetRawTransaction(ticketHash)
if err != nil { 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) sendErrorResponse("unknown transaction", http.StatusBadRequest, c)
return return
} }
@ -137,23 +105,6 @@ func feeAddress(c *gin.Context) {
return 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 // get blockheight and sdiff which is required by
// txrules.StakePoolTicketFee, and store them in the database // txrules.StakePoolTicketFee, and store them in the database
// for processing by payfee // for processing by payfee
@ -176,9 +127,8 @@ func feeAddress(c *gin.Context) {
expire := now.Add(cfg.FeeAddressExpiration).Unix() expire := now.Add(cfg.FeeAddressExpiration).Unix()
dbTicket := database.Ticket{ dbTicket := database.Ticket{
Hash: txHash.String(), Hash: ticketHash,
CommitmentSignature: signature, CommitmentAddress: commitmentAddress,
CommitmentAddress: addr.Address(),
FeeAddress: newAddress, FeeAddress: newAddress,
SDiff: blockHeader.SBits, SDiff: blockHeader.SBits,
BlockHeight: int64(blockHeader.Height), BlockHeight: int64(blockHeader.Height),

View File

@ -1,9 +1,12 @@
package webapi package webapi
import ( import (
"errors"
"fmt" "fmt"
"github.com/decred/dcrd/chaincfg/v3" "github.com/decred/dcrd/chaincfg/v3"
"github.com/decred/dcrd/dcrutil/v3"
"github.com/gin-gonic/gin"
) )
func currentVoteVersion(params *chaincfg.Params) uint32 { func currentVoteVersion(params *chaincfg.Params) uint32 {
@ -41,3 +44,17 @@ agendaLoop:
return nil 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
}

124
webapi/middleware.go Normal file
View File

@ -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)
}
}

View File

@ -12,32 +12,43 @@ import (
"github.com/decred/dcrd/txscript/v3" "github.com/decred/dcrd/txscript/v3"
"github.com/decred/dcrd/wire" "github.com/decred/dcrd/wire"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/jholdstock/dcrvsp/database"
"github.com/jholdstock/dcrvsp/rpc" "github.com/jholdstock/dcrvsp/rpc"
) )
// payFee is the handler for "POST /payfee". // payFee is the handler for "POST /payfee".
func payFee(c *gin.Context) { 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) // Get values which have been added to context by middleware.
if err != nil { 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()) log.Warnf("Invalid ticket from %s", c.ClientIP())
sendErrorResponse("invalid ticket", http.StatusBadRequest, c) sendErrorResponse("invalid ticket", http.StatusBadRequest, c)
return 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 != "" { if ticket.FeeTxHash != "" {
sendJSONResponse(payFeeResponse{ sendJSONResponse(payFeeResponse{
Timestamp: time.Now().Unix(), Timestamp: time.Now().Unix(),
TxHash: ticket.FeeTxHash, TxHash: ticket.FeeTxHash,
Request: payFeeRequest, Request: payFeeRequest,
}, c) }, c)
return
} }
// Validate VotingKey. // Validate VotingKey.
@ -129,21 +140,6 @@ findAddress:
sDiff := dcrutil.Amount(ticket.SDiff) 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() relayFee, err := fWalletClient.GetWalletFee()
if err != nil { if err != nil {
log.Errorf("GetWalletFee failed: %v", err) log.Errorf("GetWalletFee failed: %v", err)
@ -187,19 +183,6 @@ findAddress:
return 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) err = vWalletClient.AddTransaction(rawTicket.BlockHash, rawTicket.Hex)
if err != nil { if err != nil {
log.Errorf("AddTransaction failed: %v", err) log.Errorf("AddTransaction failed: %v", err)

View File

@ -1,85 +1,48 @@
package webapi package webapi
import ( import (
"encoding/base64"
"fmt"
"net/http" "net/http"
"time" "time"
"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrutil/v3"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/jholdstock/dcrvsp/database"
"github.com/jholdstock/dcrvsp/rpc" "github.com/jholdstock/dcrvsp/rpc"
) )
// setVoteChoices is the handler for "POST /setvotechoices". // setVoteChoices is the handler for "POST /setvotechoices".
func setVoteChoices(c *gin.Context) { 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 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) log.Warnf("Bad setvotechoices request from %s: %v", c.ClientIP(), err)
sendErrorResponse(err.Error(), http.StatusBadRequest, c) sendErrorResponse(err.Error(), http.StatusBadRequest, c)
return 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 voteChoices := setVoteChoicesRequest.VoteChoices
err = isValidVoteChoices(cfg.NetParams, currentVoteVersion(cfg.NetParams), voteChoices) err := isValidVoteChoices(cfg.NetParams, currentVoteVersion(cfg.NetParams), voteChoices)
if err != nil { if err != nil {
log.Warnf("Invalid votechoices from %s: %v", c.ClientIP(), err) log.Warnf("Invalid votechoices from %s: %v", c.ClientIP(), err)
sendErrorResponse(err.Error(), http.StatusBadRequest, c) sendErrorResponse(err.Error(), http.StatusBadRequest, c)
return 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 // Update VoteChoices in the database before updating the wallets. DB is
// source of truth and is less likely to error. // source of truth and is less likely to error.
err = db.UpdateVoteChoices(txHash.String(), voteChoices) err = db.UpdateVoteChoices(ticket.Hash, voteChoices)
if err != nil { if err != nil {
log.Errorf("UpdateVoteChoices error: %v", err) log.Errorf("UpdateVoteChoices error: %v", err)
sendErrorResponse("database error", http.StatusInternalServerError, c) sendErrorResponse("database error", http.StatusInternalServerError, c)

View File

@ -1,62 +1,39 @@
package webapi package webapi
import ( import (
"encoding/base64"
"fmt"
"net/http" "net/http"
"time" "time"
"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrutil/v3"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/jholdstock/dcrvsp/database"
) )
// ticketStatus is the handler for "GET /ticketstatus". // ticketStatus is the handler for "GET /ticketstatus".
func ticketStatus(c *gin.Context) { 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. // Get values which have been added to context by middleware.
ticketHashStr := ticketStatusRequest.TicketHash rawRequest := c.MustGet("RawRequest").([]byte)
_, err := chainhash.NewHashFromStr(ticketHashStr) ticket := c.MustGet("Ticket").(database.Ticket)
if err != nil { knownTicket := c.MustGet("KnownTicket").(bool)
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. if !knownTicket {
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 {
log.Warnf("Invalid ticket from %s", c.ClientIP()) log.Warnf("Invalid ticket from %s", c.ClientIP())
sendErrorResponse("invalid ticket", http.StatusBadRequest, c) sendErrorResponse("invalid ticket", http.StatusBadRequest, c)
return return
} }
// verify message var ticketStatusRequest TicketStatusRequest
message := fmt.Sprintf("vsp v3 ticketstatus %d %s", ticketStatusRequest.Timestamp, ticketHashStr) if err := binding.JSON.BindBody(rawRequest, &ticketStatusRequest); err != nil {
err = dcrutil.VerifyMessage(ticket.CommitmentAddress, signature, message, cfg.NetParams) log.Warnf("Bad ticketstatus request from %s: %v", c.ClientIP(), err)
if err != nil { sendErrorResponse(err.Error(), http.StatusBadRequest, c)
log.Warnf("Invalid signature from %s: %v", c.ClientIP(), err)
sendErrorResponse("invalid signature", http.StatusBadRequest, c)
return return
} }
sendJSONResponse(ticketStatusResponse{ sendJSONResponse(ticketStatusResponse{
Timestamp: time.Now().Unix(), Timestamp: time.Now().Unix(),
Request: ticketStatusRequest, Request: ticketStatusRequest,
Status: "active", // TODO - active, pending, expired (missed, revoked?) Status: "active",
VoteChoices: ticket.VoteChoices, VoteChoices: ticket.VoteChoices,
}, c) }, c)
} }

View File

@ -13,7 +13,6 @@ type feeResponse struct {
type FeeAddressRequest struct { type FeeAddressRequest struct {
Timestamp int64 `json:"timestamp" binding:"required"` Timestamp int64 `json:"timestamp" binding:"required"`
TicketHash string `json:"tickethash" binding:"required"` TicketHash string `json:"tickethash" binding:"required"`
Signature string `json:"signature" binding:"required"`
} }
type feeAddressResponse struct { type feeAddressResponse struct {
@ -41,7 +40,6 @@ type payFeeResponse struct {
type SetVoteChoicesRequest struct { type SetVoteChoicesRequest struct {
Timestamp int64 `json:"timestamp" binding:"required"` Timestamp int64 `json:"timestamp" binding:"required"`
TicketHash string `json:"tickethash" binding:"required"` TicketHash string `json:"tickethash" binding:"required"`
Signature string `json:"commitmentsignature" binding:"required"`
VoteChoices map[string]string `json:"votechoices" binding:"required"` VoteChoices map[string]string `json:"votechoices" binding:"required"`
} }
@ -54,7 +52,6 @@ type setVoteChoicesResponse struct {
type TicketStatusRequest struct { type TicketStatusRequest struct {
Timestamp int64 `json:"timestamp" binding:"required"` Timestamp int64 `json:"timestamp" binding:"required"`
TicketHash string `json:"tickethash" binding:"required"` TicketHash string `json:"tickethash" binding:"required"`
Signature string `json:"signature" binding:"required"`
} }
type ticketStatusResponse struct { type ticketStatusResponse struct {

View File

@ -145,17 +145,25 @@ func router(debugMode bool) *gin.Engine {
// Serve static web resources // Serve static web resources
router.Static("/public", "webapi/public/") router.Static("/public", "webapi/public/")
// These routes have no extra middleware. They can be accessed by anybody.
router.GET("/", homepage) router.GET("/", homepage)
router.GET("/api/fee", fee)
router.GET("/api/pubkey", pubKey)
api := router.Group("/api") // These API routes access the fee wallet and they need authentication.
{ feeOnly := router.Group("/api").Use(
api.GET("/fee", fee) withFeeWalletClient(), vspAuth(),
api.POST("/feeaddress", feeAddress) )
api.GET("/pubkey", pubKey) feeOnly.POST("/feeaddress", feeAddress)
api.POST("/payfee", payFee) feeOnly.GET("/ticketstatus", ticketStatus)
api.POST("/setvotechoices", setVoteChoices)
api.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 return router
} }
@ -188,7 +196,7 @@ func sendJSONResponse(resp interface{}, c *gin.Context) {
} }
sig := ed25519.Sign(cfg.SignKey, dec) 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) c.AbortWithStatusJSON(http.StatusOK, resp)
} }