* [MM-18757] POST handler for `/plugins/marketplace` (#12372)

* Implement installMarketplacePlugin

* Add InstallMarketplacePlugin endpoint

* Fix go.mod

* merge with master

* Fix go.mod

* Fix plugin tests

* Move get plugin to marketplace client

* Fix stylistic concerns

* Add trailing newline to the go.mod

* [MM-16586] Add plugin signature settings (#12390)

* MM-17149 - Extend config.json for marketplace settings (#11933)

* MM-17149 - Extend config.json for marketplace settings

* Renamed MarketplaceUrl, tracking default marketplace url

* Added EnableMarketplace to the client config

* Revert "Added EnableMarketplace to the client config"

This reverts commit 0f982c4c661c2cd9bb96264e9a01a2363c40d9c5.

* MM-17149 - Added EnableMarketplace to the client config (#11958)

* Added EnableMarketplace to the client config

* Moved EnableMarketplace setting out of limited client configuration

* Add public key settings to the config.json

* Rename PublicKeys to SignaturePublicKeyFiles

* Change filepath.Split to Base

* Remove additional prints

* Force extention of a public key file

* Remove config validation

* Remove error on delete

* Remove config cloning

* Add error messages

* Add plugin public key tests

* Rename extension to PluginSignaturePublicKeyFileExtention

* Remove EnforceVerification

* Change []*PublicKeyDescription to []string

* Change .asc extension to .plugin.asc

* Change ordering of public methods

* Change plugin key commands

* Update examples in the plugin key commands

* Remove forcing extention

* Add verify signature in settings

* Fix tabbing

* Fix naming

* Remove unused text

* Remove unused text

* Update command examples

* Fix unit tests

* Change errors.New to errors.Wrap

* Fix verbose flag

* Change .asc to .gpg

* Fix }

* Change AddPublicKey signature

* Change public.key extension

* Add plugin public key command tests

* Update en.json

* Bootstrap the public keys

* Update en.json

* Fix en.json

* Fix en.json

* Bootstrap hard-coded public key

* Remove unused texts in en.json

* Change file to name

* Add license header

* Update development public key

* Remove writeFile method

* Remove .plugin.asc extension

* Rename publiKey to mattermostPublicKey

* Remove init_public_keys string

* GolangCI

* Closing file handlers

* Fixed test that was installing nps plugin

* [MM-19798] Implement plugin signature verification (#12768)

* MM-17149 - Extend config.json for marketplace settings (#11933)

* MM-17149 - Extend config.json for marketplace settings

* Renamed MarketplaceUrl, tracking default marketplace url

* Added EnableMarketplace to the client config

* Revert "Added EnableMarketplace to the client config"

This reverts commit 0f982c4c661c2cd9bb96264e9a01a2363c40d9c5.

* MM-17149 - Added EnableMarketplace to the client config (#11958)

* Added EnableMarketplace to the client config

* Moved EnableMarketplace setting out of limited client configuration

* Add public key settings to the config.json

* Rename PublicKeys to SignaturePublicKeyFiles

* Change filepath.Split to Base

* Remove additional prints

* Force extention of a public key file

* Remove config validation

* Remove error on delete

* Remove config cloning

* Add error messages

* Add plugin public key tests

* Rename extension to PluginSignaturePublicKeyFileExtention

* Remove EnforceVerification

* Change []*PublicKeyDescription to []string

* Change .asc extension to .plugin.asc

* Change ordering of public methods

* Change plugin key commands

* Update examples in the plugin key commands

* Remove forcing extention

* Add verify signature in settings

* Fix tabbing

* Fix naming

* Remove unused text

* Remove unused text

* Update command examples

* Fix unit tests

* Change errors.New to errors.Wrap

* Fix verbose flag

* Change .asc to .gpg

* Fix }

* Change AddPublicKey signature

* Change public.key extension

* Add plugin public key command tests

* Update en.json

* Bootstrap the public keys

* Update en.json

* Fix en.json

* Fix en.json

* Bootstrap hard-coded public key

* Remove unused texts in en.json

* Change file to name

* Add license header

* Implement plugin signature verification

* Remove benburker openpgp

* Update en.json

* Update development public key

* Add support of multiple signatures in filestore

* Update en.json

* Run go mod vendor

* Fix style

* Remove writeFile method

* Remove .plugin.asc extension

* Rename publiKey to mattermostPublicKey

* Verify plugin with mattermost public key

* Remove init_public_keys string

* Add InstallPluginWithSignature method and  Refactor

* Add signature verification on claster notification

* Remove armored signature headers

* Add error strings

* Fix en.json

* Change signatureStorePath

* Implement minor fixes

* Refactor plugin install methods

* Add installPlugin method to uploadPlugin

* Update en.json

* Refactor installPlugin

* Limit number of signatures

* Close signatures

* Fix helper function

* Fix fromReadCloseSeekerToReadSeeker

* Cleaned up ReadCloseSeeker for signatures

* Remove signature truncation on FS

* GolangCI

* Add tests for armored signatures and plugin uploads

* Fix nil slice issue

* Fix TestPluginSync

* Fixed tests

* Return io.ReadSeeker from downloadFromUrl

* Add log for the found plugins in the file store

* Remove logging plugin detection info

* [MM-20134] Consume and store single-signature for each plugin (#13081)

* Consume and store single-signature for each plugin

* Fix en.json

* Remove saveSignature method

* Remove public key hash

* PR Feedback

* refactored config

* PR feedback
Этот коммит содержится в:
Ali Farooq
2019-11-18 19:02:41 -05:00
коммит произвёл GitHub
родитель d44d563ef3
Коммит a6e992ae74
56 изменённых файлов: 8692 добавлений и 80 удалений

Просмотреть файл

@@ -19,6 +19,12 @@ import (
"github.com/pkg/errors"
)
type pluginSignaturePath struct {
pluginId string
path string
signaturePath string
}
// GetPluginsEnvironment returns the plugin environment for use if plugins are enabled and
// initialized.
//
@@ -173,7 +179,7 @@ func (a *App) InitPlugins(pluginDir, webappPluginDir string) {
mlog.Debug("Installing prepackaged plugin", mlog.String("path", walkPath))
_, appErr := a.installPluginLocally(fileReader, installPluginLocallyOnlyIfNewOrUpgrade)
_, appErr := a.installPluginLocally(fileReader, nil, installPluginLocallyOnlyIfNewOrUpgrade)
if appErr != nil {
mlog.Error("Failed to unpack prepackaged plugin", mlog.Err(appErr), mlog.String("path", walkPath))
}
@@ -234,35 +240,33 @@ func (a *App) SyncPlugins() *model.AppError {
}
// Install plugins from the file store.
fileStorePaths, appErr := a.ListDirectory(fileStorePluginFolder)
pluginSignaturePathMap, appErr := a.getPluginsFromFolder()
if appErr != nil {
return model.NewAppError("SyncPlugins", "app.plugin.sync.list_filestore.app_error", nil, appErr.Error(), http.StatusInternalServerError)
return appErr
}
if len(fileStorePaths) == 0 {
mlog.Info("Found no files in plugins file store")
return nil
}
for _, path := range fileStorePaths {
if !strings.HasSuffix(path, ".tar.gz") {
mlog.Warn("Ignoring non-plugin in file store", mlog.String("bundle", path))
continue
}
var reader filesstore.ReadCloseSeeker
reader, appErr = a.FileReader(path)
for _, plugin := range pluginSignaturePathMap {
reader, appErr := a.FileReader(plugin.path)
if appErr != nil {
mlog.Error("Failed to open plugin bundle from file store.", mlog.String("bundle", path), mlog.Err(appErr))
mlog.Error("Failed to open plugin bundle from file store.", mlog.String("bundle", plugin.path), mlog.Err(appErr))
continue
}
defer reader.Close()
mlog.Info("Syncing plugin from file store", mlog.String("bundle", path))
if _, err := a.installPluginLocally(reader, installPluginLocallyAlways); err != nil {
mlog.Error("Failed to sync plugin from file store", mlog.String("bundle", path), mlog.Err(err))
var signature filesstore.ReadCloseSeeker
if *a.Config().PluginSettings.RequirePluginSignature {
signature, appErr = a.FileReader(plugin.signaturePath)
if appErr != nil {
mlog.Error("Failed to open plugin signature from file store.", mlog.Err(appErr))
continue
}
defer signature.Close()
}
mlog.Info("Syncing plugin from file store", mlog.String("bundle", plugin.path))
if _, err := a.installPluginLocally(reader, signature, installPluginLocallyAlways); err != nil {
mlog.Error("Failed to sync plugin from file store", mlog.String("bundle", plugin.path), mlog.Err(err))
}
}
return nil
}
@@ -418,6 +422,24 @@ func (a *App) GetPlugins() (*model.PluginsResponse, *model.AppError) {
return resp, nil
}
// GetMarketplacePlugin returns plugin from marketplace-server
func (a *App) GetMarketplacePlugin(request *model.InstallMarketplacePluginRequest) (*model.BaseMarketplacePlugin, *model.AppError) {
marketplaceClient, err := marketplace.NewClient(
*a.Config().PluginSettings.MarketplaceUrl,
a.HTTPService,
)
if err != nil {
return nil, model.NewAppError("GetMarketplacePlugin", "app.plugin.marketplace_client.app_error", nil, err.Error(), http.StatusInternalServerError)
}
filter := &model.MarketplacePluginFilter{Filter: request.Id}
plugin, err := marketplaceClient.GetPlugin(filter, request.Version)
if err != nil {
return nil, model.NewAppError("GetMarketplacePlugin", "app.plugin.marketplace_plugins.not_found.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return plugin, nil
}
// GetMarketplacePlugins returns a list of plugins from the marketplace-server,
// and plugins that are installed locally.
func (a *App) GetMarketplacePlugins(filter *model.MarketplacePluginFilter) ([]*model.MarketplacePlugin, *model.AppError) {
@@ -565,3 +587,33 @@ func (a *App) notifyPluginEnabled(manifest *model.Manifest) error {
return nil
}
func (a *App) getPluginsFromFolder() (map[string]*pluginSignaturePath, *model.AppError) {
fileStorePaths, appErr := a.ListDirectory(fileStorePluginFolder)
if appErr != nil {
return nil, model.NewAppError("getPluginsFromDir", "app.plugin.sync.list_filestore.app_error", nil, appErr.Error(), http.StatusInternalServerError)
}
pluginSignaturePathMap := make(map[string]*pluginSignaturePath)
for _, path := range fileStorePaths {
if strings.HasSuffix(path, ".tar.gz") {
id := strings.TrimSuffix(filepath.Base(path), ".tar.gz")
helper := &pluginSignaturePath{
pluginId: id,
path: path,
signaturePath: "",
}
pluginSignaturePathMap[id] = helper
}
}
for _, path := range fileStorePaths {
if strings.HasSuffix(path, ".sig") {
id := strings.TrimSuffix(filepath.Base(path), ".sig")
if val, ok := pluginSignaturePathMap[id]; !ok {
mlog.Error("Unknown signature", mlog.String("path", path))
} else {
val.signaturePath = path
}
}
}
return pluginSignaturePathMap, nil
}

Просмотреть файл

@@ -48,6 +48,7 @@ import (
"github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/plugin"
"github.com/mattermost/mattermost-server/services/filesstore"
"github.com/mattermost/mattermost-server/utils"
)
@@ -61,16 +62,38 @@ const fileStorePluginFolder = "plugins"
func (a *App) InstallPluginFromData(data model.PluginEventData) {
mlog.Debug("Installing plugin as per cluster message", mlog.String("plugin_id", data.Id))
fileStorePath := a.getBundleStorePath(data.Id)
reader, appErr := a.FileReader(fileStorePath)
pluginSignaturePathMap, appErr := a.getPluginsFromFolder()
if appErr != nil {
mlog.Error("Failed to open plugin bundle from filestore.", mlog.String("path", fileStorePath), mlog.Err(appErr))
mlog.Error("Failed to get plugin signatures from filestore. Can't install plugin from data.", mlog.Err(appErr))
return
}
plugin, ok := pluginSignaturePathMap[data.Id]
if !ok {
mlog.Error("Failed to get plugin signature from filestore. Can't install plugin from data.", mlog.String("plugin id", data.Id))
return
}
reader, appErr := a.FileReader(plugin.path)
if appErr != nil {
mlog.Error("Failed to open plugin bundle from file store.", mlog.String("bundle", plugin.path), mlog.Err(appErr))
return
}
defer reader.Close()
manifest, appErr := a.installPluginLocally(reader, installPluginLocallyAlways)
var signature filesstore.ReadCloseSeeker
if *a.Config().PluginSettings.RequirePluginSignature {
signature, appErr = a.FileReader(plugin.signaturePath)
if appErr != nil {
mlog.Error("Failed to open plugin signature from file store.", mlog.Err(appErr))
return
}
defer signature.Close()
}
manifest, appErr := a.installPluginLocally(reader, signature, installPluginLocallyAlways)
if appErr != nil {
mlog.Error("Failed to unpack plugin from filestore", mlog.Err(appErr), mlog.String("path", fileStorePath))
mlog.Error("Failed to sync plugin from file store", mlog.String("bundle", plugin.path), mlog.Err(appErr))
return
}
if err := a.notifyPluginEnabled(manifest); err != nil {
@@ -94,6 +117,11 @@ func (a *App) RemovePluginFromData(data model.PluginEventData) {
}
}
// InstallPluginWithSignature verifies and installs plugin.
func (a *App) InstallPluginWithSignature(pluginFile, signature io.ReadSeeker) (*model.Manifest, *model.AppError) {
return a.installPlugin(pluginFile, signature, installPluginLocallyAlways)
}
// InstallPlugin unpacks and installs a plugin but does not enable or activate it.
func (a *App) InstallPlugin(pluginFile io.ReadSeeker, replace bool) (*model.Manifest, *model.AppError) {
installationStrategy := installPluginLocallyOnlyIfNew
@@ -101,14 +129,24 @@ func (a *App) InstallPlugin(pluginFile io.ReadSeeker, replace bool) (*model.Mani
installationStrategy = installPluginLocallyAlways
}
manifest, appErr := a.installPluginLocally(pluginFile, installationStrategy)
return a.installPlugin(pluginFile, nil, installationStrategy)
}
func (a *App) installPlugin(pluginFile, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) {
manifest, appErr := a.installPluginLocally(pluginFile, signature, installationStrategy)
if appErr != nil {
return nil, appErr
}
if signature != nil {
signature.Seek(0, 0)
if _, appErr = a.WriteFile(signature, a.getSignatureStorePath(manifest.Id)); appErr != nil {
return nil, model.NewAppError("saveSignature", "app.plugin.store_signature.app_error", nil, appErr.Error(), http.StatusInternalServerError)
}
}
// Store bundle in the file store to allow access from other servers.
pluginFile.Seek(0, 0)
if _, appErr := a.WriteFile(pluginFile, a.getBundleStorePath(manifest.Id)); appErr != nil {
return nil, model.NewAppError("uploadPlugin", "app.plugin.store_bundle.app_error", nil, appErr.Error(), http.StatusInternalServerError)
}
@@ -142,11 +180,17 @@ const (
installPluginLocallyAlways
)
func (a *App) installPluginLocally(pluginFile io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) {
func (a *App) installPluginLocally(pluginFile, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) {
pluginsEnvironment := a.GetPluginsEnvironment()
if pluginsEnvironment == nil {
return nil, model.NewAppError("installPluginLocally", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
// verify signature
if signature != nil {
if err := a.VerifyPlugin(pluginFile, signature); err != nil {
return nil, err
}
}
tmpDir, err := ioutil.TempDir("", "plugintmp")
if err != nil {
@@ -154,6 +198,7 @@ func (a *App) installPluginLocally(pluginFile io.ReadSeeker, installationStrateg
}
defer os.RemoveAll(tmpDir)
pluginFile.Seek(0, 0)
if err = utils.ExtractTarGz(pluginFile, tmpDir); err != nil {
return nil, model.NewAppError("installPluginLocally", "app.plugin.extract.app_error", nil, err.Error(), http.StatusBadRequest)
}
@@ -282,9 +327,12 @@ func (a *App) removePlugin(id string) *model.AppError {
if !bundleExist {
return nil
}
if err := a.RemoveFile(storePluginFileName); err != nil {
if err = a.RemoveFile(storePluginFileName); err != nil {
return model.NewAppError("removePlugin", "app.plugin.remove_bundle.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if err = a.removeSignature(id); err != nil {
mlog.Error("Can't remove signature", mlog.Err(err))
}
a.notifyClusterPluginEvent(
model.CLUSTER_EVENT_REMOVE_PLUGIN,
@@ -336,6 +384,26 @@ func (a *App) removePluginLocally(id string) *model.AppError {
return nil
}
func (a *App) removeSignature(pluginId string) *model.AppError {
filePath := a.getSignatureStorePath(pluginId)
exists, err := a.FileExists(filePath)
if err != nil {
return model.NewAppError("removeSignature", "app.plugin.remove_bundle.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if !exists {
mlog.Debug("no plugin signature to remove", mlog.String("plugin_id", pluginId))
return nil
}
if err = a.RemoveFile(filePath); err != nil {
return model.NewAppError("removeSignature", "app.plugin.remove_bundle.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return nil
}
func (a *App) getBundleStorePath(id string) string {
return filepath.Join(fileStorePluginFolder, fmt.Sprintf("%s.tar.gz", id))
}
func (a *App) getSignatureStorePath(id string) string {
return filepath.Join(fileStorePluginFolder, fmt.Sprintf("%s.sig", id))
}

Просмотреть файл

@@ -65,7 +65,7 @@ func TestInstallPluginLocally(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
actualManifest, appErr := th.App.installPluginLocally(&nilReadSeeker{}, installPluginLocallyOnlyIfNew)
actualManifest, appErr := th.App.installPluginLocally(&nilReadSeeker{}, nil, installPluginLocallyOnlyIfNew)
require.NotNil(t, appErr)
assert.Equal(t, "app.plugin.extract.app_error", appErr.Id, appErr.Error())
require.Nil(t, actualManifest)
@@ -79,7 +79,7 @@ func TestInstallPluginLocally(t *testing.T) {
{"test", "test file"},
})
actualManifest, appErr := th.App.installPluginLocally(reader, installPluginLocallyOnlyIfNew)
actualManifest, appErr := th.App.installPluginLocally(reader, nil, installPluginLocallyOnlyIfNew)
require.NotNil(t, appErr)
assert.Equal(t, "app.plugin.manifest.app_error", appErr.Id, appErr.Error())
require.Nil(t, actualManifest)
@@ -96,7 +96,7 @@ func TestInstallPluginLocally(t *testing.T) {
{"plugin.json", manifest.ToJson()},
})
actualManifest, appError := th.App.installPluginLocally(reader, installationStrategy)
actualManifest, appError := th.App.installPluginLocally(reader, nil, installationStrategy)
if actualManifest != nil {
require.Equal(t, manifest, actualManifest)
}

46
app/plugin_public_keys.go Обычный файл
Просмотреть файл

@@ -0,0 +1,46 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package app
var mattermostPluginPublicKey []byte = []byte(`-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=qIjw
-----END PGP PUBLIC KEY BLOCK-----`)

132
app/plugin_signature.go Обычный файл
Просмотреть файл

@@ -0,0 +1,132 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"io"
"io/ioutil"
"net/http"
"path/filepath"
"github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils"
"github.com/pkg/errors"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/armor"
)
// GetPluginPublicKeyFiles returns all public keys listed in the config.
func (a *App) GetPluginPublicKeyFiles() ([]string, *model.AppError) {
return a.Config().PluginSettings.SignaturePublicKeyFiles, nil
}
// GetPublicKey will return the actual public key saved in the `name` file.
func (a *App) GetPublicKey(name string) ([]byte, *model.AppError) {
data, err := a.Srv.configStore.GetFile(name)
if err != nil {
return nil, model.NewAppError("GetPublicKey", "app.plugin.get_public_key.get_file.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return data, nil
}
// AddPublicKey will add plugin public key to the config. Overwrites the previous file
func (a *App) AddPublicKey(name string, key io.Reader) *model.AppError {
if model.IsSamlFile(&a.Config().SamlSettings, name) {
return model.NewAppError("AddPublicKey", "app.plugin.modify_saml.app_error", nil, "", http.StatusInternalServerError)
}
data, err := ioutil.ReadAll(key)
if err != nil {
return model.NewAppError("AddPublicKey", "app.plugin.write_file.read.app_error", nil, err.Error(), http.StatusInternalServerError)
}
err = a.Srv.configStore.SetFile(name, data)
if err != nil {
return model.NewAppError("AddPublicKey", "app.plugin.write_file.saving.app_error", nil, err.Error(), http.StatusInternalServerError)
}
a.UpdateConfig(func(cfg *model.Config) {
if !utils.StringInSlice(name, cfg.PluginSettings.SignaturePublicKeyFiles) {
cfg.PluginSettings.SignaturePublicKeyFiles = append(cfg.PluginSettings.SignaturePublicKeyFiles, name)
}
})
return nil
}
// DeletePublicKey will delete plugin public key from the config.
func (a *App) DeletePublicKey(name string) *model.AppError {
if model.IsSamlFile(&a.Config().SamlSettings, name) {
return model.NewAppError("AddPublicKey", "app.plugin.modify_saml.app_error", nil, "", http.StatusInternalServerError)
}
filename := filepath.Base(name)
if err := a.Srv.configStore.RemoveFile(filename); err != nil {
return model.NewAppError("DeletePublicKey", "app.plugin.delete_public_key.delete.app_error", nil, err.Error(), http.StatusInternalServerError)
}
a.UpdateConfig(func(cfg *model.Config) {
cfg.PluginSettings.SignaturePublicKeyFiles = utils.RemoveStringFromSlice(filename, cfg.PluginSettings.SignaturePublicKeyFiles)
})
return nil
}
// VerifyPlugin checks that the given signature corresponds to the given plugin and matches a trusted certificate.
func (a *App) VerifyPlugin(plugin, signature io.ReadSeeker) *model.AppError {
if err := verifySignature(bytes.NewReader(mattermostPluginPublicKey), plugin, signature); err == nil {
return nil
}
publicKeys, appErr := a.GetPluginPublicKeyFiles()
if appErr != nil {
return appErr
}
for _, pk := range publicKeys {
pkBytes, appErr := a.GetPublicKey(pk)
if appErr != nil {
mlog.Error("Unable to get public key for ", mlog.String("filename", pk))
continue
}
publicKey := bytes.NewReader(pkBytes)
plugin.Seek(0, 0)
signature.Seek(0, 0)
if err := verifySignature(publicKey, plugin, signature); err == nil {
return nil
}
}
return model.NewAppError("VerifyPlugin", "api.plugin.verify_plugin.app_error", nil, "", http.StatusInternalServerError)
}
func verifySignature(publicKey, message, signatrue io.Reader) error {
pk, err := decodeIfArmored(publicKey)
if err != nil {
return errors.Wrap(err, "can't decode public key")
}
s, err := decodeIfArmored(signatrue)
if err != nil {
return errors.Wrap(err, "can't decode signature")
}
return verifyBinarySignature(pk, message, s)
}
func verifyBinarySignature(publicKey, signedFile, signature io.Reader) error {
keyring, err := openpgp.ReadKeyRing(publicKey)
if err != nil {
return errors.Wrap(err, "can't read public key")
}
if _, err = openpgp.CheckDetachedSignature(keyring, signedFile, signature); err != nil {
return errors.Wrap(err, "error while checking the signature")
}
return nil
}
func decodeIfArmored(reader io.Reader) (io.Reader, error) {
readBytes, err := ioutil.ReadAll(reader)
if err != nil {
return nil, errors.Wrap(err, "can't read the file")
}
block, err := armor.Decode(bytes.NewReader(readBytes))
if err != nil {
return bytes.NewReader(readBytes), nil
}
return block.Body, nil
}

102
app/plugin_signature_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,102 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package app
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/mattermost/mattermost-server/utils/fileutils"
"github.com/stretchr/testify/require"
)
func TestPluginPublicKeys(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
path, _ := fileutils.FindDir("tests")
publicKeyFilename := "test-public-key.plugin.gpg"
publicKey, err := ioutil.ReadFile(filepath.Join(path, publicKeyFilename))
require.Nil(t, err)
fileReader, err := os.Open(filepath.Join(path, publicKeyFilename))
require.Nil(t, err)
defer fileReader.Close()
th.App.AddPublicKey(publicKeyFilename, fileReader)
file, err := th.App.GetPublicKey(publicKeyFilename)
require.Nil(t, err)
require.Equal(t, publicKey, file)
_, err = th.App.GetPublicKey("wrong file name")
require.NotNil(t, err)
_, err = th.App.GetPublicKey("wrong-file-name.plugin.gpg")
require.NotNil(t, err)
err = th.App.DeletePublicKey("wrong file name")
require.Nil(t, err)
err = th.App.DeletePublicKey("wrong-file-name.plugin.gpg")
require.Nil(t, err)
err = th.App.DeletePublicKey(publicKeyFilename)
require.Nil(t, err)
_, err = th.App.GetPublicKey(publicKeyFilename)
require.NotNil(t, err)
}
func TestVerifySignature(t *testing.T) {
path, _ := fileutils.FindDir("tests")
pluginFilename := "testplugin.tar.gz"
signatureFilename := "testplugin.tar.gz.sig"
armoredSignatureFilename := "testplugin.tar.gz.asc"
publicKeyFilename := "development-public-key.gpg"
armoredPublicKeyFilename := "development-public-key.asc"
t.Run("verify armored signature and armored public key", func(t *testing.T) {
publicKeyFileReader, err := os.Open(filepath.Join(path, armoredPublicKeyFilename))
require.Nil(t, err)
defer publicKeyFileReader.Close()
pluginFileReader, err := os.Open(filepath.Join(path, pluginFilename))
require.Nil(t, err)
defer pluginFileReader.Close()
signatureFileReader, err := os.Open(filepath.Join(path, armoredSignatureFilename))
require.Nil(t, err)
defer signatureFileReader.Close()
require.Nil(t, verifySignature(publicKeyFileReader, pluginFileReader, signatureFileReader))
})
t.Run("verify non armored signature and armored public key", func(t *testing.T) {
publicKeyFileReader, err := os.Open(filepath.Join(path, armoredPublicKeyFilename))
require.Nil(t, err)
defer publicKeyFileReader.Close()
pluginFileReader, err := os.Open(filepath.Join(path, pluginFilename))
require.Nil(t, err)
defer pluginFileReader.Close()
signatureFileReader, err := os.Open(filepath.Join(path, signatureFilename))
require.Nil(t, err)
defer signatureFileReader.Close()
require.Nil(t, verifySignature(publicKeyFileReader, pluginFileReader, signatureFileReader))
})
t.Run("verify armored signature and non armored public key", func(t *testing.T) {
publicKeyFileReader, err := os.Open(filepath.Join(path, publicKeyFilename))
require.Nil(t, err)
defer publicKeyFileReader.Close()
pluginFileReader, err := os.Open(filepath.Join(path, pluginFilename))
require.Nil(t, err)
defer pluginFileReader.Close()
armoredSignatureFileReader, err := os.Open(filepath.Join(path, armoredSignatureFilename))
require.Nil(t, err)
defer armoredSignatureFileReader.Close()
require.Nil(t, verifySignature(publicKeyFileReader, pluginFileReader, armoredSignatureFileReader))
})
t.Run("verify non armored signature and non armored public key", func(t *testing.T) {
publicKeyFileReader, err := os.Open(filepath.Join(path, publicKeyFilename))
require.Nil(t, err)
defer publicKeyFileReader.Close()
pluginFileReader, err := os.Open(filepath.Join(path, pluginFilename))
require.Nil(t, err)
defer pluginFileReader.Close()
signatureFileReader, err := os.Open(filepath.Join(path, signatureFilename))
require.Nil(t, err)
defer signatureFileReader.Close()
require.Nil(t, verifySignature(publicKeyFileReader, pluginFileReader, signatureFileReader))
})
}

Просмотреть файл

@@ -508,6 +508,7 @@ func TestPluginSync(t *testing.T) {
*cfg.PluginSettings.Enable = true
*cfg.PluginSettings.Directory = "./test-plugins"
*cfg.PluginSettings.ClientDirectory = "./test-client-plugins"
*cfg.PluginSettings.RequirePluginSignature = false
})
th.App.UpdateConfig(testCase.ConfigFunc)
@@ -530,7 +531,7 @@ func TestPluginSync(t *testing.T) {
// Check if installed
pluginStatus, err := env.Statuses()
require.Nil(t, err)
require.True(t, len(pluginStatus) == 1)
require.Len(t, pluginStatus, 1)
require.Equal(t, pluginStatus[0].PluginId, "testplugin")
// Bundle removed from the file store case
@@ -543,7 +544,54 @@ func TestPluginSync(t *testing.T) {
// Check if removed
pluginStatus, err = env.Statuses()
require.Nil(t, err)
require.True(t, len(pluginStatus) == 0)
require.Len(t, pluginStatus, 0)
// RequirePluginSignature = true case
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.RequirePluginSignature = true
})
pluginFileReader, err := os.Open(filepath.Join(path, "testplugin.tar.gz"))
require.NoError(t, err)
defer pluginFileReader.Close()
_, appErr = th.App.WriteFile(pluginFileReader, th.App.getBundleStorePath("testplugin.tar.gz"))
checkNoError(t, appErr)
// no signature
appErr = th.App.SyncPlugins()
checkNoError(t, appErr)
pluginStatus, err = env.Statuses()
require.Nil(t, err)
require.Len(t, pluginStatus, 0)
// Wrong signature
signatureFileReader, err := os.Open(filepath.Join(path, "testpluginv2.tar.gz.sig"))
require.NoError(t, err)
defer signatureFileReader.Close()
filePath := fmt.Sprintf("%s.sig", th.App.getBundleStorePath("testplugin"))
_, appErr = th.App.WriteFile(signatureFileReader, filePath)
checkNoError(t, appErr)
appErr = th.App.SyncPlugins()
checkNoError(t, appErr)
pluginStatus, err = env.Statuses()
require.Nil(t, err)
require.Len(t, pluginStatus, 0)
// Correct signature
signatureFileReader, err = os.Open(filepath.Join(path, "testplugin.tar.gz.sig"))
require.NoError(t, err)
defer signatureFileReader.Close()
filePath = fmt.Sprintf("%s.sig", th.App.getBundleStorePath("testplugin"))
_, appErr = th.App.WriteFile(signatureFileReader, filePath)
checkNoError(t, appErr)
appErr = th.App.SyncPlugins()
checkNoError(t, appErr)
pluginStatus, err = env.Statuses()
require.Nil(t, err)
require.Len(t, pluginStatus, 1)
require.Equal(t, pluginStatus[0].PluginId, "testplugin")
})
}
}