swisspost-evoting-go-poc/pkg/mixnet/shuffle_argument.go

291 lines
8.6 KiB
Go
Raw Normal View History

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 {
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 12:42:34 +00:00
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(
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 12:42:34 +00:00
C *elgamal.CiphertextVector, // Original ciphertexts
CPrime *elgamal.CiphertextVector, // Shuffled ciphertexts
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 12:42:34 +00:00
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
}