Always require signatures for prepackaged plugins (#31785)

* Always require signatures for prepackaged plugins

We have always required signatures for packages installed via the marketplace -- whether remotely satisfied, or sourced from the prepackaged plugin cache.

However, prepackaged plugins discovered and automatically installed on
startup did not require a valid signature. Since we already ship
signatures for all Mattermost-authored prepackaged plugins, it's easy to
simply start requiring this.

Distributions of Mattermost that bundle their own prepackaged plugins
will have to include their own signatures. This in turn requires
distributing and configuring Mattermost with a custom public key via
`PluginSettings.SignaturePublicKeyFiles`.

Note that this enhanced security is neutered with a deployment that uses
a file-based `config.json`, as any exploit that allows appending to the
prepackaged plugins cache probably also allows modifying `config.json`
to register a new public key. A [database-based
config](https://docs.mattermost.com/configure/configuration-in-your-database.html)
is recommended.

Finally, we already support an optional setting
`PluginSettings.RequirePluginSignature` to always require a plugin
signature, although this effectively disables plugin uploads and
requires extra effort to deploy the corresponding signature. In
environments where only prepackaged plugins are used, this setting is
ideal.

Fixes: https://mattermost.atlassian.net/browse/MM-64627

* setup dev key, expect no plugins if sig fails

* Fix shadow variable errors in test helpers

Pre-declare signaturePublicKey variable in loops to avoid shadowing
the outer err variable used in error handling.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Replace PrepackagedPlugin.Signature with SignaturePath for memory efficiency

- Changed PrepackagedPlugin struct to use SignaturePath string instead of Signature []byte
- Updated buildPrepackagedPlugin to use file descriptor instead of reading signature into memory
- Modified plugin installation and persistence to read from signature file paths
- Updated all tests to check SignaturePath instead of Signature field
- Removed unused bytes import from plugin.go

This change reduces memory usage by storing file paths instead of signature data
in memory while maintaining the same security verification functionality.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Этот коммит содержится в:
Jesse Hallam
2025-06-24 15:11:02 -03:00
коммит произвёл GitHub
родитель e60f878090
Коммит 60a747f975
9 изменённых файлов: 321 добавлений и 102 удалений

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

@@ -4,7 +4,6 @@
package app
import (
"bytes"
"encoding/base64"
"fmt"
"io"
@@ -316,7 +315,11 @@ func (ch *Channels) syncPlugins() *model.AppError {
wg.Add(1)
go func(plugin *pluginSignaturePath) {
defer wg.Done()
logger := ch.srv.Log().With(mlog.String("plugin_id", plugin.pluginID), mlog.String("bundle_path", plugin.bundlePath))
logger := ch.srv.Log().With(
mlog.String("plugin_id", plugin.pluginID),
mlog.String("bundle_path", plugin.bundlePath),
mlog.String("signature_path", plugin.signaturePath),
)
bundle, appErr := ch.srv.fileReader(plugin.bundlePath)
if appErr != nil {
@@ -333,7 +336,7 @@ func (ch *Channels) syncPlugins() *model.AppError {
}
defer signature.Close()
if appErr = ch.verifyPlugin(bundle, signature); appErr != nil {
if appErr = ch.verifyPlugin(logger, bundle, signature); appErr != nil {
logger.Error("Failed to validate plugin signature", mlog.Err(appErr))
return
}
@@ -923,13 +926,19 @@ func (ch *Channels) getPluginsFromFilePaths(fileStorePaths []string) map[string]
// If enabled, prepackaged plugins are installed or upgraded locally. A list of transitionally
// prepackaged plugins is also collected for later persistence to the filestore.
func (ch *Channels) processPrepackagedPlugins(prepackagedPluginsDir string) error {
logger := ch.srv.Log()
logger.Info("Processing prepackaged plugin")
prepackagedPluginsPath, found := fileutils.FindDir(prepackagedPluginsDir)
if !found {
ch.srv.Log().Debug("No prepackaged plugins directory found")
logger.Debug("No prepackaged plugins directory found")
return nil
}
ch.srv.Log().Debug("Processing prepackaged plugins in directory", mlog.String("path", prepackagedPluginsPath))
logger = logger.With(
mlog.String("prepackaged_plugins_path", prepackagedPluginsPath),
)
ch.srv.Log().Debug("Processing prepackaged plugins in directory")
var fileStorePaths []string
err := filepath.Walk(prepackagedPluginsPath, func(walkPath string, info os.FileInfo, err error) error {
@@ -971,7 +980,7 @@ func (ch *Channels) processPrepackagedPlugins(prepackagedPluginsDir string) erro
if errors.As(err, &appErr) && appErr.Id == "app.plugin.skip_installation.app_error" {
return
}
ch.srv.Log().Error("Failed to install prepackaged plugin", mlog.String("bundle_path", psPath.bundlePath), mlog.Err(err))
logger.Error("Failed to install prepackaged plugin", mlog.String("bundle_path", psPath.bundlePath), mlog.Err(err))
return
}
@@ -1004,7 +1013,10 @@ var SemVerV2 = semver.MustParse("2.0.0")
// processPrepackagedPlugin will return the prepackaged plugin metadata and will also
// install the prepackaged plugin if it had been previously enabled and AutomaticPrepackagedPlugins is true.
func (ch *Channels) processPrepackagedPlugin(pluginPath *pluginSignaturePath) (*plugin.PrepackagedPlugin, error) {
logger := ch.srv.Log().With(mlog.String("bundle_path", pluginPath.bundlePath))
logger := ch.srv.Log().With(
mlog.String("bundle_path", pluginPath.bundlePath),
mlog.String("signature_path", pluginPath.signaturePath),
)
logger.Info("Processing prepackaged plugin")
@@ -1020,7 +1032,7 @@ func (ch *Channels) processPrepackagedPlugin(pluginPath *pluginSignaturePath) (*
}
defer os.RemoveAll(tmpDir)
plugin, pluginDir, err := ch.buildPrepackagedPlugin(pluginPath, fileReader, tmpDir)
plugin, pluginDir, err := ch.buildPrepackagedPlugin(logger, pluginPath, fileReader, tmpDir)
if err != nil {
return nil, errors.Wrapf(err, "Failed to get prepackaged plugin %s", pluginPath.bundlePath)
}
@@ -1204,23 +1216,34 @@ func (ch *Channels) persistTransitionallyPrepackagedPlugins() {
go func(p *plugin.PrepackagedPlugin) {
defer wg.Done()
logger := ch.srv.Log().With(mlog.String("plugin_id", p.Manifest.Id), mlog.String("version", p.Manifest.Version))
logger := ch.srv.Log().With(
mlog.String("plugin_id", p.Manifest.Id),
mlog.String("version", p.Manifest.Version),
mlog.String("bundle_path", p.Path),
mlog.String("signature_path", p.SignaturePath),
)
logger.Info("Persisting transitionally prepackaged plugin")
bundleReader, err := os.Open(p.Path)
if err != nil {
logger.Error("Failed to read transitionally prepackaged plugin", mlog.Err(err))
return
}
defer bundleReader.Close()
signatureReader := bytes.NewReader(p.Signature)
signatureReader, err := os.Open(p.SignaturePath)
if err != nil {
logger.Error("Failed to read transitionally prepackaged plugin signature", mlog.Err(err))
return
}
defer signatureReader.Close()
// Write the plugin to the filestore, but don't bother notifying the peers,
// as there's no reason to reload the plugin to run the same version again.
appErr := ch.installPluginToFilestore(p.Manifest, bundleReader, signatureReader)
if appErr != nil {
logger.Error("Failed to persist transitionally prepackaged plugin", mlog.Err(err))
logger.Error("Failed to persist transitionally prepackaged plugin", mlog.Err(appErr))
}
}(p)
}
@@ -1231,7 +1254,31 @@ func (ch *Channels) persistTransitionallyPrepackagedPlugins() {
}
// buildPrepackagedPlugin builds a PrepackagedPlugin from the plugin at the given path, additionally returning the directory in which it was extracted.
func (ch *Channels) buildPrepackagedPlugin(pluginPath *pluginSignaturePath, pluginFile io.ReadSeeker, tmpDir string) (*plugin.PrepackagedPlugin, string, error) {
func (ch *Channels) buildPrepackagedPlugin(logger *mlog.Logger, pluginPath *pluginSignaturePath, pluginFile io.ReadSeeker, tmpDir string) (*plugin.PrepackagedPlugin, string, error) {
// Always require signature for prepackaged plugins
if pluginPath.signaturePath == "" {
return nil, "", errors.Errorf("Prepackaged plugin missing required signature file")
}
// Open signature file
signatureFile, sigErr := os.Open(pluginPath.signaturePath)
if sigErr != nil {
return nil, "", errors.Wrapf(sigErr, "Failed to open prepackaged plugin signature %s", pluginPath.signaturePath)
}
defer signatureFile.Close()
// Verify signature extraction
if _, err := pluginFile.Seek(0, io.SeekStart); err != nil {
return nil, "", errors.Wrapf(err, "Failed to seek to start of plugin file for signature verification: %s", pluginPath.bundlePath)
}
if appErr := ch.verifyPlugin(logger, pluginFile, signatureFile); appErr != nil {
return nil, "", errors.Wrapf(appErr, "Prepackaged plugin signature verification failed for %s using %s", pluginPath.bundlePath, pluginPath.signaturePath)
}
// Extract plugin after signature verification
if _, err := pluginFile.Seek(0, io.SeekStart); err != nil {
return nil, "", errors.Wrapf(err, "Failed to seek to start of plugin file for extraction: %s", pluginPath.bundlePath)
}
manifest, pluginDir, appErr := extractPlugin(pluginFile, tmpDir)
if appErr != nil {
return nil, "", errors.Wrapf(appErr, "Failed to extract plugin with path %s", pluginPath.bundlePath)
@@ -1240,24 +1287,12 @@ func (ch *Channels) buildPrepackagedPlugin(pluginPath *pluginSignaturePath, plug
plugin := new(plugin.PrepackagedPlugin)
plugin.Manifest = manifest
plugin.Path = pluginPath.bundlePath
if pluginPath.signaturePath != "" {
sig := pluginPath.signaturePath
sigReader, sigErr := os.Open(sig)
if sigErr != nil {
return nil, "", errors.Wrapf(sigErr, "Failed to open prepackaged plugin signature %s", sig)
}
bytes, sigErr := io.ReadAll(sigReader)
if sigErr != nil {
return nil, "", errors.Wrapf(sigErr, "Failed to read prepackaged plugin signature %s", sig)
}
plugin.Signature = bytes
}
plugin.SignaturePath = pluginPath.signaturePath
if manifest.IconPath != "" {
iconData, err := getIcon(filepath.Join(pluginDir, manifest.IconPath))
if err != nil {
ch.srv.Log().Warn("Error loading local plugin icon", mlog.String("plugin_id", plugin.Manifest.Id), mlog.String("icon_path", plugin.Manifest.IconPath), mlog.Err(err))
logger.Warn("Error loading local plugin icon", mlog.String("icon_path", plugin.Manifest.IconPath), mlog.Err(err))
}
plugin.IconData = iconData
}