mirror of
https://github.com/saymrwulf/swisspost-evoting-go-poc.git
synced 2026-07-24 19:43:51 +00:00
The return code the voter checks is now genuinely computed by the CCs from the submitted ciphertext, not looked up from the card: - returncode_extract.go: after a ballot is accepted, the server asks each CC to exponentiate E2 by its return-code key (product over CCs = Enc(vote^Σk)), then each CC contributes a partial-decryption factor; the server recovers vote^Σk, which equals the card base prime_sel^Σk, and looks up the short code. - The server returns that code to the voter, who checks it against the card for the chosen option; a mismatch aborts with a clear error. Soundness test: a malicious client that encrypts option A for the tally (E1) but option B in the return-code channel (E2) is REJECTED by the plaintext-equality proof — so the code shown always reflects the tallied vote. This closes the cast-as-intended gap (the old return codes were decorative, finding F16). Card lCC now uses a fixed tau so extraction can recompute it without learning the option up front. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
128 lines
4.4 KiB
Go
128 lines
4.4 KiB
Go
package party
|
|
|
|
import (
|
|
"fmt"
|
|
"math/big"
|
|
|
|
"github.com/user/evote/pkg/elgamal"
|
|
"github.com/user/evote/pkg/returncodes"
|
|
"github.com/user/evote/pkg/transport"
|
|
)
|
|
|
|
// Return-code extraction message types.
|
|
const (
|
|
MsgRCExpReq = "rc-exp-req" // server -> CCj: exponentiate E2 by k_j
|
|
MsgRCExpResp = "rc-exp-resp"
|
|
MsgRCDecReq = "rc-dec-req" // server -> CCj: partial-decrypt factor
|
|
MsgRCDecResp = "rc-dec-resp"
|
|
)
|
|
|
|
// tauFixed is the option-independent tau fed to ComputeLCCValue at both card
|
|
// generation and vote-time extraction (see codes.go for why it is fixed).
|
|
var tauFixed = big.NewInt(1)
|
|
|
|
type rcExpReq struct {
|
|
VcID string `json:"vc_id"`
|
|
E2 wireCiphertext `json:"e2"`
|
|
}
|
|
type rcExpResp struct {
|
|
Exp wireCiphertext `json:"exp"` // E2^{k_j}
|
|
}
|
|
|
|
type rcDecReq struct {
|
|
Gamma string `json:"gamma"` // gamma of the combined exponentiated ciphertext
|
|
}
|
|
type rcDecResp struct {
|
|
Factor string `json:"factor"` // gamma^{sk_j[0]}
|
|
}
|
|
|
|
// extractChoiceReturnCode (server) recovers the voter's return code from E2 by
|
|
// asking each CC to exponentiate E2 by its return-code key and to contribute a
|
|
// partial-decryption factor, then combining and looking the value up in the
|
|
// mapping table. The value recovered is vote^{Σk_j}, which equals the card base
|
|
// prime_sel^{Σk_j} — so the code returned is the code for the ACTUALLY SUBMITTED
|
|
// vote (cast-as-intended). Returns the short code, or "" if no match.
|
|
func (p *VotingServer) extractChoiceReturnCode(vcID string, e2 elgamal.Ciphertext) (string, error) {
|
|
group := p.cer.Config.Group
|
|
|
|
// Phase 1: each CC exponentiates E2 by its return-code key; product over CCs
|
|
// yields Enc(vote^{Σk}, ·, returnCodesPK).
|
|
var combined elgamal.Ciphertext
|
|
for j := 0; j < p.cer.Config.NumCCs; j++ {
|
|
env, err := p.cer.send(p.id, CCName(j), MsgRCExpReq, rcExpReq{VcID: vcID, E2: encodeCiphertext(e2)})
|
|
if err != nil {
|
|
return "", fmt.Errorf("rc-exp cc%d: %w", j, err)
|
|
}
|
|
var resp rcExpResp
|
|
if err := transport.UnmarshalPayload(env.Payload, &resp); err != nil {
|
|
return "", err
|
|
}
|
|
expCt, err := resp.Exp.decode(group)
|
|
if err != nil {
|
|
return "", fmt.Errorf("rc-exp cc%d decode: %w", j, err)
|
|
}
|
|
if j == 0 {
|
|
combined = expCt
|
|
} else {
|
|
combined = combined.Multiply(expCt)
|
|
}
|
|
}
|
|
|
|
// Phase 2: each CC contributes gamma^{sk_j[0]}; product is the decryption
|
|
// mask returnCodesPK^{R}. vote^{Σk} = phi / mask.
|
|
mask := group.Identity()
|
|
for j := 0; j < p.cer.Config.NumCCs; j++ {
|
|
env, err := p.cer.send(p.id, CCName(j), MsgRCDecReq, rcDecReq{Gamma: combined.Gamma.Value().String()})
|
|
if err != nil {
|
|
return "", fmt.Errorf("rc-dec cc%d: %w", j, err)
|
|
}
|
|
var resp rcDecResp
|
|
if err := transport.UnmarshalPayload(env.Payload, &resp); err != nil {
|
|
return "", err
|
|
}
|
|
factor, err := strToGq(resp.Factor, group)
|
|
if err != nil {
|
|
return "", fmt.Errorf("rc-dec cc%d decode: %w", j, err)
|
|
}
|
|
mask = mask.Multiply(factor)
|
|
}
|
|
voteToK := combined.GetPhi(0).Divide(mask)
|
|
|
|
// Look up the recovered value in the mapping table.
|
|
lCC := returncodes.ComputeLCCValue(voteToK, vcID, p.cer.Config.ElectionID, tauFixed)
|
|
code, err := p.st.mappingTable.Lookup(lCC)
|
|
if err != nil {
|
|
return "", nil // no match (e.g. spoiled) — not an error
|
|
}
|
|
return code, nil
|
|
}
|
|
|
|
// handleRCExp (CC) exponentiates E2 by this CC's return-code key k_j.
|
|
func (p *ControlComponent) handleRCExp(env *transport.Envelope) (*transport.Envelope, error) {
|
|
var req rcExpReq
|
|
if err := transport.UnmarshalPayload(env.Payload, &req); err != nil {
|
|
return nil, err
|
|
}
|
|
group := p.cer.Config.Group
|
|
e2, err := req.E2.decode(group)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cc%d rc-exp: %w", p.index, err)
|
|
}
|
|
k := deriveReturnCodeKey(p.st.returnCodeSecret, labelChoice, p.cer.Config.ElectionID, req.VcID, group)
|
|
return reply(p.id, env.From, MsgRCExpResp, env.Nonce, rcExpResp{Exp: encodeCiphertext(e2.Exponentiate(k))})
|
|
}
|
|
|
|
// handleRCDec (CC) returns gamma^{sk_j[0]}, its factor of the decryption mask.
|
|
func (p *ControlComponent) handleRCDec(env *transport.Envelope) (*transport.Envelope, error) {
|
|
var req rcDecReq
|
|
if err := transport.UnmarshalPayload(env.Payload, &req); err != nil {
|
|
return nil, err
|
|
}
|
|
group := p.cer.Config.Group
|
|
gamma, err := strToGq(req.Gamma, group)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cc%d rc-dec: %w", p.index, err)
|
|
}
|
|
factor := gamma.Exponentiate(p.st.keyPair.SK.Get(0))
|
|
return reply(p.id, env.From, MsgRCDecResp, env.Nonce, rcDecResp{Factor: factor.Value().String()})
|
|
}
|