diff --git a/token/services/identity/idemix/km.go b/token/services/identity/idemix/km.go index 2a996b511a..be6d93dbc5 100644 --- a/token/services/identity/idemix/km.go +++ b/token/services/identity/idemix/km.go @@ -391,31 +391,20 @@ func (p *KeyManager) IdentityType() idriver.IdentityType { return IdentityType } -// DeserializeSigningIdentity deserializes a signing identity from the given raw bytes +// DeserializeSigningIdentity deserializes a signing identity from the given raw bytes. +// p.Deserialize already verifies the identity's ZK association proof against the issuer +// public key, which is sufficient to reject identities from a different issuer. func (p *KeyManager) DeserializeSigningIdentity(ctx context.Context, raw []byte) (tdriver.SigningIdentity, error) { id, err := p.Deserialize(ctx, raw) if err != nil { return nil, err } - si := &crypto.SigningIdentity{ + return &crypto.SigningIdentity{ CSP: p.Csp, Identity: id.Identity, UserKeySKI: p.userKeySKI, NymKeySKI: id.NymPublicKey.SKI(), EnrollmentId: p.conf.Signer.EnrollmentId, - } - - // the only way to verify if this signing identity correspond to this key manager - // is to generate a signature and verify it. - msg := []byte("hello world!!!") - sigma, err := si.Sign(msg) - if err != nil { - return nil, errors.Wrap(err, "failed generating verification signature") - } - if err := si.Verify(msg, sigma); err != nil { - return nil, errors.Wrap(err, "failed verifying verification signature") - } - - return si, nil + }, nil } diff --git a/token/services/identity/idemix/km_test.go b/token/services/identity/idemix/km_test.go index 8b53572eb4..ffcac45076 100644 --- a/token/services/identity/idemix/km_test.go +++ b/token/services/identity/idemix/km_test.go @@ -199,11 +199,11 @@ func testIdentityWithEidRhNymPolicy(t *testing.T, configPath string, curveID mat _, err = keyManager.DeserializeSigner(t.Context(), []byte{0, 1, 2}) require.Error(t, err) assert.Equal(t, 3, tracker.GetCounter) - // deserialize a valid signer + // deserialize a valid signer — no key-store lookups happen in DeserializeSigningIdentity + // now that the ephemeral sign-and-verify liveness check has been removed. signer, err := keyManager.DeserializeSigner(t.Context(), id) require.NoError(t, err) - assert.Equal(t, 5, tracker.GetCounter) // this is due the call to Sign used to test if the signer belong to this key manager - assert.Equal(t, hex.EncodeToString(keyManager.userKeySKI), tracker.GetHistory[4].Key) + assert.Equal(t, 3, tracker.GetCounter) // deserialize an invalid verifier _, err = keyManager.DeserializeVerifier(t.Context(), nil) @@ -216,15 +216,12 @@ func testIdentityWithEidRhNymPolicy(t *testing.T, configPath string, curveID mat verifier, err := keyManager.DeserializeVerifier(t.Context(), id) require.NoError(t, err) - // sign and verify + // sign and verify — Sign fetches NymKey + UserKey (2 gets), Verify uses held Key objects (0 gets). sigma, err := signer.Sign([]byte("hello world!!!")) require.NoError(t, err) require.NoError(t, verifier.Verify([]byte("hello world!!!"), sigma)) - assert.Equal(t, 7, tracker.GetCounter) - assert.Equal(t, tracker.GetHistory[3].Key, tracker.GetHistory[5].Key) - assert.Equal(t, tracker.GetHistory[3].Value, tracker.GetHistory[5].Value) - assert.Equal(t, hex.EncodeToString(keyManager.userKeySKI), tracker.GetHistory[6].Key) - assert.Equal(t, tracker.GetHistory[4].Value, tracker.GetHistory[6].Value) + assert.Equal(t, 5, tracker.GetCounter) + assert.Equal(t, hex.EncodeToString(keyManager.userKeySKI), tracker.GetHistory[4].Key) } func TestIdentityStandard(t *testing.T) { @@ -421,9 +418,11 @@ func testKeyManager_DeserializeSigner(t *testing.T, configPath string, curveID m require.NoError(t, err) require.NoError(t, verifier.Verify(msg, sigma)) - // Try to deserialize id2 with provider for id, it should fail + // DeserializeSigner for a same-issuer identity now succeeds: the issuer-proof check in + // Deserialize passes, and ownership of the nym key is not verified here. Callers that + // need to distinguish locally-owned identities must use the IsMe / signer-cache path. _, err = keyManager.DeserializeSigner(t.Context(), id2) - require.Error(t, err) + require.NoError(t, err) _, err = keyManager.DeserializeVerifier(t.Context(), id2) require.NoError(t, err) @@ -437,6 +436,49 @@ func testKeyManager_DeserializeSigner(t *testing.T, configPath string, curveID m require.NoError(t, verifier.Verify(msg, sigma)) } +// TestDeserialize_RejectsDifferentIssuerIdentity verifies that p.Deserialize (and therefore +// DeserializeSigningIdentity) rejects an identity issued by a different idemix issuer. +// The sameissuer/ testdata directory contains a separate CA with a distinct IssuerPublicKey, +// so identities it issues will fail the ZK association-proof check against the local issuer key. +func TestDeserialize_RejectsDifferentIssuerIdentity(t *testing.T) { + backend, err := kvs2.NewInMemory() + require.NoError(t, err) + keyStore, err := crypto.NewKeyStore(math.FP256BN_AMCL, kvs2.Keystore(backend)) + require.NoError(t, err) + csp, err := crypto.NewBCCSP(keyStore, math.FP256BN_AMCL) + require.NoError(t, err) + + config, err := crypto.NewConfig("./testdata/fp256bn_amcl/idemix") + require.NoError(t, err) + keyManager, err := NewKeyManager(config, types.EidNymRhNym, csp) + require.NoError(t, err) + + // Build a key manager under a genuinely different issuer (separate key store so no shared state). + foreignBackend, err := kvs2.NewInMemory() + require.NoError(t, err) + foreignKeyStore, err := crypto.NewKeyStore(math.FP256BN_AMCL, kvs2.Keystore(foreignBackend)) + require.NoError(t, err) + foreignCSP, err := crypto.NewBCCSP(foreignKeyStore, math.FP256BN_AMCL) + require.NoError(t, err) + foreignConfig, err := crypto.NewConfig("./testdata/fp256bn_amcl/sameissuer/idemix") + require.NoError(t, err) + foreignKM, err := NewKeyManager(foreignConfig, types.EidNymRhNym, foreignCSP) + require.NoError(t, err) + + foreignDesc, err := foreignKM.Identity(t.Context(), nil) + require.NoError(t, err) + + // p.Deserialize verifies the ZK association proof; a different issuer's proof is invalid here. + _, err = keyManager.Deserialize(t.Context(), foreignDesc.Identity) + require.Error(t, err) + assert.Contains(t, err.Error(), "cannot deserialize, invalid identity") + + // DeserializeSigningIdentity must also fail — it delegates to Deserialize first. + _, err = keyManager.DeserializeSigningIdentity(t.Context(), foreignDesc.Identity) + require.Error(t, err) + assert.Contains(t, err.Error(), "cannot deserialize, invalid identity") +} + func TestIdentityFromFabricCA(t *testing.T) { registry := view.NewServiceProvider() @@ -707,24 +749,27 @@ func testKeyManagerErrorPaths(t *testing.T, configPath string, curveID math.Curv _, err = keyManager.Info(context.Background(), []byte("test-id"), []byte{0, 1, 2}) require.Error(t, err) - // Create another valid key manager with a different config - config2, err := crypto.NewConfig(configPath + "2") - require.NoError(t, err) - keyStore2, err := crypto.NewKeyStore(curveID, kvs2.Keystore(backend)) - require.NoError(t, err) - cryptoProvider2, err := crypto.NewBCCSP(keyStore2, curveID) - require.NoError(t, err) - keyManager2, err := NewKeyManager(config2, types.EidNymRhNym, cryptoProvider2) - require.NoError(t, err) - - // create a valid identity descriptor using keyManager2 - // then fail when trying to deserialize it using keyManager - identityDescriptor2, err := keyManager2.Identity(context.Background(), nil) - require.NoError(t, err) - - _, err = keyManager.DeserializeSigningIdentity(context.Background(), identityDescriptor2.Identity) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed verifying verification signature") + // Create a key manager backed by a genuinely different issuer key. + // fp256bn_amcl sameissuer/ testdata uses a distinct IssuerPublicKey from the main idemix/ fixtures. + // For curves without a sameissuer fixture this block is skipped. + foreignConfigPath := filepath.Join(filepath.Dir(configPath), "sameissuer", filepath.Base(configPath)) + if foreignConfig, ferr := crypto.NewConfig(foreignConfigPath); ferr == nil { + foreignStore, ferr := crypto.NewKeyStore(curveID, kvs2.Keystore(backend)) + require.NoError(t, ferr) + foreignCSP, ferr := crypto.NewBCCSP(foreignStore, curveID) + require.NoError(t, ferr) + foreignKM, ferr := NewKeyManager(foreignConfig, types.EidNymRhNym, foreignCSP) + require.NoError(t, ferr) + + foreignDesc, ferr := foreignKM.Identity(context.Background(), nil) + require.NoError(t, ferr) + + // p.Deserialize verifies the ZK association proof against the local issuer public key; + // an identity from a different issuer must be rejected before the signing identity is built. + _, err = keyManager.DeserializeSigningIdentity(context.Background(), foreignDesc.Identity) + require.Error(t, err) + assert.Contains(t, err.Error(), "cannot deserialize, invalid identity") + } } // TestKeyManagerInfoErrorCases tests error cases in Info method diff --git a/token/services/identity/provider.go b/token/services/identity/provider.go index e3a0420e6b..febe4f5490 100644 --- a/token/services/identity/provider.go +++ b/token/services/identity/provider.go @@ -216,6 +216,13 @@ func (p *Provider) RegisterIdentityDescriptor(ctx context.Context, identityDescr if err := p.storage.RegisterIdentityDescriptor(ctx, identityDescriptor, alias); err != nil { return errors.Wrapf(err, "failed to register identity descriptor") } + // StoreSignerInfo marks this identity as "mine" in the DB so that IsMe() returns true + // even after a process restart (GetExistingSignerInfo only sees identities written here). + if identityDescriptor.Signer != nil { + if err := p.storage.StoreSignerInfo(ctx, identityDescriptor.Identity, identityDescriptor.SignerInfo); err != nil { + return errors.Wrapf(err, "failed to store signer info for identity descriptor") + } + } } // update caches diff --git a/token/services/ttx/collect_endorsements_optimization_test.go b/token/services/ttx/collect_endorsements_optimization_test.go new file mode 100644 index 0000000000..4ec3b2870d --- /dev/null +++ b/token/services/ttx/collect_endorsements_optimization_test.go @@ -0,0 +1,169 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package ttx + +import ( + "context" + "testing" + + "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/metrics/disabled" + "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + "github.com/hyperledger-labs/fabric-token-sdk/token" + "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + drivermock "github.com/hyperledger-labs/fabric-token-sdk/token/driver/mock" + tokenmock "github.com/hyperledger-labs/fabric-token-sdk/token/mock" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/identity" + "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx/dep" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace" +) + +// fakeViewContext is a minimal view.Context for unit tests. +// Only the methods actually exercised by CollectEndorsementsView.Call are implemented. +type fakeViewContext struct { + goCtx context.Context + servicesFn func(v interface{}) (interface{}, error) + sessionErr error +} + +func (f *fakeViewContext) Context() context.Context { return f.goCtx } +func (f *fakeViewContext) GetService(v interface{}) (interface{}, error) { + return f.servicesFn(v) +} +func (f *fakeViewContext) GetSession(_ view.View, _ view.Identity, _ ...view.View) (view.Session, error) { + return nil, f.sessionErr +} +func (f *fakeViewContext) GetSessionByID(_ string, _ view.Identity) (view.Session, error) { + return nil, nil +} +func (f *fakeViewContext) ID() string { return "" } +func (f *fakeViewContext) Me() view.Identity { return nil } +func (f *fakeViewContext) IsMe(_ view.Identity) bool { return false } +func (f *fakeViewContext) Initiator() view.View { return nil } +func (f *fakeViewContext) Session() view.Session { return nil } +func (f *fakeViewContext) RunView(_ view.View, _ ...view.RunViewOption) (interface{}, error) { + return nil, nil +} +func (f *fakeViewContext) OnError(_ func()) {} +func (f *fakeViewContext) StartSpanFrom(ctx context.Context, _ string, _ ...trace.SpanStartOption) (context.Context, trace.Span) { + return ctx, trace.SpanFromContext(ctx) +} + +// testTMS wraps token.ManagementService to satisfy dep.TokenManagementServiceWithExtensions +// without importing the wrapper package (which imports ttx, causing a cycle). +type testTMS struct { + *token.ManagementService +} + +func (t *testTMS) SetTokenManagementService(req *token.Request) error { + if req == nil { + return errors.New("request cannot be nil") + } + req.SetTokenService(t.ManagementService) + + return nil +} + +var _ dep.TokenManagementServiceWithExtensions = (*testTMS)(nil) + +// newMockedManagementService creates a token.ManagementService backed by the +// given identity provider and wallet service mocks. +func newMockedManagementService(t *testing.T, tmsID token.TMSID, mockIP *drivermock.IdentityProvider, mockWS *drivermock.WalletService) *token.ManagementService { + t.Helper() + + mockDriverTMS := &drivermock.TokenManagerService{} + mockDriverTMS.IdentityProviderReturns(mockIP) + mockDriverTMS.WalletServiceReturns(mockWS) + + ppm := &drivermock.PublicParamsManager{} + ppm.PublicParametersReturns(&drivermock.PublicParameters{}) + mockDriverTMS.PublicParamsManagerReturns(ppm) + + mockVP := &tokenmock.VaultProvider{} + mockVault := &drivermock.Vault{} + mockVault.QueryEngineReturns(&drivermock.QueryEngine{}) + mockVP.VaultReturns(mockVault, nil) + + ms, err := token.NewManagementService(tmsID, mockDriverTMS, nil, mockVP, nil, nil) + require.NoError(t, err) + + return ms +} + +// TestRequestSignatures_RemoteIdentity_SkipsGetSigner verifies the optimization +// introduced in issue #1226: when SigService.IsMe() returns false for a signer, +// GetSigner is never invoked, avoiding the expensive idemix sign-and-verify +// deserialization that was previously triggered unconditionally. +func TestRequestSignatures_RemoteIdentity_SkipsGetSigner(t *testing.T) { + tmsID := token.TMSID{Network: "network", Channel: "channel", Namespace: "namespace"} + + // Use a properly typed identity so that multisig.Unwrap succeeds (returns ok=false). + remoteParty, err := identity.WrapWithType(driver.X509IdentityType, []byte("remote_party_key")) + require.NoError(t, err) + + // IdentityProvider: IsMe returns false (identity is not ours). + // GetSigner is intentionally not configured; zero-value return would indicate an unintended call. + mockIP := &drivermock.IdentityProvider{} + mockIP.IsMeReturns(false) + + // WalletService: no local wallet exists for the remote signer. + mockWS := &drivermock.WalletService{} + mockWS.OwnerWalletReturns(nil, errors.New("no wallet for remote party")) + + ms := newMockedManagementService(t, tmsID, mockIP, mockWS) + + req := token.NewRequest(nil, "an_anchor") + req.Metadata.Transfers = []*driver.TransferMetadata{ + { + Inputs: []*driver.TransferInputMetadata{ + { + Senders: []*driver.AuditableIdentity{{Identity: remoteParty}}, + }, + }, + }, + } + + tx := &Transaction{ + Payload: &Payload{ + tmsID: tmsID, + TokenRequest: req, + ID: "an_anchor", + }, + TMS: &testTMS{ManagementService: ms}, + } + + cev := NewCollectEndorsementsView(tx, + WithSkipAuditing(), + WithSkipApproval(), + WithSkipDistributeEnv(), + ) + + metrics := NewMetrics(&disabled.Provider{}) + callCount := 0 + ctx := &fakeViewContext{ + goCtx: t.Context(), + servicesFn: func(v interface{}) (interface{}, error) { + callCount++ + if callCount == 1 { + return metrics, nil + } + + return nil, errors.New("unexpected GetService call") + }, + sessionErr: errors.New("no session available"), + } + + _, callErr := cev.Call(ctx) + + require.Error(t, callErr, "Call should fail because remote signing cannot proceed") + assert.Equal(t, 0, mockIP.GetSignerCallCount(), + "GetSigner must not be called when IsMe() returns false for a remote party") + assert.GreaterOrEqual(t, mockIP.IsMeCallCount(), 1, + "IsMe must be called to determine whether the signer is local") +} diff --git a/token/services/ttx/collectendorsements.go b/token/services/ttx/collectendorsements.go index 5284fea2ba..c111fbe31f 100644 --- a/token/services/ttx/collectendorsements.go +++ b/token/services/ttx/collectendorsements.go @@ -254,19 +254,26 @@ func (c *CollectEndorsementsView) requestSignatures(signers []view.Identity, ver continue } - // Case: there is a signer locally bound to the party, use it to generate the signature - if signer, err := c.tx.TokenService().SigService().GetSigner(context.Context(), signerIdentity); err == nil { - logger.DebugfContext(context.Context(), "found signer for party [%s], request local signature", signerIdentity) - sigma, err := c.signLocal(context.Context(), signerIdentity, signer, requestRaw) - if err != nil { - return nil, errors.WithMessagef(err, "failed signing local for party [%s]", signerIdentity) - } - sigmas[signerIdentity.UniqueID()] = sigma + // Case: there is a signer locally bound to the party, use it to generate the signature. + // IsMe() is a cheap cache/DB lookup that avoids the expensive idemix sign-and-verify in + // GetSigner() for identities we do not own. Even when IsMe() returns true, GetSigner() + // may fail for remote wallets whose identity row exists in the DB but whose private key + // is not held locally; in that case fall through to the wallet/remote-party path. + if c.tx.TokenService().SigService().IsMe(context.Context(), signerIdentity) { + if signer, err := c.tx.TokenService().SigService().GetSigner(context.Context(), signerIdentity); err == nil { + logger.DebugfContext(context.Context(), "found signer for party [%s], request local signature", signerIdentity) + sigma, err := c.signLocal(context.Context(), signerIdentity, signer, requestRaw) + if err != nil { + return nil, errors.WithMessagef(err, "failed signing local for party [%s]", signerIdentity) + } + sigmas[signerIdentity.UniqueID()] = sigma - continue - } else { - logger.DebugfContext(context.Context(), "failed to find a signer for party [%s]: [%s]", signerIdentity, err) + continue + } else { + logger.DebugfContext(context.Context(), "IsMe true but GetSigner failed for party [%s]: [%s]", signerIdentity, err) + } } + logger.DebugfContext(context.Context(), "no local signer for party [%s], checking wallet", signerIdentity) // Case: there is a wallet bound to the party but the signer is not local, the signature is generated externally if w, err := c.tx.TokenService().WalletManager().OwnerWallet(context.Context(), signerIdentity); err == nil {