Skip to content

Commit

Permalink
add cosign verify-dockerfile command
Browse files Browse the repository at this point in the history
Signed-off-by: Jake Sanders <[email protected]>
  • Loading branch information
Jake Sanders committed Jun 29, 2021
1 parent 17ad4d0 commit 2d5eca9
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 9 deletions.
18 changes: 12 additions & 6 deletions cmd/cosign/cli/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
Expand All @@ -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 <key path>|<key url>|<kms uri> <image uri>",
ShortUsage: "cosign verify -key <key path>|<key url>|<kms uri> <image uri> [<image uri> ...]",
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.
Expand All @@ -71,6 +74,9 @@ EXAMPLES
# verify cosign claims and signing certificates on the image
cosign verify <IMAGE>
# verify multiple images
cosign verify <IMAGE_1> <IMAGE_2> ...
# additionally verify specified annotations
cosign verify -a key1=val1 -a key2=val2 <IMAGE>
Expand Down
124 changes: 124 additions & 0 deletions cmd/cosign/cli/verify_dockerfile.go
Original file line number Diff line number Diff line change
@@ -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 <key path>|<key url>|<kms uri> <path/to/Dockerfile>",
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 <path/to/Dockerfile>
# only verify the base image (the last FROM image)
cosign verify-dockerfile -base-image-only <path/to/Dockerfile>
# additionally verify specified annotations
cosign verify-dockerfile -a key1=val1 -a key2=val2 <path/to/Dockerfile>
# (experimental) additionally, verify with the transparency log
COSIGN_EXPERIMENTAL=1 cosign verify-dockerfile <path/to/Dockerfile>
# verify images with public key
cosign verify-dockerfile -key cosign.pub <path/to/Dockerfile>
# verify images with public key provided by URL
cosign verify-dockerfile -key https://host.for/<FILE> <path/to/Dockerfile>
# verify images with public key stored in Google Cloud KMS
cosign verify-dockerfile -key gcpkms://projects/<PROJECT>/locations/global/keyRings/<KEYRING>/cryptoKeys/<KEY> <path/to/Dockerfile>`,
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
}
15 changes: 12 additions & 3 deletions cmd/cosign/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
10 changes: 10 additions & 0 deletions test/e2e_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ popd
go build -o cosign ./cmd/cosign
go test -tags=e2e -race ./...

export DISTROLESS_PUB_KEY=distroless.pub
wget -O ${DISTROLESS_PUB_KEY} https://raw.githubusercontent.com/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/unsigned_build_stage.Dockerfile); then false; fi
./cosign verify-dockerfile -base-image-only -key ${DISTROLESS_PUB_KEY} ./test/testdata/unsigned_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
# Image exists, but is unsigned
if (test_image="ubuntu" ./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)"
Expand Down
17 changes: 17 additions & 0 deletions test/testdata/fancy_from.Dockerfile
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions test/testdata/single_stage.Dockerfile
Original file line number Diff line number Diff line change
@@ -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
24 changes: 24 additions & 0 deletions test/testdata/unsigned_build_stage.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# 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

# an un(co)signed image
FROM ubuntu

# blah blah

FROM gcr.io/distroless/static
17 changes: 17 additions & 0 deletions test/testdata/with_arg.Dockerfile
Original file line number Diff line number Diff line change
@@ -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}

0 comments on commit 2d5eca9

Please sign in to comment.