Plugins: Fix manifest verification (#24573)

This commit is contained in:
Arve Knudsen 2020-05-12 15:48:24 +02:00 committed by GitHub
parent 20f0ee2f22
commit 892f9f789c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 116 additions and 103 deletions

View File

@ -55,7 +55,7 @@ type PluginStartFuncs struct {
OnStart StartFunc
}
// PluginDescriptor descriptor used for registering backend plugins.
// PluginDescriptor is a descriptor used for registering backend plugins.
type PluginDescriptor struct {
pluginID string
executablePath string
@ -64,6 +64,11 @@ type PluginDescriptor struct {
startFns PluginStartFuncs
}
// PluginID returns the plugin ID.
func (pd PluginDescriptor) PluginID() string {
return pd.pluginID
}
// getV2PluginSet returns list of plugins supported on v2.
func getV2PluginSet() goplugin.PluginSet {
return goplugin.PluginSet{

View File

@ -15,7 +15,7 @@ func (pm *PluginManager) updateAppDashboards() {
query := models.GetPluginSettingsQuery{OrgId: 0}
if err := bus.Dispatch(&query); err != nil {
plog.Error("Failed to get all plugin settings", "error", err)
pm.log.Error("Failed to get all plugin settings", "error", err)
return
}

View File

@ -3,6 +3,7 @@ package plugins
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"io"
@ -10,6 +11,7 @@ import (
"os"
"path"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/util/errutil"
"golang.org/x/crypto/openpgp"
@ -18,7 +20,7 @@ import (
// Soon we can fetch keys from:
// https://grafana.com/api/plugins/ci/keys
var publicKeyText = `-----BEGIN PGP PUBLIC KEY BLOCK-----
const publicKeyText = `-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: OpenPGP.js v4.10.1
Comment: https://openpgpjs.org
@ -80,8 +82,9 @@ func readPluginManifest(body []byte) (*pluginManifest, error) {
return manifest, nil
}
// GetPluginSignatureState returns the signature state for a plugin
func GetPluginSignatureState(plugin *PluginBase) PluginSignature {
// getPluginSignatureState returns the signature state for a plugin.
func getPluginSignatureState(log log.Logger, plugin *PluginBase) PluginSignature {
log.Debug("Getting signature state of plugin", "plugin", plugin.Id)
manifestPath := path.Join(plugin.PluginDir, "MANIFEST.txt")
byteValue, err := ioutil.ReadFile(manifestPath)
@ -100,9 +103,11 @@ func GetPluginSignatureState(plugin *PluginBase) PluginSignature {
}
// Verify the manifest contents
log.Debug("Verifying contents of plugin manifest", "plugin", plugin.Id)
for p, hash := range manifest.Files {
// Open the file
f, err := os.Open(path.Join(plugin.PluginDir, p))
fp := path.Join(plugin.PluginDir, p)
f, err := os.Open(fp)
if err != nil {
return PluginSignatureModified
}
@ -110,10 +115,12 @@ func GetPluginSignatureState(plugin *PluginBase) PluginSignature {
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
log.Warn("Couldn't read plugin file", "plugin", plugin.Id, "filename", fp)
return PluginSignatureModified
}
sum := string(h.Sum(nil))
sum := hex.EncodeToString(h.Sum(nil))
if sum != hash {
log.Warn("Plugin file's signature has been modified versus manifest", "plugin", plugin.Id, "filename", fp)
return PluginSignatureModified
}
}

View File

@ -5,9 +5,10 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestManifestParsing(t *testing.T) {
func TestReadPluginManifest(t *testing.T) {
txt := `-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
@ -42,15 +43,14 @@ NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX
t.Run("valid manifest", func(t *testing.T) {
manifest, err := readPluginManifest([]byte(txt))
assert.Nil(t, err)
assert.NotNil(t, manifest)
assert.Equal(t, manifest.Plugin, "grafana-googlesheets-datasource")
require.NoError(t, err)
require.NotNil(t, manifest)
assert.Equal(t, "grafana-googlesheets-datasource", manifest.Plugin)
})
t.Run("invalid manifest", func(t *testing.T) {
modified := strings.ReplaceAll(txt, "README.md", "xxxxxxxxxx")
manifest, err := readPluginManifest([]byte(modified))
assert.NotNil(t, err)
assert.Nil(t, manifest)
_, err := readPluginManifest([]byte(modified))
require.Error(t, err)
})
}

View File

@ -140,7 +140,7 @@ func (pm *PluginManager) Init() error {
if p.IsCorePlugin {
p.Signature = PluginSignatureInternal
} else {
p.Signature = GetPluginSignatureState(p)
p.Signature = getPluginSignatureState(pm.log, p)
metrics.SetPluginBuildInformation(p.Id, p.Type, p.Info.Version)
}
}
@ -282,9 +282,9 @@ func (scanner *PluginScanner) loadPlugin(pluginJsonFilePath string) error {
pluginCommon.PluginDir = filepath.Dir(pluginJsonFilePath)
// For the time being, we choose to only require back-end plugins to be signed
// NOTE: the state is calculated again for when setting metadata on the object
// NOTE: the state is calculated again when setting metadata on the object
if pluginCommon.Backend && scanner.requireSigned {
sig := GetPluginSignatureState(&pluginCommon)
sig := getPluginSignatureState(scanner.log, &pluginCommon)
if sig != PluginSignatureValid {
scanner.log.Debug("Invalid Plugin Signature", "pluginID", pluginCommon.Id, "pluginDir", pluginCommon.PluginDir, "state", sig)
if sig == PluginSignatureUnsigned {

View File

@ -104,6 +104,24 @@ func TestPluginManager_Init(t *testing.T) {
assert.Equal(t, []error{fmt.Errorf(`plugin "test" has an invalid signature`)}, pm.scanningErrors)
})
t.Run("With external back-end plugin lacking files listed in manifest", func(t *testing.T) {
origPluginsPath := setting.PluginsPath
t.Cleanup(func() {
setting.PluginsPath = origPluginsPath
})
setting.PluginsPath = "testdata/lacking-files"
fm := &fakeBackendPluginManager{}
pm := &PluginManager{
Cfg: &setting.Cfg{},
BackendPluginManager: fm,
}
err := pm.Init()
require.NoError(t, err)
assert.Equal(t, []error{fmt.Errorf(`plugin "test"'s signature has been modified`)}, pm.scanningErrors)
})
t.Run("Transform plugins should be ignored when expressions feature is off", func(t *testing.T) {
origPluginsPath := setting.PluginsPath
t.Cleanup(func() {
@ -120,7 +138,7 @@ func TestPluginManager_Init(t *testing.T) {
require.NoError(t, err)
assert.Empty(t, pm.scanningErrors)
assert.Equal(t, 0, fm.registerCount)
assert.Empty(t, fm.registeredPlugins)
})
t.Run("Transform plugins should be loaded when expressions feature is on", func(t *testing.T) {
@ -130,18 +148,20 @@ func TestPluginManager_Init(t *testing.T) {
})
setting.PluginsPath = "testdata/behind-feature-flag"
fm := &fakeBackendPluginManager{}
pm := &PluginManager{
Cfg: &setting.Cfg{
FeatureToggles: map[string]bool{
"expressions": true,
},
},
BackendPluginManager: &fakeBackendPluginManager{},
BackendPluginManager: fm,
}
err := pm.Init()
require.NoError(t, err)
assert.Equal(t, []error{fmt.Errorf(`plugin "gel" is unsigned`)}, pm.scanningErrors)
assert.Empty(t, pm.scanningErrors)
assert.Equal(t, []string{"gel"}, fm.registeredPlugins)
})
}
@ -166,11 +186,11 @@ func TestPluginManager_IsBackendOnlyPlugin(t *testing.T) {
}
type fakeBackendPluginManager struct {
registerCount int
registeredPlugins []string
}
func (f *fakeBackendPluginManager) Register(descriptor backendplugin.PluginDescriptor) error {
f.registerCount++
f.registeredPlugins = append(f.registeredPlugins, descriptor.PluginID())
return nil
}

View File

@ -0,0 +1,23 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
{
"plugin": "gel",
"version": "1.0.0",
"files": {
"plugin.json": "b9b3bb0dab3c4655a929a1e48a957466e3e2717992bdd29da27e5eed2fae090c"
},
"time": 1589274667427,
"keyId": "7e4d0c6a708866e7"
}
-----BEGIN PGP SIGNATURE-----
Version: OpenPGP.js v4.10.1
Comment: https://openpgpjs.org
wqIEARMKAAYFAl66aCsACgkQfk0ManCIZufDMAIJAWoNVihI9ZSBpUpgXrzY
XXsI3OmHuVpzrv6M6bk5jYdzY4SyzZmdw4CB51TIDJW9SnUajlXxWLXGYY+w
B2rSYvuhAgkBlG9w5OV3jcyg/wfUrIcCO5XRHMydCg0hIOznClzuG0uWn3wm
d4RT/ap1ezislQ/91zvhsLgAIztZlm3EsNBv7sI=
=WPLw
-----END PGP SIGNATURE-----

View File

@ -5,6 +5,7 @@
"backend": true,
"info": {
"description": "Test",
"version": "1.0.0",
"author": {
"name": "Grafana Labs",
"url": "https://grafana.com"

View File

@ -1,50 +0,0 @@
System.register([], function (_export) {
"use strict";
return {
setters: [],
execute: function () {
function Datasource(instanceSettings, backendSrv) {
this.url = instanceSettings.url;
// this.testDatasource = function() {
// return backendSrv.datasourceRequest({
// method: 'GET',
// url: this.url + '/api/v4/search'
// });
// }
//
this.testDatasource = function() {
return backendSrv.datasourceRequest({
method: 'GET',
url: this.url + '/tokenTest'
});
}
}
function ConfigCtrl() {
}
ConfigCtrl.template = `
<div class="gf-form">
<label class="gf-form-label width-13">TenantId </label>
<input type="text" class="gf-form-input max-width-18" ng-model='ctrl.current.jsonData.tenantId'></input>
</div>
<div class="gf-form">
<label class="gf-form-label width-13">ClientId </label>
<input type="text" class="gf-form-input max-width-18" ng-model='ctrl.current.jsonData.clientId'></input>
</div>
<div class="gf-form">
<label class="gf-form-label width-13">Client secret</label>
<input type="text" class="gf-form-input max-width-18" ng-model='ctrl.current.secureJsonData.clientSecret'></input>
</div>
`;
_export('Datasource', Datasource);
_export('ConfigCtrl', ConfigCtrl);
}
};
});

View File

@ -1,31 +0,0 @@
{
"type": "datasource",
"name": "Test Datasource",
"id": "test-ds",
"routes": [
{
"path": "tokenTest",
"method": "*",
"url": "https://management.azure.com",
"tokenAuth": {
"url": "https://login.microsoftonline.com/{{.JsonData.tenantId}}/oauth2/token",
"params": {
"grant_type": "client_credentials",
"client_id": "{{.JsonData.clientId}}",
"client_secret": "{{.SecureJsonData.clientSecret}}",
"resource": "https://management.azure.com/"
}
}
},
{
"path": "api/v4/",
"method": "*",
"url": "http://localhost:3333",
"headers": [
{"name": "X-CH-Auth-API-Token", "content": "test {{.SecureJsonData.token}}"},
{"name": "X-CH-Auth-Email", "content": "test {{.JsonData.email}}"}
]
}
]
}

View File

@ -0,0 +1,24 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
{
"plugin": "test",
"version": "1.0.0",
"files": {
"executable": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"plugin.json": "f19e18ca5dec690b94f2ff7866372db4bc63f4ff1101ee5d9061b9a6f026f6dd"
},
"time": 1589270570251,
"keyId": "7e4d0c6a708866e7"
}
-----BEGIN PGP SIGNATURE-----
Version: OpenPGP.js v4.10.1
Comment: https://openpgpjs.org
wqIEARMKAAYFAl66WCoACgkQfk0ManCIZuf8zwIJAWBVDJyzqchZ/DN7ZDCy
Kyb63CajW/XdgHalPDB0sZ/80ExjCSprkFYPL+hUlTUHIXh+jGkfnYpMoqWA
Om77bFc2AgkBV8HTsuRw/lXUezKnDuXcgUIvHvEwKWTvtbLgcuMXMDAVAEBj
isBWmA8xvUfMzgQ9CJUHAaJ6hf1erpE4BuBqtMM=
=uBIX
-----END PGP SIGNATURE-----

View File

@ -0,0 +1,14 @@
{
"type": "datasource",
"name": "Test",
"id": "test",
"backend": true,
"info": {
"description": "Test",
"version": "1.0.0",
"author": {
"name": "Grafana Labs",
"url": "https://grafana.com"
}
}
}