From 9a0621ff200bd23c770f7ec7e0cb470a7137c45f Mon Sep 17 00:00:00 2001 From: Jin Dong Date: Tue, 15 Nov 2022 14:15:31 +0000 Subject: [PATCH 1/2] Add cosign in compose run, up, pull, push Signed-off-by: Jin Dong Add compose cosign test Signed-off-by: Jin Dong Fix naming bug Signed-off-by: Jin Dong Add compose cosign test [pass] Signed-off-by: Jin Dong Add compose experimental flag Signed-off-by: Jin Dong Refactor cosign func Signed-off-by: Jin Dong Add compose cosign doc Signed-off-by: Jin Dong --- README.md | 2 +- cmd/nerdctl/compose.go | 47 ++++++- cmd/nerdctl/compose_run_linux_test.go | 89 ++++++++++++ cmd/nerdctl/pull.go | 63 +-------- cmd/nerdctl/push.go | 50 +------ docs/cosign.md | 103 ++++++++++++++ pkg/composer/composer.go | 3 +- pkg/composer/pull.go | 14 +- pkg/composer/push.go | 14 +- pkg/composer/serviceparser/serviceparser.go | 9 ++ pkg/composer/up_service.go | 11 +- pkg/cosignutil/cosignutil.go | 143 ++++++++++++++++++++ 12 files changed, 420 insertions(+), 128 deletions(-) create mode 100644 pkg/cosignutil/cosignutil.go diff --git a/README.md b/README.md index 23d4265d0ca..afb929d1dc1 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ Major: P2P image distribution (IPFS) is completely optional. Your host is NOT connected to any P2P network, unless you opt in to [install and run IPFS daemon](https://docs.ipfs.io/install/). - Recursive read-only (RRO) bind-mount: `nerdctl run -v /mnt:/mnt:rro` (make children such as `/mnt/usb` to be read-only, too). Requires kernel >= 5.12, and crun >= 1.4 or runc >= 1.1 (PR [#3272](https://github.com/opencontainers/runc/pull/3272)). -- [Cosign integration](./docs/cosign.md): `nerdctl pull --verify=cosign` and `nerdctl push --sign=cosign` +- [Cosign integration](./docs/cosign.md): `nerdctl pull --verify=cosign` and `nerdctl push --sign=cosign`, and [in Compose](./docs/cosign.md#cosign-in-compose) - [Accelerated rootless containers using bypass4netns](./docs/rootless.md): `nerdctl run --label nerdctl/bypass4netns=true` Minor: diff --git a/cmd/nerdctl/compose.go b/cmd/nerdctl/compose.go index 180b5756e14..06afc00b8a3 100644 --- a/cmd/nerdctl/compose.go +++ b/cmd/nerdctl/compose.go @@ -19,17 +19,21 @@ package main import ( "context" "errors" + "fmt" "github.com/containerd/containerd" "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/platforms" "github.com/containerd/nerdctl/pkg/composer" + "github.com/containerd/nerdctl/pkg/composer/serviceparser" + "github.com/containerd/nerdctl/pkg/cosignutil" "github.com/containerd/nerdctl/pkg/imgutil" "github.com/containerd/nerdctl/pkg/ipfs" "github.com/containerd/nerdctl/pkg/netutil" "github.com/containerd/nerdctl/pkg/referenceutil" httpapi "github.com/ipfs/go-ipfs-http-client" ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -109,6 +113,10 @@ func getComposer(cmd *cobra.Command, client *containerd.Client) (*composer.Compo if err != nil { return nil, err } + experimental, err := cmd.Flags().GetBool("experimental") + if err != nil { + return nil, err + } o := composer.Options{ Project: projectName, @@ -118,6 +126,7 @@ func getComposer(cmd *cobra.Command, client *containerd.Client) (*composer.Compo NerdctlCmd: nerdctlCmd, NerdctlArgs: nerdctlArgs, DebugPrintFull: debugFull, + Experimental: experimental, } cniEnv, err := netutil.NewCNIEnv(cniPath, cniNetconfpath) @@ -164,7 +173,7 @@ func getComposer(cmd *cobra.Command, client *containerd.Client) (*composer.Compo return true, nil } - o.EnsureImage = func(ctx context.Context, imageName, pullMode, platform string, quiet bool) error { + o.EnsureImage = func(ctx context.Context, imageName, pullMode, platform string, ps *serviceparser.Service, quiet bool) error { ocispecPlatforms := []ocispec.Platform{platforms.DefaultSpec()} if platform != "" { parsed, err := platforms.Parse(platform) @@ -173,19 +182,43 @@ func getComposer(cmd *cobra.Command, client *containerd.Client) (*composer.Compo } ocispecPlatforms = []ocispec.Platform{parsed} // no append } - var imgErr error + + // IPFS reference if scheme, ref, err := referenceutil.ParseIPFSRefWithScheme(imageName); err == nil { ipfsClient, err := httpapi.NewLocalApi() if err != nil { return err } - _, imgErr = ipfs.EnsureImage(ctx, client, ipfsClient, cmd.OutOrStdout(), cmd.ErrOrStderr(), snapshotter, scheme, ref, + _, err = ipfs.EnsureImage(ctx, client, ipfsClient, cmd.OutOrStdout(), cmd.ErrOrStderr(), snapshotter, scheme, ref, pullMode, ocispecPlatforms, nil, quiet) - } else { - _, imgErr = imgutil.EnsureImage(ctx, client, cmd.OutOrStdout(), cmd.ErrOrStderr(), snapshotter, imageName, - pullMode, insecure, hostsDirs, ocispecPlatforms, nil, quiet) + return err + } + + ref := imageName + if verifier, ok := ps.Unparsed.Extensions[serviceparser.ComposeVerify]; ok { + switch verifier { + case "cosign": + if !o.Experimental { + return fmt.Errorf("cosign only work with enable experimental feature") + } + keyRef, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignPublicKey] + if !ok { + return fmt.Errorf("no cosign public key, service: %s", ps.Unparsed.Name) + } + + ref, err = cosignutil.VerifyCosign(ctx, ref, keyRef.(string), hostsDirs) + if err != nil { + return err + } + case "none": + logrus.Debugf("verification process skipped") + default: + return fmt.Errorf("no verifier found: %s", verifier) + } } - return imgErr + _, err = imgutil.EnsureImage(ctx, client, cmd.OutOrStdout(), cmd.ErrOrStderr(), snapshotter, ref, + pullMode, insecure, hostsDirs, ocispecPlatforms, nil, quiet) + return err } return composer.New(o, client) diff --git a/cmd/nerdctl/compose_run_linux_test.go b/cmd/nerdctl/compose_run_linux_test.go index 758b5262889..2e86b8d51f4 100644 --- a/cmd/nerdctl/compose_run_linux_test.go +++ b/cmd/nerdctl/compose_run_linux_test.go @@ -19,12 +19,14 @@ package main import ( "fmt" "io" + "os/exec" "strings" "testing" "time" "github.com/containerd/nerdctl/pkg/testutil" "github.com/containerd/nerdctl/pkg/testutil/nettestutil" + "github.com/containerd/nerdctl/pkg/testutil/testregistry" "github.com/sirupsen/logrus" "gotest.tools/v3/assert" ) @@ -430,3 +432,90 @@ services: assert.Assert(t, container.Mounts[0].Source == tmpDir, errMsg) assert.Assert(t, container.Mounts[0].Destination == destinationDir, errMsg) } + +func TestComposePushAndPullWithCosignVerify(t *testing.T) { + if _, err := exec.LookPath("cosign"); err != nil { + t.Skip() + } + testutil.DockerIncompatible(t) + testutil.RequiresBuild(t) + base := testutil.NewBase(t) + defer base.Cmd("builder", "prune").Run() + + // set up cosign and local registry + t.Setenv("COSIGN_PASSWORD", "1") + keyPair := newCosignKeyPair(t, "cosign-key-pair") + defer keyPair.cleanup() + + reg := testregistry.NewPlainHTTP(base, 5000) + defer reg.Cleanup() + localhostIP := "127.0.0.1" + t.Logf("localhost IP=%q", localhostIP) + testImageRefPrefix := fmt.Sprintf("%s:%d/", + localhostIP, reg.ListenPort) + t.Logf("testImageRefPrefix=%q", testImageRefPrefix) + + var ( + imageSvc0 = testImageRefPrefix + "composebuild_svc0" + imageSvc1 = testImageRefPrefix + "composebuild_svc1" + imageSvc2 = testImageRefPrefix + "composebuild_svc2" + ) + + dockerComposeYAML := fmt.Sprintf(` +services: + svc0: + build: . + image: %s + x-nerdctl-verify: cosign + x-nerdctl-cosign-public-key: %s + x-nerdctl-sign: cosign + x-nerdctl-cosign-private-key: %s + entrypoint: + - stty + svc1: + build: . + image: %s + x-nerdctl-verify: cosign + x-nerdctl-cosign-public-key: dummy_pub_key + x-nerdctl-sign: cosign + x-nerdctl-cosign-private-key: %s + entrypoint: + - stty + svc2: + build: . + image: %s + x-nerdctl-verify: none + x-nerdctl-sign: none + entrypoint: + - stty +`, imageSvc0, keyPair.publicKey, keyPair.privateKey, + imageSvc1, keyPair.privateKey, imageSvc2) + + dockerfile := fmt.Sprintf(`FROM %s`, testutil.AlpineImage) + + comp := testutil.NewComposeDir(t, dockerComposeYAML) + defer comp.CleanUp() + comp.WriteFile("Dockerfile", dockerfile) + + // 1. build both services/images + base.ComposeCmd("-f", comp.YAMLFullPath(), "build").AssertOK() + defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + // 2. compose push with cosign for svc0/svc1, (and none for svc2) + base.ComposeCmd("-f", comp.YAMLFullPath(), "push").AssertOK() + // 3. compose pull with cosign + base.ComposeCmd("-f", comp.YAMLFullPath(), "pull", "svc0").AssertOK() // key match + base.ComposeCmd("-f", comp.YAMLFullPath(), "pull", "svc1").AssertFail() // key mismatch + base.ComposeCmd("-f", comp.YAMLFullPath(), "pull", "svc2").AssertOK() // verify passed + // 4. compose run + const sttyPartialOutput = "speed 38400 baud" + // unbuffer(1) emulates tty, which is required by `nerdctl run -t`. + // unbuffer(1) can be installed with `apt-get install expect`. + unbuffer := []string{"unbuffer"} + base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "run", "svc0").AssertOutContains(sttyPartialOutput) // key match + base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "run", "svc1").AssertFail() // key mismatch + base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "run", "svc2").AssertOutContains(sttyPartialOutput) // verify passed + // 5. compose up + base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "svc0").AssertOK() // key match + base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "svc1").AssertFail() // key mismatch + base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "svc2").AssertOK() // verify passed +} diff --git a/cmd/nerdctl/pull.go b/cmd/nerdctl/pull.go index a50b7ba65f7..275d2313738 100644 --- a/cmd/nerdctl/pull.go +++ b/cmd/nerdctl/pull.go @@ -17,15 +17,12 @@ package main import ( - "bufio" "context" "errors" "fmt" - "os" - "os/exec" - "strings" "github.com/containerd/containerd" + "github.com/containerd/nerdctl/pkg/cosignutil" "github.com/containerd/nerdctl/pkg/imgutil" "github.com/containerd/nerdctl/pkg/ipfs" "github.com/containerd/nerdctl/pkg/platformutil" @@ -168,7 +165,7 @@ func ensureImage(cmd *cobra.Command, ctx context.Context, client *containerd.Cli return nil, err } - ref, err = verifyCosign(ctx, rawRef, keyRef, hostsDirs) + ref, err = cosignutil.VerifyCosign(ctx, rawRef, keyRef, hostsDirs) if err != nil { return nil, err } @@ -185,59 +182,3 @@ func ensureImage(cmd *cobra.Command, ctx context.Context, client *containerd.Cli } return ensured, err } - -func verifyCosign(ctx context.Context, rawRef string, keyRef string, hostsDirs []string) (string, error) { - digest, err := imgutil.ResolveDigest(ctx, rawRef, false, hostsDirs) - if err != nil { - logrus.WithError(err).Errorf("unable to resolve digest for an image %s: %v", rawRef, err) - return rawRef, err - } - ref := rawRef - if !strings.Contains(ref, "@") { - ref += "@" + digest - } - - logrus.Debugf("verifying image: %s", ref) - - cosignExecutable, err := exec.LookPath("cosign") - if err != nil { - logrus.WithError(err).Error("cosign executable not found in path $PATH") - logrus.Info("you might consider installing cosign from: https://docs.sigstore.dev/cosign/installation") - return ref, err - } - - cosignCmd := exec.Command(cosignExecutable, []string{"verify"}...) - cosignCmd.Env = os.Environ() - - if keyRef != "" { - cosignCmd.Args = append(cosignCmd.Args, "--key", keyRef) - } else { - cosignCmd.Env = append(cosignCmd.Env, "COSIGN_EXPERIMENTAL=true") - } - - cosignCmd.Args = append(cosignCmd.Args, ref) - - logrus.Debugf("running %s %v", cosignExecutable, cosignCmd.Args) - - stdout, _ := cosignCmd.StdoutPipe() - stderr, _ := cosignCmd.StderrPipe() - if err := cosignCmd.Start(); err != nil { - return ref, err - } - - scanner := bufio.NewScanner(stdout) - for scanner.Scan() { - logrus.Info("cosign: " + scanner.Text()) - } - - errScanner := bufio.NewScanner(stderr) - for errScanner.Scan() { - logrus.Info("cosign: " + errScanner.Text()) - } - - if err := cosignCmd.Wait(); err != nil { - return ref, err - } - - return ref, nil -} diff --git a/cmd/nerdctl/push.go b/cmd/nerdctl/push.go index b284df74968..6a1ce93f5d3 100644 --- a/cmd/nerdctl/push.go +++ b/cmd/nerdctl/push.go @@ -17,18 +17,16 @@ package main import ( - "bufio" "context" "fmt" "io" - "os" - "os/exec" "github.com/containerd/containerd/content" "github.com/containerd/containerd/images" "github.com/containerd/containerd/images/converter" refdocker "github.com/containerd/containerd/reference/docker" "github.com/containerd/containerd/remotes" + "github.com/containerd/nerdctl/pkg/cosignutil" "github.com/containerd/nerdctl/pkg/errutil" "github.com/containerd/nerdctl/pkg/imgutil/dockerconfigresolver" "github.com/containerd/nerdctl/pkg/imgutil/push" @@ -254,7 +252,7 @@ func pushAction(cmd *cobra.Command, args []string) error { return err } - err = signCosign(rawRef, keyRef) + err = cosignutil.SignCosign(rawRef, keyRef) if err != nil { return err } @@ -313,47 +311,3 @@ func isReusableESGZ(ctx context.Context, cs content.Store, desc ocispec.Descript } return true } - -func signCosign(rawRef string, keyRef string) error { - cosignExecutable, err := exec.LookPath("cosign") - if err != nil { - logrus.WithError(err).Error("cosign executable not found in path $PATH") - logrus.Info("you might consider installing cosign from: https://docs.sigstore.dev/cosign/installation") - return err - } - - cosignCmd := exec.Command(cosignExecutable, []string{"sign"}...) - cosignCmd.Env = os.Environ() - - if keyRef != "" { - cosignCmd.Args = append(cosignCmd.Args, "--key", keyRef) - } else { - cosignCmd.Env = append(cosignCmd.Env, "COSIGN_EXPERIMENTAL=true") - } - - cosignCmd.Args = append(cosignCmd.Args, rawRef) - - logrus.Debugf("running %s %v", cosignExecutable, cosignCmd.Args) - - stdout, _ := cosignCmd.StdoutPipe() - stderr, _ := cosignCmd.StderrPipe() - if err := cosignCmd.Start(); err != nil { - return err - } - - scanner := bufio.NewScanner(stdout) - for scanner.Scan() { - logrus.Info("cosign: " + scanner.Text()) - } - - errScanner := bufio.NewScanner(stderr) - for errScanner.Scan() { - logrus.Info("cosign: " + errScanner.Text()) - } - - if err := cosignCmd.Wait(); err != nil { - return err - } - - return nil -} diff --git a/docs/cosign.md b/docs/cosign.md index 7e8f3ab0237..c074d839d28 100644 --- a/docs/cosign.md +++ b/docs/cosign.md @@ -75,3 +75,106 @@ INFO[0003] cosign: failed to verify signature INFO[0003] cosign: main.go:46: error during command execution: no matching signatures: INFO[0003] cosign: failed to verify signature ``` + +## Cosign in Compose + +> Cosign support in Compose is also experimental and implemented based on Compose's [extension](https://github.com/compose-spec/compose-spec/blob/master/spec.md#extension) capibility. + +cosign is supported in `nerdctl compose up|run|push|pull`. You can use cosign in Compose by adding the following fields in your compose yaml. These fields are _per service_, and you can enable only `verify` or only `sign` (or both). + +```yaml +# only put cosign related fields under the service you want to sign/verify. +services: + svc0: + build: . + image: ${REGISTRY}/svc0_image # replace with your registry + # `x-nerdctl-verify` and `x-nerdctl-cosign-public-key` are for verify + # required for `nerdctl compose up|run|pull` + x-nerdctl-verify: cosign + x-nerdctl-cosign-public-key: /path/to/cosign.pub + # `x-nerdctl-sign` and `x-nerdctl-cosign-private-key` are for sign + # required for `nerdctl compose push` + x-nerdctl-sign: cosign + x-nerdctl-cosign-private-key: /path/to/cosign.key + ports: + - 8080:80 + svc1: + build: . + image: ${REGISTRY}/svc1_image # replace with your registry + ports: + - 8081:80 +``` + +Following the cosign tutorial above, first set up environment and prepare cosign key pair: + +```shell +# Generate a key-pair: cosign.key and cosign.pub +$ cosign generate-key-pair + +# Export your COSIGN_PASSWORD to prevent CLI prompting +$ export COSIGN_PASSWORD=$COSIGN_PASSWORD +``` + +We'll use the following `Dockerfile` and `docker-compose.yaml`: + +```shell +$ cat Dockerfile +FROM nginx:1.19-alpine +RUN uname -m > /usr/share/nginx/html/index.html + +$ cat docker-compose.yml +services: + svc0: + build: . + image: ${REGISTRY}/svc1_image # replace with your registry + x-nerdctl-verify: cosign + x-nerdctl-cosign-public-key: ./cosign.pub + x-nerdctl-sign: cosign + x-nerdctl-cosign-private-key: ./cosign.key + ports: + - 8080:80 + svc1: + build: . + image: ${REGISTRY}/svc1_image # replace with your registry + ports: + - 8081:80 +``` + +> The `env "COSIGN_PASSWORD="$COSIGN_PASSWORD""` part in the below commands is a walkaround to use rootful nerdctl and make the env variable visible to root (in sudo). You don't need this part if (1) you're using rootless, or (2) your `COSIGN_PASSWORD` is visible in root. + +First let's `build` and `push` the two services: + +```shell +$ sudo nerdctl compose build +INFO[0000] Building image xxxxx/svc0_image +... +INFO[0000] Building image xxxxx/svc1_image +[+] Building 0.2s (6/6) FINISHED + +$ sudo env "COSIGN_PASSWORD="$COSIGN_PASSWORD"" nerdctl compose --experimental=true push +INFO[0000] Pushing image xxxxx/svc1_image +... +INFO[0000] Pushing image xxxxx/svc0_image +INFO[0000] pushing as a reduced-platform image (application/vnd.docker.distribution.manifest.v2+json, sha256:4329abc3143b1545835de17e1302c8313a9417798b836022f4c8c8dc8b10a3e9) +INFO[0000] cosign: WARNING: Image reference xxxxx/svc0_image uses a tag, not a digest, to identify the image to sign. +INFO[0000] cosign: +INFO[0000] cosign: This can lead you to sign a different image than the intended one. Please use a +INFO[0000] cosign: digest (example.com/ubuntu@sha256:abc123...) rather than tag +INFO[0000] cosign: (example.com/ubuntu:latest) for the input to cosign. The ability to refer to +INFO[0000] cosign: images by tag will be removed in a future release. +INFO[0000] cosign: Pushing signature to: xxxxx/svc0_image +``` + +Then we can `pull` and `up` services (`run` is similar to up): + +```shell +# ensure built images are removed and pull is performed. +$ sudo nerdctl compose down +$ sudo env "COSIGN_PASSWORD="$COSIGN_PASSWORD"" nerdctl compose --experimental=true pull +$ sudo env "COSIGN_PASSWORD="$COSIGN_PASSWORD"" nerdctl compose --experimental=true up +$ sudo env "COSIGN_PASSWORD="$COSIGN_PASSWORD"" nerdctl compose --experimental=true run svc0 -- echo "hello" +# clean up compose resources. +$ sudo nerdctl compose down +``` + +Check your logs to confirm that svc0 is verified by cosign (have cosign logs) and svc1 is not. You can also change the public key in `docker-compose.yaml` to a random value to see verify failure will stop the container being `pull|up|run`. \ No newline at end of file diff --git a/pkg/composer/composer.go b/pkg/composer/composer.go index 65d1ab27723..7ce4edf212b 100644 --- a/pkg/composer/composer.go +++ b/pkg/composer/composer.go @@ -44,8 +44,9 @@ type Options struct { NetworkExists func(string) (bool, error) VolumeExists func(string) (bool, error) ImageExists func(ctx context.Context, imageName string) (bool, error) - EnsureImage func(ctx context.Context, imageName, pullMode, platform string, quiet bool) error + EnsureImage func(ctx context.Context, imageName, pullMode, platform string, ps *serviceparser.Service, quiet bool) error DebugPrintFull bool // full debug print, may leak secret env var to logs + Experimental bool // enable experimental features } func New(o Options, client *containerd.Client) (*Composer, error) { diff --git a/pkg/composer/pull.go b/pkg/composer/pull.go index 897565add04..bbdab6ceb03 100644 --- a/pkg/composer/pull.go +++ b/pkg/composer/pull.go @@ -37,11 +37,11 @@ func (c *Composer) Pull(ctx context.Context, po PullOptions, services []string) if err != nil { return err } - return c.pullServiceImage(ctx, ps.Image, ps.Unparsed.Platform, po) + return c.pullServiceImage(ctx, ps.Image, ps.Unparsed.Platform, ps, po) }) } -func (c *Composer) pullServiceImage(ctx context.Context, image string, platform string, po PullOptions) error { +func (c *Composer) pullServiceImage(ctx context.Context, image string, platform string, ps *serviceparser.Service, po PullOptions) error { logrus.Infof("Pulling image %s", image) var args []string // nolint: prealloc @@ -51,6 +51,16 @@ func (c *Composer) pullServiceImage(ctx context.Context, image string, platform if po.Quiet { args = append(args, "--quiet") } + if verifier, ok := ps.Unparsed.Extensions[serviceparser.ComposeVerify]; ok { + args = append(args, "--verify="+verifier.(string)) + } + if publicKey, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignPublicKey]; ok { + args = append(args, "--cosign-key="+publicKey.(string)) + } + if c.Options.Experimental { + args = append(args, "--experimental") + } + args = append(args, image) cmd := c.createNerdctlCmd(ctx, append([]string{"pull"}, args...)...) diff --git a/pkg/composer/push.go b/pkg/composer/push.go index 6c4597d68d2..ae8c6854e59 100644 --- a/pkg/composer/push.go +++ b/pkg/composer/push.go @@ -36,17 +36,27 @@ func (c *Composer) Push(ctx context.Context, po PushOptions, services []string) if err != nil { return err } - return c.pushServiceImage(ctx, ps.Image, ps.Unparsed.Platform, po) + return c.pushServiceImage(ctx, ps.Image, ps.Unparsed.Platform, ps, po) }) } -func (c *Composer) pushServiceImage(ctx context.Context, image string, platform string, po PushOptions) error { +func (c *Composer) pushServiceImage(ctx context.Context, image string, platform string, ps *serviceparser.Service, po PushOptions) error { logrus.Infof("Pushing image %s", image) var args []string // nolint: prealloc if platform != "" { args = append(args, "--platform="+platform) } + if signer, ok := ps.Unparsed.Extensions[serviceparser.ComposeSign]; ok { + args = append(args, "--sign="+signer.(string)) + } + if privateKey, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignPrivateKey]; ok { + args = append(args, "--cosign-key="+privateKey.(string)) + } + if c.Options.Experimental { + args = append(args, "--experimental") + } + args = append(args, image) cmd := c.createNerdctlCmd(ctx, append([]string{"push"}, args...)...) diff --git a/pkg/composer/serviceparser/serviceparser.go b/pkg/composer/serviceparser/serviceparser.go index 899f4e33214..96f600cf187 100644 --- a/pkg/composer/serviceparser/serviceparser.go +++ b/pkg/composer/serviceparser/serviceparser.go @@ -34,6 +34,14 @@ import ( "github.com/sirupsen/logrus" ) +// ComposeExtensionKey defines fields used to implement extension features. +const ( + ComposeVerify = "x-nerdctl-verify" + ComposeCosignPublicKey = "x-nerdctl-cosign-public-key" + ComposeSign = "x-nerdctl-sign" + ComposeCosignPrivateKey = "x-nerdctl-cosign-private-key" +) + func warnUnknownFields(svc types.ServiceConfig) { if unknown := reflectutil.UnknownNonEmptyFields(&svc, "Name", @@ -57,6 +65,7 @@ func warnUnknownFields(svc types.ServiceConfig) { "Entrypoint", "Environment", "Extends", // handled by the loader + "Extensions", "ExtraHosts", "Hostname", "Image", diff --git a/pkg/composer/up_service.go b/pkg/composer/up_service.go index 5f835f9797b..56c0367674b 100644 --- a/pkg/composer/up_service.go +++ b/pkg/composer/up_service.go @@ -97,17 +97,16 @@ func (c *Composer) ensureServiceImage(ctx context.Context, ps *serviceparser.Ser } if ok, err := c.ImageExists(ctx, ps.Image); err != nil { return err - } else if ok { - logrus.Debugf("Image %s already exists, not building", ps.Image) - } else { + } else if !ok { return c.buildServiceImage(ctx, ps.Image, ps.Build, ps.Unparsed.Platform, bo) } + // even when c.ImageExists returns true, we need to call c.EnsureImage + // because ps.PullMode can be "always". So no return here. + logrus.Debugf("Image %s already exists, not building", ps.Image) } - // even when c.ImageExists returns true, we need to call c.EnsureImage - // because ps.PullMode can be "always". logrus.Infof("Ensuring image %s", ps.Image) - if err := c.EnsureImage(ctx, ps.Image, ps.PullMode, ps.Unparsed.Platform, quiet); err != nil { + if err := c.EnsureImage(ctx, ps.Image, ps.PullMode, ps.Unparsed.Platform, ps, quiet); err != nil { return err } return nil diff --git a/pkg/cosignutil/cosignutil.go b/pkg/cosignutil/cosignutil.go new file mode 100644 index 00000000000..9dcaae778ac --- /dev/null +++ b/pkg/cosignutil/cosignutil.go @@ -0,0 +1,143 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package cosignutil + +import ( + "bufio" + "context" + "os" + "os/exec" + "strings" + + "github.com/containerd/nerdctl/pkg/imgutil" + "github.com/sirupsen/logrus" +) + +// SignCosign signs an image(`rawRef`) using a cosign private key (`keyRef`) +func SignCosign(rawRef string, keyRef string) error { + cosignExecutable, err := exec.LookPath("cosign") + if err != nil { + logrus.WithError(err).Error("cosign executable not found in path $PATH") + logrus.Info("you might consider installing cosign from: https://docs.sigstore.dev/cosign/installation") + return err + } + + cosignCmd := exec.Command(cosignExecutable, []string{"sign"}...) + cosignCmd.Env = os.Environ() + + // if key is empty, use keyless mode(experimental) + if keyRef != "" { + cosignCmd.Args = append(cosignCmd.Args, "--key", keyRef) + } else { + cosignCmd.Env = append(cosignCmd.Env, "COSIGN_EXPERIMENTAL=true") + } + + cosignCmd.Args = append(cosignCmd.Args, rawRef) + + logrus.Debugf("running %s %v", cosignExecutable, cosignCmd.Args) + + err = processCosignIO(cosignCmd) + if err != nil { + return err + } + + if err := cosignCmd.Wait(); err != nil { + return err + } + + return nil +} + +// VerifyCosign verifies an image(`rawRef`) with a cosign public key(`keyRef`) +// `hostsDirs` are used to resolve image `rawRef` +func VerifyCosign(ctx context.Context, rawRef string, keyRef string, hostsDirs []string) (string, error) { + digest, err := imgutil.ResolveDigest(ctx, rawRef, false, hostsDirs) + if err != nil { + logrus.WithError(err).Errorf("unable to resolve digest for an image %s: %v", rawRef, err) + return rawRef, err + } + ref := rawRef + if !strings.Contains(ref, "@") { + ref += "@" + digest + } + + logrus.Debugf("verifying image: %s", ref) + + cosignExecutable, err := exec.LookPath("cosign") + if err != nil { + logrus.WithError(err).Error("cosign executable not found in path $PATH") + logrus.Info("you might consider installing cosign from: https://docs.sigstore.dev/cosign/installation") + return ref, err + } + + cosignCmd := exec.Command(cosignExecutable, []string{"verify"}...) + cosignCmd.Env = os.Environ() + + // if key is empty, use keyless mode(experimental) + if keyRef != "" { + cosignCmd.Args = append(cosignCmd.Args, "--key", keyRef) + } else { + cosignCmd.Env = append(cosignCmd.Env, "COSIGN_EXPERIMENTAL=true") + } + + cosignCmd.Args = append(cosignCmd.Args, ref) + + logrus.Debugf("running %s %v", cosignExecutable, cosignCmd.Args) + + err = processCosignIO(cosignCmd) + if err != nil { + return ref, err + } + if err := cosignCmd.Wait(); err != nil { + return ref, err + } + + return ref, nil +} + +func processCosignIO(cosignCmd *exec.Cmd) error { + stdout, err := cosignCmd.StdoutPipe() + if err != nil { + logrus.Warn("cosign: " + err.Error()) + } + stderr, err := cosignCmd.StderrPipe() + if err != nil { + logrus.Warn("cosign: " + err.Error()) + } + if err := cosignCmd.Start(); err != nil { + // only return err if it's critical (cosign start failed.) + return err + } + + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + logrus.Info("cosign: " + scanner.Text()) + } + if err := scanner.Err(); err != nil { + logrus.Warn("cosign: " + err.Error()) + } + + errScanner := bufio.NewScanner(stderr) + for errScanner.Scan() { + logrus.Info("cosign: " + errScanner.Text()) + } + if err := errScanner.Err(); err != nil { + logrus.Warn("cosign: " + err.Error()) + } + + return nil +} From cfa27df401fc708c8a864eba9c5116f6e939e63f Mon Sep 17 00:00:00 2001 From: Jin Dong Date: Wed, 16 Nov 2022 17:35:42 +0000 Subject: [PATCH 2/2] Fix keyless mode in compose up|run Signed-off-by: Jin Dong --- cmd/nerdctl/compose.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cmd/nerdctl/compose.go b/cmd/nerdctl/compose.go index 06afc00b8a3..4e8c0108ee1 100644 --- a/cmd/nerdctl/compose.go +++ b/cmd/nerdctl/compose.go @@ -201,12 +201,14 @@ func getComposer(cmd *cobra.Command, client *containerd.Client) (*composer.Compo if !o.Experimental { return fmt.Errorf("cosign only work with enable experimental feature") } - keyRef, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignPublicKey] - if !ok { - return fmt.Errorf("no cosign public key, service: %s", ps.Unparsed.Name) + + // if key is given, use key mode, otherwise use keyless mode. + keyRef := "" + if keyVal, ok := ps.Unparsed.Extensions[serviceparser.ComposeCosignPublicKey]; ok { + keyRef = keyVal.(string) } - ref, err = cosignutil.VerifyCosign(ctx, ref, keyRef.(string), hostsDirs) + ref, err = cosignutil.VerifyCosign(ctx, ref, keyRef, hostsDirs) if err != nil { return err }