From 5f66c12d6ad29883cd6773c36b0902c89af76710 Mon Sep 17 00:00:00 2001 From: Javid Khan Date: Wed, 26 Aug 2026 23:13:31 +0530 Subject: [PATCH 1/2] oprf: reject input and info that overflow the length prefix --- oprf/client.go | 7 +++++++ oprf/keys.go | 6 ++++++ oprf/oprf_test.go | 38 ++++++++++++++++++++++++++++++++++++++ oprf/server.go | 7 +++++++ 4 files changed, 58 insertions(+) diff --git a/oprf/client.go b/oprf/client.go index 425e0486..03a842ea 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" @@ -51,6 +52,12 @@ func (c client) blind(inputs [][]byte, blinds []Blind) (*FinalizeData, *Evaluati blindedElements := make([]Blinded, len(inputs)) dst := c.params.getDST(hashToGroupDST) for i := range inputs { + // 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(inputs[i]) > math.MaxUint16 { + return nil, nil, ErrInvalidInput + } blind := blinds[i] if blind == nil || *blind.Group().Params() != *c.params.group.Params() || blind.IsZero() { return nil, nil, ErrInvalidInput 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..a7745091 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,43 @@ 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("blind", func(t *testing.T) { + client := NewClient(SuiteP256) + if _, _, err := client.Blind([][]byte{oversized}); err != ErrInvalidInput { + t.Fatalf("got %v, want %v", err, ErrInvalidInput) + } + _, _, err := client.Blind([][]byte{maxSized}) + test.CheckNoErr(t, err, "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 From 90571b3ffb2edee0ee61d46b137cd04cbe4dd673 Mon Sep 17 00:00:00 2001 From: Javid Khan Date: Mon, 21 Sep 2026 19:27:05 +0530 Subject: [PATCH 2/2] oprf: check the client input length in validate The length prefix is only written in finalizeHash, which every client Finalize reaches through validate, so bound the input there alongside the other finalize-state checks rather than in blind. The regression test now drives a full round and expects Finalize to reject a 65536-byte input while a 65535-byte input still succeeds. --- oprf/client.go | 12 ++++++------ oprf/oprf_test.go | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/oprf/client.go b/oprf/client.go index 03a842ea..007e23c1 100644 --- a/oprf/client.go +++ b/oprf/client.go @@ -52,12 +52,6 @@ func (c client) blind(inputs [][]byte, blinds []Blind) (*FinalizeData, *Evaluati blindedElements := make([]Blinded, len(inputs)) dst := c.params.getDST(hashToGroupDST) for i := range inputs { - // 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(inputs[i]) > math.MaxUint16 { - return nil, nil, ErrInvalidInput - } blind := blinds[i] if blind == nil || *blind.Group().Params() != *c.params.group.Params() || blind.IsZero() { return nil, nil, ErrInvalidInput @@ -107,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/oprf_test.go b/oprf/oprf_test.go index a7745091..efa3a8a8 100644 --- a/oprf/oprf_test.go +++ b/oprf/oprf_test.go @@ -288,13 +288,21 @@ func TestRejectsOversizedInput(t *testing.T) { oversized := make([]byte, math.MaxUint16+1) maxSized := make([]byte, math.MaxUint16) - t.Run("blind", func(t *testing.T) { + t.Run("finalize", func(t *testing.T) { client := NewClient(SuiteP256) - if _, _, err := client.Blind([][]byte{oversized}); err != ErrInvalidInput { + 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) } - _, _, err := client.Blind([][]byte{maxSized}) - test.CheckNoErr(t, err, "max-length input must be accepted") + test.CheckNoErr(t, finalize(maxSized), "max-length input must be accepted") }) t.Run("fullEvaluate", func(t *testing.T) {