diff --git a/docs/configuration.md b/docs/configuration.md index d4f1b0c7cc..a2c0c3a46c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -443,6 +443,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/token/config.go b/token/config.go index 1d4ad9d35f..4edf5d9271 100644 --- a/token/config.go +++ b/token/config.go @@ -22,6 +22,10 @@ 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) } @@ -29,3 +33,15 @@ func (m *Configuration) IsSet(key string) bool { func (m *Configuration) UnmarshalKey(key string, rawVal any) error { return m.cm.UnmarshalKey(key, rawVal) } + +// GetValidationConfig returns the validation configuration +func (m *Configuration) GetValidationConfig() (driver.ValidationConfig, error) { + config := driver.DefaultValidationConfig + 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 63bb2ed523..1c8b09109e 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.ProtocolV1), 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,7 @@ func NewValidator[P driver.PublicParameters, T driver.Input, TA driver.TransferA TransferValidators: transferValidators, IssueValidators: issueValidators, AuditingValidators: auditingValidators, + ValidationConfig: driver.DefaultValidationConfig, } } @@ -102,18 +106,31 @@ 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) ([]any, 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.Actions) > v.ValidationConfig.MaxActionCount { + return nil, nil, errors.Errorf("too many actions: %d > %d", len(tr.Actions), v.ValidationConfig.MaxActionCount) + } + // Validate protocol version if tr.Version == 0 { return nil, nil, driver.ErrInvalidVersion @@ -186,6 +203,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) @@ -265,6 +294,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 @@ -328,6 +378,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 15f81ba523..08a9b940ca 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", @@ -36,11 +35,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", @@ -48,6 +46,32 @@ func TestMinProtocolVersionEnforcement(t *testing.T) { requestVersion: driver.ProtocolV1, shouldFail: false, }, + { + name: "Minimum V1 - accepts V2", + minProtocolVersion: driver.ProtocolV1, + requestVersion: driver.ProtocolV2, + shouldFail: false, + }, + { + name: "Minimum V2 - rejects version 0 (as V1)", + minProtocolVersion: driver.ProtocolV2, + requestVersion: 0, + shouldFail: true, + expectedError: "token request protocol version [1] is below minimum required version [2]", + }, + { + name: "Minimum V2 - rejects V1", + minProtocolVersion: driver.ProtocolV2, + requestVersion: driver.ProtocolV1, + shouldFail: true, + expectedError: "token request protocol version [1] is below minimum required version [2]", + }, + { + name: "Minimum V2 - accepts V2", + minProtocolVersion: driver.ProtocolV2, + requestVersion: driver.ProtocolV2, + shouldFail: false, + }, } for _, tt := range tests { @@ -55,11 +79,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 } @@ -81,10 +107,15 @@ 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, ""}, - {"Min V1, V0 request", driver.ProtocolV1, 0, false, "version 0 is invalid"}, + {"No min, V2 request", 0, driver.ProtocolV2, true, ""}, + {"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 (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, ""}, } for _, tt := range tests { @@ -92,14 +123,14 @@ 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) diff --git a/token/driver/mock/validator.go b/token/driver/mock/validator.go index dca9a375cf..34cac7bf9d 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) ([]any, 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) ([]any, error) { var arg1Copy []byte if arg1 != nil { diff --git a/token/driver/request.go b/token/driver/request.go index 820fab5004..42be42e7a0 100644 --- a/token/driver/request.go +++ b/token/driver/request.go @@ -24,6 +24,7 @@ const ( // This version uses a secure structured format with explicit Request and Anchor fields, // providing robust boundary separation and preventing collision attacks. ProtocolV1 = 1 + ProtocolV2 = 2 // MaxAnchorSize defines the maximum allowed size for anchor parameter in bytes. // This limit prevents potential DoS attacks through excessive memory allocation. @@ -282,9 +283,14 @@ func (r *TokenRequest) ToProtos() (*request.TokenRequest, error) { } func (r *TokenRequest) FromProtos(tr *request.TokenRequest) error { - // Validate version - only ProtocolV1 (structured format) is supported - if tr.Version != uint32(ProtocolV1) { - return errors.Wrapf(ErrUnsupportedVersion, "expected [%d], got [%d]", ProtocolV1, tr.Version) + // 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) } // Store the version from the protobuf @@ -467,8 +473,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 } @@ -521,6 +537,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.OutputAuditInfo = outputsMetadata.AuditInfo i.Receivers = slices.GenericSliceOfPointers[AuditableIdentity](len(outputsMetadata.Receivers)) if err := protos.FromProtosSlice(outputsMetadata.Receivers, i.Receivers); err != nil { @@ -578,6 +597,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) @@ -728,7 +757,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") @@ -803,6 +839,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") @@ -1066,7 +1111,6 @@ func (m *TokenRequestMetadata) FromProtos(trm *request.TokenRequestMetadata) err m.Application = trm.ApplicationMetadata m.Actions = make([]*ActionMetadataEntry, 0, len(trm.Metadata)) - for _, meta := range trm.Metadata { entry := &ActionMetadataEntry{ ActionID: meta.ActionId, diff --git a/token/driver/validator.go b/token/driver/validator.go index a81e184090..396d243e96 100644 --- a/token/driver/validator.go +++ b/token/driver/validator.go @@ -12,6 +12,25 @@ import ( "github.com/LFDT-Panurus/panurus/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 = 256 * 1024 // 256KB for Idemix + // MaxIssuerRawSize is the maximum size of a raw issuer identity in bytes. + 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. + 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,19 @@ 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) +} + +// 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/manager_test.go b/token/services/tokens/manager_test.go index ab537890e1..a1a5d5656d 100644 --- a/token/services/tokens/manager_test.go +++ b/token/services/tokens/manager_test.go @@ -10,19 +10,41 @@ import ( "testing" "github.com/LFDT-Panurus/panurus/token" + "github.com/LFDT-Panurus/panurus/token/driver/mock" + tokenmock "github.com/LFDT-Panurus/panurus/token/mock" "github.com/LFDT-Panurus/panurus/token/services/tokens" - "github.com/LFDT-Panurus/panurus/token/services/tokens/mock" + tokensmock "github.com/LFDT-Panurus/panurus/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 b5b333aaa3..d7e26a3f4f 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/LFDT-Panurus/panurus/token" "github.com/LFDT-Panurus/panurus/token/driver" @@ -22,6 +23,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 +95,45 @@ 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 + configOnce sync.Once } 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} + s := &Service{ + tmsID: tmsID, + TMSProvider: TMSProvider, + NetworkProvider: networkProvider, + Storage: storage, + RequestsCache: requestsCache, + Config: driver.DefaultValidationConfig, + } + + 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 { + 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 { + logger.Warnf("TMSProvider is nil, falling back to default validation config") + } + }) } // AppendValid extracts actions from a token request, applies them to the local storage, @@ -125,6 +170,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 +200,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 +208,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 +218,55 @@ 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") + } + + t.loadConfig() + + 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) + } + + for i, tok := range req.Tokens { + if tok.Tok == nil { + return errors.Errorf("token at index %d is nil", i) + } + 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 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 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) + } + + if len(tok.Tok.Owner) == 0 && !tok.Flags.Redeemed { + return errors.Errorf("owner is missing at index %d", i) + } + + 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 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 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) + } + + 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 2e49a76499..2355b4464e 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..420b56db44 --- /dev/null +++ b/token/services/tokens/validation_test.go @@ -0,0 +1,178 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package tokens + +import ( + "context" + "strings" + "testing" + + token2 "github.com/LFDT-Panurus/panurus/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: 256 * 1024, + MaxIssuerRawSize: 256 * 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{Owner: []byte("owner1")}, + 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{Owner: []byte("owner1")}, + 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{Owner: []byte("owner1")}, + OwnerWalletID: strings.Repeat("a", 1025), + }, + }, + } + err := s.validateAppendRequest(req) + require.Error(t, err) + 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{ + { + Tok: &token2.Token{ + Owner: make([]byte, 256*1024+1), + }, + 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{Owner: []byte("owner1")}, + Issuer: make([]byte, 256*1024+1), + 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 52e82db0eb..5df9850a6c 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 5a77841a1f..6b4257d049 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 }