Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Additional votes in the structures #2506

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ env:
jobs:
go:
name: Upload release binaries
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Checkout code
uses: actions/checkout@main
Expand Down
33 changes: 33 additions & 0 deletions evoting/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,36 @@ See the [README.md](evoting-admin/README.md) in that directory.
- Paper: **Helios: Web-based Open-Audit Voting**; *Ben Adida*, 2008
- Paper: **Decentralizing authorities into scalable strongest-link cothorities**: *Ford et. al.*, 2015
- Paper: **Secure distributed key generation for discrete-log based cryptosystems**; *Gennaro et. al.*, 1999

# Extension to more than 9 candidates

## Description

The pre-June 2023 version (v3.4.9) of the evoting backend only allowed for up to 9 choices of candidates.
This is due to the fact that the encryption is done with data in an ed25519 point, which can store up to
30 bytes.
Each candidate takes 3 bytes, so there would be 10 candidates possible, but probably due to a off-by-one error
decision, it was deemed better to only allow for 9 candidates.

With the current change, a number of `Additional*` fields are added to the structures that hold the encrypted
and decrypted votes.
This has been done as a golang-slice, which is represented as a `repeated` in protobuf.
The advantage is that if it is missing, it will simply result in a 0-length slice.
So all the previous data should be fully compatible with the new system.

The shuffling / mixing is now done with the `kyber/shuffle/sequences.go`, which has one big disadvantage:
**There is no verification of the shuffle being correct**!

Anyway, due to many other security bugs, I think this is the least of concerns for now...

## Bugs encountered

The following bugs are security bugs.
If the D-voting implementation is a rewrite of this evoting
service, then you should definitely check that now it's done
correctly!

- `hashMap` doesn't hash all it should: https://github.com/dedis/cothority/issues/2508
- The authentication of the user is very bogus: https://github.com/dedis/cothority/issues/2507
- `shuffle` and `decrypt` requests sent by the leader are not trustworthy: https://github.com/dedis/cothority/issues/2509
- Blocks created by `shuffle` and `decrypt` are signed with a useless signature https://github.com/dedis/cothority/issues/2510
23 changes: 17 additions & 6 deletions evoting/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package evoting

import (
"go.dedis.ch/cothority/v3/skipchain"
"go.dedis.ch/onet/v3"

"go.dedis.ch/cothority/v3"
Expand All @@ -16,16 +17,17 @@ type Client struct {
*onet.Client
// If LookupURL is set, use it for SCIPER lookups (for tests).
LookupURL string
Roster *onet.Roster
}

// NewClient instantiates a new evoting.Client.
func NewClient() *Client {
return &Client{Client: onet.NewClient(cothority.Suite, ServiceName)}
func NewClient(roster *onet.Roster) *Client {
return &Client{Client: onet.NewClient(cothority.Suite, ServiceName), Roster: roster}
}

// Ping a random server which increments the nonce.
func (c *Client) Ping(roster *onet.Roster, nonce uint32) (*Ping, error) {
dest := roster.RandomServerIdentity()
func (c *Client) Ping(nonce uint32) (*Ping, error) {
dest := c.Roster.RandomServerIdentity()
reply := &Ping{}
if err := c.SendProtobuf(dest, &Ping{Nonce: nonce}, reply); err != nil {
return nil, err
Expand All @@ -34,8 +36,17 @@ func (c *Client) Ping(roster *onet.Roster, nonce uint32) (*Ping, error) {
}

// LookupSciper returns information about a sciper number.
func (c *Client) LookupSciper(roster *onet.Roster, sciper string) (reply *LookupSciperReply, err error) {
func (c *Client) LookupSciper(sciper string) (reply *LookupSciperReply, err error) {
reply = &LookupSciperReply{}
err = c.SendProtobuf(roster.RandomServerIdentity(), &LookupSciper{Sciper: sciper, LookupURL: c.LookupURL}, reply)
err = c.SendProtobuf(c.Roster.RandomServerIdentity(), &LookupSciper{Sciper: sciper, LookupURL: c.LookupURL}, reply)
return
}

// Reconstruct returns the reconstructed votes.
// If the election is not yet finished, it will return an error
func (c *Client) Reconstruct(id skipchain.SkipBlockID) (rr ReconstructReply, err error) {
err = c.SendProtobuf(c.Roster.List[0], &Reconstruct{
ID: id,
}, &rr)
return
}
4 changes: 2 additions & 2 deletions evoting/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func TestPing(t *testing.T) {

_, roster, _ := local.GenTree(3, true)

c := evoting.NewClient()
r, _ := c.Ping(roster, 0)
c := evoting.NewClient(roster)
r, _ := c.Ping(0)
assert.Equal(t, uint32(1), r.Nonce)
}
156 changes: 127 additions & 29 deletions evoting/evoting-admin/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/json"
"flag"
"fmt"
"go.dedis.ch/cothority/v3/evoting/lib"
"os"
"strconv"
"strings"
Expand All @@ -23,56 +24,104 @@ import (
)

var (
argRoster = flag.String("roster", "", "path to roster toml file")
argAdmins = flag.String("admins", "", "list of admin users")
argPin = flag.String("pin", "", "service pin")
argKey = flag.String("key", "", "public key of authentication server")
argID = flag.String("id", "", "ID of the master chain to modify (optional)")
argUser = flag.Int("user", 0, "The SCIPER of an existing admin of this chain")
argSig = flag.String("sig", "", "A signature proving that you can login to Tequila with the given SCIPER.")
argShow = flag.Bool("show", false, "Show the current Master config")
argDumpVoters = flag.Bool("dumpvoters", false, "Dump a list of voters for election skipchain specified with -id (ballot de-duplication has already been taken into account, order is preserved)")
argDumpElection = flag.Bool("dumpelection", false, "Dump the current election config for the election specified with -id.")
argJSON = flag.Bool("json", false, "Dump in json mode.")
argLoad = flag.String("load", "", "Load the specified json file to modify the election specified with -id.")
argRoster = flag.String("roster", "", "path to roster toml file")
argAdmins = flag.String("admins", "", "list of admin users")
argPin = flag.String("pin", "", "service pin")
argKey = flag.String("key", "", "public key of authentication server")
argPrivate = flag.String("private", "", "private key of authentication server for voting")
argID = flag.String("id", "", "ID of the master chain to modify (optional)")
argUser = flag.Int("user", 0, "The SCIPER of an existing admin of this chain")
argSig = flag.String("sig", "", "A signature proving that you can login to Tequila with the given SCIPER.")
argShow = flag.Bool("show", false, "Show the current Master config")
argDumpVoters = flag.Bool("dumpvoters", false, "Dump a list of voters for election skipchain specified with -id (ballot de-duplication has already been taken into account, order is preserved)")
argDumpElection = flag.Bool("dumpelection", false, "Dump the current election config for the election specified with -id.")
argJSON = flag.Bool("json", false, "Dump in json mode.")
argLoad = flag.String("load", "", "Load the specified json file to modify the election specified with -id.")
argVoteCandidates = flag.String("voteCandidates", "", "Coma delimited list of SCIPERs to vote for")
argDebug = flag.Int("debug", 0, "Debugging level")
)

func main() {
flag.Parse()

if *argDebug > 0 {
log.SetDebugVisible(*argDebug)
}

if *argRoster == "" {
log.Fatal("Roster argument (-roster) is required for create, update, or show.")
}
roster, err := parseRoster(*argRoster)
if err != nil {
log.Fatal("cannot parse roster: ", err)
}
client := onet.NewClient(cothority.Suite, evoting.ServiceName)
cl := evoting.NewClient(roster)

var pub kyber.Point
var priv kyber.Scalar
if *argPrivate != "" {
// If we get the private key, calculate the corresponding public key
b, err := hex.DecodeString(*argPrivate)
log.ErrFatal(err, "while parsing private key")

log.Lvl1("Setting the private key")
priv = cothority.Suite.Scalar()
err = priv.UnmarshalBinary(b)
log.ErrFatal(err, "while unmarshalling private key")
pub = cothority.Suite.Point().Mul(priv, nil)
} else if *argKey != "" {
// If we only get the public key
pub, err = parseKey(*argKey)
if err != nil {
log.Fatal("cannot parse key: ", err)
}
} else {
// If we get no key, create them again
kp := key.NewKeyPair(cothority.Suite)
priv = kp.Private
pub = kp.Public
}

if *argShow {
if *argID == "" {
log.Fatal("Please give ID of master chain")
}
id, err := hex.DecodeString(*argID)
if err != nil {
log.Fatal("id decode", err)
}
request := &evoting.GetElections{Master: id}
if priv != nil && *argUser > 0 {
request.User = uint32(*argUser)
request.Signature = lib.GenerateSignature(priv, id, request.User)
} else {
fmt.Println("You can give '-private PRIVATE_KEY -user ADMINID' for a list of available elections to this user")
}
reply := &evoting.GetElectionsReply{}
client := onet.NewClient(cothority.Suite, evoting.ServiceName)
if err = client.SendProtobuf(roster.List[0], request, reply); err != nil {
log.Fatal("get elections request: ", err)
}
m := reply.Master
fmt.Printf(" Admins: %v\n", m.Admins)
fmt.Printf(" Roster: %v\n", m.Roster.List)
fmt.Printf(" Key: %v\n", m.Key)
for _, election := range reply.Elections {
fmt.Printf("\nElection: %s\n", election.Name)
fmt.Printf(" ID: %x\n", election.ID)
}
return
}

if *argDumpVoters {
if *argID == "" {
log.Fatal("Please give ID of master chain")
}
id, err := hex.DecodeString(*argID)
if err != nil {
log.Fatal("id decode", err)
}
reply := &evoting.GetBoxReply{}
client := onet.NewClient(cothority.Suite, evoting.ServiceName)
if err = client.SendProtobuf(roster.List[0], &evoting.GetBox{ID: id}, reply); err != nil {
log.Fatal("get box request: ", err)
}
Expand All @@ -84,12 +133,14 @@ func main() {
}

if *argDumpElection {
if *argID == "" {
log.Fatal("Please give an ID")
}
id, err := hex.DecodeString(*argID)
if err != nil {
log.Fatal("id decode", err)
}
reply := &evoting.GetBoxReply{}
client := onet.NewClient(cothority.Suite, evoting.ServiceName)
if err = client.SendProtobuf(roster.List[0], &evoting.GetBox{ID: id}, reply); err != nil {
log.Fatal("get box request: ", err)
}
Expand Down Expand Up @@ -123,19 +174,34 @@ func main() {
out.WriteTo(os.Stdout)
} else {
fmt.Println(reply.Election)
if reply.Election.Stage == lib.Decrypted {
reconstructReply, err := cl.Reconstruct(id)
log.ErrFatal(err, "While getting reconstructed votes")
for i, p := range reconstructReply.Points {
d, _ := p.Data()
fmt.Printf("Vote %d with data %x", i, d)
for _, p := range reconstructReply.AdditionalPoints[i].AdditionalPoints {
d, _ := p.Data()
fmt.Printf(",%x", d)
}
fmt.Println()
}
}
}
return
}

if *argLoad != "" {
if *argID == "" {
log.Fatal("Please give ID of master chain")
}
id, err := hex.DecodeString(*argID)
if err != nil {
log.Fatal("id decode", err)
}

// Look up the election
reply := &evoting.GetBoxReply{}
client := onet.NewClient(cothority.Suite, evoting.ServiceName)
if err = client.SendProtobuf(roster.List[0], &evoting.GetBox{ID: id}, reply); err != nil {
log.Fatal("get box request:", err)
}
Expand Down Expand Up @@ -194,6 +260,50 @@ func main() {
return
}

if len(*argVoteCandidates) > 0 {
voteUser := uint32(*argUser)
if len(*argID) != 64 {
log.Fatal("Need an ID of the election chain in hex form of 32 bytes")
}
voteID, err := hex.DecodeString(*argID)
log.ErrFatal(err, "Wrong ID for election chain")

log.Lvl1("Getting election information from the chain")
reply := &evoting.GetBoxReply{}
if err = client.SendProtobuf(roster.List[0], &evoting.GetBox{ID: voteID}, reply); err != nil {
log.Fatal("get box request:", err)
}

if len(*argVoteCandidates) < 6 {
log.Fatal("Need at least one candidate to vote for")
}
voteCandidatesStrings := strings.Split(*argVoteCandidates, ",")
voteCandidates := make([]uint32, len(voteCandidatesStrings))
for i, c := range voteCandidatesStrings {
cand, err := strconv.Atoi(c)
log.ErrFatal(err, "Wrong SCIPER ID for candidate")
voteCandidates[i] = uint32(cand)
}

if priv == nil {
log.Fatal("Need the private key to vote")
}

fmt.Printf("Votes are: %+v\n", voteCandidates)

ballot := lib.CreateBallot(reply.Election.MaxChoices, reply.Election.Key, voteUser, voteCandidates)
request := &evoting.Cast{
ID: voteID,
User: voteUser,
Ballot: &ballot,
Signature: lib.GenerateSignature(priv, reply.Election.Master, voteUser),
}
replyCast := &evoting.CastReply{}
log.ErrFatal(client.SendProtobuf(roster.List[0], request, replyCast))
log.Info("Successfully cast this vote")
return
}

if *argAdmins == "" {
log.Fatal("Admin list (-admins) must have at least one id.")
}
Expand All @@ -207,18 +317,6 @@ func main() {
log.Fatal("pin must be set for create and update operations.")
}

var pub kyber.Point
if *argKey != "" {
pub, err = parseKey(*argKey)
if err != nil {
log.Fatal("cannot parse key: ", err)
}
} else {
kp := key.NewKeyPair(cothority.Suite)
log.Infof("Auth-server private key: %v", kp.Private)
pub = kp.Public
}

request := &evoting.Link{Pin: *argPin, Roster: roster, Key: pub, Admins: admins}
if *argID != "" {
id, err := hex.DecodeString(*argID)
Expand All @@ -241,11 +339,11 @@ func main() {
}
reply := &evoting.LinkReply{}

client := onet.NewClient(cothority.Suite, evoting.ServiceName)
if err = client.SendProtobuf(roster.List[0], request, reply); err != nil {
log.Fatal("link request: ", err)
}

log.Infof("Auth-server private key: %v", priv)
log.Infof("Auth-server public key: %v", pub)
log.Infof("Master ID: %x", reply.ID)
}
Expand Down
Loading