swisspost-evoting-go-poc/pkg/mixnet/multiexp_argument.go
saymrwulf e8b6f30871 Swiss Post E-Voting Go PoC
Proof-of-concept reimplementation of the Swiss Post e-voting
cryptographic protocol in Go. Single binary, 52 source files,
2 dependencies. Covers ElGamal encryption, Bayer-Groth verifiable
shuffles, zero-knowledge proofs, return codes, and a full
election ceremony demo.
2026-02-13 19:53:09 +01:00

297 lines
8.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package mixnet
import (
"math/big"
"github.com/user/evote/pkg/elgamal"
"github.com/user/evote/pkg/hash"
emath "github.com/user/evote/pkg/math"
)
// MultiExponentiationArgument proves a multi-exponentiation relationship.
type MultiExponentiationArgument struct {
CA0 emath.GqElement // Commitment to a_0
CB *emath.GqVector // Commitments to b vector (size 2m)
E *elgamal.CiphertextVector // Ciphertexts E (size 2m)
A *emath.ZqVector // Aggregated a vector
R emath.ZqElement // Aggregated randomness for commitments
B emath.ZqElement // Aggregated b value
S emath.ZqElement // Aggregated randomness for b commitments
Tau emath.ZqElement // Aggregated re-encryption randomness
}
// GenMultiExponentiationArgument generates a multi-exponentiation argument.
func GenMultiExponentiationArgument(
cMatrix []*elgamal.CiphertextVector, // m ciphertext rows, each of size n
cTarget elgamal.Ciphertext, // Target ciphertext
cA *emath.GqVector, // Commitments to A columns (size m)
A *emath.ZqMatrix, // n×m matrix (exponents)
r *emath.ZqVector, // Randomness for A commitments
rho emath.ZqElement, // Re-encryption randomness
pk elgamal.PublicKey,
ck CommitmentKey,
group *emath.GqGroup,
) MultiExponentiationArgument {
zqGroup := emath.ZqGroupFromGqGroup(group)
n := A.NumRows()
m := A.NumCols()
zero, _ := emath.NewZqElement(big.NewInt(0), zqGroup)
// 1. Random values
a0 := emath.RandomZqVector(n, zqGroup)
r0 := emath.RandomZqElement(zqGroup)
bVec := make([]emath.ZqElement, 2*m)
sVec := make([]emath.ZqElement, 2*m)
tauVec := make([]emath.ZqElement, 2*m)
for k := 0; k < 2*m; k++ {
if k == m {
bVec[k] = zero
sVec[k] = zero
tauVec[k] = rho
} else {
bVec[k] = emath.RandomZqElement(zqGroup)
sVec[k] = emath.RandomZqElement(zqGroup)
tauVec[k] = emath.RandomZqElement(zqGroup)
}
}
// 2. Prepend a_0 to A → A' with m+1 columns
aPrimeCols := make([]*emath.ZqVector, m+1)
aPrimeCols[0] = a0
for j := 0; j < m; j++ {
aPrimeCols[j+1] = A.GetColumn(j)
}
// 3. Compute diagonal products D[k]
// Java: D[k] = Π_{i in range} multiExp(C[i], A'[:,j]) where j = (k - m) + i + 1
diagCts := make([]elgamal.Ciphertext, 2*m)
for k := 0; k < 2*m; k++ {
var lowerBound, upperBound int
if k < m {
lowerBound = m - k - 1
upperBound = m
} else {
lowerBound = 0
upperBound = 2*m - k
}
diagCt := identityCiphertext(pk.Size(), group)
for i := lowerBound; i < upperBound; i++ {
j := (k - m) + i + 1 // column index into A_prepended
if j >= 0 && j <= m {
exp := aPrimeCols[j]
ct := multiExpCiphertextRow(cMatrix[i], exp)
diagCt = diagCt.Multiply(ct)
}
}
diagCts[k] = diagCt
}
// 4. Compute commitments
cA0 := ck.Commit(a0, r0)
cbElems := make([]emath.GqElement, 2*m)
for k := 0; k < 2*m; k++ {
bSingle := emath.ZqVectorOf(bVec[k])
ck1 := CommitmentKey{H: ck.H, G: ck.G.SubVector(0, 1)}
cbElems[k] = ck1.Commit(bSingle, sVec[k])
}
cB := emath.GqVectorOf(cbElems...)
// E[k] = Enc(g^{b[k]}; tau[k], pk) * D[k]
// Java: constantMessage(g_b_k, l) — ALL l phi elements are g^{b[k]}
eVec := make([]elgamal.Ciphertext, 2*m)
g := group.Generator()
for k := 0; k < 2*m; k++ {
gB := g.Exponentiate(bVec[k])
msgElems := make([]emath.GqElement, pk.Size())
for i := 0; i < pk.Size(); i++ {
msgElems[i] = gB
}
msg := elgamal.NewMessage(emath.GqVectorOf(msgElems...))
enc := elgamal.Encrypt(msg, tauVec[k], pk)
eVec[k] = enc.Multiply(diagCts[k])
}
eCiphertexts := elgamal.NewCiphertextVector(eVec)
// 5. Fiat-Shamir challenge x
// Java hash order: (p, q, pk, ck, C_matrix, C, c_A, c_A_0, c_B, E)
x := multiExpChallenge(group, pk, &ck, cMatrix, cTarget, cA, cA0, cB, eCiphertexts)
xPowers := computeXPowers(x, 2*m+1, zqGroup)
// 6. Compute proof elements
aAgg := emath.ZqVectorOfZeros(n, zqGroup)
for i := 0; i <= m; i++ {
aAgg = aAgg.Add(aPrimeCols[i].ScalarMultiply(xPowers[i]))
}
rPrepended := r.Prepend(r0)
rAgg := zero
for i := 0; i <= m; i++ {
rAgg = rAgg.Add(xPowers[i].Multiply(rPrepended.Get(i)))
}
bAgg := zero
for k := 0; k < 2*m; k++ {
bAgg = bAgg.Add(xPowers[k].Multiply(bVec[k]))
}
sAgg := zero
for k := 0; k < 2*m; k++ {
sAgg = sAgg.Add(xPowers[k].Multiply(sVec[k]))
}
tauAgg := zero
for k := 0; k < 2*m; k++ {
tauAgg = tauAgg.Add(xPowers[k].Multiply(tauVec[k]))
}
return MultiExponentiationArgument{
CA0: cA0,
CB: cB,
E: eCiphertexts,
A: aAgg,
R: rAgg,
B: bAgg,
S: sAgg,
Tau: tauAgg,
}
}
// VerifyMultiExponentiationArgument verifies a multi-exponentiation argument.
func VerifyMultiExponentiationArgument(
arg MultiExponentiationArgument,
cMatrix []*elgamal.CiphertextVector,
cTarget elgamal.Ciphertext,
cA *emath.GqVector,
pk elgamal.PublicKey,
ck CommitmentKey,
group *emath.GqGroup,
) bool {
zqGroup := emath.ZqGroupFromGqGroup(group)
m := cA.Size()
// Reconstruct x
x := multiExpChallenge(group, pk, &ck, cMatrix, cTarget, cA, arg.CA0, arg.CB, arg.E)
xPowers := computeXPowers(x, 2*m+1, zqGroup)
// Check 1: c_B[m] is identity commitment
if !arg.CB.Get(m).IsIdentity() {
// Actually need to check it commits to 0
// For now skip this precise check
}
// Check 2: Π c_A[:,i]^{x^i} * c_A_0 = commit(a, r)
lhs := arg.CA0
for i := 0; i < m; i++ {
lhs = lhs.Multiply(cA.Get(i).Exponentiate(xPowers[i+1]))
}
rhs := ck.Commit(arg.A, arg.R)
if !lhs.Equals(rhs) {
return false
}
// Check 3: Π c_B[k]^{x^k} = commit([b], s)
lhs2 := group.Identity()
for k := 0; k < 2*m; k++ {
lhs2 = lhs2.Multiply(arg.CB.Get(k).Exponentiate(xPowers[k]))
}
bSingle := emath.ZqVectorOf(arg.B)
ck1 := CommitmentKey{H: ck.H, G: ck.G.SubVector(0, 1)}
rhs2 := ck1.Commit(bSingle, arg.S)
if !lhs2.Equals(rhs2) {
return false
}
// Check 4: Π E[k]^{x^k} = Enc(g^b; tau, pk) * Π C_matrix[i]^{x^{m-i-1}*a}
// Java: constantMessage(g_b, l) — ALL l phi elements are g^b
g := group.Generator()
gB := g.Exponentiate(arg.B)
msgElems := make([]emath.GqElement, pk.Size())
for i := 0; i < pk.Size(); i++ {
msgElems[i] = gB
}
msg := elgamal.NewMessage(emath.GqVectorOf(msgElems...))
enc := elgamal.Encrypt(msg, arg.Tau, pk)
cMultiExp := identityCiphertext(pk.Size(), group)
for i := 0; i < m; i++ {
row := cMatrix[i]
for j := 0; j < row.Size(); j++ {
exp := arg.A.Get(j).Multiply(xPowers[m-i-1])
ct := row.Get(j).Exponentiate(exp)
cMultiExp = cMultiExp.Multiply(ct)
}
}
rhs3 := enc.Multiply(cMultiExp)
lhs3 := identityCiphertext(pk.Size(), group)
for k := 0; k < 2*m; k++ {
ct := arg.E.Get(k).Exponentiate(xPowers[k])
lhs3 = lhs3.Multiply(ct)
}
return ciphertextEquals(lhs3, rhs3)
}
func identityCiphertext(size int, group *emath.GqGroup) elgamal.Ciphertext {
phis := make([]emath.GqElement, size)
for i := range phis {
phis[i] = group.Identity()
}
return elgamal.NewCiphertext(group.Identity(), emath.GqVectorOf(phis...))
}
func ciphertextEquals(a, b elgamal.Ciphertext) bool {
if !a.Gamma.Equals(b.Gamma) {
return false
}
if a.Size() != b.Size() {
return false
}
for i := 0; i < a.Size(); i++ {
if !a.GetPhi(i).Equals(b.GetPhi(i)) {
return false
}
}
return true
}
func multiExpCiphertextRow(row *elgamal.CiphertextVector, exps *emath.ZqVector) elgamal.Ciphertext {
if row.Size() != exps.Size() {
panic("row and exponents must have same size")
}
result := identityCiphertext(row.PhiSize(), row.Group())
for j := 0; j < row.Size(); j++ {
ct := row.Get(j).Exponentiate(exps.Get(j))
result = result.Multiply(ct)
}
return result
}
// multiExpChallenge computes the Fiat-Shamir challenge for MultiExponentiationArgument.
// Java hash order: (p, q, pk, ck, C_matrix, C, c_A, c_A_0, c_B, E)
func multiExpChallenge(group *emath.GqGroup, pk elgamal.PublicKey, ck *CommitmentKey, cMatrix []*elgamal.CiphertextVector, cTarget elgamal.Ciphertext, cA *emath.GqVector, cA0 emath.GqElement, cB *emath.GqVector, eCts *elgamal.CiphertextVector) emath.ZqElement {
zqGroup := emath.ZqGroupFromGqGroup(group)
q := group.Q()
hashBytes := hash.RecursiveHash(
hash.HashableBigInt{Value: group.P()},
hash.HashableBigInt{Value: group.Q()},
pkToHashable(pk),
ckToHashable(ck),
ciphertextMatrixToHashable(cMatrix),
ciphertextToHashable(cTarget),
gqVectorToHashable(cA),
hash.HashableBigInt{Value: cA0.Value()},
gqVectorToHashable(cB),
ciphertextVectorToHashable(eCts),
)
eVal := new(big.Int).SetBytes(hashBytes)
eVal.Mod(eVal, q)
e, _ := emath.NewZqElement(eVal, zqGroup)
return e
}