~edwargix/tallyard

0d0324f0afabae01445b99c7662435cd32058667 — David Florness 2 years ago 5c11b7e
Add hash commitments to messages to ensure integrity

This ensures that the content of events in an election are consistent between
senders and receivers.  This protects against malicious homeserver admins who
attempt to secretly edit events before they reach voters.

This also prevents exploitation of a bug in synapse where users can edit events
and thereby change their content while keeping the IDs the same; see
https://github.com/matrix-org/synapse/issues/10310 for details.
4 files changed, 200 insertions(+), 27 deletions(-)

M election/msg.go
M election/utils.go
M election/voter.go
M go.sum
M election/msg.go => election/msg.go +84 -13
@@ 66,22 66,25 @@ type CreateElectionContent struct {
type JoinElectionContent struct {
	Version string `json:"version"`

	CreateID id.EventID `json:"create_id"`
	Input    string     `json:"input"`
	PubKey   string     `json:"pub_key"`
	SeedPart string     `json:"seed_part"`
	Commitment string     `json:"commitment"`
	CreateID   id.EventID `json:"create_id"`
	Input      string     `json:"input"`
	PubKey     string     `json:"pub_key"`
	SeedPart   string     `json:"seed_part"`
}

type StartElectionContent struct {
	Version string `json:"version"`

	CreateID id.EventID   `json:"create_id"`
	JoinIDs  []id.EventID `json:"join_ids"`
	Commitment string       `json:"commitment"`
	CreateID   id.EventID   `json:"create_id"`
	JoinIDs    []id.EventID `json:"join_ids"`
}

type KeysMessageContent struct {
	Version string `json:"version"`

	Commitment        string        `json:"commitment"`
	EvalProvingKeyURI id.ContentURI `json:"eval_proving_key_uri"`
	JoinID            id.EventID    `json:"join_id"`
	StartID           id.EventID    `json:"start_id"`


@@ 91,9 94,10 @@ type KeysMessageContent struct {
type EvalsMessageContent struct {
	Version string `json:"version"`

	Evals   []Eval       `json:"evals"`
	JoinID  id.EventID   `json:"join_id"`
	KeysIDs []id.EventID `json:"keys_ids"`
	Commitment string       `json:"commitment"`
	Evals      []Eval       `json:"evals"`
	JoinID     id.EventID   `json:"join_id"`
	KeysIDs    []id.EventID `json:"keys_ids"`
}

type Eval struct {


@@ 108,10 112,11 @@ type Eval struct {
type SumMessageContent struct {
	Version string `json:"version"`

	EvalsIDs []id.EventID `json:"evals_ids"`
	JoinID   id.EventID   `json:"join_id"`
	Sum      string       `json:"sum"`
	Proofs   []string     `json:"proofs"`
	Commitment string       `json:"commitment"`
	EvalsIDs   []id.EventID `json:"evals_ids"`
	JoinID     id.EventID   `json:"join_id"`
	Sum        string       `json:"sum"`
	Proofs     []string     `json:"proofs"`
}

func init() {


@@ 281,6 286,17 @@ func (elections *ElectionsMap) onJoinElectionMessage(evt *event.Event) (success 
		return
	}

	// ensure commitment is consistent
	commitmentHash, err := CalculateCommitment(createEvt.Content)
	if err != nil {
		warnf("we couldn't calculate the join event's commitment: %s", err)
		return
	}
	if content.Commitment != commitmentHash {
		warnf("the commitment (%s) does not match the hash of the create event (%s)", content.Commitment, commitmentHash)
		return
	}

	// SeedPart
	if content.SeedPart == "" {
		warnf("the seed part is empty")


@@ 355,6 371,9 @@ func (elections *ElectionsMap) onStartElectionMessage(evt *event.Event) (success
		return
	}

	contents := make([]event.Content, 0, 1+len(content.JoinIDs))
	contents = append(contents, createEvt.Content)

	for _, joinID := range content.JoinIDs {
		joinEvt := elections.EventStore.GetJoinEvent(evt.RoomID, joinID)
		if joinEvt == nil {


@@ 366,6 385,18 @@ func (elections *ElectionsMap) onStartElectionMessage(evt *event.Event) (success
				joinID, content.CreateID)
			return
		}
		contents = append(contents, joinEvt.Content)
	}

	// ensure commitment is consistent
	commitmentHash, err := CalculateCommitment(contents...)
	if err != nil {
		warnf("we couldn't calculate the start event's commitment: %s", err)
		return
	}
	if content.Commitment != commitmentHash {
		warnf("the commitment (%s) does not match the hash of the prerequisite events (%s)", content.Commitment, commitmentHash)
		return
	}

	if createEvt.Sender != evt.Sender {


@@ 436,6 467,17 @@ func (elections *ElectionsMap) onKeysMessage(evt *event.Event, client *mautrix.C
		return
	}

	// ensure commitment is consistent
	commitmentHash, err := CalculateCommitment(joinEvt.Content, startEvt.Content)
	if err != nil {
		warnf("we couldn't calculate the keys event's commitment: %s", err)
		return
	}
	if content.Commitment != commitmentHash {
		warnf("the commitment (%s) does not match the hash of the prerequisite events (%s)", content.Commitment, commitmentHash)
		return
	}

	// ensure keys sender also sent join event
	if evt.Sender != joinEvt.Sender {
		warnf("they did not send the join event; %s did", joinEvt.Sender)


@@ 548,6 590,8 @@ func (elections *ElectionsMap) onEvalsMessage(evt *event.Event) (success bool) {
		return
	}

	contents := []event.Content{joinEvt.Content}

	el := elections.GetElection(joinEvt.CreateID)
	if el == nil {
		// should never happen because we got the start event above


@@ 572,6 616,7 @@ func (elections *ElectionsMap) onEvalsMessage(evt *event.Event) (success bool) {
				keysEvent.JoinID, keysID, (*el.FinalJoinIDs)[i])
			return
		}
		contents = append(contents, keysEvent.Content)
	}

	if len(content.Evals) != len(*el.FinalJoinIDs) {


@@ 580,6 625,17 @@ func (elections *ElectionsMap) onEvalsMessage(evt *event.Event) (success bool) {
		return
	}

	// ensure commitment is consistent
	commitmentHash, err := CalculateCommitment(contents...)
	if err != nil {
		warnf("we couldn't calculate the evals event's commitment: %s", err)
		return
	}
	if content.Commitment != commitmentHash {
		warnf("the commitment (%s) does not match the hash of the prerequisite events (%s)", content.Commitment, commitmentHash)
		return
	}

	el.Lock()
	defer el.Save()
	defer el.Unlock()


@@ 763,6 819,8 @@ func (elections *ElectionsMap) onSumMessage(evt *event.Event) (success bool) {
		return
	}

	contents := make([]event.Content, 0, len(content.EvalsIDs)+1)

	for i, evalsID := range content.EvalsIDs {
		evalsEvt := elections.EventStore.GetEvalsEvent(evt.RoomID, evalsID)
		if evalsEvt == nil {


@@ 774,6 832,19 @@ func (elections *ElectionsMap) onSumMessage(evt *event.Event) (success bool) {
				evalsEvt.JoinID, evalsID, (*el.FinalJoinIDs)[i])
			return
		}
		contents = append(contents, evalsEvt.Content)
	}

	// ensure commitment is consistent
	contents = append(contents, joinEvt.Content)
	commitmentHash, err := CalculateCommitment(contents...)
	if err != nil {
		warnf("we couldn't calculate the sum event's commitment: %s", err)
		return
	}
	if content.Commitment != commitmentHash {
		warnf("the commitment (%s) does not match the hash of the prerequisite events (%s)", content.Commitment, commitmentHash)
		return
	}

	el.Lock()

M election/utils.go => election/utils.go +28 -0
@@ 1,12 1,17 @@
package election

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"time"

	"github.com/kyoh86/xdg"
	log "github.com/sirupsen/logrus"
	"maunium.net/go/mautrix/crypto/canonicaljson"
	"maunium.net/go/mautrix/crypto/olm"
	"maunium.net/go/mautrix/event"
	"maunium.net/go/mautrix/id"
)



@@ 20,3 25,26 @@ func LogUpload(contentURI id.ContentURI) {
	t, _ := time.Now().UTC().MarshalText()
	fmt.Fprintf(file, "%s\t%s\n", string(t), contentURI.String())
}

func CalculateCommitment(prerequisiteEventContent ...event.Content) (string, error) {
	if len(prerequisiteEventContent) == 0 {
		return "", errors.New("no content given")
	}

	olmUtility := olm.NewUtility()
	digest := ""

	for i, content := range prerequisiteEventContent {
		byts, err := json.Marshal(content)
		if err != nil {
			return "", fmt.Errorf("couldn't marshal content[%d]: %s", i, err)
		}
		canonical, err := canonicaljson.CanonicalJSON(byts)
		if err != nil {
			return "", fmt.Errorf("couldn't canonicalize marshalled content[%d]: %s", i, err)
		}
		digest = olmUtility.Sha256(digest + string(canonical))
	}

	return digest, nil
}

M election/voter.go => election/voter.go +84 -14
@@ 98,13 98,19 @@ func (el *Election) JoinElection(client *mautrix.Client, eventStore *EventStore)
		return fmt.Errorf("couldn't read random bytes: %s", err)
	}

	commitmentHash, err := CalculateCommitment(el.CreateEvt.Content)
	if err != nil {
		return fmt.Errorf("couldn't calculate join event's commitment: %s", err)
	}

	resp, err := client.SendMessageEvent(el.RoomID, JoinElectionMessage, JoinElectionContent{
		Version: tallyard.Version,

		CreateID: el.CreateEvt.ID,
		Input:    base64.StdEncoding.EncodeToString(inputBytes[:]),
		PubKey:   base64.StdEncoding.EncodeToString((*pubKey)[:]),
		SeedPart: base64.StdEncoding.EncodeToString(seedBytes[:]),
		Commitment: commitmentHash,
		CreateID:   el.CreateEvt.ID,
		Input:      base64.StdEncoding.EncodeToString(inputBytes[:]),
		PubKey:     base64.StdEncoding.EncodeToString((*pubKey)[:]),
		SeedPart:   base64.StdEncoding.EncodeToString(seedBytes[:]),
	})
	if err != nil {
		return fmt.Errorf("couldn't send join messages: %s", err)


@@ 148,10 154,25 @@ func (el *Election) StartElection(client *mautrix.Client, eventStore *EventStore
		voters = append(voters, voter.JoinEvt.ID)
	}

	var commitmentHash string
	{
		contents := make([]event.Content, 0, 1+len(el.Joins))
		contents = append(contents, el.CreateEvt.Content)
		for _, voter := range voters {
			contents = append(contents, el.Joins[voter].JoinEvt.Content)
		}
		var err error
		commitmentHash, err = CalculateCommitment(contents...)
		if err != nil {
			return fmt.Errorf("couldn't calculate start event's commitment: %s", err)
		}
	}

	resp, err := client.SendMessageEvent(el.RoomID, StartElectionMessage, StartElectionContent{
		Version:  tallyard.Version,
		CreateID: el.CreateEvt.ID,
		JoinIDs:  voters,
		Version:    tallyard.Version,
		Commitment: commitmentHash,
		CreateID:   el.CreateEvt.ID,
		JoinIDs:    voters,
	})
	if err != nil {
		return err


@@ 227,9 248,20 @@ func (el *Election) SendProvingKeys(client *mautrix.Client, eventStore *EventSto
		sumProvingKeyURI = uploadResp.ContentURI
	}

	var commitmentHash string
	{
		startEvt := eventStore.GetStartEvent(el.RoomID, *el.StartID)
		var err error
		commitmentHash, err = CalculateCommitment(el.LocalVoter.JoinEvt.Content, startEvt.Content)
		if err != nil {
			return fmt.Errorf("couldn't calculate keys event's commitment: %s", err)
		}
	}

	resp, err := client.SendMessageEvent(el.RoomID, KeysMessage, KeysMessageContent{
		Version:        tallyard.Version,

		Commitment:        commitmentHash,
		EvalProvingKeyURI: evalProvingKeyURI,
		JoinID:            el.LocalVoter.JoinEvt.ID,
		StartID:           *el.StartID,


@@ 313,12 345,31 @@ func (el *Election) SendEvals(client *mautrix.Client, eventStore *EventStore) er
		keysIDs[i] = *voter.KeysID
	}

	var commitmentHash string
	{
		contents := make([]event.Content, 0, 1+len(keysIDs))
		contents = append(contents, el.LocalVoter.JoinEvt.Content)
		for _, keysID := range keysIDs {
			keysEvent := eventStore.GetKeysEvent(el.RoomID, keysID)
			if keysEvent == nil {
				return fmt.Errorf("couldn't get keys event %s for evals commitment calculation", keysID)
			}
			contents = append(contents, keysEvent.Content)
		}
		var err error
		commitmentHash, err = CalculateCommitment(contents...)
		if err != nil {
			return fmt.Errorf("couldn't calculate evals event's commitment: %s", err)
		}
	}

	resp, err := client.SendMessageEvent(el.RoomID, EvalsMessage, EvalsMessageContent{
		Version: tallyard.Version,

		Evals:   evals,
		JoinID:  el.LocalVoter.JoinEvt.ID,
		KeysIDs: keysIDs,
		Commitment: commitmentHash,
		Evals:      evals,
		JoinID:     el.LocalVoter.JoinEvt.ID,
		KeysIDs:    keysIDs,
	})
	el.RUnlock()
	if err != nil {


@@ 399,14 450,33 @@ func (el *Election) SendSum(client *mautrix.Client, eventStore *EventStore) erro
		}
	}

	var commitmentHash string
	{
		contents := make([]event.Content, 0, len(evalsIDs)+1)
		for _, evalsID := range evalsIDs {
			evalsEvent := eventStore.GetEvalsEvent(el.RoomID, evalsID)
			if evalsEvent == nil {
				return fmt.Errorf("couldn't get evals event %s for sum commitment calculation", evalsID)
			}
			contents = append(contents, evalsEvent.Content)
		}
		contents = append(contents, el.LocalVoter.JoinEvt.Content)
		var err error
		commitmentHash, err = CalculateCommitment(contents...)
		if err != nil {
			return fmt.Errorf("couldn't calculate sum event's commitment: %s", err)
		}
	}

	sumBytes := el.LocalVoter.Sum.Bytes()
	resp, err := client.SendMessageEvent(el.RoomID, SumMessage, SumMessageContent{
		Version:  tallyard.Version,

		EvalsIDs: evalsIDs,
		JoinID:   el.LocalVoter.JoinEvt.ID,
		Sum:      base64.StdEncoding.EncodeToString(sumBytes[:]),
		Proofs:   proofs,
		Commitment: commitmentHash,
		EvalsIDs:   evalsIDs,
		JoinID:     el.LocalVoter.JoinEvt.ID,
		Sum:        base64.StdEncoding.EncodeToString(sumBytes[:]),
		Proofs:     proofs,
	})
	el.RUnlock()
	if err != nil {

M go.sum => go.sum +4 -0
@@ 106,9 106,13 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tidwall/gjson v1.10.2 h1:APbLGOM0rrEkd8WBw9C24nllro4ajFuJu0Sc9hRz8Bo=
github.com/tidwall/gjson v1.10.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.3 h1:5+deguEhHSEjmuICXZ21uSSsXotWMA0orU783+Z7Cp8=
github.com/tidwall/sjson v1.2.3/go.mod h1:5WdjKx3AQMvCJ4RG6/2UYT7dLrGvJUV1x4jdTAyGvZs=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=