From aa9838bd2526731883784230080cd26144a569f1 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Thu, 27 Apr 2023 17:54:28 +0200 Subject: [PATCH] Chore: Refactor manifest verifier (#67218) --- pkg/api/plugin_resource_test.go | 5 +- pkg/plugins/ifaces.go | 4 + pkg/plugins/manager/loader/loader_test.go | 5 +- .../manager/manager_integration_test.go | 5 +- pkg/plugins/manager/signature/manifest.go | 67 ++-- .../manager/signature/manifest_test.go | 17 +- .../signature/manifestverifier/verifier.go | 286 ------------------ .../signature/statickey/static_retriever.go | 56 ++++ .../backgroundsvcs/background_services.go | 6 +- .../keyretriever/dynamic/dynamic_retriever.go | 234 ++++++++++++++ .../dynamic/dynamic_retriever_test.go} | 32 +- .../keyretriever/retriever.go | 29 ++ .../keyretriever/retriever_test.go | 26 ++ .../pluginsintegration/pluginsintegration.go | 5 + 14 files changed, 416 insertions(+), 361 deletions(-) delete mode 100644 pkg/plugins/manager/signature/manifestverifier/verifier.go create mode 100644 pkg/plugins/manager/signature/statickey/static_retriever.go create mode 100644 pkg/services/pluginsintegration/keyretriever/dynamic/dynamic_retriever.go rename pkg/{plugins/manager/signature/manifestverifier/verifier_test.go => services/pluginsintegration/keyretriever/dynamic/dynamic_retriever_test.go} (75%) create mode 100644 pkg/services/pluginsintegration/keyretriever/retriever.go create mode 100644 pkg/services/pluginsintegration/keyretriever/retriever_test.go diff --git a/pkg/api/plugin_resource_test.go b/pkg/api/plugin_resource_test.go index deb9373d4fa..f9f0f9f741e 100644 --- a/pkg/api/plugin_resource_test.go +++ b/pkg/api/plugin_resource_test.go @@ -14,7 +14,6 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" @@ -26,6 +25,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" + "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/plugins/manager/store" "github.com/grafana/grafana/pkg/plugins/pluginscdn" @@ -36,7 +36,6 @@ import ( "github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest" "github.com/grafana/grafana/pkg/services/pluginsintegration" "github.com/grafana/grafana/pkg/services/pluginsintegration/config" - "github.com/grafana/grafana/pkg/services/pluginsintegration/keystore" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service" @@ -67,7 +66,7 @@ func TestCallResource(t *testing.T) { reg := registry.ProvideService() l := loader.ProvideService(pCfg, fakes.NewFakeLicensingService(), signature.NewUnsignedAuthorizer(pCfg), reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg), fakes.NewFakeRoleRegistry(), - assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(pCfg, keystore.ProvideService(kvstore.NewFakeKVStore()))) + assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(pCfg, statickey.New())) srcs := sources.ProvideService(cfg, pCfg) ps, err := store.ProvideService(reg, srcs, l) require.NoError(t, err) diff --git a/pkg/plugins/ifaces.go b/pkg/plugins/ifaces.go index b146f32bcd5..a75b218a028 100644 --- a/pkg/plugins/ifaces.go +++ b/pkg/plugins/ifaces.go @@ -157,3 +157,7 @@ type KeyStore interface { GetLastUpdated(ctx context.Context) (*time.Time, error) SetLastUpdated(ctx context.Context) error } + +type KeyRetriever interface { + GetPublicKey(ctx context.Context, keyID string) (string, error) +} diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index e96b6291313..ccb93246490 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -11,7 +11,6 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/plugins/config" @@ -21,9 +20,9 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" "github.com/grafana/grafana/pkg/plugins/manager/loader/initializer" "github.com/grafana/grafana/pkg/plugins/manager/signature" + "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/plugins/pluginscdn" - "github.com/grafana/grafana/pkg/services/pluginsintegration/keystore" "github.com/grafana/grafana/pkg/setting" ) @@ -1388,7 +1387,7 @@ func newLoader(cfg *config.Cfg, cbs ...func(loader *Loader)) *Loader { l := New(cfg, &fakes.FakeLicensingService{}, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(), fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakeRoleRegistry(), assetpath.ProvideService(pluginscdn.ProvideService(cfg)), finder.NewLocalFinder(cfg), - signature.ProvideService(cfg, keystore.ProvideService(kvstore.NewFakeKVStore()))) + signature.ProvideService(cfg, statickey.New())) for _, cb := range cbs { cb(l) diff --git a/pkg/plugins/manager/manager_integration_test.go b/pkg/plugins/manager/manager_integration_test.go index 074cbdaa0db..5b2fe031811 100644 --- a/pkg/plugins/manager/manager_integration_test.go +++ b/pkg/plugins/manager/manager_integration_test.go @@ -14,7 +14,6 @@ import ( "gopkg.in/ini.v1" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" @@ -26,13 +25,13 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" + "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/plugins/manager/store" "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/pluginsintegration/config" - "github.com/grafana/grafana/pkg/services/pluginsintegration/keystore" plicensing "github.com/grafana/grafana/pkg/services/pluginsintegration/licensing" "github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/setting" @@ -119,7 +118,7 @@ func TestIntegrationPluginManager(t *testing.T) { lic := plicensing.ProvideLicensing(cfg, &licensing.OSSLicensingService{Cfg: cfg}) l := loader.ProvideService(pCfg, lic, signature.NewUnsignedAuthorizer(pCfg), reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg), fakes.NewFakeRoleRegistry(), - assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(pCfg, keystore.ProvideService(kvstore.NewFakeKVStore()))) + assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(pCfg, statickey.New())) srcs := sources.ProvideService(cfg, pCfg) ps, err := store.ProvideService(reg, srcs, l) require.NoError(t, err) diff --git a/pkg/plugins/manager/signature/manifest.go b/pkg/plugins/manager/signature/manifest.go index 992a9080566..abb22e48155 100644 --- a/pkg/plugins/manager/signature/manifest.go +++ b/pkg/plugins/manager/signature/manifest.go @@ -1,6 +1,7 @@ package signature import ( + "bytes" "context" "crypto/sha256" "encoding/hex" @@ -15,13 +16,14 @@ import ( "runtime" "strings" + "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/clearsign" + "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/gobwas/glob" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/log" - "github.com/grafana/grafana/pkg/plugins/manager/signature/manifestverifier" "github.com/grafana/grafana/pkg/setting" ) @@ -56,28 +58,19 @@ func (m *PluginManifest) isV2() bool { } type Signature struct { - verifier *manifestverifier.ManifestVerifier - mlog log.Logger + log log.Logger + kr plugins.KeyRetriever } var _ plugins.SignatureCalculator = &Signature{} -func ProvideService(cfg *config.Cfg, kv plugins.KeyStore) *Signature { - log := log.New("plugin.signature") +func ProvideService(cfg *config.Cfg, kr plugins.KeyRetriever) *Signature { return &Signature{ - verifier: manifestverifier.New(cfg, log, kv), - mlog: log, + log: log.New("plugin.signature"), + kr: kr, } } -func (s *Signature) IsDisabled() bool { - return s.verifier.IsDisabled() -} - -func (s *Signature) Run(ctx context.Context) error { - return s.verifier.Run(ctx) -} - // readPluginManifest attempts to read and verify the plugin manifest // if any error occurs or the manifest is not valid, this will return an error func (s *Signature) readPluginManifest(ctx context.Context, body []byte) (*PluginManifest, error) { @@ -109,7 +102,7 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu return plugins.Signature{}, fmt.Errorf("files: %w", err) } if len(fsFiles) == 0 { - s.mlog.Warn("No plugin file information in directory", "pluginID", plugin.JSONData.ID) + s.log.Warn("No plugin file information in directory", "pluginID", plugin.JSONData.ID) return plugins.Signature{ Status: plugins.SignatureInvalid, }, nil @@ -118,13 +111,13 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu f, err := plugin.FS.Open("MANIFEST.txt") if err != nil { if errors.Is(err, plugins.ErrFileNotExist) { - s.mlog.Debug("Could not find a MANIFEST.txt", "id", plugin.JSONData.ID, "err", err) + s.log.Debug("Could not find a MANIFEST.txt", "id", plugin.JSONData.ID, "err", err) return plugins.Signature{ Status: plugins.SignatureUnsigned, }, nil } - s.mlog.Debug("Could not open MANIFEST.txt", "id", plugin.JSONData.ID, "err", err) + s.log.Debug("Could not open MANIFEST.txt", "id", plugin.JSONData.ID, "err", err) return plugins.Signature{ Status: plugins.SignatureInvalid, }, nil @@ -134,13 +127,13 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu return } if err = f.Close(); err != nil { - s.mlog.Warn("Failed to close plugin MANIFEST file", "err", err) + s.log.Warn("Failed to close plugin MANIFEST file", "err", err) } }() byteValue, err := io.ReadAll(f) if err != nil || len(byteValue) < 10 { - s.mlog.Debug("MANIFEST.TXT is invalid", "id", plugin.JSONData.ID) + s.log.Debug("MANIFEST.TXT is invalid", "id", plugin.JSONData.ID) return plugins.Signature{ Status: plugins.SignatureUnsigned, }, nil @@ -148,7 +141,7 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu manifest, err := s.readPluginManifest(ctx, byteValue) if err != nil { - s.mlog.Debug("Plugin signature invalid", "id", plugin.JSONData.ID, "err", err) + s.log.Debug("Plugin signature invalid", "id", plugin.JSONData.ID, "err", err) return plugins.Signature{ Status: plugins.SignatureInvalid, }, nil @@ -170,10 +163,10 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu // Validate that plugin is running within defined root URLs if len(manifest.RootURLs) > 0 { if match, err := urlMatch(manifest.RootURLs, setting.AppUrl, manifest.SignatureType); err != nil { - s.mlog.Warn("Could not verify if root URLs match", "plugin", plugin.JSONData.ID, "rootUrls", manifest.RootURLs) + s.log.Warn("Could not verify if root URLs match", "plugin", plugin.JSONData.ID, "rootUrls", manifest.RootURLs) return plugins.Signature{}, err } else if !match { - s.mlog.Warn("Could not find root URL that matches running application URL", "plugin", plugin.JSONData.ID, + s.log.Warn("Could not find root URL that matches running application URL", "plugin", plugin.JSONData.ID, "appUrl", setting.AppUrl, "rootUrls", manifest.RootURLs) return plugins.Signature{ Status: plugins.SignatureInvalid, @@ -185,7 +178,7 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu // Verify the manifest contents for p, hash := range manifest.Files { - err = verifyHash(s.mlog, plugin, p, hash) + err = verifyHash(s.log, plugin, p, hash) if err != nil { return plugins.Signature{ Status: plugins.SignatureModified, @@ -215,13 +208,13 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu } if len(unsignedFiles) > 0 { - s.mlog.Warn("The following files were not included in the signature", "plugin", plugin.JSONData.ID, "files", unsignedFiles) + s.log.Warn("The following files were not included in the signature", "plugin", plugin.JSONData.ID, "files", unsignedFiles) return plugins.Signature{ Status: plugins.SignatureModified, }, nil } - s.mlog.Debug("Plugin signature valid", "id", plugin.JSONData.ID) + s.log.Debug("Plugin signature valid", "id", plugin.JSONData.ID) return plugins.Signature{ Status: plugins.SignatureValid, Type: manifest.SignatureType, @@ -331,5 +324,25 @@ func (s *Signature) validateManifest(ctx context.Context, m PluginManifest, bloc } } - return s.verifier.Verify(ctx, m.KeyID, block) + return s.Verify(ctx, m.KeyID, block) +} + +func (s *Signature) Verify(ctx context.Context, keyID string, block *clearsign.Block) error { + publicKey, err := s.kr.GetPublicKey(ctx, keyID) + if err != nil { + return err + } + + keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(publicKey)) + if err != nil { + return fmt.Errorf("%v: %w", "failed to parse public key", err) + } + + if _, err = openpgp.CheckDetachedSignature(keyring, + bytes.NewBuffer(block.Bytes), + block.ArmoredSignature.Body, &packet.Config{}); err != nil { + return fmt.Errorf("%v: %w", "failed to check signature", err) + } + + return nil } diff --git a/pkg/plugins/manager/signature/manifest_test.go b/pkg/plugins/manager/signature/manifest_test.go index 069f7495136..2816a8a1b13 100644 --- a/pkg/plugins/manager/signature/manifest_test.go +++ b/pkg/plugins/manager/signature/manifest_test.go @@ -11,11 +11,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/manager/fakes" - "github.com/grafana/grafana/pkg/services/pluginsintegration/keystore" + "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "github.com/grafana/grafana/pkg/setting" ) @@ -52,7 +51,7 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX -----END PGP SIGNATURE-----` t.Run("valid manifest", func(t *testing.T) { - s := ProvideService(&config.Cfg{}, keystore.ProvideService(kvstore.NewFakeKVStore())) + s := ProvideService(&config.Cfg{}, statickey.New()) manifest, err := s.readPluginManifest(context.Background(), []byte(txt)) require.NoError(t, err) @@ -69,7 +68,7 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX t.Run("invalid manifest", func(t *testing.T) { modified := strings.ReplaceAll(txt, "README.md", "xxxxxxxxxx") - s := ProvideService(&config.Cfg{}, keystore.ProvideService(kvstore.NewFakeKVStore())) + s := ProvideService(&config.Cfg{}, statickey.New()) _, err := s.readPluginManifest(context.Background(), []byte(modified)) require.Error(t, err) }) @@ -107,7 +106,7 @@ khdr/tZ1PDgRxMqB/u+Vtbpl0xSxgblnrDOYMSI= -----END PGP SIGNATURE-----` t.Run("valid manifest", func(t *testing.T) { - s := ProvideService(&config.Cfg{}, keystore.ProvideService(kvstore.NewFakeKVStore())) + s := ProvideService(&config.Cfg{}, statickey.New()) manifest, err := s.readPluginManifest(context.Background(), []byte(txt)) require.NoError(t, err) @@ -161,7 +160,7 @@ func TestCalculate(t *testing.T) { setting.AppUrl = tc.appURL basePath := filepath.Join(parentDir, "testdata/non-pvt-with-root-url/plugin") - s := ProvideService(&config.Cfg{}, keystore.ProvideService(kvstore.NewFakeKVStore())) + s := ProvideService(&config.Cfg{}, statickey.New()) sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { return plugins.External @@ -189,7 +188,7 @@ func TestCalculate(t *testing.T) { basePath := "../testdata/renderer-added-file/plugin" runningWindows = true - s := ProvideService(&config.Cfg{}, keystore.ProvideService(kvstore.NewFakeKVStore())) + s := ProvideService(&config.Cfg{}, statickey.New()) sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{ PluginClassFunc: func(ctx context.Context) plugins.Class { return plugins.External @@ -253,7 +252,7 @@ func TestCalculate(t *testing.T) { toSlash = tc.platform.toSlashFunc() fromSlash = tc.platform.fromSlashFunc() - s := ProvideService(&config.Cfg{}, keystore.ProvideService(kvstore.NewFakeKVStore())) + s := ProvideService(&config.Cfg{}, statickey.New()) pfs, err := tc.fsFactory() require.NoError(t, err) pfs, err = newPathSeparatorOverrideFS(string(tc.platform.separator), pfs) @@ -721,7 +720,7 @@ func Test_validateManifest(t *testing.T) { } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - s := ProvideService(&config.Cfg{}, keystore.ProvideService(kvstore.NewFakeKVStore())) + s := ProvideService(&config.Cfg{}, statickey.New()) err := s.validateManifest(context.Background(), *tc.manifest, nil) require.Errorf(t, err, tc.expectedErr) }) diff --git a/pkg/plugins/manager/signature/manifestverifier/verifier.go b/pkg/plugins/manager/signature/manifestverifier/verifier.go deleted file mode 100644 index 4424b8a8e42..00000000000 --- a/pkg/plugins/manager/signature/manifestverifier/verifier.go +++ /dev/null @@ -1,286 +0,0 @@ -package manifestverifier - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "net" - "net/http" - "net/url" - "sync" - "time" - - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/ProtonMail/go-crypto/openpgp/clearsign" - "github.com/ProtonMail/go-crypto/openpgp/packet" - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/config" - "github.com/grafana/grafana/pkg/plugins/log" - - // Only used for getting the feature flag value - "github.com/grafana/grafana/pkg/services/featuremgmt" -) - -const publicKeySyncInterval = 10 * 24 * time.Hour // 10 days - -// ManifestKeys is the database representation of public keys -// used to verify plugin manifests. -type ManifestKeys struct { - KeyID string `json:"keyId"` - PublicKey string `json:"public"` - Since int64 `json:"since"` -} - -type ManifestVerifier struct { - cfg *config.Cfg - mlog log.Logger - - lock sync.Mutex - cli http.Client - kv plugins.KeyStore - hasKeys bool -} - -func New(cfg *config.Cfg, mlog log.Logger, kv plugins.KeyStore) *ManifestVerifier { - pmv := &ManifestVerifier{ - cfg: cfg, - mlog: mlog, - cli: makeHttpClient(), - kv: kv, - } - return pmv -} - -// IsDisabled disables dynamic retrieval of public keys from the API server. -func (pmv *ManifestVerifier) IsDisabled() bool { - return pmv.cfg == nil || pmv.cfg.Features == nil || !pmv.cfg.Features.IsEnabled(featuremgmt.FlagPluginsAPIManifestKey) -} - -func (pmv *ManifestVerifier) Run(ctx context.Context) error { - // do an initial update if necessary - err := pmv.updateKeys(ctx) - if err != nil { - pmv.mlog.Error("Error downloading plugin manifest keys", "error", err) - } - - // calculate initial send delay - lastUpdated, err := pmv.kv.GetLastUpdated(ctx) - if err != nil { - return err - } - nextSendInterval := time.Until(lastUpdated.Add(publicKeySyncInterval)) - if nextSendInterval < time.Minute { - nextSendInterval = time.Minute - } - - downloadKeysTicker := time.NewTicker(nextSendInterval) - defer downloadKeysTicker.Stop() - - select { - case <-downloadKeysTicker.C: - err = pmv.updateKeys(ctx) - if err != nil { - pmv.mlog.Error("Error downloading plugin manifest keys", "error", err) - } - - if nextSendInterval != publicKeySyncInterval { - nextSendInterval = publicKeySyncInterval - downloadKeysTicker.Reset(nextSendInterval) - } - case <-ctx.Done(): - return ctx.Err() - } - - return ctx.Err() -} - -func (pmv *ManifestVerifier) updateKeys(ctx context.Context) error { - pmv.lock.Lock() - defer pmv.lock.Unlock() - - lastUpdated, err := pmv.kv.GetLastUpdated(ctx) - if err != nil { - return err - } - if time.Since(*lastUpdated) < publicKeySyncInterval { - // Cache is still valid - return nil - } - - return pmv.downloadKeys(ctx) -} - -const publicKeyID = "7e4d0c6a708866e7" -const publicKeyText = `-----BEGIN PGP PUBLIC KEY BLOCK----- -Version: OpenPGP.js v4.10.1 -Comment: https://openpgpjs.org - -xpMEXpTXXxMFK4EEACMEIwQBiOUQhvGbDLvndE0fEXaR0908wXzPGFpf0P0Z -HJ06tsq+0higIYHp7WTNJVEZtcwoYLcPRGaa9OQqbUU63BEyZdgAkPTz3RFd -5+TkDWZizDcaVFhzbDd500yTwexrpIrdInwC/jrgs7Zy/15h8KA59XXUkdmT -YB6TR+OA9RKME+dCJozNGUdyYWZhbmEgPGVuZ0BncmFmYW5hLmNvbT7CvAQQ -EwoAIAUCXpTXXwYLCQcIAwIEFQgKAgQWAgEAAhkBAhsDAh4BAAoJEH5NDGpw -iGbnaWoCCQGQ3SQnCkRWrG6XrMkXOKfDTX2ow9fuoErN46BeKmLM4f1EkDZQ -Tpq3SE8+My8B5BIH3SOcBeKzi3S57JHGBdFA+wIJAYWMrJNIvw8GeXne+oUo -NzzACdvfqXAZEp/HFMQhCKfEoWGJE8d2YmwY2+3GufVRTI5lQnZOHLE8L/Vc -1S5MXESjzpcEXpTXXxIFK4EEACMEIwQBtHX/SD5Qm3v4V92qpaIZQgtTX0sT -cFPjYWAHqsQ1iENrYN/vg1wU3ADlYATvydOQYvkTyT/tbDvx2Fse8PL84MQA -YKKQ6AJ3gLVvmeouZdU03YoV4MYaT8KbnJUkZQZkqdz2riOlySNI9CG3oYmv -omjUAtzgAgnCcurfGLZkkMxlmY8DAQoJwqQEGBMKAAkFAl6U118CGwwACgkQ -fk0ManCIZuc0jAIJAVw2xdLr4ZQqPUhubrUyFcqlWoW8dQoQagwO8s8ubmby -KuLA9FWJkfuuRQr+O9gHkDVCez3aism7zmJBqIOi38aNAgjJ3bo6leSS2jR/ -x5NqiKVi83tiXDPncDQYPymOnMhW0l7CVA7wj75HrFvvlRI/4MArlbsZ2tBn -N1c5v9v/4h6qeA== -=DNbR ------END PGP PUBLIC KEY BLOCK----- -` - -// Retrieve the key from the API and store it in the database -func (pmv *ManifestVerifier) downloadKeys(ctx context.Context) error { - var data struct { - Items []ManifestKeys - } - - url, err := url.JoinPath(pmv.cfg.GrafanaComURL, "/api/plugins/ci/keys") // nolint:gosec URL is provided by config - if err != nil { - return err - } - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - return err - } - - resp, err := pmv.cli.Do(req) - if err != nil { - return err - } - defer func() { - err := resp.Body.Close() - if err != nil { - pmv.mlog.Warn("error closing response body", "error", err) - } - }() - - if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { - return err - } - - if len(data.Items) == 0 { - return errors.New("missing public key") - } - - cachedKeys, err := pmv.kv.ListKeys(ctx) - if err != nil { - return err - } - - shouldKeep := make(map[string]bool) - for _, key := range data.Items { - err = pmv.kv.Set(ctx, key.KeyID, key.PublicKey) - if err != nil { - return err - } - shouldKeep[key.KeyID] = true - } - - // Delete keys that are no longer in the API - for _, key := range cachedKeys { - if !shouldKeep[key] { - err = pmv.kv.Del(ctx, key) - if err != nil { - return err - } - } - } - - // Update the last updated timestamp - return pmv.kv.SetLastUpdated(ctx) -} - -func (pmv *ManifestVerifier) ensureKeys(ctx context.Context) error { - if pmv.hasKeys { - return nil - } - keys, err := pmv.kv.ListKeys(ctx) - if err != nil { - return err - } - if len(keys) == 0 { - // Populate with the default key - err := pmv.kv.Set(ctx, publicKeyID, publicKeyText) - if err != nil { - return err - } - } - pmv.hasKeys = true - return nil -} - -// getPublicKey loads public keys from: -// - The hard-coded value if the feature flag is not enabled. -// - A cached value from kv storage if it has been already retrieved. This cache is populated from the grafana.com API. -func (pmv *ManifestVerifier) getPublicKey(ctx context.Context, keyID string) (string, error) { - if pmv.IsDisabled() { - return publicKeyText, nil - } - - pmv.lock.Lock() - defer pmv.lock.Unlock() - - err := pmv.ensureKeys(ctx) - if err != nil { - return "", err - } - - key, exist, err := pmv.kv.Get(ctx, keyID) - if err != nil { - return "", err - } - if exist { - return key, nil - } - - return "", fmt.Errorf("missing public key for %s", keyID) -} - -func (pmv *ManifestVerifier) Verify(ctx context.Context, keyID string, block *clearsign.Block) error { - publicKey, err := pmv.getPublicKey(ctx, keyID) - if err != nil { - return err - } - - keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(publicKey)) - if err != nil { - return fmt.Errorf("%v: %w", "failed to parse public key", err) - } - - if _, err = openpgp.CheckDetachedSignature(keyring, - bytes.NewBuffer(block.Bytes), - block.ArmoredSignature.Body, &packet.Config{}); err != nil { - return fmt.Errorf("%v: %w", "failed to check signature", err) - } - - return nil -} - -// Same configuration as pkg/plugins/repo/client.go -func makeHttpClient() http.Client { - tr := &http.Transport{ - Proxy: http.ProxyFromEnvironment, - DialContext: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - }).DialContext, - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - } - - return http.Client{ - Timeout: 10 * time.Second, - Transport: tr, - } -} diff --git a/pkg/plugins/manager/signature/statickey/static_retriever.go b/pkg/plugins/manager/signature/statickey/static_retriever.go new file mode 100644 index 00000000000..d73a7aaf85d --- /dev/null +++ b/pkg/plugins/manager/signature/statickey/static_retriever.go @@ -0,0 +1,56 @@ +package statickey + +import ( + "context" + "fmt" + + "github.com/grafana/grafana/pkg/plugins" +) + +const publicKeyID = "7e4d0c6a708866e7" +const publicKeyText = `-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: OpenPGP.js v4.10.1 +Comment: https://openpgpjs.org + +xpMEXpTXXxMFK4EEACMEIwQBiOUQhvGbDLvndE0fEXaR0908wXzPGFpf0P0Z +HJ06tsq+0higIYHp7WTNJVEZtcwoYLcPRGaa9OQqbUU63BEyZdgAkPTz3RFd +5+TkDWZizDcaVFhzbDd500yTwexrpIrdInwC/jrgs7Zy/15h8KA59XXUkdmT +YB6TR+OA9RKME+dCJozNGUdyYWZhbmEgPGVuZ0BncmFmYW5hLmNvbT7CvAQQ +EwoAIAUCXpTXXwYLCQcIAwIEFQgKAgQWAgEAAhkBAhsDAh4BAAoJEH5NDGpw +iGbnaWoCCQGQ3SQnCkRWrG6XrMkXOKfDTX2ow9fuoErN46BeKmLM4f1EkDZQ +Tpq3SE8+My8B5BIH3SOcBeKzi3S57JHGBdFA+wIJAYWMrJNIvw8GeXne+oUo +NzzACdvfqXAZEp/HFMQhCKfEoWGJE8d2YmwY2+3GufVRTI5lQnZOHLE8L/Vc +1S5MXESjzpcEXpTXXxIFK4EEACMEIwQBtHX/SD5Qm3v4V92qpaIZQgtTX0sT +cFPjYWAHqsQ1iENrYN/vg1wU3ADlYATvydOQYvkTyT/tbDvx2Fse8PL84MQA +YKKQ6AJ3gLVvmeouZdU03YoV4MYaT8KbnJUkZQZkqdz2riOlySNI9CG3oYmv +omjUAtzgAgnCcurfGLZkkMxlmY8DAQoJwqQEGBMKAAkFAl6U118CGwwACgkQ +fk0ManCIZuc0jAIJAVw2xdLr4ZQqPUhubrUyFcqlWoW8dQoQagwO8s8ubmby +KuLA9FWJkfuuRQr+O9gHkDVCez3aism7zmJBqIOi38aNAgjJ3bo6leSS2jR/ +x5NqiKVi83tiXDPncDQYPymOnMhW0l7CVA7wj75HrFvvlRI/4MArlbsZ2tBn +N1c5v9v/4h6qeA== +=DNbR +-----END PGP PUBLIC KEY BLOCK----- +` + +type KeyRetriever struct{} + +var _ plugins.KeyRetriever = (*KeyRetriever)(nil) + +func New() *KeyRetriever { + return &KeyRetriever{} +} + +func (kr *KeyRetriever) GetPublicKey(ctx context.Context, keyID string) (string, error) { + if keyID == publicKeyID { + return publicKeyText, nil + } + return "", fmt.Errorf("missing public key for %s", keyID) +} + +func GetDefaultKey() string { + return publicKeyText +} + +func GetDefaultKeyID() string { + return publicKeyID +} diff --git a/pkg/server/backgroundsvcs/background_services.go b/pkg/server/backgroundsvcs/background_services.go index 1900d9e455f..680eb8a31ba 100644 --- a/pkg/server/backgroundsvcs/background_services.go +++ b/pkg/server/backgroundsvcs/background_services.go @@ -8,7 +8,6 @@ import ( uss "github.com/grafana/grafana/pkg/infra/usagestats/service" "github.com/grafana/grafana/pkg/infra/usagestats/statscollector" "github.com/grafana/grafana/pkg/plugins/manager/process" - "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/auth" @@ -24,6 +23,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert" "github.com/grafana/grafana/pkg/services/notifications" plugindashboardsservice "github.com/grafana/grafana/pkg/services/plugindashboards/service" + "github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever/dynamic" "github.com/grafana/grafana/pkg/services/provisioning" publicdashboardsmetric "github.com/grafana/grafana/pkg/services/publicdashboards/metric" "github.com/grafana/grafana/pkg/services/rendering" @@ -51,7 +51,7 @@ func ProvideBackgroundServiceRegistry( grpcServerProvider grpcserver.Provider, secretMigrationProvider secretsMigrations.SecretMigrationProvider, loginAttemptService *loginattemptimpl.Service, bundleService *supportbundlesimpl.Service, publicDashboardsMetric *publicdashboardsmetric.Service, - signature *signature.Signature, + keyRetriever *dynamic.KeyRetriever, // Need to make sure these are initialized, is there a better place to put them? _ dashboardsnapshots.Service, _ *alerting.AlertNotificationService, _ serviceaccounts.Service, _ *guardian.Provider, @@ -88,7 +88,7 @@ func ProvideBackgroundServiceRegistry( loginAttemptService, bundleService, publicDashboardsMetric, - signature, + keyRetriever, ) } diff --git a/pkg/services/pluginsintegration/keyretriever/dynamic/dynamic_retriever.go b/pkg/services/pluginsintegration/keyretriever/dynamic/dynamic_retriever.go new file mode 100644 index 00000000000..4459de59c40 --- /dev/null +++ b/pkg/services/pluginsintegration/keyretriever/dynamic/dynamic_retriever.go @@ -0,0 +1,234 @@ +package dynamic + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net" + "net/http" + "net/url" + "sync" + "time" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" + "github.com/grafana/grafana/pkg/services/featuremgmt" +) + +const publicKeySyncInterval = 10 * 24 * time.Hour // 10 days + +// ManifestKeys is the database representation of public keys +// used to verify plugin manifests. +type ManifestKeys struct { + KeyID string `json:"keyId"` + PublicKey string `json:"public"` + Since int64 `json:"since"` +} + +type KeyRetriever struct { + cfg *config.Cfg + log log.Logger + + lock sync.Mutex + cli http.Client + kv plugins.KeyStore + hasKeys bool +} + +var _ plugins.KeyRetriever = (*KeyRetriever)(nil) + +func ProvideService(cfg *config.Cfg, kv plugins.KeyStore) *KeyRetriever { + kr := &KeyRetriever{ + cfg: cfg, + log: log.New("plugin.signature.key_retriever"), + cli: makeHttpClient(), + kv: kv, + } + return kr +} + +// IsDisabled disables dynamic retrieval of public keys from the API server. +func (kr *KeyRetriever) IsDisabled() bool { + return !kr.cfg.Features.IsEnabled(featuremgmt.FlagPluginsAPIManifestKey) +} + +func (kr *KeyRetriever) Run(ctx context.Context) error { + // do an initial update if necessary + err := kr.updateKeys(ctx) + if err != nil { + kr.log.Error("Error downloading plugin manifest keys", "error", err) + } + + // calculate initial send delay + lastUpdated, err := kr.kv.GetLastUpdated(ctx) + if err != nil { + return err + } + nextSendInterval := time.Until(lastUpdated.Add(publicKeySyncInterval)) + if nextSendInterval < time.Minute { + nextSendInterval = time.Minute + } + + downloadKeysTicker := time.NewTicker(nextSendInterval) + defer downloadKeysTicker.Stop() + + select { + case <-downloadKeysTicker.C: + err = kr.updateKeys(ctx) + if err != nil { + kr.log.Error("Error downloading plugin manifest keys", "error", err) + } + + if nextSendInterval != publicKeySyncInterval { + nextSendInterval = publicKeySyncInterval + downloadKeysTicker.Reset(nextSendInterval) + } + case <-ctx.Done(): + return ctx.Err() + } + + return ctx.Err() +} + +func (kr *KeyRetriever) updateKeys(ctx context.Context) error { + kr.lock.Lock() + defer kr.lock.Unlock() + + lastUpdated, err := kr.kv.GetLastUpdated(ctx) + if err != nil { + return err + } + if time.Since(*lastUpdated) < publicKeySyncInterval { + // Cache is still valid + return nil + } + + return kr.downloadKeys(ctx) +} + +// Retrieve the key from the API and store it in the database +func (kr *KeyRetriever) downloadKeys(ctx context.Context) error { + var data struct { + Items []ManifestKeys + } + + url, err := url.JoinPath(kr.cfg.GrafanaComURL, "/api/plugins/ci/keys") // nolint:gosec URL is provided by config + if err != nil { + return err + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return err + } + + resp, err := kr.cli.Do(req) + if err != nil { + return err + } + defer func() { + err := resp.Body.Close() + if err != nil { + kr.log.Warn("error closing response body", "error", err) + } + }() + + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return err + } + + if len(data.Items) == 0 { + return errors.New("missing public key") + } + + cachedKeys, err := kr.kv.ListKeys(ctx) + if err != nil { + return err + } + + shouldKeep := make(map[string]bool) + for _, key := range data.Items { + err = kr.kv.Set(ctx, key.KeyID, key.PublicKey) + if err != nil { + return err + } + shouldKeep[key.KeyID] = true + } + + // Delete keys that are no longer in the API + for _, key := range cachedKeys { + if !shouldKeep[key] { + err = kr.kv.Del(ctx, key) + if err != nil { + return err + } + } + } + + // Update the last updated timestamp + return kr.kv.SetLastUpdated(ctx) +} + +func (kr *KeyRetriever) ensureKeys(ctx context.Context) error { + if kr.hasKeys { + return nil + } + keys, err := kr.kv.ListKeys(ctx) + if err != nil { + return err + } + if len(keys) == 0 { + // Populate with the default key + err := kr.kv.Set(ctx, statickey.GetDefaultKeyID(), statickey.GetDefaultKey()) + if err != nil { + return err + } + } + kr.hasKeys = true + return nil +} + +// GetPublicKey loads public keys from: +// - The hard-coded value if the feature flag is not enabled. +// - A cached value from kv storage if it has been already retrieved. This cache is populated from the grafana.com API. +func (kr *KeyRetriever) GetPublicKey(ctx context.Context, keyID string) (string, error) { + kr.lock.Lock() + defer kr.lock.Unlock() + + err := kr.ensureKeys(ctx) + if err != nil { + return "", err + } + + key, exist, err := kr.kv.Get(ctx, keyID) + if err != nil { + return "", err + } + if exist { + return key, nil + } + + return "", fmt.Errorf("missing public key for %s", keyID) +} + +// Same configuration as pkg/plugins/repo/client.go +func makeHttpClient() http.Client { + tr := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } + + return http.Client{ + Timeout: 10 * time.Second, + Transport: tr, + } +} diff --git a/pkg/plugins/manager/signature/manifestverifier/verifier_test.go b/pkg/services/pluginsintegration/keyretriever/dynamic/dynamic_retriever_test.go similarity index 75% rename from pkg/plugins/manager/signature/manifestverifier/verifier_test.go rename to pkg/services/pluginsintegration/keyretriever/dynamic/dynamic_retriever_test.go index 2d5ed092d22..acd46e7db69 100644 --- a/pkg/plugins/manager/signature/manifestverifier/verifier_test.go +++ b/pkg/services/pluginsintegration/keyretriever/dynamic/dynamic_retriever_test.go @@ -1,42 +1,20 @@ -package manifestverifier +package dynamic import ( "context" "encoding/json" "net/http" "net/http/httptest" - "os" "testing" "time" - "github.com/ProtonMail/go-crypto/openpgp/clearsign" "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/plugins/config" - "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/pluginsintegration/keystore" "github.com/stretchr/testify/require" ) -func Test_Verify(t *testing.T) { - t.Run("it should verify a manifest with the default key", func(t *testing.T) { - v := New(&config.Cfg{}, log.New("test"), keystore.ProvideService(kvstore.NewFakeKVStore())) - - body, err := os.ReadFile("../../testdata/test-app/MANIFEST.txt") - if err != nil { - t.Fatal(err) - } - - block, _ := clearsign.Decode(body) - if block == nil { - t.Fatal("failed to decode") - } - - err = v.Verify(context.Background(), "7e4d0c6a708866e7", block) - require.NoError(t, err) - }) -} - func setFakeAPIServer(t *testing.T, publicKey string, keyID string) (*httptest.Server, chan bool) { done := make(chan bool) return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -64,14 +42,14 @@ func setFakeAPIServer(t *testing.T, publicKey string, keyID string) (*httptest.S })), done } func Test_PublicKeyUpdate(t *testing.T) { - t.Run("it should verify a manifest with the API key", func(t *testing.T) { + t.Run("it should retrieve an API key", func(t *testing.T) { cfg := &config.Cfg{ Features: featuremgmt.WithFeatures([]interface{}{featuremgmt.FlagPluginsAPIManifestKey}...), } expectedKey := "fake" s, done := setFakeAPIServer(t, expectedKey, "7e4d0c6a708866e7") cfg.GrafanaComURL = s.URL - v := New(cfg, log.New("test"), keystore.ProvideService(kvstore.NewFakeKVStore())) + v := ProvideService(cfg, keystore.ProvideService(kvstore.NewFakeKVStore())) go func() { err := v.Run(context.Background()) require.NoError(t, err) @@ -94,7 +72,7 @@ func Test_PublicKeyUpdate(t *testing.T) { expectedKey := "fake" s, done := setFakeAPIServer(t, expectedKey, "7e4d0c6a708866e7") cfg.GrafanaComURL = s.URL - v := New(cfg, log.New("test"), keystore.ProvideService(kvstore.NewFakeKVStore())) + v := ProvideService(cfg, keystore.ProvideService(kvstore.NewFakeKVStore())) go func() { err := v.Run(context.Background()) require.NoError(t, err) @@ -116,7 +94,7 @@ func Test_PublicKeyUpdate(t *testing.T) { expectedKey := "fake" s, done := setFakeAPIServer(t, expectedKey, "other") cfg.GrafanaComURL = s.URL - v := New(cfg, log.New("test"), keystore.ProvideService(kvstore.NewFakeKVStore())) + v := ProvideService(cfg, keystore.ProvideService(kvstore.NewFakeKVStore())) go func() { err := v.Run(context.Background()) require.NoError(t, err) diff --git a/pkg/services/pluginsintegration/keyretriever/retriever.go b/pkg/services/pluginsintegration/keyretriever/retriever.go new file mode 100644 index 00000000000..b22ef5d0948 --- /dev/null +++ b/pkg/services/pluginsintegration/keyretriever/retriever.go @@ -0,0 +1,29 @@ +package keyretriever + +import ( + "context" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" + "github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever/dynamic" +) + +var _ plugins.KeyRetriever = (*Service)(nil) + +type Service struct { + kr plugins.KeyRetriever +} + +func ProvideService(dkr *dynamic.KeyRetriever) *Service { + s := &Service{} + if !dkr.IsDisabled() { + s.kr = dkr + } else { + s.kr = statickey.New() + } + return s +} + +func (kr *Service) GetPublicKey(ctx context.Context, keyID string) (string, error) { + return kr.kr.GetPublicKey(ctx, keyID) +} diff --git a/pkg/services/pluginsintegration/keyretriever/retriever_test.go b/pkg/services/pluginsintegration/keyretriever/retriever_test.go new file mode 100644 index 00000000000..5a35674d81a --- /dev/null +++ b/pkg/services/pluginsintegration/keyretriever/retriever_test.go @@ -0,0 +1,26 @@ +package keyretriever + +import ( + "context" + "testing" + + "github.com/grafana/grafana/pkg/infra/kvstore" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever/dynamic" + "github.com/grafana/grafana/pkg/services/pluginsintegration/keystore" + "github.com/stretchr/testify/require" +) + +func Test_GetPublicKey(t *testing.T) { + t.Run("it should return a static key", func(t *testing.T) { + cfg := &config.Cfg{ + Features: featuremgmt.WithFeatures(), + } + kr := ProvideService(dynamic.ProvideService(cfg, keystore.ProvideService(kvstore.NewFakeKVStore()))) + key, err := kr.GetPublicKey(context.Background(), statickey.GetDefaultKeyID()) + require.NoError(t, err) + require.Equal(t, statickey.GetDefaultKey(), key) + }) +} diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index 64c5daf6478..2f6d18670a0 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -26,6 +26,8 @@ import ( "github.com/grafana/grafana/pkg/services/oauthtoken" "github.com/grafana/grafana/pkg/services/pluginsintegration/clientmiddleware" "github.com/grafana/grafana/pkg/services/pluginsintegration/config" + "github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever" + "github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever/dynamic" "github.com/grafana/grafana/pkg/services/pluginsintegration/keystore" "github.com/grafana/grafana/pkg/services/pluginsintegration/licensing" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" @@ -71,6 +73,9 @@ var WireSet = wire.NewSet( signature.ProvideService, wire.Bind(new(plugins.KeyStore), new(*keystore.Service)), keystore.ProvideService, + wire.Bind(new(plugins.KeyRetriever), new(*keyretriever.Service)), + keyretriever.ProvideService, + dynamic.ProvideService, ) // WireExtensionSet provides a wire.ProviderSet of plugin providers that can be