From b9450b749a207ae4245b35be8b1d47ac6b12326b Mon Sep 17 00:00:00 2001 From: Armando Faz Date: Wed, 15 Apr 2026 16:56:35 -0700 Subject: [PATCH 1/3] zk/qndleq: Fix challenge in qndleq. --- zk/qndleq/internal_test.go | 21 +++++++++++++++++++++ zk/qndleq/qndleq.go | 27 ++++++++++++++++++++++++--- zk/qndleq/qndleq_test.go | 12 ++++++++++++ 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/zk/qndleq/internal_test.go b/zk/qndleq/internal_test.go index 693814923..fc5ba6dfc 100644 --- a/zk/qndleq/internal_test.go +++ b/zk/qndleq/internal_test.go @@ -49,3 +49,24 @@ func TestForgedProofSecParamZero(t *testing.T) { test.CheckOk(!forged.Verify(g, gx, h, hx, N), "forged proof must be rejected", t) } + +func TestChallenge(t *testing.T) { + g, gx := big.NewInt(4), big.NewInt(16) + h, hx := big.NewInt(9), big.NewInt(81) + gP := big.NewInt(50) + hP := big.NewInt(60) + N := big.NewInt(101) + + invalidValues := []*big.Int{ + new(big.Int).Neg(g), // Negative + big.NewInt(0), // Zero + new(big.Int).Set(N), // N + new(big.Int).Add(N, N), // bigger than N + } + + for _, invalidValue := range invalidValues { + c, err := doChallenge(invalidValue, gx, h, hx, gP, hP, N, 128) + test.CheckIsErr(t, err, "doChallenge must fail") + test.CheckOk(c == nil, "challenge must be nil", t) + } +} diff --git a/zk/qndleq/qndleq.go b/zk/qndleq/qndleq.go index bafb5d733..80024d713 100644 --- a/zk/qndleq/qndleq.go +++ b/zk/qndleq/qndleq.go @@ -127,13 +127,18 @@ func doChallenge(g, gx, h, hx, gP, hP, N *big.Int, secParam uint) (*big.Int, err return nil, ErrSecParam } + err := checkBounds(N, g, gx, h, hx, gP, hP) + if err != nil { + return nil, err + } + modulusLenBytes := (N.BitLen() + 7) / 8 nBytes := make([]byte, modulusLenBytes) cByteLen := (secParam + 7) / 8 cBytes := make([]byte, cByteLen) H := sha3.NewShake256() - _, err := H.Write(g.FillBytes(nBytes)) + _, err = H.Write(g.FillBytes(nBytes)) if err != nil { return nil, err } @@ -171,5 +176,21 @@ func doChallenge(g, gx, h, hx, gP, hP, N *big.Int, secParam uint) (*big.Int, err return new(big.Int).SetBytes(cBytes), nil } -// ErrSecParam is returned when the security parameter is less than 128. -var ErrSecParam = errors.New("zk/qndleq: the security parameter must be greater than 128") +// checkBounds returns nil if 0 < x[i] < N for all 0 <= i < len(x); +// otherwise, returns ErrBounds. +func checkBounds(N *big.Int, x ...*big.Int) error { + for _, xi := range x { + if !(0 < xi.Sign() && xi.Cmp(N) < 0) { + return ErrBounds + } + } + + return nil +} + +var ( + // ErrSecParam is returned when the security parameter is less than 128. + ErrSecParam = errors.New("zk/qndleq: the security parameter must be greater than 128") + // ErrBounds is returned when a value is not in the range 0 to N. + ErrBounds = errors.New("zk/qndleq: input must be greater than 0 and less than N") +) diff --git a/zk/qndleq/qndleq_test.go b/zk/qndleq/qndleq_test.go index a8af777c2..2ec8bdea2 100644 --- a/zk/qndleq/qndleq_test.go +++ b/zk/qndleq/qndleq_test.go @@ -34,6 +34,18 @@ func TestProve(t *testing.T) { } } +func TestInvalidStatement(t *testing.T) { + g, gx := big.NewInt(4), big.NewInt(16) // 4^2 == 16 mod 101 + h, hx := big.NewInt(9), big.NewInt(81) // 9^2 == 81 mod 101 + N := big.NewInt(101) + incorrectX := big.NewInt(3) + + p, err := qndleq.Prove(rand.Reader, incorrectX, g, gx, h, hx, N, 128) + test.CheckNoErr(t, err, "an alleged proof must be computed") + isValid := p.Verify(g, gx, h, hx, N) + test.CheckOk(isValid == false, "proof verification must fail", t) +} + func TestSampleQn(t *testing.T) { const testTimes = 1 << 7 one := big.NewInt(1) From 6253769c71a97426e2fd42dba7a2a481977cb7f3 Mon Sep 17 00:00:00 2001 From: Armando Faz Date: Thu, 16 Apr 2026 15:03:38 -0700 Subject: [PATCH 2/3] zk/qndleq: Check challenge is not congruent to zero. --- zk/qndleq/internal_test.go | 25 ++++++++++++++-- zk/qndleq/qndleq.go | 59 ++++++++++++++++++++++++++++---------- zk/qndleq/qndleq_test.go | 12 ++++---- 3 files changed, 73 insertions(+), 23 deletions(-) diff --git a/zk/qndleq/internal_test.go b/zk/qndleq/internal_test.go index fc5ba6dfc..01e52374b 100644 --- a/zk/qndleq/internal_test.go +++ b/zk/qndleq/internal_test.go @@ -9,8 +9,8 @@ import ( ) func TestForgedProofSecParamZero(t *testing.T) { - p := big.NewInt(1019) - q := big.NewInt(1021) + // Safe primes: https://oeis.org/A005385 + p, q := big.NewInt(1019), big.NewInt(1187) N := new(big.Int).Mul(p, q) g, err := SampleQn(rand.Reader, N) @@ -51,11 +51,14 @@ func TestForgedProofSecParamZero(t *testing.T) { } func TestChallenge(t *testing.T) { + // Safe primes: https://oeis.org/A005385 + p, q := big.NewInt(1019), big.NewInt(1187) + N := new(big.Int).Mul(p, q) + g, gx := big.NewInt(4), big.NewInt(16) h, hx := big.NewInt(9), big.NewInt(81) gP := big.NewInt(50) hP := big.NewInt(60) - N := big.NewInt(101) invalidValues := []*big.Int{ new(big.Int).Neg(g), // Negative @@ -70,3 +73,19 @@ func TestChallenge(t *testing.T) { test.CheckOk(c == nil, "challenge must be nil", t) } } + +func TestChallengeZero(t *testing.T) { + // Safe primes: https://oeis.org/A005385 + p, q := big.NewInt(1019), big.NewInt(1187) + N := new(big.Int).Mul(p, q) + g, gx := big.NewInt(4), big.NewInt(16) // 4^2 == 16 mod 101 + h, hx := big.NewInt(9), big.NewInt(81) // 9^2 == 81 mod 101 + + // Proof must fail as challenge is congruent to zero modulo m = (p-1)(q-1)/4. + c, _ := new(big.Int).SetString("325783150686773390995072744979517913979", 10) + z, _ := new(big.Int).SetString("909208770437996720153744987183938443953758507571077689625761350579371458087594451128", 10) + invalidProof := Proof{z, c, 128} + + isValid := invalidProof.Verify(g, gx, h, hx, N) + test.CheckOk(isValid == false, "proof verification must fail", t) +} diff --git a/zk/qndleq/qndleq.go b/zk/qndleq/qndleq.go index 80024d713..964261d0e 100644 --- a/zk/qndleq/qndleq.go +++ b/zk/qndleq/qndleq.go @@ -72,30 +72,57 @@ func SampleQn(random io.Reader, N *big.Int) (*big.Int, error) { func Prove(random io.Reader, x, g, gx, h, hx, N *big.Int, secParam uint) (*Proof, error) { rSizeBits := uint(N.BitLen()) + 2*secParam rSizeBytes := (rSizeBits + 7) / 8 - rBytes := make([]byte, rSizeBytes) - _, err := io.ReadFull(random, rBytes) - if err != nil { - return nil, err - } - r := new(big.Int).SetBytes(rBytes) - gP := new(big.Int).Exp(g, r, N) - hP := new(big.Int).Exp(h, r, N) + ONE := big.NewInt(1) + NUM_TRIES := 10 + var r, gP, hP, gc, hc big.Int + for i := 0; i < NUM_TRIES; i++ { + _, err := io.ReadFull(random, rBytes) + if err != nil { + return nil, err + } - c, err := doChallenge(g, gx, h, hx, gP, hP, N, secParam) - if err != nil { - return nil, err - } + r.SetBytes(rBytes) + gP.Exp(g, &r, N) + hP.Exp(h, &r, N) - z := new(big.Int) - z.Mul(c, x).Add(z, r) + c, err := doChallenge(g, gx, h, hx, &gP, &hP, N, secParam) + if err != nil { + return nil, err + } - return &Proof{z, c, secParam}, nil + // Challenge must not be congruent to zero. + // c != 0 mod m, where m = (p-1)(q-1)/4, and N = p*q. + // Check this by doing an Exp because m is unknown. + // + // This is valid assuming N is a power of two safe prime numbers. + // In the verification equation, c multiplies the witness. + // When c is zero, it removes the witness allowing to trivially + // pass the verification check. + gc.Exp(g, c, N) + hc.Exp(h, c, N) + if gc.Cmp(ONE) != 0 && hc.Cmp(ONE) != 0 { + z := new(big.Int).Mul(c, x) + z.Add(z, &r) + return &Proof{z, c, secParam}, nil + } + } + + return nil, ErrProve } // Verify checks whether x = Log_g(g^x) = Log_h(h^x). func (p Proof) Verify(g, gx, h, hx, N *big.Int) bool { + // Check c != 0 (mod m), where m = (p-1)(q-1)/4, + // by doing an Exp as m is unknown. + ONE := big.NewInt(1) + gc := new(big.Int).Exp(g, p.c, N) + hc := new(big.Int).Exp(h, p.c, N) + if gc.Cmp(ONE) == 0 || hc.Cmp(ONE) == 0 { + return false + } + gPNum := new(big.Int).Exp(g, p.z, N) gPDen := new(big.Int).Exp(gx, p.c, N) ok := gPDen.ModInverse(gPDen, N) @@ -193,4 +220,6 @@ var ( ErrSecParam = errors.New("zk/qndleq: the security parameter must be greater than 128") // ErrBounds is returned when a value is not in the range 0 to N. ErrBounds = errors.New("zk/qndleq: input must be greater than 0 and less than N") + // ErrProve is returned when Prove cannot produce a proof. + ErrProve = errors.New("zk/qndleq: Prove cannot produce a proof") ) diff --git a/zk/qndleq/qndleq_test.go b/zk/qndleq/qndleq_test.go index 2ec8bdea2..86a7d5b39 100644 --- a/zk/qndleq/qndleq_test.go +++ b/zk/qndleq/qndleq_test.go @@ -35,14 +35,16 @@ func TestProve(t *testing.T) { } func TestInvalidStatement(t *testing.T) { - g, gx := big.NewInt(4), big.NewInt(16) // 4^2 == 16 mod 101 - h, hx := big.NewInt(9), big.NewInt(81) // 9^2 == 81 mod 101 - N := big.NewInt(101) + // Safe primes: https://oeis.org/A005385 + p, q := big.NewInt(1019), big.NewInt(1187) + N := new(big.Int).Mul(p, q) + g, gx := big.NewInt(4), big.NewInt(16) // 4^2 == 16 + h, hx := big.NewInt(9), big.NewInt(81) // 9^2 == 81 incorrectX := big.NewInt(3) - p, err := qndleq.Prove(rand.Reader, incorrectX, g, gx, h, hx, N, 128) + proof, err := qndleq.Prove(rand.Reader, incorrectX, g, gx, h, hx, N, 128) test.CheckNoErr(t, err, "an alleged proof must be computed") - isValid := p.Verify(g, gx, h, hx, N) + isValid := proof.Verify(g, gx, h, hx, N) test.CheckOk(isValid == false, "proof verification must fail", t) } From 1a0623d5621eead6a7107ffc4c3206a22f0312ee Mon Sep 17 00:00:00 2001 From: Armando Faz Date: Tue, 21 Apr 2026 10:10:25 -0700 Subject: [PATCH 3/3] zk/qndleq: Sample safe primes in tests. --- zk/qndleq/internal_test.go | 34 +++++++++++++++++++++++----------- zk/qndleq/qndleq.go | 29 +++++++++++++++++++---------- zk/qndleq/qndleq_test.go | 24 ++++++++++++++---------- 3 files changed, 56 insertions(+), 31 deletions(-) diff --git a/zk/qndleq/internal_test.go b/zk/qndleq/internal_test.go index 01e52374b..3ce172a98 100644 --- a/zk/qndleq/internal_test.go +++ b/zk/qndleq/internal_test.go @@ -50,15 +50,14 @@ func TestForgedProofSecParamZero(t *testing.T) { test.CheckOk(!forged.Verify(g, gx, h, hx, N), "forged proof must be rejected", t) } -func TestChallenge(t *testing.T) { +func TestOutOfBounds(t *testing.T) { // Safe primes: https://oeis.org/A005385 p, q := big.NewInt(1019), big.NewInt(1187) N := new(big.Int).Mul(p, q) + x := big.NewInt(2) g, gx := big.NewInt(4), big.NewInt(16) h, hx := big.NewInt(9), big.NewInt(81) - gP := big.NewInt(50) - hP := big.NewInt(60) invalidValues := []*big.Int{ new(big.Int).Neg(g), // Negative @@ -67,21 +66,34 @@ func TestChallenge(t *testing.T) { new(big.Int).Add(N, N), // bigger than N } - for _, invalidValue := range invalidValues { - c, err := doChallenge(invalidValue, gx, h, hx, gP, hP, N, 128) - test.CheckIsErr(t, err, "doChallenge must fail") - test.CheckOk(c == nil, "challenge must be nil", t) - } + t.Run("prove", func(t *testing.T) { + for _, invalidValue := range invalidValues { + p, err := Prove(rand.Reader, x, invalidValue, gx, h, hx, N, 128) + test.CheckIsErr(t, err, "Prove must fail") + test.CheckOk(p == nil, "proof must be nil", t) + } + }) + + t.Run("verify", func(t *testing.T) { + for _, invalidValue := range invalidValues { + p, err := Prove(rand.Reader, x, g, gx, h, hx, N, 128) + test.CheckNoErr(t, err, "Prove must succeed") + + isValid := p.Verify(invalidValue, gx, h, hx, N) + test.CheckOk(isValid == false, "proof verification must return false", t) + } + }) } func TestChallengeZero(t *testing.T) { // Safe primes: https://oeis.org/A005385 p, q := big.NewInt(1019), big.NewInt(1187) N := new(big.Int).Mul(p, q) - g, gx := big.NewInt(4), big.NewInt(16) // 4^2 == 16 mod 101 - h, hx := big.NewInt(9), big.NewInt(81) // 9^2 == 81 mod 101 + g, gx := big.NewInt(4), big.NewInt(16) // 4^2 == 16 mod N + h, hx := big.NewInt(9), big.NewInt(81) // 9^2 == 81 mod N - // Proof must fail as challenge is congruent to zero modulo m = (p-1)(q-1)/4. + // Proof must fail as challenge is congruent to zero modulo m = (p-1)(q-1)/4 = 509*593. + // c = m * 1079334709418571583321702591065767 c, _ := new(big.Int).SetString("325783150686773390995072744979517913979", 10) z, _ := new(big.Int).SetString("909208770437996720153744987183938443953758507571077689625761350579371458087594451128", 10) invalidProof := Proof{z, c, 128} diff --git a/zk/qndleq/qndleq.go b/zk/qndleq/qndleq.go index 964261d0e..446d223b3 100644 --- a/zk/qndleq/qndleq.go +++ b/zk/qndleq/qndleq.go @@ -70,12 +70,17 @@ func SampleQn(random io.Reader, N *big.Int) (*big.Int, error) { // Note: this function does not run in constant time because it uses // big.Int arithmetic. func Prove(random io.Reader, x, g, gx, h, hx, N *big.Int, secParam uint) (*Proof, error) { + err := checkBounds(N, g, gx, h, hx) + if err != nil { + return nil, err + } + rSizeBits := uint(N.BitLen()) + 2*secParam rSizeBytes := (rSizeBits + 7) / 8 rBytes := make([]byte, rSizeBytes) ONE := big.NewInt(1) - NUM_TRIES := 10 + const NUM_TRIES = 10 var r, gP, hP, gc, hc big.Int for i := 0; i < NUM_TRIES; i++ { _, err := io.ReadFull(random, rBytes) @@ -96,7 +101,7 @@ func Prove(random io.Reader, x, g, gx, h, hx, N *big.Int, secParam uint) (*Proof // c != 0 mod m, where m = (p-1)(q-1)/4, and N = p*q. // Check this by doing an Exp because m is unknown. // - // This is valid assuming N is a power of two safe prime numbers. + // This is valid assuming N is the product of two safe prime numbers. // In the verification equation, c multiplies the witness. // When c is zero, it removes the witness allowing to trivially // pass the verification check. @@ -114,6 +119,11 @@ func Prove(random io.Reader, x, g, gx, h, hx, N *big.Int, secParam uint) (*Proof // Verify checks whether x = Log_g(g^x) = Log_h(h^x). func (p Proof) Verify(g, gx, h, hx, N *big.Int) bool { + err := checkBounds(N, g, gx, h, hx) + if err != nil { + return false + } + // Check c != 0 (mod m), where m = (p-1)(q-1)/4, // by doing an Exp as m is unknown. ONE := big.NewInt(1) @@ -154,18 +164,13 @@ func doChallenge(g, gx, h, hx, gP, hP, N *big.Int, secParam uint) (*big.Int, err return nil, ErrSecParam } - err := checkBounds(N, g, gx, h, hx, gP, hP) - if err != nil { - return nil, err - } - modulusLenBytes := (N.BitLen() + 7) / 8 nBytes := make([]byte, modulusLenBytes) cByteLen := (secParam + 7) / 8 cBytes := make([]byte, cByteLen) H := sha3.NewShake256() - _, err = H.Write(g.FillBytes(nBytes)) + _, err := H.Write(g.FillBytes(nBytes)) if err != nil { return nil, err } @@ -206,6 +211,10 @@ func doChallenge(g, gx, h, hx, gP, hP, N *big.Int, secParam uint) (*big.Int, err // checkBounds returns nil if 0 < x[i] < N for all 0 <= i < len(x); // otherwise, returns ErrBounds. func checkBounds(N *big.Int, x ...*big.Int) error { + if N.Sign() <= 0 { + return ErrBounds + } + for _, xi := range x { if !(0 < xi.Sign() && xi.Cmp(N) < 0) { return ErrBounds @@ -220,6 +229,6 @@ var ( ErrSecParam = errors.New("zk/qndleq: the security parameter must be greater than 128") // ErrBounds is returned when a value is not in the range 0 to N. ErrBounds = errors.New("zk/qndleq: input must be greater than 0 and less than N") - // ErrProve is returned when Prove cannot produce a proof. - ErrProve = errors.New("zk/qndleq: Prove cannot produce a proof") + // ErrProve is returned when Prove exhausted the number of proof tries. + ErrProve = errors.New("zk/qndleq: exhausted the number of proof tries") ) diff --git a/zk/qndleq/qndleq_test.go b/zk/qndleq/qndleq_test.go index 86a7d5b39..4eb9096d5 100644 --- a/zk/qndleq/qndleq_test.go +++ b/zk/qndleq/qndleq_test.go @@ -6,20 +6,24 @@ import ( "testing" "github.com/cloudflare/circl/internal/test" + cmath "github.com/cloudflare/circl/math" "github.com/cloudflare/circl/zk/qndleq" ) func TestProve(t *testing.T) { - const testTimes = 1 << 8 - const SecParam = 128 - one := big.NewInt(1) - max := new(big.Int).Lsh(one, 256) + const ( + testTimes = 1 << 8 + SecParam = 128 + BitLength = 128 // [Warning]: this is only for tests, use a secure bit length above 2048 bits. + ) + + p, err := cmath.SafePrime(rand.Reader, BitLength) + test.CheckNoErr(t, err, "failed to generate a safe prime") + q, err := cmath.SafePrime(rand.Reader, BitLength) + test.CheckNoErr(t, err, "failed to generate a safe prime") + N := new(big.Int).Mul(p, q) for i := 0; i < testTimes; i++ { - N, _ := rand.Int(rand.Reader, max) - if N.Bit(0) == 0 { - N.Add(N, one) - } x, _ := rand.Int(rand.Reader, N) g, err := qndleq.SampleQn(rand.Reader, N) test.CheckNoErr(t, err, "failed to sampleQn") @@ -38,8 +42,8 @@ func TestInvalidStatement(t *testing.T) { // Safe primes: https://oeis.org/A005385 p, q := big.NewInt(1019), big.NewInt(1187) N := new(big.Int).Mul(p, q) - g, gx := big.NewInt(4), big.NewInt(16) // 4^2 == 16 - h, hx := big.NewInt(9), big.NewInt(81) // 9^2 == 81 + g, gx := big.NewInt(4), big.NewInt(16) // 4^2 == 16 mod N + h, hx := big.NewInt(9), big.NewInt(81) // 9^2 == 81 mod N incorrectX := big.NewInt(3) proof, err := qndleq.Prove(rand.Reader, incorrectX, g, gx, h, hx, N, 128)