diff --git a/integration/token/fungible/views/boolpolicy.go b/integration/token/fungible/views/boolpolicy.go index 180f195870..057522510f 100644 --- a/integration/token/fungible/views/boolpolicy.go +++ b/integration/token/fungible/views/boolpolicy.go @@ -217,8 +217,12 @@ func (m *PolicyAcceptSpendView) Call(context view.Context) (any, error) { continue } - if len(in.Owner) != 0 && sigService.IsMe(context.Context(), in.Owner) { - assert.Fail("received tx consumes additional token [%s] owned by this responder", in.Id) + if len(in.Owner) != 0 { + isMe, err := sigService.IsMe(context.Context(), in.Owner) + assert.NoError(err, "failed to check ownership of input [%s]", in.Id) + if isMe { + assert.Fail("received tx consumes additional token [%s] owned by this responder", in.Id) + } } } assert.True(matched, "received tx does not consume the token named in SpendRequest") diff --git a/integration/token/fungible/views/multisig.go b/integration/token/fungible/views/multisig.go index 60edd7075b..86ea23a628 100644 --- a/integration/token/fungible/views/multisig.go +++ b/integration/token/fungible/views/multisig.go @@ -206,8 +206,12 @@ func (m *MultiSigAcceptSpendView) Call(context view.Context) (any, error) { continue } // Any other input must NOT belong to this responder. - if len(in.Owner) != 0 && sigService.IsMe(context.Context(), in.Owner) { - assert.Fail("received tx consumes additional token [%s] owned by this responder", in.Id) + if len(in.Owner) != 0 { + isMe, err := sigService.IsMe(context.Context(), in.Owner) + assert.NoError(err, "failed to check ownership of input [%s]", in.Id) + if isMe { + assert.Fail("received tx consumes additional token [%s] owned by this responder", in.Id) + } } } assert.True(matched, "received tx does not consume the token named in SpendRequest") diff --git a/integration/token/fungible/views/utils.go b/integration/token/fungible/views/utils.go index e9cce941a3..6eb2c053d7 100644 --- a/integration/token/fungible/views/utils.go +++ b/integration/token/fungible/views/utils.go @@ -29,7 +29,9 @@ func AssertTokens(sp token.ServiceProvider, tx *ttx.Transaction, outputs *token. assert.NoError(err, "failed to get token db for [%s]", tx.TokenService().ID()) for _, output := range outputs.Outputs() { tokenID := output.ID(token.RequestAnchor(tx.ID())) - if output.Owner.Equal(id) || tx.TokenService().SigService().IsMe(ctx, output.Owner) { + isMe, err := tx.TokenService().SigService().IsMe(ctx, output.Owner) + assert.NoError(err, "failed to check ownership of [%s]", output.Owner) + if output.Owner.Equal(id) || isMe { // check it exists toks, err := db.GetTokens(ctx, tokenID) assert.NoError(err, "failed to retrieve token [%s]", tokenID) diff --git a/token/driver/mock/ip.go b/token/driver/mock/ip.go index 5ca70aff42..f957d62be9 100644 --- a/token/driver/mock/ip.go +++ b/token/driver/mock/ip.go @@ -9,7 +9,7 @@ import ( ) type IdentityProvider struct { - AreMeStub func(context.Context, ...driver.Identity) []string + AreMeStub func(context.Context, ...driver.Identity) ([]string, error) areMeMutex sync.RWMutex areMeArgsForCall []struct { arg1 context.Context @@ -17,9 +17,11 @@ type IdentityProvider struct { } areMeReturns struct { result1 []string + result2 error } areMeReturnsOnCall map[int]struct { result1 []string + result2 error } BindStub func(context.Context, driver.Identity, ...driver.Identity) error bindMutex sync.RWMutex @@ -109,7 +111,7 @@ type IdentityProvider struct { result1 driver.Signer result2 error } - IsMeStub func(context.Context, driver.Identity) bool + IsMeStub func(context.Context, driver.Identity) (bool, error) isMeMutex sync.RWMutex isMeArgsForCall []struct { arg1 context.Context @@ -117,9 +119,11 @@ type IdentityProvider struct { } isMeReturns struct { result1 bool + result2 error } isMeReturnsOnCall map[int]struct { result1 bool + result2 error } RegisterRecipientDataStub func(context.Context, *driver.RecipientData) error registerRecipientDataMutex sync.RWMutex @@ -165,7 +169,7 @@ type IdentityProvider struct { invocationsMutex sync.RWMutex } -func (fake *IdentityProvider) AreMe(arg1 context.Context, arg2 ...driver.Identity) []string { +func (fake *IdentityProvider) AreMe(arg1 context.Context, arg2 ...driver.Identity) ([]string, error) { fake.areMeMutex.Lock() ret, specificReturn := fake.areMeReturnsOnCall[len(fake.areMeArgsForCall)] fake.areMeArgsForCall = append(fake.areMeArgsForCall, struct { @@ -180,9 +184,9 @@ func (fake *IdentityProvider) AreMe(arg1 context.Context, arg2 ...driver.Identit return stub(arg1, arg2...) } if specificReturn { - return ret.result1 + return ret.result1, ret.result2 } - return fakeReturns.result1 + return fakeReturns.result1, fakeReturns.result2 } func (fake *IdentityProvider) AreMeCallCount() int { @@ -191,7 +195,7 @@ func (fake *IdentityProvider) AreMeCallCount() int { return len(fake.areMeArgsForCall) } -func (fake *IdentityProvider) AreMeCalls(stub func(context.Context, ...driver.Identity) []string) { +func (fake *IdentityProvider) AreMeCalls(stub func(context.Context, ...driver.Identity) ([]string, error)) { fake.areMeMutex.Lock() defer fake.areMeMutex.Unlock() fake.AreMeStub = stub @@ -204,27 +208,30 @@ func (fake *IdentityProvider) AreMeArgsForCall(i int) (context.Context, []driver return argsForCall.arg1, argsForCall.arg2 } -func (fake *IdentityProvider) AreMeReturns(result1 []string) { +func (fake *IdentityProvider) AreMeReturns(result1 []string, result2 error) { fake.areMeMutex.Lock() defer fake.areMeMutex.Unlock() fake.AreMeStub = nil fake.areMeReturns = struct { result1 []string - }{result1} + result2 error + }{result1, result2} } -func (fake *IdentityProvider) AreMeReturnsOnCall(i int, result1 []string) { +func (fake *IdentityProvider) AreMeReturnsOnCall(i int, result1 []string, result2 error) { fake.areMeMutex.Lock() defer fake.areMeMutex.Unlock() fake.AreMeStub = nil if fake.areMeReturnsOnCall == nil { fake.areMeReturnsOnCall = make(map[int]struct { result1 []string + result2 error }) } fake.areMeReturnsOnCall[i] = struct { result1 []string - }{result1} + result2 error + }{result1, result2} } func (fake *IdentityProvider) Bind(arg1 context.Context, arg2 driver.Identity, arg3 ...driver.Identity) error { @@ -636,7 +643,7 @@ func (fake *IdentityProvider) GetSignerReturnsOnCall(i int, result1 driver.Signe }{result1, result2} } -func (fake *IdentityProvider) IsMe(arg1 context.Context, arg2 driver.Identity) bool { +func (fake *IdentityProvider) IsMe(arg1 context.Context, arg2 driver.Identity) (bool, error) { fake.isMeMutex.Lock() ret, specificReturn := fake.isMeReturnsOnCall[len(fake.isMeArgsForCall)] fake.isMeArgsForCall = append(fake.isMeArgsForCall, struct { @@ -651,9 +658,9 @@ func (fake *IdentityProvider) IsMe(arg1 context.Context, arg2 driver.Identity) b return stub(arg1, arg2) } if specificReturn { - return ret.result1 + return ret.result1, ret.result2 } - return fakeReturns.result1 + return fakeReturns.result1, fakeReturns.result2 } func (fake *IdentityProvider) IsMeCallCount() int { @@ -662,7 +669,7 @@ func (fake *IdentityProvider) IsMeCallCount() int { return len(fake.isMeArgsForCall) } -func (fake *IdentityProvider) IsMeCalls(stub func(context.Context, driver.Identity) bool) { +func (fake *IdentityProvider) IsMeCalls(stub func(context.Context, driver.Identity) (bool, error)) { fake.isMeMutex.Lock() defer fake.isMeMutex.Unlock() fake.IsMeStub = stub @@ -675,27 +682,30 @@ func (fake *IdentityProvider) IsMeArgsForCall(i int) (context.Context, driver.Id return argsForCall.arg1, argsForCall.arg2 } -func (fake *IdentityProvider) IsMeReturns(result1 bool) { +func (fake *IdentityProvider) IsMeReturns(result1 bool, result2 error) { fake.isMeMutex.Lock() defer fake.isMeMutex.Unlock() fake.IsMeStub = nil fake.isMeReturns = struct { result1 bool - }{result1} + result2 error + }{result1, result2} } -func (fake *IdentityProvider) IsMeReturnsOnCall(i int, result1 bool) { +func (fake *IdentityProvider) IsMeReturnsOnCall(i int, result1 bool, result2 error) { fake.isMeMutex.Lock() defer fake.isMeMutex.Unlock() fake.IsMeStub = nil if fake.isMeReturnsOnCall == nil { fake.isMeReturnsOnCall = make(map[int]struct { result1 bool + result2 error }) } fake.isMeReturnsOnCall[i] = struct { result1 bool - }{result1} + result2 error + }{result1, result2} } func (fake *IdentityProvider) RegisterRecipientData(arg1 context.Context, arg2 *driver.RecipientData) error { diff --git a/token/driver/wallet.go b/token/driver/wallet.go index 3a62b16cc7..8bcca32209 100644 --- a/token/driver/wallet.go +++ b/token/driver/wallet.go @@ -39,10 +39,14 @@ type IdentityProvider interface { RegisterSigner(ctx context.Context, identity Identity, signer Signer, verifier Verifier, signerInfo []byte, ephemeral bool) error // AreMe checks a list of identities and returns those that have signers registered with this provider. - AreMe(ctx context.Context, identities ...Identity) []string + // A non-nil error means the ownership check could not be completed (for example a storage failure); + // in that case the returned slice must not be treated as an authoritative answer. + AreMe(ctx context.Context, identities ...Identity) ([]string, error) // IsMe returns true if a signer has been registered for the specified identity. - IsMe(ctx context.Context, party Identity) bool + // A non-nil error means ownership could not be determined and the boolean must be ignored; + // callers must not treat a false-with-error as an authoritative "not mine". + IsMe(ctx context.Context, party Identity) (bool, error) // GetEnrollmentID extracts the enrollment identifier from the provided audit information for a specific identity. GetEnrollmentID(ctx context.Context, identity Identity, auditInfo []byte) (string, error) diff --git a/token/services/identity/membership/lm.go b/token/services/identity/membership/lm.go index c84ab50344..a4308b9ce0 100644 --- a/token/services/identity/membership/lm.go +++ b/token/services/identity/membership/lm.go @@ -100,7 +100,7 @@ type IdentityStoreService interface { // //go:generate counterfeiter -o mock/ip.go -fake-name IdentityProvider . IdentityProvider type IdentityProvider interface { - IsMe(context.Context, idriver.Identity) bool + IsMe(context.Context, idriver.Identity) (bool, error) // Bind an ephemeral identity to another identity Bind(ctx context.Context, longTerm idriver.Identity, ephemeralIdentities ...idriver.Identity) error // RegisterIdentityDescriptor register the passed identity descriptor with an alias @@ -293,7 +293,8 @@ func (l *LocalMembership) Close() { // IsMe reports whether the given identity belongs to this local membership set. // It delegates to the configured IdentityProvider to determine membership. -func (l *LocalMembership) IsMe(ctx context.Context, id token.Identity) bool { +// A non-nil error means membership could not be determined (the boolean must be ignored). +func (l *LocalMembership) IsMe(ctx context.Context, id token.Identity) (bool, error) { return l.IdentityProvider.IsMe(ctx, id) } diff --git a/token/services/identity/membership/lm_security_test.go b/token/services/identity/membership/lm_security_test.go index 8602077da1..5496ab3ef0 100644 --- a/token/services/identity/membership/lm_security_test.go +++ b/token/services/identity/membership/lm_security_test.go @@ -188,7 +188,7 @@ func TestGetIdentityInfo_NotFoundDoesNotLeakOtherIdentities(t *testing.T) { ip := &mock.IdentityProvider{} ip.BindReturns(nil) - ip.IsMeReturns(false) + ip.IsMeReturns(false, nil) iss := &mock.IdentityStoreService{} iss.ConfigurationExistsReturns(false, nil) diff --git a/token/services/identity/membership/lm_test.go b/token/services/identity/membership/lm_test.go index fe8b37347f..6beb07c6ea 100644 --- a/token/services/identity/membership/lm_test.go +++ b/token/services/identity/membership/lm_test.go @@ -34,7 +34,7 @@ import ( func TestNewLocalMembership_IsMe(t *testing.T) { ip := &mock.IdentityProvider{} - ip.IsMeReturns(true) + ip.IsMeReturns(true, nil) lm := membership.NewLocalMembership( logging.MustGetLogger("test"), @@ -47,7 +47,9 @@ func TestNewLocalMembership_IsMe(t *testing.T) { ip, ) - assert.True(t, lm.IsMe(t.Context(), []byte("any"))) + isMe, err := lm.IsMe(t.Context(), []byte("any")) + require.NoError(t, err) + assert.True(t, isMe) assert.Equal(t, token.Identity("netid"), lm.DefaultNetworkIdentity()) } diff --git a/token/services/identity/membership/mock/ip.go b/token/services/identity/membership/mock/ip.go index 872ff39490..2e31a9b48a 100644 --- a/token/services/identity/membership/mock/ip.go +++ b/token/services/identity/membership/mock/ip.go @@ -23,7 +23,7 @@ type IdentityProvider struct { bindReturnsOnCall map[int]struct { result1 error } - IsMeStub func(context.Context, driver.Identity) bool + IsMeStub func(context.Context, driver.Identity) (bool, error) isMeMutex sync.RWMutex isMeArgsForCall []struct { arg1 context.Context @@ -31,9 +31,11 @@ type IdentityProvider struct { } isMeReturns struct { result1 bool + result2 error } isMeReturnsOnCall map[int]struct { result1 bool + result2 error } RegisterIdentityDescriptorStub func(context.Context, *driver.IdentityDescriptor, driver.Identity) error registerIdentityDescriptorMutex sync.RWMutex @@ -115,7 +117,7 @@ func (fake *IdentityProvider) BindReturnsOnCall(i int, result1 error) { }{result1} } -func (fake *IdentityProvider) IsMe(arg1 context.Context, arg2 driver.Identity) bool { +func (fake *IdentityProvider) IsMe(arg1 context.Context, arg2 driver.Identity) (bool, error) { fake.isMeMutex.Lock() ret, specificReturn := fake.isMeReturnsOnCall[len(fake.isMeArgsForCall)] fake.isMeArgsForCall = append(fake.isMeArgsForCall, struct { @@ -130,9 +132,9 @@ func (fake *IdentityProvider) IsMe(arg1 context.Context, arg2 driver.Identity) b return stub(arg1, arg2) } if specificReturn { - return ret.result1 + return ret.result1, ret.result2 } - return fakeReturns.result1 + return fakeReturns.result1, fakeReturns.result2 } func (fake *IdentityProvider) IsMeCallCount() int { @@ -141,7 +143,7 @@ func (fake *IdentityProvider) IsMeCallCount() int { return len(fake.isMeArgsForCall) } -func (fake *IdentityProvider) IsMeCalls(stub func(context.Context, driver.Identity) bool) { +func (fake *IdentityProvider) IsMeCalls(stub func(context.Context, driver.Identity) (bool, error)) { fake.isMeMutex.Lock() defer fake.isMeMutex.Unlock() fake.IsMeStub = stub @@ -154,27 +156,30 @@ func (fake *IdentityProvider) IsMeArgsForCall(i int) (context.Context, driver.Id return argsForCall.arg1, argsForCall.arg2 } -func (fake *IdentityProvider) IsMeReturns(result1 bool) { +func (fake *IdentityProvider) IsMeReturns(result1 bool, result2 error) { fake.isMeMutex.Lock() defer fake.isMeMutex.Unlock() fake.IsMeStub = nil fake.isMeReturns = struct { result1 bool - }{result1} + result2 error + }{result1, result2} } -func (fake *IdentityProvider) IsMeReturnsOnCall(i int, result1 bool) { +func (fake *IdentityProvider) IsMeReturnsOnCall(i int, result1 bool, result2 error) { fake.isMeMutex.Lock() defer fake.isMeMutex.Unlock() fake.IsMeStub = nil if fake.isMeReturnsOnCall == nil { fake.isMeReturnsOnCall = make(map[int]struct { result1 bool + result2 error }) } fake.isMeReturnsOnCall[i] = struct { result1 bool - }{result1} + result2 error + }{result1, result2} } func (fake *IdentityProvider) RegisterIdentityDescriptor(arg1 context.Context, arg2 *driver.IdentityDescriptor, arg3 driver.Identity) error { diff --git a/token/services/identity/provider.go b/token/services/identity/provider.go index 0952394cbb..e37792cf1c 100644 --- a/token/services/identity/provider.go +++ b/token/services/identity/provider.go @@ -148,15 +148,25 @@ func (p *Provider) RegisterSigner(ctx context.Context, identity driver.Identity, // Each identity is resolved via the signer cache and configured storage. // There is no secondary "is me" cache: a real cache would need careful handling for // single-use identities (for example Idemix nyms) and is intentionally omitted here. -func (p *Provider) AreMe(ctx context.Context, identities ...driver.Identity) []string { +// A non-nil error means the storage lookup failed and the returned slice is not +// authoritative: identities that could not be checked are absent from it precisely because +// the check failed, not because they are unknown. +func (p *Provider) AreMe(ctx context.Context, identities ...driver.Identity) ([]string, error) { p.Logger.DebugfContext(ctx, "identity [%s] is me?", identities) return p.areMe(ctx, identities...) } -// IsMe returns true if a signer was ever registered for the passed identity -func (p *Provider) IsMe(ctx context.Context, identity driver.Identity) bool { - return len(p.AreMe(ctx, identity)) > 0 +// IsMe returns true if a signer was ever registered for the passed identity. +// A non-nil error means ownership could not be determined; the boolean must be ignored in +// that case rather than treated as an authoritative "not mine". +func (p *Provider) IsMe(ctx context.Context, identity driver.Identity) (bool, error) { + me, err := p.AreMe(ctx, identity) + if err != nil { + return false, err + } + + return len(me) > 0, nil } // GetAuditInfo returns the audit information associated to the passed identity, nil otherwise. @@ -243,7 +253,7 @@ func (p *Provider) RegisterIdentityDescriptor(ctx context.Context, identityDescr return nil } -func (p *Provider) areMe(ctx context.Context, identities ...driver.Identity) []string { +func (p *Provider) areMe(ctx context.Context, identities ...driver.Identity) ([]string, error) { p.Logger.DebugfContext(ctx, "is me [%s]?", identities) idHashes := make([]string, len(identities)) for i, id := range identities { @@ -264,19 +274,20 @@ func (p *Provider) areMe(ctx context.Context, identities ...driver.Identity) []s } if len(notFound) == 0 { - return result.ToSlice() + return result.ToSlice(), nil } - // check Storage + // check Storage. A failure here must not be flattened into a negative answer: the + // identities we could not check are exactly the ones in notFound, and silently dropping + // them would report an owned identity as not-owned. Propagate instead so callers can tell + // "confirmed not mine" from "couldn't check". found, err := p.storage.GetExistingSignerInfo(ctx, notFound...) if err != nil { - p.Logger.Errorf("failed checking if a signer exists [%s]", err) - - return result.ToSlice() + return nil, errors.Wrapf(err, "failed checking if a signer exists") } result.Add(found...) - return result.ToSlice() + return result.ToSlice(), nil } func (p *Provider) getSigner(ctx context.Context, identity driver.Identity, idHash string) (driver.Signer, error) { diff --git a/token/services/identity/provider_test.go b/token/services/identity/provider_test.go index 0e474e6b03..4283af865a 100644 --- a/token/services/identity/provider_test.go +++ b/token/services/identity/provider_test.go @@ -70,10 +70,61 @@ func TestProvider_RegisterSigner_And_IsMe(t *testing.T) { require.Equal(t, 1, storage.RegisterIdentityDescriptorCallCount()) // provider should now consider this identity as "me" - isMe := p.IsMe(t.Context(), id) + isMe, err := p.IsMe(t.Context(), id) + require.NoError(t, err) assert.True(t, isMe) } +// TestProvider_IsMe_StorageErrorIsPropagated is a regression test for issue #2066: a storage +// failure while checking ownership of an uncached identity must be surfaced, not silently +// flattened into a "not mine" answer. +func TestProvider_IsMe_StorageErrorIsPropagated(t *testing.T) { + storage := &idmock.Storage{} + des := &idmock.Deserializer{} + nbs := &idmock.NetworkBinderService{} + eidu := &idmock.EnrollmentIDUnmarshaler{} + + p := identity.NewProvider(logging.MustGetLogger(), storage, des, nbs, eidu, nil) + + // The identity is not warm in the cache, so areMe must consult storage, which fails. + storage.GetExistingSignerInfoReturns(nil, errors.New("boom")) + + id := driver.Identity("owned_but_uncached") + + isMe, err := p.IsMe(t.Context(), id) + require.Error(t, err, "a storage failure must be propagated, not reported as a confident 'not mine'") + assert.False(t, isMe, "the boolean must not be trusted when an error is returned") + + me, err := p.AreMe(t.Context(), id) + require.Error(t, err) + assert.Nil(t, me) +} + +// TestProvider_AreMe_StorageErrorDoesNotLeakCacheOnlyResult verifies the exact failure mode from +// issue #2066: when some identities are cache hits and the storage lookup for the rest fails, +// AreMe must return the error rather than a cache-only slice that would report the uncheckable +// identities as "not mine". +func TestProvider_AreMe_StorageErrorDoesNotLeakCacheOnlyResult(t *testing.T) { + storage := &idmock.Storage{} + des := &idmock.Deserializer{} + nbs := &idmock.NetworkBinderService{} + eidu := &idmock.EnrollmentIDUnmarshaler{} + + p := identity.NewProvider(logging.MustGetLogger(), storage, des, nbs, eidu, nil) + + // Warm the cache for `cached` by registering a signer for it. + cached := driver.Identity("cached") + storage.RegisterIdentityDescriptorReturns(nil) + require.NoError(t, p.RegisterSigner(t.Context(), cached, &drvmock.Signer{}, &drvmock.Verifier{}, nil, false)) + + // The storage lookup for the remaining (uncached) identity fails. + storage.GetExistingSignerInfoReturns(nil, errors.New("boom")) + + me, err := p.AreMe(t.Context(), cached, driver.Identity("uncached")) + require.Error(t, err, "a storage failure must not be masked by the cache-hit portion of the result") + assert.Nil(t, me) +} + func TestProvider_GetSigner_Deserializable(t *testing.T) { storage := &idmock.Storage{} des := &idmock.Deserializer{} diff --git a/token/services/identity/role/mock/ip.go b/token/services/identity/role/mock/ip.go index 3cf149eb6b..f2e3bb55c8 100644 --- a/token/services/identity/role/mock/ip.go +++ b/token/services/identity/role/mock/ip.go @@ -10,7 +10,7 @@ import ( ) type IdentityProvider struct { - AreMeStub func(context.Context, ...driver.Identity) []string + AreMeStub func(context.Context, ...driver.Identity) ([]string, error) areMeMutex sync.RWMutex areMeArgsForCall []struct { arg1 context.Context @@ -18,9 +18,11 @@ type IdentityProvider struct { } areMeReturns struct { result1 []string + result2 error } areMeReturnsOnCall map[int]struct { result1 []string + result2 error } BindStub func(context.Context, driver.Identity, ...driver.Identity) error bindMutex sync.RWMutex @@ -110,7 +112,7 @@ type IdentityProvider struct { result1 driver.Signer result2 error } - IsMeStub func(context.Context, driver.Identity) bool + IsMeStub func(context.Context, driver.Identity) (bool, error) isMeMutex sync.RWMutex isMeArgsForCall []struct { arg1 context.Context @@ -118,9 +120,11 @@ type IdentityProvider struct { } isMeReturns struct { result1 bool + result2 error } isMeReturnsOnCall map[int]struct { result1 bool + result2 error } RegisterRecipientDataStub func(context.Context, *driver.RecipientData) error registerRecipientDataMutex sync.RWMutex @@ -166,7 +170,7 @@ type IdentityProvider struct { invocationsMutex sync.RWMutex } -func (fake *IdentityProvider) AreMe(arg1 context.Context, arg2 ...driver.Identity) []string { +func (fake *IdentityProvider) AreMe(arg1 context.Context, arg2 ...driver.Identity) ([]string, error) { fake.areMeMutex.Lock() ret, specificReturn := fake.areMeReturnsOnCall[len(fake.areMeArgsForCall)] fake.areMeArgsForCall = append(fake.areMeArgsForCall, struct { @@ -181,9 +185,9 @@ func (fake *IdentityProvider) AreMe(arg1 context.Context, arg2 ...driver.Identit return stub(arg1, arg2...) } if specificReturn { - return ret.result1 + return ret.result1, ret.result2 } - return fakeReturns.result1 + return fakeReturns.result1, fakeReturns.result2 } func (fake *IdentityProvider) AreMeCallCount() int { @@ -192,7 +196,7 @@ func (fake *IdentityProvider) AreMeCallCount() int { return len(fake.areMeArgsForCall) } -func (fake *IdentityProvider) AreMeCalls(stub func(context.Context, ...driver.Identity) []string) { +func (fake *IdentityProvider) AreMeCalls(stub func(context.Context, ...driver.Identity) ([]string, error)) { fake.areMeMutex.Lock() defer fake.areMeMutex.Unlock() fake.AreMeStub = stub @@ -205,27 +209,30 @@ func (fake *IdentityProvider) AreMeArgsForCall(i int) (context.Context, []driver return argsForCall.arg1, argsForCall.arg2 } -func (fake *IdentityProvider) AreMeReturns(result1 []string) { +func (fake *IdentityProvider) AreMeReturns(result1 []string, result2 error) { fake.areMeMutex.Lock() defer fake.areMeMutex.Unlock() fake.AreMeStub = nil fake.areMeReturns = struct { result1 []string - }{result1} + result2 error + }{result1, result2} } -func (fake *IdentityProvider) AreMeReturnsOnCall(i int, result1 []string) { +func (fake *IdentityProvider) AreMeReturnsOnCall(i int, result1 []string, result2 error) { fake.areMeMutex.Lock() defer fake.areMeMutex.Unlock() fake.AreMeStub = nil if fake.areMeReturnsOnCall == nil { fake.areMeReturnsOnCall = make(map[int]struct { result1 []string + result2 error }) } fake.areMeReturnsOnCall[i] = struct { result1 []string - }{result1} + result2 error + }{result1, result2} } func (fake *IdentityProvider) Bind(arg1 context.Context, arg2 driver.Identity, arg3 ...driver.Identity) error { @@ -637,7 +644,7 @@ func (fake *IdentityProvider) GetSignerReturnsOnCall(i int, result1 driver.Signe }{result1, result2} } -func (fake *IdentityProvider) IsMe(arg1 context.Context, arg2 driver.Identity) bool { +func (fake *IdentityProvider) IsMe(arg1 context.Context, arg2 driver.Identity) (bool, error) { fake.isMeMutex.Lock() ret, specificReturn := fake.isMeReturnsOnCall[len(fake.isMeArgsForCall)] fake.isMeArgsForCall = append(fake.isMeArgsForCall, struct { @@ -652,9 +659,9 @@ func (fake *IdentityProvider) IsMe(arg1 context.Context, arg2 driver.Identity) b return stub(arg1, arg2) } if specificReturn { - return ret.result1 + return ret.result1, ret.result2 } - return fakeReturns.result1 + return fakeReturns.result1, fakeReturns.result2 } func (fake *IdentityProvider) IsMeCallCount() int { @@ -663,7 +670,7 @@ func (fake *IdentityProvider) IsMeCallCount() int { return len(fake.isMeArgsForCall) } -func (fake *IdentityProvider) IsMeCalls(stub func(context.Context, driver.Identity) bool) { +func (fake *IdentityProvider) IsMeCalls(stub func(context.Context, driver.Identity) (bool, error)) { fake.isMeMutex.Lock() defer fake.isMeMutex.Unlock() fake.IsMeStub = stub @@ -676,27 +683,30 @@ func (fake *IdentityProvider) IsMeArgsForCall(i int) (context.Context, driver.Id return argsForCall.arg1, argsForCall.arg2 } -func (fake *IdentityProvider) IsMeReturns(result1 bool) { +func (fake *IdentityProvider) IsMeReturns(result1 bool, result2 error) { fake.isMeMutex.Lock() defer fake.isMeMutex.Unlock() fake.IsMeStub = nil fake.isMeReturns = struct { result1 bool - }{result1} + result2 error + }{result1, result2} } -func (fake *IdentityProvider) IsMeReturnsOnCall(i int, result1 bool) { +func (fake *IdentityProvider) IsMeReturnsOnCall(i int, result1 bool, result2 error) { fake.isMeMutex.Lock() defer fake.isMeMutex.Unlock() fake.IsMeStub = nil if fake.isMeReturnsOnCall == nil { fake.isMeReturnsOnCall = make(map[int]struct { result1 bool + result2 error }) } fake.isMeReturnsOnCall[i] = struct { result1 bool - }{result1} + result2 error + }{result1, result2} } func (fake *IdentityProvider) RegisterRecipientData(arg1 context.Context, arg2 *driver.RecipientData) error { diff --git a/token/services/identity/role/mock/lm.go b/token/services/identity/role/mock/lm.go index 53b12231da..1665ea30c9 100644 --- a/token/services/identity/role/mock/lm.go +++ b/token/services/identity/role/mock/lm.go @@ -76,7 +76,7 @@ type LocalMembership struct { result1 []string result2 error } - IsMeStub func(context.Context, driver.Identity) bool + IsMeStub func(context.Context, driver.Identity) (bool, error) isMeMutex sync.RWMutex isMeArgsForCall []struct { arg1 context.Context @@ -84,9 +84,11 @@ type LocalMembership struct { } isMeReturns struct { result1 bool + result2 error } isMeReturnsOnCall map[int]struct { result1 bool + result2 error } RegisterIdentityStub func(context.Context, driver.IdentityConfiguration) error registerIdentityMutex sync.RWMutex @@ -426,7 +428,7 @@ func (fake *LocalMembership) IDsReturnsOnCall(i int, result1 []string, result2 e }{result1, result2} } -func (fake *LocalMembership) IsMe(arg1 context.Context, arg2 driver.Identity) bool { +func (fake *LocalMembership) IsMe(arg1 context.Context, arg2 driver.Identity) (bool, error) { fake.isMeMutex.Lock() ret, specificReturn := fake.isMeReturnsOnCall[len(fake.isMeArgsForCall)] fake.isMeArgsForCall = append(fake.isMeArgsForCall, struct { @@ -441,9 +443,9 @@ func (fake *LocalMembership) IsMe(arg1 context.Context, arg2 driver.Identity) bo return stub(arg1, arg2) } if specificReturn { - return ret.result1 + return ret.result1, ret.result2 } - return fakeReturns.result1 + return fakeReturns.result1, fakeReturns.result2 } func (fake *LocalMembership) IsMeCallCount() int { @@ -452,7 +454,7 @@ func (fake *LocalMembership) IsMeCallCount() int { return len(fake.isMeArgsForCall) } -func (fake *LocalMembership) IsMeCalls(stub func(context.Context, driver.Identity) bool) { +func (fake *LocalMembership) IsMeCalls(stub func(context.Context, driver.Identity) (bool, error)) { fake.isMeMutex.Lock() defer fake.isMeMutex.Unlock() fake.IsMeStub = stub @@ -465,27 +467,30 @@ func (fake *LocalMembership) IsMeArgsForCall(i int) (context.Context, driver.Ide return argsForCall.arg1, argsForCall.arg2 } -func (fake *LocalMembership) IsMeReturns(result1 bool) { +func (fake *LocalMembership) IsMeReturns(result1 bool, result2 error) { fake.isMeMutex.Lock() defer fake.isMeMutex.Unlock() fake.IsMeStub = nil fake.isMeReturns = struct { result1 bool - }{result1} + result2 error + }{result1, result2} } -func (fake *LocalMembership) IsMeReturnsOnCall(i int, result1 bool) { +func (fake *LocalMembership) IsMeReturnsOnCall(i int, result1 bool, result2 error) { fake.isMeMutex.Lock() defer fake.isMeMutex.Unlock() fake.IsMeStub = nil if fake.isMeReturnsOnCall == nil { fake.isMeReturnsOnCall = make(map[int]struct { result1 bool + result2 error }) } fake.isMeReturnsOnCall[i] = struct { result1 bool - }{result1} + result2 error + }{result1, result2} } func (fake *LocalMembership) RegisterIdentity(arg1 context.Context, arg2 driver.IdentityConfiguration) error { diff --git a/token/services/identity/role/role.go b/token/services/identity/role/role.go index c7159b34f0..ea0216b0b7 100644 --- a/token/services/identity/role/role.go +++ b/token/services/identity/role/role.go @@ -26,7 +26,7 @@ import ( //go:generate counterfeiter -o mock/lm.go -fake-name LocalMembership . LocalMembership type LocalMembership interface { DefaultNetworkIdentity() driver.Identity - IsMe(ctx context.Context, id driver.Identity) bool + IsMe(ctx context.Context, id driver.Identity) (bool, error) GetIdentityInfo(ctx context.Context, label string, auditInfo []byte) (idriver.IdentityInfo, error) GetIdentifier(ctx context.Context, id driver.Identity) (string, error) GetDefaultIdentifier() string @@ -170,7 +170,15 @@ func (r *Role) mapStringToID(ctx context.Context, v string) (driver.Identity, st r.logger.DebugfContext(ctx, "passed node identity as view identity") return nil, defaultIdentifier, nil - case r.localMembership.IsMe(ctx, labelAsIdentity): + } + + // IsMe can fail (e.g. a storage error); a failure must not be silently treated as + // "not a local member", so propagate it instead of folding it into the fallthrough. + isMe, err := r.localMembership.IsMe(ctx, labelAsIdentity) + if err != nil { + return nil, "", errors.Wrapf(err, "failed checking if label [%s] is a local member", label) + } + if isMe { r.logger.DebugfContext(ctx, "passed a local member") id := labelAsIdentity if idIdentifier, err := r.localMembership.GetIdentifier(ctx, id); err == nil { @@ -220,7 +228,15 @@ func (r *Role) mapIdentityToID(ctx context.Context, v driver.Identity) (driver.I r.logger.DebugfContext(ctx, "passed identity is the node identity (same bytes)") return nil, defaultIdentifier, nil - case r.localMembership.IsMe(ctx, id): + } + + // IsMe can fail (e.g. a storage error); a failure must not be silently treated as + // "not me", so propagate it instead of falling through to label resolution. + isMe, err := r.localMembership.IsMe(ctx, id) + if err != nil { + return nil, "", errors.Wrapf(err, "failed checking if identity [%s] is me", id) + } + if isMe { r.logger.DebugfContext(ctx, "passed identity is me") if idIdentifier, err := r.localMembership.GetIdentifier(ctx, id); err == nil { return id, idIdentifier, nil diff --git a/token/services/identity/role/role_test.go b/token/services/identity/role/role_test.go index 58acb41da4..045e88aacd 100644 --- a/token/services/identity/role/role_test.go +++ b/token/services/identity/role/role_test.go @@ -97,10 +97,10 @@ func TestRole_MapToIdentity_mapStringToID_branches(t *testing.T) { return "", errors.New("no") } }) - m.IsMeCalls(func(ctx context.Context, id driver.Identity) bool { + m.IsMeCalls(func(ctx context.Context, id driver.Identity) (bool, error) { s := string(id) - return s == "member" || s == "member2" + return s == "member" || s == "member2", nil }) // If GetIdentifier succeeds immediately @@ -160,7 +160,7 @@ func TestRole_MapToIdentity_mapStringToID_branches(t *testing.T) { // fallback: return label as identifier // make IsMe return false for unknown - m.IsMeCalls(func(ctx context.Context, id driver.Identity) bool { return false }) + m.IsMeCalls(func(ctx context.Context, id driver.Identity) (bool, error) { return false, nil }) id, ident, err = r.MapToIdentity(ctx, "unknown") require.NoError(t, err) require.Nil(t, id) @@ -170,10 +170,10 @@ func TestRole_MapToIdentity_mapStringToID_branches(t *testing.T) { func TestRole_MapToIdentity_mapIdentityToID_branches(t *testing.T) { ctx, r, m := setup(t) // Use stubs for IsMe, GetIdentifier and GetIdentityInfo - m.IsMeCalls(func(ctx context.Context, id driver.Identity) bool { + m.IsMeCalls(func(ctx context.Context, id driver.Identity) (bool, error) { s := string(id) - return s == "me" || s == "me2" + return s == "me" || s == "me2", nil }) m.GetIdentifierCalls(func(ctx context.Context, id driver.Identity) (string, error) { s := string(id) diff --git a/token/services/ttx/boolpolicy/spend.go b/token/services/ttx/boolpolicy/spend.go index 2a8192180d..36b9ac620e 100644 --- a/token/services/ttx/boolpolicy/spend.go +++ b/token/services/ttx/boolpolicy/spend.go @@ -150,7 +150,10 @@ func (c *RequestSpendView) Call(context view.Context) (any, error) { if err != nil { return nil, errors.Wrapf(err, "failed getting TMS for [%s]", c.options.TMSID()) } - areMe := tms.SigService().AreMe(context.Context(), c.parties...) + areMe, err := tms.SigService().AreMe(context.Context(), c.parties...) + if err != nil { + return nil, errors.Wrapf(err, "failed checking which parties are me") + } collector := utils.NewAnswersCollector[string, *SpendResponse](len(c.parties), c.timeout) counter := 0 for _, party := range c.parties { diff --git a/token/services/ttx/boolpolicy/spend_test.go b/token/services/ttx/boolpolicy/spend_test.go index eb46d0f8bd..cdf4fdc61d 100644 --- a/token/services/ttx/boolpolicy/spend_test.go +++ b/token/services/ttx/boolpolicy/spend_test.go @@ -71,7 +71,7 @@ func newSpendTestContext(t *testing.T, areMe []string, sessions map[string]view. tms.PublicParamsManagerReturns(ppm) ip := &driver_mock.IdentityProvider{} - ip.AreMeReturns(areMe) + ip.AreMeReturns(areMe, nil) tms.IdentityProviderReturns(ip) tms.DeserializerReturns(&driver_mock.Deserializer{}) diff --git a/token/services/ttx/collectendorsements.go b/token/services/ttx/collectendorsements.go index 65cc899e1e..0ca741a24d 100644 --- a/token/services/ttx/collectendorsements.go +++ b/token/services/ttx/collectendorsements.go @@ -691,7 +691,11 @@ func (c *CollectEndorsementsView) prepareDistributionList(context view.Context, remainingIds = append(remainingIds, id) } } - mine.Add(c.tx.TokenService().SigService().AreMe(context.Context(), remainingIds...)...) + remainingMine, err := c.tx.TokenService().SigService().AreMe(context.Context(), remainingIds...) + if err != nil { + return nil, errors.Wrapf(err, "failed checking which of the remaining [%d] identities are me", len(remainingIds)) + } + mine.Add(remainingMine...) logger.DebugfContext(context.Context(), "%d/%d ids were mine", mine.Length(), len(allIds)) var distributionListCompressed []distributionListEntry diff --git a/token/services/ttx/dep/providers.go b/token/services/ttx/dep/providers.go index 5f71773b4e..7e5dc72ff6 100644 --- a/token/services/ttx/dep/providers.go +++ b/token/services/ttx/dep/providers.go @@ -58,7 +58,8 @@ func GetNetworkProvider(sp token.ServiceProvider) (NetworkProvider, error) { // SignatureService defines the subset of function of the signature service needed by the ttx service. type SignatureService interface { // IsMe checks if the given party is a local party meaning that a signer is bound to that identity. - IsMe(ctx context.Context, party token.Identity) bool + // A non-nil error means ownership could not be determined and the boolean must be ignored. + IsMe(ctx context.Context, party token.Identity) (bool, error) } // TokenManagementService defines the interface of a token management service needed by ttx service. diff --git a/token/services/ttx/endorse.go b/token/services/ttx/endorse.go index f01d7e3261..85c2b9d920 100644 --- a/token/services/ttx/endorse.go +++ b/token/services/ttx/endorse.go @@ -254,7 +254,11 @@ func extractRequiredSigners(ctx context.Context, sigService dep.SignatureService } subset := make([]token.Identity, 0, len(res)) for _, res := range res { - if sigService.IsMe(ctx, res) { + isMe, err := sigService.IsMe(ctx, res) + if err != nil { + return nil, errors.Wrapf(err, "failed checking if signer [%s] is me", res) + } + if isMe { subset = append(subset, res) } } diff --git a/token/services/ttx/endorse_test.go b/token/services/ttx/endorse_test.go index 3239535a32..61f0190156 100644 --- a/token/services/ttx/endorse_test.go +++ b/token/services/ttx/endorse_test.go @@ -70,7 +70,7 @@ func newTestEndorseViewContext(t *testing.T, input *TestEndorseViewContextInput) tms.IDReturns(tmsID) tokenDes := &mock.Deserializer{} tokenIP := &mock.IdentityProvider{} - tokenIP.IsMeReturns(true) + tokenIP.IsMeReturns(true, nil) tokenSigner := &mock.Signer{} tokenSigner.SignReturns([]byte("a_token_sigma"), nil) tokenIP.GetSignerReturns(tokenSigner, nil) diff --git a/token/services/ttx/multisig/spend.go b/token/services/ttx/multisig/spend.go index fbf3ff84d3..045d9d65c7 100644 --- a/token/services/ttx/multisig/spend.go +++ b/token/services/ttx/multisig/spend.go @@ -135,7 +135,10 @@ func (c *RequestSpendView) Call(context view.Context) (any, error) { if err != nil { return nil, errors.Wrapf(err, "failed getting TMS for [%s]", c.options.TMSID()) } - areMe := tms.SigService().AreMe(context.Context(), c.parties...) + areMe, err := tms.SigService().AreMe(context.Context(), c.parties...) + if err != nil { + return nil, errors.Wrapf(err, "failed checking which parties are me") + } for _, party := range c.parties { logger.DebugfContext(context.Context(), "notify party [%s] about request...", party) if slices.Contains(areMe, party.UniqueID()) { diff --git a/token/services/ttx/multisig/spend_test.go b/token/services/ttx/multisig/spend_test.go index c937379749..ea70268750 100644 --- a/token/services/ttx/multisig/spend_test.go +++ b/token/services/ttx/multisig/spend_test.go @@ -71,7 +71,7 @@ func newSpendTestContext(t *testing.T, areMe []string, sessions map[string]view. tms.PublicParamsManagerReturns(ppm) ip := &driver_mock.IdentityProvider{} - ip.AreMeReturns(areMe) + ip.AreMeReturns(areMe, nil) tms.IdentityProviderReturns(ip) tms.DeserializerReturns(&driver_mock.Deserializer{}) diff --git a/token/sig.go b/token/sig.go index 958e441e1b..2d23bd92b9 100644 --- a/token/sig.go +++ b/token/sig.go @@ -64,13 +64,17 @@ func (s *SignatureService) RegisterEphemeralSigner(ctx context.Context, identity return s.identityProvider.RegisterSigner(ctx, identity, signer, verifier, nil, true) } -// AreMe returns the hashes of the passed identities that have a signer registered before -func (s *SignatureService) AreMe(ctx context.Context, identities ...Identity) []string { +// AreMe returns the hashes of the passed identities that have a signer registered before. +// A non-nil error means the ownership check could not be completed; the returned slice is +// then not authoritative and must not be treated as "these and only these are mine". +func (s *SignatureService) AreMe(ctx context.Context, identities ...Identity) ([]string, error) { return s.identityProvider.AreMe(ctx, identities...) } -// IsMe returns true if for the given identity there is a signer registered -func (s *SignatureService) IsMe(ctx context.Context, party Identity) bool { +// IsMe returns true if for the given identity there is a signer registered. +// A non-nil error means ownership could not be determined; the boolean must be ignored in +// that case rather than treated as an authoritative "not mine". +func (s *SignatureService) IsMe(ctx context.Context, party Identity) (bool, error) { return s.identityProvider.IsMe(ctx, party) } diff --git a/token/sig_test.go b/token/sig_test.go index 310cefc332..4d59f41f21 100644 --- a/token/sig_test.go +++ b/token/sig_test.go @@ -125,11 +125,12 @@ func TestSignatureService_IsMe(t *testing.T) { identityProvider: ip, } - ip.IsMeReturns(true) + ip.IsMeReturns(true, nil) id := []byte("identity") - isMe := service.IsMe(t.Context(), id) + isMe, err := service.IsMe(t.Context(), id) + require.NoError(t, err) assert.True(t, isMe) } @@ -181,10 +182,11 @@ func TestSignatureService_AreMe(t *testing.T) { id2 := []byte("identity2") expectedHashes := []string{"hash1", "hash2"} - ip.AreMeReturns(expectedHashes) + ip.AreMeReturns(expectedHashes, nil) - hashes := service.AreMe(t.Context(), id1, id2) + hashes, err := service.AreMe(t.Context(), id1, id2) + require.NoError(t, err) assert.Equal(t, expectedHashes, hashes) assert.Equal(t, 1, ip.AreMeCallCount()) }