Skip to content

Commit

Permalink
farcaster: several improvements on the proof verification
Browse files Browse the repository at this point in the history
1. Use the FID to generate the Nullifier, this way a user with
multiple publicKeys cannot cast more than 1 vote.
2. Check processID matches and it is available in the URL field
3. Set the votePackage to use index=0, so use buttonIndex-1

Signed-off-by: p4u <[email protected]>
  • Loading branch information
p4u committed Feb 13, 2024
1 parent 9b7c4ac commit 5cc88b9
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 27 deletions.
33 changes: 23 additions & 10 deletions test/farcaster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"go.vocdoni.io/dvote/types"
"go.vocdoni.io/dvote/util"
"go.vocdoni.io/dvote/vochain/state"
"go.vocdoni.io/dvote/vochain/transaction/proofs/farcasterproof"
"go.vocdoni.io/proto/build/go/models"
"google.golang.org/protobuf/proto"
)
Expand All @@ -31,6 +32,8 @@ func TestAPIFarcasterVote(t *testing.T) {
api.ElectionHandler,
api.WalletHandler,
)
// We need to disable the election ID verification, since the signed farcaster frame messages are hardcoded and contain the processId in the URL.
farcasterproof.DisableElectionIDVerification = true
// Block 1
server.VochainAPP.AdvanceTestBlock()

Expand Down Expand Up @@ -74,10 +77,8 @@ func TestAPIFarcasterVote(t *testing.T) {
qt.Assert(t, json.Unmarshal(resp, censusData), qt.IsNil)
qt.Assert(t, censusData.CensusID, qt.IsNotNil)
root := censusData.CensusID

election := createFarcasterElection(t, c, server.Account, censusData.CensusID, server.VochainAPP.ChainID())

// Block 2
server.VochainAPP.AdvanceTestBlock()
waitUntilHeight(t, c, 2)

Expand All @@ -88,7 +89,7 @@ func TestAPIFarcasterVote(t *testing.T) {
qt.Assert(t, censusData.Weight.String(), qt.Equals, "1")

votePackage := &state.VotePackage{
Votes: []int{frameVote1.buttonIndex},
Votes: []int{frameVote1.buttonIndex - 1},
}
votePackageBytes, err := votePackage.Encode()
qt.Assert(t, err, qt.IsNil)
Expand Down Expand Up @@ -161,7 +162,7 @@ func TestAPIFarcasterVote(t *testing.T) {
qt.Assert(t, censusData.Weight.String(), qt.Equals, "1")

votePackage = &state.VotePackage{
Votes: []int{frameVote2.buttonIndex},
Votes: []int{frameVote2.buttonIndex - 1},
}
votePackageBytes, err = votePackage.Encode()
qt.Assert(t, err, qt.IsNil)
Expand Down Expand Up @@ -256,7 +257,7 @@ func createFarcasterElection(t testing.TB, c *testutil.TestHTTPclient,
Mode: &models.ProcessMode{AutoStart: true, Interruptible: true},
VoteOptions: &models.ProcessVoteOptions{
MaxCount: 1,
MaxValue: 4,
MaxValue: 3,
},
EnvelopeType: &models.EnvelopeType{
EncryptedVotes: false,
Expand Down Expand Up @@ -288,20 +289,32 @@ func createFarcasterElection(t testing.TB, c *testutil.TestHTTPclient,
return election
}

// var farcasterElectionID = "63f57be98f806f959214b4581eb8791e6c80eaf72102d4e93f76100000000003"

type farcasterFrame struct {
signedMessage string
pubkey string
buttonIndex int
fid int
}

var frameVote1 = farcasterFrame{
signedMessage: "0a4a080d109fc20e18ecb4de2e200182013a0a1a68747470733a2f2f63656c6f6e692e766f63646f6e692e6e657410021a1a089fc20e121423f83af6df9c2c960a5ba50af25fe887325bf91812144183c8a56b1d3e1874a4165013cc92e067a362b01801224022709f664c73d7fed004ab7f6bc09c376a17abb221d19096fa739f0422172535151c0cc5382aceed7338356adae00982c9ae69746fca3c9fbebb23e832562a0328013220ec327cd438995a59ce78fddd29631e9b2e41eafc3a6946dd26b4da749f47140d",
signedMessage: "0a8b01080d109fc20e18b4a7f52e200182017b0a5b68747470733a2f2f63656c6f6e692e766f63646f6e692e6e65742f3633663537626539386638303666393539323134623435383165623837393165366338306561663732313032643465393366373631303030303030303030303310011a1a089fc20e1214000000000000000000000000000000000000000112142d9bd29806c7e54cf5f80f98d9adf710a2ebc58518012240379f4f9897901b24544fba46fcb51183f79d79a5041c47f554c5a4e407c020fdbf43ef27490b944e05372850b1dc78dd97c728a88bdbc14f0174ed589c795a0928013220ec327cd438995a59ce78fddd29631e9b2e41eafc3a6946dd26b4da749f47140d",
pubkey: "ec327cd438995a59ce78fddd29631e9b2e41eafc3a6946dd26b4da749f47140d",
buttonIndex: 2,
buttonIndex: 1,
fid: 237855,
}

var frameVote2 = farcasterFrame{
signedMessage: "0a4a080d10eced12188bc0de2e200182013a0a1a68747470733a2f2f63656c6f6e692e766f63646f6e692e6e657410021a1a089fc20e121423f83af6df9c2c960a5ba50af25fe887325bf9181214da970b6c4b9e905c9dd95965b5e5e300e88bc5b5180122409e819761b9292c72d48856369ba84926513ef9a341d4564a40d028dd8651d9c305c163023ac440dc3ca93a2993c12b5c3a7361215b913e8e7e9771b41bd6190c28013220d843cf9636184c99fe6ed7db8b5934c2cd31dd19b63a2409b305898fa560459c",
pubkey: "d843cf9636184c99fe6ed7db8b5934c2cd31dd19b63a2409b305898fa560459c",
buttonIndex: 2,
buttonIndex: 3,
signedMessage: "0a8901080d10e04e18d1aaf52e200182017a0a5b68747470733a2f2f63656c6f6e692e766f63646f6e692e6e65742f3633663537626539386638303666393539323134623435383165623837393165366338306561663732313032643465393366373631303030303030303030303310031a1908e04e12140000000000000000000000000000000000000001121496f560a1f5c90fa24278277321d7be35d18cf0711801224029863962ecff4b7db6dd8736fc1c238ed6ed5a147d3a36e6eac32e06f10d2dcc1df1618d5da6ce21286e0233656ef985a5b1cced2bee5f2cbb4fd1bfc168aa0128013220d6424e655287aa61df38205da19ddab23b0ff9683c6800e0dbc3e8b65d3eb2e3",
pubkey: "d6424e655287aa61df38205da19ddab23b0ff9683c6800e0dbc3e8b65d3eb2e3",
fid: 10080,
}

//var frameVote3 = farcasterFrame{
// buttonIndex: 1,
// fid: 195929,
// signedMessage: "0a8b01080d10d9fa0b18f1a8f52e200182017b0a5b68747470733a2f2f63656c6f6e692e766f63646f6e692e6e65742f3633663537626539386638303666393539323134623435383165623837393165366338306561663732313032643465393366373631303030303030303030303310011a1a08d9fa0b121400000000000000000000000000000000000000011214e597fda6f6252ae1d20011d81072a13b17afa0901801224089b9d6427af2558188669b498890871d733f67d51c2df0d93a3da84727d09f0d87a6b9b83b02adcb5546b93619b0d8395cc84d675c3f68f92e55db526710c10a28013220b44b21f817a968423a3669c13865deafa7389ae0df89c6ad615cfc17b86118f8",
// pubkey: "b44b21f817a968423a3669c13865deafa7389ae0df89c6ad615cfc17b86118f8",
//}
91 changes: 74 additions & 17 deletions vochain/transaction/proofs/farcasterproof/farcasterproof.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package farcasterproof
import (
"bytes"
"crypto/ed25519"
"encoding/binary"
"encoding/hex"
"fmt"
"math/big"
"regexp"

"github.com/ethereum/go-ethereum/common"
"go.vocdoni.io/dvote/crypto/ethereum"
"go.vocdoni.io/dvote/log"
"go.vocdoni.io/proto/build/go/models"
"google.golang.org/protobuf/proto"
Expand All @@ -18,9 +21,21 @@ import (
)

const (
frameHashSize = 20
frameHashSize = 20
pollURLpattern = `([0-9a-fA-F]{64})`
)

var (
// DisableElectionIDVerification is a flag to dissable the election ID verification on the poll URL.
// This should be used only for testing purposes.
DisableElectionIDVerification = false
re *regexp.Regexp
)

func init() {
re = regexp.MustCompile(pollURLpattern)
}

// FarcasterVerifier is a proof verifier for the Farcaster frame protocol.
type FarcasterVerifier struct{}

Expand Down Expand Up @@ -53,8 +68,26 @@ func (*FarcasterVerifier) Verify(process *models.Process, envelope *models.VoteE
return false, nil, fmt.Errorf("vote package contains more than one vote")
}

if uint32(vp.Votes[0]) != frameAction.ButtonIndex {
return false, nil, fmt.Errorf("vote package button index mismatch (got %d, expected %d)", frameAction.ButtonIndex, vp.Votes[0])
if uint32(vp.Votes[0]) != frameAction.ButtonIndex-1 {
return false, nil, fmt.Errorf("vote package button index mismatch (got %d, expected %d)", frameAction.ButtonIndex, vp.Votes[0]+1)
}

// Verify the vote URL matches with the process ID. We enforce the process ID to be present in the poll URL because it
// is the only way to ensure the process ID is correct.
if !DisableElectionIDVerification {
matches := re.FindStringSubmatch(string(frameAction.Url))
// If a match is found, matches[1] contains the processID
if len(matches) > 1 {
votePID, err := hex.DecodeString(matches[1])
if err != nil {
return false, nil, fmt.Errorf("failed to decode process ID: %w", err)
}
if !bytes.Equal(votePID, envelope.ProcessId) {
return false, nil, fmt.Errorf("process ID mismatch (got %x, expected %x)", votePID, envelope.ProcessId)
}
} else {
return false, nil, fmt.Errorf("no process ID found on poll URL")
}
}

// Verify the census arbo proof (is the signer of the frame action allowed to vote?)
Expand All @@ -74,25 +107,36 @@ func (*FarcasterVerifier) Verify(process *models.Process, envelope *models.VoteE
return true, weight, nil
}

// VerifyFrameSignature validates the frame message and returns de deserialized frame action and public key.
func VerifyFrameSignature(messageBody []byte) (*farcasterpb.FrameActionBody, ed25519.PublicKey, error) {
// DecodeMessage decodes the signed message body and returns the frame action body, the message data and the public key.
func DecodeMessage(signedMessageBody []byte) (*farcasterpb.FrameActionBody, *farcasterpb.Message, error) {
msg := farcasterpb.Message{}
if err := proto.Unmarshal(messageBody, &msg); err != nil {
if err := proto.Unmarshal(signedMessageBody, &msg); err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal Message: %w", err)
}
log.Debugf("farcaster signed message: %s", log.FormatProto(&msg))

if msg.Data == nil {
return nil, nil, fmt.Errorf("invalid message data")
}
if msg.Data.Type != farcasterpb.MessageType_MESSAGE_TYPE_FRAME_ACTION {
return nil, nil, fmt.Errorf("invalid message type, got %s", msg.Data.Type.String())
}
if msg.SignatureScheme != farcasterpb.SignatureScheme_SIGNATURE_SCHEME_ED25519 {
return nil, nil, fmt.Errorf("invalid signature scheme")
}
if msg.Data.Type != farcasterpb.MessageType_MESSAGE_TYPE_FRAME_ACTION {
return nil, nil, fmt.Errorf("invalid message type, got %s", msg.Data.Type.String())
actionBody := msg.Data.GetFrameActionBody()
if actionBody == nil {
return nil, nil, fmt.Errorf("invalid action body")
}
pubkey := msg.GetSigner()
return actionBody, &msg, nil
}

// VerifyFrameSignature validates the frame message and returns de deserialized frame action and public key.
func VerifyFrameSignature(messageBody []byte) (*farcasterpb.FrameActionBody, ed25519.PublicKey, error) {
actionBody, msg, err := DecodeMessage(messageBody)
if err != nil {
return nil, nil, fmt.Errorf("failed to decode message body: %w", err)
}

pubkey := msg.GetSigner()
if pubkey == nil {
return nil, nil, fmt.Errorf("signer is nil")
}
Expand All @@ -117,10 +161,6 @@ func VerifyFrameSignature(messageBody []byte) (*farcasterpb.FrameActionBody, ed2
if !ed25519.Verify(pubkey, hashed, msg.GetSignature()) {
return nil, nil, fmt.Errorf("signature verification failed")
}
actionBody := msg.Data.GetFrameActionBody()
if actionBody == nil {
return nil, nil, fmt.Errorf("invalid action body")
}

return actionBody, pubkey, nil
}
Expand All @@ -147,8 +187,25 @@ func InitializeFarcasterFrameVote(voteEnvelope *models.VoteEnvelope, height uint
if frameProof.PublicKey == nil {
return nil, fmt.Errorf("farcaster frame public key not found on transaction")
}
_, msg, err := DecodeMessage(frameProof.SignedFrameMessageBody)
if err != nil {
return nil, fmt.Errorf("failed to decode farcaster frame message: %w", err)
}
if msg.Data == nil {
return nil, fmt.Errorf("farcaster frame cast ID not found on transaction")
}
// Generate the voter ID and assign it to the vote
vote.VoterID = append([]byte{state.VoterIDTypeEd25519}, frameProof.PublicKey...)
vote.Nullifier = state.GenerateNullifier(common.Address(vote.VoterID.Address()), vote.ProcessID)
// Generate the nullifier
vote.Nullifier = GenerateNullifier(msg.Data.Fid, voteEnvelope.ProcessId)
return vote, nil
}

// GenerateNullifier generates a nullifier for a farcaster frame vote.
// As nullifier we use: hash(farcasterID+processID) because the farcasterID is unique per voter while
// the public key is not.
func GenerateNullifier(farcasterID uint64, processID []byte) []byte {
fidBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(fidBytes, farcasterID)
return ethereum.HashRaw(append(fidBytes, processID...))
}
4 changes: 4 additions & 0 deletions vocone/vocone.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"go.vocdoni.io/dvote/vochain/indexer"
"go.vocdoni.io/dvote/vochain/keykeeper"
"go.vocdoni.io/dvote/vochain/state"
"go.vocdoni.io/dvote/vochain/transaction/proofs/farcasterproof"
"go.vocdoni.io/dvote/vochain/vochaininfo"
"go.vocdoni.io/proto/build/go/models"
"google.golang.org/protobuf/proto"
Expand Down Expand Up @@ -120,6 +121,9 @@ func NewVocone(dataDir string, keymanager *ethereum.SignKeys, disableIPFS bool,
}
}

// Disable election ID verification on the farcaster proof for testing purposes
farcasterproof.DisableElectionIDVerification = true

return vc, err
}

Expand Down

0 comments on commit 5cc88b9

Please sign in to comment.