Skip to content

Commit

Permalink
Add cosign verify-manifest command (#490)
Browse files Browse the repository at this point in the history
* Add `cosign verify-manifest` command

Signed-off-by: Joshua Hansen <j4ah4n@gmail.com>

* add allowed extension check

Signed-off-by: Batuhan Apaydın <batuhan.apaydin@trendyol.com>

* Remove Dockerfile refs from help

Signed-off-by: Joshua Hansen <josh.hansen@handsome.is>

* Fix lint, doc checks

Signed-off-by: Joshua Hansen <josh.hansen@handsome.is>

Co-authored-by: Batuhan Apaydın <batuhan.apaydin@trendyol.com>
  • Loading branch information
joshes and developer-guy committed Jul 29, 2021
1 parent 7e9cdfb commit 0fdfaa9
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 0 deletions.
130 changes: 130 additions & 0 deletions cmd/cosign/cli/verify_manifest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// 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 (
"context"
"flag"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"

"github.com/peterbourgon/ff/v3/ffcli"
"github.com/pkg/errors"
)

// VerifyCommand verifies all image signatures on a supplied k8s resource
type VerifyManifestCommand struct {
VerifyCommand
}

// Verify builds and returns an ffcli command
func VerifyManifest() *ffcli.Command {
cmd := VerifyManifestCommand{VerifyCommand: VerifyCommand{}}
flagset := flag.NewFlagSet("cosign verify-manifest", flag.ExitOnError)
applyVerifyFlags(&cmd.VerifyCommand, flagset)

return &ffcli.Command{
Name: "verify-manifest",
ShortUsage: "cosign verify-manifest -key <key path>|<key url>|<kms uri> <path/to/manifest>",
ShortHelp: "Verify all signatures of images specified in the manifest",
LongHelp: `Verify all signature of images in a Kubernetes resource manifest by checking claims
against the transparency log.
EXAMPLES
# verify cosign claims and signing certificates on images in the manifest
cosign verify-manifest <path/to/my-deployment.yaml>
# additionally verify specified annotations
cosign verify-manifest -a key1=val1 -a key2=val2 <path/to/my-deployment.yaml>
# (experimental) additionally, verify with the transparency log
COSIGN_EXPERIMENTAL=1 cosign verify-manifest <path/to/my-deployment.yaml>
# verify images with public key
cosign verify-manifest -key cosign.pub <path/to/my-deployment.yaml>
# verify images with public key provided by URL
cosign verify-manifest -key https://host.for/<FILE> <path/to/my-deployment.yaml>
# verify images with public key stored in Azure Key Vault
cosign verify-manifest -key azurekms://[VAULT_NAME][VAULT_URI]/[KEY] <path/to/my-deployment.yaml>
# verify images with public key stored in AWS KMS
cosign verify-manifest -key awskms://[ENDPOINT]/[ID/ALIAS/ARN] <path/to/my-deployment.yaml>
# verify images with public key stored in Google Cloud KMS
cosign verify-manifest -key gcpkms://projects/[PROJECT]/locations/global/keyRings/[KEYRING]/cryptoKeys/[KEY] <path/to/my-deployment.yaml>
# verify images with public key stored in Hashicorp Vault
cosign verify-manifest -key hashivault://[KEY] <path/to/my-deployment.yaml>`,

FlagSet: flagset,
Exec: cmd.Exec,
}
}

// Exec runs the verification command
func (c *VerifyManifestCommand) Exec(ctx context.Context, args []string) error {
if len(args) != 1 {
return flag.ErrHelp
}

manifestPath := args[0]

err := isExtensionAllowed(manifestPath)
if err != nil {
return errors.Wrap(err, "check if extension is valid")
}

manifest, err := ioutil.ReadFile(manifestPath)
if err != nil {
return fmt.Errorf("could not read manifest: %v", err)
}

images := getImagesFromYamlManifest(string(manifest))
if len(images) == 0 {
return errors.New("no images found in manifest")
}
fmt.Fprintf(os.Stderr, "Extracted image(s): %s\n", strings.Join(images, ", "))

return c.VerifyCommand.Exec(ctx, images)
}

func getImagesFromYamlManifest(manifest string) []string {
var images []string
re := regexp.MustCompile(`image:\s?(?P<Image>.*)\s?`)
for _, s := range re.FindAllStringSubmatch(manifest, -1) {
images = append(images, s[1])
}
return images
}

func isExtensionAllowed(ext string) error {
allowedExtensions := allowedExtensionsForManifest()
for _, v := range allowedExtensions {
if strings.EqualFold(filepath.Ext(strings.TrimSpace(ext)), v) {
return nil
}
}
return fmt.Errorf("only %v manifests are supported at this time", allowedExtensions)
}

func allowedExtensionsForManifest() []string {
return []string{".yaml", ".yml"}
}
89 changes: 89 additions & 0 deletions cmd/cosign/cli/verify_manifest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// 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 (
"reflect"
"testing"
)

const SingleContainerManifest = `
apiVersion: v1
kind: Pod
metadata:
name: single-pod
spec:
restartPolicy: Never
containers:
- name: nginx-container
image: nginx:1.21.1
`

const MultiContainerManifest = `
apiVersion: v1
kind: Pod
metadata:
name: multi-pod
spec:
restartPolicy: Never
volumes:
- name: shared-data
emptyDir: {}
containers:
- name: nginx-container
image: nginx:1.21.1
volumeMounts:
- name: shared-data
mountPath: /usr/share/nginx/html
- name: ubuntu-container
image: ubuntu:21.10
volumeMounts:
- name: shared-data
mountPath: /pod-data
command: ["/bin/sh"]
args: ["-c", "echo Hello, World > /pod-data/index.html"]
`

func TestGetImagesFromYamlManifest(t *testing.T) {
testCases := []struct {
name string
fileContents string
expected []string
}{
{
name: "single image",
fileContents: SingleContainerManifest,
expected: []string{"nginx:1.21.1"},
},
{
name: "multi image",
fileContents: MultiContainerManifest,
expected: []string{"nginx:1.21.1", "ubuntu:21.10"},
},
{
name: "no images found",
fileContents: ``,
expected: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := getImagesFromYamlManifest(tc.fileContents)
if !reflect.DeepEqual(tc.expected, got) {
t.Errorf("getImagesFromYamlManifest returned %v, wanted %v", got, tc.expected)
}
})
}
}
1 change: 1 addition & 0 deletions cmd/cosign/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ func main() {
cli.VerifyAttestation(),
cli.VerifyBlob(),
cli.VerifyDockerfile(),
cli.VerifyManifest(),
// Upload sub-tree
upload.Upload(),
// Download sub-tree
Expand Down
4 changes: 4 additions & 0 deletions test/e2e_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ test_image="gcr.io/distroless/base" ./cosign verify-dockerfile -key ${DISTROLESS
# Image exists, but is unsigned
if (test_image="ubuntu" ./cosign verify-dockerfile -key ${DISTROLESS_PUB_KEY} ./test/testdata/with_arg.Dockerfile); then false; fi

# Test `cosign verify-manifest`
./cosign verify-manifest -key ${DISTROLESS_PUB_KEY} ./test/testdata/signed_manifest.yaml
if (./cosign verify-manifest -key ${DISTROLESS_PUB_KEY} ./test/testdata/unsigned_manifest.yaml); 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
23 changes: 23 additions & 0 deletions test/testdata/signed_manifest.yaml
Original file line number Diff line number Diff line change
@@ -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.

apiVersion: v1
kind: Pod
metadata:
name: single-pod
spec:
restartPolicy: Never
containers:
- name: distroless
image: gcr.io/distroless/base
23 changes: 23 additions & 0 deletions test/testdata/unsigned_manifest.yaml
Original file line number Diff line number Diff line change
@@ -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.

apiVersion: v1
kind: Pod
metadata:
name: single-pod
spec:
restartPolicy: Never
containers:
- name: nginx-container
image: nginx

0 comments on commit 0fdfaa9

Please sign in to comment.