From 468f5d15ab698880a31c061f03117eee37493748 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 9 Apr 2020 00:00:16 -0700 Subject: [PATCH] Plugins: add a signature status flag (#23420) --- pkg/api/dtos/plugins.go | 30 ++++----- pkg/api/plugins.go | 2 + pkg/plugins/manifest.go | 114 +++++++++++++++++++++++++++++++++++ pkg/plugins/manifest_test.go | 53 ++++++++++++++++ pkg/plugins/models.go | 11 ++++ pkg/plugins/plugins.go | 5 +- 6 files changed, 200 insertions(+), 15 deletions(-) create mode 100644 pkg/plugins/manifest.go create mode 100644 pkg/plugins/manifest_test.go diff --git a/pkg/api/dtos/plugins.go b/pkg/api/dtos/plugins.go index 8b32bea41dc..1e7a38e9958 100644 --- a/pkg/api/dtos/plugins.go +++ b/pkg/api/dtos/plugins.go @@ -19,23 +19,25 @@ type PluginSetting struct { JsonData map[string]interface{} `json:"jsonData"` DefaultNavUrl string `json:"defaultNavUrl"` - LatestVersion string `json:"latestVersion"` - HasUpdate bool `json:"hasUpdate"` - State plugins.PluginState `json:"state"` + LatestVersion string `json:"latestVersion"` + HasUpdate bool `json:"hasUpdate"` + State plugins.PluginState `json:"state"` + Signature plugins.PluginSignature `json:"signature"` } type PluginListItem struct { - Name string `json:"name"` - Type string `json:"type"` - Id string `json:"id"` - Enabled bool `json:"enabled"` - Pinned bool `json:"pinned"` - Info *plugins.PluginInfo `json:"info"` - LatestVersion string `json:"latestVersion"` - HasUpdate bool `json:"hasUpdate"` - DefaultNavUrl string `json:"defaultNavUrl"` - Category string `json:"category"` - State plugins.PluginState `json:"state"` + Name string `json:"name"` + Type string `json:"type"` + Id string `json:"id"` + Enabled bool `json:"enabled"` + Pinned bool `json:"pinned"` + Info *plugins.PluginInfo `json:"info"` + LatestVersion string `json:"latestVersion"` + HasUpdate bool `json:"hasUpdate"` + DefaultNavUrl string `json:"defaultNavUrl"` + Category string `json:"category"` + State plugins.PluginState `json:"state"` + Signature plugins.PluginSignature `json:"signature"` } type PluginList []PluginListItem diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 3f856db667b..450cd06a7a2 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -100,6 +100,7 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) Response { HasUpdate: pluginDef.GrafanaNetHasUpdate, DefaultNavUrl: pluginDef.DefaultNavUrl, State: pluginDef.State, + Signature: pluginDef.Signature, } if pluginSetting, exists := pluginSettingsMap[pluginDef.Id]; exists { @@ -151,6 +152,7 @@ func GetPluginSettingByID(c *models.ReqContext) Response { LatestVersion: def.GrafanaNetVersion, HasUpdate: def.GrafanaNetHasUpdate, State: def.State, + Signature: def.Signature, } query := models.GetPluginSettingByIdQuery{PluginId: pluginID, OrgId: c.OrgId} diff --git a/pkg/plugins/manifest.go b/pkg/plugins/manifest.go new file mode 100644 index 00000000000..82c60bf0ff4 --- /dev/null +++ b/pkg/plugins/manifest.go @@ -0,0 +1,114 @@ +package plugins + +import ( + "crypto/sha256" + "fmt" + "io" + "io/ioutil" + "os" + "path" +) + +// Soon we can fetch keys from: +// https://grafana.com/api/plugins/ci/keys +var publicKeyText = `-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: OpenPGP.js v4.10.1 +Comment: https://openpgpjs.org + +xjMEXo5V+RYJKwYBBAHaRw8BAQdAxIzDC0767A5eOHESiU8ACz5c9BWIrkbJ +/5a4m/zsFWnNG0pvbiBTbWl0aCA8am9uQGV4YW1wbGUuY29tPsJ4BBAWCgAg +BQJejlX5BgsJBwgDAgQVCAoCBBYCAQACGQECGwMCHgEACgkQ1uNw7xqtn452 +hQD+LK/+1k5vdVVQDxRDyjN3+6Wiy/jK2wwH1JtHdnTUKKsA/iot3glN57wb +gaIQgQSZaE5E9tsIhGYhhNi8R743Oh4GzjgEXo5V+RIKKwYBBAGXVQEFAQEH +QCmdY+K50okUPp1NCFJxdje+Icr859fTwwRy9+hq+vUIAwEIB8JhBBgWCAAJ +BQJejlX5AhsMAAoJENbjcO8arZ+OpMwBAIcGCY1jMPo64h9G4MmFyPjL+wxn +U2YVAvfHQZnN+gD3AP47klt0/0tmSlbNwEvimZxA3tpUfNrtUO1K4E8VxSIn +Dg== +=PA1c +-----END PGP PUBLIC KEY BLOCK----- +` + +// PluginManifest holds details for the file manifest +type PluginManifest struct { + Plugin string `json:"plugin"` + Version string `json:"version"` + KeyID string `json:"keyId"` + Time int64 `json:"time"` + Files map[string]string `json:"files"` +} + +// 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 readPluginManifest(body []byte) (*PluginManifest, error) { + fmt.Printf("TODO... verify: %s", publicKeyText) + // block, _ := clearsign.Decode(body) + // if block == nil { + // return nil, fmt.Errorf("unable to decode manifest") + // } + + // txt := string(block.Plaintext) + // fmt.Printf("PLAINTEXT: %s", txt) + + // // Convert to a well typed object + // manifest := &PluginManifest{} + // err := json.Unmarshal(block.Plaintext, &manifest) + // if err != nil { + // return nil, fmt.Errorf("Error parsing manifest JSON: %s", err) + // } + + // keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(publicKeyText)) + // if err != nil { + // return nil, fmt.Errorf("failed to parse public key: %s", err) + // } + + // if _, err := openpgp.CheckDetachedSignature(keyring, + // bytes.NewBuffer(block.Bytes), + // block.ArmoredSignature.Body); err != nil { + // return nil, fmt.Errorf("failed to check signature: %s", err) + // } + + // return manifest, nil + return nil, fmt.Errorf("not yet parsing the manifest") +} + +// GetPluginSignatureState returns the signature state for a plugin +func GetPluginSignatureState(plugin *PluginBase) PluginSignature { + manifestPath := path.Join(plugin.PluginDir, "MANIFEST.txt") + + byteValue, err := ioutil.ReadFile(manifestPath) + if err != nil || len(byteValue) < 10 { + return PluginSignatureUnsigned + } + + manifest, err := readPluginManifest(byteValue) + if err != nil { + return PluginSignatureInvalid + } + + // Make sure the versions all match + if manifest.Plugin != plugin.Id || manifest.Version != plugin.Info.Version { + return PluginSignatureModified + } + + // Verify the manifest contents + for p, hash := range manifest.Files { + // Open the file + f, err := os.Open(path.Join(plugin.PluginDir, p)) + if err != nil { + return PluginSignatureModified + } + defer f.Close() + + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return PluginSignatureModified + } + sum := string(h.Sum(nil)) + if sum != hash { + return PluginSignatureModified + } + } + + // Everything OK + return PluginSignatureValid +} diff --git a/pkg/plugins/manifest_test.go b/pkg/plugins/manifest_test.go new file mode 100644 index 00000000000..4c5345ebdfc --- /dev/null +++ b/pkg/plugins/manifest_test.go @@ -0,0 +1,53 @@ +package plugins + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestManifestParsing(t *testing.T) { + + Convey("Should validate manifest", t, func() { + txt := ` +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +{ + "files": { + "LICENSE": "7df059597099bb7dcf25d2a9aedfaf4465f72d8d", + "README.md": "4ebed28a02dc029719296aa847bffcea8eb5b9ff", + "gfx_sheets_darwin_amd64": "4493f107eb175b085f020c1afea04614232dc0fd", + "gfx_sheets_linux_amd64": "d8b05884e3829d1389a9c0e4b79b0aba8c19ca4a", + "gfx_sheets_windows_amd64.exe": "88f33db20182e17c72c2823fe3bed87d8c45b0fd", + "img/config-page.png": "e6d8f6704dbe85d5f032d4e8ba44ebc5d4a68c43", + "img/dashboard.png": "63d79d0e0f9db21ea168324bd4e180d6892b9d2b", + "img/graph.png": "7ea6295954b24be55b27320af2074852fb088fa1", + "img/query-editor.png": "262f2bfddb004c7ce567042e8096f9e033c9b1bd", + "img/sheets.svg": "f134ab85caff88b59ea903c5491c6a08c221622f", + "module.js": "40b8c38cea260caed3cdc01d6e3c1eca483ab5c1", + "plugin.json": "bfcae42976f0feca58eed3636655bce51702d3ed" + }, + "plugin": "grafana-googlesheets-datasource", + "version": "1.2.3", + "keyId": "ABC", + "time": 1586404562862 +} +-----BEGIN PGP SIGNATURE----- +Version: OpenPGP.js v4.10.1 +Comment: https://openpgpjs.org + +wl4EARYKAAYFAl6OnNMACgkQ1uNw7xqtn45r0QEAqmoB/Q5NsJZNxnM69m2A +eQhcWNyo7yxO/4NZhVvBiJkA/iXUtptWbba3aw9TSZLn95LaUjKf4YUov29r +qX6kODEP +=YjQO +-----END PGP SIGNATURE----- +` + + manifest, err := readPluginManifest([]byte(txt)) + + // Always an error for now + So(err, ShouldNotBeNil) + So(manifest, ShouldBeNil) + }) +} diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index a6714a07e27..0286be7abfe 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -24,6 +24,16 @@ var ( PluginStateBeta PluginState = "beta" ) +type PluginSignature string + +const ( + PluginSignatureInternal PluginSignature = "internal" // core plugin, no signature + PluginSignatureValid PluginSignature = "valid" // signed and accurate MANIFEST + PluginSignatureInvalid PluginSignature = "invalid" // invalid signature + PluginSignatureModified PluginSignature = "modified" // valid signature, but content mismatch + PluginSignatureUnsigned PluginSignature = "unsigned" // no MANIFEST file +) + type PluginNotFoundError struct { PluginId string } @@ -49,6 +59,7 @@ type PluginBase struct { HideFromList bool `json:"hideFromList,omitempty"` Preload bool `json:"preload"` State PluginState `json:"state,omitempty"` + Signature PluginSignature `json:"signature"` IncludedInAppId string `json:"-"` PluginDir string `json:"-"` diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 78680dff11e..48c13cae829 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -122,7 +122,10 @@ func (pm *PluginManager) Init() error { } for _, p := range Plugins { - if !p.IsCorePlugin { + if p.IsCorePlugin { + p.Signature = PluginSignatureInternal + } else { + p.Signature = GetPluginSignatureState(p) metrics.SetPluginBuildInformation(p.Id, p.Type, p.Info.Version) } }