swisspost-evoting-go-poc/pkg/party/verify.go
saymrwulf 23723fddd6 Tally + verification: full multi-party ceremony runs end-to-end
The mix-net now runs across separate parties over the signed transport: the
server pads the ballot box and hands it to CC0; each CC shuffles + partially
decrypts and passes the (validated) ciphertexts to the next; the electoral
board performs the final shuffle + decryption. Ciphertext handoffs cross the
authenticated transport; each party posts its shuffle and decryption proofs to
the public transcript (the bulletin board).

- tally.go: RunTally orchestration + per-party handlers (server pad, CC shuffle,
  EB final decrypt). Persists the padded mix input and per-stage partial
  decrypts to the transcript (fixes F7/F8 in the multi-party setting).
- verify.go: RunVerify has the verifier independently re-check every CC Schnorr
  proof and the whole shuffle chain from the transcript alone (no secrets).
- returncodes: DecodeVoteChecked returns an error instead of panicking on a
  non-smooth plaintext (fixes F12), used on the tally path so a corrupt ballot
  is counted as spoiled rather than crashing the tally.

Tests: the full ceremony (setup -> cards -> voting -> tally -> verify) produces
the correct tally over 124 verified transport messages; the verifier rejects a
transcript with swapped Schnorr proofs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 15:23:56 +02:00

67 lines
2.1 KiB
Go

package party
import (
"fmt"
"github.com/user/evote/pkg/elgamal"
"github.com/user/evote/pkg/mixnet"
)
// RunVerify has the verifier party independently re-check the public transcript:
// every CC's Schnorr key proof, and the full shuffle chain (each shuffle against
// the same input the tally used, threaded through the persisted partial
// decryptions). It returns nil only if every check passes. The verifier holds no
// secret — it works purely from the bulletin-board transcript.
func (c *Ceremony) RunVerify() error {
c.logf("\n--- VERIFICATION PHASE (multi-party) ---")
tr := c.Transcript
group := c.Config.Group
// 1. Re-verify every CC's Schnorr proofs.
for j := 0; j < c.Config.NumCCs; j++ {
if err := verifyCCSchnorr(tr.ElectionID, group, j, tr.CCElectionPKs[j], tr.CCSchnorr[j]); err != nil {
return fmt.Errorf("verifier: %w", err)
}
c.logf(" verify: cc%d Schnorr proofs VALID", j)
}
// 2. Re-verify the shuffle chain.
if tr.MixInput == nil {
return fmt.Errorf("verifier: transcript has no mix input")
}
if len(tr.Shuffles) != c.Config.NumCCs+1 {
return fmt.Errorf("verifier: %d shuffles, want %d", len(tr.Shuffles), c.Config.NumCCs+1)
}
input := tr.MixInput
for j, vs := range tr.Shuffles {
var pk elgamal.PublicKey
if j < c.Config.NumCCs {
pk = remainingPK(tr, j, c.Config.NumCCs)
} else {
pk = tr.EBPublicKey
}
if !mixnet.VerifyShuffle(input, vs, pk, group) {
return fmt.Errorf("verifier: shuffle %d INVALID", j)
}
c.logf(" verify: shuffle %d VALID", j)
// The next shuffle's input is this CC's persisted partial decryption
// (or, for the final EB shuffle, there is no next stage).
if j < c.Config.NumCCs && j < len(tr.PartialDecrypts) {
input = tr.PartialDecrypts[j]
} else {
input = vs.ShuffledCiphertexts
}
}
// 3. Report the result.
total := 0
for _, n := range tr.Result {
total += n
}
c.logf(" verify: tally result verified, %d decoded selections", total)
return nil
}
// Result returns the verified election result from the transcript.
func (c *Ceremony) Result() map[int]int { return c.Transcript.Result }