From 6401fdff39a05f49611c21b70b03bdd3ee91bb3d Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Wed, 15 Apr 2026 12:32:41 +0200 Subject: [PATCH] Exclude retraction presentations from Discovery Service search Retraction markers are stored on the timeline so Get() can replicate them, but must not surface from Search(). With an empty query, no credential join was applied, so retraction VPs (which have no credentials) were returned to API callers. Fixes #4192 Co-Authored-By: Claude Opus 4.6 (1M context) --- discovery/module_test.go | 21 +++++++++++++++++++++ discovery/store.go | 4 ++++ 2 files changed, 25 insertions(+) diff --git a/discovery/module_test.go b/discovery/module_test.go index 56a0a031cb..bfa83817b8 100644 --- a/discovery/module_test.go +++ b/discovery/module_test.go @@ -483,6 +483,27 @@ func TestModule_Search(t *testing.T) { actualJSON, _ := json.Marshal(results) assert.JSONEq(t, string(expectedJSON), string(actualJSON)) }) + t.Run("retracted presentations are not returned", func(t *testing.T) { + resetStore(t, storageEngine.GetSQLDatabase()) + m, testContext := setupModule(t, storageEngine, func(module *Module) { + module.config.Client.RefreshInterval = 0 + }) + testContext.verifier.EXPECT().VerifyVP(gomock.Any(), true, true, nil).Times(2) + + vpAliceRetract := createPresentationCustom(aliceDID, func(claims map[string]interface{}, vp *vc.VerifiablePresentation) { + vp.Type = append(vp.Type, retractionPresentationType) + claims["retract_jti"] = vpAlice.ID.String() + claims[jwt.AudienceKey] = []string{testServiceID} + }) + + require.NoError(t, m.Register(context.Background(), testServiceID, vpAlice)) + require.NoError(t, m.Register(context.Background(), testServiceID, vpAliceRetract)) + + // Empty query: no credential-join filter, so retraction markers leak through. + results, err := m.Search(testServiceID, map[string]string{}) + require.NoError(t, err) + assert.Empty(t, results, "retraction presentations must not be returned from Search") + }) t.Run("unknown service ID", func(t *testing.T) { m, _ := setupModule(t, storageEngine) _, err := m.Search("unknown", nil) diff --git a/discovery/store.go b/discovery/store.go index 5dfba82986..be65f92113 100644 --- a/discovery/store.go +++ b/discovery/store.go @@ -292,6 +292,10 @@ func (s *sqlStore) search(serviceID string, query map[string]string, allowUnvali if err != nil { return nil, fmt.Errorf("failed to parse presentation '%s': %w", match.PresentationID, err) } + // Retraction markers are stored on the timeline (for Get()) but must not surface in search results. + if presentation.IsType(retractionPresentationType) { + continue + } results = append(results, *presentation) } return results, nil