From b61aff62cd5088314cee39fae42cec0ad855bd7c Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sun, 20 Feb 2022 17:32:44 -0800 Subject: [PATCH] check notary v2 signature while looking for available signatures expanded repo info also provides information if manifests of repo is signed or not previously it was looking for only cosign signature. Signed-off-by: Shivam Mishra --- pkg/extensions/search/common/common_test.go | 160 +++++++++++++++++--- pkg/extensions/search/common/oci_layout.go | 62 ++++++-- 2 files changed, 192 insertions(+), 30 deletions(-) diff --git a/pkg/extensions/search/common/common_test.go b/pkg/extensions/search/common/common_test.go index ede7cfd0..03a23413 100644 --- a/pkg/extensions/search/common/common_test.go +++ b/pkg/extensions/search/common/common_test.go @@ -6,13 +6,19 @@ package common_test import ( "context" "encoding/json" + "fmt" + "io/ioutil" "os" + "os/exec" "path" "testing" "time" "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/sigstore/cosign/cmd/cosign/cli/generate" + "github.com/sigstore/cosign/cmd/cosign/cli/options" + "github.com/sigstore/cosign/cmd/cosign/cli/sign" . "github.com/smartystreets/goconvey/convey" "gopkg.in/resty.v1" "zotregistry.io/zot/pkg/api" @@ -71,7 +77,7 @@ type ImageInfo struct { Labels string } -func testSetup(t *testing.T) error { +func testSetup(t *testing.T, subpath string) error { t.Helper() dir := t.TempDir() @@ -79,19 +85,89 @@ func testSetup(t *testing.T) error { rootDir = dir - subRootDir = subDir + subRootDir = path.Join(subDir, subpath) err := CopyFiles("../../../../test/data", rootDir) if err != nil { return err } - err = CopyFiles("../../../../test/data", subDir) + return CopyFiles("../../../../test/data", subRootDir) +} + +func signUsingCosign(port string) error { + cwd, err := os.Getwd() + So(err, ShouldBeNil) + + defer func() { _ = os.Chdir(cwd) }() + + tdir, err := ioutil.TempDir("", "cosign") if err != nil { return err } - return nil + defer os.RemoveAll(tdir) + + _ = os.Chdir(tdir) + + // generate a keypair + os.Setenv("COSIGN_PASSWORD", "") + + err = generate.GenerateKeyPairCmd(context.TODO(), "", nil) + if err != nil { + return err + } + + imageURL := fmt.Sprintf("localhost:%s/%s@%s", port, "zot-cve-test", + "sha256:63a795ca90aa6e7cca60941e826810a4cd0a2e73ea02bf458241df2a5c973e29") + + // sign the image + return sign.SignCmd(&options.RootOptions{Verbose: true, Timeout: 1 * time.Minute}, + options.KeyOpts{KeyRef: path.Join(tdir, "cosign.key"), PassFunc: generate.GetPass}, + options.RegistryOptions{AllowInsecure: true}, + map[string]interface{}{"tag": "1.0"}, + []string{imageURL}, + "", "", true, "", "", "", false, false, "") +} + +func signUsingNotary(port string) error { + cwd, err := os.Getwd() + if err != nil { + return err + } + + defer func() { _ = os.Chdir(cwd) }() + + tdir, err := ioutil.TempDir("", "notation") + if err != nil { + return err + } + + defer os.RemoveAll(tdir) + + _ = os.Chdir(tdir) + + _, err = exec.LookPath("notation") + if err != nil { + return err + } + + os.Setenv("XDG_CONFIG_HOME", tdir) + + // generate a keypair + cmd := exec.Command("notation", "cert", "generate-test", "--trust", "notation-sign-test") + + err = cmd.Run() + if err != nil { + return err + } + + // sign the image + image := fmt.Sprintf("localhost:%s/%s:%s", port, "zot-cve-test", "0.0.1") + + cmd = exec.Command("notation", "sign", "--key", "notation-sign-test", "--plain-http", image) + + return cmd.Run() } func getTags() ([]common.TagInfo, []common.TagInfo) { @@ -184,7 +260,8 @@ func TestImageFormat(t *testing.T) { func TestLatestTagSearchHTTP(t *testing.T) { Convey("Test latest image search by timestamp", t, func() { - err := testSetup(t) + subpath := "/a" + err := testSetup(t, subpath) if err != nil { panic(err) } @@ -194,7 +271,7 @@ func TestLatestTagSearchHTTP(t *testing.T) { conf.HTTP.Port = port conf.Storage.RootDirectory = rootDir conf.Storage.SubPaths = make(map[string]config.StorageConfig) - conf.Storage.SubPaths["/a"] = config.StorageConfig{RootDirectory: subRootDir} + conf.Storage.SubPaths[subpath] = config.StorageConfig{RootDirectory: subRootDir} defaultVal := true conf.Extensions = &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{Enable: &defaultVal}, @@ -323,7 +400,8 @@ func TestLatestTagSearchHTTP(t *testing.T) { func TestExpandedRepoInfo(t *testing.T) { Convey("Test expanded repo info", t, func() { - err := testSetup(t) + subpath := "/a" + err := testSetup(t, subpath) if err != nil { panic(err) } @@ -333,7 +411,7 @@ func TestExpandedRepoInfo(t *testing.T) { conf.HTTP.Port = port conf.Storage.RootDirectory = rootDir conf.Storage.SubPaths = make(map[string]config.StorageConfig) - conf.Storage.SubPaths["/a"] = config.StorageConfig{RootDirectory: subRootDir} + conf.Storage.SubPaths[subpath] = config.StorageConfig{RootDirectory: subRootDir} defaultVal := true conf.Extensions = &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{Enable: &defaultVal}, @@ -375,7 +453,7 @@ func TestExpandedRepoInfo(t *testing.T) { So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) - query := "{ExpandedRepoInfo(repo:\"zot-test\"){Manifests%20{Digest%20IsSigned%20Tag%20Layers%20{Size%20Digest}}}}" + query := "{ExpandedRepoInfo(repo:\"zot-cve-test\"){Manifests%20{Digest%20IsSigned%20Tag%20Layers%20{Size%20Digest}}}}" resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + query) So(resp, ShouldNotBeNil) @@ -388,15 +466,15 @@ func TestExpandedRepoInfo(t *testing.T) { So(err, ShouldBeNil) So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests), ShouldNotEqual, 0) So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests[0].Layers), ShouldNotEqual, 0) + for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.Manifests { + if m.Digest == "63a795ca90aa6e7cca60941e826810a4cd0a2e73ea02bf458241df2a5c973e29" { + So(m.IsSigned, ShouldEqual, false) + } + } - query = "{ExpandedRepoInfo(repo:\"\"){Manifests%20{Digest%20Tag%20IsSigned%20Layers%20{Size%20Digest}}}}" - - resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + query) - So(resp, ShouldNotBeNil) + err = signUsingCosign(port) So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, 200) - query = "{ExpandedRepoInfo(repo:\"a/zot-test\"){Manifests%20{Digest%20Tag%20IsSigned%20%Layers%20{Size%20Digest}}}}" resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + query) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) @@ -406,16 +484,58 @@ func TestExpandedRepoInfo(t *testing.T) { So(err, ShouldBeNil) So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests), ShouldNotEqual, 0) So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests[0].Layers), ShouldNotEqual, 0) + for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.Manifests { + if m.Digest == "63a795ca90aa6e7cca60941e826810a4cd0a2e73ea02bf458241df2a5c973e29" { + So(m.IsSigned, ShouldEqual, true) + } + } + + query = "{ExpandedRepoInfo(repo:\"\"){Manifests%20{Digest%20Tag%20IsSigned%20Layers%20{Size%20Digest}}}}" + + resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + query) + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + query = "{ExpandedRepoInfo(repo:\"zot-test\"){Manifests%20{Digest%20Tag%20IsSigned%20%Layers%20{Size%20Digest}}}}" + resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + query) + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + err = json.Unmarshal(resp.Body(), responseStruct) + So(err, ShouldBeNil) + So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests), ShouldNotEqual, 0) + So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests[0].Layers), ShouldNotEqual, 0) + for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.Manifests { + if m.Digest == "2bacca16b9df395fc855c14ccf50b12b58d35d468b8e7f25758aff90f89bf396" { + So(m.IsSigned, ShouldEqual, false) + } + } + + err = signUsingNotary(port) + So(err, ShouldBeNil) + + resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "/query?query=" + query) + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + err = json.Unmarshal(resp.Body(), responseStruct) + So(err, ShouldBeNil) + So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests), ShouldNotEqual, 0) + So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests[0].Layers), ShouldNotEqual, 0) + for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.Manifests { + if m.Digest == "2bacca16b9df395fc855c14ccf50b12b58d35d468b8e7f25758aff90f89bf396" { + So(m.IsSigned, ShouldEqual, true) + } + } var manifestDigest digest.Digest manifestDigest, _, _ = GetOciLayoutDigests("../../../../test/data/zot-test") err = os.Remove(path.Join(rootDir, "zot-test/blobs/sha256", manifestDigest.Encoded())) - if err != nil { - panic(err) - } - - query = "{ExpandedRepoInfo(repo:\"zot-test\"){Manifests%20{Digest%20Tag%20IsSigned%20%Layers%20{Size%20Digest}}}}" + So(err, ShouldBeNil) resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + query) So(resp, ShouldNotBeNil) diff --git a/pkg/extensions/search/common/oci_layout.go b/pkg/extensions/search/common/oci_layout.go index f4131fab..90a05805 100644 --- a/pkg/extensions/search/common/oci_layout.go +++ b/pkg/extensions/search/common/oci_layout.go @@ -4,6 +4,7 @@ package common import ( "encoding/json" goerrors "errors" + "fmt" "path" "strconv" "strings" @@ -18,8 +19,6 @@ import ( "zotregistry.io/zot/pkg/storage" ) -const cosignedAnnotation = "dev.cosign.signature.baseimage" - // OciLayoutInfo ... type OciLayoutUtils struct { Log log.Logger @@ -202,6 +201,51 @@ func (olu OciLayoutUtils) GetImageTagsWithTimestamp(repo string) ([]TagInfo, err return tagsInfo, nil } +// check notary signature corresponding to repo name, manifest digest and mediatype. +func (olu OciLayoutUtils) checkNotarySignature(name, digest, mediaType string) bool { + imageStore := olu.StoreController.GetImageStore(name) + + _, err := imageStore.GetReferrers(name, digest, mediaType) + if err != nil { + olu.Log.Info().Str("repo", name).Str("digest", + digest).Str("mediatype", mediaType).Msg("invalid notary signature") + + return false + } + + return true +} + +// check cosign signature corresponding to manifest. +func (olu OciLayoutUtils) checkCosignSignature(name, digest string) bool { + imageStore := olu.StoreController.GetImageStore(name) + + // if manifest is signed using cosign mechanism, cosign adds a new manifest. + // new manifest is tagged as sha256-.sig. + reference := fmt.Sprintf("sha256-%s.sig", digest) + + _, _, _, err := imageStore.GetImageManifest(name, reference) // nolint: dogsled + if err != nil { + olu.Log.Info().Str("repo", name).Str("digest", + digest).Msg("invalid cosign signature") + + return false + } + + return true +} + +// checks if manifest is signed or not +// checks for notary or cosign signature +// if cosign signature found it does not looks for notary signature. +func (olu OciLayoutUtils) checkManifestSignature(name, digest, mediaType string) bool { + if !olu.checkCosignSignature(name, digest) { + return olu.checkNotarySignature(name, digest, mediaType) + } + + return true +} + func (olu OciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error) { repo := RepoInfo{} @@ -214,27 +258,29 @@ func (olu OciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error) { return RepoInfo{}, err } - for _, manifest := range manifestList { + for _, man := range manifestList { manifestInfo := Manifest{} - manifestInfo.Digest = manifest.Digest.Encoded() + manifestInfo.Digest = man.Digest.Encoded() manifestInfo.IsSigned = false - tag, ok := manifest.Annotations[ispec.AnnotationRefName] + tag, ok := man.Annotations[ispec.AnnotationRefName] if !ok { tag = "latest" } manifestInfo.Tag = tag - manifest, err := olu.GetImageBlobManifest(name, manifest.Digest) + manifest, err := olu.GetImageBlobManifest(name, man.Digest) if err != nil { olu.Log.Error().Err(err).Msg("error getting image manifest blob") return RepoInfo{}, err } + manifestInfo.IsSigned = olu.checkManifestSignature(name, man.Digest.Encoded(), man.MediaType) + layers := make([]Layer, 0) for _, layer := range manifest.Layers { @@ -245,10 +291,6 @@ func (olu OciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error) { layerInfo.Size = strconv.FormatInt(layer.Size, 10) layers = append(layers, layerInfo) - - if _, ok := layer.Annotations[cosignedAnnotation]; ok { - manifestInfo.IsSigned = true - } } manifestInfo.Layers = layers