diff --git a/oprf/client.go b/oprf/client.go index 425e0486..007e23c1 100644 --- a/oprf/client.go +++ b/oprf/client.go @@ -2,6 +2,7 @@ package oprf import ( "crypto/rand" + "math" "github.com/cloudflare/circl/group" "github.com/cloudflare/circl/zk/dleq" @@ -100,6 +101,12 @@ func (c client) validate(f *FinalizeData, e *Evaluation) (err error) { wantGroup := *c.params.group.Params() for i := range l { + // finalizeHash frames the input with I2OSP(len(input), 2), so an input + // of 2^16 bytes or more silently wraps the length prefix. Reject it, as + // scalarFromInfo already does for info. + if len(f.inputs[i]) > math.MaxUint16 { + return ErrInvalidInput + } blind := f.blinds[i] blinded := f.evalReq.Elements[i] evaluated := e.Elements[i] diff --git a/oprf/keys.go b/oprf/keys.go index cc0b96de..4ff9186f 100644 --- a/oprf/keys.go +++ b/oprf/keys.go @@ -3,6 +3,7 @@ package oprf import ( "encoding/binary" "io" + "math" "github.com/cloudflare/circl/group" ) @@ -112,6 +113,11 @@ func DeriveKey(s Suite, mode Mode, seed, info []byte) (*PrivateKey, error) { if len(seed) != 32 { return nil, ErrInvalidSeed } + // deriveInput frames info with I2OSP(len(info), 2), so info of 2^16 bytes + // or more silently wraps the length prefix. + if len(info) > math.MaxUint16 { + return nil, ErrInvalidInfo + } p.m = mode lenInfo := []byte{0, 0} diff --git a/oprf/oprf_test.go b/oprf/oprf_test.go index 9b346628..efa3a8a8 100644 --- a/oprf/oprf_test.go +++ b/oprf/oprf_test.go @@ -6,6 +6,7 @@ import ( "encoding" "encoding/binary" "fmt" + "math" "testing" "github.com/cloudflare/circl/group" @@ -277,6 +278,51 @@ func TestDeterministicBlindRejectsInvalidBlind(t *testing.T) { } } +func TestRejectsOversizedInput(t *testing.T) { + // RFC 9497 frames the input and info with I2OSP(len, 2), so a length of + // 2^16 or more wraps the prefix. Reject those, but keep accepting the + // maximum admissible length so valid callers are unaffected. + key, err := GenerateKey(SuiteP256, rand.Reader) + test.CheckNoErr(t, err, "failed private key generation") + + oversized := make([]byte, math.MaxUint16+1) + maxSized := make([]byte, math.MaxUint16) + + t.Run("finalize", func(t *testing.T) { + client := NewClient(SuiteP256) + server := NewServer(SuiteP256, key) + finalize := func(input []byte) error { + finData, evalReq, err := client.Blind([][]byte{input}) + test.CheckNoErr(t, err, "blind failed") + evaluation, err := server.Evaluate(evalReq) + test.CheckNoErr(t, err, "evaluate failed") + _, err = client.Finalize(finData, evaluation) + return err + } + if err := finalize(oversized); err != ErrInvalidInput { + t.Fatalf("got %v, want %v", err, ErrInvalidInput) + } + test.CheckNoErr(t, finalize(maxSized), "max-length input must be accepted") + }) + + t.Run("fullEvaluate", func(t *testing.T) { + server := NewServer(SuiteP256, key) + if _, err := server.FullEvaluate(oversized); err != ErrInvalidInput { + t.Fatalf("got %v, want %v", err, ErrInvalidInput) + } + _, err := server.FullEvaluate(maxSized) + test.CheckNoErr(t, err, "max-length input must be accepted") + }) + + t.Run("deriveKey", func(t *testing.T) { + if _, err := DeriveKey(SuiteP256, BaseMode, make([]byte, 32), oversized); err != ErrInvalidInfo { + t.Fatalf("got %v, want %v", err, ErrInvalidInfo) + } + _, err := DeriveKey(SuiteP256, BaseMode, make([]byte, 32), maxSized) + test.CheckNoErr(t, err, "max-length info must be accepted") + }) +} + func TestFinalizeRejectsMalformedState(t *testing.T) { key, err := GenerateKey(SuiteP256, rand.Reader) test.CheckNoErr(t, err, "failed private key generation") diff --git a/oprf/server.go b/oprf/server.go index 688b8564..6196110f 100644 --- a/oprf/server.go +++ b/oprf/server.go @@ -3,6 +3,7 @@ package oprf import ( "crypto/rand" "crypto/subtle" + "math" "github.com/cloudflare/circl/group" "github.com/cloudflare/circl/zk/dleq" @@ -91,6 +92,12 @@ func (s server) secretFromInfo(info []byte) (t, tInv group.Scalar, err error) { } func (s server) fullEvaluate(input, info []byte) ([]byte, error) { + // finalizeHash frames the input with I2OSP(len(input), 2), so an input of + // 2^16 bytes or more silently wraps the length prefix. Reject it, as + // scalarFromInfo already does for info. + if len(input) > math.MaxUint16 { + return nil, ErrInvalidInput + } evalSecret := s.privateKey.k if s.params.m == PartialObliviousMode { var err error