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

256 lines
7.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"
)
// HadamardArgument proves that b = ∏_j a_j (entrywise Hadamard product).
type HadamardArgument struct {
CB *emath.GqVector // Intermediate commitments (size m)
Zero ZeroArgument // Nested ZeroArgument
}
// GenHadamardArgument generates a Hadamard argument.
func GenHadamardArgument(
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 A columns (size m)
cb emath.GqElement, // Commitment to b (Hadamard product)
A *emath.ZqMatrix, // n×m matrix
b *emath.ZqVector, // Hadamard product (size n)
r *emath.ZqVector, // Randomness for A columns (size m)
s emath.ZqElement, // Randomness for cb
pk elgamal.PublicKey, // Public key (needed for Fiat-Shamir hash)
ck CommitmentKey,
group *emath.GqGroup,
) HadamardArgument {
zqGroup := emath.ZqGroupFromGqGroup(group)
n := A.NumRows()
m := A.NumCols()
emitArgument("hadamard",
"Hadamard argument: prove the entrywise product of A's columns equals b",
`\text{HadamardArgument}:\ \mathbf{b} = \mathbf{a}_1 \circ \mathbf{a}_2 \circ \cdots \circ \mathbf{a}_m \quad(\circ = \text{entrywise product})`,
"HadamardArgument: b = a_1 ∘ a_2 ∘ … ∘ a_m (∘ = entrywise product)",
dims(m, n))
// 1. Compute intermediate products b_j = ∏_{i=0}^j A[:,i] (entrywise)
bIntermediate := make([]*emath.ZqVector, m)
bIntermediate[0] = A.GetColumn(0)
for j := 1; j < m; j++ {
bIntermediate[j] = bIntermediate[j-1].MultiplyElementWise(A.GetColumn(j))
}
// 2. Build c_B and s_vector
cbVec := make([]emath.GqElement, m)
sVec := make([]emath.ZqElement, m)
cbVec[0] = cA.Get(0)
sVec[0] = r.Get(0)
for j := 1; j < m-1; j++ {
sVec[j] = emath.RandomZqElement(zqGroup)
cbVec[j] = ck.Commit(bIntermediate[j], sVec[j])
}
cbVec[m-1] = cb
sVec[m-1] = s
cB := emath.GqVectorOf(cbVec...)
// 3. Fiat-Shamir for x
// Java hash order: (p, q, pk, ck, c_A, c_b, c_B)
x := hadamardChallengeX(group, pk, &ck, cA, cb, cB)
// 4. Compute x^i powers
xPowers := computeXPowers(x, m+1, zqGroup)
// 5. Fiat-Shamir for y (with "1" prefix)
// Java hash order: ("1", p, q, pk, ck, c_A, c_b, c_B)
y := hadamardChallengeY(group, pk, &ck, cA, cb, cB)
// 6. Prepare ZeroArgument matrices
one, _ := emath.NewZqElement(big.NewInt(1), zqGroup)
negOnes := make([]emath.ZqElement, n)
for i := range negOnes {
negOnes[i] = one.Negate()
}
aZeroCols := make([]*emath.ZqVector, m)
for j := 0; j < m-1; j++ {
aZeroCols[j] = A.GetColumn(j + 1)
}
aZeroCols[m-1] = emath.ZqVectorOf(negOnes...)
// Java: d_matrix[i] = x^(i+1) * b_vectors[i] for i=0..m-2
dZeroCols := make([]*emath.ZqVector, m)
for i := 0; i < m-1; i++ {
dZeroCols[i] = bIntermediate[i].ScalarMultiply(xPowers[i+1])
}
// Java: d[j] = Σ(i=1..m-1) x^i * b_vectors[i][j]
dFinal := emath.ZqVectorOfZeros(n, zqGroup)
for i := 1; i < m; i++ {
dFinal = dFinal.Add(bIntermediate[i].ScalarMultiply(xPowers[i]))
}
dZeroCols[m-1] = dFinal
// Build c_A_zero and c_D_zero
// Java: r_zero = [r[1:], 0] — last randomness is 0, not random
zero, _ := emath.NewZqElement(big.NewInt(0), zqGroup)
cAZero := make([]emath.GqElement, m)
rZero := make([]emath.ZqElement, m)
for j := 0; j < m-1; j++ {
cAZero[j] = cA.Get(j + 1)
rZero[j] = r.Get(j + 1)
}
// Java: c_minus_one = commit((-1,...,-1), 0)
cAZero[m-1] = ck.Commit(emath.ZqVectorOf(negOnes...), zero)
rZero[m-1] = zero
// Java: c_D_vector[i] = c_B[i]^(x^(i+1)) for i=0..m-2
cDZero := make([]emath.GqElement, m)
sDZero := make([]emath.ZqElement, m)
for i := 0; i < m-1; i++ {
sDZero[i] = sVec[i].Multiply(xPowers[i+1])
cDZero[i] = cB.Get(i).Exponentiate(xPowers[i+1])
}
// Java: t = Σ(i=1..m-1) x^i * s_vector[i]
sDFinal := zqGroup.Identity()
for i := 1; i < m; i++ {
sDFinal = sDFinal.Add(sVec[i].Multiply(xPowers[i]))
}
sDZero[m-1] = sDFinal
// Java: c_D = Π(i=1..m-1) c_B[i]^(x^i)
cDFinal := group.Identity()
for i := 1; i < m; i++ {
cDFinal = cDFinal.Multiply(cB.Get(i).Exponentiate(xPowers[i]))
}
cDZero[m-1] = cDFinal
// Build matrices
aZeroMatrix := emath.ZqMatrixFromColumns(aZeroCols)
dZeroMatrix := emath.ZqMatrixFromColumns(dZeroCols)
cAZeroVec := emath.GqVectorOf(cAZero...)
cDZeroVec := emath.GqVectorOf(cDZero...)
rZeroVec := emath.ZqVectorOf(rZero...)
sDZeroVec := emath.ZqVectorOf(sDZero...)
// 7. Generate ZeroArgument (now with pk)
zeroArg := GenZeroArgument(cAZeroVec, cDZeroVec, aZeroMatrix, dZeroMatrix, rZeroVec, sDZeroVec, y, pk, ck, group)
return HadamardArgument{
CB: cB,
Zero: zeroArg,
}
}
// VerifyHadamardArgument verifies a Hadamard argument.
func VerifyHadamardArgument(
arg HadamardArgument,
cA *emath.GqVector,
cb emath.GqElement,
pk elgamal.PublicKey,
ck CommitmentKey,
group *emath.GqGroup,
) bool {
m := cA.Size()
// Check c_B[0] = c_A[0]
if !arg.CB.Get(0).Equals(cA.Get(0)) {
return false
}
// Check c_B[m-1] = c_b
if !arg.CB.Get(m - 1).Equals(cb) {
return false
}
// Reconstruct x and y
x := hadamardChallengeX(group, pk, &ck, cA, cb, arg.CB)
y := hadamardChallengeY(group, pk, &ck, cA, cb, arg.CB)
zqGroup := emath.ZqGroupFromGqGroup(group)
xPowers := computeXPowers(x, m+1, zqGroup)
// Reconstruct c_A_zero and c_D_zero for verification
n := arg.Zero.APrime.Size()
one, _ := emath.NewZqElement(big.NewInt(1), zqGroup)
negOnes := make([]emath.ZqElement, n)
for i := range negOnes {
negOnes[i] = one.Negate()
}
cAZero := make([]emath.GqElement, m)
for j := 0; j < m-1; j++ {
cAZero[j] = cA.Get(j + 1)
}
// For the c_A_zero, the last element (commitment to -1s) needs to be
// derived from the ZeroArgument itself. Since VerifyZeroArgument handles
// all commitment checks internally, we pass through.
cNegOnes := ck.Commit(emath.ZqVectorOf(negOnes...), zqGroup.Identity())
cAZero[m-1] = cNegOnes
// Build c_D_zero
// Java: c_D_vector[i] = c_B[i]^(x^(i+1)) for i=0..m-2
cDZero := make([]emath.GqElement, m)
for i := 0; i < m-1; i++ {
cDZero[i] = arg.CB.Get(i).Exponentiate(xPowers[i+1])
}
// Java: c_D = Π(i=1..m-1) c_B[i]^(x^i)
cDFinal := group.Identity()
for i := 1; i < m; i++ {
cDFinal = cDFinal.Multiply(arg.CB.Get(i).Exponentiate(xPowers[i]))
}
cDZero[m-1] = cDFinal
cAZeroVec := emath.GqVectorOf(cAZero...)
cDZeroVec := emath.GqVectorOf(cDZero...)
// Verify the ZeroArgument
return VerifyZeroArgument(arg.Zero, cAZeroVec, cDZeroVec, y, pk, ck, group)
}
// hadamardChallengeX computes the X challenge for HadamardArgument.
// Java hash order: (p, q, pk, ck, c_A, c_b, c_B)
func hadamardChallengeX(group *emath.GqGroup, pk elgamal.PublicKey, ck *CommitmentKey, cA *emath.GqVector, cb emath.GqElement, cB *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),
gqVectorToHashable(cA),
hash.HashableBigInt{Value: cb.Value()},
gqVectorToHashable(cB),
)
eVal := new(big.Int).SetBytes(hashBytes)
eVal.Mod(eVal, q)
e, _ := emath.NewZqElement(eVal, zqGroup)
return e
}
// hadamardChallengeY computes the Y challenge for HadamardArgument.
// Java hash order: ("1", p, q, pk, ck, c_A, c_b, c_B)
func hadamardChallengeY(group *emath.GqGroup, pk elgamal.PublicKey, ck *CommitmentKey, cA *emath.GqVector, cb emath.GqElement, cB *emath.GqVector) emath.ZqElement {
zqGroup := emath.ZqGroupFromGqGroup(group)
q := group.Q()
hashBytes := hash.RecursiveHash(
hash.HashableString{Value: "1"},
hash.HashableBigInt{Value: group.P()},
hash.HashableBigInt{Value: group.Q()},
pkToHashable(pk),
ckToHashable(ck),
gqVectorToHashable(cA),
hash.HashableBigInt{Value: cb.Value()},
gqVectorToHashable(cB),
)
eVal := new(big.Int).SetBytes(hashBytes)
eVal.Mod(eVal, q)
e, _ := emath.NewZqElement(eVal, zqGroup)
return e
}