diff --git a/cmd/porter/bundle.go b/cmd/porter/bundle.go index 237e81b78..8c4a537ea 100644 --- a/cmd/porter/bundle.go +++ b/cmd/porter/bundle.go @@ -177,6 +177,7 @@ Note: if overrides for registry/tag/reference are provided, this command only re "viper-key": {"force-overwrite"}, } f.BoolVar(&opts.AutoBuildDisabled, "autobuild-disabled", false, "Do not automatically build the bundle from source when the last build is out-of-date.") + f.BoolVar(&opts.SignBundle, "sign-bundle", false, "Sign the bundle using the configured signing plugin") return &cmd } diff --git a/cmd/porter/installations.go b/cmd/porter/installations.go index 388384972..66848031f 100644 --- a/cmd/porter/installations.go +++ b/cmd/porter/installations.go @@ -255,6 +255,7 @@ The docker driver runs the bundle container using the local Docker host. To use "Create the installation in the specified namespace. Defaults to the global namespace.") f.StringSliceVarP(&opts.Labels, "label", "l", nil, "Associate the specified labels with the installation. May be specified multiple times.") + f.BoolVar(&opts.VerifyBundleBeforeExecution, "verify-bundle", false, "Verify the bundle signature before executing") addBundleActionFlags(f, opts) // Allow configuring the --driver flag with runtime-driver, to avoid conflicts with other commands diff --git a/magefile.go b/magefile.go index 5d5f1532a..c713f2742 100644 --- a/magefile.go +++ b/magefile.go @@ -24,6 +24,9 @@ import ( "get.porter.sh/porter/tests/tester" mageci "github.com/carolynvs/magex/ci" "github.com/carolynvs/magex/mgx" + magepkg "github.com/carolynvs/magex/pkg" + "github.com/carolynvs/magex/pkg/archive" + "github.com/carolynvs/magex/pkg/downloads" "github.com/carolynvs/magex/shx" "github.com/carolynvs/magex/xplat" "github.com/magefile/mage/mg" @@ -567,7 +570,7 @@ func chmodRecursive(name string, mode os.FileMode) error { // Run integration tests (slow). func TestIntegration() { - mg.Deps(tests.EnsureTestCluster, copySchema, TryRegisterLocalHostAlias, BuildTestMixin, BuildTestPlugin) + mg.Deps(tests.EnsureTestCluster, copySchema, TryRegisterLocalHostAlias, BuildTestMixin, BuildTestPlugin, EnsureCosign, EnsureNotation) var run string runTest := os.Getenv("PORTER_RUN_TEST") @@ -723,3 +726,49 @@ func getPorterHome() string { func SetupDCO() error { return git.SetupDCO() } + +func EnsureCosign() { + if ok, _ := magepkg.IsCommandAvailable("cosign", "version", "v2.2.2"); ok { + return + } + + opts := downloads.DownloadOptions{ + UrlTemplate: "https://github.com/sigstore/cosign/releases/download/v{{.VERSION}}/cosign-{{.GOOS}}-{{.GOARCH}}{{.EXT}}", + Name: "cosign", + Version: "2.2.2", + } + + if runtime.GOOS == "windows" { + opts.Ext = ".exe" + } + + err := downloads.DownloadToGopathBin(opts) + mgx.Must(err) +} + +func EnsureNotation() { + if ok, _ := magepkg.IsCommandAvailable("notation", "version", "1.1.0"); ok { + return + } + + target := "notation{{.EXT}}" + if runtime.GOOS == "windows" { + target = "notation.exe" + } + + opts := archive.DownloadArchiveOptions{ + DownloadOptions: downloads.DownloadOptions{ + UrlTemplate: "https://github.com/notaryproject/notation/releases/download/v{{.VERSION}}/notation_{{.VERSION}}_{{.GOOS}}_{{.GOARCH}}{{.EXT}}", + Name: "notation", + Version: "1.1.0", + }, + ArchiveExtensions: map[string]string{ + "linux": ".tar.gz", + "darwin": ".tar.gz", + "windows": ".zip", + }, + TargetFileTemplate: target, + } + err := archive.DownloadToGopathBin(opts) + mgx.Must(err) +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 40c5810c6..9502659d0 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -186,6 +186,18 @@ func (c *Config) GetSecretsPlugin(name string) (SecretsPlugin, error) { return SecretsPlugin{}, errors.New("secrets %q not defined") } +func (c *Config) GetSigningPlugin(name string) (SigningPlugin, error) { + if c != nil { + for _, cs := range c.Data.SigningPlugin { + if cs.Name == name { + return cs, nil + } + } + } + + return SigningPlugin{}, errors.New("signing %q not defined") +} + // GetHomeDir determines the absolute path to the porter home directory. // Hierarchy of checks: // - PORTER_HOME diff --git a/pkg/config/datastore.go b/pkg/config/datastore.go index 3d8e6cb6a..3f0e23e6a 100644 --- a/pkg/config/datastore.go +++ b/pkg/config/datastore.go @@ -64,12 +64,21 @@ type Data struct { // DefaultSecrets to use when one is not specified by a flag. DefaultSecrets string `mapstructure:"default-secrets"` + // DefaultSigningPlugin is the plugin to use when no plugin is specified. + DefaultSigningPlugin string `mapstructure:"default-signing-plugin"` + + // DefaultSigning to use when one is not specified by a flag. + DefaultSigning string `mapstructure:"default-signer"` + // Namespace is the default namespace for commands that do not override it with a flag. Namespace string `mapstructure:"namespace"` // SecretsPlugin defined in the configuration file. SecretsPlugin []SecretsPlugin `mapstructure:"secrets"` + // SigningPlugin defined in the configuration file. + SigningPlugin []SigningPlugin `mapstructure:"signers"` + // Logs are settings related to Porter's log files. Logs LogConfig `mapstructure:"logs"` @@ -94,11 +103,17 @@ func DefaultDataStore() Data { RuntimeDriver: RuntimeDriverDocker, DefaultStoragePlugin: "mongodb-docker", DefaultSecretsPlugin: "host", + DefaultSigningPlugin: "", Logs: LogConfig{Level: "info"}, Verbosity: DefaultVerbosity, } } +// SigningPlugin is the plugin stanza for signing. +type SigningPlugin struct { + PluginConfig `mapstructure:",squash"` +} + // SecretsPlugin is the plugin stanza for secrets. type SecretsPlugin struct { PluginConfig `mapstructure:",squash"` diff --git a/pkg/grpc/portergrpc/portergrpc.go b/pkg/grpc/portergrpc/portergrpc.go index 53f6201a9..f59e42c4b 100644 --- a/pkg/grpc/portergrpc/portergrpc.go +++ b/pkg/grpc/portergrpc/portergrpc.go @@ -8,6 +8,8 @@ import ( "get.porter.sh/porter/pkg/porter" "get.porter.sh/porter/pkg/secrets" secretsplugin "get.porter.sh/porter/pkg/secrets/pluginstore" + "get.porter.sh/porter/pkg/signing" + signingplugin "get.porter.sh/porter/pkg/signing/pluginstore" "get.porter.sh/porter/pkg/storage" storageplugin "get.porter.sh/porter/pkg/storage/pluginstore" "google.golang.org/grpc" @@ -30,7 +32,8 @@ func NewPorterServer(cfg *config.Config) (*PorterServer, error) { func (s *PorterServer) NewConnectionInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { storage := storage.NewPluginAdapter(storageplugin.NewStore(s.PorterConfig)) secretStorage := secrets.NewPluginAdapter(secretsplugin.NewStore(s.PorterConfig)) - p := porter.NewFor(s.PorterConfig, storage, secretStorage) + signer := signing.NewPluginAdapter(signingplugin.NewSigner(s.PorterConfig)) + p := porter.NewFor(s.PorterConfig, storage, secretStorage, signer) if _, err := p.Connect(ctx); err != nil { return nil, err } diff --git a/pkg/porter/helpers.go b/pkg/porter/helpers.go index 922514c78..973a5f291 100644 --- a/pkg/porter/helpers.go +++ b/pkg/porter/helpers.go @@ -22,6 +22,7 @@ import ( "get.porter.sh/porter/pkg/mixin" "get.porter.sh/porter/pkg/plugins" "get.porter.sh/porter/pkg/secrets" + "get.porter.sh/porter/pkg/signing" "get.porter.sh/porter/pkg/storage" "get.porter.sh/porter/pkg/tracing" "get.porter.sh/porter/pkg/yaml" @@ -63,13 +64,14 @@ func NewTestPorter(t *testing.T) *TestPorter { tc := config.NewTestConfig(t) testStore := storage.NewTestStore(tc) testSecrets := secrets.NewTestSecretsProvider() + testSigner := signing.NewTestSigningProvider() testCredentials := storage.NewTestCredentialProviderFor(t, testStore, testSecrets) testParameters := storage.NewTestParameterProviderFor(t, testStore, testSecrets) testCache := cache.NewTestCache(cache.New(tc.Config)) testInstallations := storage.NewTestInstallationProviderFor(t, testStore) testRegistry := cnabtooci.NewTestRegistry() - p := NewFor(tc.Config, testStore, testSecrets) + p := NewFor(tc.Config, testStore, testSecrets, testSigner) p.Config = tc.Config p.Mixins = mixin.NewTestMixinProvider() p.Plugins = plugins.NewTestPluginProvider() @@ -113,7 +115,7 @@ func (p *TestPorter) SetupIntegrationTest() context.Context { t := p.TestConfig.TestContext.T // Undo changes above to make a unit test friendly Porter, so we hit the host - p.Porter = NewFor(p.Config, p.TestStore, p.TestSecrets) + p.Porter = NewFor(p.Config, p.TestStore, p.TestSecrets, p.Signer) // Run the test in a temp directory ctx, testDir, _ := p.TestConfig.SetupIntegrationTest() diff --git a/pkg/porter/install.go b/pkg/porter/install.go index 819987286..d49ef3f39 100644 --- a/pkg/porter/install.go +++ b/pkg/porter/install.go @@ -88,6 +88,35 @@ func (p *Porter) InstallBundle(ctx context.Context, opts InstallOptions) error { return fmt.Errorf("error saving installation record: %w", err) } + if opts.VerifyBundleBeforeExecution { + ref, ok, err := i.Bundle.GetBundleReference() + if err != nil { + return err + } + log.Debugf("verifying bundle signature for %s", ref.String()) + if !ok { + return log.Errorf("unable to get reference for bundle %s: %w", ref.String(), err) + } + err = p.Signer.Verify(ctx, ref.String()) + if err != nil { + return log.Errorf("unable to verify signature: %w", err) + } + log.Debugf("bundle signature verified for %s", ref.String()) + + bun, err := opts.GetOptions().GetBundleReference(ctx, p) + if err != nil { + return log.Errorf("unable to get bundle reference") + } + + invocationImage := bun.Definition.InvocationImages[0].Image + log.Debugf("verifying invocation image signature for %s", invocationImage) + err = p.Signer.Verify(ctx, invocationImage) + if err != nil { + return log.Errorf("unable to verify signature: %w", err) + } + log.Debugf("invocation image signature verified for %s", invocationImage) + } + // Run install using the updated installation record return p.ExecuteAction(ctx, i, opts) } diff --git a/pkg/porter/internal_plugins.go b/pkg/porter/internal_plugins.go index 57f45328e..e839622e1 100644 --- a/pkg/porter/internal_plugins.go +++ b/pkg/porter/internal_plugins.go @@ -13,6 +13,9 @@ import ( secretsplugins "get.porter.sh/porter/pkg/secrets/plugins" "get.porter.sh/porter/pkg/secrets/plugins/filesystem" "get.porter.sh/porter/pkg/secrets/plugins/host" + signingplugins "get.porter.sh/porter/pkg/signing/plugins" + "get.porter.sh/porter/pkg/signing/plugins/cosign" + "get.porter.sh/porter/pkg/signing/plugins/notation" storageplugins "get.porter.sh/porter/pkg/storage/plugins" "get.porter.sh/porter/pkg/storage/plugins/mongodb" "get.porter.sh/porter/pkg/storage/plugins/mongodb_docker" @@ -143,5 +146,19 @@ func getInternalPlugins() map[string]InternalPlugin { return mongodb_docker.NewPlugin(c.Context, pluginCfg) }, }, + notation.PluginKey: { + Interface: signingplugins.PluginInterface, + ProtocolVersion: signingplugins.PluginProtocolVersion, + Create: func(c *config.Config, pluginCfg interface{}) (plugin.Plugin, error) { + return notation.NewPlugin(c.Context, pluginCfg) + }, + }, + cosign.PluginKey: { + Interface: signingplugins.PluginInterface, + ProtocolVersion: signingplugins.PluginProtocolVersion, + Create: func(c *config.Config, pluginCfg interface{}) (plugin.Plugin, error) { + return cosign.NewPlugin(c.Context, pluginCfg) + }, + }, } } diff --git a/pkg/porter/lifecycle.go b/pkg/porter/lifecycle.go index ed022f90e..eda08a19b 100644 --- a/pkg/porter/lifecycle.go +++ b/pkg/porter/lifecycle.go @@ -79,6 +79,8 @@ type BundleExecutionOptions struct { // A cache of the final resolved set of parameters that are passed to the bundle // Do not use directly, use GetParameters instead. finalParams map[string]interface{} + + VerifyBundleBeforeExecution bool } func NewBundleExecutionOptions() *BundleExecutionOptions { diff --git a/pkg/porter/porter.go b/pkg/porter/porter.go index 874752dab..92dc0a745 100644 --- a/pkg/porter/porter.go +++ b/pkg/porter/porter.go @@ -17,6 +17,8 @@ import ( "get.porter.sh/porter/pkg/plugins" "get.porter.sh/porter/pkg/secrets" secretsplugin "get.porter.sh/porter/pkg/secrets/pluginstore" + "get.porter.sh/porter/pkg/signing" + signingplugin "get.porter.sh/porter/pkg/signing/pluginstore" "get.porter.sh/porter/pkg/storage" "get.porter.sh/porter/pkg/storage/migrations" storageplugin "get.porter.sh/porter/pkg/storage/pluginstore" @@ -46,6 +48,7 @@ type Porter struct { CNAB cnabprovider.CNABProvider Secrets secrets.Store Storage storage.Provider + Signer signing.Signer } // New porter client, initialized with useful defaults. @@ -53,10 +56,11 @@ func New() *Porter { c := config.New() storage := storage.NewPluginAdapter(storageplugin.NewStore(c)) secretStorage := secrets.NewPluginAdapter(secretsplugin.NewStore(c)) - return NewFor(c, storage, secretStorage) + signer := signing.NewPluginAdapter(signingplugin.NewSigner(c)) + return NewFor(c, storage, secretStorage, signer) } -func NewFor(c *config.Config, store storage.Store, secretStorage secrets.Store) *Porter { +func NewFor(c *config.Config, store storage.Store, secretStorage secrets.Store, signer signing.Signer) *Porter { cache := cache.New(c) storageManager := migrations.NewManager(c, store) @@ -64,6 +68,7 @@ func NewFor(c *config.Config, store storage.Store, secretStorage secrets.Store) credStorage := storage.NewCredentialStore(storageManager, secretStorage) paramStorage := storage.NewParameterStore(storageManager, secretStorage) sanitizerService := storage.NewSanitizer(paramStorage, secretStorage) + storageManager.Initialize(sanitizerService) // we have a bit of a dependency problem here that it would be great to figure out eventually return &Porter{ @@ -80,6 +85,7 @@ func NewFor(c *config.Config, store storage.Store, secretStorage secrets.Store) Plugins: plugins.NewPackageManager(c), CNAB: cnabprovider.NewRuntime(c, installationStorage, credStorage, paramStorage, secretStorage, sanitizerService), Sanitizer: sanitizerService, + Signer: signer, } } @@ -133,6 +139,11 @@ func (p *Porter) Close() error { bigErr = multierror.Append(bigErr, err) } + err = p.Signer.Close() + if err != nil { + bigErr = multierror.Append(bigErr, err) + } + return bigErr.ErrorOrNil() } diff --git a/pkg/porter/publish.go b/pkg/porter/publish.go index b93c296c7..2f3379916 100644 --- a/pkg/porter/publish.go +++ b/pkg/porter/publish.go @@ -31,6 +31,7 @@ type PublishOptions struct { Tag string Registry string ArchiveFile string + SignBundle bool } // Validate performs validation on the publish options @@ -215,6 +216,24 @@ func (p *Porter) publishFromFile(ctx context.Context, opts PublishOptions) error return err } + if opts.SignBundle { + log.Debugf("signing bundle %s", bundleRef.String()) + inImage, err := cnab.CalculateTemporaryImageTag(bundleRef.Reference) + if err != nil { + return log.Errorf("error calculation temporary image tag: %w", err) + } + log.Debugf("Signing invocation image %s.", inImage.String()) + err = p.Signer.Sign(context.Background(), inImage.String()) + if err != nil { + return log.Errorf("error signing invocation image: %w", err) + } + log.Debugf("Signing bundle artifact %s.", bundleRef.Reference.String()) + err = p.Signer.Sign(context.Background(), bundleRef.Reference.String()) + if err != nil { + return log.Errorf("error signing bundle artifact: %w", err) + } + } + // Perhaps we have a cached version of a bundle with the same reference, previously pulled // If so, replace it, as it is most likely out-of-date per this publish err = p.refreshCachedBundle(bundleRef) diff --git a/pkg/signing/helpers.go b/pkg/signing/helpers.go new file mode 100644 index 000000000..697ee1b5b --- /dev/null +++ b/pkg/signing/helpers.go @@ -0,0 +1,19 @@ +package signing + +import "get.porter.sh/porter/pkg/signing/plugins/mock" + +var _ Signer = &TestSigningProvider{} + +type TestSigningProvider struct { + PluginAdapter + + signer *mock.Signer +} + +func NewTestSigningProvider() TestSigningProvider { + signer := mock.NewSigner() + return TestSigningProvider{ + PluginAdapter: NewPluginAdapter(signer), + signer: signer, + } +} diff --git a/pkg/signing/plugin_adapter.go b/pkg/signing/plugin_adapter.go new file mode 100644 index 000000000..1b116a716 --- /dev/null +++ b/pkg/signing/plugin_adapter.go @@ -0,0 +1,40 @@ +package signing + +import ( + "context" + "io" + + "get.porter.sh/porter/pkg/signing/plugins" +) + +var _ Signer = PluginAdapter{} + +// PluginAdapter converts between the low-level plugins.SigningProtocol and +// the signing.Signer interface. +type PluginAdapter struct { + plugin plugins.SigningProtocol +} + +// NewPluginAdapter wraps the specified storage plugin. +func NewPluginAdapter(plugin plugins.SigningProtocol) PluginAdapter { + return PluginAdapter{plugin: plugin} +} + +func (a PluginAdapter) Close() error { + if closer, ok := a.plugin.(io.Closer); ok { + return closer.Close() + } + return nil +} + +func (a PluginAdapter) Sign(ctx context.Context, ref string) error { + return a.plugin.Sign(ctx, ref) +} + +func (a PluginAdapter) Verify(ctx context.Context, ref string) error { + return a.plugin.Verify(ctx, ref) +} + +func (a PluginAdapter) Connect(ctx context.Context) error { + return a.plugin.Connect(ctx) +} diff --git a/pkg/signing/plugins/cosign/cosign.go b/pkg/signing/plugins/cosign/cosign.go new file mode 100644 index 000000000..6dbe81f8d --- /dev/null +++ b/pkg/signing/plugins/cosign/cosign.go @@ -0,0 +1,96 @@ +package cosign + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + + "get.porter.sh/porter/pkg/portercontext" + "get.porter.sh/porter/pkg/signing/plugins" + "get.porter.sh/porter/pkg/tracing" +) + +var _ plugins.SigningProtocol = &Cosign{} + +// Signer implements an in-memory signer for testing. +type Cosign struct { + PublicKey string + PrivateKey string + RegistryMode string + Experimental bool + InsecureRegistry bool +} + +func NewSigner(c *portercontext.Context, cfg PluginConfig) *Cosign { + + s := &Cosign{ + PublicKey: cfg.PublicKey, + PrivateKey: cfg.PrivateKey, + RegistryMode: cfg.RegistryMode, + Experimental: cfg.Experimental, + InsecureRegistry: cfg.InsecureRegistry, + } + + return s +} + +func (s *Cosign) Connect(ctx context.Context) error { + //lint:ignore SA4006 ignore unused ctx for now + ctx, log := tracing.StartSpan(ctx) + defer log.EndSpan() + + if err := exec.Command("cosign", "version").Run(); err != nil { + return errors.New("cosign was not found") + } + + return nil +} + +func (s *Cosign) Sign(ctx context.Context, ref string) error { + //lint:ignore SA4006 ignore unused ctx for now + ctx, log := tracing.StartSpan(ctx) + defer log.EndSpan() + log.Infof("Cosign Signer is Signing %s", ref) + args := []string{"sign", ref, "--tlog-upload=false", "--key", s.PrivateKey, "--yes"} + if s.RegistryMode != "" { + args = append(args, "--registry-referrers-mode", s.RegistryMode) + } + if s.InsecureRegistry { + args = append(args, "--allow-insecure-registry") + } + cmd := exec.Command("cosign", args...) + cmd.Env = append(cmd.Env, os.Environ()...) + if s.Experimental { + cmd.Env = append(cmd.Env, "COSIGN_EXPERIMENTAL=1") + } + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("%s: %w", string(out), err) + } + log.Infof("%s", out) + return nil +} + +func (s *Cosign) Verify(ctx context.Context, ref string) error { + //lint:ignore SA4006 ignore unused ctx for now + ctx, log := tracing.StartSpan(ctx) + defer log.EndSpan() + + log.Infof("Cosign Signer is Verifying %s", ref) + args := []string{"verify", "--key", s.PublicKey, ref, "--insecure-ignore-tlog"} + if s.RegistryMode == "oci-1-1" { + args = append(args, "--experimental-oci11") + } + if s.InsecureRegistry { + args = append(args, "--allow-insecure-registry") + } + cmd := exec.Command("cosign", args...) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("%s: %w", string(out), err) + } + log.Infof("%s", out) + return nil +} diff --git a/pkg/signing/plugins/cosign/doc.go b/pkg/signing/plugins/cosign/doc.go new file mode 100644 index 000000000..8c13a50f3 --- /dev/null +++ b/pkg/signing/plugins/cosign/doc.go @@ -0,0 +1,3 @@ +// Package mock provides an in-memory implementation of a secret store +// suitable for unit testing. +package cosign diff --git a/pkg/signing/plugins/cosign/plugin.go b/pkg/signing/plugins/cosign/plugin.go new file mode 100644 index 000000000..370e7bd4e --- /dev/null +++ b/pkg/signing/plugins/cosign/plugin.go @@ -0,0 +1,40 @@ +package cosign + +import ( + "fmt" + + "get.porter.sh/porter/pkg/portercontext" + "get.porter.sh/porter/pkg/signing" + "get.porter.sh/porter/pkg/signing/plugins" + "get.porter.sh/porter/pkg/signing/pluginstore" + "github.com/hashicorp/go-plugin" + "github.com/mitchellh/mapstructure" +) + +const PluginKey = plugins.PluginInterface + ".porter.cosign" + +var _ plugins.SigningProtocol = &Plugin{} + +type PluginConfig struct { + //theses are paths + PublicKey string `mapstructure:"publickey,omitempty"` + PrivateKey string `mapstructure:"privatekey,omitempty"` + RegistryMode string `mapstructure:"registrymode,omitempty"` + Experimental bool `mapstructure:"experimental,omitempty"` + InsecureRegistry bool `mapstructure:"insecureregistry,omitempty"` +} + +// Plugin is the plugin wrapper for accessing secrets from a local filesystem. +type Plugin struct { + signing.Signer +} + +func NewPlugin(c *portercontext.Context, rawCfg interface{}) (plugin.Plugin, error) { + cfg := PluginConfig{} + if err := mapstructure.Decode(rawCfg, &cfg); err != nil { + return nil, fmt.Errorf("error reading plugin configuration: %w", err) + } + + impl := NewSigner(c, cfg) + return pluginstore.NewPlugin(c, impl), nil +} diff --git a/pkg/signing/plugins/mock/doc.go b/pkg/signing/plugins/mock/doc.go new file mode 100644 index 000000000..0c680d461 --- /dev/null +++ b/pkg/signing/plugins/mock/doc.go @@ -0,0 +1,3 @@ +// Package mock provides an in-memory implementation of a secret store +// suitable for unit testing. +package mock diff --git a/pkg/signing/plugins/mock/mock.go b/pkg/signing/plugins/mock/mock.go new file mode 100644 index 000000000..9effbb300 --- /dev/null +++ b/pkg/signing/plugins/mock/mock.go @@ -0,0 +1,52 @@ +package mock + +import ( + "context" + b64 "encoding/base64" + + "get.porter.sh/porter/pkg/signing/plugins" + "get.porter.sh/porter/pkg/tracing" +) + +var _ plugins.SigningProtocol = &Signer{} + +// Signer implements an in-memory signer for testing. +type Signer struct { + Signatures map[string]string +} + +func NewSigner() *Signer { + s := &Signer{ + Signatures: make(map[string]string), + } + + return s +} + +func (s *Signer) Connect(ctx context.Context) error { + //lint:ignore SA4006 ignore unused ctx for now + ctx, log := tracing.StartSpan(ctx) + defer log.EndSpan() + return nil +} + +func (s *Signer) Sign(ctx context.Context, ref string) error { + //lint:ignore SA4006 ignore unused ctx for now + ctx, log := tracing.StartSpan(ctx) + defer log.EndSpan() + + s.Signatures[ref] = b64.StdEncoding.EncodeToString([]byte(ref)) + return nil +} + +func (s *Signer) Verify(ctx context.Context, ref string) error { + //lint:ignore SA4006 ignore unused ctx for now + ctx, log := tracing.StartSpan(ctx) + defer log.EndSpan() + + if _, ok := s.Signatures[ref]; !ok { + return log.Errorf("%s is not signed", ref) + } + + return nil +} diff --git a/pkg/signing/plugins/notation/doc.go b/pkg/signing/plugins/notation/doc.go new file mode 100644 index 000000000..289752dc0 --- /dev/null +++ b/pkg/signing/plugins/notation/doc.go @@ -0,0 +1,3 @@ +// Package notation provides an implementation of a signing plugin +// using the users's local notation installation +package notation diff --git a/pkg/signing/plugins/notation/notation.go b/pkg/signing/plugins/notation/notation.go new file mode 100644 index 000000000..66cea612c --- /dev/null +++ b/pkg/signing/plugins/notation/notation.go @@ -0,0 +1,78 @@ +package notation + +import ( + "context" + "errors" + "fmt" + "os/exec" + + "get.porter.sh/porter/pkg/portercontext" + "get.porter.sh/porter/pkg/signing/plugins" + "get.porter.sh/porter/pkg/tracing" +) + +var _ plugins.SigningProtocol = &Signer{} + +// Signer implements an in-memory signer for testing. +type Signer struct { + + // Need the key we want to use + SigningKey string + InsecureRegistry bool +} + +func NewSigner(c *portercontext.Context, cfg PluginConfig) *Signer { + s := &Signer{ + SigningKey: cfg.SigningKey, + InsecureRegistry: cfg.InsecureRegistry, + } + return s +} + +func (s *Signer) Connect(ctx context.Context) error { + //lint:ignore SA4006 ignore unused ctx for now + ctx, log := tracing.StartSpan(ctx) + defer log.EndSpan() + + if err := exec.Command("notation", "version").Run(); err != nil { + return errors.New("notation was not found") + } + + return nil +} + +func (s *Signer) Sign(ctx context.Context, ref string) error { + //lint:ignore SA4006 ignore unused ctx for now + ctx, log := tracing.StartSpan(ctx) + defer log.EndSpan() + + args := []string{"sign", ref, "--key", s.SigningKey} + if s.InsecureRegistry { + args = append(args, "--insecure-registry") + } + cmd := exec.Command("notation", args...) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("%s: %w", string(out), err) + } + log.Infof("%s", out) + return nil +} + +func (s *Signer) Verify(ctx context.Context, ref string) error { + //lint:ignore SA4006 ignore unused ctx for now + ctx, log := tracing.StartSpan(ctx) + defer log.EndSpan() + + args := []string{"verify", ref} + if s.InsecureRegistry { + args = append(args, "--insecure-registry") + } + cmd := exec.Command("notation", args...) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("%s: %w", string(out), err) + } + log.Infof("%s", out) + return nil +} diff --git a/pkg/signing/plugins/notation/plugin.go b/pkg/signing/plugins/notation/plugin.go new file mode 100644 index 000000000..03b9c9cfa --- /dev/null +++ b/pkg/signing/plugins/notation/plugin.go @@ -0,0 +1,36 @@ +package notation + +import ( + "fmt" + + "get.porter.sh/porter/pkg/portercontext" + "get.porter.sh/porter/pkg/signing" + "get.porter.sh/porter/pkg/signing/plugins" + "get.porter.sh/porter/pkg/signing/pluginstore" + "github.com/hashicorp/go-plugin" + "github.com/mitchellh/mapstructure" +) + +const PluginKey = plugins.PluginInterface + ".porter.notation" + +var _ plugins.SigningProtocol = &Plugin{} + +type PluginConfig struct { + SigningKey string `mapstructure:"key,omitempty"` + InsecureRegistry bool `mapstructure:"insecureregistry,omitempty"` +} + +// Plugin is the plugin wrapper for accessing secrets from a local filesystem. +type Plugin struct { + signing.Signer +} + +func NewPlugin(c *portercontext.Context, rawCfg interface{}) (plugin.Plugin, error) { + cfg := PluginConfig{} + if err := mapstructure.Decode(rawCfg, &cfg); err != nil { + return nil, fmt.Errorf("error reading plugin configuration: %w", err) + } + + impl := NewSigner(c, cfg) + return pluginstore.NewPlugin(c, impl), nil +} diff --git a/pkg/signing/plugins/proto/doc.go b/pkg/signing/plugins/proto/doc.go new file mode 100644 index 000000000..2062163e9 --- /dev/null +++ b/pkg/signing/plugins/proto/doc.go @@ -0,0 +1,4 @@ +//go:generate protoc pkg/signing/plugins/proto/signing_protocol.proto --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative --proto_path=. + +// Package proto is the protobuf definition for the SigningProtocol +package proto diff --git a/pkg/signing/plugins/proto/signing_protocol.pb.go b/pkg/signing/plugins/proto/signing_protocol.pb.go new file mode 100644 index 000000000..a18e5bac4 --- /dev/null +++ b/pkg/signing/plugins/proto/signing_protocol.pb.go @@ -0,0 +1,433 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v3.12.4 +// source: signing_protocol.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SignRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ref string `protobuf:"bytes,1,opt,name=Ref,proto3" json:"Ref,omitempty"` +} + +func (x *SignRequest) Reset() { + *x = SignRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_signing_protocol_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SignRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignRequest) ProtoMessage() {} + +func (x *SignRequest) ProtoReflect() protoreflect.Message { + mi := &file_signing_protocol_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignRequest.ProtoReflect.Descriptor instead. +func (*SignRequest) Descriptor() ([]byte, []int) { + return file_signing_protocol_proto_rawDescGZIP(), []int{0} +} + +func (x *SignRequest) GetRef() string { + if x != nil { + return x.Ref + } + return "" +} + +type VerifyRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ref string `protobuf:"bytes,1,opt,name=Ref,proto3" json:"Ref,omitempty"` +} + +func (x *VerifyRequest) Reset() { + *x = VerifyRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_signing_protocol_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VerifyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VerifyRequest) ProtoMessage() {} + +func (x *VerifyRequest) ProtoReflect() protoreflect.Message { + mi := &file_signing_protocol_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VerifyRequest.ProtoReflect.Descriptor instead. +func (*VerifyRequest) Descriptor() ([]byte, []int) { + return file_signing_protocol_proto_rawDescGZIP(), []int{1} +} + +func (x *VerifyRequest) GetRef() string { + if x != nil { + return x.Ref + } + return "" +} + +type ConnectRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *ConnectRequest) Reset() { + *x = ConnectRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_signing_protocol_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ConnectRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConnectRequest) ProtoMessage() {} + +func (x *ConnectRequest) ProtoReflect() protoreflect.Message { + mi := &file_signing_protocol_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConnectRequest.ProtoReflect.Descriptor instead. +func (*ConnectRequest) Descriptor() ([]byte, []int) { + return file_signing_protocol_proto_rawDescGZIP(), []int{2} +} + +type SignResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *SignResponse) Reset() { + *x = SignResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_signing_protocol_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SignResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignResponse) ProtoMessage() {} + +func (x *SignResponse) ProtoReflect() protoreflect.Message { + mi := &file_signing_protocol_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignResponse.ProtoReflect.Descriptor instead. +func (*SignResponse) Descriptor() ([]byte, []int) { + return file_signing_protocol_proto_rawDescGZIP(), []int{3} +} + +type VerifyResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *VerifyResponse) Reset() { + *x = VerifyResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_signing_protocol_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VerifyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VerifyResponse) ProtoMessage() {} + +func (x *VerifyResponse) ProtoReflect() protoreflect.Message { + mi := &file_signing_protocol_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VerifyResponse.ProtoReflect.Descriptor instead. +func (*VerifyResponse) Descriptor() ([]byte, []int) { + return file_signing_protocol_proto_rawDescGZIP(), []int{4} +} + +type ConnectResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *ConnectResponse) Reset() { + *x = ConnectResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_signing_protocol_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ConnectResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConnectResponse) ProtoMessage() {} + +func (x *ConnectResponse) ProtoReflect() protoreflect.Message { + mi := &file_signing_protocol_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConnectResponse.ProtoReflect.Descriptor instead. +func (*ConnectResponse) Descriptor() ([]byte, []int) { + return file_signing_protocol_proto_rawDescGZIP(), []int{5} +} + +var File_signing_protocol_proto protoreflect.FileDescriptor + +var file_signing_protocol_proto_rawDesc = []byte{ + 0x0a, 0x16, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x73, 0x22, 0x1f, 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x10, 0x0a, 0x03, 0x52, 0x65, 0x66, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x52, + 0x65, 0x66, 0x22, 0x21, 0x0a, 0x0d, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x52, 0x65, 0x66, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x52, 0x65, 0x66, 0x22, 0x10, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0e, 0x0a, 0x0c, 0x53, 0x69, 0x67, 0x6e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x10, 0x0a, 0x0e, 0x56, 0x65, 0x72, 0x69, 0x66, + 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x11, 0x0a, 0x0f, 0x43, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xbf, 0x01, 0x0a, + 0x0f, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, + 0x12, 0x33, 0x0a, 0x04, 0x53, 0x69, 0x67, 0x6e, 0x12, 0x14, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x73, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, + 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x06, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x12, + 0x16, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x73, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x3c, 0x0a, 0x07, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x12, 0x17, 0x2e, 0x70, 0x6c, + 0x75, 0x67, 0x69, 0x6e, 0x73, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x2e, 0x43, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x30, + 0x5a, 0x2e, 0x67, 0x65, 0x74, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x72, 0x2e, 0x73, 0x68, 0x2f, + 0x70, 0x6f, 0x72, 0x74, 0x65, 0x72, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x69, 0x67, 0x6e, 0x69, + 0x6e, 0x67, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_signing_protocol_proto_rawDescOnce sync.Once + file_signing_protocol_proto_rawDescData = file_signing_protocol_proto_rawDesc +) + +func file_signing_protocol_proto_rawDescGZIP() []byte { + file_signing_protocol_proto_rawDescOnce.Do(func() { + file_signing_protocol_proto_rawDescData = protoimpl.X.CompressGZIP(file_signing_protocol_proto_rawDescData) + }) + return file_signing_protocol_proto_rawDescData +} + +var file_signing_protocol_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_signing_protocol_proto_goTypes = []interface{}{ + (*SignRequest)(nil), // 0: plugins.SignRequest + (*VerifyRequest)(nil), // 1: plugins.VerifyRequest + (*ConnectRequest)(nil), // 2: plugins.ConnectRequest + (*SignResponse)(nil), // 3: plugins.SignResponse + (*VerifyResponse)(nil), // 4: plugins.VerifyResponse + (*ConnectResponse)(nil), // 5: plugins.ConnectResponse +} +var file_signing_protocol_proto_depIdxs = []int32{ + 0, // 0: plugins.SigningProtocol.Sign:input_type -> plugins.SignRequest + 1, // 1: plugins.SigningProtocol.Verify:input_type -> plugins.VerifyRequest + 2, // 2: plugins.SigningProtocol.Connect:input_type -> plugins.ConnectRequest + 3, // 3: plugins.SigningProtocol.Sign:output_type -> plugins.SignResponse + 4, // 4: plugins.SigningProtocol.Verify:output_type -> plugins.VerifyResponse + 5, // 5: plugins.SigningProtocol.Connect:output_type -> plugins.ConnectResponse + 3, // [3:6] is the sub-list for method output_type + 0, // [0:3] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_signing_protocol_proto_init() } +func file_signing_protocol_proto_init() { + if File_signing_protocol_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_signing_protocol_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SignRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_signing_protocol_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VerifyRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_signing_protocol_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ConnectRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_signing_protocol_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SignResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_signing_protocol_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VerifyResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_signing_protocol_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ConnectResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_signing_protocol_proto_rawDesc, + NumEnums: 0, + NumMessages: 6, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_signing_protocol_proto_goTypes, + DependencyIndexes: file_signing_protocol_proto_depIdxs, + MessageInfos: file_signing_protocol_proto_msgTypes, + }.Build() + File_signing_protocol_proto = out.File + file_signing_protocol_proto_rawDesc = nil + file_signing_protocol_proto_goTypes = nil + file_signing_protocol_proto_depIdxs = nil +} diff --git a/pkg/signing/plugins/proto/signing_protocol.proto b/pkg/signing/plugins/proto/signing_protocol.proto new file mode 100644 index 000000000..cf728cfdb --- /dev/null +++ b/pkg/signing/plugins/proto/signing_protocol.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; +package plugins; + +option go_package = "get.porter.sh/porter/pkg/signing/plugins/proto"; + +message SignRequest { + string Ref = 1; +} + +message VerifyRequest { + string Ref = 1; +} + +message ConnectRequest {} + +message SignResponse {} + +message VerifyResponse {} + +message ConnectResponse {} + +service SigningProtocol { + rpc Sign(SignRequest) returns (SignResponse); + rpc Verify(VerifyRequest) returns (VerifyResponse); + rpc Connect(ConnectRequest) returns (ConnectResponse); +} \ No newline at end of file diff --git a/pkg/signing/plugins/proto/signing_protocol_grpc.pb.go b/pkg/signing/plugins/proto/signing_protocol_grpc.pb.go new file mode 100644 index 000000000..3d5cb293e --- /dev/null +++ b/pkg/signing/plugins/proto/signing_protocol_grpc.pb.go @@ -0,0 +1,177 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v3.12.4 +// source: signing_protocol.proto + +package proto + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// SigningProtocolClient is the client API for SigningProtocol service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type SigningProtocolClient interface { + Sign(ctx context.Context, in *SignRequest, opts ...grpc.CallOption) (*SignResponse, error) + Verify(ctx context.Context, in *VerifyRequest, opts ...grpc.CallOption) (*VerifyResponse, error) + Connect(ctx context.Context, in *ConnectRequest, opts ...grpc.CallOption) (*ConnectResponse, error) +} + +type signingProtocolClient struct { + cc grpc.ClientConnInterface +} + +func NewSigningProtocolClient(cc grpc.ClientConnInterface) SigningProtocolClient { + return &signingProtocolClient{cc} +} + +func (c *signingProtocolClient) Sign(ctx context.Context, in *SignRequest, opts ...grpc.CallOption) (*SignResponse, error) { + out := new(SignResponse) + err := c.cc.Invoke(ctx, "/plugins.SigningProtocol/Sign", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *signingProtocolClient) Verify(ctx context.Context, in *VerifyRequest, opts ...grpc.CallOption) (*VerifyResponse, error) { + out := new(VerifyResponse) + err := c.cc.Invoke(ctx, "/plugins.SigningProtocol/Verify", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *signingProtocolClient) Connect(ctx context.Context, in *ConnectRequest, opts ...grpc.CallOption) (*ConnectResponse, error) { + out := new(ConnectResponse) + err := c.cc.Invoke(ctx, "/plugins.SigningProtocol/Connect", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// SigningProtocolServer is the server API for SigningProtocol service. +// All implementations must embed UnimplementedSigningProtocolServer +// for forward compatibility +type SigningProtocolServer interface { + Sign(context.Context, *SignRequest) (*SignResponse, error) + Verify(context.Context, *VerifyRequest) (*VerifyResponse, error) + Connect(context.Context, *ConnectRequest) (*ConnectResponse, error) + mustEmbedUnimplementedSigningProtocolServer() +} + +// UnimplementedSigningProtocolServer must be embedded to have forward compatible implementations. +type UnimplementedSigningProtocolServer struct { +} + +func (UnimplementedSigningProtocolServer) Sign(context.Context, *SignRequest) (*SignResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Sign not implemented") +} +func (UnimplementedSigningProtocolServer) Verify(context.Context, *VerifyRequest) (*VerifyResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Verify not implemented") +} +func (UnimplementedSigningProtocolServer) Connect(context.Context, *ConnectRequest) (*ConnectResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Connect not implemented") +} +func (UnimplementedSigningProtocolServer) mustEmbedUnimplementedSigningProtocolServer() {} + +// UnsafeSigningProtocolServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to SigningProtocolServer will +// result in compilation errors. +type UnsafeSigningProtocolServer interface { + mustEmbedUnimplementedSigningProtocolServer() +} + +func RegisterSigningProtocolServer(s grpc.ServiceRegistrar, srv SigningProtocolServer) { + s.RegisterService(&SigningProtocol_ServiceDesc, srv) +} + +func _SigningProtocol_Sign_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SignRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SigningProtocolServer).Sign(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/plugins.SigningProtocol/Sign", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SigningProtocolServer).Sign(ctx, req.(*SignRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SigningProtocol_Verify_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(VerifyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SigningProtocolServer).Verify(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/plugins.SigningProtocol/Verify", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SigningProtocolServer).Verify(ctx, req.(*VerifyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SigningProtocol_Connect_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ConnectRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SigningProtocolServer).Connect(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/plugins.SigningProtocol/Connect", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SigningProtocolServer).Connect(ctx, req.(*ConnectRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// SigningProtocol_ServiceDesc is the grpc.ServiceDesc for SigningProtocol service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var SigningProtocol_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "plugins.SigningProtocol", + HandlerType: (*SigningProtocolServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Sign", + Handler: _SigningProtocol_Sign_Handler, + }, + { + MethodName: "Verify", + Handler: _SigningProtocol_Verify_Handler, + }, + { + MethodName: "Connect", + Handler: _SigningProtocol_Connect_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "signing_protocol.proto", +} diff --git a/pkg/signing/plugins/signing_plugin.go b/pkg/signing/plugins/signing_plugin.go new file mode 100644 index 000000000..faaa18b4d --- /dev/null +++ b/pkg/signing/plugins/signing_plugin.go @@ -0,0 +1,19 @@ +package plugins + +import "errors" + +const ( + // PluginInterface for signing. This first part of the + // three-part plugin key is only seen/used by the plugins when the host is + // communicating with the plugin and is not exposed to users. + PluginInterface = "signing" + + // PluginProtocolVersion is the currently supported plugin protocol version for secrets. + PluginProtocolVersion = 1 +) + +var ( + // ErrNotImplemented is the error to be returned if a method is not implemented + // in a secret plugin + ErrNotImplemented = errors.New("not implemented") +) diff --git a/pkg/signing/plugins/signing_protocol.go b/pkg/signing/plugins/signing_protocol.go new file mode 100644 index 000000000..24e8d29f0 --- /dev/null +++ b/pkg/signing/plugins/signing_protocol.go @@ -0,0 +1,17 @@ +package plugins + +import "context" + +// SigningProtocol is the interface that signing plugins must implement. +// This defines the protocol used to communicate with signing plugins. +type SigningProtocol interface { + Connect(ctx context.Context) error + + // Resolve a secret value from a secret store + // - ref is OCI reference to verify + Sign(ctx context.Context, ref string) error + + // Verify attempts to verify the signature of a given reference + // - ref is OCI reference to verify + Verify(ctx context.Context, ref string) error +} diff --git a/pkg/signing/pluginstore/doc.go b/pkg/signing/pluginstore/doc.go new file mode 100644 index 000000000..c129a9004 --- /dev/null +++ b/pkg/signing/pluginstore/doc.go @@ -0,0 +1,3 @@ +// Package pluginstore is an internal Porter package that implements the +// plugins.SigningPlugin interface. +package pluginstore diff --git a/pkg/signing/pluginstore/grpc.go b/pkg/signing/pluginstore/grpc.go new file mode 100644 index 000000000..34127794f --- /dev/null +++ b/pkg/signing/pluginstore/grpc.go @@ -0,0 +1,81 @@ +package pluginstore + +import ( + "context" + + "get.porter.sh/porter/pkg/portercontext" + "get.porter.sh/porter/pkg/signing/plugins" + "get.porter.sh/porter/pkg/signing/plugins/proto" +) + +var _ plugins.SigningProtocol = &GClient{} + +// GClient is a gRPC implementation of the signing client. +type GClient struct { + client proto.SigningProtocolClient +} + +func NewClient(client proto.SigningProtocolClient) *GClient { + return &GClient{client} +} + +func (m *GClient) Sign(ctx context.Context, ref string) error { + req := &proto.SignRequest{ + Ref: ref, + } + + _, err := m.client.Sign(ctx, req) + if err != nil { + return err + } + return nil +} + +func (m *GClient) Verify(ctx context.Context, ref string) error { + req := &proto.VerifyRequest{ + Ref: ref, + } + _, err := m.client.Verify(ctx, req) + return err +} + +func (m *GClient) Connect(ctx context.Context) error { + req := &proto.ConnectRequest{} + _, err := m.client.Connect(ctx, req) + return err +} + +// GServer is a gRPC wrapper around a SecretsProtocol plugin +type GServer struct { + c *portercontext.Context + impl plugins.SigningProtocol + proto.UnsafeSigningProtocolServer +} + +func NewServer(c *portercontext.Context, impl plugins.SigningProtocol) *GServer { + return &GServer{c: c, impl: impl} +} + +func (m *GServer) Sign(ctx context.Context, request *proto.SignRequest) (*proto.SignResponse, error) { + err := m.impl.Sign(ctx, request.Ref) + if err != nil { + return nil, err + } + return &proto.SignResponse{}, nil +} + +func (m *GServer) Verify(ctx context.Context, request *proto.VerifyRequest) (*proto.VerifyResponse, error) { + err := m.impl.Verify(ctx, request.Ref) + if err != nil { + return nil, err + } + return &proto.VerifyResponse{}, nil +} + +func (m *GServer) Connect(ctx context.Context, request *proto.ConnectRequest) (*proto.ConnectResponse, error) { + err := m.impl.Connect(ctx) + if err != nil { + return nil, err + } + return &proto.ConnectResponse{}, nil +} diff --git a/pkg/signing/pluginstore/plugin.go b/pkg/signing/pluginstore/plugin.go new file mode 100644 index 000000000..b7026fea5 --- /dev/null +++ b/pkg/signing/pluginstore/plugin.go @@ -0,0 +1,38 @@ +package pluginstore + +import ( + "context" + + "get.porter.sh/porter/pkg/portercontext" + "get.porter.sh/porter/pkg/signing/plugins" + "get.porter.sh/porter/pkg/signing/plugins/proto" + "github.com/hashicorp/go-plugin" + "google.golang.org/grpc" +) + +var _ plugin.GRPCPlugin = Plugin{} + +// Plugin is the shared implementation of a storage plugin wrapper. +type Plugin struct { + plugin.Plugin + impl plugins.SigningProtocol + context *portercontext.Context +} + +// NewPlugin creates an instance of a storage plugin. +func NewPlugin(c *portercontext.Context, impl plugins.SigningProtocol) Plugin { + return Plugin{ + context: c, + impl: impl, + } +} + +func (p Plugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { + impl := NewServer(p.context, p.impl) + proto.RegisterSigningProtocolServer(s, impl) + return nil +} + +func (p Plugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, conn *grpc.ClientConn) (interface{}, error) { + return NewClient(proto.NewSigningProtocolClient(conn)), nil +} diff --git a/pkg/signing/pluginstore/signer.go b/pkg/signing/pluginstore/signer.go new file mode 100644 index 000000000..e564fcb2d --- /dev/null +++ b/pkg/signing/pluginstore/signer.go @@ -0,0 +1,125 @@ +package pluginstore + +import ( + "context" + "errors" + "fmt" + + "get.porter.sh/porter/pkg/config" + "get.porter.sh/porter/pkg/plugins/pluggable" + "get.porter.sh/porter/pkg/signing/plugins" + "get.porter.sh/porter/pkg/tracing" + "go.opentelemetry.io/otel/attribute" +) + +var _ plugins.SigningProtocol = &Signer{} + +// Signer is a plugin-backed source of signing. It resolves the appropriate +// plugin based on Porter's config and implements the plugins.SigningProtocol interface +// using the backing plugin. +// +// Connects just-in-time, but you must call Close to release resources. +type Signer struct { + *config.Config + plugin plugins.SigningProtocol + conn *pluggable.PluginConnection +} + +func NewSigner(c *config.Config) *Signer { + return &Signer{ + Config: c, + } +} + +// NewSigningPluginConfig for signing sources. +func NewSigningPluginConfig() pluggable.PluginTypeConfig { + return pluggable.PluginTypeConfig{ + Interface: plugins.PluginInterface, + Plugin: &Plugin{}, + GetDefaultPluggable: func(c *config.Config) string { + return c.Data.DefaultSigning + }, + GetPluggable: func(c *config.Config, name string) (pluggable.Entry, error) { + return c.GetSigningPlugin(name) + }, + GetDefaultPlugin: func(c *config.Config) string { + return c.Data.DefaultSigningPlugin + }, + ProtocolVersion: plugins.PluginProtocolVersion, + } +} + +func (s *Signer) Sign(ctx context.Context, ref string) error { + ctx, span := tracing.StartSpan(ctx, + attribute.String("ref", ref)) + defer span.EndSpan() + + if err := s.Connect(ctx); err != nil { + return err + } + + err := s.plugin.Sign(ctx, ref) + if err != nil { + return span.Error(err) + } + return nil +} + +func (s *Signer) Verify(ctx context.Context, ref string) error { + ctx, span := tracing.StartSpan(ctx, + attribute.String("ref", ref)) + defer span.EndSpan() + + if err := s.Connect(ctx); err != nil { + return err + } + + err := s.plugin.Verify(ctx, ref) + if errors.Is(err, plugins.ErrNotImplemented) { + return span.Error(fmt.Errorf(`the current signing plugin does not support verifying signatures. You need to edit your porter configuration file and configure a different signing plugin: %w`, err)) + } + + return span.Error(err) +} + +// Connect initializes the plugin for use. +// The plugin itself is responsible for ensuring it was called. +// Close is called automatically when the plugin is used by Porter. +func (s *Signer) Connect(ctx context.Context) error { + if s.plugin != nil { + return nil + } + + ctx, span := tracing.StartSpan(ctx) + defer span.EndSpan() + + pluginType := NewSigningPluginConfig() + + l := pluggable.NewPluginLoader(s.Config) + conn, err := l.Load(ctx, pluginType) + if err != nil { + return span.Error(err) + } + s.conn = conn + + store, ok := conn.GetClient().(plugins.SigningProtocol) + if !ok { + conn.Close(ctx) + return span.Error(fmt.Errorf("the interface (%T) exposed by the %s plugin was not plugins.SigningProtocol", conn.GetClient(), conn)) + } + s.plugin = store + + if err = s.plugin.Connect(ctx); err != nil { + return err + } + + return nil +} + +func (s *Signer) Close() error { + if s.conn != nil { + s.conn.Close(context.Background()) + s.conn = nil + } + return nil +} diff --git a/pkg/signing/signer.go b/pkg/signing/signer.go new file mode 100644 index 000000000..d4cc42792 --- /dev/null +++ b/pkg/signing/signer.go @@ -0,0 +1,18 @@ +package signing + +import "context" + +// Signer is an interface for signing and verifying Porter +// bundles and invocation images. +type Signer interface { + Close() error + + // Sign generates a signature for the specified reference, which + // can be a Porter bundle or an invocation image. + Sign(ctx context.Context, ref string) error + // Verify attempts to verify a signature for the specified + // reference, which can be a Porter bundle or an invocation image. + Verify(ctx context.Context, ref string) error + // TODO + Connect(ctx context.Context) error +} diff --git a/tests/integration/signing_test.go b/tests/integration/signing_test.go new file mode 100644 index 000000000..f2d0174cd --- /dev/null +++ b/tests/integration/signing_test.go @@ -0,0 +1,138 @@ +//go:build integration + +package integration + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "testing" + + "get.porter.sh/porter/pkg/cnab" + "get.porter.sh/porter/tests/tester" + "github.com/carolynvs/magex/shx" + "github.com/docker/distribution/reference" + "github.com/google/go-containerregistry/pkg/crane" + "github.com/opencontainers/go-digest" + "github.com/stretchr/testify/require" +) + +func TestCosign(t *testing.T) { + t.Parallel() + + testr, err := tester.NewTestWithConfig(t, "tests/integration/testdata/signing/config/config-cosign.yaml") + require.NoError(t, err, "tester.NewTest failed") + defer testr.Close() + reg := testr.StartTestRegistry(tester.TestRegistryOptions{UseTLS: true}) + defer reg.Close() + ref := cnab.MustParseOCIReference(fmt.Sprintf("%s/cosign:v1.0.0", reg.String())) + + cmd := shx.Command("cosign", "generate-key-pair").Env("COSIGN_PASSWORD='test'").In(testr.PorterHomeDir) + err = cmd.RunE() + require.NoError(t, err, "Generate cosign key pair failed") + _, output, err := testr.RunPorterWith(func(pc *shx.PreparedCommand) { + pc.Args("publish", "--sign-bundle", "--insecure-registry", "-f", "testdata/bundles/signing/porter.yaml", "-r", ref.String()) + pc.Env("COSIGN_PASSWORD='test'") + }) + require.NoError(t, err, "Publish failed") + + ref = toRefWithDigest(t, ref) + invocationImageRef := getInvocationImageDigest(t, output) + + _, output = testr.RequirePorter("install", "--verify-bundle", "--reference", ref.String(), "--insecure-registry") + fmt.Println(output) + require.Contains(t, output, fmt.Sprintf("bundle signature verified for %s", ref.String())) + require.Contains(t, output, fmt.Sprintf("invocation image signature verified for %s", invocationImageRef.String())) +} + +func TestNotation(t *testing.T) { + t.Parallel() + + testr, err := tester.NewTestWithConfig(t, "tests/integration/testdata/signing/config/config-notation.yaml") + require.NoError(t, err, "tester.NewTest failed") + defer testr.Close() + reg := testr.StartTestRegistry(tester.TestRegistryOptions{UseTLS: false}) + defer reg.Close() + ref := cnab.MustParseOCIReference(fmt.Sprintf("%s/cosign:v1.0.0", reg.String())) + + cmd := shx.Command("notation", "cert", "generate-test", "porter-test.org") + err = cmd.RunE() + require.NoError(t, err, "Generate notation certificate failed") + defer func() { + output, err := shx.Command("notation", "key", "ls").Output() + require.NoError(t, err) + keyRegex := regexp.MustCompile(`(/.+porter-test\.org\.key)`) + keyMatches := keyRegex.FindAllStringSubmatch(output, -1) + require.Len(t, keyMatches, 1) + crtRegex := regexp.MustCompile(`key\s+(/.+porter-test\.org\.crt)`) + crtMatches := crtRegex.FindAllStringSubmatch(output, -1) + require.Len(t, crtMatches, 1) + err = shx.Command("notation", "key", "delete", "porter-test.org").RunV() + require.NoError(t, err) + err = shx.Command("notation", "cert", "delete", "--type", "ca", "--store", "porter-test.org", "porter-test.org.crt", "--yes").RunV() + require.NoError(t, err) + err = os.Remove(keyMatches[0][1]) + require.NoError(t, err) + err = os.Remove(crtMatches[0][1]) + require.NoError(t, err) + }() + trustPolicy := ` + { + "version": "1.0", + "trustPolicies": [ + { + "name": "porter-test-images", + "registryScopes": [ "*" ], + "signatureVerification": { + "level" : "strict" + }, + "trustStores": [ "ca:porter-test.org" ], + "trustedIdentities": [ + "*" + ] + } + ] + }` + trustPolicyPath := filepath.Join(testr.PorterHomeDir, "trustpolicy.json") + err = os.WriteFile(trustPolicyPath, []byte(trustPolicy), 0644) + require.NoError(t, err, "Creation of trust policy failed") + err = shx.Command("notation", "policy", "import", trustPolicyPath).RunE() + require.NoError(t, err, "importing trust policy failed") + + _, output, err := testr.RunPorterWith(func(pc *shx.PreparedCommand) { + pc.Args("publish", "--sign-bundle", "--insecure-registry", "-f", "testdata/bundles/signing/porter.yaml", "-r", ref.String()) + }) + require.NoError(t, err, "Publish failed") + + ref = toRefWithDigest(t, ref) + invocationImageRef := getInvocationImageDigest(t, output) + + _, output = testr.RequirePorter("install", "--verify-bundle", "--reference", ref.String(), "--insecure-registry") + fmt.Println(output) + require.Contains(t, output, fmt.Sprintf("bundle signature verified for %s", ref.String())) + require.Contains(t, output, fmt.Sprintf("invocation image signature verified for %s", invocationImageRef.String())) +} + +func toRefWithDigest(t *testing.T, ref cnab.OCIReference) cnab.OCIReference { + desc, err := crane.Head(ref.String(), crane.Insecure) + require.NoError(t, err) + ref.Named = reference.TrimNamed(ref.Named) + ref, err = ref.WithDigest(digest.Digest(desc.Digest.String())) + require.NoError(t, err) + return ref +} + +func getInvocationImageDigest(t *testing.T, output string) cnab.OCIReference { + r := regexp.MustCompile(`(?m:^Signing invocation image (localhost:\d+/cosign:porter-[0-9a-z]+)\.)`) + matches := r.FindAllStringSubmatch(output, -1) + require.Len(t, matches, 1) + invocationImageRefString := matches[0][1] + desc, err := crane.Head(invocationImageRefString, crane.Insecure) + require.NoError(t, err) + ref := cnab.MustParseOCIReference(invocationImageRefString) + ref.Named = reference.TrimNamed(ref.Named) + ref, err = ref.WithDigest(digest.Digest(desc.Digest.String())) + require.NoError(t, err) + return ref +} diff --git a/tests/integration/testdata/bundles/signing/porter.yaml b/tests/integration/testdata/bundles/signing/porter.yaml new file mode 100644 index 000000000..72481b453 --- /dev/null +++ b/tests/integration/testdata/bundles/signing/porter.yaml @@ -0,0 +1,29 @@ +schemaVersion: 1.0.0-alpha.1 +name: mybun +version: 0.1.0 +description: "An example Porter configuration" +registry: "localhost:5000" + +mixins: + - exec + +install: + - exec: + description: "Install Hello World" + command: echo + arguments: + - install + +upgrade: + - exec: + description: "Upgrade Hello World" + command: echo + arguments: + - upgrade + +uninstall: + - exec: + description: "Uninstall Hello World" + command: echo + arguments: + - uninstall diff --git a/tests/integration/testdata/signing/config/config-cosign.yaml b/tests/integration/testdata/signing/config/config-cosign.yaml new file mode 100644 index 000000000..5ce44e16e --- /dev/null +++ b/tests/integration/testdata/signing/config/config-cosign.yaml @@ -0,0 +1,16 @@ +default-signer: cosign +default-storage: testdb + +signers: + - name: cosign + plugin: cosign + config: + publickey: ${env.PORTER_HOME}/cosign.pub + privatekey: ${env.PORTER_HOME}/cosign.key + insecureregistry: true + +storage: + - name: testdb + plugin: mongodb + config: + url: mongodb://localhost:27017/${env.PORTER_TEST_DB_NAME}?connect=direct diff --git a/tests/integration/testdata/signing/config/config-notation.yaml b/tests/integration/testdata/signing/config/config-notation.yaml new file mode 100644 index 000000000..3f47b36fb --- /dev/null +++ b/tests/integration/testdata/signing/config/config-notation.yaml @@ -0,0 +1,15 @@ +default-signer: notation +default-storage: testdb + +signers: + - name: notation + plugin: notation + config: + key: porter-test.org + insecureregistry: true + +storage: + - name: testdb + plugin: mongodb + config: + url: mongodb://localhost:27017/${env.PORTER_TEST_DB_NAME}?connect=direct