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
31 changes: 31 additions & 0 deletions token/services/identity/wallet/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,37 @@ func (s *Service) RegisterRecipientIdentity(ctx context.Context, data *tdriver.R

s.Logger.DebugfContext(ctx, "register recipient identity [%s] with audit info [%s]", data.Identity, utils.Hashable(data.AuditInfo))

// Step 1: Validate basic structure (nil checks, empty checks, length bounds)
if err := validateBasicStructure(data); err != nil {
return errors.Wrap(err, "basic structure validation failed")
}

// Step 2: Validate JSON structure
if err := validateJSONStructure(data.AuditInfo); err != nil {
return errors.Wrap(err, "JSON structure validation failed")
}

// Step 3: Decode the typed identity once and validate its structure.
typedID, err := identity.UnmarshalTypedIdentity(data.Identity)
if err != nil {
return errors.Wrap(err, "failed to unmarshal typed identity")
}
if err := validateIdentityStructure(typedID); err != nil {
return errors.Wrap(err, "identity structure validation failed")
}

// Step 4: Validate enrollment ID and revocation handle for non-composite types.
// Composite types (MultiSig, HTLC, Policy) have no enrollment ID or revocation handle.
if !isCompositeIdentityType(typedID.Type) {
if err := s.validateEnrollmentID(ctx, data); err != nil {
return errors.Wrap(err, "enrollment ID validation failed")
}
if err := s.validateRevocationHandle(ctx, data); err != nil {
return errors.Wrap(err, "revocation handle validation failed")
}
}

// Step 5: Match identity against audit info (cryptographic binding)
if err := s.Deserializer.MatchIdentity(ctx, data.Identity, data.AuditInfo); err != nil {
return errors.Wrapf(err, "failed to match identity to audit information for [%s]:[%s]", data.Identity, utils.Hashable(data.AuditInfo))
}
Expand Down
48 changes: 40 additions & 8 deletions token/services/identity/wallet/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

"github.com/LFDT-Panurus/panurus/token/driver"
dmock "github.com/LFDT-Panurus/panurus/token/driver/mock"
"github.com/LFDT-Panurus/panurus/token/services/identity"
idriver "github.com/LFDT-Panurus/panurus/token/services/identity/driver"
"github.com/LFDT-Panurus/panurus/token/services/identity/wallet"
wmock "github.com/LFDT-Panurus/panurus/token/services/identity/wallet/mock"
Expand Down Expand Up @@ -84,35 +85,45 @@ func TestRegisterRecipientIdentityFailuresAndSuccess(t *testing.T) {
d := &dmock.Deserializer{}
regSvc := wallet.NewService(&logging.MockLogger{}, ip, d, nil)

// Create a properly typed identity
typedID, err := identity.WrapWithType(driver.X509IdentityType, []byte("raw-identity"))
require.NoError(t, err)

// nil data
err := regSvc.RegisterRecipientIdentity(ctx, nil)
err = regSvc.RegisterRecipientIdentity(ctx, nil)
require.Error(t, err)

// RegisterRecipientIdentity fails
mockVerifier := &dmock.Verifier{}
mockVerifier.VerifyReturns(nil)
d.GetOwnerVerifierReturns(mockVerifier, nil)
d.MatchIdentityReturns(nil)
ip.GetEnrollmentIDReturns("alice", nil)
ip.GetRevocationHandlerReturns("rh", nil)
ip.RegisterRecipientIdentityReturns(errors.New("rri"))
err = regSvc.RegisterRecipientIdentity(ctx, &driver.RecipientData{Identity: driver.Identity("id")})
err = regSvc.RegisterRecipientIdentity(ctx, &driver.RecipientData{Identity: typedID, AuditInfo: []byte(`{"EID":"alice","RH":"rh"}`)})
require.Error(t, err)
ip.RegisterRecipientIdentityReturns(nil)

// MatchIdentity fails
d.MatchIdentityReturns(errors.New("mismatch"))
err = regSvc.RegisterRecipientIdentity(ctx, &driver.RecipientData{Identity: driver.Identity("id"), AuditInfo: []byte("ai")})
err = regSvc.RegisterRecipientIdentity(ctx, &driver.RecipientData{Identity: typedID, AuditInfo: []byte(`{"EID":"alice","RH":"rh"}`)})
require.Error(t, err)
d.MatchIdentityReturns(nil)

// RegisterRecipientData fails
ip.RegisterRecipientDataReturns(errors.New("rrd"))
err = regSvc.RegisterRecipientIdentity(ctx, &driver.RecipientData{Identity: driver.Identity("id"), AuditInfo: []byte("ai")})
err = regSvc.RegisterRecipientIdentity(ctx, &driver.RecipientData{Identity: typedID, AuditInfo: []byte(`{"EID":"alice","RH":"rh"}`)})
require.Error(t, err)
ip.RegisterRecipientDataReturns(nil)

// success
ip.RegisterRecipientIdentityCalls(func(context.Context, driver.Identity) error { return nil })
d.MatchIdentityCalls(func(context.Context, driver.Identity, []byte) error { return nil })
d.GetOwnerVerifierCalls(func(context.Context, driver.Identity) (driver.Verifier, error) { return &dmock.Verifier{}, nil })
d.GetOwnerVerifierCalls(func(context.Context, driver.Identity) (driver.Verifier, error) { return mockVerifier, nil })
ip.RegisterRecipientDataCalls(func(context.Context, *driver.RecipientData) error { return nil })

err = regSvc.RegisterRecipientIdentity(ctx, &driver.RecipientData{Identity: driver.Identity("id"), AuditInfo: []byte("ai")})
err = regSvc.RegisterRecipientIdentity(ctx, &driver.RecipientData{Identity: typedID, AuditInfo: []byte(`{"EID":"alice","RH":"rh"}`)})
require.NoError(t, err)
}

Expand All @@ -128,27 +139,48 @@ func (r *ipWithRollback) RollbackPartialRecipientRegistration(ctx context.Contex

func TestRegisterRecipientIdentity_MatchIdentityFailureSkipsIdentityProvider(t *testing.T) {
ctx := t.Context()

// Create a properly typed identity
typedID, err := identity.WrapWithType(driver.X509IdentityType, []byte("raw-identity"))
require.NoError(t, err)

ip := &dmock.IdentityProvider{}
ip.GetEnrollmentIDReturns("alice", nil)
ip.GetRevocationHandlerReturns("rh", nil)
d := &dmock.Deserializer{}
mockVerifier := &dmock.Verifier{}
mockVerifier.VerifyReturns(nil)
d.GetOwnerVerifierReturns(mockVerifier, nil)
d.MatchIdentityReturns(errors.New("mismatch"))
regSvc := wallet.NewService(&logging.MockLogger{}, ip, d, nil)

err := regSvc.RegisterRecipientIdentity(ctx, &driver.RecipientData{Identity: driver.Identity("id"), AuditInfo: []byte("ai")})
err = regSvc.RegisterRecipientIdentity(ctx, &driver.RecipientData{Identity: typedID, AuditInfo: []byte(`{"EID":"alice","RH":"rh"}`)})
require.Error(t, err)
require.Zero(t, ip.RegisterRecipientIdentityCallCount())
require.Equal(t, 1, d.MatchIdentityCallCount())
}

func TestRegisterRecipientIdentity_RollbackWhenRegisterRecipientDataFails(t *testing.T) {
ctx := t.Context()

// Create a properly typed identity
typedID, err := identity.WrapWithType(driver.X509IdentityType, []byte("raw-identity"))
require.NoError(t, err)

base := &dmock.IdentityProvider{}
base.GetEnrollmentIDReturns("alice", nil)
base.GetRevocationHandlerReturns("rh", nil)
base.RegisterRecipientIdentityReturns(nil)
base.RegisterRecipientDataReturns(errors.New("rrd"))
ip := &ipWithRollback{IdentityProvider: base}
d := &dmock.Deserializer{}
mockVerifier := &dmock.Verifier{}
mockVerifier.VerifyReturns(nil)
d.GetOwnerVerifierReturns(mockVerifier, nil)
d.MatchIdentityReturns(nil)
regSvc := wallet.NewService(&logging.MockLogger{}, ip, d, nil)

err := regSvc.RegisterRecipientIdentity(ctx, &driver.RecipientData{Identity: driver.Identity("id"), AuditInfo: []byte("ai")})
err = regSvc.RegisterRecipientIdentity(ctx, &driver.RecipientData{Identity: typedID, AuditInfo: []byte(`{"EID":"alice","RH":"rh"}`)})
require.Error(t, err)
require.Equal(t, 1, ip.rollbackCalls)
}
Expand Down
168 changes: 168 additions & 0 deletions token/services/identity/wallet/validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package wallet

import (
"context"
"encoding/json"

tdriver "github.com/LFDT-Panurus/panurus/token/driver"
"github.com/LFDT-Panurus/panurus/token/services/identity"
"github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors"
)

// Validation errors
var (
ErrEmptyIdentity = errors.New("empty identity")
ErrEmptyAuditInfo = errors.New("empty audit info")
ErrIdentityTooShort = errors.New("identity too short")
ErrIdentityTooLarge = errors.New("identity too large")
ErrEmptyRawIdentity = errors.New("empty raw identity data")
ErrAuditInfoTooLarge = errors.New("audit info too large")
ErrInvalidJSON = errors.New("audit info is not valid JSON")
ErrEmptyEnrollmentID = errors.New("empty enrollment ID")
ErrEnrollmentIDTooLong = errors.New("enrollment ID too long")
ErrEmptyRevocationHandle = errors.New("empty revocation handle")
ErrRevocationHandleTooLong = errors.New("revocation handle too long")
)

const (
// MaxEnrollmentIDLength defines the maximum allowed length for enrollment IDs
MaxEnrollmentIDLength = 256
// MaxRevocationHandleLength defines the maximum allowed length for revocation handles
MaxRevocationHandleLength = 512
// MinIdentityLength defines the minimum allowed length for identity data
MinIdentityLength = 10
// MaxIdentityLength defines the maximum allowed length for identity data (prevents DoS).
// Set generously so legitimate identities are never rejected: composite identities
// (MultiSig, Policy) aggregate several inner identities and X509 chains / Idemix proofs
// can run to several KB. The bound exists only to reject pathological multi-MB blobs.
MaxIdentityLength = 1 << 20 // 1 MiB
// MaxAuditInfoLength defines the maximum allowed length for audit info (prevents DoS)
MaxAuditInfoLength = 50000
)

// validateBasicStructure performs nil and empty checks on RecipientData
func validateBasicStructure(data *tdriver.RecipientData) error {
if data == nil {
return ErrNilRecipientData
}
if len(data.Identity) == 0 {
return ErrEmptyIdentity
}
if len(data.Identity) < MinIdentityLength {
return errors.Wrapf(ErrIdentityTooShort, "identity is %d bytes (min %d)", len(data.Identity), MinIdentityLength)
}
if len(data.Identity) > MaxIdentityLength {
return errors.Wrapf(ErrIdentityTooLarge, "identity is %d bytes (max %d)", len(data.Identity), MaxIdentityLength)
}
if len(data.AuditInfo) == 0 {
return ErrEmptyAuditInfo
}
if len(data.AuditInfo) > MaxAuditInfoLength {
return errors.Wrapf(ErrAuditInfoTooLarge, "audit info is %d bytes (max %d)", len(data.AuditInfo), MaxAuditInfoLength)
}

return nil
}

// validateJSONStructure validates that audit info is valid JSON
func validateJSONStructure(auditInfo []byte) error {
if !json.Valid(auditInfo) {
return ErrInvalidJSON
}

return nil
}

// validateIdentityStructure validates the decoded typed identity: the type must be
// recognized and the raw identity payload must not be empty.
func validateIdentityStructure(typedID *identity.TypedIdentity) error {
// Validate the identity type is recognized
if !isValidIdentityType(typedID.Type) {
return errors.Errorf("unknown identity type: %d", typedID.Type)
}

// Validate raw identity data is not empty
if len(typedID.Identity) == 0 {
return ErrEmptyRawIdentity
}

return nil
}

// validateEnrollmentID validates the enrollment ID is present and within length bounds.
// The caller is responsible for skipping composite identity types that have no enrollment ID.
func (s *Service) validateEnrollmentID(ctx context.Context, data *tdriver.RecipientData) error {
// Extract enrollment ID
eid, err := s.IdentityProvider.GetEnrollmentID(ctx, data.Identity, data.AuditInfo)
if err != nil {
return errors.Wrap(err, "failed to extract enrollment ID")
}

// Check not empty
if len(eid) == 0 {
return ErrEmptyEnrollmentID
}

// Check length
if len(eid) > MaxEnrollmentIDLength {
return errors.Wrapf(ErrEnrollmentIDTooLong, "enrollment ID is %d bytes (max %d)", len(eid), MaxEnrollmentIDLength)
}

return nil
}

// validateRevocationHandle validates the revocation handle is present and within length bounds.
// The caller is responsible for skipping composite identity types that have no revocation handle.
func (s *Service) validateRevocationHandle(ctx context.Context, data *tdriver.RecipientData) error {
// Extract revocation handle
rh, err := s.IdentityProvider.GetRevocationHandler(ctx, data.Identity, data.AuditInfo)
if err != nil {
return errors.Wrap(err, "failed to extract revocation handle")
}

// Check not empty
if len(rh) == 0 {
return ErrEmptyRevocationHandle
}

// Check reasonable length
if len(rh) > MaxRevocationHandleLength {
return errors.Wrapf(ErrRevocationHandleTooLong, "revocation handle is %d bytes (max %d)", len(rh), MaxRevocationHandleLength)
}

return nil
}

// isValidIdentityType checks if the identity type is one of the recognized types
func isValidIdentityType(t tdriver.IdentityType) bool {
switch t {
case tdriver.IdemixIdentityType,
tdriver.X509IdentityType,
tdriver.IdemixNymIdentityType,
tdriver.HTLCScriptIdentityType,
tdriver.MultiSigIdentityType,
tdriver.PolicyIdentityType:
return true
default:
return false
}
}

// isCompositeIdentityType checks if the identity type is a composite type
// Composite types (MultiSig, HTLC, Policy) don't have enrollment IDs or revocation handles
func isCompositeIdentityType(t tdriver.IdentityType) bool {
switch t {
case tdriver.MultiSigIdentityType,
tdriver.HTLCScriptIdentityType,
tdriver.PolicyIdentityType:
return true
default:
return false
}
}
Loading
Loading