diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf73504..586d5c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,6 +70,8 @@ jobs: # Codecov - name: Codecov uses: codecov/codecov-action@bbeaa140357942e4e8d8e15f1cd2f4e612f64c59 # pin@master + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: file: .github/coverage.out diff --git a/README.md b/README.md index 258a7e1..2a2105d 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,15 @@ # (V)OPRF : (Verifiable) Oblivious Pseudorandom Functions -[![VOPRF](https://github.com/bytemare/voprf/actions/workflows/ci.yml/badge.svg)](https://github.com/bytemare/voprf/actions/workflows/ci.yml) +[![VOPRF](https://github.com/bytemare/voprf/actions/workflows/ci.yml/badge.svg?branch=)](https://github.com/bytemare/voprf/actions/workflows/ci.yml) [![Go Reference](https://pkg.go.dev/badge/github.com/bytemare/voprf.svg)](https://pkg.go.dev/github.com/bytemare/voprf) [![codecov](https://codecov.io/gh/bytemare/voprf/branch/main/graph/badge.svg?token=5bQfB0OctA)](https://codecov.io/gh/bytemare/voprf) -Package voprf provides abstracted access to Oblivious Pseudorandom Functions (OPRF) over elliptic curves. - -This implementation supports the OPRF, VOPRF, and POPRF protocols as specified in the latest [internet draft](https://tools.ietf.org/html/draft-irtf-cfrg-voprf). +Package voprf provides abstracted access to Oblivious Pseudorandom Functions (OPRF) over Elliptic Curves as specified in +[RFC9497](https://datatracker.ietf.org/doc/rfc9497) and fully supports the OPRF, VOPRF, and POPRF protocols. ## Versioning -[SemVer](http://semver.org/) is used for versioning. For the versions available, see the [tags on this repository](https://github.com/bytemare/voprf/tags). - -Minor v0.x versions match the corresponding CFRG draft version, the master branch implements the latest changes of [the draft development](https://github.com/cfrg/draft-irtf-cfrg-voprf). +[SemVer](http://semver.org) is used for versioning. For the versions available, see the [tags on this repository](https://github.com/bytemare/voprf/tags). ## Contributing diff --git a/SECURITY.md b/SECURITY.md index f83bfea..6c03d46 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,6 @@ ## Supported Versions -The VOPRF protocol is still in the process of specification. Therefore, this implementation evolves with the draft. Only the latest version will be benefit from security fixes. Maintainers of projects using this implementation of VOPRF are invited to update their dependency. ## Reporting a Vulnerability diff --git a/client_state.go b/client_state.go index e28adfd..4e46ae3 100644 --- a/client_state.go +++ b/client_state.go @@ -28,7 +28,7 @@ type State struct { // Export extracts the client's internal values that can be imported in another client for session resumption. func (c *Client) Export() *State { s := &State{ - Identifier: c.id, + Identifier: c.ciphersuite, TweakedKey: nil, ServerPublicKey: nil, Input: nil, diff --git a/doc.go b/doc.go index e0d24d0..d0fca86 100644 --- a/doc.go +++ b/doc.go @@ -9,7 +9,5 @@ // Package voprf provides abstracted access to Oblivious Pseudorandom Functions (OPRF) // and VOPRF Oblivious Pseudorandom Functions (VOPRF) using Elliptic Curves (EC(V)OPRF). // -// This work in progress implements https://tools.ietf.org/html/draft-irtf-cfrg-voprf -// -// Integrations can use either base or verifiable mode with additive or multiplicative operations. +// This implements RFC9497. package voprf diff --git a/evaluation.go b/evaluation.go index 1cf8329..9efe734 100644 --- a/evaluation.go +++ b/evaluation.go @@ -16,7 +16,7 @@ import ( // Evaluation holds the serialized evaluated elements and serialized proof. type Evaluation struct { - // Elements represents the unique serialization of an Elements + // Elements represents the unique serialization of Elements Elements [][]byte `json:"e"` // Proofs diff --git a/examples_test.go b/examples_test.go index 062dde1..dd7990e 100644 --- a/examples_test.go +++ b/examples_test.go @@ -10,6 +10,7 @@ package voprf_test import ( "encoding/hex" + "fmt" "github.com/bytemare/voprf" ) @@ -41,6 +42,7 @@ func exchangeWithServer(blinded []byte, verifiable bool) []byte { return ev } +// This shows you how to set up and run the base OPRF client. func Example_client() { input := []byte("input") @@ -52,13 +54,15 @@ func Example_client() { // The client blinds the initial input, and sends this to the server. blinded := client.Blind(input, nil) + fmt.Printf("Send these %d bytes to the server.\n", len(blinded)) - // Exchange with the server is not covered here. The following call is to mock an exchange with a server. - ev := exchangeWithServer(blinded, false) + // Exchange with the server is not covered in this example. Let's say the server sends the following serialized + // evaluation. + evaluation, _ := hex.DecodeString("00010020b4d261d982c6edd2fea53e8a39c1df6393f23cb9d1b4768891ec2f43b8d8e831") // The client needs to decode the evaluation to finalize the process. eval := new(voprf.Evaluation) - if err := eval.Deserialize(ev); err != nil { + if err = eval.Deserialize(evaluation); err != nil { panic(err) } @@ -67,9 +71,10 @@ func Example_client() { if output == nil || err != nil { panic(err) } - // Output: + // Output:Send these 32 bytes to the server. } +// This shows you how to set up and run the Verifiable OPRF client. func Example_verifiableClient() { ciphersuite := voprf.Ristretto255Sha512 input := []byte("input") @@ -85,11 +90,11 @@ func Example_verifiableClient() { blinded := client.Blind(input, nil) // Exchange with the server is not covered here. The following call is to mock an exchange with a server. - ev := exchangeWithServer(blinded, true) + evaluation := exchangeWithServer(blinded, true) // The client needs to decode the evaluation to finalize the process. eval := new(voprf.Evaluation) - if err := eval.Deserialize(ev); err != nil { + if err := eval.Deserialize(evaluation); err != nil { panic(err) } @@ -102,6 +107,7 @@ func Example_verifiableClient() { // Output: } +// This shows you how to set up and run the base OPRF server. func Example_server() { // We suppose the client sends this blinded element. blinded, _ := hex.DecodeString("7eaf3d7cbe43d54637274342ce53578b2aba836f297f4f07997a6e1dced1c058") @@ -123,6 +129,7 @@ func Example_server() { // Output: } +// This shows you how to set up and run the Verifiable OPRF server. func Example_verifiableServer() { privateKey, _ := hex.DecodeString("8132542d5ed08594e7522b5eac6bee38bab5868996c25a3fd2a7739be1856b04") diff --git a/oprf.go b/oprf.go index ce0c4b4..7452b21 100644 --- a/oprf.go +++ b/oprf.go @@ -75,7 +75,7 @@ func (c Ciphersuite) new(mode Mode) *oprf { return &oprf{ hash: hashes[c].Get(), contextString: contextString(mode, c), - id: c, + ciphersuite: c, mode: mode, group: groups[c], } @@ -130,30 +130,34 @@ func (c Ciphersuite) KeyGen() *KeyPair { pk := c.Group().Base().Multiply(sk) return &KeyPair{ - ID: c, - PublicKey: pk.Encode(), - SecretKey: sk.Encode(), + Ciphersuite: c, + PublicKey: pk, + SecretKey: sk, } } // DeriveKeyPair deterministically generates a private and public key pair from input seed. -func (c Ciphersuite) DeriveKeyPair(mode Mode, seed, info []byte) (*group.Scalar, *group.Element) { +func (c Ciphersuite) DeriveKeyPair(mode Mode, seed, info []byte) *KeyPair { dst := concatenate([]byte(deriveKeyPairDST), contextString(mode, c)) deriveInput := concatenate(seed, lengthPrefixEncode(info)) var counter uint8 - var s *group.Scalar + var sk *group.Scalar - for s == nil || s.IsZero() { + for sk == nil || sk.IsZero() { if counter > 255 { panic("impossible to generate non-zero scalar") } - s = c.Group().HashToScalar(concatenate(deriveInput, []byte{counter}), dst) + sk = c.Group().HashToScalar(concatenate(deriveInput, []byte{counter}), dst) counter++ } - return s, c.Group().Base().Multiply(s) + return &KeyPair{ + Ciphersuite: c, + PublicKey: c.Group().Base().Multiply(sk), + SecretKey: sk, + } } // Client returns a (P|V)OPRF client. For the OPRF mode, serverPublicKey should be nil, and non-nil otherwise. @@ -189,19 +193,19 @@ func (c Ciphersuite) Server(mode Mode, privateKey []byte) (*Server, error) { type oprf struct { hash *hash.Hash - id Ciphersuite + ciphersuite Ciphersuite contextString []byte mode Mode group group.Group } -func contextString(mode Mode, id Ciphersuite) []byte { - ctx := make([]byte, 0, len(Version)+3+len(id.String())) +func contextString(mode Mode, ciphersuite Ciphersuite) []byte { + ctx := make([]byte, 0, len(Version)+3+len(ciphersuite.String())) ctx = append(ctx, Version...) ctx = append(ctx, "-"...) ctx = append(ctx, byte(mode)) ctx = append(ctx, "-"...) - ctx = append(ctx, id.String()...) + ctx = append(ctx, ciphersuite.String()...) return ctx } diff --git a/server.go b/server.go index f6d1619..d33f7ff 100644 --- a/server.go +++ b/server.go @@ -128,48 +128,6 @@ func (s *Server) EvaluateBatchWithRandom(blindedElements [][]byte, random, info return s.innerEvaluateBatch(blindedElements, random, info) } -// FullEvaluate reproduces the full PRF but without the blinding operations, using the client's input. -// This should output the same digest as the client's Finalize() function. -func (s *Server) FullEvaluate(input, info []byte) ([]byte, error) { - p := s.HashToGroup(input) - - scalar, _, err := s.getPrivateKeys(info) - if err != nil { - return nil, err - } - - t := p.Multiply(scalar) - - if s.oprf.mode == OPRF || s.oprf.mode == VOPRF { - info = nil - } - - return s.hashTranscript(input, info, t.Encode()), nil -} - -// VerifyFinalize takes the client input (the un-blinded element) and the client's finalize() output, -// and returns whether it can match the client's output. -func (s *Server) VerifyFinalize(input, info, output []byte) bool { - digest, err := s.FullEvaluate(input, info) - if err != nil { - return false - } - - return ctEqual(digest, output) -} - -// VerifyFinalizeBatch takes the batch of client input (the un-blinded elements) and the client's finalize() outputs, -// and returns whether it can match the client's outputs. -func (s *Server) VerifyFinalizeBatch(input, output [][]byte, info []byte) bool { - res := true - - for i, in := range input { - res = s.VerifyFinalize(in, info, output[i]) - } - - return res -} - // PrivateKey returns the server's serialized private key. func (s *Server) PrivateKey() []byte { return s.privateKey.Encode() @@ -182,5 +140,5 @@ func (s *Server) PublicKey() []byte { // Ciphersuite returns the cipher suite used in the server's instance. func (s *Server) Ciphersuite() Ciphersuite { - return s.oprf.id + return s.oprf.ciphersuite } diff --git a/tests/helper_test.go b/tests/helper_test.go index 449bb3d..0eda225 100644 --- a/tests/helper_test.go +++ b/tests/helper_test.go @@ -19,6 +19,7 @@ import ( "testing" group "github.com/bytemare/crypto" + "github.com/bytemare/hash" "github.com/bytemare/voprf" ) @@ -33,29 +34,46 @@ type configuration struct { curve elliptic.Curve ciphersuite voprf.Ciphersuite name string + hash hash.Hashing + group group.Group } var configurationTable = []configuration{ { name: "Ristretto255", ciphersuite: voprf.Ristretto255Sha512, + group: group.Ristretto255Sha512, + hash: hash.SHA512, curve: nil, }, { name: "P256Sha256", ciphersuite: voprf.P256Sha256, + group: group.P256Sha256, + hash: hash.SHA256, curve: elliptic.P256(), }, { name: "P384Sha512", ciphersuite: voprf.P384Sha384, + group: group.P384Sha384, + hash: hash.SHA384, curve: elliptic.P384(), }, { name: "P521Sha512", ciphersuite: voprf.P521Sha512, + group: group.P521Sha512, + hash: hash.SHA512, curve: elliptic.P521(), }, + { + name: "Secp256k1Sha256", + ciphersuite: voprf.Secp256k1, + group: group.Secp256k1, + hash: hash.SHA256, + curve: nil, + }, } func testAll(t *testing.T, f func(*configuration)) { @@ -102,16 +120,16 @@ func randomBytes(length int) []byte { return r } -func getBadNistElement(t *testing.T, id group.Group) []byte { - size := id.ElementLength() +func getBadNistElement(t *testing.T, g group.Group) []byte { + size := g.ElementLength() element := randomBytes(size) // detag compression element[0] = 4 // test if invalid compression is detected - err := id.NewElement().Decode(element) + err := g.NewElement().Decode(element) if err == nil { - t.Errorf("detagged compressed point did not yield an error for group %s", id) + t.Errorf("detagged compressed point did not yield an error for group %s", g) } return element @@ -183,19 +201,19 @@ func lengthPrefixEncode(input []byte) []byte { return append(i2osp2(len(input)), input...) } -func contextString(mode voprf.Mode, id voprf.Ciphersuite) []byte { - ctx := make([]byte, 0, len(voprf.Version)+3+len(id.String())) +func contextString(mode voprf.Mode, g voprf.Ciphersuite) []byte { + ctx := make([]byte, 0, len(voprf.Version)+3+len(g.String())) ctx = append(ctx, voprf.Version...) ctx = append(ctx, "-"...) ctx = append(ctx, byte(mode)) ctx = append(ctx, "-"...) - ctx = append(ctx, id.String()...) + ctx = append(ctx, g.String()...) return ctx } -func deriveKeyPair(seed, info []byte, mode voprf.Mode, id voprf.Ciphersuite) (*group.Scalar, *group.Element) { - dst := concatenate([]byte(deriveKeyPairDST), contextString(mode, id)) +func deriveKeyPair(seed, info []byte, mode voprf.Mode, g voprf.Ciphersuite) (*group.Scalar, *group.Element) { + dst := concatenate([]byte(deriveKeyPairDST), contextString(mode, g)) deriveInput := concatenate(seed, lengthPrefixEncode(info)) var counter uint8 @@ -206,9 +224,9 @@ func deriveKeyPair(seed, info []byte, mode voprf.Mode, id voprf.Ciphersuite) (*g panic("impossible to generate non-zero scalar") } - s = id.Group().HashToScalar(concatenate(deriveInput, []byte{counter}), dst) + s = g.Group().HashToScalar(concatenate(deriveInput, []byte{counter}), dst) counter++ } - return s, id.Group().Base().Multiply(s) + return s, g.Group().Base().Multiply(s) } diff --git a/tests/state_test.go b/tests/state_test.go index d312f30..595d89c 100644 --- a/tests/state_test.go +++ b/tests/state_test.go @@ -68,7 +68,7 @@ func TestClient_State(t *testing.T) { for _, mode := range []voprf.Mode{voprf.OPRF, voprf.VOPRF, voprf.POPRF} { t.Run(fmt.Sprintf("State test for mode %v", mode), func(t *testing.T) { - client, err := suite.Client(mode, kp.PublicKey) + client, err := suite.Client(mode, kp.PublicKey.Encode()) if err != nil { t.Fatal(err) } diff --git a/tests/vectors_test.go b/tests/vectors_test.go index d77ee6f..324d1fb 100644 --- a/tests/vectors_test.go +++ b/tests/vectors_test.go @@ -226,10 +226,10 @@ func (v vector) checkParams(t *testing.T) { //} } -func testBlind(t *testing.T, id voprf.Ciphersuite, client *voprf.Client, input, blind, expected, info []byte) { - s := id.Group().NewScalar() +func testBlind(t *testing.T, ciphersuite voprf.Ciphersuite, client *voprf.Client, input, blind, expected, info []byte) { + s := ciphersuite.Group().NewScalar() if err := s.Decode(blind); err != nil { - t.Fatal(fmt.Errorf("blind decoding to scalar in suite %v errored with %q", id, err)) + t.Fatal(fmt.Errorf("blind decoding to scalar in suite %v errored with %q", ciphersuite, err)) } client.SetBlinds([]*group.Scalar{s}) @@ -256,7 +256,7 @@ func testBlindBatchWithBlinds(t *testing.T, client *voprf.Client, inputs, blinds func testOPRF( t *testing.T, - id voprf.Ciphersuite, + ciphersuite voprf.Ciphersuite, mode voprf.Mode, client *voprf.Client, server *voprf.Server, @@ -266,7 +266,7 @@ func testOPRF( // OPRFClient Blinding if test.Batch == 1 { - testBlind(t, id, client, test.Input[0], test.Blind[0], test.BlindedElement[0], test.Info) + testBlind(t, ciphersuite, client, test.Input[0], test.Blind[0], test.BlindedElement[0], test.Info) } else { testBlindBatchWithBlinds(t, client, test.Input, test.Blind, test.BlindedElement, test.Info) } @@ -324,10 +324,6 @@ func testOPRF( if !bytes.Equal(test.Output[0], output) { t.Fatal("finalize() output is not valid.") } - - if !server.VerifyFinalize(test.Input[0], test.Info, output) { - t.Fatal("VerifyFinalize() returned false.") - } } else { output, err := client.FinalizeBatch(ev, test.Info) if err != nil { @@ -339,10 +335,6 @@ func testOPRF( t.Fatal("finalizeBatch() output is not valid.") } } - - if !server.VerifyFinalizeBatch(test.Input, output, test.Info) { - t.Fatal("VerifyFinalize() returned false.") - } } } diff --git a/tests/voprf_test.go b/tests/voprf_test.go index 71b044b..88a4b33 100644 --- a/tests/voprf_test.go +++ b/tests/voprf_test.go @@ -1,5 +1,4 @@ // SPDX-License-Identifier: MIT -// SPDX-License-Identifier: MIT // // Copyright (C) 2024 Daniel Bourdrez. All Rights Reserved. // @@ -105,6 +104,39 @@ func TestBatching(t *testing.T) { }) } +func TestAvailability(t *testing.T) { + testAll(t, func(c *configuration) { + if !c.ciphersuite.Available() { + t.Fatal("expected availability") + } + }) +} + +func TestCiphersuiteGroup(t *testing.T) { + testAll(t, func(c *configuration) { + if c.ciphersuite.Group() != c.group { + t.Fatal("expected equality") + } + + ciphersuite, err := voprf.FromGroup(c.group) + if err != nil { + t.Fatal(err) + } + + if ciphersuite != c.ciphersuite { + t.Fatal("expected equality") + } + }) +} + +func TestCiphersuiteHashes(t *testing.T) { + testAll(t, func(c *configuration) { + if c.hash != c.ciphersuite.Hash() { + t.Fatal("expected equality") + } + }) +} + func TestServerKeys(t *testing.T) { mode := voprf.OPRF @@ -149,9 +181,9 @@ func TestDeriveKeyPair(t *testing.T) { refPk := ciphersuite.Group().NewElement() _ = refPk.Decode(encodedReferencePublicKeyR255) - sk, pk := ciphersuite.DeriveKeyPair(voprf.OPRF, random, info) + keyPair := ciphersuite.DeriveKeyPair(voprf.OPRF, random, info) - if sk.Equal(refSk) != 1 || pk.Equal(refPk) != 1 { + if keyPair.SecretKey.Equal(refSk) != 1 || keyPair.PublicKey.Equal(refPk) != 1 { t.Fatal("expected equality") } } diff --git a/utils.go b/utils.go index 9b57cf4..a08cb6d 100644 --- a/utils.go +++ b/utils.go @@ -11,13 +11,16 @@ package voprf import ( "crypto/subtle" "encoding/binary" + + group "github.com/bytemare/crypto" ) -// KeyPair assembles a VOPRF key pair. The SecretKey can be used as the evaluation key for the group identified by ID. +// KeyPair assembles a VOPRF key pair. The SecretKey can be used as the evaluation key for +// the group identified by Ciphersuite. type KeyPair struct { - ID Ciphersuite - PublicKey []byte - SecretKey []byte + PublicKey *group.Element + SecretKey *group.Scalar + Ciphersuite Ciphersuite } func i2osp2(value int) []byte {