From 0d0324f0afabae01445b99c7662435cd32058667 Mon Sep 17 00:00:00 2001 From: David Florness Date: Sat, 1 Jan 2022 13:21:40 -0600 Subject: [PATCH] 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. --- election/msg.go | 97 +++++++++++++++++++++++++++++++++++++++------- election/utils.go | 28 ++++++++++++++ election/voter.go | 98 ++++++++++++++++++++++++++++++++++++++++------- go.sum | 4 ++ 4 files changed, 200 insertions(+), 27 deletions(-) diff --git a/election/msg.go b/election/msg.go index 50c9671..4412212 100644 --- a/election/msg.go +++ b/election/msg.go @@ -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() diff --git a/election/utils.go b/election/utils.go index fbf78bc..3a9c773 100644 --- a/election/utils.go +++ b/election/utils.go @@ -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 +} diff --git a/election/voter.go b/election/voter.go index 2c9f1e3..9dfa678 100644 --- a/election/voter.go +++ b/election/voter.go @@ -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 { diff --git a/go.sum b/go.sum index 7df0a2e..6ac3928 100644 --- a/go.sum +++ b/go.sum @@ -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= -- 2.38.4