Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
160 lines
5.9 KiB
Go
160 lines
5.9 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
|
|
}
|
|
|
|
// Next, if enabled, try verifying using the hard-coded MFI public key.
|
|
if ch.srv.Config().FeatureFlags.EnableMFIPluginSignaturePublicKey {
|
|
if _, err := plugin.Seek(0, io.SeekStart); err != nil {
|
|
logger.Warn("Unable to seek in plugin reader for MFI public key")
|
|
} else if _, err := signature.Seek(0, io.SeekStart); err != nil {
|
|
logger.Warn("Unable to seek in signature reader for MFI public key")
|
|
} else if err := verifySignature(bytes.NewReader(mfiPluginPublicKey), plugin, signature); err == nil {
|
|
logger.Debug("Plugin signature verified using hard-coded MFI 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
|
|
}
|