Files
mostlymatter/server/channels/app/plugin_prepackaged_test.go
Jesse Hallam 60a747f975 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>
2025-06-24 15:11:02 -03:00

177 строки
5.3 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/utils/fileutils"
)
func TestBuildPrepackagedPlugin(t *testing.T) {
mainHelper.Parallel(t)
testsPath, found := fileutils.FindDir("tests")
require.True(t, found, "tests directory not found")
// Read public key file once for all subtests
publicKeyData, err := os.ReadFile(filepath.Join(testsPath, "development-public-key.asc"))
require.NoError(t, err)
t.Run("valid plugin with signature and icon data", func(t *testing.T) {
th := Setup(t)
defer th.TearDown()
// Import development public key for signature verification
appErr := th.App.AddPublicKey("development-public-key.asc", bytes.NewBuffer(publicKeyData))
require.Nil(t, appErr)
// Create test plugin path
pluginPath := &pluginSignaturePath{
pluginID: "testplugin",
bundlePath: filepath.Join(testsPath, "testplugin.tar.gz"),
signaturePath: filepath.Join(testsPath, "testplugin.tar.gz.sig"),
}
// Open plugin file
pluginFile, err := os.Open(pluginPath.bundlePath)
require.NoError(t, err)
defer pluginFile.Close()
// Create logger
logger := mlog.CreateConsoleTestLogger(t)
// Test buildPrepackagedPlugin
plugin, pluginDir, err := th.App.ch.buildPrepackagedPlugin(logger, pluginPath, pluginFile, t.TempDir())
require.NoError(t, err)
require.NotNil(t, plugin)
require.NotEmpty(t, pluginDir)
// Verify plugin fields
assert.NotNil(t, plugin.Manifest)
assert.Equal(t, pluginPath.bundlePath, plugin.Path)
assert.Equal(t, pluginPath.signaturePath, plugin.SignaturePath)
assert.Equal(t, "testplugin", plugin.Manifest.Id)
// Verify plugin has icon data loaded
assert.Equal(t, "assets/icon.svg", plugin.Manifest.IconPath)
assert.NotEmpty(t, plugin.IconData, "Plugin should have icon data loaded")
})
t.Run("missing signature file", func(t *testing.T) {
th := Setup(t)
defer th.TearDown()
// Create plugin path with empty signature path
pluginPath := &pluginSignaturePath{
pluginID: "testplugin",
bundlePath: filepath.Join(testsPath, "testplugin.tar.gz"),
signaturePath: "", // Empty signature path
}
pluginFile, err := os.Open(pluginPath.bundlePath)
require.NoError(t, err)
defer pluginFile.Close()
logger := mlog.CreateConsoleTestLogger(t)
plugin, pluginDir, err := th.App.ch.buildPrepackagedPlugin(logger, pluginPath, pluginFile, t.TempDir())
require.Error(t, err)
require.Nil(t, plugin)
require.Empty(t, pluginDir)
assert.Contains(t, err.Error(), "Prepackaged plugin missing required signature file")
})
t.Run("nonexistent signature file", func(t *testing.T) {
th := Setup(t)
defer th.TearDown()
pluginPath := &pluginSignaturePath{
pluginID: "testplugin",
bundlePath: filepath.Join(testsPath, "testplugin.tar.gz"),
signaturePath: "/nonexistent/signature.sig",
}
pluginFile, err := os.Open(pluginPath.bundlePath)
require.NoError(t, err)
defer pluginFile.Close()
logger := mlog.CreateConsoleTestLogger(t)
plugin, pluginDir, err := th.App.ch.buildPrepackagedPlugin(logger, pluginPath, pluginFile, t.TempDir())
require.Error(t, err)
require.Nil(t, plugin)
require.Empty(t, pluginDir)
assert.Contains(t, err.Error(), "Failed to open prepackaged plugin signature")
})
t.Run("empty signature file", func(t *testing.T) {
th := Setup(t)
defer th.TearDown()
// Import development public key
appErr := th.App.AddPublicKey("development-public-key.asc", bytes.NewBuffer(publicKeyData))
require.Nil(t, appErr)
// Create empty signature file
tmpSig, err := os.CreateTemp("", "*.sig")
require.NoError(t, err)
tmpSig.Close()
defer os.Remove(tmpSig.Name())
pluginPath := &pluginSignaturePath{
pluginID: "testplugin",
bundlePath: filepath.Join(testsPath, "testplugin.tar.gz"),
signaturePath: tmpSig.Name(),
}
pluginFile, err := os.Open(pluginPath.bundlePath)
require.NoError(t, err)
defer pluginFile.Close()
logger := mlog.CreateConsoleTestLogger(t)
plugin, pluginDir, err := th.App.ch.buildPrepackagedPlugin(logger, pluginPath, pluginFile, t.TempDir())
require.Error(t, err)
require.Nil(t, plugin)
require.Empty(t, pluginDir)
assert.Contains(t, err.Error(), "Prepackaged plugin signature verification failed")
})
t.Run("signature verification failure", func(t *testing.T) {
th := Setup(t)
defer th.TearDown()
// Use mismatched plugin and signature (testplugin.tar.gz with testplugin2.tar.gz.sig)
pluginPath := &pluginSignaturePath{
pluginID: "testplugin",
bundlePath: filepath.Join(testsPath, "testplugin.tar.gz"),
signaturePath: filepath.Join(testsPath, "testplugin2.tar.gz.sig"),
}
pluginFile, err := os.Open(pluginPath.bundlePath)
require.NoError(t, err)
defer pluginFile.Close()
logger := mlog.CreateConsoleTestLogger(t)
plugin, pluginDir, err := th.App.ch.buildPrepackagedPlugin(logger, pluginPath, pluginFile, t.TempDir())
require.Error(t, err)
require.Nil(t, plugin)
require.Empty(t, pluginDir)
assert.Contains(t, err.Error(), "Prepackaged plugin signature verification failed")
})
}