swisspost-evoting-go-poc/pkg/protocol/tally.go
saymrwulf ec4be74e17 Due-diligence hardening + Rust transport-security layer
Correctness/security review of the whole PoC, with fixes and regression tests.

Cryptographic soundness:
- mixnet: enforce the multi-exponentiation c_{B_m}=commit(0;0) check that was
  stubbed out with an empty if — without it a malicious mixer can prove a
  non-permutation shuffle.
- zkp: derive all four Fiat-Shamir challenges via RecursiveHashToZq instead of
  a biased `hash mod q` (which also capped the challenge space at 256 bits for
  production-sized groups).

Verification honesty:
- protocol: VerifyTally now actually calls zkp.VerifySchnorrProof and returns
  the true aggregate result instead of an unconditional true.
- protocol: persist the padded mix input (event.MixInput) so the verifier checks
  shuffle 0 against the same padding the tally used (fixes false INVALID for N<2).

Other correctness:
- kdf: length-prefix BuildKDFInfo parts so the info encoding is injective.
- math: GqElementFromSquareRoot accepts the valid root q (off-by-one that could
  panic in HashAndSquare); RandomGqElement samples the full canonical range.
- cmd: validate demo --voters/--options instead of panicking on degenerate values.
- protocol: use crypto/rand in the demo driver (drop the last math/rand import).

Transport security (new): pkg/transportsec exposes Ed25519 signatures and X25519
ECDH — implemented in Rust (rust/transportsec: ed25519-dalek, x25519-dalek),
linked into Go via cgo. No RSA. Cross-language conformance test proves the Rust
Ed25519 signatures interoperate with Go's crypto/ed25519. Makefile builds the
Rust static lib before the Go binary.

Tests: added unit/round-trip/tamper coverage for math, hash, elgamal, zkp,
mixnet, kdf, returncodes, protocol (end-to-end), and the Rust FFI bridge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 14:42:34 +02:00

215 lines
6.6 KiB
Go

package protocol
import (
"fmt"
"math/big"
"github.com/user/evote/pkg/elgamal"
"github.com/user/evote/pkg/hash"
emath "github.com/user/evote/pkg/math"
"github.com/user/evote/pkg/mixnet"
"github.com/user/evote/pkg/returncodes"
"github.com/user/evote/pkg/zkp"
)
// Tally performs the complete tally phase.
func Tally(event *ElectionEvent) {
cfg := event.Config
group := cfg.Group
fmt.Println("\n--- TALLY PHASE ---")
// 1. Get mixnet initial ciphertexts
ballotCts := event.BallotBox.GetCiphertexts()
N := ballotCts.Size()
// Ensure at least 2 ciphertexts (add trivial if needed). The padded vector
// is persisted as event.MixInput so the verifier checks shuffle 0 against
// the EXACT same input rather than re-padding with fresh randomness.
if N < 2 {
zqGroup := emath.ZqGroupFromGqGroup(group)
for N < 2 {
r := emath.RandomZqElement(zqGroup)
trivial := elgamal.EncryptOnes(r, event.ElectionPK)
ballotCts = ballotCts.Append(trivial)
N++
}
}
event.MixInput = ballotCts
fmt.Printf(" Mixing %d ciphertexts through %d CCs + EB\n", N, cfg.NumCCs)
// 2. MixDecOnline: Each CC shuffles and partially decrypts
currentCts := ballotCts
event.ShuffleResults = make([]mixnet.VerifiableShuffle, 0)
event.PartiallyDecrypted = make([]*elgamal.CiphertextVector, 0)
event.DecryptionProofs = make([][]zkp.DecryptionProof, 0)
for j := 0; j < cfg.NumCCs; j++ {
cc := event.CCs[j]
fmt.Printf(" CC%d: shuffle + partial decrypt...\n", j)
// Compute remaining public key (CCs j..3 + EB)
remainingPKs := make([]elgamal.PublicKey, 0)
for k := j; k < cfg.NumCCs; k++ {
remainingPKs = append(remainingPKs, event.CCs[k].ElectionKeyPair.PK)
}
remainingPKs = append(remainingPKs, event.EB.PK)
remainingPK := elgamal.CombinePublicKeys(remainingPKs...)
// Shuffle under remaining PK
vs := mixnet.GenVerifiableShuffle(currentCts, remainingPK, group)
event.ShuffleResults = append(event.ShuffleResults, vs)
// Partial decrypt with this CC's key
decrypted := make([]elgamal.Ciphertext, vs.ShuffledCiphertexts.Size())
var decProofs []zkp.DecryptionProof
for i := 0; i < vs.ShuffledCiphertexts.Size(); i++ {
ct := vs.ShuffledCiphertexts.Get(i)
dec := elgamal.PartialDecrypt(ct, cc.ElectionKeyPair.SK)
decrypted[i] = dec
// Generate decryption proof
msg := elgamal.Decrypt(ct, cc.ElectionKeyPair.SK)
proof := zkp.GenDecryptionProof(ct, cc.ElectionKeyPair.SK, cc.ElectionKeyPair.PK, msg, group)
decProofs = append(decProofs, proof)
}
event.DecryptionProofs = append(event.DecryptionProofs, decProofs)
currentCts = elgamal.NewCiphertextVector(decrypted)
event.PartiallyDecrypted = append(event.PartiallyDecrypted, currentCts)
}
// 3. MixDecOffline: EB final shuffle + decrypt
fmt.Println(" EB: final shuffle + decrypt...")
// Final shuffle under EB key
vs := mixnet.GenVerifiableShuffle(currentCts, event.EB.PK, group)
event.ShuffleResults = append(event.ShuffleResults, vs)
// Final decryption with EB key
event.DecryptedVotes = make([]*emath.GqVector, vs.ShuffledCiphertexts.Size())
for i := 0; i < vs.ShuffledCiphertexts.Size(); i++ {
ct := vs.ShuffledCiphertexts.Get(i)
msg := elgamal.Decrypt(ct, event.EB.SK)
event.DecryptedVotes[i] = msg.Elements
}
// 4. Process plaintexts: factorize to decode votes
fmt.Println(" Processing plaintexts...")
processPlaintexts(event)
}
func processPlaintexts(event *ElectionEvent) {
cfg := event.Config
numActualVotes := event.BallotBox.Size()
for i := 0; i < len(event.DecryptedVotes); i++ {
msg := event.DecryptedVotes[i]
// Check if this is a trivial vote (all ones)
if msg.Get(0).IsIdentity() {
continue
}
// Factorize the first element to get selected options
voteProduct := msg.Get(0).Value()
selectedOptions := returncodes.DecodeVote(voteProduct, event.Primes)
for _, opt := range selectedOptions {
event.FinalResult[opt]++
}
}
fmt.Printf("\n--- ELECTION RESULT ---\n")
fmt.Printf(" Total votes cast: %d\n", numActualVotes)
for opt := 0; opt < cfg.NumOptions; opt++ {
count := event.FinalResult[opt]
fmt.Printf(" Option %d: %d votes\n", opt, count)
}
}
// VerifyTally verifies all shuffle proofs and decryption proofs.
func VerifyTally(event *ElectionEvent) bool {
cfg := event.Config
group := cfg.Group
fmt.Println("\n--- VERIFICATION ---")
allValid := true
// 1. Verify Schnorr proofs for CC keys (really invokes the ZK verifier).
for j := 0; j < cfg.NumCCs; j++ {
cc := event.CCs[j]
ccOK := true
for i := 0; i < cfg.NumOptions; i++ {
auxInfo := []hash.Hashable{
hash.HashableBigInt{Value: big.NewInt(int64(i))},
hash.HashableString{Value: cfg.ElectionID},
hash.HashableBigInt{Value: big.NewInt(int64(j))},
}
if !zkp.VerifySchnorrProof(cc.SchnorrProofs[i], cc.ElectionKeyPair.PK.Get(i), group, auxInfo...) {
ccOK = false
}
}
if ccOK {
fmt.Printf(" CC%d: Schnorr proofs OK\n", j)
} else {
fmt.Printf(" CC%d: Schnorr proofs INVALID\n", j)
allValid = false
}
}
// 2. Verify shuffle proofs against the SAME padded input the tally used.
ballotCts := event.MixInput
if ballotCts == nil {
// Tally not run, or legacy event: fall back to the ballot box.
ballotCts = event.BallotBox.GetCiphertexts()
}
for j, vs := range event.ShuffleResults {
var pk elgamal.PublicKey
if j < cfg.NumCCs {
remainingPKs := make([]elgamal.PublicKey, 0)
for k := j; k < cfg.NumCCs; k++ {
remainingPKs = append(remainingPKs, event.CCs[k].ElectionKeyPair.PK)
}
remainingPKs = append(remainingPKs, event.EB.PK)
pk = elgamal.CombinePublicKeys(remainingPKs...)
} else {
pk = event.EB.PK
}
valid := mixnet.VerifyShuffle(ballotCts, vs, pk, group)
if valid {
fmt.Printf(" Shuffle %d: proof VALID\n", j)
} else {
fmt.Printf(" Shuffle %d: proof INVALID\n", j)
allValid = false
}
// Update ciphertexts for next shuffle:
// After each CC shuffle, we use the PARTIALLY DECRYPTED ciphertexts as input to the next shuffle.
// The shuffle proof verifies the shuffle of `ballotCts` → `vs.ShuffledCiphertexts`.
// But the next shuffle's input is the partially decrypted version of `vs.ShuffledCiphertexts`.
if j < cfg.NumCCs && j < len(event.PartiallyDecrypted) {
ballotCts = event.PartiallyDecrypted[j]
} else {
ballotCts = vs.ShuffledCiphertexts
}
}
// 3. Verify vote count consistency
totalVotes := 0
for _, count := range event.FinalResult {
totalVotes += count
}
fmt.Printf(" Total votes decoded: %d (expected: %d)\n", totalVotes, event.BallotBox.Size())
if allValid {
fmt.Println(" Verification complete: ALL CHECKS PASSED.")
} else {
fmt.Println(" Verification complete: FAILURES DETECTED.")
}
return allValid
}