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

259 lines
7 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"
)
// ZeroArgument is a proof that Σ_i a_i ★ b_{i-1} = 0 for committed matrices A, B.
type ZeroArgument 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
CA0 emath.GqElement // Commitment to a_0 (prepended column)
CBm emath.GqElement // Commitment to b_m (appended column)
CD *emath.GqVector // Commitments to diagonal d vector (size 2m+1)
APrime *emath.ZqVector // Aggregated a' vector
BPrime *emath.ZqVector // Aggregated b' vector
RPrime emath.ZqElement // Aggregated randomness for A
SPrime emath.ZqElement // Aggregated randomness for B
TPrime emath.ZqElement // Aggregated randomness for D
}
// GenZeroArgument generates a ZeroArgument proof.
func GenZeroArgument(
cA *emath.GqVector, // Commitments to A columns (size m)
cB *emath.GqVector, // Commitments to B columns (size m)
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
A *emath.ZqMatrix, // n×m matrix
B *emath.ZqMatrix, // n×m matrix
r *emath.ZqVector, // Randomness for A (size m)
s *emath.ZqVector, // Randomness for B (size m)
y emath.ZqElement, // Star map parameter
pk elgamal.PublicKey, // Public key (needed for Fiat-Shamir hash)
ck CommitmentKey,
group *emath.GqGroup,
) ZeroArgument {
zqGroup := emath.ZqGroupFromGqGroup(group)
n := A.NumRows()
m := A.NumCols()
emitArgument("zero",
"Zero argument: prove a bilinear map over A and B vanishes",
`\text{ZeroArgument}:\ \sum_{i=1}^{m} \mathbf{a}_i \ast_y \mathbf{b}_i = 0, \quad \mathbf{a}\ast_y\mathbf{b}=\textstyle\sum_j a_j b_j y^{j}`,
"ZeroArgument: Σ_i (a_i _y b_i) = 0 with bilinear star-map _y",
dims(m, n))
// 1. Prepend random a_0 to A, append random b_m to B
a0 := emath.RandomZqVector(n, zqGroup)
r0 := emath.RandomZqElement(zqGroup)
cA0 := ck.Commit(a0, r0)
bm := emath.RandomZqVector(n, zqGroup)
sm := emath.RandomZqElement(zqGroup)
cBm := ck.Commit(bm, sm)
// A' = [a_0 | A] (m+1 columns, n rows)
aPrimeCols := make([]*emath.ZqVector, m+1)
aPrimeCols[0] = a0
for j := 0; j < m; j++ {
aPrimeCols[j+1] = A.GetColumn(j)
}
// B' = [B | b_m] (m+1 columns, n rows)
bPrimeCols := make([]*emath.ZqVector, m+1)
for j := 0; j < m; j++ {
bPrimeCols[j] = B.GetColumn(j)
}
bPrimeCols[m] = bm
// r' = [r_0 | r] and s' = [s | s_m]
rPrepended := r.Prepend(r0)
sAppended := s.Append(sm)
// 2. Compute d vector (diagonal star map products)
// Java formula: d[k] = Σ StarMap(A'[i], B'[j]) where j = (m - k) + i
// Bounds: i = max(0, k-m) to m, break when j > m
dSize := 2*m + 1
dVec := make([]emath.ZqElement, dSize)
zero, _ := emath.NewZqElement(big.NewInt(0), zqGroup)
for k := 0; k < dSize; k++ {
dVec[k] = zero
for i := max(0, k-m); i <= m; i++ {
j := (m - k) + i
if j > m {
break
}
if j >= 0 {
sm := StarMap(aPrimeCols[i], bPrimeCols[j], y)
dVec[k] = dVec[k].Add(sm)
}
}
}
// 3. Generate randomness for d (Java: t[m+1] = 0)
tVec := make([]emath.ZqElement, dSize)
for k := 0; k < dSize; k++ {
if k == m+1 {
tVec[k] = zero
} else {
tVec[k] = emath.RandomZqElement(zqGroup)
}
}
// 4. Compute commitments to d
cdElems := make([]emath.GqElement, dSize)
for k := 0; k < dSize; k++ {
cdElems[k] = ck.H.Exponentiate(tVec[k]).Multiply(ck.G.Get(0).Exponentiate(dVec[k]))
}
cD := emath.GqVectorOf(cdElems...)
// 5. Fiat-Shamir challenge x
// Java hash order: (p, q, pk, ck, c_A_0, c_B_m, c_d, c_B, c_A)
x := zeroArgumentChallenge(group, pk, &ck, cA0, cBm, cD, cB, cA)
// 6. Compute x^i powers
xPowers := computeXPowers(x, 2*m+1, zqGroup)
// 7. Compute proof elements
aPrimeVec := emath.ZqVectorOfZeros(n, zqGroup)
for i := 0; i <= m; i++ {
scaled := aPrimeCols[i].ScalarMultiply(xPowers[i])
aPrimeVec = aPrimeVec.Add(scaled)
}
bPrimeVec := emath.ZqVectorOfZeros(n, zqGroup)
for i := 0; i <= m; i++ {
scaled := bPrimeCols[i].ScalarMultiply(xPowers[m-i])
bPrimeVec = bPrimeVec.Add(scaled)
}
rPrimeVal := zero
for i := 0; i <= m; i++ {
rPrimeVal = rPrimeVal.Add(xPowers[i].Multiply(rPrepended.Get(i)))
}
sPrimeVal := zero
for i := 0; i <= m; i++ {
sPrimeVal = sPrimeVal.Add(xPowers[m-i].Multiply(sAppended.Get(i)))
}
tPrimeVal := zero
for k := 0; k < dSize; k++ {
tPrimeVal = tPrimeVal.Add(xPowers[k].Multiply(tVec[k]))
}
return ZeroArgument{
CA0: cA0,
CBm: cBm,
CD: cD,
APrime: aPrimeVec,
BPrime: bPrimeVec,
RPrime: rPrimeVal,
SPrime: sPrimeVal,
TPrime: tPrimeVal,
}
}
// VerifyZeroArgument verifies a ZeroArgument proof.
func VerifyZeroArgument(
arg ZeroArgument,
cA *emath.GqVector,
cB *emath.GqVector,
y emath.ZqElement,
pk elgamal.PublicKey,
ck CommitmentKey,
group *emath.GqGroup,
) bool {
zqGroup := emath.ZqGroupFromGqGroup(group)
m := cA.Size()
// 1. Reconstruct x
x := zeroArgumentChallenge(group, pk, &ck, arg.CA0, arg.CBm, arg.CD, cB, cA)
xPowers := computeXPowers(x, 2*m+1, zqGroup)
// 2. Check c_D[m+1] commits to 0 (Java: c_d.get(m+1) == 1)
if !arg.CD.Get(m + 1).IsIdentity() {
return false
}
// 3. Check Π(c_A[:,i]^{x^(i+1)}) * c_A_0^{x^0} = commit(a', r')
lhs1 := arg.CA0.Exponentiate(xPowers[0])
for i := 0; i < m; i++ {
lhs1 = lhs1.Multiply(cA.Get(i).Exponentiate(xPowers[i+1]))
}
rhs1 := ck.Commit(arg.APrime, arg.RPrime)
if !lhs1.Equals(rhs1) {
return false
}
// 4. Check Π(c_B[:,i]^{x^(m-i)}) * c_B_m^{x^0} = commit(b', s')
lhs2 := arg.CBm.Exponentiate(xPowers[0])
for i := 0; i < m; i++ {
lhs2 = lhs2.Multiply(cB.Get(i).Exponentiate(xPowers[m-i]))
}
rhs2 := ck.Commit(arg.BPrime, arg.SPrime)
if !lhs2.Equals(rhs2) {
return false
}
// 5. Check Π(c_D[k]^{x^k}) = commit(starMap(a', b', y), t')
lhs3 := group.Identity()
for k := 0; k < arg.CD.Size(); k++ {
lhs3 = lhs3.Multiply(arg.CD.Get(k).Exponentiate(xPowers[k]))
}
starMapVal := StarMap(arg.APrime, arg.BPrime, y)
rhs3 := ck.H.Exponentiate(arg.TPrime).Multiply(ck.G.Get(0).Exponentiate(starMapVal))
return lhs3.Equals(rhs3)
}
// zeroArgumentChallenge computes the Fiat-Shamir challenge for ZeroArgument.
// Java hash order: (p, q, pk, ck, c_A_0, c_B_m, c_d, c_B, c_A)
func zeroArgumentChallenge(group *emath.GqGroup, pk elgamal.PublicKey, ck *CommitmentKey, cA0, cBm emath.GqElement, cD, cB, 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),
hash.HashableBigInt{Value: cA0.Value()},
hash.HashableBigInt{Value: cBm.Value()},
gqVectorToHashable(cD),
gqVectorToHashable(cB),
gqVectorToHashable(cA),
)
eVal := new(big.Int).SetBytes(hashBytes)
eVal.Mod(eVal, q)
e, _ := emath.NewZqElement(eVal, zqGroup)
return e
}
func computeXPowers(x emath.ZqElement, count int, group *emath.ZqGroup) []emath.ZqElement {
powers := make([]emath.ZqElement, count)
one, _ := emath.NewZqElement(big.NewInt(1), group)
powers[0] = one
if count > 1 {
powers[1] = x
for i := 2; i < count; i++ {
powers[i] = powers[i-1].Multiply(x)
}
}
return powers
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func min(a, b int) int {
if a < b {
return a
}
return b
}