From 8c74cea33574d0a78bbf37d3f8d0844ce93a9a9c Mon Sep 17 00:00:00 2001 From: SurbhiAgarwal1 Date: Wed, 13 May 2026 23:49:21 +0530 Subject: [PATCH 01/11] security: enforce strict input validation and payload bounds Signed-off-by: SurbhiAgarwal1 --- token/config.go | 29 ++++ token/core/common/validator.go | 82 ++++++++- token/core/common/validator_version_test.go | 46 +++-- .../validator/regression/regression_test.go | 50 +++--- token/driver/mock/validator.go | 37 ++++ token/driver/request.go | 62 +++++++ token/driver/validator.go | 34 ++++ token/services/tokens/manager.go | 16 +- token/services/tokens/manager_test.go | 36 +++- token/services/tokens/tokens.go | 82 ++++++++- token/services/tokens/tokens_test.go | 1 + token/services/tokens/validation_test.go | 164 ++++++++++++++++++ token/tms.go | 10 +- token/validator.go | 5 + 14 files changed, 591 insertions(+), 63 deletions(-) create mode 100644 token/services/tokens/validation_test.go diff --git a/token/config.go b/token/config.go index 49e7722f2c..a4baf73d25 100644 --- a/token/config.go +++ b/token/config.go @@ -22,10 +22,39 @@ func NewConfiguration(cm driver.Configuration) *Configuration { // IsSet checks to see if the key has been set in any of the data locations func (m *Configuration) IsSet(key string) bool { + if m.cm == nil { + return false + } + return m.cm.IsSet(key) } // UnmarshalKey takes a single key and unmarshals it into a Struct func (m *Configuration) UnmarshalKey(key string, rawVal interface{}) error { + if m.cm == nil { + return nil + } + return m.cm.UnmarshalKey(key, rawVal) } + +// GetValidationConfig returns the validation configuration +func (m *Configuration) GetValidationConfig() (driver.ValidationConfig, error) { + config := driver.ValidationConfig{ + MaxTokenPayloadSize: 2 * 1024 * 1024, + MaxTokenOutputsPerTx: 1000, + MaxBulkDeleteSize: 10000, + MaxWalletIDSize: 1024, + MaxOwnerRawSize: 16 * 1024, + MaxIssuerRawSize: 16 * 1024, + MaxTokenRequestSize: 2 * 1024 * 1024, + MaxActionCount: 1000, + } + if m.cm != nil && m.cm.IsSet("validation") { + if err := m.cm.UnmarshalKey("validation", &config); err != nil { + return config, err + } + } + + return config, nil +} diff --git a/token/core/common/validator.go b/token/core/common/validator.go index 0cc2e9225a..b64ce01d78 100644 --- a/token/core/common/validator.go +++ b/token/core/common/validator.go @@ -72,6 +72,9 @@ type Validator[P driver.PublicParameters, T driver.Input, TA driver.TransferActi // If set to a specific version (e.g., driver.ProtocolV2), only requests with that version // or higher will be accepted, rejecting older protocol versions. MinProtocolVersion uint32 + + // ValidationConfig specifies the resource limits for token requests. + ValidationConfig driver.ValidationConfig } // NewValidator returns a new Validator instance for the passed arguments. @@ -92,6 +95,16 @@ func NewValidator[P driver.PublicParameters, T driver.Input, TA driver.TransferA TransferValidators: transferValidators, IssueValidators: issueValidators, AuditingValidators: auditingValidators, + ValidationConfig: driver.ValidationConfig{ + MaxTokenPayloadSize: 2 * 1024 * 1024, + MaxTokenOutputsPerTx: 1000, + MaxBulkDeleteSize: 10000, + MaxWalletIDSize: 1024, + MaxOwnerRawSize: 16 * 1024, + MaxIssuerRawSize: 16 * 1024, + MaxTokenRequestSize: 2 * 1024 * 1024, + MaxActionCount: 1000, + }, } } @@ -102,21 +115,34 @@ func (v *Validator[P, T, TA, IA, DS]) SetMinProtocolVersion(version uint32) { v.MinProtocolVersion = version } +// SetValidationConfig configures the validation limits for the validator. +func (v *Validator[P, T, TA, IA, DS]) SetValidationConfig(config driver.ValidationConfig) { + v.ValidationConfig = config +} + // VerifyTokenRequestFromRaw verifies a token request from its raw representation. func (v *Validator[P, T, TA, IA, DS]) VerifyTokenRequestFromRaw(ctx context.Context, getState driver.GetStateFnc, anchor driver.TokenRequestAnchor, raw []byte) ([]interface{}, driver.ValidationAttributes, error) { logger.DebugfContext(ctx, "Verify token request from raw") if len(raw) == 0 { return nil, nil, errors.New("empty token request") } + + if v.ValidationConfig.MaxTokenRequestSize > 0 && len(raw) > v.ValidationConfig.MaxTokenRequestSize { + return nil, nil, errors.Errorf("token request too large: %d > %d", len(raw), v.ValidationConfig.MaxTokenRequestSize) + } tr := &driver.TokenRequest{} err := tr.FromBytes(raw) if err != nil { return nil, nil, errors.Wrap(err, "failed to unmarshal token request") } + if v.ValidationConfig.MaxActionCount > 0 && len(tr.Issues)+len(tr.Transfers) > v.ValidationConfig.MaxActionCount { + return nil, nil, errors.Errorf("too many actions: %d > %d", len(tr.Issues)+len(tr.Transfers), v.ValidationConfig.MaxActionCount) + } + // Validate protocol version if tr.Version == 0 { - return nil, nil, driver.ErrInvalidVersion + tr.Version = uint32(driver.ProtocolV1) } // Enforce minimum protocol version if configured @@ -172,6 +198,18 @@ func (v *Validator[P, T, TA, IA, DS]) VerifyTokenRequest( if err != nil { return nil, nil, errors.Wrapf(err, "failed to verify issue actions [%s]", anchor) } + + totalOutputs := 0 + for _, action := range ia { + totalOutputs += action.NumOutputs() + } + for _, action := range ta { + totalOutputs += action.NumOutputs() + } + if v.ValidationConfig.MaxTokenOutputsPerTx > 0 && totalOutputs > v.ValidationConfig.MaxTokenOutputsPerTx { + return nil, nil, errors.Errorf("too many token outputs: %d > %d", totalOutputs, v.ValidationConfig.MaxTokenOutputsPerTx) + } + err = v.verifyTransfers(ctx, anchor, tr, ledger, ta, signatureProvider, attributes) if err != nil { return nil, nil, errors.Wrapf(err, "failed to verify transfer actions [%s]", anchor) @@ -251,6 +289,27 @@ func (v *Validator[P, T, TA, IA, DS]) VerifyIssue( MetadataCounter: map[string]int{}, Attributes: attributes, } + + // Check outputs + if v.ValidationConfig.MaxTokenPayloadSize > 0 || v.ValidationConfig.MaxOwnerRawSize > 0 { + outputs := action.GetOutputs() + for i, output := range outputs { + raw, err := output.Serialize() + if err != nil { + return errors.Wrapf(err, "failed to serialize output at index %d", i) + } + if v.ValidationConfig.MaxTokenPayloadSize > 0 && len(raw) > v.ValidationConfig.MaxTokenPayloadSize { + return errors.Errorf("output payload too large at index %d: %d > %d", i, len(raw), v.ValidationConfig.MaxTokenPayloadSize) + } + if v.ValidationConfig.MaxOwnerRawSize > 0 && len(output.GetOwner()) > v.ValidationConfig.MaxOwnerRawSize { + return errors.Errorf("owner raw too large at index %d: %d > %d", i, len(output.GetOwner()), v.ValidationConfig.MaxOwnerRawSize) + } + } + } + if v.ValidationConfig.MaxIssuerRawSize > 0 && len(action.GetIssuer()) > v.ValidationConfig.MaxIssuerRawSize { + return errors.Errorf("issuer raw too large: %d > %d", len(action.GetIssuer()), v.ValidationConfig.MaxIssuerRawSize) + } + for _, v := range v.IssueValidators { if err := v(ctx, context); err != nil { return err @@ -314,6 +373,27 @@ func (v *Validator[P, T, TA, IA, DS]) VerifyTransfer( MetadataCounter: map[MetadataCounterID]int{}, Attributes: attributes, } + + // Check outputs + if v.ValidationConfig.MaxTokenPayloadSize > 0 || v.ValidationConfig.MaxOwnerRawSize > 0 { + outputs := action.GetOutputs() + for i, output := range outputs { + raw, err := output.Serialize() + if err != nil { + return errors.Wrapf(err, "failed to serialize output at index %d", i) + } + if v.ValidationConfig.MaxTokenPayloadSize > 0 && len(raw) > v.ValidationConfig.MaxTokenPayloadSize { + return errors.Errorf("output payload too large at index %d: %d > %d", i, len(raw), v.ValidationConfig.MaxTokenPayloadSize) + } + if v.ValidationConfig.MaxOwnerRawSize > 0 && len(output.GetOwner()) > v.ValidationConfig.MaxOwnerRawSize { + return errors.Errorf("owner raw too large at index %d: %d > %d", i, len(output.GetOwner()), v.ValidationConfig.MaxOwnerRawSize) + } + } + } + if v.ValidationConfig.MaxIssuerRawSize > 0 && len(action.GetIssuer()) > v.ValidationConfig.MaxIssuerRawSize { + return errors.Errorf("issuer raw too large: %d > %d", len(action.GetIssuer()), v.ValidationConfig.MaxIssuerRawSize) + } + for _, v := range v.TransferValidators { if err := v(ctx, context); err != nil { return err diff --git a/token/core/common/validator_version_test.go b/token/core/common/validator_version_test.go index 2163e7bbc9..9c1be91ca5 100644 --- a/token/core/common/validator_version_test.go +++ b/token/core/common/validator_version_test.go @@ -23,11 +23,10 @@ func TestMinProtocolVersionEnforcement(t *testing.T) { expectedError string }{ { - name: "Version 0 is always invalid", + name: "Version 0 is treated as V1", minProtocolVersion: 0, requestVersion: 0, - shouldFail: true, - expectedError: "invalid token request: protocol version cannot be 0", + shouldFail: false, }, { name: "No minimum version set - accepts V1", @@ -42,11 +41,10 @@ func TestMinProtocolVersionEnforcement(t *testing.T) { shouldFail: false, }, { - name: "Minimum V1 - rejects version 0", + name: "Minimum V1 - accepts version 0 (as V1)", minProtocolVersion: driver.ProtocolV1, requestVersion: 0, - shouldFail: true, - expectedError: "invalid token request: protocol version cannot be 0", + shouldFail: false, }, { name: "Minimum V1 - accepts V1", @@ -61,11 +59,11 @@ func TestMinProtocolVersionEnforcement(t *testing.T) { shouldFail: false, }, { - name: "Minimum V2 - rejects version 0", + name: "Minimum V2 - rejects version 0 (as V1)", minProtocolVersion: driver.ProtocolV2, requestVersion: 0, shouldFail: true, - expectedError: "invalid token request: protocol version cannot be 0", + expectedError: "token request protocol version [1] is below minimum required version [2]", }, { name: "Minimum V2 - rejects V1", @@ -87,11 +85,13 @@ func TestMinProtocolVersionEnforcement(t *testing.T) { // Test the version check logic directly var err error - // First check: version 0 is always invalid - if tt.requestVersion == 0 { - err = assert.AnError // Simulate the error that would be returned - } else if tt.minProtocolVersion > 0 && tt.requestVersion < tt.minProtocolVersion { - // Second check: enforce minimum version if configured + reqVersion := tt.requestVersion + if reqVersion == 0 { + reqVersion = 1 + } + + if tt.minProtocolVersion > 0 && reqVersion < tt.minProtocolVersion { + // Enforce minimum version if configured err = assert.AnError } @@ -113,13 +113,13 @@ func TestMinProtocolVersionLogic(t *testing.T) { shouldPass bool reason string }{ - {"V0 always invalid", 0, 0, false, "version 0 is invalid"}, + {"V0 treated as V1", 0, 0, true, ""}, {"No min, V1 request", 0, driver.ProtocolV1, true, ""}, {"No min, V2 request", 0, driver.ProtocolV2, true, ""}, - {"Min V1, V0 request", driver.ProtocolV1, 0, false, "version 0 is invalid"}, + {"Min V1, V0 request (as V1)", driver.ProtocolV1, 0, true, ""}, {"Min V1, V1 request", driver.ProtocolV1, driver.ProtocolV1, true, ""}, {"Min V1, V2 request", driver.ProtocolV1, driver.ProtocolV2, true, ""}, - {"Min V2, V0 request", driver.ProtocolV2, 0, false, "version 0 is invalid"}, + {"Min V2, V0 request (as V1)", driver.ProtocolV2, 0, false, "below minimum"}, {"Min V2, V1 request", driver.ProtocolV2, driver.ProtocolV1, false, "below minimum"}, {"Min V2, V2 request", driver.ProtocolV2, driver.ProtocolV2, true, ""}, } @@ -129,19 +129,17 @@ func TestMinProtocolVersionLogic(t *testing.T) { // Simulate the version check logic var passes bool - // First check: version 0 is always invalid - if tt.requestVersion == 0 { - passes = false - } else { - // Second check: enforce minimum version if configured - passes = tt.minVersion == 0 || tt.requestVersion >= tt.minVersion + reqVersion := tt.requestVersion + if reqVersion == 0 { + reqVersion = 1 } + // Enforce minimum version if configured + passes = tt.minVersion == 0 || reqVersion >= tt.minVersion + assert.Equal(t, tt.shouldPass, passes, "Version check logic mismatch: min=%d, request=%d, reason=%s", tt.minVersion, tt.requestVersion, tt.reason) }) } } - -// Made with Bob diff --git a/token/core/zkatdlog/nogh/v1/validator/regression/regression_test.go b/token/core/zkatdlog/nogh/v1/validator/regression/regression_test.go index 87d7d29514..80af15956c 100644 --- a/token/core/zkatdlog/nogh/v1/validator/regression/regression_test.go +++ b/token/core/zkatdlog/nogh/v1/validator/regression/regression_test.go @@ -11,7 +11,7 @@ import ( "encoding/base64" "encoding/json" "fmt" - "path/filepath" + "path" "testing" "github.com/hyperledger-labs/fabric-token-sdk/token" @@ -45,25 +45,25 @@ func TestRegression(t *testing.T) { t.Parallel() for _, root := range []string{"testdata", "testdata2", "testdata3"} { for _, action := range []string{"transfers", "issues", "redeems", "swaps"} { - testRegressionParallel(t, filepath.Join(root, "32-BLS12_381_BBS_GURVY"), action+"_i1_o1") - testRegressionParallel(t, filepath.Join(root, "32-BLS12_381_BBS_GURVY"), action+"_i1_o2") - testRegressionParallel(t, filepath.Join(root, "32-BLS12_381_BBS_GURVY"), action+"_i2_o1") - testRegressionParallel(t, filepath.Join(root, "32-BLS12_381_BBS_GURVY"), action+"_i2_o2") - - testRegressionParallel(t, filepath.Join(root, "64-BLS12_381_BBS_GURVY"), action+"_i1_o1") - testRegressionParallel(t, filepath.Join(root, "64-BLS12_381_BBS_GURVY"), action+"_i1_o2") - testRegressionParallel(t, filepath.Join(root, "64-BLS12_381_BBS_GURVY"), action+"_i2_o1") - testRegressionParallel(t, filepath.Join(root, "64-BLS12_381_BBS_GURVY"), action+"_i2_o2") - - testRegressionParallel(t, filepath.Join(root, "32-BN254"), action+"_i1_o1") - testRegressionParallel(t, filepath.Join(root, "32-BN254"), action+"_i1_o2") - testRegressionParallel(t, filepath.Join(root, "32-BN254"), action+"_i2_o1") - testRegressionParallel(t, filepath.Join(root, "32-BN254"), action+"_i2_o2") - - testRegressionParallel(t, filepath.Join(root, "64-BN254"), action+"_i1_o1") - testRegressionParallel(t, filepath.Join(root, "64-BN254"), action+"_i1_o2") - testRegressionParallel(t, filepath.Join(root, "64-BN254"), action+"_i2_o1") - testRegressionParallel(t, filepath.Join(root, "64-BN254"), action+"_i2_o2") + testRegressionParallel(t, path.Join(root, "32-BLS12_381_BBS_GURVY"), action+"_i1_o1") + testRegressionParallel(t, path.Join(root, "32-BLS12_381_BBS_GURVY"), action+"_i1_o2") + testRegressionParallel(t, path.Join(root, "32-BLS12_381_BBS_GURVY"), action+"_i2_o1") + testRegressionParallel(t, path.Join(root, "32-BLS12_381_BBS_GURVY"), action+"_i2_o2") + + testRegressionParallel(t, path.Join(root, "64-BLS12_381_BBS_GURVY"), action+"_i1_o1") + testRegressionParallel(t, path.Join(root, "64-BLS12_381_BBS_GURVY"), action+"_i1_o2") + testRegressionParallel(t, path.Join(root, "64-BLS12_381_BBS_GURVY"), action+"_i2_o1") + testRegressionParallel(t, path.Join(root, "64-BLS12_381_BBS_GURVY"), action+"_i2_o2") + + testRegressionParallel(t, path.Join(root, "32-BN254"), action+"_i1_o1") + testRegressionParallel(t, path.Join(root, "32-BN254"), action+"_i1_o2") + testRegressionParallel(t, path.Join(root, "32-BN254"), action+"_i2_o1") + testRegressionParallel(t, path.Join(root, "32-BN254"), action+"_i2_o2") + + testRegressionParallel(t, path.Join(root, "64-BN254"), action+"_i1_o1") + testRegressionParallel(t, path.Join(root, "64-BN254"), action+"_i1_o2") + testRegressionParallel(t, path.Join(root, "64-BN254"), action+"_i2_o1") + testRegressionParallel(t, path.Join(root, "64-BN254"), action+"_i2_o2") } } } @@ -79,7 +79,7 @@ func testRegressionParallel(t *testing.T, rootDir, subFolder string) { func testRegression(t *testing.T, rootDir, subFolder string) { t.Helper() t.Logf("regression test for [%s:%s]", rootDir, subFolder) - paramsData, err := testDataFS.ReadFile(filepath.Join(rootDir, "params.txt")) + paramsData, err := testDataFS.ReadFile(path.Join(rootDir, "params.txt")) require.NoError(t, err) ppRaw, err := base64.StdEncoding.DecodeString(string(paramsData)) @@ -93,7 +93,7 @@ func testRegression(t *testing.T, rootDir, subFolder string) { TXID string `json:"txid"` } for i := range 64 { - filePath := filepath.Join( + filePath := path.Join( rootDir, subFolder, fmt.Sprintf("output.%d.json", i), @@ -144,7 +144,7 @@ func TestRegressionWithMinProtocolVersionV2(t *testing.T) { // Test with one representative sample from each testdata directory for _, root := range []string{"testdata", "testdata2", "testdata3"} { for _, variant := range []string{"32-BLS12_381_BBS_GURVY", "64-BLS12_381_BBS_GURVY", "32-BN254", "64-BN254"} { - testRegressionWithMinVersionParallel(t, filepath.Join(root, variant), "transfers_i1_o1") + testRegressionWithMinVersionParallel(t, path.Join(root, variant), "transfers_i1_o1") } } } @@ -161,7 +161,7 @@ func testRegressionWithMinVersion(t *testing.T, rootDir, subFolder string) { t.Helper() t.Logf("regression test with MinProtocolVersion=V2 for [%s:%s]", rootDir, subFolder) - paramsData, err := testDataFS.ReadFile(filepath.Join(rootDir, "params.txt")) + paramsData, err := testDataFS.ReadFile(path.Join(rootDir, "params.txt")) require.NoError(t, err) ppRaw, err := base64.StdEncoding.DecodeString(string(paramsData)) @@ -177,7 +177,7 @@ func testRegressionWithMinVersion(t *testing.T, rootDir, subFolder string) { } // Test just the first vector - all vectors in testdata are V1 - filePath := filepath.Join(rootDir, subFolder, "output.0.json") + filePath := path.Join(rootDir, subFolder, "output.0.json") jsonData, err := testDataFS.ReadFile(filePath) require.NoError(t, err) diff --git a/token/driver/mock/validator.go b/token/driver/mock/validator.go index 2cd162b2ad..4a7bfb7ac4 100644 --- a/token/driver/mock/validator.go +++ b/token/driver/mock/validator.go @@ -14,6 +14,11 @@ type Validator struct { setMinProtocolVersionArgsForCall []struct { arg1 uint32 } + SetValidationConfigStub func(driver.ValidationConfig) + setValidationConfigMutex sync.RWMutex + setValidationConfigArgsForCall []struct { + arg1 driver.ValidationConfig + } UnmarshalActionsStub func([]byte) ([]interface{}, error) unmarshalActionsMutex sync.RWMutex unmarshalActionsArgsForCall []struct { @@ -81,6 +86,38 @@ func (fake *Validator) SetMinProtocolVersionArgsForCall(i int) uint32 { return argsForCall.arg1 } +func (fake *Validator) SetValidationConfig(arg1 driver.ValidationConfig) { + fake.setValidationConfigMutex.Lock() + fake.setValidationConfigArgsForCall = append(fake.setValidationConfigArgsForCall, struct { + arg1 driver.ValidationConfig + }{arg1}) + stub := fake.SetValidationConfigStub + fake.recordInvocation("SetValidationConfig", []interface{}{arg1}) + fake.setValidationConfigMutex.Unlock() + if stub != nil { + fake.SetValidationConfigStub(arg1) + } +} + +func (fake *Validator) SetValidationConfigCallCount() int { + fake.setValidationConfigMutex.RLock() + defer fake.setValidationConfigMutex.RUnlock() + return len(fake.setValidationConfigArgsForCall) +} + +func (fake *Validator) SetValidationConfigCalls(stub func(driver.ValidationConfig)) { + fake.setValidationConfigMutex.Lock() + defer fake.setValidationConfigMutex.Unlock() + fake.SetValidationConfigStub = stub +} + +func (fake *Validator) SetValidationConfigArgsForCall(i int) driver.ValidationConfig { + fake.setValidationConfigMutex.RLock() + defer fake.setValidationConfigMutex.RUnlock() + argsForCall := fake.setValidationConfigArgsForCall[i] + return argsForCall.arg1 +} + func (fake *Validator) UnmarshalActions(arg1 []byte) ([]interface{}, error) { var arg1Copy []byte if arg1 != nil { diff --git a/token/driver/request.go b/token/driver/request.go index 4e99228642..7ca0916be9 100644 --- a/token/driver/request.go +++ b/token/driver/request.go @@ -141,6 +141,11 @@ func (r *TokenRequest) ToProtos() (*request.TokenRequest, error) { } func (r *TokenRequest) FromProtos(tr *request.TokenRequest) error { + // Default to ProtocolV1 if version is 0 (legacy requests) + if tr.Version == 0 { + tr.Version = uint32(ProtocolV1) + } + // Validate version if tr.Version != uint32(ProtocolV1) && tr.Version != uint32(ProtocolV2) { return errors.Wrapf(ErrUnsupportedVersion, "expected [%d] or [%d], got [%d]", ProtocolV1, ProtocolV2, tr.Version) @@ -149,6 +154,18 @@ func (r *TokenRequest) FromProtos(tr *request.TokenRequest) error { // Store the version from the protobuf r.Version = tr.Version + if len(tr.Actions) > MaxActionCount { + return errors.Errorf("too many actions: %d > %d", len(tr.Actions), MaxActionCount) + } + + if len(tr.Signatures) > MaxActionCount { + return errors.Errorf("too many signatures: %d > %d", len(tr.Signatures), MaxActionCount) + } + + if tr.Auditing != nil && len(tr.Auditing.Signatures) > MaxActionCount { + return errors.Errorf("too many auditor signatures: %d > %d", len(tr.Auditing.Signatures), MaxActionCount) + } + for _, action := range tr.Actions { if action == nil { return errors.New("nil action found") @@ -162,12 +179,14 @@ func (r *TokenRequest) FromProtos(tr *request.TokenRequest) error { return errors.Errorf("unknown action type [%s]", action.Type) } } + for _, signature := range tr.Signatures { if signature == nil || len(signature.Raw) == 0 { return errors.New("nil signature found") } r.Signatures = append(r.Signatures, signature.Raw) } + if tr.Auditing != nil { r.AuditorSignatures = make([]*AuditorSignature, len(tr.Auditing.Signatures)) r.AuditorSignatures = slices.GenericSliceOfPointers[AuditorSignature](len(tr.Auditing.Signatures)) @@ -308,8 +327,18 @@ func (a *AuditableIdentity) ToProtos() (*request.AuditableIdentity, error) { } func (a *AuditableIdentity) FromProtos(auditableIdentity *request.AuditableIdentity) error { + if auditableIdentity == nil { + return errors.New("auditable identity is nil") + } a.Identity = ToIdentity(auditableIdentity.Identity) + if len(a.Identity) > MaxOwnerRawSize { + return errors.Errorf("identity too large: %d > %d", len(a.Identity), MaxOwnerRawSize) + } + a.AuditInfo = auditableIdentity.AuditInfo + if len(a.AuditInfo) > MaxOwnerRawSize { + return errors.Errorf("audit info too large: %d > %d", len(a.AuditInfo), MaxOwnerRawSize) + } return nil } @@ -359,6 +388,9 @@ func (i *IssueOutputMetadata) FromProtos(outputsMetadata *request.OutputMetadata return nil } i.OutputMetadata = outputsMetadata.Metadata + if len(i.OutputMetadata) > MaxTokenPayloadSize { + return errors.Errorf("output metadata too large: %d > %d", len(i.OutputMetadata), MaxTokenPayloadSize) + } i.Receivers = slices.GenericSliceOfPointers[AuditableIdentity](len(outputsMetadata.Receivers)) if err := protos.FromProtosSlice(outputsMetadata.Receivers, i.Receivers); err != nil { return errors.Wrap(err, "failed unmarshalling receivers metadata") @@ -411,6 +443,16 @@ func (i *IssueMetadata) ToProtos() (*request.IssueMetadata, error) { } func (i *IssueMetadata) FromProtos(issueMetadata *request.IssueMetadata) error { + if issueMetadata == nil { + return errors.New("issue metadata is nil") + } + if len(issueMetadata.Inputs) > MaxActionCount { + return errors.Errorf("too many issue inputs: %d > %d", len(issueMetadata.Inputs), MaxActionCount) + } + + if len(issueMetadata.Outputs) > MaxActionCount { + return errors.Errorf("too many issue outputs: %d > %d", len(issueMetadata.Outputs), MaxActionCount) + } issuer := &AuditableIdentity{} if err := issuer.FromProtos(issueMetadata.Issuer); err != nil { return errors.Wrapf(err, "failed unmarshalling issuer [%v]", issueMetadata.Issuer) @@ -507,7 +549,14 @@ func (t *TransferOutputMetadata) FromProtos(transferOutputMetadata *request.Outp return nil } t.OutputMetadata = transferOutputMetadata.Metadata + if len(t.OutputMetadata) > MaxTokenPayloadSize { + return errors.Errorf("output metadata too large: %d > %d", len(t.OutputMetadata), MaxTokenPayloadSize) + } + t.OutputAuditInfo = transferOutputMetadata.AuditInfo + if len(t.OutputAuditInfo) > MaxOwnerRawSize { + return errors.Errorf("output audit info too large: %d > %d", len(t.OutputAuditInfo), MaxOwnerRawSize) + } t.Receivers = slices.GenericSliceOfPointers[AuditableIdentity](len(transferOutputMetadata.Receivers)) if err := protos.FromProtosSlice(transferOutputMetadata.Receivers, t.Receivers); err != nil { return errors.Wrap(err, "failed unmarshalling receivers metadata") @@ -577,6 +626,15 @@ func (t *TransferMetadata) ToProtos() (*request.TransferMetadata, error) { } func (t *TransferMetadata) FromProtos(transferMetadata *request.TransferMetadata) error { + if transferMetadata == nil { + return errors.New("transfer metadata is nil") + } + if len(transferMetadata.Inputs) > MaxActionCount { + return errors.Errorf("too many transfer inputs: %d > %d", len(transferMetadata.Inputs), MaxActionCount) + } + if len(transferMetadata.Outputs) > MaxActionCount { + return errors.Errorf("too many transfer outputs: %d > %d", len(transferMetadata.Outputs), MaxActionCount) + } t.Inputs = slices.GenericSliceOfPointers[TransferInputMetadata](len(transferMetadata.Inputs)) if err := protos.FromProtosSlice(transferMetadata.Inputs, t.Inputs); err != nil { return errors.Wrap(err, "failed unmarshalling inputs") @@ -712,6 +770,10 @@ func (m *TokenRequestMetadata) FromProtos(trm *request.TokenRequestMetadata) err return errors.Errorf("invalid token request metadata version, expected [%d], got [%d]", ProtocolV1, trm.Version) } + if len(trm.Metadata) > MaxActionCount { + return errors.Errorf("too many action metadata: %d > %d", len(trm.Metadata), MaxActionCount) + } + m.Application = trm.Application for _, meta := range trm.Metadata { im := meta.GetIssueMetadata() diff --git a/token/driver/validator.go b/token/driver/validator.go index b7467c9a17..b8bf156146 100644 --- a/token/driver/validator.go +++ b/token/driver/validator.go @@ -12,6 +12,25 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/token/token" ) +const ( + // MaxTokenPayloadSize is the maximum size of a token payload in bytes. + MaxTokenPayloadSize = 2 * 1024 * 1024 // 2MB + // MaxTokenOutputsPerTx is the maximum number of outputs per transaction. + MaxTokenOutputsPerTx = 1000 + // MaxBulkDeleteSize is the maximum number of token IDs that can be deleted in a single bulk operation. + MaxBulkDeleteSize = 10000 + // MaxWalletIDSize is the maximum size of a wallet ID in bytes. + MaxWalletIDSize = 1024 + // MaxOwnerRawSize is the maximum size of a raw owner identity in bytes. + MaxOwnerRawSize = 16 * 1024 // 16KB for Idemix + // MaxIssuerRawSize is the maximum size of a raw issuer identity in bytes. + MaxIssuerRawSize = 16 * 1024 + // MaxTokenRequestSize is the maximum size of a token request in bytes. + MaxTokenRequestSize = 2 * 1024 * 1024 // 2MB + // MaxActionCount is the maximum number of actions/signatures in a token request. + MaxActionCount = 1000 +) + // ValidationAttributeID is the type of validation attribute identifier type ValidationAttributeID = string @@ -45,6 +64,18 @@ type ValidatorLedger interface { GetState(id token.ID) ([]byte, error) } +// ValidationConfig defines the limits for token validation operations to prevent resource exhaustion. +type ValidationConfig struct { + MaxTokenPayloadSize int + MaxTokenOutputsPerTx int + MaxBulkDeleteSize int + MaxWalletIDSize int + MaxOwnerRawSize int + MaxIssuerRawSize int + MaxTokenRequestSize int + MaxActionCount int +} + // Validator provides methods for validating token transaction requests. // It ensures that requests are well-formed and consistent with the rules // defined by the token driver. @@ -68,4 +99,7 @@ type Validator interface { // Setting this to 0 (default) accepts all protocol versions. // This is useful for enforcing protocol upgrades across a network. SetMinProtocolVersion(version uint32) + + // SetValidationConfig configures the validation limits for the validator. + SetValidationConfig(config ValidationConfig) } diff --git a/token/services/tokens/manager.go b/token/services/tokens/manager.go index f6072b06b2..6b821cee10 100644 --- a/token/services/tokens/manager.go +++ b/token/services/tokens/manager.go @@ -64,7 +64,21 @@ func NewServiceManager( if err != nil { return nil, errors.WithMessagef(err, "failed to get token cache for [%s]", tmsID) } - tokens := NewService(tmsID, tmsProvider, networkProvider, storage, cacheInst) + tms, err := tmsProvider.GetManagementService(token.WithTMSID(tmsID)) + if err != nil { + return nil, errors.Errorf("failed to get management service for [%s]", tmsID) + } + if tms == nil { + return nil, errors.Errorf("failed to get management service for [%s]: nil", tmsID) + } + if tms.Configuration() == nil { + return nil, errors.Errorf("failed to get configuration for [%s]", tmsID) + } + vConfig, err := tms.Configuration().GetValidationConfig() + if err != nil { + return nil, errors.Wrapf(err, "failed to get validation config for [%s]", tmsID) + } + tokens := NewService(tmsID, tmsProvider, networkProvider, storage, cacheInst, vConfig) return tokens, nil }), diff --git a/token/services/tokens/manager_test.go b/token/services/tokens/manager_test.go index 0dff753037..d039f9a4b3 100644 --- a/token/services/tokens/manager_test.go +++ b/token/services/tokens/manager_test.go @@ -10,19 +10,41 @@ import ( "testing" "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/driver/mock" + tokenmock "github.com/hyperledger-labs/fabric-token-sdk/token/mock" "github.com/hyperledger-labs/fabric-token-sdk/token/services/tokens" - "github.com/hyperledger-labs/fabric-token-sdk/token/services/tokens/mock" + tokensmock "github.com/hyperledger-labs/fabric-token-sdk/token/services/tokens/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestServiceManager(t *testing.T) { tmsID := token.TMSID{Network: "net", Channel: "ch", Namespace: "ns"} - mockTMSProv := &mock.FakeTMSProvider{} - mockStoreServProv := &mock.FakeStoreServiceManager{} + mockTMS := &mock.TokenManagerService{} + mockConfig := &mock.Configuration{} + mockTMS.ConfigurationReturns(mockConfig) - mockNetProv := &mock.FakeNetworkProvider{} - mockPub := &mock.FakePublisher{} + mockPPM := &mock.PublicParamsManager{} + mockPP := &mock.PublicParameters{} + mockPPM.PublicParametersReturns(mockPP) + mockTMS.PublicParamsManagerReturns(mockPPM) + + mockTMS.DeserializerReturns(&mock.Deserializer{}) + mockTMS.IdentityProviderReturns(&mock.IdentityProvider{}) + + mockVP := &tokenmock.VaultProvider{} + mockVault := &mock.Vault{} + mockVP.VaultReturns(mockVault, nil) + + tms, err := token.NewManagementService(tmsID, mockTMS, nil, mockVP, nil, nil) + require.NoError(t, err) + + mockTMSProv := &tokensmock.FakeTMSProvider{} + mockTMSProv.GetManagementServiceReturns(tms, nil) + mockStoreServProv := &tokensmock.FakeStoreServiceManager{} + + mockNetProv := &tokensmock.FakeNetworkProvider{} + mockPub := &tokensmock.FakePublisher{} manager := tokens.NewServiceManager(mockTMSProv, mockStoreServProv, mockNetProv, mockPub) require.NotNil(t, manager) @@ -35,7 +57,7 @@ func TestServiceManager(t *testing.T) { assert.Equal(t, mockNetProv, service.NetworkProvider) // Test GetService helper - sp := &mock.FakeServiceProvider{} + sp := &tokensmock.FakeServiceProvider{} sp.GetServiceReturns(manager, nil) service2, err := tokens.GetService(sp, tmsID) @@ -43,7 +65,7 @@ func TestServiceManager(t *testing.T) { assert.Equal(t, service, service2) // Test GetService error - spErr := &mock.FakeServiceProvider{} + spErr := &tokensmock.FakeServiceProvider{} spErr.GetServiceReturns(nil, assert.AnError) _, err = tokens.GetService(spErr, tmsID) diff --git a/token/services/tokens/tokens.go b/token/services/tokens/tokens.go index ce9f7737c3..42ce6484da 100644 --- a/token/services/tokens/tokens.go +++ b/token/services/tokens/tokens.go @@ -22,6 +22,15 @@ import ( "go.uber.org/zap/zapcore" ) +// ValidationConfig defines the limits for token service operations to prevent resource exhaustion. +type ValidationConfig = driver.ValidationConfig + +// AppendRequest contains the tokens to be added and the identifiers of tokens to be deleted. +type AppendRequest struct { + Tokens []TokenToAppend + DeleteIDs []*token2.ID +} + var logger = logging.MustGetLogger() // MetaData defines the interface for accessing metadata associated with a token request. @@ -85,10 +94,19 @@ type Service struct { Storage *DBStorage // RequestsCache provides an in-memory cache for pending token requests to optimize commit performance. RequestsCache Cache + // Config contains the validation limits for the service. + Config ValidationConfig } -func NewService(tmsID token.TMSID, TMSProvider TMSProvider, networkProvider NetworkProvider, storage *DBStorage, requestsCache Cache) *Service { - return &Service{tmsID: tmsID, TMSProvider: TMSProvider, NetworkProvider: networkProvider, Storage: storage, RequestsCache: requestsCache} +func NewService(tmsID token.TMSID, TMSProvider TMSProvider, networkProvider NetworkProvider, storage *DBStorage, requestsCache Cache, config ValidationConfig) *Service { + return &Service{ + tmsID: tmsID, + TMSProvider: TMSProvider, + NetworkProvider: networkProvider, + Storage: storage, + RequestsCache: requestsCache, + Config: config, + } } // AppendValid extracts actions from a token request, applies them to the local storage, @@ -125,6 +143,19 @@ func (t *Service) AppendValid(ctx context.Context, tx dbdriver.Transaction, txID } defer t.removeCachedTokenRequest(string(txID)) + return t.Append(ctx, tx, txID, &AppendRequest{ + Tokens: toAppend, + DeleteIDs: toSpend, + }) +} + +// Append applies the passed request to the local storage. +func (t *Service) Append(ctx context.Context, tx dbdriver.Transaction, txID token.RequestAnchor, req *AppendRequest) (err error) { + err = t.validateAppendRequest(req) + if err != nil { + return errors.Wrapf(err, "validation failed") + } + logger.DebugfContext(ctx, "transaction [%s] start db transaction", txID) ts, err := t.Storage.ContinueTransaction(tx) if err != nil { @@ -142,7 +173,7 @@ func (t *Service) AppendValid(ctx context.Context, tx dbdriver.Transaction, txID }() logger.DebugfContext(ctx, "append tokens") - for _, tta := range toAppend { + for _, tta := range req.Tokens { err = ts.AppendToken(ctx, tta) if err != nil { return errors.WithMessagef(err, "transaction [%s], failed to append token", txID) @@ -150,7 +181,7 @@ func (t *Service) AppendValid(ctx context.Context, tx dbdriver.Transaction, txID } logger.DebugfContext(ctx, "delete spend tokens") - err = ts.DeleteTokens(ctx, string(txID), toSpend) + err = ts.DeleteTokens(ctx, string(txID), req.DeleteIDs) if err != nil { return errors.WithMessagef(err, "transaction [%s], failed to delete tokens", txID) } @@ -160,6 +191,49 @@ func (t *Service) AppendValid(ctx context.Context, tx dbdriver.Transaction, txID return nil } +func (t *Service) validateAppendRequest(req *AppendRequest) error { + if req == nil { + return errors.New("request is nil") + } + + if len(req.Tokens) > t.Config.MaxTokenOutputsPerTx { + return errors.Errorf("too many token outputs: %d > %d", len(req.Tokens), t.Config.MaxTokenOutputsPerTx) + } + + for i, tok := range req.Tokens { + if tok.Tok == nil { + return errors.Errorf("token at index %d is nil", i) + } + if len(tok.TokenOnLedger) > t.Config.MaxTokenPayloadSize { + return errors.Errorf("token payload too large at index %d: %d > %d", i, len(tok.TokenOnLedger), t.Config.MaxTokenPayloadSize) + } + + if len(tok.TokenOnLedgerMetadata) > t.Config.MaxTokenPayloadSize { + return errors.Errorf("token metadata too large at index %d: %d > %d", i, len(tok.TokenOnLedgerMetadata), t.Config.MaxTokenPayloadSize) + } + + // OwnerWalletID can be empty in audit/sync flows where the wallet is not local. + + if len(tok.OwnerWalletID) > t.Config.MaxWalletIDSize { + return errors.Errorf("wallet_id too large at index %d: %d > %d", i, len(tok.OwnerWalletID), t.Config.MaxWalletIDSize) + } + + if len(tok.Tok.Owner) > t.Config.MaxOwnerRawSize { + return errors.Errorf("owner_raw too large at index %d: %d > %d", i, len(tok.Tok.Owner), t.Config.MaxOwnerRawSize) + } + + if len(tok.Issuer) > t.Config.MaxIssuerRawSize { + return errors.Errorf("issuer_raw too large at index %d: %d > %d", i, len(tok.Issuer), t.Config.MaxIssuerRawSize) + } + } + + if len(req.DeleteIDs) > t.Config.MaxBulkDeleteSize { + return errors.Errorf("too many bulk deletes: %d > %d", len(req.DeleteIDs), t.Config.MaxBulkDeleteSize) + } + + return nil +} + // CacheRequest extracts actions from a token request and caches them locally to avoid redundant parsing during the commit phase. func (t *Service) CacheRequest(ctx context.Context, request *token.Request) error { toSpend, toAppend, err := t.extractActions(ctx, request.Anchor, request) diff --git a/token/services/tokens/tokens_test.go b/token/services/tokens/tokens_test.go index 2b9dd93ee5..b0fbc24f04 100644 --- a/token/services/tokens/tokens_test.go +++ b/token/services/tokens/tokens_test.go @@ -24,6 +24,7 @@ func TestParse(t *testing.T) { ts := &tokens.Service{ TMSProvider: nil, Storage: &tokens.DBStorage{}, + Config: driver.ValidationConfig{}, } md := &mock.FakeMetaData{} diff --git a/token/services/tokens/validation_test.go b/token/services/tokens/validation_test.go new file mode 100644 index 0000000000..11027e885c --- /dev/null +++ b/token/services/tokens/validation_test.go @@ -0,0 +1,164 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package tokens + +import ( + "context" + "strings" + "testing" + + token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidateAppendRequest(t *testing.T) { + s := &Service{ + Config: ValidationConfig{ + MaxTokenPayloadSize: 1024, + MaxTokenOutputsPerTx: 10, + MaxBulkDeleteSize: 5, + MaxWalletIDSize: 1024, + MaxOwnerRawSize: 16 * 1024, + MaxIssuerRawSize: 16 * 1024, + }, + } + + t.Run("Success", func(t *testing.T) { + req := &AppendRequest{ + Tokens: []TokenToAppend{ + { + Tok: &token2.Token{ + Owner: []byte("owner1"), + }, + TokenOnLedger: []byte("payload1"), + TokenOnLedgerMetadata: []byte("meta1"), + OwnerWalletID: "wallet1", + Issuer: []byte("issuer1"), + }, + }, + DeleteIDs: []*token2.ID{{TxId: "tx1", Index: 0}}, + } + err := s.validateAppendRequest(req) + assert.NoError(t, err) + }) + + t.Run("NilRequest", func(t *testing.T) { + err := s.validateAppendRequest(nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "request is nil") + }) + + t.Run("TooManyOutputs", func(t *testing.T) { + req := &AppendRequest{ + Tokens: make([]TokenToAppend, 11), + } + err := s.validateAppendRequest(req) + require.Error(t, err) + assert.Contains(t, err.Error(), "too many token outputs") + }) + + t.Run("PayloadTooLarge", func(t *testing.T) { + req := &AppendRequest{ + Tokens: []TokenToAppend{ + { + Tok: &token2.Token{}, + TokenOnLedger: make([]byte, 1025), + OwnerWalletID: "w1", + }, + }, + } + err := s.validateAppendRequest(req) + require.Error(t, err) + assert.Contains(t, err.Error(), "token payload too large") + }) + + t.Run("MetadataTooLarge", func(t *testing.T) { + req := &AppendRequest{ + Tokens: []TokenToAppend{ + { + Tok: &token2.Token{}, + TokenOnLedgerMetadata: make([]byte, 1025), + OwnerWalletID: "w1", + }, + }, + } + err := s.validateAppendRequest(req) + require.Error(t, err) + assert.Contains(t, err.Error(), "token metadata too large") + }) + + t.Run("WalletIDTooLarge", func(t *testing.T) { + req := &AppendRequest{ + Tokens: []TokenToAppend{ + { + Tok: &token2.Token{}, + OwnerWalletID: strings.Repeat("a", 1025), + }, + }, + } + err := s.validateAppendRequest(req) + require.Error(t, err) + assert.Contains(t, err.Error(), "wallet_id too large") + }) + + t.Run("OwnerRawTooLarge", func(t *testing.T) { + req := &AppendRequest{ + Tokens: []TokenToAppend{ + { + Tok: &token2.Token{ + Owner: make([]byte, 16385), + }, + OwnerWalletID: "w1", + }, + }, + } + err := s.validateAppendRequest(req) + require.Error(t, err) + assert.Contains(t, err.Error(), "owner_raw too large") + }) + + t.Run("IssuerRawTooLarge", func(t *testing.T) { + req := &AppendRequest{ + Tokens: []TokenToAppend{ + { + Tok: &token2.Token{}, + Issuer: make([]byte, 16385), + OwnerWalletID: "w1", + }, + }, + } + err := s.validateAppendRequest(req) + require.Error(t, err) + assert.Contains(t, err.Error(), "issuer_raw too large") + }) + + t.Run("TooManyBulkDeletes", func(t *testing.T) { + req := &AppendRequest{ + DeleteIDs: make([]*token2.ID, 6), + } + err := s.validateAppendRequest(req) + require.Error(t, err) + assert.Contains(t, err.Error(), "too many bulk deletes") + }) +} + +func TestAppendValidationIntegration(t *testing.T) { + s := &Service{ + Config: ValidationConfig{ + MaxTokenOutputsPerTx: 1, + }, + } + ctx := context.Background() + req := &AppendRequest{ + Tokens: make([]TokenToAppend, 2), + } + err := s.Append(ctx, nil, "tx1", req) + require.Error(t, err) + assert.Contains(t, err.Error(), "validation failed") + assert.Contains(t, err.Error(), "too many token outputs") +} diff --git a/token/tms.go b/token/tms.go index b463029e1a..12fe863c2b 100644 --- a/token/tms.go +++ b/token/tms.go @@ -241,9 +241,17 @@ func (t *ManagementService) init() error { if err != nil { return errors.WithMessagef(err, "failed to get validator") } - t.validator = &Validator{backend: validator} t.auth = &Authorization{Authorization: t.tms.Authorization()} t.conf = NewConfiguration(t.tms.Configuration()) + if validator != nil { + t.validator = &Validator{backend: validator} + vConfig, err := t.conf.GetValidationConfig() + if err != nil { + return errors.WithMessagef(err, "failed to get validation config") + } + t.validator.SetValidationConfig(vConfig) + } + t.tokensService = &TokensService{ts: t.tms.TokensService(), tus: t.tms.TokensUpgradeService()} t.publicParametersManager = &PublicParametersManager{ ppm: t.tms.PublicParamsManager(), diff --git a/token/validator.go b/token/validator.go index 6464a6244f..9fa9a7efa7 100644 --- a/token/validator.go +++ b/token/validator.go @@ -57,6 +57,11 @@ func (c *Validator) UnmarshallAndVerifyWithMetadata(ctx context.Context, ledger return res, meta, nil } +// SetValidationConfig configures the validation limits for the validator. +func (c *Validator) SetValidationConfig(config driver.ValidationConfig) { + c.backend.SetValidationConfig(config) +} + type stateGetter struct { f driver.GetStateFnc } From 11516432a2ac63446eb56f4ae37ca9fcba498450 Mon Sep 17 00:00:00 2001 From: SurbhiAgarwal1 Date: Wed, 20 May 2026 15:08:54 +0530 Subject: [PATCH 02/11] fix(tokens): load validation config lazily to avoid recursive deadlock during initialization Signed-off-by: SurbhiAgarwal1 --- plan.md | 23 +++++++++++++++++++++++ token/services/tokens/manager.go | 16 +--------------- token/services/tokens/tokens.go | 24 ++++++++++++++++++++++-- 3 files changed, 46 insertions(+), 17 deletions(-) create mode 100644 plan.md diff --git a/plan.md b/plan.md new file mode 100644 index 0000000000..51d589b7db --- /dev/null +++ b/plan.md @@ -0,0 +1,23 @@ +# Plan: Enforce Strict Input Validation and Fix Integration Regressions + +## Goal +Resolve the deadlock regression introduced by direct recursive calls to `GetManagementService` within `NewServiceManager`'s lazy initialization, while preserving strict input validation, proper bounds configuration, and robust mock testing. + +## Implementation Steps +- [x] Initialize `plan.md` in project root with steps and progress. +- [x] Refactor `tokens.go` to support lazy loading of `ValidationConfig` during `Append` rather than creation time in `NewServiceManager`. +- [x] Revert `NewServiceManager` in `manager.go` to its original signature and initialization flow to prevent the recursive lookup deadlock. +- [x] Ensure all local test coverage for validation passes successfully. +- [x] Verify formatting and formatting checks with `make checks`. +- [x] Push clean commits to Surbhi's `security/tokens-validation` branch. + +## Implementation Progress +- [x] Lazy ValidationConfig resolution fully implemented. +- [x] Lazy initialization deadlock completely eliminated. +- [x] All unit test validation suites verified passing. + +## Plan Status +✅ COMPLETE + +## Notes & Decisions +- Direct calls to `tmsProvider.GetManagementService` inside the `ServiceManager`'s lazy provider are recursive and trigger a Mutex deadlock because the `ManagementServiceProvider` holds the creation lock. By moving config loading lazily to `Append` execution time, we completely avoid the deadlock since the lock is released after initialization. diff --git a/token/services/tokens/manager.go b/token/services/tokens/manager.go index 6b821cee10..f6072b06b2 100644 --- a/token/services/tokens/manager.go +++ b/token/services/tokens/manager.go @@ -64,21 +64,7 @@ func NewServiceManager( if err != nil { return nil, errors.WithMessagef(err, "failed to get token cache for [%s]", tmsID) } - tms, err := tmsProvider.GetManagementService(token.WithTMSID(tmsID)) - if err != nil { - return nil, errors.Errorf("failed to get management service for [%s]", tmsID) - } - if tms == nil { - return nil, errors.Errorf("failed to get management service for [%s]: nil", tmsID) - } - if tms.Configuration() == nil { - return nil, errors.Errorf("failed to get configuration for [%s]", tmsID) - } - vConfig, err := tms.Configuration().GetValidationConfig() - if err != nil { - return nil, errors.Wrapf(err, "failed to get validation config for [%s]", tmsID) - } - tokens := NewService(tmsID, tmsProvider, networkProvider, storage, cacheInst, vConfig) + tokens := NewService(tmsID, tmsProvider, networkProvider, storage, cacheInst) return tokens, nil }), diff --git a/token/services/tokens/tokens.go b/token/services/tokens/tokens.go index 42ce6484da..376f22ebe1 100644 --- a/token/services/tokens/tokens.go +++ b/token/services/tokens/tokens.go @@ -98,14 +98,13 @@ type Service struct { Config ValidationConfig } -func NewService(tmsID token.TMSID, TMSProvider TMSProvider, networkProvider NetworkProvider, storage *DBStorage, requestsCache Cache, config ValidationConfig) *Service { +func NewService(tmsID token.TMSID, TMSProvider TMSProvider, networkProvider NetworkProvider, storage *DBStorage, requestsCache Cache) *Service { return &Service{ tmsID: tmsID, TMSProvider: TMSProvider, NetworkProvider: networkProvider, Storage: storage, RequestsCache: requestsCache, - Config: config, } } @@ -151,6 +150,27 @@ func (t *Service) AppendValid(ctx context.Context, tx dbdriver.Transaction, txID // Append applies the passed request to the local storage. func (t *Service) Append(ctx context.Context, tx dbdriver.Transaction, txID token.RequestAnchor, req *AppendRequest) (err error) { + if t.Config == (ValidationConfig{}) { + t.Config = ValidationConfig{ + MaxTokenPayloadSize: 2 * 1024 * 1024, + MaxTokenOutputsPerTx: 1000, + MaxBulkDeleteSize: 10000, + MaxWalletIDSize: 1024, + MaxOwnerRawSize: 16 * 1024, + MaxIssuerRawSize: 16 * 1024, + MaxTokenRequestSize: 2 * 1024 * 1024, + MaxActionCount: 1000, + } + if t.TMSProvider != nil { + tms, err := t.TMSProvider.GetManagementService(token.WithTMSID(t.tmsID)) + if err == nil && tms != nil && tms.Configuration() != nil { + if vConfig, err := tms.Configuration().GetValidationConfig(); err == nil { + t.Config = vConfig + } + } + } + } + err = t.validateAppendRequest(req) if err != nil { return errors.Wrapf(err, "validation failed") From 5d52bfbe3bb0f8462ff04feeb5bbf1b68829dec2 Mon Sep 17 00:00:00 2001 From: SurbhiAgarwal1 Date: Wed, 20 May 2026 15:56:18 +0530 Subject: [PATCH 03/11] fix: optimize input validation limits for large Idemix payloads Signed-off-by: SurbhiAgarwal1 --- plan.md | 26 +++++++++--------------- token/config.go | 4 ++-- token/core/common/validator.go | 4 ++-- token/driver/validator.go | 4 ++-- token/services/tokens/tokens.go | 4 ++-- token/services/tokens/validation_test.go | 8 ++++---- 6 files changed, 22 insertions(+), 28 deletions(-) diff --git a/plan.md b/plan.md index 51d589b7db..3c8a5182a1 100644 --- a/plan.md +++ b/plan.md @@ -1,23 +1,17 @@ -# Plan: Enforce Strict Input Validation and Fix Integration Regressions +# Implementation Plan - Optimize and Fix Validation Bounds for Idemix Identities ## Goal -Resolve the deadlock regression introduced by direct recursive calls to `GetManagementService` within `NewServiceManager`'s lazy initialization, while preserving strict input validation, proper bounds configuration, and robust mock testing. +Increase `MaxOwnerRawSize` and `MaxIssuerRawSize` from `16 * 1024` (16KB) to `256 * 1024` (256KB) across all configurations, validators, and driver files. This resolves the regression where standard `zkatdlog` integration tests (such as `update-t2`) fail because serialized Idemix identities, audit info, and output audit info naturally exceed the highly restrictive 16KB boundary during request unmarshaling and validation. ## Implementation Steps -- [x] Initialize `plan.md` in project root with steps and progress. -- [x] Refactor `tokens.go` to support lazy loading of `ValidationConfig` during `Append` rather than creation time in `NewServiceManager`. -- [x] Revert `NewServiceManager` in `manager.go` to its original signature and initialization flow to prevent the recursive lookup deadlock. -- [x] Ensure all local test coverage for validation passes successfully. -- [x] Verify formatting and formatting checks with `make checks`. -- [x] Push clean commits to Surbhi's `security/tokens-validation` branch. +- [x] 1. Update `MaxOwnerRawSize` and `MaxIssuerRawSize` constants in `token/driver/validator.go` to `256 * 1024`. +- [x] 2. Update default `MaxOwnerRawSize` and `MaxIssuerRawSize` values in `token/config.go` to `256 * 1024`. +- [x] 3. Update default `MaxOwnerRawSize` and `MaxIssuerRawSize` values in `token/services/tokens/tokens.go` to `256 * 1024`. +- [x] 4. Update default `MaxOwnerRawSize` and `MaxIssuerRawSize` values in `token/core/common/validator.go` to `256 * 1024`. +- [x] 5. Update mock/validation tests in `token/services/tokens/validation_test.go` to match the new `256 * 1024` limit behavior where applicable. +- [x] 6. Verify formatting, run code checks, and update plan progress. -## Implementation Progress -- [x] Lazy ValidationConfig resolution fully implemented. -- [x] Lazy initialization deadlock completely eliminated. -- [x] All unit test validation suites verified passing. +## Notes & Decisions +- Setting the limit to `256 * 1024` (256KB) ensures all standard Idemix credentials, public keys, and zero-knowledge proof audit structures easily fit within the bounds while still offering robust protection against oversized payload resource exhaustion attacks (well below the `2 * 1024 * 1024` overall payload limit). -## Plan Status ✅ COMPLETE - -## Notes & Decisions -- Direct calls to `tmsProvider.GetManagementService` inside the `ServiceManager`'s lazy provider are recursive and trigger a Mutex deadlock because the `ManagementServiceProvider` holds the creation lock. By moving config loading lazily to `Append` execution time, we completely avoid the deadlock since the lock is released after initialization. diff --git a/token/config.go b/token/config.go index a4baf73d25..75b5301e0e 100644 --- a/token/config.go +++ b/token/config.go @@ -45,8 +45,8 @@ func (m *Configuration) GetValidationConfig() (driver.ValidationConfig, error) { MaxTokenOutputsPerTx: 1000, MaxBulkDeleteSize: 10000, MaxWalletIDSize: 1024, - MaxOwnerRawSize: 16 * 1024, - MaxIssuerRawSize: 16 * 1024, + MaxOwnerRawSize: 256 * 1024, + MaxIssuerRawSize: 256 * 1024, MaxTokenRequestSize: 2 * 1024 * 1024, MaxActionCount: 1000, } diff --git a/token/core/common/validator.go b/token/core/common/validator.go index b64ce01d78..7820084af4 100644 --- a/token/core/common/validator.go +++ b/token/core/common/validator.go @@ -100,8 +100,8 @@ func NewValidator[P driver.PublicParameters, T driver.Input, TA driver.TransferA MaxTokenOutputsPerTx: 1000, MaxBulkDeleteSize: 10000, MaxWalletIDSize: 1024, - MaxOwnerRawSize: 16 * 1024, - MaxIssuerRawSize: 16 * 1024, + MaxOwnerRawSize: 256 * 1024, + MaxIssuerRawSize: 256 * 1024, MaxTokenRequestSize: 2 * 1024 * 1024, MaxActionCount: 1000, }, diff --git a/token/driver/validator.go b/token/driver/validator.go index b8bf156146..8d8563b08c 100644 --- a/token/driver/validator.go +++ b/token/driver/validator.go @@ -22,9 +22,9 @@ const ( // MaxWalletIDSize is the maximum size of a wallet ID in bytes. MaxWalletIDSize = 1024 // MaxOwnerRawSize is the maximum size of a raw owner identity in bytes. - MaxOwnerRawSize = 16 * 1024 // 16KB for Idemix + MaxOwnerRawSize = 256 * 1024 // 256KB for Idemix // MaxIssuerRawSize is the maximum size of a raw issuer identity in bytes. - MaxIssuerRawSize = 16 * 1024 + MaxIssuerRawSize = 256 * 1024 // MaxTokenRequestSize is the maximum size of a token request in bytes. MaxTokenRequestSize = 2 * 1024 * 1024 // 2MB // MaxActionCount is the maximum number of actions/signatures in a token request. diff --git a/token/services/tokens/tokens.go b/token/services/tokens/tokens.go index 376f22ebe1..d9052a1077 100644 --- a/token/services/tokens/tokens.go +++ b/token/services/tokens/tokens.go @@ -156,8 +156,8 @@ func (t *Service) Append(ctx context.Context, tx dbdriver.Transaction, txID toke MaxTokenOutputsPerTx: 1000, MaxBulkDeleteSize: 10000, MaxWalletIDSize: 1024, - MaxOwnerRawSize: 16 * 1024, - MaxIssuerRawSize: 16 * 1024, + MaxOwnerRawSize: 256 * 1024, + MaxIssuerRawSize: 256 * 1024, MaxTokenRequestSize: 2 * 1024 * 1024, MaxActionCount: 1000, } diff --git a/token/services/tokens/validation_test.go b/token/services/tokens/validation_test.go index 11027e885c..a39522c1d4 100644 --- a/token/services/tokens/validation_test.go +++ b/token/services/tokens/validation_test.go @@ -23,8 +23,8 @@ func TestValidateAppendRequest(t *testing.T) { MaxTokenOutputsPerTx: 10, MaxBulkDeleteSize: 5, MaxWalletIDSize: 1024, - MaxOwnerRawSize: 16 * 1024, - MaxIssuerRawSize: 16 * 1024, + MaxOwnerRawSize: 256 * 1024, + MaxIssuerRawSize: 256 * 1024, }, } @@ -111,7 +111,7 @@ func TestValidateAppendRequest(t *testing.T) { Tokens: []TokenToAppend{ { Tok: &token2.Token{ - Owner: make([]byte, 16385), + Owner: make([]byte, 256*1024+1), }, OwnerWalletID: "w1", }, @@ -127,7 +127,7 @@ func TestValidateAppendRequest(t *testing.T) { Tokens: []TokenToAppend{ { Tok: &token2.Token{}, - Issuer: make([]byte, 16385), + Issuer: make([]byte, 256*1024+1), OwnerWalletID: "w1", }, }, From 4d24272e13a88ecf958f2fab2d0e44052b446d57 Mon Sep 17 00:00:00 2001 From: SurbhiAgarwal1 Date: Tue, 16 Jun 2026 20:09:10 +0530 Subject: [PATCH 04/11] fix(tokens): address mentor feedback on validation and config loading Signed-off-by: SurbhiAgarwal1 --- docs/configuration.md | 30 ++++++++++++++++++ plan.md | 17 ----------- token/services/tokens/tokens.go | 54 +++++++++++++++++++-------------- 3 files changed, 62 insertions(+), 39 deletions(-) delete mode 100644 plan.md diff --git a/docs/configuration.md b/docs/configuration.md index 777c35f569..8119032125 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -371,6 +371,36 @@ Default values: --- +### Optional: token.validation + +Configuration related to strict input validation limits for tokens and identity service payloads. If not specified, the default configuration is: + +```yaml +token: + validation: + maxTokenPayloadSize: 2097152 + maxTokenOutputsPerTx: 1000 + maxBulkDeleteSize: 10000 + maxWalletIDSize: 1024 + maxOwnerRawSize: 262144 + maxIssuerRawSize: 262144 + maxTokenRequestSize: 2097152 + maxActionCount: 1000 +``` + +Default values: + +- maxTokenPayloadSize: 2097152 (2MB) - Maximum size in bytes of a token's payload (TokenOnLedger/TokenOnLedgerMetadata). +- maxTokenOutputsPerTx: 1000 - Maximum number of token outputs allowed per transaction append request. +- maxBulkDeleteSize: 10000 - Maximum number of IDs that can be deleted in a bulk operation. +- maxWalletIDSize: 1024 (1KB) - Maximum size in bytes for a wallet ID. +- maxOwnerRawSize: 262144 (256KB) - Maximum raw size in bytes for an owner identity. +- maxIssuerRawSize: 262144 (256KB) - Maximum raw size in bytes for an issuer identity. +- maxTokenRequestSize: 2097152 (2MB) - Maximum allowed size in bytes for a full token request payload. +- maxActionCount: 1000 - Maximum number of actions allowed in a single token request. + +--- + ### Optional: token.fabricx.lookup If not specified, the default configuration is: diff --git a/plan.md b/plan.md deleted file mode 100644 index 3c8a5182a1..0000000000 --- a/plan.md +++ /dev/null @@ -1,17 +0,0 @@ -# Implementation Plan - Optimize and Fix Validation Bounds for Idemix Identities - -## Goal -Increase `MaxOwnerRawSize` and `MaxIssuerRawSize` from `16 * 1024` (16KB) to `256 * 1024` (256KB) across all configurations, validators, and driver files. This resolves the regression where standard `zkatdlog` integration tests (such as `update-t2`) fail because serialized Idemix identities, audit info, and output audit info naturally exceed the highly restrictive 16KB boundary during request unmarshaling and validation. - -## Implementation Steps -- [x] 1. Update `MaxOwnerRawSize` and `MaxIssuerRawSize` constants in `token/driver/validator.go` to `256 * 1024`. -- [x] 2. Update default `MaxOwnerRawSize` and `MaxIssuerRawSize` values in `token/config.go` to `256 * 1024`. -- [x] 3. Update default `MaxOwnerRawSize` and `MaxIssuerRawSize` values in `token/services/tokens/tokens.go` to `256 * 1024`. -- [x] 4. Update default `MaxOwnerRawSize` and `MaxIssuerRawSize` values in `token/core/common/validator.go` to `256 * 1024`. -- [x] 5. Update mock/validation tests in `token/services/tokens/validation_test.go` to match the new `256 * 1024` limit behavior where applicable. -- [x] 6. Verify formatting, run code checks, and update plan progress. - -## Notes & Decisions -- Setting the limit to `256 * 1024` (256KB) ensures all standard Idemix credentials, public keys, and zero-knowledge proof audit structures easily fit within the bounds while still offering robust protection against oversized payload resource exhaustion attacks (well below the `2 * 1024 * 1024` overall payload limit). - -✅ COMPLETE diff --git a/token/services/tokens/tokens.go b/token/services/tokens/tokens.go index d9052a1077..6518cedf85 100644 --- a/token/services/tokens/tokens.go +++ b/token/services/tokens/tokens.go @@ -99,13 +99,44 @@ type Service struct { } func NewService(tmsID token.TMSID, TMSProvider TMSProvider, networkProvider NetworkProvider, storage *DBStorage, requestsCache Cache) *Service { - return &Service{ + s := &Service{ tmsID: tmsID, TMSProvider: TMSProvider, NetworkProvider: networkProvider, Storage: storage, RequestsCache: requestsCache, + Config: ValidationConfig{ + MaxTokenPayloadSize: 2 * 1024 * 1024, + MaxTokenOutputsPerTx: 1000, + MaxBulkDeleteSize: 10000, + MaxWalletIDSize: 1024, + MaxOwnerRawSize: 256 * 1024, + MaxIssuerRawSize: 256 * 1024, + MaxTokenRequestSize: 2 * 1024 * 1024, + MaxActionCount: 1000, + }, } + + if TMSProvider != nil { + tms, err := TMSProvider.GetManagementService(token.WithTMSID(tmsID)) + if err == nil && tms != nil && tms.Configuration() != nil { + if vConfig, err := tms.Configuration().GetValidationConfig(); err == nil { + s.Config = vConfig + } else { + logger.Warnf("failed reading validation config, falling back to default values: %s", err) + } + } else { + if err != nil { + logger.Warnf("failed getting token management service [%s], falling back to default validation config: %s", tmsID, err) + } else { + logger.Warnf("token management service or configuration is nil for [%s], falling back to default validation config", tmsID) + } + } + } else { + logger.Warnf("TMSProvider is nil, falling back to default validation config") + } + + return s } // AppendValid extracts actions from a token request, applies them to the local storage, @@ -150,27 +181,6 @@ func (t *Service) AppendValid(ctx context.Context, tx dbdriver.Transaction, txID // Append applies the passed request to the local storage. func (t *Service) Append(ctx context.Context, tx dbdriver.Transaction, txID token.RequestAnchor, req *AppendRequest) (err error) { - if t.Config == (ValidationConfig{}) { - t.Config = ValidationConfig{ - MaxTokenPayloadSize: 2 * 1024 * 1024, - MaxTokenOutputsPerTx: 1000, - MaxBulkDeleteSize: 10000, - MaxWalletIDSize: 1024, - MaxOwnerRawSize: 256 * 1024, - MaxIssuerRawSize: 256 * 1024, - MaxTokenRequestSize: 2 * 1024 * 1024, - MaxActionCount: 1000, - } - if t.TMSProvider != nil { - tms, err := t.TMSProvider.GetManagementService(token.WithTMSID(t.tmsID)) - if err == nil && tms != nil && tms.Configuration() != nil { - if vConfig, err := tms.Configuration().GetValidationConfig(); err == nil { - t.Config = vConfig - } - } - } - } - err = t.validateAppendRequest(req) if err != nil { return errors.Wrapf(err, "validation failed") From d06de98df7a70a36670fd2de2a24959da14bb6d3 Mon Sep 17 00:00:00 2001 From: SurbhiAgarwal1 Date: Tue, 16 Jun 2026 21:04:38 +0530 Subject: [PATCH 05/11] fix(tokens): use sync.Once for lazy loading to prevent deadlock Signed-off-by: SurbhiAgarwal1 --- token/services/tokens/tokens.go | 42 ++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/token/services/tokens/tokens.go b/token/services/tokens/tokens.go index 6518cedf85..e700205465 100644 --- a/token/services/tokens/tokens.go +++ b/token/services/tokens/tokens.go @@ -9,6 +9,7 @@ package tokens import ( "context" "runtime/debug" + "sync" "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" "github.com/hyperledger-labs/fabric-token-sdk/token" @@ -95,7 +96,8 @@ type Service struct { // RequestsCache provides an in-memory cache for pending token requests to optimize commit performance. RequestsCache Cache // Config contains the validation limits for the service. - Config ValidationConfig + Config ValidationConfig + configOnce sync.Once } func NewService(tmsID token.TMSID, TMSProvider TMSProvider, networkProvider NetworkProvider, storage *DBStorage, requestsCache Cache) *Service { @@ -117,26 +119,30 @@ func NewService(tmsID token.TMSID, TMSProvider TMSProvider, networkProvider Netw }, } - if TMSProvider != nil { - tms, err := TMSProvider.GetManagementService(token.WithTMSID(tmsID)) - if err == nil && tms != nil && tms.Configuration() != nil { - if vConfig, err := tms.Configuration().GetValidationConfig(); err == nil { - s.Config = vConfig + return s +} + +func (t *Service) loadConfig() { + t.configOnce.Do(func() { + if t.TMSProvider != nil { + tms, err := t.TMSProvider.GetManagementService(token.WithTMSID(t.tmsID)) + if err == nil && tms != nil && tms.Configuration() != nil { + if vConfig, err := tms.Configuration().GetValidationConfig(); err == nil { + t.Config = vConfig + } else { + logger.Warnf("failed reading validation config, falling back to default values: %s", err) + } } else { - logger.Warnf("failed reading validation config, falling back to default values: %s", err) + if err != nil { + logger.Warnf("failed getting token management service [%s], falling back to default validation config: %s", t.tmsID, err) + } else { + logger.Warnf("token management service or configuration is nil for [%s], falling back to default validation config", t.tmsID) + } } } else { - if err != nil { - logger.Warnf("failed getting token management service [%s], falling back to default validation config: %s", tmsID, err) - } else { - logger.Warnf("token management service or configuration is nil for [%s], falling back to default validation config", tmsID) - } + logger.Warnf("TMSProvider is nil, falling back to default validation config") } - } else { - logger.Warnf("TMSProvider is nil, falling back to default validation config") - } - - return s + }) } // AppendValid extracts actions from a token request, applies them to the local storage, @@ -226,6 +232,8 @@ func (t *Service) validateAppendRequest(req *AppendRequest) error { return errors.New("request is nil") } + t.loadConfig() + if len(req.Tokens) > t.Config.MaxTokenOutputsPerTx { return errors.Errorf("too many token outputs: %d > %d", len(req.Tokens), t.Config.MaxTokenOutputsPerTx) } From fa667303bfab13fb0727f7aee97c954cd2587675 Mon Sep 17 00:00:00 2001 From: SurbhiAgarwal1 Date: Thu, 18 Jun 2026 22:51:27 +0530 Subject: [PATCH 06/11] fix(tokens): check for missing owner in token append validation Signed-off-by: SurbhiAgarwal1 --- token/services/tokens/tokens.go | 4 ++++ token/services/tokens/validation_test.go | 22 ++++++++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/token/services/tokens/tokens.go b/token/services/tokens/tokens.go index e700205465..0670515ccf 100644 --- a/token/services/tokens/tokens.go +++ b/token/services/tokens/tokens.go @@ -256,6 +256,10 @@ func (t *Service) validateAppendRequest(req *AppendRequest) error { return errors.Errorf("wallet_id too large at index %d: %d > %d", i, len(tok.OwnerWalletID), t.Config.MaxWalletIDSize) } + if len(tok.Tok.Owner) == 0 { + return errors.Errorf("owner is missing at index %d", i) + } + if len(tok.Tok.Owner) > t.Config.MaxOwnerRawSize { return errors.Errorf("owner_raw too large at index %d: %d > %d", i, len(tok.Tok.Owner), t.Config.MaxOwnerRawSize) } diff --git a/token/services/tokens/validation_test.go b/token/services/tokens/validation_test.go index a39522c1d4..70cc069117 100644 --- a/token/services/tokens/validation_test.go +++ b/token/services/tokens/validation_test.go @@ -66,7 +66,7 @@ func TestValidateAppendRequest(t *testing.T) { req := &AppendRequest{ Tokens: []TokenToAppend{ { - Tok: &token2.Token{}, + Tok: &token2.Token{Owner: []byte("owner1")}, TokenOnLedger: make([]byte, 1025), OwnerWalletID: "w1", }, @@ -81,7 +81,7 @@ func TestValidateAppendRequest(t *testing.T) { req := &AppendRequest{ Tokens: []TokenToAppend{ { - Tok: &token2.Token{}, + Tok: &token2.Token{Owner: []byte("owner1")}, TokenOnLedgerMetadata: make([]byte, 1025), OwnerWalletID: "w1", }, @@ -96,7 +96,7 @@ func TestValidateAppendRequest(t *testing.T) { req := &AppendRequest{ Tokens: []TokenToAppend{ { - Tok: &token2.Token{}, + Tok: &token2.Token{Owner: []byte("owner1")}, OwnerWalletID: strings.Repeat("a", 1025), }, }, @@ -106,6 +106,20 @@ func TestValidateAppendRequest(t *testing.T) { assert.Contains(t, err.Error(), "wallet_id too large") }) + t.Run("OwnerMissing", func(t *testing.T) { + req := &AppendRequest{ + Tokens: []TokenToAppend{ + { + Tok: &token2.Token{}, + OwnerWalletID: "w1", + }, + }, + } + err := s.validateAppendRequest(req) + require.Error(t, err) + assert.Contains(t, err.Error(), "owner is missing at index 0") + }) + t.Run("OwnerRawTooLarge", func(t *testing.T) { req := &AppendRequest{ Tokens: []TokenToAppend{ @@ -126,7 +140,7 @@ func TestValidateAppendRequest(t *testing.T) { req := &AppendRequest{ Tokens: []TokenToAppend{ { - Tok: &token2.Token{}, + Tok: &token2.Token{Owner: []byte("owner1")}, Issuer: make([]byte, 256*1024+1), OwnerWalletID: "w1", }, From 0601d5135a8fdba9a8a4c927c67d42e3476cbf21 Mon Sep 17 00:00:00 2001 From: Surbhi Agarwal Date: Tue, 14 Jul 2026 23:23:11 +0530 Subject: [PATCH 07/11] security: enforce strict input validation and payload bounds Address PR feedback on validation configs Signed-off-by: Surbhi Agarwal --- token/config.go | 11 +---------- token/core/common/validator.go | 13 ++----------- token/driver/validator.go | 12 ++++++++++++ token/services/tokens/tokens.go | 25 ++++++++----------------- 4 files changed, 23 insertions(+), 38 deletions(-) diff --git a/token/config.go b/token/config.go index 8796511aac..e0636d4b08 100644 --- a/token/config.go +++ b/token/config.go @@ -36,16 +36,7 @@ func (m *Configuration) UnmarshalKey(key string, rawVal any) error { // GetValidationConfig returns the validation configuration func (m *Configuration) GetValidationConfig() (driver.ValidationConfig, error) { - config := driver.ValidationConfig{ - MaxTokenPayloadSize: 2 * 1024 * 1024, - MaxTokenOutputsPerTx: 1000, - MaxBulkDeleteSize: 10000, - MaxWalletIDSize: 1024, - MaxOwnerRawSize: 256 * 1024, - MaxIssuerRawSize: 256 * 1024, - MaxTokenRequestSize: 2 * 1024 * 1024, - MaxActionCount: 1000, - } + config := driver.DefaultValidationConfig if m.cm != nil && m.cm.IsSet("validation") { if err := m.cm.UnmarshalKey("validation", &config); err != nil { return config, err diff --git a/token/core/common/validator.go b/token/core/common/validator.go index 63a54fee84..f79caa8871 100644 --- a/token/core/common/validator.go +++ b/token/core/common/validator.go @@ -95,16 +95,7 @@ func NewValidator[P driver.PublicParameters, T driver.Input, TA driver.TransferA TransferValidators: transferValidators, IssueValidators: issueValidators, AuditingValidators: auditingValidators, - ValidationConfig: driver.ValidationConfig{ - MaxTokenPayloadSize: 2 * 1024 * 1024, - MaxTokenOutputsPerTx: 1000, - MaxBulkDeleteSize: 10000, - MaxWalletIDSize: 1024, - MaxOwnerRawSize: 256 * 1024, - MaxIssuerRawSize: 256 * 1024, - MaxTokenRequestSize: 2 * 1024 * 1024, - MaxActionCount: 1000, - }, + ValidationConfig: driver.DefaultValidationConfig, } } @@ -142,7 +133,7 @@ func (v *Validator[P, T, TA, IA, DS]) VerifyTokenRequestFromRaw(ctx context.Cont // Validate protocol version if tr.Version == 0 { - tr.Version = uint32(driver.ProtocolV1) + return nil, nil, driver.ErrInvalidVersion } // Enforce minimum protocol version if configured diff --git a/token/driver/validator.go b/token/driver/validator.go index fd1869bc0b..f27788fe3b 100644 --- a/token/driver/validator.go +++ b/token/driver/validator.go @@ -103,3 +103,15 @@ type Validator interface { // SetValidationConfig configures the validation limits for the validator. SetValidationConfig(config ValidationConfig) } + +// DefaultValidationConfig holds the default resource limits. +var DefaultValidationConfig = ValidationConfig{ + MaxTokenPayloadSize: 2 * 1024 * 1024, + MaxTokenOutputsPerTx: 1000, + MaxBulkDeleteSize: 10000, + MaxWalletIDSize: 1024, + MaxOwnerRawSize: 256 * 1024, + MaxIssuerRawSize: 256 * 1024, + MaxTokenRequestSize: 2 * 1024 * 1024, + MaxActionCount: 1000, +} diff --git a/token/services/tokens/tokens.go b/token/services/tokens/tokens.go index 0670515ccf..655206542d 100644 --- a/token/services/tokens/tokens.go +++ b/token/services/tokens/tokens.go @@ -107,16 +107,7 @@ func NewService(tmsID token.TMSID, TMSProvider TMSProvider, networkProvider Netw NetworkProvider: networkProvider, Storage: storage, RequestsCache: requestsCache, - Config: ValidationConfig{ - MaxTokenPayloadSize: 2 * 1024 * 1024, - MaxTokenOutputsPerTx: 1000, - MaxBulkDeleteSize: 10000, - MaxWalletIDSize: 1024, - MaxOwnerRawSize: 256 * 1024, - MaxIssuerRawSize: 256 * 1024, - MaxTokenRequestSize: 2 * 1024 * 1024, - MaxActionCount: 1000, - }, + Config: driver.DefaultValidationConfig, } return s @@ -234,7 +225,7 @@ func (t *Service) validateAppendRequest(req *AppendRequest) error { t.loadConfig() - if len(req.Tokens) > t.Config.MaxTokenOutputsPerTx { + if t.Config.MaxTokenOutputsPerTx > 0 && len(req.Tokens) > t.Config.MaxTokenOutputsPerTx { return errors.Errorf("too many token outputs: %d > %d", len(req.Tokens), t.Config.MaxTokenOutputsPerTx) } @@ -242,17 +233,17 @@ func (t *Service) validateAppendRequest(req *AppendRequest) error { if tok.Tok == nil { return errors.Errorf("token at index %d is nil", i) } - if len(tok.TokenOnLedger) > t.Config.MaxTokenPayloadSize { + if t.Config.MaxTokenPayloadSize > 0 && len(tok.TokenOnLedger) > t.Config.MaxTokenPayloadSize { return errors.Errorf("token payload too large at index %d: %d > %d", i, len(tok.TokenOnLedger), t.Config.MaxTokenPayloadSize) } - if len(tok.TokenOnLedgerMetadata) > t.Config.MaxTokenPayloadSize { + if t.Config.MaxTokenPayloadSize > 0 && len(tok.TokenOnLedgerMetadata) > t.Config.MaxTokenPayloadSize { return errors.Errorf("token metadata too large at index %d: %d > %d", i, len(tok.TokenOnLedgerMetadata), t.Config.MaxTokenPayloadSize) } // OwnerWalletID can be empty in audit/sync flows where the wallet is not local. - if len(tok.OwnerWalletID) > t.Config.MaxWalletIDSize { + if t.Config.MaxWalletIDSize > 0 && len(tok.OwnerWalletID) > t.Config.MaxWalletIDSize { return errors.Errorf("wallet_id too large at index %d: %d > %d", i, len(tok.OwnerWalletID), t.Config.MaxWalletIDSize) } @@ -260,16 +251,16 @@ func (t *Service) validateAppendRequest(req *AppendRequest) error { return errors.Errorf("owner is missing at index %d", i) } - if len(tok.Tok.Owner) > t.Config.MaxOwnerRawSize { + if t.Config.MaxOwnerRawSize > 0 && len(tok.Tok.Owner) > t.Config.MaxOwnerRawSize { return errors.Errorf("owner_raw too large at index %d: %d > %d", i, len(tok.Tok.Owner), t.Config.MaxOwnerRawSize) } - if len(tok.Issuer) > t.Config.MaxIssuerRawSize { + if t.Config.MaxIssuerRawSize > 0 && len(tok.Issuer) > t.Config.MaxIssuerRawSize { return errors.Errorf("issuer_raw too large at index %d: %d > %d", i, len(tok.Issuer), t.Config.MaxIssuerRawSize) } } - if len(req.DeleteIDs) > t.Config.MaxBulkDeleteSize { + if t.Config.MaxBulkDeleteSize > 0 && len(req.DeleteIDs) > t.Config.MaxBulkDeleteSize { return errors.Errorf("too many bulk deletes: %d > %d", len(req.DeleteIDs), t.Config.MaxBulkDeleteSize) } From c79b051be88c84ff535081496e1132ee084d7b3c Mon Sep 17 00:00:00 2001 From: Surbhi Agarwal Date: Wed, 15 Jul 2026 00:05:06 +0530 Subject: [PATCH 08/11] style: fix gofmt issues Signed-off-by: Surbhi Agarwal --- token/core/common/validator.go | 2 +- token/driver/validator.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/token/core/common/validator.go b/token/core/common/validator.go index dbc6f1508d..d3cafd4afe 100644 --- a/token/core/common/validator.go +++ b/token/core/common/validator.go @@ -95,7 +95,7 @@ func NewValidator[P driver.PublicParameters, T driver.Input, TA driver.TransferA TransferValidators: transferValidators, IssueValidators: issueValidators, AuditingValidators: auditingValidators, - ValidationConfig: driver.DefaultValidationConfig, + ValidationConfig: driver.DefaultValidationConfig, } } diff --git a/token/driver/validator.go b/token/driver/validator.go index 8d5b4c4731..396d243e96 100644 --- a/token/driver/validator.go +++ b/token/driver/validator.go @@ -114,4 +114,4 @@ var DefaultValidationConfig = ValidationConfig{ MaxIssuerRawSize: 256 * 1024, MaxTokenRequestSize: 2 * 1024 * 1024, MaxActionCount: 1000, -} +} From 1b5f2890bbe9b28dd9aea3e8818694c83c67aeec Mon Sep 17 00:00:00 2001 From: Surbhi Agarwal Date: Wed, 15 Jul 2026 00:49:24 +0530 Subject: [PATCH 09/11] fix(tokens): fix validation_test.go imports and formatting issues Signed-off-by: Surbhi Agarwal --- token/services/tokens/validation_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/token/services/tokens/validation_test.go b/token/services/tokens/validation_test.go index 70cc069117..420b56db44 100644 --- a/token/services/tokens/validation_test.go +++ b/token/services/tokens/validation_test.go @@ -11,7 +11,7 @@ import ( "strings" "testing" - token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" + token2 "github.com/LFDT-Panurus/panurus/token/token" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) From 5bccde524659b7996828387be7175399e53f5cc2 Mon Sep 17 00:00:00 2001 From: Surbhi Agarwal Date: Wed, 15 Jul 2026 02:49:18 +0530 Subject: [PATCH 10/11] fix(tokens): restore version 0 rejection in validator Signed-off-by: Surbhi Agarwal --- token/core/common/validator.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/token/core/common/validator.go b/token/core/common/validator.go index d3cafd4afe..1c8b09109e 100644 --- a/token/core/common/validator.go +++ b/token/core/common/validator.go @@ -131,9 +131,9 @@ func (v *Validator[P, T, TA, IA, DS]) VerifyTokenRequestFromRaw(ctx context.Cont return nil, nil, errors.Errorf("too many actions: %d > %d", len(tr.Actions), v.ValidationConfig.MaxActionCount) } - // Legacy support: treat V0 as V1 + // Validate protocol version if tr.Version == 0 { - tr.Version = uint32(driver.ProtocolV1) + return nil, nil, driver.ErrInvalidVersion } // Enforce minimum protocol version if configured From 8c4ab8225d48154b2245274fc6ac257b54234562 Mon Sep 17 00:00:00 2001 From: Surbhi Agarwal Date: Wed, 15 Jul 2026 02:50:23 +0530 Subject: [PATCH 11/11] fix(tokens): allow empty owner for redeemed tokens in append validation Signed-off-by: Surbhi Agarwal --- token/services/tokens/tokens.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/token/services/tokens/tokens.go b/token/services/tokens/tokens.go index 73ba6d930f..d7e26a3f4f 100644 --- a/token/services/tokens/tokens.go +++ b/token/services/tokens/tokens.go @@ -247,7 +247,7 @@ func (t *Service) validateAppendRequest(req *AppendRequest) error { return errors.Errorf("wallet_id too large at index %d: %d > %d", i, len(tok.OwnerWalletID), t.Config.MaxWalletIDSize) } - if len(tok.Tok.Owner) == 0 { + if len(tok.Tok.Owner) == 0 && !tok.Flags.Redeemed { return errors.Errorf("owner is missing at index %d", i) }