Chore: Refactor manifest verifier (#67218)

pull/67413/head
Andres Martinez Gotor 2 years ago committed by GitHub
parent 62587eee88
commit aa9838bd25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      pkg/api/plugin_resource_test.go
  2. 4
      pkg/plugins/ifaces.go
  3. 5
      pkg/plugins/manager/loader/loader_test.go
  4. 5
      pkg/plugins/manager/manager_integration_test.go
  5. 67
      pkg/plugins/manager/signature/manifest.go
  6. 17
      pkg/plugins/manager/signature/manifest_test.go
  7. 286
      pkg/plugins/manager/signature/manifestverifier/verifier.go
  8. 56
      pkg/plugins/manager/signature/statickey/static_retriever.go
  9. 6
      pkg/server/backgroundsvcs/background_services.go
  10. 234
      pkg/services/pluginsintegration/keyretriever/dynamic/dynamic_retriever.go
  11. 32
      pkg/services/pluginsintegration/keyretriever/dynamic/dynamic_retriever_test.go
  12. 29
      pkg/services/pluginsintegration/keyretriever/retriever.go
  13. 26
      pkg/services/pluginsintegration/keyretriever/retriever_test.go
  14. 5
      pkg/services/pluginsintegration/pluginsintegration.go

@ -14,7 +14,6 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/db" "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/localcache"
"github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" "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/loader/finder"
"github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature" "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/sources"
"github.com/grafana/grafana/pkg/plugins/manager/store" "github.com/grafana/grafana/pkg/plugins/manager/store"
"github.com/grafana/grafana/pkg/plugins/pluginscdn" "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/oauthtoken/oauthtokentest"
"github.com/grafana/grafana/pkg/services/pluginsintegration" "github.com/grafana/grafana/pkg/services/pluginsintegration"
"github.com/grafana/grafana/pkg/services/pluginsintegration/config" "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/pluginaccesscontrol"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service"
@ -67,7 +66,7 @@ func TestCallResource(t *testing.T) {
reg := registry.ProvideService() reg := registry.ProvideService()
l := loader.ProvideService(pCfg, fakes.NewFakeLicensingService(), signature.NewUnsignedAuthorizer(pCfg), l := loader.ProvideService(pCfg, fakes.NewFakeLicensingService(), signature.NewUnsignedAuthorizer(pCfg),
reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg), fakes.NewFakeRoleRegistry(), 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) srcs := sources.ProvideService(cfg, pCfg)
ps, err := store.ProvideService(reg, srcs, l) ps, err := store.ProvideService(reg, srcs, l)
require.NoError(t, err) require.NoError(t, err)

@ -157,3 +157,7 @@ type KeyStore interface {
GetLastUpdated(ctx context.Context) (*time.Time, error) GetLastUpdated(ctx context.Context) (*time.Time, error)
SetLastUpdated(ctx context.Context) error SetLastUpdated(ctx context.Context) error
} }
type KeyRetriever interface {
GetPublicKey(ctx context.Context, keyID string) (string, error)
}

@ -11,7 +11,6 @@ import (
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/plugins/config" "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/finder"
"github.com/grafana/grafana/pkg/plugins/manager/loader/initializer" "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"
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
"github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/plugins/manager/sources"
"github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/plugins/pluginscdn"
"github.com/grafana/grafana/pkg/services/pluginsintegration/keystore"
"github.com/grafana/grafana/pkg/setting" "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(), l := New(cfg, &fakes.FakeLicensingService{}, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(),
fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakeRoleRegistry(), fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakeRoleRegistry(),
assetpath.ProvideService(pluginscdn.ProvideService(cfg)), finder.NewLocalFinder(cfg), assetpath.ProvideService(pluginscdn.ProvideService(cfg)), finder.NewLocalFinder(cfg),
signature.ProvideService(cfg, keystore.ProvideService(kvstore.NewFakeKVStore()))) signature.ProvideService(cfg, statickey.New()))
for _, cb := range cbs { for _, cb := range cbs {
cb(l) cb(l)

@ -14,7 +14,6 @@ import (
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
"github.com/grafana/grafana/pkg/infra/db" "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/infra/tracing"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" "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/loader/finder"
"github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature" "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/sources"
"github.com/grafana/grafana/pkg/plugins/manager/store" "github.com/grafana/grafana/pkg/plugins/manager/store"
"github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/plugins/pluginscdn"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/services/pluginsintegration/config" "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" plicensing "github.com/grafana/grafana/pkg/services/pluginsintegration/licensing"
"github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/services/searchV2"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
@ -119,7 +118,7 @@ func TestIntegrationPluginManager(t *testing.T) {
lic := plicensing.ProvideLicensing(cfg, &licensing.OSSLicensingService{Cfg: cfg}) lic := plicensing.ProvideLicensing(cfg, &licensing.OSSLicensingService{Cfg: cfg})
l := loader.ProvideService(pCfg, lic, signature.NewUnsignedAuthorizer(pCfg), l := loader.ProvideService(pCfg, lic, signature.NewUnsignedAuthorizer(pCfg),
reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg), fakes.NewFakeRoleRegistry(), 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) srcs := sources.ProvideService(cfg, pCfg)
ps, err := store.ProvideService(reg, srcs, l) ps, err := store.ProvideService(reg, srcs, l)
require.NoError(t, err) require.NoError(t, err)

@ -1,6 +1,7 @@
package signature package signature
import ( import (
"bytes"
"context" "context"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
@ -15,13 +16,14 @@ import (
"runtime" "runtime"
"strings" "strings"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/clearsign" "github.com/ProtonMail/go-crypto/openpgp/clearsign"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/gobwas/glob" "github.com/gobwas/glob"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/plugins/manager/signature/manifestverifier"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
@ -56,28 +58,19 @@ func (m *PluginManifest) isV2() bool {
} }
type Signature struct { type Signature struct {
verifier *manifestverifier.ManifestVerifier log log.Logger
mlog log.Logger kr plugins.KeyRetriever
} }
var _ plugins.SignatureCalculator = &Signature{} var _ plugins.SignatureCalculator = &Signature{}
func ProvideService(cfg *config.Cfg, kv plugins.KeyStore) *Signature { func ProvideService(cfg *config.Cfg, kr plugins.KeyRetriever) *Signature {
log := log.New("plugin.signature")
return &Signature{ return &Signature{
verifier: manifestverifier.New(cfg, log, kv), log: log.New("plugin.signature"),
mlog: log, 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 // readPluginManifest attempts to read and verify the plugin manifest
// if any error occurs or the manifest is not valid, this will return an error // 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) { 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) return plugins.Signature{}, fmt.Errorf("files: %w", err)
} }
if len(fsFiles) == 0 { 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{ return plugins.Signature{
Status: plugins.SignatureInvalid, Status: plugins.SignatureInvalid,
}, nil }, nil
@ -118,13 +111,13 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
f, err := plugin.FS.Open("MANIFEST.txt") f, err := plugin.FS.Open("MANIFEST.txt")
if err != nil { if err != nil {
if errors.Is(err, plugins.ErrFileNotExist) { 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{ return plugins.Signature{
Status: plugins.SignatureUnsigned, Status: plugins.SignatureUnsigned,
}, nil }, 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{ return plugins.Signature{
Status: plugins.SignatureInvalid, Status: plugins.SignatureInvalid,
}, nil }, nil
@ -134,13 +127,13 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
return return
} }
if err = f.Close(); err != nil { 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) byteValue, err := io.ReadAll(f)
if err != nil || len(byteValue) < 10 { 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{ return plugins.Signature{
Status: plugins.SignatureUnsigned, Status: plugins.SignatureUnsigned,
}, nil }, nil
@ -148,7 +141,7 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
manifest, err := s.readPluginManifest(ctx, byteValue) manifest, err := s.readPluginManifest(ctx, byteValue)
if err != nil { 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{ return plugins.Signature{
Status: plugins.SignatureInvalid, Status: plugins.SignatureInvalid,
}, nil }, 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 // Validate that plugin is running within defined root URLs
if len(manifest.RootURLs) > 0 { if len(manifest.RootURLs) > 0 {
if match, err := urlMatch(manifest.RootURLs, setting.AppUrl, manifest.SignatureType); err != nil { 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 return plugins.Signature{}, err
} else if !match { } 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) "appUrl", setting.AppUrl, "rootUrls", manifest.RootURLs)
return plugins.Signature{ return plugins.Signature{
Status: plugins.SignatureInvalid, Status: plugins.SignatureInvalid,
@ -185,7 +178,7 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
// Verify the manifest contents // Verify the manifest contents
for p, hash := range manifest.Files { for p, hash := range manifest.Files {
err = verifyHash(s.mlog, plugin, p, hash) err = verifyHash(s.log, plugin, p, hash)
if err != nil { if err != nil {
return plugins.Signature{ return plugins.Signature{
Status: plugins.SignatureModified, Status: plugins.SignatureModified,
@ -215,13 +208,13 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
} }
if len(unsignedFiles) > 0 { 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{ return plugins.Signature{
Status: plugins.SignatureModified, Status: plugins.SignatureModified,
}, nil }, nil
} }
s.mlog.Debug("Plugin signature valid", "id", plugin.JSONData.ID) s.log.Debug("Plugin signature valid", "id", plugin.JSONData.ID)
return plugins.Signature{ return plugins.Signature{
Status: plugins.SignatureValid, Status: plugins.SignatureValid,
Type: manifest.SignatureType, 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
} }

@ -11,11 +11,10 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/manager/fakes" "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" "github.com/grafana/grafana/pkg/setting"
) )
@ -52,7 +51,7 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX
-----END PGP SIGNATURE-----` -----END PGP SIGNATURE-----`
t.Run("valid manifest", func(t *testing.T) { 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)) manifest, err := s.readPluginManifest(context.Background(), []byte(txt))
require.NoError(t, err) require.NoError(t, err)
@ -69,7 +68,7 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX
t.Run("invalid manifest", func(t *testing.T) { t.Run("invalid manifest", func(t *testing.T) {
modified := strings.ReplaceAll(txt, "README.md", "xxxxxxxxxx") 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)) _, err := s.readPluginManifest(context.Background(), []byte(modified))
require.Error(t, err) require.Error(t, err)
}) })
@ -107,7 +106,7 @@ khdr/tZ1PDgRxMqB/u+Vtbpl0xSxgblnrDOYMSI=
-----END PGP SIGNATURE-----` -----END PGP SIGNATURE-----`
t.Run("valid manifest", func(t *testing.T) { 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)) manifest, err := s.readPluginManifest(context.Background(), []byte(txt))
require.NoError(t, err) require.NoError(t, err)
@ -161,7 +160,7 @@ func TestCalculate(t *testing.T) {
setting.AppUrl = tc.appURL setting.AppUrl = tc.appURL
basePath := filepath.Join(parentDir, "testdata/non-pvt-with-root-url/plugin") 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{ sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class { PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.External return plugins.External
@ -189,7 +188,7 @@ func TestCalculate(t *testing.T) {
basePath := "../testdata/renderer-added-file/plugin" basePath := "../testdata/renderer-added-file/plugin"
runningWindows = true runningWindows = true
s := ProvideService(&config.Cfg{}, keystore.ProvideService(kvstore.NewFakeKVStore())) s := ProvideService(&config.Cfg{}, statickey.New())
sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{ sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class { PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.External return plugins.External
@ -253,7 +252,7 @@ func TestCalculate(t *testing.T) {
toSlash = tc.platform.toSlashFunc() toSlash = tc.platform.toSlashFunc()
fromSlash = tc.platform.fromSlashFunc() fromSlash = tc.platform.fromSlashFunc()
s := ProvideService(&config.Cfg{}, keystore.ProvideService(kvstore.NewFakeKVStore())) s := ProvideService(&config.Cfg{}, statickey.New())
pfs, err := tc.fsFactory() pfs, err := tc.fsFactory()
require.NoError(t, err) require.NoError(t, err)
pfs, err = newPathSeparatorOverrideFS(string(tc.platform.separator), pfs) pfs, err = newPathSeparatorOverrideFS(string(tc.platform.separator), pfs)
@ -721,7 +720,7 @@ func Test_validateManifest(t *testing.T) {
} }
for _, tc := range tcs { for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) { 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) err := s.validateManifest(context.Background(), *tc.manifest, nil)
require.Errorf(t, err, tc.expectedErr) require.Errorf(t, err, tc.expectedErr)
}) })

@ -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,
}
}

@ -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
}

@ -8,7 +8,6 @@ import (
uss "github.com/grafana/grafana/pkg/infra/usagestats/service" uss "github.com/grafana/grafana/pkg/infra/usagestats/service"
"github.com/grafana/grafana/pkg/infra/usagestats/statscollector" "github.com/grafana/grafana/pkg/infra/usagestats/statscollector"
"github.com/grafana/grafana/pkg/plugins/manager/process" "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/registry"
"github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/auth" "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/ngalert"
"github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/notifications"
plugindashboardsservice "github.com/grafana/grafana/pkg/services/plugindashboards/service" 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" "github.com/grafana/grafana/pkg/services/provisioning"
publicdashboardsmetric "github.com/grafana/grafana/pkg/services/publicdashboards/metric" publicdashboardsmetric "github.com/grafana/grafana/pkg/services/publicdashboards/metric"
"github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/rendering"
@ -51,7 +51,7 @@ func ProvideBackgroundServiceRegistry(
grpcServerProvider grpcserver.Provider, secretMigrationProvider secretsMigrations.SecretMigrationProvider, loginAttemptService *loginattemptimpl.Service, grpcServerProvider grpcserver.Provider, secretMigrationProvider secretsMigrations.SecretMigrationProvider, loginAttemptService *loginattemptimpl.Service,
bundleService *supportbundlesimpl.Service, bundleService *supportbundlesimpl.Service,
publicDashboardsMetric *publicdashboardsmetric.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? // Need to make sure these are initialized, is there a better place to put them?
_ dashboardsnapshots.Service, _ *alerting.AlertNotificationService, _ dashboardsnapshots.Service, _ *alerting.AlertNotificationService,
_ serviceaccounts.Service, _ *guardian.Provider, _ serviceaccounts.Service, _ *guardian.Provider,
@ -88,7 +88,7 @@ func ProvideBackgroundServiceRegistry(
loginAttemptService, loginAttemptService,
bundleService, bundleService,
publicDashboardsMetric, publicDashboardsMetric,
signature, keyRetriever,
) )
} }

@ -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,
}
}

@ -1,42 +1,20 @@
package manifestverifier package dynamic
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os"
"testing" "testing"
"time" "time"
"github.com/ProtonMail/go-crypto/openpgp/clearsign"
"github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/plugins/config" "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/featuremgmt"
"github.com/grafana/grafana/pkg/services/pluginsintegration/keystore" "github.com/grafana/grafana/pkg/services/pluginsintegration/keystore"
"github.com/stretchr/testify/require" "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) { func setFakeAPIServer(t *testing.T, publicKey string, keyID string) (*httptest.Server, chan bool) {
done := make(chan bool) done := make(chan bool)
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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 })), done
} }
func Test_PublicKeyUpdate(t *testing.T) { 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{ cfg := &config.Cfg{
Features: featuremgmt.WithFeatures([]interface{}{featuremgmt.FlagPluginsAPIManifestKey}...), Features: featuremgmt.WithFeatures([]interface{}{featuremgmt.FlagPluginsAPIManifestKey}...),
} }
expectedKey := "fake" expectedKey := "fake"
s, done := setFakeAPIServer(t, expectedKey, "7e4d0c6a708866e7") s, done := setFakeAPIServer(t, expectedKey, "7e4d0c6a708866e7")
cfg.GrafanaComURL = s.URL cfg.GrafanaComURL = s.URL
v := New(cfg, log.New("test"), keystore.ProvideService(kvstore.NewFakeKVStore())) v := ProvideService(cfg, keystore.ProvideService(kvstore.NewFakeKVStore()))
go func() { go func() {
err := v.Run(context.Background()) err := v.Run(context.Background())
require.NoError(t, err) require.NoError(t, err)
@ -94,7 +72,7 @@ func Test_PublicKeyUpdate(t *testing.T) {
expectedKey := "fake" expectedKey := "fake"
s, done := setFakeAPIServer(t, expectedKey, "7e4d0c6a708866e7") s, done := setFakeAPIServer(t, expectedKey, "7e4d0c6a708866e7")
cfg.GrafanaComURL = s.URL cfg.GrafanaComURL = s.URL
v := New(cfg, log.New("test"), keystore.ProvideService(kvstore.NewFakeKVStore())) v := ProvideService(cfg, keystore.ProvideService(kvstore.NewFakeKVStore()))
go func() { go func() {
err := v.Run(context.Background()) err := v.Run(context.Background())
require.NoError(t, err) require.NoError(t, err)
@ -116,7 +94,7 @@ func Test_PublicKeyUpdate(t *testing.T) {
expectedKey := "fake" expectedKey := "fake"
s, done := setFakeAPIServer(t, expectedKey, "other") s, done := setFakeAPIServer(t, expectedKey, "other")
cfg.GrafanaComURL = s.URL cfg.GrafanaComURL = s.URL
v := New(cfg, log.New("test"), keystore.ProvideService(kvstore.NewFakeKVStore())) v := ProvideService(cfg, keystore.ProvideService(kvstore.NewFakeKVStore()))
go func() { go func() {
err := v.Run(context.Background()) err := v.Run(context.Background())
require.NoError(t, err) require.NoError(t, err)

@ -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)
}

@ -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)
})
}

@ -26,6 +26,8 @@ import (
"github.com/grafana/grafana/pkg/services/oauthtoken" "github.com/grafana/grafana/pkg/services/oauthtoken"
"github.com/grafana/grafana/pkg/services/pluginsintegration/clientmiddleware" "github.com/grafana/grafana/pkg/services/pluginsintegration/clientmiddleware"
"github.com/grafana/grafana/pkg/services/pluginsintegration/config" "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/keystore"
"github.com/grafana/grafana/pkg/services/pluginsintegration/licensing" "github.com/grafana/grafana/pkg/services/pluginsintegration/licensing"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
@ -71,6 +73,9 @@ var WireSet = wire.NewSet(
signature.ProvideService, signature.ProvideService,
wire.Bind(new(plugins.KeyStore), new(*keystore.Service)), wire.Bind(new(plugins.KeyStore), new(*keystore.Service)),
keystore.ProvideService, 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 // WireExtensionSet provides a wire.ProviderSet of plugin providers that can be

Loading…
Cancel
Save