Skip to content

Commit

Permalink
feat: cosign sign use executable avoid deps
Browse files Browse the repository at this point in the history
Signed-off-by: Batuhan Apaydın <batuhan.apaydin@trendyol.com>
Co-authored-by: Furkan Türkal <furkan.turkal@trendyol.com>
Signed-off-by: Batuhan Apaydın <batuhan.apaydin@trendyol.com>
  • Loading branch information
developer-guy and Dentrax committed Dec 8, 2021
1 parent c3a5ce9 commit a006eaf
Show file tree
Hide file tree
Showing 9 changed files with 325 additions and 1 deletion.
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,8 @@ COPY . /go/src/github.com/containerd/nerdctl
WORKDIR /go/src/github.com/containerd/nerdctl
VOLUME /tmp
ENV CGO_ENABLED=0
# copy cosign binary for integration test
COPY --from=gcr.io/projectsigstore/cosign:v1.3.1@sha256:3cd9b3a866579dc2e0cf2fdea547f4c9a27139276cc373165c26842bc594b8bd /ko-app/cosign /usr/local/bin/cosign
# enable offline ipfs for integration test
COPY ./Dockerfile.d/test-integration-etc_containerd-stargz-grpc_config.toml /etc/containerd-stargz-grpc/config.toml
COPY ./Dockerfile.d/test-integration-ipfs-offline.service /usr/local/lib/systemd/system/
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

✅ Supports [P2P image distribution (IPFS)](./docs/ipfs.md)

✅ Supports [container image signing and verifying (cosign)](./docs/cosign.md)

nerdctl is a **non-core** sub-project of containerd.

## Examples
Expand Down
97 changes: 96 additions & 1 deletion cmd/nerdctl/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,20 @@
package main

import (
"bufio"
"context"
"errors"
"fmt"
"os"
"os/exec"

"github.com/containerd/nerdctl/pkg/imgutil"
"github.com/containerd/nerdctl/pkg/ipfs"
"github.com/containerd/nerdctl/pkg/platformutil"
"github.com/containerd/nerdctl/pkg/referenceutil"
"github.com/containerd/nerdctl/pkg/strutil"
httpapi "github.com/ipfs/go-ipfs-http-client"
"github.com/sirupsen/logrus"

"github.com/spf13/cobra"
)
Expand All @@ -38,6 +44,9 @@ func newPullCommand() *cobra.Command {
SilenceErrors: true,
}
pullCommand.Flags().String("unpack", "auto", "Unpack the image for the current single platform (auto/true/false)")
pullCommand.Flags().String("cosign-key", "",
"path to the public key file, KMS, URI or Kubernetes Secret")

pullCommand.RegisterFlagCompletionFunc("unpack", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"auto", "true", "false"}, cobra.ShellCompDirectiveNoFileComp
})
Expand All @@ -47,6 +56,7 @@ func newPullCommand() *cobra.Command {
pullCommand.Flags().StringSlice("platform", nil, "Pull content for a specific platform")
pullCommand.RegisterFlagCompletionFunc("platform", shellCompletePlatforms)
pullCommand.Flags().Bool("all-platforms", false, "Pull content for all platforms")
pullCommand.Flags().String("verify", "none", "Verify the image with none|cosign. Default none")
// #endregion
pullCommand.Flags().BoolP("quiet", "q", false, "Suppress verbose output")

Expand All @@ -57,6 +67,7 @@ func pullAction(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errors.New("image name needs to be specified")
}
rawRef := args[0]
client, ctx, cancel, err := newClient(cmd)
if err != nil {
return err
Expand Down Expand Up @@ -96,7 +107,16 @@ func pullAction(cmd *cobra.Command, args []string) error {
return err
}

verifier, err := cmd.Flags().GetString("verify")
if err != nil {
return err
}

if scheme, ref, err := referenceutil.ParseIPFSRefWithScheme(args[0]); err == nil {
if verifier != "none" {
return errors.New("--verify flag is not supported on IPFS as of now")
}

ipfsClient, err := httpapi.NewLocalApi()
if err != nil {
return err
Expand All @@ -108,5 +128,80 @@ func pullAction(cmd *cobra.Command, args []string) error {

_, err = imgutil.EnsureImage(ctx, client, cmd.OutOrStdout(), cmd.ErrOrStderr(), snapshotter, args[0],
"always", insecure, ocispecPlatforms, unpack, quiet)
return err

if err != nil {
return err
}

switch verifier {
case "cosign":
keyRef, err := cmd.Flags().GetString("cosign-key")
if err != nil {
return err
}

if err := verifyCosign(ctx, rawRef, keyRef); err != nil {
return err
}

return nil
case "none":
logrus.Debugf("verification process skipped")
default:
return fmt.Errorf("no verifier found: %s", verifier)
}
return nil
}

func verifyCosign(ctx context.Context, rawRef string, keyRef string) error {
digest, err := imgutil.ResolveDigest(ctx, rawRef, false)
rawRef = rawRef + "@" + digest
if err != nil {
logrus.WithError(err).Errorf("unable to resolve digest for an image %s: %v", rawRef, err)
return err
}

logrus.Debugf("verifying image: %s", rawRef)

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{"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, 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
}
121 changes: 121 additions & 0 deletions cmd/nerdctl/pull_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
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 main

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"testing"

"github.com/containerd/nerdctl/pkg/testutil"
"gotest.tools/v3/assert"
)

type cosignKeyPair struct {
publicKey string
privateKey string
cleanup func()
}

func newCosignKeyPair(t testing.TB, path string) *cosignKeyPair {
td, err := os.MkdirTemp(t.TempDir(), path)
assert.NilError(t, err)

cmd := exec.Command("cosign", "generate-key-pair")
cmd.Dir = td
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("failed to run %v: %v (%q)", cmd.Args, err, string(out))
}

publicKey := filepath.Join(td, "cosign.pub")
privateKey := filepath.Join(td, "cosign.key")

return &cosignKeyPair{
publicKey: publicKey,
privateKey: privateKey,
cleanup: func() {
_ = os.RemoveAll(td)
},
}
}

func TestImageVerifyWithCosign(t *testing.T) {
if _, err := exec.LookPath("cosign"); err != nil {
t.Skip()
}
testutil.DockerIncompatible(t)
t.Setenv("COSIGN_PASSWORD", "1")
keyPair := newCosignKeyPair(t, "cosign-key-pair")
defer keyPair.cleanup()
base := testutil.NewBase(t)
reg := newTestRegistry(base, "test-image-cosign")
defer reg.cleanup()
localhostIP := "127.0.0.1"
t.Logf("localhost IP=%q", localhostIP)
testImageRef := fmt.Sprintf("%s:%d/test-push-signed-image",
localhostIP, reg.listenPort)
t.Logf("testImageRef=%q", testImageRef)

dockerfile := fmt.Sprintf(`FROM %s
CMD ["echo", "nerdctl-build-test-string"]
`, testutil.CommonImage)

buildCtx, err := createBuildContext(dockerfile)
assert.NilError(t, err)
defer os.RemoveAll(buildCtx)

base.Cmd("build", "-t", testImageRef, buildCtx).AssertOK()
base.Cmd("push", testImageRef, "--sign=cosign", "--cosign-key="+keyPair.privateKey).AssertOK()
base.Cmd("pull", testImageRef, "--verify=cosign", "--cosign-key="+keyPair.publicKey).AssertOK()
}

func TestImageVerifyWithCosignShouldFailWhenKeyIsNotCorrect(t *testing.T) {
if _, err := exec.LookPath("cosign"); err != nil {
t.Skip()
}
testutil.DockerIncompatible(t)
t.Setenv("COSIGN_PASSWORD", "1")
keyPair := newCosignKeyPair(t, "cosign-key-pair")
defer keyPair.cleanup()
base := testutil.NewBase(t)
reg := newTestRegistry(base, "test-image-cosign")
defer reg.cleanup()
localhostIP := "127.0.0.1"
t.Logf("localhost IP=%q", localhostIP)
testImageRef := fmt.Sprintf("%s:%d/test-push-signed-image-wrong",
localhostIP, reg.listenPort)
t.Logf("testImageRef=%q", testImageRef)

dockerfile := fmt.Sprintf(`FROM %s
CMD ["echo", "nerdctl-build-test-string"]
`, testutil.CommonImage)

buildCtx, err := createBuildContext(dockerfile)
assert.NilError(t, err)
defer os.RemoveAll(buildCtx)

base.Cmd("build", "-t", testImageRef, buildCtx).AssertOK()
base.Cmd("push", testImageRef, "--sign=cosign", "--cosign-key="+keyPair.privateKey).AssertOK()

t.Setenv("COSIGN_PASSWORD", "2")

newKeyPair := newCosignKeyPair(t, "cosign-key-pair-test")

base.Cmd("pull", testImageRef, "--verify=cosign", "--cosign-key="+newKeyPair.publicKey).AssertFail()
}
76 changes: 76 additions & 0 deletions cmd/nerdctl/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@
package main

import (
"bufio"
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"

"github.com/containerd/containerd/content"
"github.com/containerd/containerd/images/converter"
Expand Down Expand Up @@ -61,6 +64,11 @@ func newPushCommand() *cobra.Command {
pushCommand.Flags().Bool("estargz", false, "Convert the image into eStargz")
pushCommand.Flags().Bool("ipfs-ensure-image", true, "Ensure the entire contents of the image is locally available before push")

pushCommand.Flags().String("sign", "none", "Sign the image with none|cosign. Default none")

pushCommand.Flags().String("cosign-key", "",
"path to the private key file, KMS URI or Kubernetes Secret")

return pushCommand
}

Expand Down Expand Up @@ -187,6 +195,30 @@ func pushAction(cmd *cobra.Command, args []string) error {
return err
}
}

signer, err := cmd.Flags().GetString("sign")

if err != nil {
return err
}
switch signer {
case "cosign":
keyRef, err := cmd.Flags().GetString("cosign-key")
if err != nil {
return err
}

err = signCosign(rawRef, keyRef)
if err != nil {
return err
}
case "none":
logrus.Debugf("signing process skipped")
default:
return fmt.Errorf("no signers found: %s", signer)

}

return nil
}

Expand Down Expand Up @@ -235,3 +267,47 @@ 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
}
1 change: 1 addition & 0 deletions cmd/nerdctl/run_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package main
import (
"context"
"fmt"

"github.com/containerd/containerd/containers"
"github.com/containerd/containerd/oci"
"github.com/docker/go-units"
Expand Down
Empty file added docs/cosign.md
Empty file.
Loading

0 comments on commit a006eaf

Please sign in to comment.