2026-07-06 13:11:34 +00:00
|
|
|
package party
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"math/big"
|
|
|
|
|
|
|
|
|
|
"github.com/user/evote/pkg/elgamal"
|
|
|
|
|
emath "github.com/user/evote/pkg/math"
|
|
|
|
|
"github.com/user/evote/pkg/zkp"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// This file is the validated serialization boundary between parties. Crypto
|
|
|
|
|
// objects have unexported fields and cannot be JSON-marshaled directly, so they
|
|
|
|
|
// travel as decimal-string DTOs. Every decode routes group elements through the
|
|
|
|
|
// checked constructors (NewGqElement / NewZqElement), so a peer can never inject
|
|
|
|
|
// a value outside G_q or Z_q — this closes the small-subgroup / non-residue hole
|
|
|
|
|
// (finding M4) at the point where proofs and ciphertexts cross a trust boundary.
|
|
|
|
|
|
|
|
|
|
// --- scalar helpers ---
|
|
|
|
|
|
|
|
|
|
func gqToStr(e emath.GqElement) string { return e.Value().String() }
|
|
|
|
|
func zqToStr(e emath.ZqElement) string { return e.Value().String() }
|
|
|
|
|
|
|
|
|
|
func strToGq(s string, group *emath.GqGroup) (emath.GqElement, error) {
|
|
|
|
|
v, ok := new(big.Int).SetString(s, 10)
|
|
|
|
|
if !ok {
|
|
|
|
|
return emath.GqElement{}, fmt.Errorf("invalid integer %q", s)
|
|
|
|
|
}
|
|
|
|
|
return emath.NewGqElement(v, group) // validates membership in G_q
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func strToZq(s string, zq *emath.ZqGroup) (emath.ZqElement, error) {
|
|
|
|
|
v, ok := new(big.Int).SetString(s, 10)
|
|
|
|
|
if !ok {
|
|
|
|
|
return emath.ZqElement{}, fmt.Errorf("invalid integer %q", s)
|
|
|
|
|
}
|
|
|
|
|
return emath.NewZqElement(v, zq) // validates range [0, q)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- vectors ---
|
|
|
|
|
|
|
|
|
|
func gqVecToStrs(v *emath.GqVector) []string {
|
|
|
|
|
out := make([]string, v.Size())
|
|
|
|
|
for i := 0; i < v.Size(); i++ {
|
|
|
|
|
out[i] = gqToStr(v.Get(i))
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func strsToGqVec(ss []string, group *emath.GqGroup) (*emath.GqVector, error) {
|
|
|
|
|
elems := make([]emath.GqElement, len(ss))
|
|
|
|
|
for i, s := range ss {
|
|
|
|
|
e, err := strToGq(s, group)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("gq vector element %d: %w", i, err)
|
|
|
|
|
}
|
|
|
|
|
elems[i] = e
|
|
|
|
|
}
|
|
|
|
|
return emath.GqVectorOf(elems...), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func zqVecToStrs(v *emath.ZqVector) []string {
|
|
|
|
|
out := make([]string, v.Size())
|
|
|
|
|
for i := 0; i < v.Size(); i++ {
|
|
|
|
|
out[i] = zqToStr(v.Get(i))
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func strsToZqVec(ss []string, zq *emath.ZqGroup) (*emath.ZqVector, error) {
|
|
|
|
|
elems := make([]emath.ZqElement, len(ss))
|
|
|
|
|
for i, s := range ss {
|
|
|
|
|
e, err := strToZq(s, zq)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("zq vector element %d: %w", i, err)
|
|
|
|
|
}
|
|
|
|
|
elems[i] = e
|
|
|
|
|
}
|
|
|
|
|
return emath.ZqVectorOf(elems...), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- public key ---
|
|
|
|
|
|
|
|
|
|
type wirePublicKey struct {
|
|
|
|
|
Elements []string `json:"elements"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func encodePK(pk elgamal.PublicKey) wirePublicKey {
|
|
|
|
|
return wirePublicKey{Elements: gqVecToStrs(pk.Elements)}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (w wirePublicKey) decode(group *emath.GqGroup) (elgamal.PublicKey, error) {
|
|
|
|
|
vec, err := strsToGqVec(w.Elements, group)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return elgamal.PublicKey{}, fmt.Errorf("public key: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return elgamal.PublicKey{Elements: vec}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Schnorr proof ---
|
|
|
|
|
|
|
|
|
|
type wireSchnorr struct {
|
|
|
|
|
E string `json:"e"`
|
|
|
|
|
Z string `json:"z"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func encodeSchnorr(p zkp.SchnorrProof) wireSchnorr {
|
|
|
|
|
return wireSchnorr{E: zqToStr(p.E), Z: zqToStr(p.Z)}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (w wireSchnorr) decode(zq *emath.ZqGroup) (zkp.SchnorrProof, error) {
|
|
|
|
|
e, err := strToZq(w.E, zq)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return zkp.SchnorrProof{}, fmt.Errorf("schnorr E: %w", err)
|
|
|
|
|
}
|
|
|
|
|
z, err := strToZq(w.Z, zq)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return zkp.SchnorrProof{}, fmt.Errorf("schnorr Z: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return zkp.SchnorrProof{E: e, Z: z}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 13:19:07 +00:00
|
|
|
// --- exponentiation proof (same shape as Schnorr) ---
|
|
|
|
|
|
|
|
|
|
func encodeExponentiation(p zkp.ExponentiationProof) wireSchnorr {
|
|
|
|
|
return wireSchnorr{E: zqToStr(p.E), Z: zqToStr(p.Z)}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (w wireSchnorr) decodeExponentiation(zq *emath.ZqGroup) (zkp.ExponentiationProof, error) {
|
|
|
|
|
e, err := strToZq(w.E, zq)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return zkp.ExponentiationProof{}, fmt.Errorf("exp E: %w", err)
|
|
|
|
|
}
|
|
|
|
|
z, err := strToZq(w.Z, zq)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return zkp.ExponentiationProof{}, fmt.Errorf("exp Z: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return zkp.ExponentiationProof{E: e, Z: z}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 13:47:53 +00:00
|
|
|
// --- plaintext-equality proof (E scalar + Z vector of 2) ---
|
|
|
|
|
|
|
|
|
|
type wirePlaintextEquality struct {
|
|
|
|
|
E string `json:"e"`
|
|
|
|
|
Z []string `json:"z"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func encodePlaintextEquality(p zkp.PlaintextEqualityProof) wirePlaintextEquality {
|
|
|
|
|
return wirePlaintextEquality{E: zqToStr(p.E), Z: zqVecToStrs(p.Z)}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (w wirePlaintextEquality) decode(zq *emath.ZqGroup) (zkp.PlaintextEqualityProof, error) {
|
|
|
|
|
e, err := strToZq(w.E, zq)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return zkp.PlaintextEqualityProof{}, fmt.Errorf("pe E: %w", err)
|
|
|
|
|
}
|
|
|
|
|
z, err := strsToZqVec(w.Z, zq)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return zkp.PlaintextEqualityProof{}, fmt.Errorf("pe Z: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if z.Size() != 2 {
|
|
|
|
|
return zkp.PlaintextEqualityProof{}, fmt.Errorf("pe Z must have size 2, got %d", z.Size())
|
|
|
|
|
}
|
|
|
|
|
return zkp.PlaintextEqualityProof{E: e, Z: z}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 13:11:34 +00:00
|
|
|
// --- ciphertext ---
|
|
|
|
|
|
|
|
|
|
type wireCiphertext struct {
|
|
|
|
|
Gamma string `json:"gamma"`
|
|
|
|
|
Phis []string `json:"phis"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func encodeCiphertext(ct elgamal.Ciphertext) wireCiphertext {
|
|
|
|
|
return wireCiphertext{Gamma: gqToStr(ct.Gamma), Phis: gqVecToStrs(ct.Phis)}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (w wireCiphertext) decode(group *emath.GqGroup) (elgamal.Ciphertext, error) {
|
|
|
|
|
gamma, err := strToGq(w.Gamma, group)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return elgamal.Ciphertext{}, fmt.Errorf("ciphertext gamma: %w", err)
|
|
|
|
|
}
|
|
|
|
|
phis, err := strsToGqVec(w.Phis, group)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return elgamal.Ciphertext{}, fmt.Errorf("ciphertext phis: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return elgamal.NewCiphertext(gamma, phis), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- ciphertext vector ---
|
|
|
|
|
|
|
|
|
|
type wireCiphertextVector struct {
|
|
|
|
|
Cts []wireCiphertext `json:"cts"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func encodeCiphertextVector(v *elgamal.CiphertextVector) wireCiphertextVector {
|
|
|
|
|
out := make([]wireCiphertext, v.Size())
|
|
|
|
|
for i := 0; i < v.Size(); i++ {
|
|
|
|
|
out[i] = encodeCiphertext(v.Get(i))
|
|
|
|
|
}
|
|
|
|
|
return wireCiphertextVector{Cts: out}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (w wireCiphertextVector) decode(group *emath.GqGroup) (*elgamal.CiphertextVector, error) {
|
|
|
|
|
cts := make([]elgamal.Ciphertext, len(w.Cts))
|
|
|
|
|
for i, wc := range w.Cts {
|
|
|
|
|
ct, err := wc.decode(group)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("ciphertext %d: %w", i, err)
|
|
|
|
|
}
|
|
|
|
|
cts[i] = ct
|
|
|
|
|
}
|
|
|
|
|
return elgamal.NewCiphertextVector(cts), nil
|
|
|
|
|
}
|