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
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.

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

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/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)

View File

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

View File

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

View File

@ -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 {

View File

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