* 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>
148 строки
5.3 KiB
Go
148 строки
5.3 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"net/http"
|
|
"path/filepath"
|
|
"slices"
|
|
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/crypto/openpgp" //nolint:staticcheck
|
|
"golang.org/x/crypto/openpgp/armor" //nolint:staticcheck
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
|
|
"github.com/mattermost/mattermost/server/v8/channels/utils"
|
|
)
|
|
|
|
// GetPublicKey will return the actual public key saved in the `name` file.
|
|
func (a *App) GetPublicKey(name string) ([]byte, *model.AppError) {
|
|
return a.Srv().getPublicKey(name)
|
|
}
|
|
|
|
func (s *Server) getPublicKey(name string) ([]byte, *model.AppError) {
|
|
data, err := s.platform.GetConfigFile(name)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetPublicKey", "app.plugin.get_public_key.get_file.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
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 isSamlFile(&a.Config().SamlSettings, name) {
|
|
return model.NewAppError("AddPublicKey", "app.plugin.modify_saml.app_error", nil, "", http.StatusInternalServerError)
|
|
}
|
|
data, err := io.ReadAll(key)
|
|
if err != nil {
|
|
return model.NewAppError("AddPublicKey", "app.plugin.write_file.read.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
err = a.Srv().platform.SetConfigFile(name, data)
|
|
if err != nil {
|
|
return model.NewAppError("AddPublicKey", "app.plugin.write_file.saving.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
a.UpdateConfig(func(cfg *model.Config) {
|
|
if !slices.Contains(cfg.PluginSettings.SignaturePublicKeyFiles, name) {
|
|
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 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().platform.RemoveConfigFile(filename); err != nil {
|
|
return model.NewAppError("DeletePublicKey", "app.plugin.delete_public_key.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
a.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.PluginSettings.SignaturePublicKeyFiles = utils.RemoveStringFromSlice(filename, cfg.PluginSettings.SignaturePublicKeyFiles)
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ch *Channels) verifyPlugin(logger *mlog.Logger, plugin, signature io.ReadSeeker) *model.AppError {
|
|
// First try verifying using the hard-coded public key.
|
|
if err := verifySignature(bytes.NewReader(mattermostPluginPublicKey), plugin, signature); err == nil {
|
|
logger.Debug("Plugin signature verified using hard-coded public key")
|
|
return nil
|
|
}
|
|
|
|
// If that fails, try any of the admin-configured public keys.
|
|
publicKeys := ch.srv.Config().PluginSettings.SignaturePublicKeyFiles
|
|
for _, pk := range publicKeys {
|
|
pkBytes, appErr := ch.srv.getPublicKey(pk)
|
|
if appErr != nil {
|
|
logger.Warn("Unable to read configured signature public key file", mlog.String("public_key_path", pk))
|
|
continue
|
|
}
|
|
publicKey := bytes.NewReader(pkBytes)
|
|
if _, err := plugin.Seek(0, io.SeekStart); err != nil {
|
|
logger.Warn("Unable to seek in public key reader for ", mlog.String("public_key_path", pk))
|
|
continue
|
|
}
|
|
if _, err := signature.Seek(0, io.SeekStart); err != nil {
|
|
logger.Warn("Unable to seek in signature for public key ", mlog.String("public_key_path", pk))
|
|
continue
|
|
}
|
|
if err := verifySignature(publicKey, plugin, signature); err == nil {
|
|
logger.Debug("Plugin signature verified using configured public key", mlog.String("public_key_path", pk))
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return model.NewAppError("VerifyPlugin", "api.plugin.verify_plugin.app_error", nil, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
func verifySignature(publicKey, message, signature io.Reader) error {
|
|
pk, err := decodeIfArmored(publicKey)
|
|
if err != nil {
|
|
return errors.Wrap(err, "can't decode public key")
|
|
}
|
|
s, err := decodeIfArmored(signature)
|
|
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 := io.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
|
|
}
|
|
|
|
// isSamlFile checks if filename is a SAML file.
|
|
func isSamlFile(saml *model.SamlSettings, filename string) bool {
|
|
return filename == *saml.PublicCertificateFile || filename == *saml.PrivateKeyFile || filename == *saml.IdpCertificateFile
|
|
}
|