Skip to content

Commit

Permalink
Merge pull request #606 from AkihiroSuda/carry-556
Browse files Browse the repository at this point in the history
[Carry 556] feat: cosign sign
  • Loading branch information
AkihiroSuda authored Dec 13, 2021
2 parents 8619990 + 3dda9f8 commit e671087
Show file tree
Hide file tree
Showing 9 changed files with 419 additions and 4 deletions.
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
9 changes: 8 additions & 1 deletion 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 Expand Up @@ -126,6 +128,7 @@ Major:
- [P2P image distribution using IPFS](./docs/ipfs.md): `nerdctl run ipfs://CID`
- 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`

Minor:
- Namespacing: `nerdctl --namespace=<NS> ps` .
Expand Down Expand Up @@ -719,6 +722,8 @@ Flags:
- :nerd_face: `--all-platforms`: Pull content for all platforms
- :nerd_face: `--unpack`: Unpack the image for the current single platform (auto/true/false)
- :whale: `-q, --quiet`: Suppress verbose output
- :nerd_face: `--verify`: Verify the image (none|cosign). See [`docs/cosign.md`](./docs/cosign.md) for details.
- :nerd_face: `--cosign-key`: Path to the public key file, KMS, URI or Kubernetes Secret for `--verify=cosign`

Unimplemented `docker pull` flags: `--all-tags`, `--disable-content-trust` (default true)

Expand All @@ -732,6 +737,8 @@ Usage: `nerdctl push [OPTIONS] NAME[:TAG]`
Flags:
- :nerd_face: `--platform=(amd64|arm64|...)`: Push content for a specific platform
- :nerd_face: `--all-platforms`: Push content for all platforms
- :nerd_face: `--sign`: Sign the image (none|cosign). See [`docs/cosign.md`](./docs/cosign.md) for details.
- :nerd_face: `--cosign-key`: Path to the private key file, KMS, URI or Kubernetes Secret for `--sign=cosign`

Unimplemented `docker push` flags: `--all-tags`, `--disable-content-trust` (default true), `--quiet`

Expand Down Expand Up @@ -1223,7 +1230,7 @@ Image:

- `docker image prune`

- `docker trust *`
- `docker trust *` (Instead, nerdctl supports `nerdctl pull --verify=cosign` and `nerdctl push --sign=cosign`. See [`./docs/cosign.md`](docs/cosign.md).)
- `docker manifest *`

Network management:
Expand Down
111 changes: 108 additions & 3 deletions cmd/nerdctl/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,21 @@
package main

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

"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 @@ -48,6 +55,15 @@ func newPullCommand() *cobra.Command {
pullCommand.RegisterFlagCompletionFunc("platform", shellCompletePlatforms)
pullCommand.Flags().Bool("all-platforms", false, "Pull content for all platforms")
// #endregion

// #region verify flags
pullCommand.Flags().String("verify", "none", "Verify the image (none|cosign)")
pullCommand.RegisterFlagCompletionFunc("verify", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"none", "cosign"}, cobra.ShellCompDirectiveNoFileComp
})
pullCommand.Flags().String("cosign-key", "", "Path to the public key file, KMS, URI or Kubernetes Secret for --verify=cosign")
// #endregion

pullCommand.Flags().BoolP("quiet", "q", false, "Suppress verbose output")

return pullCommand
Expand All @@ -57,6 +73,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 +113,16 @@ func pullAction(cmd *cobra.Command, args []string) error {
return err
}

if scheme, ref, err := referenceutil.ParseIPFSRefWithScheme(args[0]); err == nil {
verifier, err := cmd.Flags().GetString("verify")
if err != nil {
return err
}

if scheme, ref, err := referenceutil.ParseIPFSRefWithScheme(rawRef); 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 @@ -106,7 +132,86 @@ func pullAction(cmd *cobra.Command, args []string) error {
return err
}

_, err = imgutil.EnsureImage(ctx, client, cmd.OutOrStdout(), cmd.ErrOrStderr(), snapshotter, args[0],
ref := rawRef
switch verifier {
case "cosign":
keyRef, err := cmd.Flags().GetString("cosign-key")
if err != nil {
return err
}

ref, err = verifyCosign(ctx, rawRef, keyRef)
if err != nil {
return err
}
case "none":
logrus.Debugf("verification process skipped")
default:
return fmt.Errorf("no verifier found: %s", verifier)
}

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

if err != nil {
return err
}

return nil
}

func verifyCosign(ctx context.Context, rawRef string, keyRef string) (string, error) {
digest, err := imgutil.ResolveDigest(ctx, rawRef, false)
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
}
120 changes: 120 additions & 0 deletions cmd/nerdctl/pull_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
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()
base.Cmd("pull", testImageRef, "--verify=cosign", "--cosign-key="+keyPair.publicKey).AssertOK()

t.Setenv("COSIGN_PASSWORD", "2")
newKeyPair := newCosignKeyPair(t, "cosign-key-pair-test")
base.Cmd("pull", testImageRef, "--verify=cosign", "--cosign-key="+newKeyPair.publicKey).AssertFail()
}
Loading

0 comments on commit e671087

Please sign in to comment.