diff --git a/cmd/finch/nerdctl.go b/cmd/finch/nerdctl.go index b9dfe73a9..cb6751490 100644 --- a/cmd/finch/nerdctl.go +++ b/cmd/finch/nerdctl.go @@ -85,6 +85,7 @@ func (nc *nerdctlCommand) run(cmdName string, args []string) error { var ( nerdctlArgs, envs, fileEnvs []string skip bool + cosign bool ) for i, arg := range args { @@ -121,10 +122,27 @@ func (nc *nerdctlCommand) run(cmdName string, args []string) error { arg = fmt.Sprintf("%s%s", arg[0:11], resolvedIP) } nerdctlArgs = append(nerdctlArgs, arg) + case arg == "--sign=cosign_data" || arg == "--verify=cosign_data": + cosign = true + nerdctlArgs = append(nerdctlArgs, arg) default: nerdctlArgs = append(nerdctlArgs, arg) } } + limaArgs := []string{"shell", limaInstanceName, "sudo"} + + // The env variables are not passed through by `limactl shell` by default so we need to assign the variables explicitly. + // https://github.com/lima-vm/lima/issues/1419 + if cosign { + cosignEnv := "COSIGN_PASSWORD" + v, b := nc.systemDeps.LookupEnv(cosignEnv) + if b { + limaArgs = append(limaArgs, fmt.Sprintf("%s=%s", cosignEnv, v)) + } + } + + limaArgs = append(limaArgs, nerdctlCmdName, cmdName) + // to handle environment variables properly, we add all entries found via // env-file includes to the map first and then all command line environment // flags, making sure that command line overrides environment file options, @@ -146,7 +164,7 @@ func (nc *nerdctlCommand) run(cmdName string, args []string) error { } finalArgs = append(finalArgs, nerdctlArgs...) - limaArgs := append([]string{"shell", limaInstanceName, "sudo", nerdctlCmdName, cmdName}, finalArgs...) + limaArgs = append(limaArgs, finalArgs...) if nc.shouldReplaceForHelp(cmdName, args) { return nc.creator.RunWithReplacingStdout([]command.Replacement{{Source: "nerdctl", Target: "finch"}}, limaArgs...) diff --git a/cmd/finch/nerdctl_test.go b/cmd/finch/nerdctl_test.go index 702efd16c..bbf625fc4 100644 --- a/cmd/finch/nerdctl_test.go +++ b/cmd/finch/nerdctl_test.go @@ -496,6 +496,74 @@ func TestNerdctlCommand_run(t *testing.T) { Return(fmt.Errorf("failed to replace")) }, }, + { + name: "with COSIGN_PASSWORD env var and --sign=cosign_data", + cmdName: "push", + args: []string{"--sign=cosign_data", "test:tag"}, + wantErr: nil, + mockSvc: func( + t *testing.T, + lcc *mocks.LimaCmdCreator, + ncsd *mocks.NerdctlCommandSystemDeps, + logger *mocks.Logger, + ctrl *gomock.Controller, + fs afero.Fs, + ) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") + ncsd.EXPECT().LookupEnv("COSIGN_PASSWORD").Return("test", true) + c := mocks.NewCommand(ctrl) + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "COSIGN_PASSWORD=test", nerdctlCmdName, "push", "--sign=cosign_data", "test:tag").Return(c) + c.EXPECT().Run() + }, + }, + { + name: "with COSIGN_PASSWORD env var and --verify=cosign_data", + cmdName: "pull", + args: []string{"--verify=cosign_data", "test:tag"}, + wantErr: nil, + mockSvc: func( + t *testing.T, + lcc *mocks.LimaCmdCreator, + ncsd *mocks.NerdctlCommandSystemDeps, + logger *mocks.Logger, + ctrl *gomock.Controller, + fs afero.Fs, + ) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") + ncsd.EXPECT().LookupEnv("COSIGN_PASSWORD").Return("test", true) + c := mocks.NewCommand(ctrl) + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", "COSIGN_PASSWORD=test", nerdctlCmdName, "pull", "--verify=cosign_data", "test:tag").Return(c) + c.EXPECT().Run() + }, + }, + { + name: "with COSIGN_PASSWORD env var without cosign_data arg", + cmdName: "pull", + args: []string{"test:tag"}, + wantErr: nil, + mockSvc: func( + t *testing.T, + lcc *mocks.LimaCmdCreator, + ncsd *mocks.NerdctlCommandSystemDeps, + logger *mocks.Logger, + ctrl *gomock.Controller, + fs afero.Fs, + ) { + getVMStatusC := mocks.NewCommand(ctrl) + lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC) + getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil) + logger.EXPECT().Debugf("Status of virtual machine: %s", "Running") + c := mocks.NewCommand(ctrl) + lcc.EXPECT().Create("shell", limaInstanceName, "sudo", nerdctlCmdName, "pull", "test:tag").Return(c) + c.EXPECT().Run() + }, + }, } for _, tc := range testCases { diff --git a/e2e/container/container_test.go b/e2e/container/container_test.go index db4f463ef..297ec0113 100644 --- a/e2e/container/container_test.go +++ b/e2e/container/container_test.go @@ -79,6 +79,7 @@ func TestContainer(t *testing.T) { tests.NetworkInspect(o) tests.NetworkLs(o) tests.NetworkRm(o) + testCosign(o) }) gomega.RegisterFailHandler(ginkgo.Fail) diff --git a/e2e/container/cosign_data/test-1.key b/e2e/container/cosign_data/test-1.key new file mode 100644 index 000000000..118e1fe95 --- /dev/null +++ b/e2e/container/cosign_data/test-1.key @@ -0,0 +1,11 @@ +-----BEGIN ENCRYPTED COSIGN PRIVATE KEY----- +eyJrZGYiOnsibmFtZSI6InNjcnlwdCIsInBhcmFtcyI6eyJOIjozMjc2OCwiciI6 +OCwicCI6MX0sInNhbHQiOiIvYW9saHRuZEZTSHZsQjBZSnBTOVI1VlcyOE5HUmox +VkJNL2VDZWlvVEV3PSJ9LCJjaXBoZXIiOnsibmFtZSI6Im5hY2wvc2VjcmV0Ym94 +Iiwibm9uY2UiOiJGRGpKU1BTdnN4WmQ0N2orRlgvUjlPbVB0WlpkTVh4dyJ9LCJj +aXBoZXJ0ZXh0IjoibzlCOXJJbmZPNXZaeE9PMFBSdFdjYlNUQmxibXA5OVVWTnEv +ZFhJN0hzd09yZFpVeTA1MmdUT3AyVkFsSjk2aTNFZitiY095QlU1MWt1UDd2R2gy +U1ljU2VkbWQvejEzM3owNUovZytjUll3bHRuNkowOTgwZ0xUR1NKdWxobFNIYWpC +Q25LS1RmY2tIb0dUU0dsZkU1aFk1UFdyRGlQTmc3VVA4bk1lc2JCWlRPMnFjaUdE +bTI0a21ON1RIOEljRlJ4T3Y1NkFNWm1tTUE9PSJ9 +-----END ENCRYPTED COSIGN PRIVATE KEY----- diff --git a/e2e/container/cosign_data/test-1.pub b/e2e/container/cosign_data/test-1.pub new file mode 100644 index 000000000..598bfa34c --- /dev/null +++ b/e2e/container/cosign_data/test-1.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfkODzHTFabSz0T+X758IqIB6pi3u +Km4JQCcEDFv94s6J4msvNOhmiAv3PQ/b9dutQ7QODWJAdm3cp6CMd87e1w== +-----END PUBLIC KEY----- diff --git a/e2e/container/cosign_data/test-2.key b/e2e/container/cosign_data/test-2.key new file mode 100644 index 000000000..e4e97c381 --- /dev/null +++ b/e2e/container/cosign_data/test-2.key @@ -0,0 +1,11 @@ +-----BEGIN ENCRYPTED COSIGN PRIVATE KEY----- +eyJrZGYiOnsibmFtZSI6InNjcnlwdCIsInBhcmFtcyI6eyJOIjozMjc2OCwiciI6 +OCwicCI6MX0sInNhbHQiOiJiYlVrSzkzdmY2dEhIM1czNTJ1UWM2ajNidzduUjdW +cTdReHJaRVlJYXM4PSJ9LCJjaXBoZXIiOnsibmFtZSI6Im5hY2wvc2VjcmV0Ym94 +Iiwibm9uY2UiOiJBWlhuSThncGhjbzdFdDBVOGhzKy91Tk5OdFpyTWNBOSJ9LCJj +aXBoZXJ0ZXh0IjoiaVJsUG9IRlJnQmJTamc2NnhvTTM4dm0vNExwelJXeUlKZi8x +dFIraEJ3dHdRMTQwZjBsQlpHdHRRMXZYcEE4UktHK09vTnNUcFNUSGh6R2phd3VN +ZXNzaVdsWkxHVTVjSTU5UVZ4TW40bGI4ME5iNWF2NlA5ZjVYT2x2aHhXbnMzUnpt +R091L0hCOHd1ZnBUUnBlQlZNLzZkZmFyV05ZbE92VzRYaWVvZXNEQ0hOV3JWbzFJ +WWdCbysxTEFYbzdYWkQ4cXNVWFdJd3ZONkE9PSJ9 +-----END ENCRYPTED COSIGN PRIVATE KEY----- diff --git a/e2e/container/cosign_data/test-2.pub b/e2e/container/cosign_data/test-2.pub new file mode 100644 index 000000000..49248d56c --- /dev/null +++ b/e2e/container/cosign_data/test-2.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERyYVqiPX1IGvTiAuJCZIOfPllOm/ +HTi7DaswFLwNXVOOC7FLP3L9YzQ0q24bFBqkSQqgWeycKsOOPCbF0nMLHQ== +-----END PUBLIC KEY----- diff --git a/e2e/container/cosign_test.go b/e2e/container/cosign_test.go new file mode 100644 index 000000000..87ad178c6 --- /dev/null +++ b/e2e/container/cosign_test.go @@ -0,0 +1,53 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package container + +import ( + "fmt" + "os" + + "github.com/onsi/ginkgo/v2" + "github.com/runfinch/common-tests/command" + "github.com/runfinch/common-tests/ffs" + "github.com/runfinch/common-tests/fnet" + "github.com/runfinch/common-tests/option" +) + +const alpineImage = "public.ecr.aws/docker/library/alpine:latest" +const registryImage = "public.ecr.aws/docker/library/registry:latest" + +var testCosign = func(o *option.Option) { + ginkgo.Describe("Cosign", func() { + var buildContext string + var port int + + ginkgo.BeforeEach(func() { + command.RemoveAll(o) + buildContext = ffs.CreateBuildContext(fmt.Sprintf(`FROM %s + CMD ["echo", "bar"] + `, alpineImage)) + ginkgo.DeferCleanup(os.RemoveAll, buildContext) + port = fnet.GetFreePort() + command.Run(o, "run", "-dp", fmt.Sprintf("%d:5000", port), "--name", "registry", registryImage) + }) + + ginkgo.AfterEach(func() { + command.RemoveAll(o) + }) + + ginkgo.It("should succeed to verify the signature of the image only when it is signed with the matched key", func() { + tag := fmt.Sprintf(`localhost:%d/test-push:tag`, port) + command.Run(o, "build", "-t", tag, buildContext) + os.Setenv("COSIGN_PASSWORD", "test-1") + defer os.Unsetenv("COSIGN_PASSWORD") + command.Run(o, "push", "--sign=cosign", "--cosign-key=./e2e/cosign_data/test1.key", tag) + command.Run(o, "pull", "--verify=cosign", "--cosign-key=./e2e/cosign_data/test-1.pub", tag) + command.Run(o, "run", "-d", "--verify=cosign", "--cosign-key=./e2e/cosign_data/test-1.pub", tag) + command.RunWithoutSuccessfulExit(o, "pull", "-d", "--verify=cosign_data", "--cosign-key=./e2e/cosign_data/test-2.pub", tag) + command.RunWithoutSuccessfulExit(o, "run", "-d", "--verify=cosign_data", "--cosign-key=./e2e/cosign_data/test-2.pub", tag) + command.RunWithoutSuccessfulExit(o, "pull", "-d", "--verify=cosign_data", "--cosign-key=./e2e/cosign_data/test-2.pub", alpineImage) + command.RunWithoutSuccessfulExit(o, "run", "-d", "--verify=cosign_data", "--cosign-key=./e2e/cosign_data/test-2.pub", alpineImage) + }) + }) +}