From 197eed8935efd3f7349ef96a1b9ed1adc2fc1a2f Mon Sep 17 00:00:00 2001 From: armfazh Date: Thu, 17 Jul 2025 15:56:51 -0700 Subject: [PATCH] Adding ARC implementation. Compliant with https://datatracker.ietf.org/doc/draft-yun-privacypass-crypto-arc-00 --- ac/arc/arc_test.go | 106 +++++++++++ ac/arc/builder.go | 216 ++++++++++++++++++++++ ac/arc/credential.go | 239 ++++++++++++++++++++++++ ac/arc/doc.go | 55 ++++++ ac/arc/example_test.go | 85 +++++++++ ac/arc/keys.go | 119 ++++++++++++ ac/arc/presentation.go | 227 +++++++++++++++++++++++ ac/arc/proofs.go | 192 +++++++++++++++++++ ac/arc/suite.go | 147 +++++++++++++++ ac/arc/testdata/draft_v01.json.gz | Bin 0 -> 2713 bytes ac/arc/vectors_test.go | 296 ++++++++++++++++++++++++++++++ ac/doc.go | 2 + internal/conv/conv.go | 22 +++ 13 files changed, 1706 insertions(+) create mode 100644 ac/arc/arc_test.go create mode 100644 ac/arc/builder.go create mode 100644 ac/arc/credential.go create mode 100644 ac/arc/doc.go create mode 100644 ac/arc/example_test.go create mode 100644 ac/arc/keys.go create mode 100644 ac/arc/presentation.go create mode 100644 ac/arc/proofs.go create mode 100644 ac/arc/suite.go create mode 100644 ac/arc/testdata/draft_v01.json.gz create mode 100644 ac/arc/vectors_test.go create mode 100644 ac/doc.go diff --git a/ac/arc/arc_test.go b/ac/arc/arc_test.go new file mode 100644 index 00000000..32703fa0 --- /dev/null +++ b/ac/arc/arc_test.go @@ -0,0 +1,106 @@ +package arc + +import ( + "crypto/rand" + "testing" + + "github.com/cloudflare/circl/internal/test" +) + +func TestCompressed(t *testing.T) { + id := SuiteP256 + s := id.getSuite() + p := s.g.Params() + + t.Run("okCompressed", func(t *testing.T) { + z := eltCom{s.g.RandomElement(rand.Reader)} + test.CheckMarshal(t, &z, &eltCom{s.newElement()}) + + enc, err := z.MarshalBinary() + test.CheckNoErr(t, err, "error on marshalling") + test.CheckOk(len(enc) == int(p.CompressedElementLength), "bad length", t) + }) + + t.Run("badCompressed", func(t *testing.T) { + // Skip on groups that do not admit point compression. + if p.CompressedElementLength == p.ElementLength { + t.Skip() + } + + // Fails when unmarshaling non-compressed points. + x := s.g.RandomElement(rand.Reader) + enc, err := x.MarshalBinary() + test.CheckNoErr(t, err, "error on marshalling") + test.CheckOk(uint(len(enc)) == p.ElementLength, "bad length", t) + + xx := eltCom{s.newElement()} + err1 := xx.UnmarshalBinary(enc) + test.CheckIsErr(t, err1, "should fail") + }) +} + +func BenchmarkArc(b *testing.B) { + b.Run(SuiteP256.String(), SuiteP256.benchmarkArc) + b.Run(SuiteRistretto255.String(), SuiteRistretto255.benchmarkArc) +} + +func (id SuiteID) benchmarkArc(b *testing.B) { + reqContext := []byte("Credential for Alice") + presContext := []byte("Presentation for example.com") + priv := KeyGen(rand.Reader, id) + pub := priv.PublicKey() + + fin, credReq := Request(rand.Reader, id, reqContext) + credRes, err := Response(rand.Reader, &priv, &credReq) + test.CheckNoErr(b, err, "failed Response") + + credential, err := Finalize(&fin, &credReq, credRes, &pub) + test.CheckNoErr(b, err, "failed Finalize") + + const MaxPres = 1000 + state, err := NewState(credential, presContext, MaxPres) + test.CheckNoErr(b, err, "failed NewState") + nonce, pres, err := state.Present(rand.Reader) + test.CheckNoErr(b, err, "failed Finalize") + + ok := Verify(&priv, pres, reqContext, presContext, *nonce, MaxPres) + test.CheckOk(ok, "verify failed", b) + + b.Run("KeyGen", func(b *testing.B) { + for range b.N { + k := KeyGen(rand.Reader, id) + _ = k.PublicKey() + } + }) + + b.Run("Request", func(b *testing.B) { + for range b.N { + _, _ = Request(rand.Reader, id, reqContext) + } + }) + + b.Run("Response", func(b *testing.B) { + for range b.N { + _, _ = Response(rand.Reader, &priv, &credReq) + } + }) + + b.Run("Finalize", func(b *testing.B) { + for range b.N { + _, _ = Finalize(&fin, &credReq, credRes, &pub) + } + }) + + b.Run("Present", func(b *testing.B) { + for range b.N { + s, _ := NewState(credential, presContext, MaxPres) + _, _, _ = s.Present(rand.Reader) + } + }) + + b.Run("Verify", func(b *testing.B) { + for range b.N { + _ = Verify(&priv, pres, reqContext, presContext, *nonce, MaxPres) + } + }) +} diff --git a/ac/arc/builder.go b/ac/arc/builder.go new file mode 100644 index 00000000..aa070ae6 --- /dev/null +++ b/ac/arc/builder.go @@ -0,0 +1,216 @@ +package arc + +import ( + "crypto/rand" + "io" + "slices" + + "github.com/cloudflare/circl/internal/conv" + "golang.org/x/crypto/cryptobyte" +) + +type proof struct { + chal scalar + resp []scalar +} + +func (p proof) String() string { return printAny(p.chal, p.resp) } + +func (p *proof) init(s *suite, num uint) { + p.chal = s.newScalar() + p.resp = make([]scalar, num) + for i := range num { + p.resp[i] = s.newScalar() + } +} + +func (p proof) IsEqual(x proof) bool { + return slices.EqualFunc( + append([]scalar{p.chal}, p.resp...), + append([]scalar{x.chal}, x.resp...), + scalar.IsEqual) +} + +func (p proof) Marshal(b *cryptobyte.Builder) error { + v := make([]cryptobyte.MarshalingValue, 0, 1+len(p.resp)) + v = append(v, p.chal) + for i := range p.resp { + v = append(v, p.resp[i]) + } + + return conv.MarshalSlice(b, v...) +} + +func (p proof) Unmarshal(s *cryptobyte.String) bool { + v := make([]conv.UnmarshalingValue, 0, 1+len(p.resp)) + v = append(v, p.chal) + for i := range p.resp { + v = append(v, p.resp[i]) + } + + return conv.UnmarshalSlice(s, v...) +} + +type ( + scalarIndex uint + elemIndex uint + mul struct { + s scalarIndex + e elemIndex + } + constraint struct { + c []mul + z elemIndex + } +) + +type builder struct { + *suite + ctx string + scalarLabels [][]byte + elemLabels [][]byte + elems []elt + cons []constraint +} + +func (b *builder) AppendScalar(label []byte) scalarIndex { + b.scalarLabels = append(b.scalarLabels, label) + return scalarIndex(len(b.scalarLabels) - 1) +} + +func (b *builder) AppendElement(label []byte, e elt) elemIndex { + b.elems = append(b.elems, e) + b.elemLabels = append(b.elemLabels, label) + return elemIndex(len(b.elems) - 1) +} + +func (b *builder) Constrain(z elemIndex, m ...mul) { + b.cons = append(b.cons, constraint{m, z}) +} + +func (b *builder) calcChallenge(elems ...[]elt) scalar { + length := 0 + for i := range elems { + length += len(elems[i]) + } + + sizeElement := b.suite.sizeElement() + length *= 2 + int(sizeElement) + cb := cryptobyte.NewFixedBuilder(make([]byte, 0, length)) + for _, list := range elems { + for i := range list { + cb.AddUint16(uint16(sizeElement)) + _ = eltCom{list[i]}.Marshal(cb) + } + } + + return b.suite.hashToScalar(cb.BytesOrPanic(), b.suite.chalContext(b.ctx)) +} + +type prover struct { + builder + scalars []scalar +} + +func newProver(id SuiteID, ctx string) (p prover) { + p.builder = builder{suite: id.getSuite(), ctx: ctx} + return +} + +func (c *prover) AppendScalar(label []byte, s scalar) scalarIndex { + c.scalars = append(c.scalars, s) + return c.builder.AppendScalar(label) +} + +func (c *prover) Prove(rnd io.Reader) (p proof) { + if rnd == nil { + rnd = rand.Reader + } + + blindings := make([]scalar, len(c.scalars)) + for i := range blindings { + blindings[i] = c.suite.randomScalar(rnd) + } + + blindedElts := make([]elt, len(c.cons)) + t := c.suite.newElement() + for i := range c.cons { + index := c.cons[i].z + if index > elemIndex(len(c.elems)) { + panic(ErrInvalidIndex) + } + + sum := c.suite.newElement() + for j := range c.cons[i].c { + scalarIdx := c.cons[i].c[j].s + elemIdx := c.cons[i].c[j].e + + if scalarIdx > scalarIndex(len(blindings)) { + panic(ErrInvalidIndex) + } + + if elemIdx > elemIndex(len(c.elems)) { + panic(ErrInvalidIndex) + } + + t.Mul(c.elems[elemIdx], blindings[scalarIdx]) + sum.Add(sum, t) + } + + blindedElts[i] = sum + } + + p.init(c.suite, uint(len(c.scalars))) + p.chal = c.calcChallenge(c.elems, blindedElts) + for i := range p.resp { + p.resp[i].Sub(blindings[i], p.resp[i].Mul(p.chal, c.scalars[i])) + } + + clear(blindings) + return p +} + +type verifier struct{ builder } + +func newVerifier(id SuiteID, ctx string) (v verifier) { + v.builder = builder{suite: id.getSuite(), ctx: ctx} + return +} + +func (v *verifier) Verify(p *proof) bool { + if len(v.elems) != len(v.elemLabels) { + return false + } + + blindedElts := make([]elt, len(v.cons)) + t := v.suite.newElement() + for i := range v.cons { + index := v.cons[i].z + if index > elemIndex(len(v.elems)) { + panic(ErrInvalidIndex) + } + + sum := v.suite.newElement() + sum.Mul(v.elems[index], p.chal) + for j := range v.cons[i].c { + scalarIdx := v.cons[i].c[j].s + elemIdx := v.cons[i].c[j].e + + if scalarIdx > scalarIndex(len(p.resp)) { + panic(ErrInvalidIndex) + } + + if elemIdx > elemIndex(len(v.elems)) { + panic(ErrInvalidIndex) + } + + t.Mul(v.elems[elemIdx], p.resp[scalarIdx]) + sum.Add(sum, t) + } + + blindedElts[i] = sum + } + + chal := v.calcChallenge(v.elems, blindedElts) + return p.chal.IsEqual(chal) +} diff --git a/ac/arc/credential.go b/ac/arc/credential.go new file mode 100644 index 00000000..7d089e13 --- /dev/null +++ b/ac/arc/credential.go @@ -0,0 +1,239 @@ +package arc + +import ( + "io" + "slices" + + "github.com/cloudflare/circl/internal/conv" + "golang.org/x/crypto/cryptobyte" +) + +type Finalizer struct { + m1, m2, r1, r2 scalar + ID SuiteID +} + +func (f Finalizer) String() string { + return printAny(f.m1, f.m2, f.r1, f.r2) +} + +func (f *Finalizer) UnmarshalBinary(b []byte) error { + return conv.UnmarshalBinary(f, b) +} + +func (f *Finalizer) MarshalBinary() ([]byte, error) { + return conv.MarshalBinary(f) +} + +func (f *Finalizer) Marshal(b *cryptobyte.Builder) error { + return conv.MarshalSlice(b, f.m1, f.m2, f.r1, f.r2) +} + +func (f *Finalizer) Unmarshal(s *cryptobyte.String) bool { + suite := f.ID.getSuite() + suite.initScalar(&f.m1, &f.m2, &f.r1, &f.r2) + return conv.UnmarshalSlice(s, f.m1, f.m2, f.r1, f.r2) +} + +func (f *Finalizer) IsEqual(g *Finalizer) bool { + return f.ID == g.ID && slices.EqualFunc( + []scalar{f.m1, f.m2, f.r1, f.r2}, + []scalar{g.m1, g.m2, g.r1, g.r2}, + scalar.IsEqual) +} + +type CredentialRequest struct { + m1, m2 elt + proof reqProof + ID SuiteID +} + +func (s SuiteID) NewCredentialRequest() (c CredentialRequest) { return } + +func (c CredentialRequest) String() string { + return printAny(c.m1, c.m2, proof(c.proof)) +} + +func (c *CredentialRequest) UnmarshalBinary(b []byte) error { + return conv.UnmarshalBinary(c, b) +} + +func (c *CredentialRequest) MarshalBinary() ([]byte, error) { + return conv.MarshalBinary(c) +} + +func (c *CredentialRequest) Marshal(b *cryptobyte.Builder) error { + return conv.MarshalSlice(b, eltCom{c.m1}, eltCom{c.m2}, proof(c.proof)) +} + +func (c *CredentialRequest) Unmarshal(s *cryptobyte.String) bool { + suite := c.ID.getSuite() + suite.initElt(&c.m1, &c.m2) + c.proof.init(suite) + return conv.UnmarshalSlice(s, eltCom{c.m1}, eltCom{c.m2}, proof(c.proof)) +} + +func (c *CredentialRequest) IsEqual(d *CredentialRequest) bool { + return c.ID == d.ID && slices.EqualFunc( + []elt{c.m1, c.m2}, + []elt{d.m1, d.m2}, + elt.IsEqual, + ) && proof(c.proof).IsEqual(proof(d.proof)) +} + +type CredentialResponse struct { + u, encUPrime, x0Aux, x1Aux, x2Aux, hAux elt + proof resProof + ID SuiteID +} + +func (c CredentialResponse) String() string { + return printAny(c.u, c.encUPrime, c.x0Aux, c.x1Aux, c.x2Aux, c.hAux, proof(c.proof)) +} + +func (c *CredentialResponse) UnmarshalBinary(b []byte) error { + return conv.UnmarshalBinary(c, b) +} + +func (c *CredentialResponse) MarshalBinary() ([]byte, error) { + return conv.MarshalBinary(c) +} + +func (c *CredentialResponse) Marshal(b *cryptobyte.Builder) error { + return conv.MarshalSlice(b, + eltCom{c.u}, eltCom{c.encUPrime}, eltCom{c.x0Aux}, eltCom{c.x1Aux}, + eltCom{c.x2Aux}, eltCom{c.hAux}, proof(c.proof)) +} + +func (c *CredentialResponse) Unmarshal(s *cryptobyte.String) bool { + suite := c.ID.getSuite() + suite.initElt(&c.u, &c.encUPrime, &c.x0Aux, &c.x1Aux, &c.x2Aux, &c.hAux) + c.proof.init(suite) + return conv.UnmarshalSlice(s, + eltCom{c.u}, eltCom{c.encUPrime}, eltCom{c.x0Aux}, eltCom{c.x1Aux}, + eltCom{c.x2Aux}, eltCom{c.hAux}, proof(c.proof)) +} + +func (c *CredentialResponse) IsEqual(d *CredentialResponse) bool { + return c.ID == d.ID && slices.EqualFunc( + []elt{c.u, c.encUPrime, c.x0Aux, c.x1Aux, c.x2Aux, c.hAux}, + []elt{d.u, d.encUPrime, d.x0Aux, d.x1Aux, d.x2Aux, d.hAux}, + elt.IsEqual, + ) && proof(c.proof).IsEqual(proof(d.proof)) +} + +type Credential struct { + m1 scalar + u, uPrime, x1 elt + ID SuiteID +} + +func (c Credential) String() string { + return printAny(c.m1, c.u, c.uPrime, c.x1) +} + +func (c *Credential) UnmarshalBinary(b []byte) error { + return conv.UnmarshalBinary(c, b) +} + +func (c *Credential) MarshalBinary() ([]byte, error) { + return conv.MarshalBinary(c) +} + +func (c *Credential) Marshal(b *cryptobyte.Builder) error { + return conv.MarshalSlice(b, + c.m1, eltCom{c.u}, eltCom{c.uPrime}, eltCom{c.x1}) +} + +func (c *Credential) Unmarshal(s *cryptobyte.String) bool { + suite := c.ID.getSuite() + suite.initScalar(&c.m1) + suite.initElt(&c.u, &c.uPrime, &c.x1) + return conv.UnmarshalSlice(s, c.m1, eltCom{c.u}, eltCom{c.uPrime}, eltCom{c.x1}) +} + +func (c *Credential) IsEqual(d *Credential) bool { + return c.ID == d.ID && c.m1.IsEqual(d.m1) && slices.EqualFunc( + []elt{c.u, c.uPrime, c.x1}, + []elt{d.u, d.uPrime, d.x1}, + elt.IsEqual) +} + +func Request( + rnd io.Reader, id SuiteID, ctx []byte, +) (fin Finalizer, credReq CredentialRequest) { + s := id.getSuite() + fin = Finalizer{ + ID: id, + m1: s.randomScalar(rnd), + m2: s.hashToScalar(ctx, labelRequestContext), + r1: s.randomScalar(rnd), + r2: s.randomScalar(rnd), + } + + credReq.ID = id + s.initElt(&credReq.m1, &credReq.m2) + t := s.newElement() + credReq.m1.Add(credReq.m1.MulGen(fin.m1), t.Mul(s.genH, fin.r1)) + credReq.m2.Add(credReq.m2.MulGen(fin.m2), t.Mul(s.genH, fin.r2)) + credReq.makeProof(rnd, &fin) + return fin, credReq +} + +func Response( + rnd io.Reader, priv *PrivateKey, credReq *CredentialRequest, +) (*CredentialResponse, error) { + if !credReq.verifyProof() { + return nil, ErrVerifyReqProof + } + + pub := priv.PublicKey() + res := new(CredentialResponse) + res.ID = priv.ID + s := priv.ID.getSuite() + s.initElt(&res.u, &res.encUPrime, &res.x0Aux, &res.x1Aux, &res.x2Aux, &res.hAux) + res.proof.init(s) + + b := s.randomScalar(rnd) + res.u.MulGen(b) + t := s.newElement() + res.encUPrime.Add(pub.x0, t.Mul(credReq.m1, priv.x1)) + res.encUPrime.Add(res.encUPrime, t.Mul(credReq.m2, priv.x2)) + res.encUPrime.Mul(res.encUPrime, b) + + e := s.newScalar() + e.Mul(b, priv.x0Blinding) + res.x0Aux.Mul(s.genH, e) + res.x1Aux.Mul(pub.x1, b) + res.x2Aux.Mul(pub.x2, b) + res.hAux.Mul(s.genH, b) + res.makeProof(rnd, priv, b, credReq) + return res, nil +} + +func Finalize( + fin *Finalizer, + credReq *CredentialRequest, + credRes *CredentialResponse, + pub *PublicKey, +) (*Credential, error) { + if !credRes.verifyProof(pub, credReq) { + return nil, ErrVerifyResProof + } + + s := pub.ID.getSuite() + t := s.newElement() + uPrime := s.newElement() + uPrime.Add(credRes.x0Aux, t.Mul(credRes.x1Aux, fin.r1)) + uPrime.Add(uPrime, t.Mul(credRes.x2Aux, fin.r2)) + uPrime.Neg(uPrime) + uPrime.Add(uPrime, credRes.encUPrime) + + return &Credential{ + ID: pub.ID, + m1: fin.m1.Copy(), + u: credRes.u.Copy(), + uPrime: uPrime, + x1: pub.x1.Copy(), + }, nil +} diff --git a/ac/arc/doc.go b/ac/arc/doc.go new file mode 100644 index 00000000..e95e41b0 --- /dev/null +++ b/ac/arc/doc.go @@ -0,0 +1,55 @@ +// Package arc provides Anonymous Rate-Limited Credentials. +// +// This package implements ARC, an anonymous credential system for rate limiting. +// Implementation is compliant with the privacy pass draft [1]. +// +// [1] https://datatracker.ietf.org/doc/html/draft-yun-privacypass-crypto-arc-00 +// +// # Key Generation +// +// Server generates a pair of keys. +// +// priv := KeyGen(rand.Reader, SuiteP256) +// pub := priv.PublicKey() +// +// # Credential Issuance +// +// Client reaches Server to generate a credential. +// +// Client(pub) Server(priv,pub) +// ------------------------------------------ +// fin,req := Request() +// --- req --> +// res := Response(priv) +// <--- res --- +// cred := Finalize(fin,req,res,pub) +// ------------------------------------------ +// +// # Presentation +// +// Client uses a credential to generate a fixed number of presentations. +// +// Client(N) Server(priv,pub,N) +// ------------------------------------------ +// s := NewState(cred, N) +// for i := range N { +// pres_i := s.Present() +// --- pres_i --> +// b := Verify(priv, pres_i, N) +// = Ok/Invalid +// } +// ------------------------------------------ +package arc + +import "errors" + +var ( + ErrSuite = errors.New("invalid suite identifier") + ErrVerifyReqProof = errors.New("invalid credential request proof") + ErrVerifyResProof = errors.New("invalid credential response proof") + ErrLimitValid = errors.New("limit must be larger than zero") + ErrLimitExceeded = errors.New("presentation count exceeds limit") + ErrContextLength = errors.New("context length exceeded") + ErrInvalidNonce = errors.New("invalid nonce associated to presentation") + ErrInvalidIndex = errors.New("invalid index in proof") +) diff --git a/ac/arc/example_test.go b/ac/arc/example_test.go new file mode 100644 index 00000000..d072e337 --- /dev/null +++ b/ac/arc/example_test.go @@ -0,0 +1,85 @@ +package arc_test + +import ( + "crypto/rand" + "errors" + "fmt" + "log" + + "github.com/cloudflare/circl/ac/arc" +) + +var ( + requestContext = []byte("Credential for Alice") + presentationContext = []byte("Presentation for example.com") +) + +func ExampleCredential() { + priv, pub := getKeys(arc.SuiteP256) + credential := getCredential(arc.SuiteP256, &priv, &pub) + fmt.Print(credential != nil) + // Output: true +} + +func getKeys(id arc.SuiteID) (arc.PrivateKey, arc.PublicKey) { + priv := arc.KeyGen(rand.Reader, id) + pub := priv.PublicKey() + return priv, pub +} + +func getCredential(id arc.SuiteID, priv *arc.PrivateKey, pub *arc.PublicKey) *arc.Credential { + // Client + fin, credReq := arc.Request(rand.Reader, id, requestContext) + + // ----- credReq ----> + + // Server + credRes, err := arc.Response(rand.Reader, priv, &credReq) + if err != nil { + log.Fatal(err) + } + + // <----- credRes ---- + + // Client + credential, err := arc.Finalize(&fin, &credReq, credRes, pub) + if err != nil { + log.Fatal(err) + } + + return credential +} + +func ExamplePresentation() { + priv, pub := getKeys(arc.SuiteP256) + credential := getCredential(arc.SuiteP256, &priv, &pub) + + // Client + const MaxPres = 3 + state, err0 := arc.NewState(credential, presentationContext, MaxPres) + if err0 != nil { + log.Fatal(err0) + } + + // Valid presentations. + for range MaxPres { + // Client + nonce, pres, err := state.Present(rand.Reader) + if err != nil { + log.Fatal(err) + } + + // Server + isValid := arc.Verify(&priv, pres, requestContext, presentationContext, *nonce, MaxPres) + fmt.Println(isValid) + } + + // Error after spending MaxPres presentations. + nonce, pres, err := state.Present(rand.Reader) + fmt.Println(nonce, pres, errors.Is(err, arc.ErrLimitExceeded)) + // Output: + // true + // true + // true + // true +} diff --git a/ac/arc/keys.go b/ac/arc/keys.go new file mode 100644 index 00000000..b224082d --- /dev/null +++ b/ac/arc/keys.go @@ -0,0 +1,119 @@ +package arc + +import ( + "crypto" + "crypto/rand" + "io" + "slices" + + "github.com/cloudflare/circl/internal/conv" + "golang.org/x/crypto/cryptobyte" +) + +type PrivateKey struct { + x0, x1, x2, x0Blinding scalar + pub *PublicKey + ID SuiteID +} + +func (k PrivateKey) String() string { + return printAny(k.x0, k.x1, k.x2, k.x0Blinding) +} + +func (k *PrivateKey) UnmarshalBinary(b []byte) error { + return conv.UnmarshalBinary(k, b) +} + +func (k *PrivateKey) MarshalBinary() ([]byte, error) { + return conv.MarshalBinary(k) +} + +func (k *PrivateKey) Marshal(b *cryptobyte.Builder) error { + return conv.MarshalSlice(b, k.x0, k.x1, k.x2, k.x0Blinding) +} + +func (k *PrivateKey) Unmarshal(s *cryptobyte.String) bool { + suite := k.ID.getSuite() + suite.initScalar(&k.x0, &k.x1, &k.x2, &k.x0Blinding) + return conv.UnmarshalSlice(s, k.x0, k.x1, k.x2, k.x0Blinding) +} + +func (k *PrivateKey) Equal(priv crypto.PrivateKey) bool { + x, ok := priv.(*PrivateKey) + return ok && k.ID == x.ID && + slices.EqualFunc( + []scalar{k.x0, k.x1, k.x2, k.x0Blinding}, + []scalar{x.x0, x.x1, x.x2, x.x0Blinding}, + scalar.IsEqual) +} + +func (k *PrivateKey) Public() crypto.PublicKey { return k.PublicKey() } +func (k *PrivateKey) PublicKey() PublicKey { + if k.pub == nil { + s := k.ID.getSuite() + x0 := s.newElement() + x1 := s.newElement() + x2 := s.newElement() + x0.Add(x0.MulGen(k.x0), x1.Mul(s.genH, k.x0Blinding)) + x1.Mul(s.genH, k.x1) + x2.Mul(s.genH, k.x2) + k.pub = &PublicKey{ + ID: k.ID, + x0: x0, + x1: x1, + x2: x2, + } + } + + return *k.pub +} + +type PublicKey struct { + x0, x1, x2 elt + ID SuiteID +} + +func (k PublicKey) String() string { + return printAny(k.x0, k.x1, k.x2) +} + +func (k *PublicKey) UnmarshalBinary(b []byte) error { + return conv.UnmarshalBinary(k, b) +} + +func (k *PublicKey) MarshalBinary() ([]byte, error) { + return conv.MarshalBinary(k) +} + +func (k *PublicKey) Marshal(b *cryptobyte.Builder) error { + return conv.MarshalSlice(b, eltCom{k.x0}, eltCom{k.x1}, eltCom{k.x2}) +} + +func (k *PublicKey) Unmarshal(s *cryptobyte.String) bool { + suite := k.ID.getSuite() + suite.initElt(&k.x0, &k.x1, &k.x2) + return conv.UnmarshalSlice(s, eltCom{k.x0}, eltCom{k.x1}, eltCom{k.x2}) +} + +func (k *PublicKey) Equal(pub crypto.PublicKey) bool { + x, ok := pub.(*PublicKey) + return ok && k.ID == x.ID && slices.EqualFunc( + []elt{k.x0, k.x1, k.x2}, + []elt{x.x0, x.x1, x.x2}, + elt.IsEqual) +} + +func KeyGen(rnd io.Reader, id SuiteID) PrivateKey { + if rnd == nil { + rnd = rand.Reader + } + + s := id.getSuite() + return PrivateKey{ + ID: id, + x0: s.randomScalar(rnd), + x1: s.randomScalar(rnd), + x2: s.randomScalar(rnd), + x0Blinding: s.randomScalar(rnd), + } +} diff --git a/ac/arc/presentation.go b/ac/arc/presentation.go new file mode 100644 index 00000000..357d0982 --- /dev/null +++ b/ac/arc/presentation.go @@ -0,0 +1,227 @@ +package arc + +import ( + "crypto/rand" + "io" + "math" + "math/big" + "slices" + + "github.com/cloudflare/circl/internal/conv" + "golang.org/x/crypto/cryptobyte" +) + +type nonceSet struct { + limit big.Int + bitField big.Int + available uint16 +} + +func newNonceSet(limit uint16) (n nonceSet) { + n.available = limit + n.limit.SetUint64(uint64(limit)) + n.bitField.SetUint64(1) + n.bitField.Lsh(&n.bitField, uint(limit)) + return +} + +func (n *nonceSet) AddRandom(rnd io.Reader) uint16 { + for { + chosen, _ := rand.Int(rnd, &n.limit) + x := chosen.Uint64() + if n.bitField.Bit(int(x)) == 0 { + n.bitField.SetBit(&n.bitField, int(x), 1) + n.available -= 1 + return uint16(x) + } + } +} + +func (n *nonceSet) Marshal(b *cryptobyte.Builder) error { + limit := n.limit.Uint64() + data := make([]byte, (limit+1+7)/8) + n.bitField.FillBytes(data) + b.AddUint64(limit) + b.AddUint16(n.available) + b.AddBytes(data) + return nil +} + +func (n *nonceSet) Unmarshal(s *cryptobyte.String) bool { + var limit uint64 + if !s.ReadUint64(&limit) || limit == 0 || limit >= math.MaxUint16 { + return false + } + + var available uint16 + data := make([]byte, (limit+1+7)/8) + if !s.ReadUint16(&available) || !s.CopyBytes(data) { + return false + } + + n.limit.SetUint64(limit) + n.available = available + n.bitField.SetBytes(data) + return true +} + +type Presentation struct { + u, uPrimeCom, m1Com, tag elt + proof presProof + ID SuiteID +} + +func (p Presentation) String() string { + return printAny(p.u, p.uPrimeCom, p.m1Com, p.tag, proof(p.proof)) +} + +func (p *Presentation) UnmarshalBinary(b []byte) error { + return conv.UnmarshalBinary(p, b) +} + +func (p *Presentation) MarshalBinary() ([]byte, error) { + return conv.MarshalBinary(p) +} + +func (p *Presentation) Marshal(b *cryptobyte.Builder) error { + return conv.MarshalSlice(b, + eltCom{p.u}, eltCom{p.uPrimeCom}, eltCom{p.m1Com}, eltCom{p.tag}, + proof(p.proof)) +} + +func (p *Presentation) Unmarshal(s *cryptobyte.String) bool { + suite := p.ID.getSuite() + suite.initElt(&p.u, &p.uPrimeCom, &p.m1Com, &p.tag) + p.proof.init(suite) + return conv.UnmarshalSlice(s, eltCom{p.u}, eltCom{p.uPrimeCom}, + eltCom{p.m1Com}, eltCom{p.tag}, proof(p.proof)) +} + +func (p *Presentation) IsEqual(q *Presentation) bool { + return p.ID == q.ID && slices.EqualFunc( + []elt{p.u, p.uPrimeCom, p.m1Com, p.tag}, + []elt{q.u, q.uPrimeCom, q.m1Com, q.tag}, + elt.IsEqual, + ) && proof(p.proof).IsEqual(proof(q.proof)) +} + +type State struct { + presCtx []byte + cred Credential + nonce nonceSet + ID SuiteID +} + +func NewState(cred *Credential, presCtx []byte, limit uint16) (*State, error) { + if limit == 0 { + return nil, ErrLimitValid + } + + if len(presCtx) >= math.MaxUint16 { + return nil, ErrContextLength + } + + return &State{ + ID: cred.ID, + cred: *cred, + presCtx: presCtx, + nonce: newNonceSet(limit), + }, nil +} + +func (s *State) UnmarshalBinary(b []byte) error { + return conv.UnmarshalBinary(s, b) +} + +func (s *State) MarshalBinary() ([]byte, error) { + return conv.MarshalBinary(s) +} + +func (s *State) Marshal(b *cryptobyte.Builder) error { + b.AddValue(&s.cred) + b.AddValue(&s.nonce) + b.AddUint16(uint16(len(s.presCtx))) + b.AddBytes(s.presCtx) + return nil +} + +func (s *State) Unmarshal(str *cryptobyte.String) bool { + var n uint16 + s.cred.ID = s.ID + if !s.cred.Unmarshal(str) || + !s.nonce.Unmarshal(str) || + !str.ReadUint16(&n) { + return false + } + + s.presCtx = make([]byte, n) + return str.CopyBytes(s.presCtx) +} + +func (s *State) Present(rnd io.Reader) (*uint16, *Presentation, error) { + if s.nonce.available == 0 { + return nil, nil, ErrLimitExceeded + } + + if rnd == nil { + rnd = rand.Reader + } + + suite := s.cred.ID.getSuite() + a := suite.randomScalar(rnd) + r := suite.randomScalar(rnd) + z := suite.randomScalar(rnd) + + p := &Presentation{ID: s.ID} + suite.initElt(&p.u, &p.uPrimeCom, &p.m1Com, &p.tag) + p.u.Mul(s.cred.u, a) + p.uPrimeCom.Mul(s.cred.uPrime, a) + rG := suite.newElement() + rG.MulGen(r) + p.uPrimeCom.Add(p.uPrimeCom, rG) + + p.m1Com.Mul(p.u, s.cred.m1) + t := suite.newElement() + t.Mul(suite.genH, z) + p.m1Com.Add(p.m1Com, t) + + genT := suite.hashToGroup(s.presCtx, labelTag) + nonce := s.nonce.AddRandom(rnd) + nonceScl := suite.newScalar().SetUint64(uint64(nonce)) + e := suite.newScalar() + e.Add(nonceScl, s.cred.m1) + e.Inv(e) + p.tag.Mul(genT, e) + + V := suite.newElement() + V.Mul(s.cred.x1, z) + t.Neg(rG) + V.Add(V, t) + + m1Tag := suite.newElement() + m1Tag.Mul(p.tag, s.cred.m1) + p.makeProof(rnd, genT, V, m1Tag, s.cred.x1, s.cred.m1, r, z, nonceScl) + + return &nonce, p, nil +} + +func Verify( + priv *PrivateKey, + pres *Presentation, + reqCtx, presCtx []byte, + nonce, limit uint16, +) bool { + if nonce > limit { + panic(ErrInvalidNonce) + } + + s := priv.ID.getSuite() + genT := s.hashToGroup(presCtx, labelTag) + e := s.newScalar().SetUint64(uint64(nonce)) + + m1Tag := s.newElement() + m1Tag.Mul(pres.tag, e) + m1Tag.Neg(m1Tag) + m1Tag.Add(genT, m1Tag) + return pres.verifyProof(priv, reqCtx, genT, m1Tag) +} diff --git a/ac/arc/proofs.go b/ac/arc/proofs.go new file mode 100644 index 00000000..93a0af9d --- /dev/null +++ b/ac/arc/proofs.go @@ -0,0 +1,192 @@ +package arc + +import "io" + +type ( + reqProof proof + resProof proof + presProof proof +) + +func (p *reqProof) init(s *suite) { (*proof)(p).init(s, 4) } +func (p *resProof) init(s *suite) { (*proof)(p).init(s, 7) } +func (p *presProof) init(s *suite) { (*proof)(p).init(s, 4) } + +func (c *CredentialRequest) build(b *builder, sc *[4]scalarIndex) { + m1Var, m2Var, r1Var, r2Var := sc[0], sc[1], sc[2], sc[3] + + genGVar := b.AppendElement([]byte("genG"), b.suite.genG) + genHVar := b.AppendElement([]byte("genH"), b.suite.genH) + m1EncVar := b.AppendElement([]byte("m1Enc"), c.m1) + m2EncVar := b.AppendElement([]byte("m2Enc"), c.m2) + + b.Constrain(m1EncVar, mul{m1Var, genGVar}, mul{r1Var, genHVar}) + b.Constrain(m2EncVar, mul{m2Var, genGVar}, mul{r2Var, genHVar}) +} + +func (c *CredentialRequest) makeProof(rnd io.Reader, fin *Finalizer) { + p := newProver(c.ID, labelCRequest) + + var sc [4]scalarIndex + sc[0] = p.AppendScalar([]byte("m1"), fin.m1) + sc[1] = p.AppendScalar([]byte("m2"), fin.m2) + sc[2] = p.AppendScalar([]byte("r1"), fin.r1) + sc[3] = p.AppendScalar([]byte("r2"), fin.r2) + + c.build(&p.builder, &sc) + c.proof = reqProof(p.Prove(rnd)) +} + +func (c *CredentialRequest) verifyProof() bool { + v := newVerifier(c.ID, labelCRequest) + + var sc [4]scalarIndex + sc[0] = v.AppendScalar([]byte("m1")) + sc[1] = v.AppendScalar([]byte("m2")) + sc[2] = v.AppendScalar([]byte("r1")) + sc[3] = v.AppendScalar([]byte("r2")) + + c.build(&v.builder, &sc) + return v.Verify((*proof)(&c.proof)) +} + +func (c *CredentialResponse) build( + b *builder, sc *[7]scalarIndex, req *CredentialRequest, pub *PublicKey, +) { + x0Var, x1Var, x2Var, x0BliVar := sc[0], sc[1], sc[2], sc[3] + bVar, t1Var, t2Var := sc[4], sc[5], sc[6] + + genGVar := b.AppendElement([]byte("genG"), b.suite.genG) + genHVar := b.AppendElement([]byte("genH"), b.suite.genH) + m1EncVar := b.AppendElement([]byte("m1Enc"), req.m1) + m2EncVar := b.AppendElement([]byte("m2Enc"), req.m2) + uVar := b.AppendElement([]byte("U"), c.u) + encUPrimeVar := b.AppendElement([]byte("encUPrime"), c.encUPrime) + X0Var := b.AppendElement([]byte("X0"), pub.x0) + X1Var := b.AppendElement([]byte("X1"), pub.x1) + X2Var := b.AppendElement([]byte("X2"), pub.x2) + x0AuxVar := b.AppendElement([]byte("X0Aux"), c.x0Aux) + x1AuxVar := b.AppendElement([]byte("X1Aux"), c.x1Aux) + x2AuxVar := b.AppendElement([]byte("X2Aux"), c.x2Aux) + hAuxVar := b.AppendElement([]byte("HAux"), c.hAux) + + b.Constrain(X0Var, mul{x0Var, genGVar}, mul{x0BliVar, genHVar}) + b.Constrain(X1Var, mul{x1Var, genHVar}) + b.Constrain(X2Var, mul{x2Var, genHVar}) + b.Constrain(hAuxVar, mul{bVar, genHVar}) + b.Constrain(x0AuxVar, mul{x0BliVar, hAuxVar}) + b.Constrain(x1AuxVar, mul{t1Var, genHVar}) + b.Constrain(x1AuxVar, mul{bVar, X1Var}) + b.Constrain(x2AuxVar, mul{bVar, X2Var}) + b.Constrain(x2AuxVar, mul{t2Var, genHVar}) + b.Constrain(uVar, mul{bVar, genGVar}) + b.Constrain(encUPrimeVar, mul{bVar, X0Var}, mul{t1Var, m1EncVar}, + mul{t2Var, m2EncVar}) +} + +func (c *CredentialResponse) makeProof( + rnd io.Reader, + priv *PrivateKey, + b scalar, + req *CredentialRequest, +) { + pub := priv.PublicKey() + p := newProver(pub.ID, labelCResponse) + + var sc [7]scalarIndex + t1 := b.Group().NewScalar() + t2 := b.Group().NewScalar() + sc[0] = p.AppendScalar([]byte("x0"), priv.x0) + sc[1] = p.AppendScalar([]byte("x1"), priv.x1) + sc[2] = p.AppendScalar([]byte("x2"), priv.x2) + sc[3] = p.AppendScalar([]byte("x0Blinding"), priv.x0Blinding) + sc[4] = p.AppendScalar([]byte("b"), b) + sc[5] = p.AppendScalar([]byte("t1"), t1.Mul(b, priv.x1)) + sc[6] = p.AppendScalar([]byte("t2"), t2.Mul(b, priv.x2)) + + c.build(&p.builder, &sc, req, &pub) + c.proof = resProof(p.Prove(rnd)) +} + +func (c *CredentialResponse) verifyProof(pub *PublicKey, req *CredentialRequest) bool { + v := newVerifier(pub.ID, labelCResponse) + + var sc [7]scalarIndex + sc[0] = v.AppendScalar([]byte("x0")) + sc[1] = v.AppendScalar([]byte("x1")) + sc[2] = v.AppendScalar([]byte("x2")) + sc[3] = v.AppendScalar([]byte("x0Blinding")) + sc[4] = v.AppendScalar([]byte("b")) + sc[5] = v.AppendScalar([]byte("t1")) + sc[6] = v.AppendScalar([]byte("t2")) + + c.build(&v.builder, &sc, req, pub) + return v.Verify((*proof)(&c.proof)) +} + +func (p *Presentation) build( + b *builder, sc *[4]scalarIndex, x1, generatorT, V, m1Tag elt, +) { + m1Var, zVar, rNegVar, nonceVar := sc[0], sc[1], sc[2], sc[3] + + genGVar := b.AppendElement([]byte("genG"), b.suite.genG) + genHVar := b.AppendElement([]byte("genH"), b.suite.genH) + UVar := b.AppendElement([]byte("U"), p.u) + _ = b.AppendElement([]byte("UPrimeCommit"), p.uPrimeCom) + m1CommitVar := b.AppendElement([]byte("m1Commit"), p.m1Com) + VVar := b.AppendElement([]byte("V"), V) + X1Var := b.AppendElement([]byte("X1"), x1) + tagVar := b.AppendElement([]byte("tag"), p.tag) + genTVar := b.AppendElement([]byte("genT"), generatorT) + m1TagVar := b.AppendElement([]byte("m1Tag"), m1Tag) + + b.Constrain(m1CommitVar, mul{m1Var, UVar}, mul{zVar, genHVar}) + b.Constrain(VVar, mul{zVar, X1Var}, mul{rNegVar, genGVar}) + b.Constrain(genTVar, mul{m1Var, tagVar}, mul{nonceVar, tagVar}) + b.Constrain(m1TagVar, mul{m1Var, tagVar}) +} + +func (p *Presentation) makeProof( + rnd io.Reader, + generatorT, V, m1Tag, x1 elt, + m1, r, z, nonce scalar, +) { + pp := newProver(p.ID, labelCPresentation) + + var sc [4]scalarIndex + sc[0] = pp.AppendScalar([]byte("m1"), m1) + sc[1] = pp.AppendScalar([]byte("z"), z) + sc[2] = pp.AppendScalar([]byte("-r"), r.Neg(r)) + sc[3] = pp.AppendScalar([]byte("nonce"), nonce) + + p.build(&pp.builder, &sc, x1, generatorT, V, m1Tag) + p.proof = presProof(pp.Prove(rnd)) +} + +func (p *Presentation) verifyProof( + priv *PrivateKey, + reqCtx []byte, + generatorT, m1Tag elt, +) bool { + s := priv.ID.getSuite() + m2 := s.hashToScalar(reqCtx, labelRequestContext) + m2.Mul(m2, priv.x2) + t := s.newElement() + V := s.newElement() + V.Mul(p.u, priv.x0) + V.Add(V, t.Mul(p.m1Com, priv.x1)) + V.Add(V, t.Mul(p.u, m2)) + V.Add(V, t.Neg(p.uPrimeCom)) + + v := newVerifier(priv.ID, labelCPresentation) + + var sc [4]scalarIndex + sc[0] = v.AppendScalar([]byte("m1")) + sc[1] = v.AppendScalar([]byte("z")) + sc[2] = v.AppendScalar([]byte("-r")) + sc[3] = v.AppendScalar([]byte("nonce")) + + pub := priv.PublicKey() + p.build(&v.builder, &sc, pub.x1, generatorT, V, m1Tag) + return v.Verify((*proof)(&p.proof)) +} diff --git a/ac/arc/suite.go b/ac/arc/suite.go new file mode 100644 index 00000000..ba757923 --- /dev/null +++ b/ac/arc/suite.go @@ -0,0 +1,147 @@ +package arc + +import ( + "fmt" + "io" + "strings" + + "github.com/cloudflare/circl/group" + "github.com/cloudflare/circl/internal/conv" + "golang.org/x/crypto/cryptobyte" +) + +// SuiteID is an identifier of the supported suite. +type SuiteID int + +const ( + // SuiteP256 uses the P256 elliptic curve group. + SuiteP256 SuiteID = iota + 1 + // SuiteRistretto255 uses the Ristretto elliptic curve group. + SuiteRistretto255 +) + +func (id SuiteID) String() string { + switch id { + case SuiteP256: + return suiteNameP256 + case SuiteRistretto255: + return suiteNameRistretto255 + default: + return ErrSuite.Error() + } +} + +func (id SuiteID) getSuite() *suite { + switch id { + case SuiteP256: + return &suiteP256 + case SuiteRistretto255: + return &suiteRistretto255 + default: + panic(ErrSuite) + } +} + +var suiteP256, suiteRistretto255 suite + +func init() { + initSuite(&suiteP256, group.P256, contextStringP256) + initSuite(&suiteRistretto255, group.Ristretto255, contextStringRist) +} + +type suite struct { + g group.Group + genG, genH elt + ctx string +} + +func initSuite(s *suite, g group.Group, context string) { + s.g = g + s.ctx = context + s.genG = s.g.Generator() + b, _ := eltCom{s.genG}.MarshalBinary() + s.genH = s.hashToGroup(b, labelGenH) +} + +func (s *suite) chalContext(ctx string) string { return s.ctx + ctx } +func (s *suite) sizeElement() uint { return s.g.Params().CompressedElementLength } +func (s *suite) newElement() elt { return s.g.NewElement() } +func (s *suite) newScalar() scalar { return s.g.NewScalar() } +func (s *suite) randomScalar(rnd io.Reader) scalar { return s.g.RandomNonZeroScalar(rnd) } +func (s *suite) hashToScalar(msg []byte, dst string) scalar { + return s.g.HashToScalar(msg, []byte(labelHashScalar+s.ctx+dst)) +} + +func (s *suite) hashToGroup(msg []byte, dst string) elt { + return s.g.HashToElement(msg, []byte(labelHashGroup+s.ctx+dst)) +} + +func (s *suite) initElt(v ...*elt) { + for i := range v { + *v[i] = s.g.NewElement() + } +} + +func (s *suite) initScalar(v ...*scalar) { + for i := range v { + *v[i] = s.g.NewScalar() + } +} + +func printAny(v ...any) (s string) { + var b strings.Builder + for i := range v { + fmt.Fprintf(&b, "%v\n", v[i]) + } + return b.String() +} + +type ( + scalar = group.Scalar + elt = group.Element +) + +// eltCom enforces the use of compressed elements for serialization. +type eltCom struct{ elt } + +func (e eltCom) Size() uint { + return e.elt.Group().Params().CompressedElementLength +} + +func (e eltCom) MarshalBinary() ([]byte, error) { + return conv.MarshalBinaryLen(e, e.Size()) +} + +func (e eltCom) UnmarshalBinary(b []byte) error { + return conv.UnmarshalBinary(e, b) +} + +func (e eltCom) Marshal(b *cryptobyte.Builder) error { + data, err := e.elt.MarshalBinaryCompress() + if err != nil { + return err + } + + b.AddBytes(data) + return nil +} + +func (e eltCom) Unmarshal(s *cryptobyte.String) bool { + data := make([]byte, e.Size()) + return s.CopyBytes(data) && e.elt.UnmarshalBinary(data) == nil +} + +const ( + suiteNameP256 = "P256" + contextStringP256 = "ARCV1-P256" + suiteNameRistretto255 = "Ristretto255" + contextStringRist = "ARCV1-Ristretto255" + labelGenH = "generatorH" + labelTag = "Tag" + labelHashScalar = "HashToScalar-" + labelHashGroup = "HashToGroup-" + labelRequestContext = "requestContext" + labelCRequest = "CredentialRequest" + labelCResponse = "CredentialResponse" + labelCPresentation = "CredentialPresentation" +) diff --git a/ac/arc/testdata/draft_v01.json.gz b/ac/arc/testdata/draft_v01.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..2b4b5adad3a5c901ae3939ad426a5d39e2f27516 GIT binary patch literal 2713 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 value")