2026-02-13 18:53:09 +00:00
|
|
|
|
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
|
2026-02-13 18:53:09 +00:00
|
|
|
|
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()
|
|
|
|
|
|
|
Instrument the deep Bayer-Groth layers: commitments, sub-arguments, decryption
Take the live-math cockpit down to the level of the Swiss Post crypto-primitives
class structure. The shuffle proof is no longer one line — you can now watch it
being constructed:
- Pedersen matrix commitment (CommitmentService analog): c_A = Comm(A; r),
c_{A,j} = h^{r_j} Π g_i^{A_ij}, emitted from CommitMatrix.
- All five Bayer-Groth sub-arguments, mirroring the *ArgumentService classes:
ShuffleArgument (composition + x,y,z challenges), ProductArgument,
HadamardArgument (entrywise product), ZeroArgument (bilinear star-map),
SingleValueProductArgument, MultiExponentiationArgument — each emits its
defining relation as LaTeX with live dimensions.
- Partial decryption + decryption proof (DecryptionProofService analog):
φ'_i = φ_i·γ_i^{-sk} with the ZK proof that log_g(pk) = log_γ(γ^sk).
New trace.KindArgument. Low-level Commit stays uninstrumented (called in
verification too — would flood the stream); CommitMatrix is the semantic step.
Test: a 6-voter ceremony (N=6 → 2×3 shuffle matrix, so m>1 and the full argument
tree runs) captures 345 live events across 8 kinds, and asserts all five named
sub-arguments appear.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:26:03 +00:00
|
|
|
|
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))
|
|
|
|
|
|
|
2026-02-13 18:53:09 +00:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|