Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
16 changes: 16 additions & 0 deletions token/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,26 @@ 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 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
}
71 changes: 71 additions & 0 deletions token/core/common/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -92,6 +95,7 @@ func NewValidator[P driver.PublicParameters, T driver.Input, TA driver.TransferA
TransferValidators: transferValidators,
IssueValidators: issueValidators,
AuditingValidators: auditingValidators,
ValidationConfig: driver.DefaultValidationConfig,
}
}

Expand All @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Version 0 is now accepted, but it used to be rejected. Why this is removed? This makes validation weaker, which is the opposite of what this PR is for, and it's not related to payload size limits.

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A limit of 0 means two opposite things:

  • common/validator.go: 0 = no limit (checks are guarded with if MaxX > 0)
  • tokens.go validateAppendRequest: 0 = reject everything (no guard, so len(req.Tokens) > 0 always fails)

So an operator who sets maxTokenOutputsPerTx: 0 expecting "unlimited" would instead block every append.

I think we need to fix this and add the same > 0 guard in tokens.go so 0 means "unlimited" in both places.

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
Expand Down Expand Up @@ -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
Expand Down
69 changes: 50 additions & 19 deletions token/core/common/validator_version_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -36,30 +35,57 @@ 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",
minProtocolVersion: driver.ProtocolV1,
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 {
t.Run(tt.name, func(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
}

Expand All @@ -81,25 +107,30 @@ 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 {
t.Run(tt.name, func(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)
Expand Down
37 changes: 37 additions & 0 deletions token/driver/mock/validator.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading