Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

zk/dleq: Adding DLEQ proofs for Qn, the subgroup of squares in (Z/nZ)* #451

Merged
merged 1 commit into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Alternatively, look at the [Cloudflare Go](https://github.com/cloudflare/go/tree

- [Schnorr](./zk/dl): Prove knowledge of the Discrete Logarithm. ([RFC-8235])
- [DLEQ](./zk/dleq): Prove knowledge of the Discrete Logarithm Equality. ([RFC-9497])

- [DLEQ in Qn](./zk/qndleq): Prove knowledge of the Discrete Logarithm Equality for subgroup of squares in (Z/nZ)*.

### Symmetric Cryptography

Expand Down
133 changes: 133 additions & 0 deletions zk/qndleq/qndleq.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Package qndleq provides zero-knowledge proofs of Discrete-Logarithm Equivalence (DLEQ) on Qn.
//
// This package implements proofs on the group Qn (the subgroup of squares in (Z/nZ)*).
//
// # Notation
//
// Z/nZ is the ring of integers modulo N.
// (Z/nZ)* is the multiplicative group of Z/nZ, a.k.a. the units of Z/nZ, the elements with inverse mod N.
// Qn is the subgroup of squares in (Z/nZ)*.
//
// A number x belongs to Qn if
//
// gcd(x, N) = 1, and
// exists y such that x = y^2 mod N.
//
// # References
//
// [DLEQ Proof] "Wallet databases with observers" by Chaum-Pedersen.
// https://doi.org/10.1007/3-540-48071-4_7
//
// [Qn] "Practical Threshold Signatures" by Shoup.
// https://www.iacr.org/archive/eurocrypt2000/1807/18070209-new.pdf
package qndleq

import (
"crypto/rand"
"io"
"math/big"

"github.com/cloudflare/circl/internal/sha3"
)

type Proof struct {
Z, C *big.Int
SecParam uint
}

// SampleQn returns an element of Qn (the subgroup of squares in (Z/nZ)*).
// SampleQn will return error for any error returned by crypto/rand.Int.
func SampleQn(random io.Reader, N *big.Int) (*big.Int, error) {
bwesterb marked this conversation as resolved.
Show resolved Hide resolved
one := big.NewInt(1)
gcd := new(big.Int)
x := new(big.Int)

for {
y, err := rand.Int(random, N)
if err != nil {
return nil, err
}
// x is a square by construction.
x.Mul(y, y).Mod(x, N)
gcd.GCD(nil, nil, x, N)
// now check whether h is coprime to N.
if gcd.Cmp(one) == 0 {
return x, nil
}
}
}

// Prove creates a DLEQ Proof that attests that the pairs (g,gx)
// and (h,hx) have the same discrete logarithm equal to x.
//
// Given g, h in Qn (the subgroup of squares in (Z/nZ)*), it holds
//
// gx = g^x mod N
// hx = h^x mod N
// x = Log_g(g^x) = Log_h(h^x)
//
// 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) {
armfazh marked this conversation as resolved.
Show resolved Hide resolved
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)

c := doChallenge(g, gx, h, hx, gP, hP, N, secParam)
z := new(big.Int)
z.Mul(c, x).Add(z, r)

return &Proof{Z: z, C: c, SecParam: secParam}, nil
}

// Verify checks whether x = Log_g(g^x) = Log_h(h^x).
func (p Proof) Verify(g, gx, h, hx, N *big.Int) bool {
gPNum := new(big.Int).Exp(g, p.Z, N)
gPDen := new(big.Int).Exp(gx, p.C, N)
ok := gPDen.ModInverse(gPDen, N)
if ok == nil {
return false
}
gP := gPNum.Mul(gPNum, gPDen)
gP.Mod(gP, N)

hPNum := new(big.Int).Exp(h, p.Z, N)
hPDen := new(big.Int).Exp(hx, p.C, N)
ok = hPDen.ModInverse(hPDen, N)
if ok == nil {
return false
}
hP := hPNum.Mul(hPNum, hPDen)
hP.Mod(hP, N)

c := doChallenge(g, gx, h, hx, gP, hP, N, p.SecParam)

return p.C.Cmp(c) == 0
}

func doChallenge(g, gx, h, hx, gP, hP, N *big.Int, secParam uint) *big.Int {
modulusLenBytes := (N.BitLen() + 7) / 8
nBytes := make([]byte, modulusLenBytes)
cByteLen := (secParam + 7) / 8
cBytes := make([]byte, cByteLen)

H := sha3.NewShake256()
_, _ = H.Write(g.FillBytes(nBytes))
_, _ = H.Write(h.FillBytes(nBytes))
_, _ = H.Write(gx.FillBytes(nBytes))
_, _ = H.Write(hx.FillBytes(nBytes))
_, _ = H.Write(gP.FillBytes(nBytes))
_, _ = H.Write(hP.FillBytes(nBytes))
_, _ = H.Read(cBytes)

return new(big.Int).SetBytes(cBytes)
}
84 changes: 84 additions & 0 deletions zk/qndleq/qndleq_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package qndleq_test

import (
"crypto/rand"
"math/big"
"testing"

"github.com/cloudflare/circl/internal/test"
"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)

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")
h, err := qndleq.SampleQn(rand.Reader, N)
test.CheckNoErr(t, err, "failed to sampleQn")
gx := new(big.Int).Exp(g, x, N)
hx := new(big.Int).Exp(h, x, N)

proof, err := qndleq.Prove(rand.Reader, x, g, gx, h, hx, N, SecParam)
test.CheckNoErr(t, err, "failed to generate proof")
test.CheckOk(proof.Verify(g, gx, h, hx, N), "failed to verify", t)
}
}

func TestSampleQn(t *testing.T) {
const testTimes = 1 << 7
one := big.NewInt(1)
max := new(big.Int).Lsh(one, 256)

for i := 0; i < testTimes; i++ {
N, _ := rand.Int(rand.Reader, max)
if N.Bit(0) == 0 {
N.Add(N, one)
}
a, err := qndleq.SampleQn(rand.Reader, N)
test.CheckNoErr(t, err, "failed to sampleQn")
jac := big.Jacobi(a, N)
test.CheckOk(jac == 1, "Jacoby symbol should be one", t)
gcd := new(big.Int).GCD(nil, nil, a, N)
test.CheckOk(gcd.Cmp(one) == 0, "should be coprime to N", t)
}
}

func Benchmark_qndleq(b *testing.B) {
const SecParam = 128
one := big.NewInt(1)
max := new(big.Int).Lsh(one, 256)

N, _ := rand.Int(rand.Reader, max)
if N.Bit(0) == 0 {
N.Add(N, one)
}
x, _ := rand.Int(rand.Reader, N)
g, _ := qndleq.SampleQn(rand.Reader, N)
h, _ := qndleq.SampleQn(rand.Reader, N)
gx := new(big.Int).Exp(g, x, N)
hx := new(big.Int).Exp(h, x, N)

proof, _ := qndleq.Prove(rand.Reader, x, g, gx, h, hx, N, SecParam)

b.Run("Prove", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = qndleq.Prove(rand.Reader, x, g, gx, h, hx, N, SecParam)
}
})

b.Run("Verify", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = proof.Verify(g, gx, h, hx, N)
}
})
}