2026-02-13 18:53:09 +00:00
|
|
|
package kdf
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"crypto/sha256"
|
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
|
|
|
"encoding/binary"
|
2026-02-13 18:53:09 +00:00
|
|
|
"io"
|
|
|
|
|
"math/big"
|
|
|
|
|
|
|
|
|
|
"golang.org/x/crypto/argon2"
|
|
|
|
|
"golang.org/x/crypto/hkdf"
|
|
|
|
|
|
|
|
|
|
emath "github.com/user/evote/pkg/math"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// DeriveKey derives a key using HKDF-SHA256.
|
|
|
|
|
func DeriveKey(prk, info []byte, length int) []byte {
|
|
|
|
|
reader := hkdf.Expand(sha256.New, prk, info)
|
|
|
|
|
key := make([]byte, length)
|
|
|
|
|
_, err := io.ReadFull(reader, key)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic("HKDF expand failed: " + err.Error())
|
|
|
|
|
}
|
|
|
|
|
return key
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// KDFToZq derives a Z_q element using HKDF-SHA256.
|
|
|
|
|
// PRK is the pseudorandom key, info is the context info, q is the modulus.
|
|
|
|
|
func KDFToZq(prk []byte, info []byte, q *big.Int) *big.Int {
|
|
|
|
|
// Derive enough bytes: ceil(q.BitLen() / 8) + extra for uniformity
|
|
|
|
|
byteLen := (q.BitLen()+7)/8 + 16 // extra 16 bytes for rejection sampling avoidance
|
|
|
|
|
derived := DeriveKey(prk, info, byteLen)
|
|
|
|
|
val := new(big.Int).SetBytes(derived)
|
|
|
|
|
return val.Mod(val, q)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// KDFToZqElement derives a ZqElement using HKDF-SHA256.
|
|
|
|
|
func KDFToZqElement(prk []byte, info []byte, group *emath.ZqGroup) emath.ZqElement {
|
|
|
|
|
val := KDFToZq(prk, info, group.Q())
|
|
|
|
|
e, err := emath.NewZqElement(val, group)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic("KDFToZqElement: " + err.Error())
|
|
|
|
|
}
|
|
|
|
|
return e
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BuildKDFInfo builds a KDF info string from label and context parts.
|
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
|
|
|
// Each part is length-prefixed (4-byte big-endian) so the encoding is
|
|
|
|
|
// injective: distinct part tuples can never collide into the same info
|
|
|
|
|
// string (e.g. ("e1","23x") and ("e12","3x") must derive different keys).
|
2026-02-13 18:53:09 +00:00
|
|
|
func BuildKDFInfo(parts ...string) []byte {
|
|
|
|
|
var info []byte
|
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
|
|
|
var lenBuf [4]byte
|
2026-02-13 18:53:09 +00:00
|
|
|
for _, p := range parts {
|
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
|
|
|
binary.BigEndian.PutUint32(lenBuf[:], uint32(len(p)))
|
|
|
|
|
info = append(info, lenBuf[:]...)
|
2026-02-13 18:53:09 +00:00
|
|
|
info = append(info, []byte(p)...)
|
|
|
|
|
}
|
|
|
|
|
return info
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Argon2id derives a key using Argon2id.
|
|
|
|
|
func Argon2id(password, salt []byte, time, memory uint32, threads uint8, keyLen uint32) []byte {
|
|
|
|
|
return argon2.IDKey(password, salt, time, memory, threads, keyLen)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DefaultArgon2id uses the default parameters from the Swiss Post e-voting system.
|
|
|
|
|
func DefaultArgon2id(password, salt []byte) []byte {
|
|
|
|
|
// Typical parameters from the protocol
|
|
|
|
|
return Argon2id(password, salt, 3, 64*1024, 4, 32)
|
|
|
|
|
}
|