diff --git a/README.md b/README.md index 1f24168ad..12ae6dbe8 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/zk/qndleq/qndleq.go b/zk/qndleq/qndleq.go new file mode 100644 index 000000000..c5a1cbc4c --- /dev/null +++ b/zk/qndleq/qndleq.go @@ -0,0 +1,143 @@ +// 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) { + 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) { + 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 mustWrite(w *sha3.State, b []byte) { + n, _ := w.Write(b) + if len(b) != n { + panic("qndleq: failed to write on hash") + } +} + +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() + mustWrite(&H, g.FillBytes(nBytes)) + mustWrite(&H, h.FillBytes(nBytes)) + mustWrite(&H, gx.FillBytes(nBytes)) + mustWrite(&H, hx.FillBytes(nBytes)) + mustWrite(&H, gP.FillBytes(nBytes)) + mustWrite(&H, hP.FillBytes(nBytes)) + _, err := H.Read(cBytes) + if err != nil { + panic(err) + } + + return new(big.Int).SetBytes(cBytes) +} diff --git a/zk/qndleq/qndleq_test.go b/zk/qndleq/qndleq_test.go new file mode 100644 index 000000000..a8af777c2 --- /dev/null +++ b/zk/qndleq/qndleq_test.go @@ -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) + } + }) +}