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/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)

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

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

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

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

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

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

@ -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 (
"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)

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

Loading…
Cancel
Save