mirror of
https://github.com/saymrwulf/swisspost-evoting-go-poc.git
synced 2026-05-14 20:58:03 +00:00
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.
290 lines
8.6 KiB
Go
290 lines
8.6 KiB
Go
package mixnet
|
||
|
||
import (
|
||
"math/big"
|
||
|
||
"github.com/user/evote/pkg/elgamal"
|
||
"github.com/user/evote/pkg/hash"
|
||
emath "github.com/user/evote/pkg/math"
|
||
)
|
||
|
||
// ShuffleArgument is the top-level proof combining ProductArgument and MultiExponentiationArgument.
|
||
type ShuffleArgument struct {
|
||
CA *emath.GqVector // Commitments to permutation matrix columns
|
||
CB *emath.GqVector // Commitments to B = x^π columns
|
||
Product ProductArgument // Product argument
|
||
MultiExp MultiExponentiationArgument // Multi-exponentiation argument
|
||
}
|
||
|
||
// GenShuffleArgument generates a shuffle argument proving C' is a valid shuffle of C.
|
||
func GenShuffleArgument(
|
||
C *elgamal.CiphertextVector, // Original ciphertexts
|
||
CPrime *elgamal.CiphertextVector, // Shuffled ciphertexts
|
||
perm Permutation, // Permutation used
|
||
rho *emath.ZqVector, // Re-encryption exponents
|
||
pk elgamal.PublicKey,
|
||
ck CommitmentKey,
|
||
group *emath.GqGroup,
|
||
) ShuffleArgument {
|
||
zqGroup := emath.ZqGroupFromGqGroup(group)
|
||
N := C.Size()
|
||
m, n := GetMatrixDimensions(N)
|
||
|
||
// 1. Convert permutation to m×n matrix A
|
||
aCols := make([]*emath.ZqVector, m)
|
||
rA := emath.RandomZqVector(m, zqGroup)
|
||
for j := 0; j < m; j++ {
|
||
col := make([]emath.ZqElement, n)
|
||
for i := 0; i < n; i++ {
|
||
val := big.NewInt(int64(perm.Apply(n*j + i)))
|
||
col[i], _ = emath.NewZqElement(val, zqGroup)
|
||
}
|
||
aCols[j] = emath.ZqVectorOf(col...)
|
||
}
|
||
A := emath.ZqMatrixFromColumns(aCols)
|
||
|
||
// Commit to A columns
|
||
cA := ck.CommitMatrix(A, rA)
|
||
|
||
// 2. Fiat-Shamir for x
|
||
x := shuffleArgumentChallengeX(group, pk, &ck, C, CPrime, cA)
|
||
|
||
// 3. Compute B matrix: b[i] = x^(π[i])
|
||
xPowers := computeXPowers(x, N, zqGroup)
|
||
bCols := make([]*emath.ZqVector, m)
|
||
rB := emath.RandomZqVector(m, zqGroup)
|
||
for j := 0; j < m; j++ {
|
||
col := make([]emath.ZqElement, n)
|
||
for i := 0; i < n; i++ {
|
||
piVal := perm.Apply(n*j + i)
|
||
col[i] = xPowers[piVal]
|
||
}
|
||
bCols[j] = emath.ZqVectorOf(col...)
|
||
}
|
||
B := emath.ZqMatrixFromColumns(bCols)
|
||
cB := ck.CommitMatrix(B, rB)
|
||
|
||
// 4. Fiat-Shamir for y and z
|
||
y := shuffleArgumentChallengeY(group, pk, &ck, C, CPrime, cA, cB)
|
||
z := shuffleArgumentChallengeZ(group, pk, &ck, C, CPrime, cA, cB)
|
||
|
||
// 5. Compute D = y*A + B (element-wise)
|
||
one, _ := emath.NewZqElement(big.NewInt(1), zqGroup)
|
||
dCols := make([]*emath.ZqVector, m)
|
||
for j := 0; j < m; j++ {
|
||
dCols[j] = aCols[j].ScalarMultiply(y).Add(bCols[j])
|
||
}
|
||
|
||
// 6. Compute D - z for product argument
|
||
dzCols := make([]*emath.ZqVector, m)
|
||
for j := 0; j < m; j++ {
|
||
dzCol := make([]emath.ZqElement, n)
|
||
for i := 0; i < n; i++ {
|
||
dzCol[i] = dCols[j].Get(i).Subtract(z)
|
||
}
|
||
dzCols[j] = emath.ZqVectorOf(dzCol...)
|
||
}
|
||
DZ := emath.ZqMatrixFromColumns(dzCols)
|
||
|
||
// Compute b_product = Π_{i=0}^{N-1} (y*i + x^i - z)
|
||
bProduct := one
|
||
for i := 0; i < N; i++ {
|
||
iBI := big.NewInt(int64(i))
|
||
iElem, _ := emath.NewZqElement(iBI, zqGroup)
|
||
term := y.Multiply(iElem).Add(xPowers[i]).Subtract(z)
|
||
bProduct = bProduct.Multiply(term)
|
||
}
|
||
|
||
// Commitment to D-z
|
||
rDZ := make([]emath.ZqElement, m)
|
||
cDZ := make([]emath.GqElement, m)
|
||
for j := 0; j < m; j++ {
|
||
rDZ[j] = y.Multiply(rA.Get(j)).Add(rB.Get(j))
|
||
cNegZ := ck.Commit(emath.ZqVectorOf(func() []emath.ZqElement {
|
||
v := make([]emath.ZqElement, n)
|
||
for i := range v {
|
||
v[i] = z.Negate()
|
||
}
|
||
return v
|
||
}()...), zqGroup.Identity())
|
||
cDZ[j] = cA.Get(j).Exponentiate(y).Multiply(cB.Get(j)).Multiply(cNegZ)
|
||
}
|
||
cDZVec := emath.GqVectorOf(cDZ...)
|
||
rDZVec := emath.ZqVectorOf(rDZ...)
|
||
|
||
// 7. Product argument
|
||
prodArg := GenProductArgument(cDZVec, bProduct, DZ, rDZVec, pk, ck, group)
|
||
|
||
// 8. Multi-exponentiation argument
|
||
zero, _ := emath.NewZqElement(big.NewInt(0), zqGroup)
|
||
rhoAgg := zero
|
||
for i := 0; i < N; i++ {
|
||
piVal := perm.Apply(i)
|
||
rhoAgg = rhoAgg.Add(rho.Get(i).Multiply(xPowers[piVal]))
|
||
}
|
||
rhoAgg = rhoAgg.Negate()
|
||
|
||
// Build C' as m ciphertext rows of n
|
||
cPrimeRows := make([]*elgamal.CiphertextVector, m)
|
||
for j := 0; j < m; j++ {
|
||
rowCts := make([]elgamal.Ciphertext, n)
|
||
for i := 0; i < n; i++ {
|
||
rowCts[i] = CPrime.Get(n*j + i)
|
||
}
|
||
cPrimeRows[j] = elgamal.NewCiphertextVector(rowCts)
|
||
}
|
||
|
||
// Compute C_agg = Π C[i]^{x^i}
|
||
cAgg := identityCiphertext(C.PhiSize(), group)
|
||
for i := 0; i < N; i++ {
|
||
ct := C.Get(i).Exponentiate(xPowers[i])
|
||
cAgg = cAgg.Multiply(ct)
|
||
}
|
||
|
||
multiExpArg := GenMultiExponentiationArgument(cPrimeRows, cAgg, cB, B, rB, rhoAgg, pk, ck, group)
|
||
|
||
return ShuffleArgument{
|
||
CA: cA,
|
||
CB: cB,
|
||
Product: prodArg,
|
||
MultiExp: multiExpArg,
|
||
}
|
||
}
|
||
|
||
// VerifyShuffleArgument verifies a shuffle argument.
|
||
func VerifyShuffleArgument(
|
||
arg ShuffleArgument,
|
||
C *elgamal.CiphertextVector,
|
||
CPrime *elgamal.CiphertextVector,
|
||
pk elgamal.PublicKey,
|
||
ck CommitmentKey,
|
||
group *emath.GqGroup,
|
||
) bool {
|
||
zqGroup := emath.ZqGroupFromGqGroup(group)
|
||
N := C.Size()
|
||
m, n := GetMatrixDimensions(N)
|
||
|
||
// Reconstruct challenges
|
||
x := shuffleArgumentChallengeX(group, pk, &ck, C, CPrime, arg.CA)
|
||
y := shuffleArgumentChallengeY(group, pk, &ck, C, CPrime, arg.CA, arg.CB)
|
||
z := shuffleArgumentChallengeZ(group, pk, &ck, C, CPrime, arg.CA, arg.CB)
|
||
|
||
xPowers := computeXPowers(x, N, zqGroup)
|
||
one, _ := emath.NewZqElement(big.NewInt(1), zqGroup)
|
||
|
||
// Compute b_product
|
||
bProduct := one
|
||
for i := 0; i < N; i++ {
|
||
iBI := big.NewInt(int64(i))
|
||
iElem, _ := emath.NewZqElement(iBI, zqGroup)
|
||
term := y.Multiply(iElem).Add(xPowers[i]).Subtract(z)
|
||
bProduct = bProduct.Multiply(term)
|
||
}
|
||
|
||
// Reconstruct c_{D-z}
|
||
cDZ := make([]emath.GqElement, m)
|
||
for j := 0; j < m; j++ {
|
||
cNegZ := ck.Commit(emath.ZqVectorOf(func() []emath.ZqElement {
|
||
v := make([]emath.ZqElement, n)
|
||
for i := range v {
|
||
v[i] = z.Negate()
|
||
}
|
||
return v
|
||
}()...), zqGroup.Identity())
|
||
cDZ[j] = arg.CA.Get(j).Exponentiate(y).Multiply(arg.CB.Get(j)).Multiply(cNegZ)
|
||
}
|
||
cDZVec := emath.GqVectorOf(cDZ...)
|
||
|
||
// Verify product argument
|
||
if !VerifyProductArgument(arg.Product, cDZVec, bProduct, pk, ck, group) {
|
||
return false
|
||
}
|
||
|
||
// Reconstruct cMatrix and cAgg for multi-exponentiation
|
||
cPrimeRows := make([]*elgamal.CiphertextVector, m)
|
||
for j := 0; j < m; j++ {
|
||
rowCts := make([]elgamal.Ciphertext, n)
|
||
for i := 0; i < n; i++ {
|
||
rowCts[i] = CPrime.Get(n*j + i)
|
||
}
|
||
cPrimeRows[j] = elgamal.NewCiphertextVector(rowCts)
|
||
}
|
||
|
||
cAgg := identityCiphertext(C.PhiSize(), group)
|
||
for i := 0; i < N; i++ {
|
||
ct := C.Get(i).Exponentiate(xPowers[i])
|
||
cAgg = cAgg.Multiply(ct)
|
||
}
|
||
|
||
// Verify multi-exponentiation argument
|
||
return VerifyMultiExponentiationArgument(arg.MultiExp, cPrimeRows, cAgg, arg.CB, pk, ck, group)
|
||
}
|
||
|
||
// shuffleArgumentChallengeX computes the X challenge.
|
||
// Java hash order: (p, q, pk, ck, C_vector, C_prime, c_A)
|
||
func shuffleArgumentChallengeX(group *emath.GqGroup, pk elgamal.PublicKey, ck *CommitmentKey, C, CPrime *elgamal.CiphertextVector, cA *emath.GqVector) 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),
|
||
ciphertextVectorToHashable(C),
|
||
ciphertextVectorToHashable(CPrime),
|
||
gqVectorToHashable(cA),
|
||
)
|
||
|
||
eVal := new(big.Int).SetBytes(hashBytes)
|
||
eVal.Mod(eVal, q)
|
||
e, _ := emath.NewZqElement(eVal, zqGroup)
|
||
return e
|
||
}
|
||
|
||
// shuffleArgumentChallengeY computes the Y challenge.
|
||
// Java hash order: (c_B, p, q, pk, ck, C_vector, C_prime, c_A)
|
||
func shuffleArgumentChallengeY(group *emath.GqGroup, pk elgamal.PublicKey, ck *CommitmentKey, C, CPrime *elgamal.CiphertextVector, cA, cB *emath.GqVector) emath.ZqElement {
|
||
zqGroup := emath.ZqGroupFromGqGroup(group)
|
||
q := group.Q()
|
||
|
||
hashBytes := hash.RecursiveHash(
|
||
gqVectorToHashable(cB),
|
||
hash.HashableBigInt{Value: group.P()},
|
||
hash.HashableBigInt{Value: group.Q()},
|
||
pkToHashable(pk),
|
||
ckToHashable(ck),
|
||
ciphertextVectorToHashable(C),
|
||
ciphertextVectorToHashable(CPrime),
|
||
gqVectorToHashable(cA),
|
||
)
|
||
|
||
eVal := new(big.Int).SetBytes(hashBytes)
|
||
eVal.Mod(eVal, q)
|
||
e, _ := emath.NewZqElement(eVal, zqGroup)
|
||
return e
|
||
}
|
||
|
||
// shuffleArgumentChallengeZ computes the Z challenge.
|
||
// Java hash order: ("1", c_B, p, q, pk, ck, C_vector, C_prime, c_A)
|
||
func shuffleArgumentChallengeZ(group *emath.GqGroup, pk elgamal.PublicKey, ck *CommitmentKey, C, CPrime *elgamal.CiphertextVector, cA, cB *emath.GqVector) emath.ZqElement {
|
||
zqGroup := emath.ZqGroupFromGqGroup(group)
|
||
q := group.Q()
|
||
|
||
hashBytes := hash.RecursiveHash(
|
||
hash.HashableString{Value: "1"},
|
||
gqVectorToHashable(cB),
|
||
hash.HashableBigInt{Value: group.P()},
|
||
hash.HashableBigInt{Value: group.Q()},
|
||
pkToHashable(pk),
|
||
ckToHashable(ck),
|
||
ciphertextVectorToHashable(C),
|
||
ciphertextVectorToHashable(CPrime),
|
||
gqVectorToHashable(cA),
|
||
)
|
||
|
||
eVal := new(big.Int).SetBytes(hashBytes)
|
||
eVal.Mod(eVal, q)
|
||
e, _ := emath.NewZqElement(eVal, zqGroup)
|
||
return e
|
||
}
|