From ee95ab0ffc08ec6a2db326c1eb6f77ea1961fddc Mon Sep 17 00:00:00 2001 From: Andreea Lupu <58118008+Andreea-Lupu@users.noreply.github.com> Date: Mon, 13 Feb 2023 20:43:52 +0200 Subject: [PATCH] fix: call notation-go libs instead of using notation binary (#1104) fix: add loading notation path Signed-off-by: Andreea-Lupu Co-authored-by: Roxana Nemulescu --- Makefile | 7 +- go.mod | 6 +- go.sum | 15 +- pkg/api/controller_test.go | 153 +++--- pkg/api/routes.go | 19 +- pkg/cli/client.go | 8 +- pkg/cli/image_cmd_test.go | 43 +- pkg/extensions/search/common/common_test.go | 83 +++- pkg/extensions/search/common/oci_layout.go | 6 +- pkg/extensions/sync/on_demand.go | 37 +- pkg/extensions/sync/signatures.go | 48 +- pkg/extensions/sync/sync.go | 18 +- pkg/extensions/sync/sync_internal_test.go | 97 +++- pkg/extensions/sync/sync_test.go | 429 ++++++++++++---- pkg/storage/common.go | 9 +- pkg/storage/common_test.go | 21 +- pkg/storage/local/local.go | 15 +- pkg/test/common.go | 513 +++++++++++++++++++- pkg/test/common_test.go | 481 +++++++++++++++++- test/blackbox/annotations.bats | 26 +- test/blackbox/sync.bats | 30 +- 21 files changed, 1731 insertions(+), 333 deletions(-) diff --git a/Makefile b/Makefile index 547d8fd6..46c29707 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ STACKER := $(shell which stacker) GOLINTER := $(TOOLSDIR)/bin/golangci-lint GOLINTER_VERSION := v1.49.0 NOTATION := $(TOOLSDIR)/bin/notation +NOTATION_VERSION := 1.0.0-rc.1 COSIGN := $(TOOLSDIR)/bin/cosign HELM := $(TOOLSDIR)/bin/helm ORAS := $(TOOLSDIR)/bin/oras @@ -85,7 +86,7 @@ exporter-minimal: modcheck build-metadata .PHONY: test test: $(if $(findstring ui,$(EXTENSIONS)), ui) -test: check-skopeo $(TESTDATA) $(NOTATION) $(ORAS) +test: check-skopeo $(TESTDATA) $(ORAS) go test -failfast -tags $(EXTENSIONS),containers_image_openpgp -v -trimpath -race -timeout 15m -cover -coverpkg ./... -coverprofile=coverage-extended.txt -covermode=atomic ./... go test -failfast -tags containers_image_openpgp -v -trimpath -race -cover -coverpkg ./... -coverprofile=coverage-minimal.txt -covermode=atomic ./... # development-mode unit tests possibly using failure injection @@ -95,7 +96,7 @@ test: check-skopeo $(TESTDATA) $(NOTATION) $(ORAS) .PHONY: privileged-test privileged-test: $(if $(findstring ui,$(EXTENSIONS)), ui) -privileged-test: check-skopeo $(TESTDATA) $(NOTATION) +privileged-test: check-skopeo $(TESTDATA) go test -failfast -tags needprivileges,$(EXTENSIONS),containers_image_openpgp -v -trimpath -race -timeout 15m -cover -coverpkg ./... -coverprofile=coverage-dev-needprivileges.txt -covermode=atomic ./pkg/storage/... ./pkg/cli/... -run ^TestElevatedPrivileges $(TESTDATA): check-skopeo @@ -115,7 +116,7 @@ check-skopeo: $(NOTATION): mkdir -p $(TOOLSDIR)/bin - curl -Lo notation.tar.gz https://github.com/notaryproject/notation/releases/download/v0.7.1-alpha.1/notation_0.7.1-alpha.1_linux_amd64.tar.gz + curl -Lo notation.tar.gz https://github.com/notaryproject/notation/releases/download/v$(NOTATION_VERSION)/notation_$(NOTATION_VERSION)_linux_amd64.tar.gz tar xvzf notation.tar.gz -C $(TOOLSDIR)/bin notation rm notation.tar.gz diff --git a/go.mod b/go.mod index 7b5be36c..5aad12c8 100644 --- a/go.mod +++ b/go.mod @@ -55,10 +55,11 @@ require ( github.com/aws/aws-sdk-go-v2/service/dynamodb v1.18.1 github.com/containers/image/v5 v5.23.0 github.com/gobwas/glob v0.2.3 - github.com/notaryproject/notation-go v0.12.0-beta.1 + github.com/notaryproject/notation-go v1.0.0-rc.1 github.com/opencontainers/distribution-spec/specs-go v0.0.0-20230117141039-067a0f5b0e25 github.com/sigstore/cosign v1.13.1 github.com/swaggo/http-swagger v1.3.3 + oras.land/oras-go/v2 v2.0.0-rc.5 ) require ( @@ -376,7 +377,7 @@ require ( github.com/mozillazg/docker-credential-acr-helper v0.3.0 // indirect github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de // indirect github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 // indirect - github.com/notaryproject/notation-core-go v0.2.0-beta.1 // indirect + github.com/notaryproject/notation-core-go v1.0.0-rc.1 github.com/oklog/ulid v1.3.1 // indirect github.com/open-policy-agent/opa v0.45.0 // indirect github.com/opencontainers/runc v1.1.4 // indirect @@ -495,7 +496,6 @@ require ( k8s.io/klog/v2 v2.70.1 // indirect k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect - oras.land/oras-go/v2 v2.0.0-rc.3 // indirect sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect sigs.k8s.io/release-utils v0.7.3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect diff --git a/go.sum b/go.sum index ae9de82c..e2cd89ef 100644 --- a/go.sum +++ b/go.sum @@ -839,6 +839,7 @@ github.com/fullstorydev/grpcurl v1.8.1/go.mod h1:3BWhvHZwNO7iLXaQlojdg5NA6SxUDeP github.com/fullstorydev/grpcurl v1.8.6/go.mod h1:WhP7fRQdhxz2TkL97u+TCb505sxfH78W1usyoB3tepw= github.com/fullstorydev/grpcurl v1.8.7 h1:xJWosq3BQovQ4QrdPO72OrPiWuGgEsxY8ldYsJbPrqI= github.com/fullstorydev/grpcurl v1.8.7/go.mod h1:pVtM4qe3CMoLaIzYS8uvTuDj2jVYmXqMUkZeijnXp/E= +github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/getlantern/deepcopy v0.0.0-20160317154340-7f45deb8130a h1:yU/FENpkHYISWsQrbr3pcZOBj0EuRjPzNc1+dTCLu44= github.com/getlantern/deepcopy v0.0.0-20160317154340-7f45deb8130a/go.mod h1:AEugkNu3BjBxyz958nJ5holD9PRjta6iprcoUauDbU4= @@ -1632,10 +1633,10 @@ github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 h1:D9EvfGQvlkKaD github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484/go.mod h1:O1EljZ+oHprtxDDPHiMWVo/5dBT6PlvWX5PSwj80aBA= github.com/nmcclain/ldap v0.0.0-20210720162743-7f8d1e44eeba h1:DO8NFYdcRv1dnyAINJIBm6Bw2XibtLvQniNFGzf2W8E= github.com/nmcclain/ldap v0.0.0-20210720162743-7f8d1e44eeba/go.mod h1:4S0XndRL8HNOaQBfdViJ2F/GPCgL524xlXRuXFH12/U= -github.com/notaryproject/notation-core-go v0.2.0-beta.1 h1:8tFxNycWCcPLti9ZYST5kjkX2wMXtX9YPvMjiBAQ1tA= -github.com/notaryproject/notation-core-go v0.2.0-beta.1/go.mod h1:s8DZptmN1rZS0tBLTPt/w+d4o6eAcGWTYYJlXaJhQ4U= -github.com/notaryproject/notation-go v0.12.0-beta.1 h1:LATXX7gt/Y7a+vqLVN4Ydmd6GfaPAFRdKgUEjaEYhUM= -github.com/notaryproject/notation-go v0.12.0-beta.1/go.mod h1:sfOLDfdt7IXtzU9tyGwhsWDYY357+OWr1ktCfHfLdOk= +github.com/notaryproject/notation-core-go v1.0.0-rc.1 h1:ACi0gr6mD1bzp9+gu3P0meJ/N6iWHlyM9zgtdnooNAA= +github.com/notaryproject/notation-core-go v1.0.0-rc.1/go.mod h1:n8Gbvl9sKa00KptkKEL5XKUyMTIALe74QipKauE2rj4= +github.com/notaryproject/notation-go v1.0.0-rc.1 h1:WobIGCUPcPUDCD2qGMCccTfLm2J5y1bsh4SFVsyMIaA= +github.com/notaryproject/notation-go v1.0.0-rc.1/go.mod h1:xk4q0GXqGfEiy7WmyHi3Om3OM2dgHk0OHL6NIiJv5vA= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -2089,6 +2090,7 @@ github.com/vbauerster/mpb/v7 v7.5.3 h1:BkGfmb6nMrrBQDFECR/Q7RkKCw7ylMetCb4079CGs github.com/vbauerster/mpb/v7 v7.5.3/go.mod h1:i+h4QY6lmLvBNK2ah1fSreiw3ajskRlBp9AhY/PnuOE= github.com/vektah/gqlparser/v2 v2.5.1 h1:ZGu+bquAY23jsxDRcYpWjttRZrUz07LbiY77gUOHcr4= github.com/vektah/gqlparser/v2 v2.5.1/go.mod h1:mPgqFBu/woKTVYWyNk8cO3kh4S/f4aRFZrvOnp3hmCs= +github.com/veraison/go-cose v1.0.0-rc.2 h1:zH3QmP4N5kwpdGauceIT3aJm8iUyV9OqpUOb+7CF7rQ= github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= @@ -2106,6 +2108,7 @@ github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgq github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= github.com/xanzy/go-gitlab v0.73.1 h1:UMagqUZLJdjss1SovIC+kJCH4k2AZWXl58gJd38Y/hI= github.com/xanzy/go-gitlab v0.73.1/go.mod h1:d/a0vswScO7Agg1CZNz15Ic6SSvBG9vfw8egL99t4kA= @@ -3194,8 +3197,8 @@ modernc.org/z v1.5.1 h1:RTNHdsrOpeoSeOF4FbzTo8gBYByaJ5xT7NgZ9ZqRiJM= modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= oras.land/oras-go v1.2.0 h1:yoKosVIbsPoFMqAIFHTnrmOuafHal+J/r+I5bdbVWu4= oras.land/oras-go v1.2.0/go.mod h1:pFNs7oHp2dYsYMSS82HaX5l4mpnGO7hbpPN6EWH2ltc= -oras.land/oras-go/v2 v2.0.0-rc.3 h1:O4GeIwJ9Ge7rbCkqa/M7DLrL55ww+ZEc+Rhc63OYitU= -oras.land/oras-go/v2 v2.0.0-rc.3/go.mod h1:PrY+cCglzK/DrQoJUtxbYVbL94ZHecVS3eJR01RglpE= +oras.land/oras-go/v2 v2.0.0-rc.5 h1:enT2ZMNo383bH3INm1/+mw4d09AaMbqx0BMhsgEDUSg= +oras.land/oras-go/v2 v2.0.0-rc.5/go.mod h1:YGHvWBGuqRlZgUyXUIoKsR3lcuCOb3DAtG0SEsEw1iY= pack.ag/amqp v0.11.2/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index 83a1cbee..56ed40e5 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -18,7 +18,6 @@ import ( "net/http/httptest" "net/url" "os" - "os/exec" "path" "regexp" "strconv" @@ -3964,53 +3963,28 @@ func TestImageSignatures(t *testing.T) { tdir := t.TempDir() _ = os.Chdir(tdir) - // "notation" (notaryv2) doesn't yet support exported apis, so use the binary instead - notPath, err := exec.LookPath("notation") - So(notPath, ShouldNotBeNil) + test.NotationPathLock.Lock() + defer test.NotationPathLock.Unlock() + + test.LoadNotationPath(tdir) + + err = test.GenerateNotationCerts(tdir, "good") So(err, ShouldBeNil) - os.Setenv("XDG_CONFIG_HOME", tdir) - - // generate a keypair - cmd := exec.Command("notation", "cert", "generate-test", "--trust", "good") - err = cmd.Run() + err = test.GenerateNotationCerts(tdir, "bad") So(err, ShouldBeNil) - // generate another keypair - cmd = exec.Command("notation", "cert", "generate-test", "--trust", "bad") - err = cmd.Run() - So(err, ShouldBeNil) - - // sign the image image := fmt.Sprintf("localhost:%s/%s:%s", port, repoName, "1.0") - cmd = exec.Command("notation", "sign", "--key", "good", "--plain-http", image) - err = cmd.Run() + err = test.SignWithNotation("good", image, tdir) So(err, ShouldBeNil) - // verify the image - cmd = exec.Command("notation", "verify", "--cert", "good", "--plain-http", image) - out, err := cmd.CombinedOutput() + err = test.VerifyWithNotation(image, tdir) So(err, ShouldBeNil) - msg := string(out) - So(msg, ShouldNotBeEmpty) - So(strings.Contains(msg, "verification failure"), ShouldBeFalse) // check list - cmd = exec.Command("notation", "list", "--plain-http", image) - out, err = cmd.CombinedOutput() + sigs, err := test.ListNotarySignatures(image, tdir) + So(len(sigs), ShouldEqual, 1) So(err, ShouldBeNil) - msg = strings.TrimSuffix(string(out), "\n") - So(msg, ShouldNotBeEmpty) - _, err = godigest.Parse(msg) - So(err, ShouldBeNil) - - // verify the image with incorrect key - cmd = exec.Command("notation", "verify", "--cert", "bad", "--plain-http", image) - out, err = cmd.CombinedOutput() - So(err, ShouldNotBeNil) - msg = string(out) - So(msg, ShouldNotBeEmpty) - So(strings.Contains(msg, "verification failure"), ShouldBeTrue) // check unsupported manifest media type resp, err := resty.R().SetHeader("Content-Type", "application/vnd.unsupported.image.manifest.v1+json"). @@ -4027,49 +4001,45 @@ func TestImageSignatures(t *testing.T) { Convey("Validate corrupted signature", func() { // verify with corrupted signature resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get( - fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, repoName, digest.String())) + fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, digest.String())) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusOK) - var refs api.ReferenceList + var refs ispec.Index err = json.Unmarshal(resp.Body(), &refs) So(err, ShouldBeNil) - So(len(refs.References), ShouldEqual, 1) + So(len(refs.Manifests), ShouldEqual, 1) err = os.WriteFile(path.Join(dir, repoName, "blobs", - strings.ReplaceAll(refs.References[0].Digest.String(), ":", "/")), []byte("corrupt"), 0o600) + strings.ReplaceAll(refs.Manifests[0].Digest.String(), ":", "/")), []byte("corrupt"), 0o600) So(err, ShouldBeNil) resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get( - fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, repoName, digest.String())) + fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, digest.String())) So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) - cmd = exec.Command("notation", "verify", "--cert", "good", "--plain-http", image) - out, err = cmd.CombinedOutput() + So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError) + + err = test.VerifyWithNotation(image, tdir) So(err, ShouldNotBeNil) - msg = string(out) - So(msg, ShouldNotBeEmpty) }) Convey("Validate deleted signature", func() { // verify with corrupted signature resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get( - fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, repoName, digest.String())) + fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, digest.String())) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusOK) - var refs api.ReferenceList + var refs ispec.Index err = json.Unmarshal(resp.Body(), &refs) So(err, ShouldBeNil) - So(len(refs.References), ShouldEqual, 1) + So(len(refs.Manifests), ShouldEqual, 1) err = os.Remove(path.Join(dir, repoName, "blobs", - strings.ReplaceAll(refs.References[0].Digest.String(), ":", "/"))) + strings.ReplaceAll(refs.Manifests[0].Digest.String(), ":", "/"))) So(err, ShouldBeNil) resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get( - fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, repoName, digest.String())) + fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, digest.String())) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) - cmd = exec.Command("notation", "verify", "--cert", "good", "--plain-http", image) - out, err = cmd.CombinedOutput() + + err = test.VerifyWithNotation(image, tdir) So(err, ShouldNotBeNil) - msg = string(out) - So(msg, ShouldNotBeEmpty) }) }) @@ -5192,7 +5162,7 @@ func TestManifestImageIndex(t *testing.T) { Layers: layers, Manifest: manifest, }, baseURL, repoName) - So(err, ShouldBeNil) + So(err, ShouldNotBeNil) content, err = json.Marshal(manifest) So(err, ShouldBeNil) @@ -5217,7 +5187,7 @@ func TestManifestImageIndex(t *testing.T) { Layers: layers, Manifest: manifest, }, baseURL, repoName) - So(err, ShouldBeNil) + So(err, ShouldNotBeNil) content, err = json.Marshal(manifest) So(err, ShouldBeNil) @@ -5273,7 +5243,7 @@ func TestManifestImageIndex(t *testing.T) { Layers: layers, Manifest: manifest, }, baseURL, repoName) - So(err, ShouldBeNil) + So(err, ShouldNotBeNil) content, err = json.Marshal(manifest) So(err, ShouldBeNil) digest = godigest.FromBytes(content) @@ -5446,7 +5416,7 @@ func TestManifestImageIndex(t *testing.T) { Layers: layers, Manifest: manifest, }, baseURL, repoName) - So(err, ShouldBeNil) + So(err, ShouldNotBeNil) content, err = json.Marshal(manifest) So(err, ShouldBeNil) digest = godigest.FromBytes(content) @@ -6200,23 +6170,17 @@ func TestGCSignaturesAndUntaggedManifests(t *testing.T) { So(err, ShouldBeNil) - // "notation" (notaryv2) doesn't yet support exported apis, so use the binary instead - notPath, err := exec.LookPath("notation") - So(notPath, ShouldNotBeNil) - So(err, ShouldBeNil) + test.NotationPathLock.Lock() + defer test.NotationPathLock.Unlock() - os.Setenv("XDG_CONFIG_HOME", tdir) + test.LoadNotationPath(tdir) // generate a keypair - cmd := exec.Command("notation", "cert", "generate-test", "--trust", "good") - output, err := cmd.CombinedOutput() - t.Log(string(output)) + err = test.GenerateNotationCerts(tdir, "good") So(err, ShouldBeNil) // sign the image - cmd = exec.Command("notation", "sign", "--key", "good", "--plain-http", image) - output, err = cmd.CombinedOutput() - t.Log(string(output)) + err = test.SignWithNotation("good", image, tdir) So(err, ShouldBeNil) // get cosign signature manifest @@ -6228,15 +6192,21 @@ func TestGCSignaturesAndUntaggedManifests(t *testing.T) { // get notation signature manifest resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get( - fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, repoName, digest.String())) + fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, digest.String())) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusOK) + var index ispec.Index + + err = json.Unmarshal(resp.Body(), &index) + So(err, ShouldBeNil) + So(len(index.Manifests), ShouldEqual, 1) + Convey("Trigger gcNotationSignatures() error", func() { - var refs api.ReferenceList + var refs ispec.Index err = json.Unmarshal(resp.Body(), &refs) - err := os.Chmod(path.Join(dir, repoName, "blobs", "sha256", refs.References[0].Digest.Encoded()), 0o000) + err := os.Chmod(path.Join(dir, repoName, "blobs", "sha256", refs.Manifests[0].Digest.Encoded()), 0o000) So(err, ShouldBeNil) // trigger gc @@ -6250,9 +6220,26 @@ func TestGCSignaturesAndUntaggedManifests(t *testing.T) { Manifest: manifest, Tag: tag, }, baseURL, repoName) + So(err, ShouldNotBeNil) + + err = os.Chmod(path.Join(dir, repoName, "blobs", "sha256", refs.Manifests[0].Digest.Encoded()), 0o755) So(err, ShouldBeNil) - err = os.Chmod(path.Join(dir, repoName, "blobs", "sha256", refs.References[0].Digest.Encoded()), 0o755) + content, err := os.ReadFile(path.Join(dir, repoName, "blobs", "sha256", refs.Manifests[0].Digest.Encoded())) + So(err, ShouldBeNil) + err = os.WriteFile(path.Join(dir, repoName, "blobs", "sha256", refs.Manifests[0].Digest.Encoded()), []byte("corrupt"), 0o600) //nolint:lll + So(err, ShouldBeNil) + + err = test.UploadImage( + test.Image{ + Config: cfg, + Layers: layers, + Manifest: manifest, + Tag: tag, + }, baseURL, repoName) + So(err, ShouldNotBeNil) + + err = os.WriteFile(path.Join(dir, repoName, "blobs", "sha256", refs.Manifests[0].Digest.Encoded()), content, 0o600) So(err, ShouldBeNil) }) @@ -6296,14 +6283,22 @@ func TestGCSignaturesAndUntaggedManifests(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get( - fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, repoName, digest.String())) + fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, digest.String())) So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + err = json.Unmarshal(resp.Body(), &index) + So(err, ShouldBeNil) + So(len(index.Manifests), ShouldEqual, 0) resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get( - fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, repoName, newManifestDigest.String())) + fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, newManifestDigest.String())) So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + err = json.Unmarshal(resp.Body(), &index) + So(err, ShouldBeNil) + So(len(index.Manifests), ShouldEqual, 0) // untagged image should also be gc'ed resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, untaggedManifestDigest)) diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 019c993c..30b8ceb4 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -431,7 +431,7 @@ func getReferrers(ctx context.Context, routeHandler *RouteHandler, artifactTypes []string, ) (ispec.Index, error) { references, err := imgStore.GetReferrers(name, digest, artifactTypes) - if err != nil { + if err != nil || len(references.Manifests) == 0 { if routeHandler.c.Config.Extensions != nil && routeHandler.c.Config.Extensions.Sync != nil && *routeHandler.c.Config.Extensions.Sync.Enable { @@ -495,11 +495,11 @@ func (rh *RouteHandler) GetReferrers(response http.ResponseWriter, request *http if errors.Is(err, zerr.ErrManifestNotFound) || errors.Is(err, zerr.ErrRepoNotFound) { rh.c.Log.Error().Err(err).Str("name", name).Str("digest", digest.String()).Msg("manifest not found") response.WriteHeader(http.StatusNotFound) + } else { + rh.c.Log.Error().Err(err).Str("name", name).Str("digest", digest.String()).Msg("unable to get references") + response.WriteHeader(http.StatusInternalServerError) } - rh.c.Log.Error().Err(err).Str("name", name).Str("digest", digest.String()).Msg("unable to get references") - response.WriteHeader(http.StatusInternalServerError) - return } @@ -1662,7 +1662,7 @@ func getOrasReferrers(ctx context.Context, routeHandler *RouteHandler, if routeHandler.c.Config.Extensions != nil && routeHandler.c.Config.Extensions.Sync != nil && *routeHandler.c.Config.Extensions.Sync.Enable { - routeHandler.c.Log.Info().Msgf("signature not found, trying to get signature %s:%s by syncing on demand", + routeHandler.c.Log.Info().Msgf("artifact not found, trying to get artifact %s:%s by syncing on demand", name, digest.String()) errSync := ext.SyncOneImage(ctx, routeHandler.c.Config, routeHandler.c.StoreController, @@ -1736,8 +1736,13 @@ func (rh *RouteHandler) GetOrasReferrers(response http.ResponseWriter, request * refs, err := getOrasReferrers(request.Context(), rh, imgStore, name, digest, artifactType) //nolint:contextcheck if err != nil { - rh.c.Log.Error().Err(err).Str("name", name).Str("digest", digest.String()).Msg("unable to get references") - response.WriteHeader(http.StatusNotFound) + if errors.Is(err, zerr.ErrManifestNotFound) || errors.Is(err, zerr.ErrRepoNotFound) { + rh.c.Log.Error().Err(err).Str("name", name).Str("digest", digest.String()).Msg("manifest not found") + response.WriteHeader(http.StatusNotFound) + } else { + rh.c.Log.Error().Err(err).Str("name", name).Str("digest", digest.String()).Msg("unable to get references") + response.WriteHeader(http.StatusInternalServerError) + } return } diff --git a/pkg/cli/client.go b/pkg/cli/client.go index 874d3201..b6123d13 100644 --- a/pkg/cli/client.go +++ b/pkg/cli/client.go @@ -18,10 +18,10 @@ import ( "time" notreg "github.com/notaryproject/notation-go/registry" + ispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sigstore/cosign/pkg/oci/remote" zotErrors "zotregistry.io/zot/errors" - "zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/common" ) @@ -236,14 +236,14 @@ func (p *requestsPool) doJob(ctx context.Context, job *manifestJob) { isSigned = true } - var referrers api.ReferenceList + var referrers ispec.Index if !isSigned { - _, err = makeGETRequest(ctx, fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers?artifactType=%s", + _, err = makeGETRequest(ctx, fmt.Sprintf("%s/v2/%s/referrers/%s?artifactType=%s", *job.config.servURL, job.imageName, digestStr, notreg.ArtifactTypeNotation), job.username, job.password, *job.config.verifyTLS, *job.config.debug, &referrers, job.config.resultWriter) if err == nil { - for _, reference := range referrers.References { + for _, reference := range referrers.Manifests { if reference.ArtifactType == notreg.ArtifactTypeNotation { isSigned = true diff --git a/pkg/cli/image_cmd_test.go b/pkg/cli/image_cmd_test.go index c303102f..6a1c11db 100644 --- a/pkg/cli/image_cmd_test.go +++ b/pkg/cli/image_cmd_test.go @@ -10,7 +10,6 @@ import ( "fmt" "log" "os" - "os/exec" "path" "regexp" "strings" @@ -254,46 +253,6 @@ func TestSearchImageCmd(t *testing.T) { }) } -func SignImageUsingNotary(repoTag, port string) error { - cwd, err := os.Getwd() - if err != nil { - return err - } - - defer func() { _ = os.Chdir(cwd) }() - - tdir, err := os.MkdirTemp("", "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", port, repoTag) - - cmd = exec.Command("notation", "sign", "--key", "notation-sign-test", "--plain-http", image) - - return cmd.Run() -} - func TestSignature(t *testing.T) { Convey("Test from real server", t, func() { currentWorkingDir, err := os.Getwd() @@ -427,7 +386,7 @@ func TestSignature(t *testing.T) { digest := godigest.FromBytes(content) So(digest, ShouldNotBeNil) - err = SignImageUsingNotary("repo7:0.0.1", port) + err = test.SignImageUsingNotary("repo7:0.0.1", port) So(err, ShouldBeNil) t.Logf("%s", ctlr.Config.Storage.RootDirectory) diff --git a/pkg/extensions/search/common/common_test.go b/pkg/extensions/search/common/common_test.go index 49955a0e..e20e21db 100644 --- a/pkg/extensions/search/common/common_test.go +++ b/pkg/extensions/search/common/common_test.go @@ -24,7 +24,6 @@ import ( godigest "github.com/opencontainers/go-digest" "github.com/opencontainers/image-spec/specs-go" ispec "github.com/opencontainers/image-spec/specs-go/v1" - artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1" . "github.com/smartystreets/goconvey/convey" "gopkg.in/resty.v1" @@ -4085,7 +4084,7 @@ func TestRepoDBWhenPushingImages(t *testing.T) { baseURL, "repo1", ) - So(err, ShouldBeNil) + So(err, ShouldNotBeNil) }) Convey("SetManifestMeta succeeds but SetRepoTag fails", func() { @@ -4458,10 +4457,10 @@ func TestRepoDBWhenDeletingImages(t *testing.T) { signatureReference := "" - var sigManifestContent artifactspec.Manifest + var sigManifestContent ispec.Artifact for _, manifest := range indexContent.Manifests { - if manifest.MediaType == artifactspec.MediaTypeArtifactManifest { + if manifest.MediaType == ispec.MediaTypeArtifactManifest { signatureReference = manifest.Digest.String() manifestBlob, _, _, err := storage.GetImageManifest(repo, signatureReference) So(err, ShouldBeNil) @@ -4854,6 +4853,82 @@ func TestBaseOciLayoutUtils(t *testing.T) { _, err := olu.GetImageInfo("", "") So(err, ShouldNotBeNil) }) + + Convey("CheckManifestSignature: notation", t, func() { + // GetReferrers - fails => checkNotarySignature returns false + mockStoreController := mocks.MockedImageStore{ + GetImageManifestFn: func(name, reference string) ([]byte, godigest.Digest, string, error) { + return []byte{}, "", "", zerr.ErrRepoNotFound + }, + GetReferrersFn: func(name string, digest godigest.Digest, mediaTypes []string) (ispec.Index, error) { + return ispec.Index{}, ErrTestError + }, + } + + storeController := storage.StoreController{DefaultStore: mockStoreController} + olu := common.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", "")) + + check := olu.CheckManifestSignature("rep", godigest.FromString("")) + So(check, ShouldBeFalse) + + // checkNotarySignature -> true + dir := t.TempDir() + + port := GetFreePort() + baseURL := GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + conf.Storage.RootDirectory = dir + defaultVal := true + conf.Extensions = &extconf.ExtensionConfig{ + Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, + } + + conf.Extensions.Search.CVE = nil + + ctlr := api.NewController(conf) + + ctlrManager := NewControllerManager(ctlr) + ctlrManager.StartAndWait(port) + defer ctlrManager.StopServer() + + // push test image to repo + config, layers, manifest, err := GetImageComponents(100) + So(err, ShouldBeNil) + + layersSize1 := 0 + for _, l := range layers { + layersSize1 += len(l) + } + + repo := "repo" + tag := "1.0.1" + err = UploadImage( + Image{ + Manifest: manifest, + Config: config, + Layers: layers, + Tag: tag, + }, + baseURL, + repo, + ) + So(err, ShouldBeNil) + + olu = common.NewBaseOciLayoutUtils(ctlr.StoreController, log.NewLogger("debug", "")) + manifestList, err := olu.GetImageManifests(repo) + So(err, ShouldBeNil) + So(len(manifestList), ShouldEqual, 1) + + isSigned := olu.CheckManifestSignature(repo, manifestList[0].Digest) + So(isSigned, ShouldBeFalse) + + err = SignImageUsingNotary(fmt.Sprintf("%s:%s", repo, tag), port) + So(err, ShouldBeNil) + + isSigned = olu.CheckManifestSignature(repo, manifestList[0].Digest) + So(isSigned, ShouldBeTrue) + }) } func TestSearchSize(t *testing.T) { diff --git a/pkg/extensions/search/common/oci_layout.go b/pkg/extensions/search/common/oci_layout.go index 7392110a..b3ddbde5 100644 --- a/pkg/extensions/search/common/oci_layout.go +++ b/pkg/extensions/search/common/oci_layout.go @@ -218,7 +218,7 @@ func (olu BaseOciLayoutUtils) checkNotarySignature(name string, digest godigest. imageStore := olu.StoreController.GetImageStore(name) mediaType := notreg.ArtifactTypeNotation - _, err := imageStore.GetOrasReferrers(name, digest, mediaType) + referrers, err := imageStore.GetReferrers(name, digest, []string{mediaType}) if err != nil { olu.Log.Info().Err(err).Str("repo", name).Str("digest", digest.String()).Str("mediatype", mediaType).Msg("invalid notary signature") @@ -226,6 +226,10 @@ func (olu BaseOciLayoutUtils) checkNotarySignature(name string, digest godigest. return false } + if len(referrers.Manifests) == 0 { + return false + } + return true } diff --git a/pkg/extensions/sync/on_demand.go b/pkg/extensions/sync/on_demand.go index 4eca3a4e..59c5ce43 100644 --- a/pkg/extensions/sync/on_demand.go +++ b/pkg/extensions/sync/on_demand.go @@ -300,14 +300,14 @@ func syncRun(regCfg RegistryConfig, Err(err).Msgf("couldn't get upstream image %s cosign manifest", upstreamImageRef.DockerReference()) } - refs, err := sig.getNotaryRefs(upstreamRepo, upstreamImageDigest.String()) + index, err := sig.getOCIRefs(upstreamRepo, upstreamImageDigest.String()) if err != nil { log.Error().Str("errorType", common.TypeOf(err)). - Err(err).Msgf("couldn't get upstream image %s notary references", upstreamImageRef.DockerReference()) + Err(err).Msgf("couldn't get upstream image %s OCI references", upstreamImageRef.DockerReference()) } // check if upstream image is signed - if cosignManifest == nil && len(refs.References) == 0 { + if cosignManifest == nil && len(getNotationManifestsFromOCIRefs(index)) == 0 { // upstream image not signed if regCfg.OnlySigned != nil && *regCfg.OnlySigned { // skip unsigned images @@ -355,12 +355,6 @@ func syncRun(regCfg RegistryConfig, return false, err } - index, err := sig.getOCIRefs(upstreamRepo, upstreamImageDigest.String()) - if err != nil { - log.Error().Str("errorType", common.TypeOf(err)). - Err(err).Msgf("couldn't get upstream image %s oci references", upstreamImageRef.DockerReference()) - } - err = sig.syncOCIRefs(localRepo, upstreamRepo, upstreamImageDigest.String(), index) if err != nil { return false, err @@ -374,10 +368,16 @@ func syncRun(regCfg RegistryConfig, return false, err } - err = sig.syncNotaryRefs(localRepo, upstreamRepo, upstreamImageDigest.String(), refs) + refs, err := sig.getORASRefs(upstreamRepo, upstreamImageDigest.String()) if err != nil { log.Error().Str("errorType", common.TypeOf(err)). - Err(err).Msgf("couldn't copy image notary signature %s/%s:%s", utils.upstreamAddr, upstreamRepo, reference) + Err(err).Msgf("couldn't get upstream image %s ORAS references", upstreamImageRef.DockerReference()) + } + + err = sig.syncORASRefs(localRepo, upstreamRepo, upstreamImageDigest.String(), refs) + if err != nil { + log.Error().Str("errorType", common.TypeOf(err)). + Err(err).Msgf("couldn't copy image ORAS references %s/%s:%s", utils.upstreamAddr, upstreamRepo, reference) return false, err } @@ -409,27 +409,28 @@ func syncSignaturesArtifacts(sig *signaturesCopier, localRepo, upstreamRepo, ref return err } case artifactType == OrasArtifact: - // is notary signature - refs, err := sig.getNotaryRefs(upstreamRepo, reference) + // is oras artifact + refs, err := sig.getORASRefs(upstreamRepo, reference) if err != nil { sig.log.Error().Str("errorType", common.TypeOf(err)). - Err(err).Msgf("couldn't get upstream image %s/%s:%s notary references", upstreamURL, upstreamRepo, reference) + Err(err).Msgf("couldn't get upstream image %s/%s:%s ORAS references", upstreamURL, upstreamRepo, reference) return err } - err = sig.syncNotaryRefs(localRepo, upstreamRepo, reference, refs) + err = sig.syncORASRefs(localRepo, upstreamRepo, reference, refs) if err != nil { sig.log.Error().Str("errorType", common.TypeOf(err)). - Err(err).Msgf("couldn't copy image signature %s/%s:%s", upstreamURL, upstreamRepo, reference) + Err(err).Msgf("couldn't copy image ORAS references %s/%s:%s", upstreamURL, upstreamRepo, reference) return err } case artifactType == OCIReference: + // this contains notary signatures index, err := sig.getOCIRefs(upstreamRepo, reference) if err != nil { sig.log.Error().Str("errorType", common.TypeOf(err)). - Err(err).Msgf("couldn't get oci references %s/%s:%s", upstreamURL, upstreamRepo, reference) + Err(err).Msgf("couldn't get OCI references %s/%s:%s", upstreamURL, upstreamRepo, reference) return err } @@ -437,7 +438,7 @@ func syncSignaturesArtifacts(sig *signaturesCopier, localRepo, upstreamRepo, ref err = sig.syncOCIRefs(localRepo, upstreamRepo, reference, index) if err != nil { sig.log.Error().Str("errorType", common.TypeOf(err)). - Err(err).Msgf("couldn't copy oci references %s/%s:%s", upstreamURL, upstreamRepo, reference) + Err(err).Msgf("couldn't copy OCI references %s/%s:%s", upstreamURL, upstreamRepo, reference) return err } diff --git a/pkg/extensions/sync/signatures.go b/pkg/extensions/sync/signatures.go index fd621dfc..33908fe7 100644 --- a/pkg/extensions/sync/signatures.go +++ b/pkg/extensions/sync/signatures.go @@ -73,7 +73,7 @@ func (sig *signaturesCopier) getCosignManifest(repo, digestStr string) (*ispec.M return &cosignManifest, nil } -func (sig *signaturesCopier) getNotaryRefs(repo, digestStr string) (ReferenceList, error) { +func (sig *signaturesCopier) getORASRefs(repo, digestStr string) (ReferenceList, error) { var referrers ReferenceList getReferrersURL := sig.upstreamURL @@ -89,12 +89,12 @@ func (sig *signaturesCopier) getNotaryRefs(repo, digestStr string) (ReferenceLis getReferrersURL.String(), "application/json", sig.log) if err != nil { if statusCode == http.StatusNotFound { - sig.log.Info().Err(err).Msg("couldn't find any notary signatures/oras artifacts") + sig.log.Info().Err(err).Msg("couldn't find any ORAS artifact") return referrers, zerr.ErrSyncReferrerNotFound } - sig.log.Error().Err(err).Msg("couldn't get notary signatures/oras artifacts") + sig.log.Error().Err(err).Msg("couldn't get ORAS artifacts") return referrers, err } @@ -188,25 +188,25 @@ func (sig *signaturesCopier) syncCosignSignature(localRepo, remoteRepo, digestSt return nil } -func (sig *signaturesCopier) syncNotaryRefs(localRepo, remoteRepo, digestStr string, referrers ReferenceList, +func (sig *signaturesCopier) syncORASRefs(localRepo, remoteRepo, digestStr string, referrers ReferenceList, ) error { if len(referrers.References) == 0 { return nil } - skipNotarySig, err := sig.canSkipNotaryRefs(localRepo, digestStr, referrers) + skipORASRefs, err := sig.canSkipORASRefs(localRepo, digestStr, referrers) if err != nil { - sig.log.Error().Err(err).Msgf("couldn't check if the upstream image %s:%s notary signature can be skipped", + sig.log.Error().Err(err).Msgf("couldn't check if the upstream image %s:%s ORAS artifact can be skipped", remoteRepo, digestStr) } - if skipNotarySig { + if skipORASRefs { return nil } imageStore := sig.storeController.GetImageStore(localRepo) - sig.log.Info().Msg("syncing notary signatures") + sig.log.Info().Msg("syncing ORAS artifacts") for _, ref := range referrers.References { // get referrer manifest @@ -222,13 +222,13 @@ func (sig *signaturesCopier) syncNotaryRefs(localRepo, remoteRepo, digestStr str if err != nil { if statusCode == http.StatusNotFound { sig.log.Error().Str("errorType", common.TypeOf(err)). - Err(err).Msgf("couldn't find any notary manifest: %s", getRefManifestURL.String()) + Err(err).Msgf("couldn't find any ORAS manifest: %s", getRefManifestURL.String()) return zerr.ErrSyncReferrerNotFound } sig.log.Error().Str("errorType", common.TypeOf(err)). - Err(err).Msgf("couldn't get notary manifest: %s", getRefManifestURL.String()) + Err(err).Msgf("couldn't get ORAS manifest: %s", getRefManifestURL.String()) return err } @@ -243,13 +243,13 @@ func (sig *signaturesCopier) syncNotaryRefs(localRepo, remoteRepo, digestStr str oras.MediaTypeArtifactManifest, body) if err != nil { sig.log.Error().Str("errorType", common.TypeOf(err)). - Err(err).Msg("couldn't upload notary sig manifest") + Err(err).Msg("couldn't upload ORAS manifest") return err } } - sig.log.Info().Msgf("successfully synced notary signature for repo %s digest %s", localRepo, digestStr) + sig.log.Info().Msgf("successfully synced ORAS artifacts for repo %s digest %s", localRepo, digestStr) return nil } @@ -409,33 +409,33 @@ func (sig *signaturesCopier) syncOCIArtifact(localRepo, remoteRepo, reference st return nil } -func (sig *signaturesCopier) canSkipNotaryRefs(localRepo, digestStr string, refs ReferenceList, +func (sig *signaturesCopier) canSkipORASRefs(localRepo, digestStr string, refs ReferenceList, ) (bool, error) { imageStore := sig.storeController.GetImageStore(localRepo) digest := godigest.Digest(digestStr) - // check notary signature already synced + // check oras artifacts already synced if len(refs.References) > 0 { - localRefs, err := imageStore.GetOrasReferrers(localRepo, digest, notreg.ArtifactTypeNotation) + localRefs, err := imageStore.GetOrasReferrers(localRepo, digest, "") if err != nil { if errors.Is(err, zerr.ErrManifestNotFound) { return false, nil } sig.log.Error().Str("errorType", common.TypeOf(err)). - Err(err).Msgf("couldn't get local notary signature %s:%s manifest", localRepo, digestStr) + Err(err).Msgf("couldn't get local ORAS artifact %s:%s manifest", localRepo, digestStr) return false, err } if !artifactDescriptorsEqual(localRefs, refs.References) { - sig.log.Info().Msgf("upstream notary signatures %s:%s changed, syncing again", localRepo, digestStr) + sig.log.Info().Msgf("upstream ORAS artifacts %s:%s changed, syncing again", localRepo, digestStr) return false, nil } } - sig.log.Info().Msgf("skipping notary signature %s:%s, already synced", localRepo, digestStr) + sig.log.Info().Msgf("skipping ORAS artifact %s:%s, already synced", localRepo, digestStr) return true, nil } @@ -608,3 +608,15 @@ func getCosignTagFromImageDigest(digestStr string) string { return digestStr } + +func getNotationManifestsFromOCIRefs(ociRefs ispec.Index) []ispec.Descriptor { + notaryManifests := []ispec.Descriptor{} + + for _, ref := range ociRefs.Manifests { + if ref.ArtifactType == notreg.ArtifactTypeNotation { + notaryManifests = append(notaryManifests, ref) + } + } + + return notaryManifests +} diff --git a/pkg/extensions/sync/sync.go b/pkg/extensions/sync/sync.go index 32dfa461..4af4bbb9 100644 --- a/pkg/extensions/sync/sync.go +++ b/pkg/extensions/sync/sync.go @@ -334,15 +334,15 @@ func syncRegistry(ctx context.Context, regCfg RegistryConfig, return err } - refs, err := sig.getNotaryRefs(upstreamRepo, upstreamImageDigest.String()) + index, err := sig.getOCIRefs(upstreamRepo, upstreamImageDigest.String()) if err != nil && !errors.Is(err, zerr.ErrSyncReferrerNotFound) { - log.Error().Err(err).Msgf("couldn't get upstream image %s notary references", upstreamImageRef.DockerReference()) + log.Error().Err(err).Msgf("couldn't get upstream image %s OCI references", upstreamImageRef.DockerReference()) return err } // check if upstream image is signed - if cosignManifest == nil && len(refs.References) == 0 { + if cosignManifest == nil && len(getNotationManifestsFromOCIRefs(index)) == 0 { // upstream image not signed if regCfg.OnlySigned != nil && *regCfg.OnlySigned { // skip unsigned images @@ -399,17 +399,17 @@ func syncRegistry(ctx context.Context, regCfg RegistryConfig, // sync signatures if err = retry.RetryIfNecessary(ctx, func() error { - index, err := sig.getOCIRefs(upstreamRepo, upstreamImageDigest.String()) - if err != nil && !errors.Is(err, zerr.ErrSyncReferrerNotFound) { - return err - } - err = sig.syncOCIRefs(localRepo, upstreamRepo, upstreamImageDigest.String(), index) if err != nil { return err } - err = sig.syncNotaryRefs(localRepo, upstreamRepo, upstreamImageDigest.String(), refs) + refs, err := sig.getORASRefs(upstreamRepo, upstreamImageDigest.String()) + if err != nil && !errors.Is(err, zerr.ErrSyncReferrerNotFound) { + return err + } + + err = sig.syncORASRefs(localRepo, upstreamRepo, upstreamImageDigest.String(), refs) if err != nil { return err } diff --git a/pkg/extensions/sync/sync_internal_test.go b/pkg/extensions/sync/sync_internal_test.go index f46a3549..836d4ea6 100644 --- a/pkg/extensions/sync/sync_internal_test.go +++ b/pkg/extensions/sync/sync_internal_test.go @@ -358,8 +358,10 @@ func TestSyncInternal(t *testing.T) { So(err, ShouldBeNil) So(regURL, ShouldNotBeNil) - ref := artifactspec.Descriptor{ - Digest: "fakeDigest", + ref := ispec.Descriptor{ + MediaType: ispec.MediaTypeArtifactManifest, + Digest: "fakeDigest", + ArtifactType: "application/vnd.cncf.notary.signature", } desc := ispec.Descriptor{ @@ -383,7 +385,7 @@ func TestSyncInternal(t *testing.T) { err = sig.syncCosignSignature(testImage, testImage, testImageTag, &manifest) So(err, ShouldNotBeNil) - err = sig.syncNotaryRefs(testImage, testImage, "invalidDigest", ReferenceList{[]artifactspec.Descriptor{ref}}) + err = sig.syncOCIRefs(testImage, testImage, "invalidDigest", ispec.Index{Manifests: []ispec.Descriptor{ref}}) So(err, ShouldNotBeNil) }) @@ -398,9 +400,11 @@ func TestSyncInternal(t *testing.T) { imageStore := local.NewImageStore(storageDir, false, storage.DefaultGCDelay, false, false, log, metrics, nil, nil) - refs := ReferenceList{[]artifactspec.Descriptor{ + refs := ispec.Index{Manifests: []ispec.Descriptor{ { - Digest: "fakeDigest", + MediaType: ispec.MediaTypeArtifactManifest, + Digest: "fakeDigest", + ArtifactType: "application/vnd.cncf.notary.signature", }, }} @@ -425,14 +429,34 @@ func TestSyncInternal(t *testing.T) { client := &http.Client{} sig := newSignaturesCopier(client, Credentials{}, *regURL, storage.StoreController{DefaultStore: imageStore}, log) - canBeSkipped, err = sig.canSkipNotaryRefs(testImage, testImageManifestDigest.String(), refs) + canBeSkipped, err = sig.canSkipOCIRefs(testImage, testImageManifestDigest.String(), refs) + So(err, ShouldBeNil) + So(canBeSkipped, ShouldBeFalse) + + var index ispec.Index + indexPath := path.Join(imageStore.RootDir(), testImage, "index.json") + buf, err := os.ReadFile(indexPath) + So(err, ShouldBeNil) + err = json.Unmarshal(buf, &index) + So(err, ShouldBeNil) + index.Manifests = append(index.Manifests, ispec.Descriptor{ + MediaType: ispec.MediaTypeArtifactManifest, + Digest: godigest.FromString(""), + ArtifactType: "application/vnd.cncf.notary.signature", + }) + indexBuf, err := json.Marshal(index) + So(err, ShouldBeNil) + err = os.WriteFile(indexPath, indexBuf, 0o600) + So(err, ShouldBeNil) + + canBeSkipped, err = sig.canSkipOCIRefs(testImage, testImageManifestDigest.String(), refs) So(err, ShouldBeNil) So(canBeSkipped, ShouldBeFalse) err = os.Chmod(path.Join(imageStore.RootDir(), testImage, "index.json"), 0o000) So(err, ShouldBeNil) - canBeSkipped, err = sig.canSkipNotaryRefs(testImage, testImageManifestDigest.String(), refs) + canBeSkipped, err = sig.canSkipOCIRefs(testImage, testImageManifestDigest.String(), refs) So(err, ShouldNotBeNil) So(canBeSkipped, ShouldBeFalse) @@ -457,6 +481,65 @@ func TestSyncInternal(t *testing.T) { canBeSkipped, err = sig.canSkipCosignSignature(testImage, testImageManifestDigest.String(), &cosignManifest) So(err, ShouldBeNil) So(canBeSkipped, ShouldBeFalse) + + // test canSkipOrasRefs() + refList := ReferenceList{} + canBeSkipped, err = sig.canSkipORASRefs(testImage, testImageManifestDigest.String(), refList) + So(err, ShouldBeNil) + So(canBeSkipped, ShouldBeTrue) + + refList.References = append(refList.References, artifactspec.Descriptor{ + MediaType: artifactspec.MediaTypeArtifactManifest, + Digest: godigest.FromString(""), + ArtifactType: "application/vnd.oras.artifact.ref", + }) + canBeSkipped, err = sig.canSkipORASRefs(testImage, testImageManifestDigest.String(), refList) + So(err, ShouldBeNil) + So(canBeSkipped, ShouldBeFalse) + + err = sig.syncORASRefs(testImage, testImage, testImageManifestDigest.String(), refList) + So(err, ShouldNotBeNil) + + buf, err = os.ReadFile(indexPath) + So(err, ShouldBeNil) + err = json.Unmarshal(buf, &index) + So(err, ShouldBeNil) + blobs := []artifactspec.Descriptor{} + manifest := artifactspec.Manifest{ + MediaType: artifactspec.MediaTypeArtifactManifest, + Blobs: blobs, + Subject: &artifactspec.Descriptor{Digest: testImageManifestDigest}, + } + manifestBuf, err := json.Marshal(manifest) + So(err, ShouldBeNil) + index.Manifests = append(index.Manifests, ispec.Descriptor{ + MediaType: artifactspec.MediaTypeArtifactManifest, + Digest: godigest.FromBytes(manifestBuf), + ArtifactType: "application/vnd.oras.artifact", + }) + indexBuf, err = json.Marshal(index) + So(err, ShouldBeNil) + err = os.WriteFile(indexPath, indexBuf, 0o600) + So(err, ShouldBeNil) + + err = os.Chmod(path.Join(imageStore.RootDir(), testImage, "index.json"), 0o000) + So(err, ShouldBeNil) + + canBeSkipped, err = sig.canSkipORASRefs(testImage, testImageManifestDigest.String(), refList) + So(err, ShouldNotBeNil) + So(canBeSkipped, ShouldBeFalse) + + err = os.Chmod(path.Join(imageStore.RootDir(), testImage, "index.json"), 0o755) + So(err, ShouldBeNil) + + err = os.WriteFile(path.Join(imageStore.RootDir(), testImage, + "blobs", "sha256", godigest.FromBytes(manifestBuf).Encoded()), + manifestBuf, 0o600) + So(err, ShouldBeNil) + + canBeSkipped, err = sig.canSkipORASRefs(testImage, testImageManifestDigest.String(), refList) + So(err, ShouldBeNil) + So(canBeSkipped, ShouldBeFalse) }) Convey("Test filterRepos()", t, func() { diff --git a/pkg/extensions/sync/sync_test.go b/pkg/extensions/sync/sync_test.go index 450f68d2..2151cde0 100644 --- a/pkg/extensions/sync/sync_test.go +++ b/pkg/extensions/sync/sync_test.go @@ -10,7 +10,6 @@ import ( "encoding/json" "errors" "fmt" - "io" "log" "net/http" "os" @@ -22,11 +21,10 @@ import ( "testing" "time" - "github.com/notaryproject/notation-go" notreg "github.com/notaryproject/notation-go/registry" godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" - oraspec "github.com/oras-project/artifacts-spec/specs-go/v1" + artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1" perr "github.com/pkg/errors" "github.com/sigstore/cosign/cmd/cosign/cli/generate" "github.com/sigstore/cosign/cmd/cosign/cli/options" @@ -74,33 +72,13 @@ type TagsList struct { } type ReferenceList struct { - References []notation.Descriptor `json:"references"` + References []ispec.Descriptor `json:"references"` } type catalog struct { Repositories []string `json:"repositories"` } -func copyFile(sourceFilePath, destFilePath string) error { - destFile, err := os.Create(destFilePath) - if err != nil { - return err - } - defer destFile.Close() - - sourceFile, err := os.Open(sourceFilePath) - if err != nil { - return err - } - defer sourceFile.Close() - - if _, err = io.Copy(destFile, sourceFile); err != nil { - return err - } - - return nil -} - func startUpstreamServer( t *testing.T, secure, basicAuth bool, ) (*api.Controller, string, string, string, *resty.Client) { @@ -324,6 +302,180 @@ func TestORAS(t *testing.T) { So(err, ShouldBeNil) So(string(output), ShouldContainSubstring, "helloworld") }) + + Convey("Verify get and sync oras refs", t, func() { + updateDuration, _ := time.ParseDuration("30m") + + sctlr, srcBaseURL, srcDir, _, _ := startUpstreamServer(t, false, false) + + defer func() { + sctlr.Shutdown() + }() + + repoName := testImage + var digest godigest.Digest + So(func() { digest = pushRepo(srcBaseURL, repoName) }, ShouldNotPanic) + + regex := ".*" + var semver bool + var tlsVerify bool + + syncRegistryConfig := sync.RegistryConfig{ + Content: []sync.Content{ + { + Prefix: repoName, + Tags: &sync.Tags{ + Regex: ®ex, + Semver: &semver, + }, + }, + }, + URLs: []string{srcBaseURL}, + PollInterval: updateDuration, + TLSVerify: &tlsVerify, + CertDir: "", + OnDemand: true, + } + + defaultVal := true + syncConfig := &sync.Config{ + Enable: &defaultVal, + Registries: []sync.RegistryConfig{syncRegistryConfig}, + } + + dctlr, destBaseURL, destDir, destClient := startDownstreamServer(t, false, syncConfig) + + defer func() { + dctlr.Shutdown() + }() + + // wait for sync + var destTagsList TagsList + + for { + resp, err := destClient.R().Get(destBaseURL + "/v2/" + repoName + "/tags/list") + if err != nil { + panic(err) + } + + err = json.Unmarshal(resp.Body(), &destTagsList) + if err != nil { + panic(err) + } + + if len(destTagsList.Tags) > 0 { + break + } + + time.Sleep(500 * time.Millisecond) + } + + time.Sleep(1 * time.Second) + + // get oras refs from downstream, should be synced + getORASReferrersURL := destBaseURL + path.Join("/oras/artifacts/v1/", repoName, "manifests", digest.String(), "referrers") //nolint:lll + + resp, err := resty.R().Get(getORASReferrersURL) + + So(err, ShouldBeNil) + So(resp, ShouldNotBeEmpty) + So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) + + err = os.Chmod(path.Join(destDir, testImage, "index.json"), 0o000) + So(err, ShouldBeNil) + + resp, err = resty.R().Get(getORASReferrersURL) + + So(err, ShouldBeNil) + So(resp, ShouldNotBeEmpty) + So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError) + + err = os.Chmod(path.Join(destDir, testImage, "index.json"), 0o755) + So(err, ShouldBeNil) + + // get manifest digest from source + resp, err = destClient.R().Get(srcBaseURL + "/v2/" + testImage + "/manifests/" + digest.String()) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + digest = godigest.FromBytes(resp.Body()) + + content := []byte("blob content") + adigest := pushBlob(srcBaseURL, repoName, content) + + artifactManifest := ispec.Artifact{ + MediaType: artifactspec.MediaTypeArtifactManifest, + ArtifactType: "application/vnd.oras.artifact", + Blobs: []ispec.Descriptor{ + { + MediaType: "application/octet-stream", + Digest: adigest, + Size: int64(len(content)), + }, + }, + Subject: &ispec.Descriptor{ + MediaType: "application/vnd.oci.image.manifest.v1+json", + Digest: digest, + Size: int64(len(resp.Body())), + }, + } + + content, err = json.Marshal(artifactManifest) + if err != nil { + panic(err) + } + + adigest = godigest.FromBytes(content) + + // put OCI reference artifact mediaType artifact + _, err = resty.R().SetHeader("Content-Type", artifactspec.MediaTypeArtifactManifest). + SetBody(content).Put(srcBaseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, adigest.String())) + if err != nil { + panic(err) + } + + err = os.Chmod(path.Join(destDir, testImage, "index.json"), 0o000) + So(err, ShouldBeNil) + + resp, err = resty.R().Get(getORASReferrersURL) + + So(err, ShouldBeNil) + So(resp, ShouldNotBeEmpty) + So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError) + + err = os.Chmod(path.Join(destDir, testImage, "index.json"), 0o755) + So(err, ShouldBeNil) + + resp, err = resty.R().Get(getORASReferrersURL) + + So(err, ShouldBeNil) + So(resp, ShouldNotBeEmpty) + + var refs ReferenceList + + err = json.Unmarshal(resp.Body(), &refs) + So(err, ShouldBeNil) + + So(len(refs.References), ShouldEqual, 1) + + err = os.RemoveAll(path.Join(destDir, repoName)) + So(err, ShouldBeNil) + + err = os.WriteFile(path.Join(srcDir, repoName, "blobs", "sha256", adigest.Encoded()), []byte("wrong content"), 0o600) + So(err, ShouldBeNil) + + _, err = resty.R().SetHeader("Content-Type", artifactspec.MediaTypeArtifactManifest). + SetBody(content).Put(srcBaseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, adigest.String())) + if err != nil { + panic(err) + } + + resp, err = resty.R().Get(getORASReferrersURL) + + So(err, ShouldBeNil) + So(resp, ShouldNotBeEmpty) + So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) + }) } func TestOnDemand(t *testing.T) { @@ -994,19 +1146,19 @@ func TestTLS(t *testing.T) { destClientCertDir := t.TempDir() destFilePath := path.Join(destClientCertDir, "ca.crt") - err = copyFile(CACert, destFilePath) + err = test.CopyFile(CACert, destFilePath) if err != nil { panic(err) } destFilePath = path.Join(destClientCertDir, "client.cert") - err = copyFile(ClientCert, destFilePath) + err = test.CopyFile(ClientCert, destFilePath) if err != nil { panic(err) } destFilePath = path.Join(destClientCertDir, "client.key") - err = copyFile(ClientKey, destFilePath) + err = test.CopyFile(ClientKey, destFilePath) if err != nil { panic(err) } @@ -1664,7 +1816,7 @@ func TestInvalidCerts(t *testing.T) { clientCertDir := t.TempDir() destFilePath := path.Join(clientCertDir, "ca.crt") - err := copyFile(CACert, destFilePath) + err := test.CopyFile(CACert, destFilePath) if err != nil { panic(err) } @@ -1681,13 +1833,13 @@ func TestInvalidCerts(t *testing.T) { } destFilePath = path.Join(clientCertDir, "client.cert") - err = copyFile(ClientCert, destFilePath) + err = test.CopyFile(ClientCert, destFilePath) if err != nil { panic(err) } destFilePath = path.Join(clientCertDir, "client.key") - err = copyFile(ClientKey, destFilePath) + err = test.CopyFile(ClientKey, destFilePath) if err != nil { panic(err) } @@ -1739,7 +1891,7 @@ func TestCertsWithWrongPerms(t *testing.T) { clientCertDir := t.TempDir() destFilePath := path.Join(clientCertDir, "ca.crt") - err := copyFile(CACert, destFilePath) + err := test.CopyFile(CACert, destFilePath) if err != nil { panic(err) } @@ -1748,13 +1900,13 @@ func TestCertsWithWrongPerms(t *testing.T) { So(err, ShouldBeNil) destFilePath = path.Join(clientCertDir, "client.cert") - err = copyFile(ClientCert, destFilePath) + err = test.CopyFile(ClientCert, destFilePath) if err != nil { panic(err) } destFilePath = path.Join(clientCertDir, "client.key") - err = copyFile(ClientKey, destFilePath) + err = test.CopyFile(ClientKey, destFilePath) if err != nil { panic(err) } @@ -2439,25 +2591,25 @@ func TestPeriodicallySignaturesErr(t *testing.T) { Convey("Trigger error on notary signature", func() { // trigger permission error on notary signature on upstream - notaryURLPath := path.Join("/oras/artifacts/v1/", repoName, "manifests", imageManifestDigest.String(), "referrers") + notaryURLPath := path.Join("/v2/", repoName, "referrers", imageManifestDigest.String()) // based on image manifest digest get referrers resp, err := resty.R(). SetHeader("Content-Type", "application/json"). - SetQueryParam("artifactType", "application/vnd.cncf.notary.v2.signature"). + SetQueryParam("artifactType", "application/vnd.cncf.notary.signature"). Get(srcBaseURL + notaryURLPath) So(err, ShouldBeNil) So(resp, ShouldNotBeEmpty) - var referrers ReferenceList + var referrers ispec.Index err = json.Unmarshal(resp.Body(), &referrers) So(err, ShouldBeNil) // read manifest - var artifactManifest oraspec.Manifest - for _, ref := range referrers.References { + var artifactManifest ispec.Artifact + for _, ref := range referrers.Manifests { refPath := path.Join(srcDir, repoName, "blobs", string(ref.Digest.Algorithm()), ref.Digest.Encoded()) body, err := os.ReadFile(refPath) So(err, ShouldBeNil) @@ -2481,10 +2633,17 @@ func TestPeriodicallySignaturesErr(t *testing.T) { // should not be synced nor sync on demand resp, err = resty.R().SetHeader("Content-Type", "application/json"). - SetQueryParam("artifactType", "application/vnd.cncf.notary.v2.signature"). + SetQueryParam("artifactType", "application/vnd.cncf.notary.signature"). Get(destBaseURL + notaryURLPath) So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, 404) + So(resp.StatusCode(), ShouldEqual, 200) + + var index ispec.Index + + err = json.Unmarshal(resp.Body(), &index) + So(err, ShouldBeNil) + + So(len(index.Manifests), ShouldEqual, 0) }) Convey("Trigger error on artifact references", func() { @@ -2600,6 +2759,7 @@ func TestSignatures(t *testing.T) { defer func() { _ = os.Chdir(cwd) }() tdir := t.TempDir() _ = os.Chdir(tdir) + generateKeyPairs(tdir) So(func() { signImage(tdir, srcPort, repoName, digest) }, ShouldNotPanic) @@ -2671,13 +2831,9 @@ func TestSignatures(t *testing.T) { // notation verify the image image := fmt.Sprintf("localhost:%s/%s:%s", destPort, repoName, "1.0") - cmd := exec.Command("notation", "verify", "--cert", "good", "--plain-http", image) - out, err := cmd.CombinedOutput() - So(err, ShouldBeNil) - msg := string(out) - So(msg, ShouldNotBeEmpty) - So(strings.Contains(msg, "verification failure"), ShouldBeFalse) + err = test.VerifyWithNotation(image, tdir) + So(err, ShouldBeNil) // cosign verify the image vrfy := verify.VerifyCommand{ @@ -2703,23 +2859,23 @@ func TestSignatures(t *testing.T) { err = json.Unmarshal(resp.Body(), &index) So(err, ShouldBeNil) - So(len(index.Manifests), ShouldEqual, 2) + So(len(index.Manifests), ShouldEqual, 3) // test negative cases (trigger errors) // test notary signatures errors // based on manifest digest get referrers - getReferrersURL := srcBaseURL + path.Join("/oras/artifacts/v1/", repoName, "manifests", digest.String(), "referrers") + getReferrersURL := srcBaseURL + path.Join("/v2/", repoName, "referrers", digest.String()) resp, err = resty.R(). SetHeader("Content-Type", "application/json"). - SetQueryParam("artifactType", "application/vnd.cncf.notary.v2.signature"). + SetQueryParam("artifactType", "application/vnd.cncf.notary.signature"). Get(getReferrersURL) So(err, ShouldBeNil) So(resp, ShouldNotBeEmpty) - var referrers ReferenceList + var referrers ispec.Index err = json.Unmarshal(resp.Body(), &referrers) So(err, ShouldBeNil) @@ -2728,8 +2884,8 @@ func TestSignatures(t *testing.T) { err = os.RemoveAll(path.Join(destDir, repoName)) So(err, ShouldBeNil) - var artifactManifest oraspec.Manifest - for _, ref := range referrers.References { + var artifactManifest ispec.Artifact + for _, ref := range referrers.Manifests { refPath := path.Join(srcDir, repoName, "blobs", string(ref.Digest.Algorithm()), ref.Digest.Encoded()) body, err := os.ReadFile(refPath) So(err, ShouldBeNil) @@ -2757,7 +2913,7 @@ func TestSignatures(t *testing.T) { So(err, ShouldBeNil) // triggers perm denied on notary manifest on downstream - for _, ref := range referrers.References { + for _, ref := range referrers.Manifests { refPath := path.Join(destDir, repoName, "blobs", string(ref.Digest.Algorithm()), ref.Digest.Encoded()) err := os.MkdirAll(refPath, 0o755) So(err, ShouldBeNil) @@ -3498,12 +3654,8 @@ func TestSignaturesOnDemand(t *testing.T) { // notation verify the synced image image := fmt.Sprintf("localhost:%s/%s:%s", destPort, repoName, "1.0") - cmd := exec.Command("notation", "verify", "--cert", "good", "--plain-http", image) - out, err := cmd.CombinedOutput() + err = test.VerifyWithNotation(image, tdir) So(err, ShouldBeNil) - msg := string(out) - So(msg, ShouldNotBeEmpty) - So(strings.Contains(msg, "verification failure"), ShouldBeFalse) // cosign verify the synced image vrfy := verify.VerifyCommand{ @@ -3568,6 +3720,107 @@ func TestSignaturesOnDemand(t *testing.T) { err = os.Chmod(srcSignatureBlobPath, 0o755) So(err, ShouldBeNil) }) + + Convey("Verify sync signatures on demand feature: notation - negative cases", t, func() { + sctlr, srcBaseURL, srcDir, _, _ := startUpstreamServer(t, false, false) + + defer func() { + sctlr.Shutdown() + }() + + // create repo, push and sign it + repoName := testSignedImage + var digest godigest.Digest + So(func() { digest = pushRepo(srcBaseURL, repoName) }, ShouldNotPanic) + + splittedURL := strings.SplitAfter(srcBaseURL, ":") + srcPort := splittedURL[len(splittedURL)-1] + + cwd, err := os.Getwd() + So(err, ShouldBeNil) + + defer func() { _ = os.Chdir(cwd) }() + tdir := t.TempDir() + _ = os.Chdir(tdir) + + generateKeyPairs(tdir) + + So(func() { signImage(tdir, srcPort, repoName, digest) }, ShouldNotPanic) + + var tlsVerify bool + + syncRegistryConfig := sync.RegistryConfig{ + URLs: []string{srcBaseURL}, + TLSVerify: &tlsVerify, + CertDir: "", + OnDemand: true, + } + + defaultVal := true + syncConfig := &sync.Config{ + Enable: &defaultVal, + Registries: []sync.RegistryConfig{syncRegistryConfig}, + } + + destPort := test.GetFreePort() + destConfig := config.New() + destBaseURL := test.GetBaseURL(destPort) + destConfig.HTTP.Port = destPort + + destDir := t.TempDir() + + destConfig.Storage.RootDirectory = destDir + destConfig.Storage.Dedupe = false + destConfig.Storage.GC = false + + destConfig.Extensions = &extconf.ExtensionConfig{} + destConfig.Extensions.Search = nil + destConfig.Extensions.Sync = syncConfig + destConfig.Log.Output = path.Join(destDir, "sync.log") + + dctlr := api.NewController(destConfig) + dcm := test.NewControllerManager(dctlr) + + dcm.StartAndWait(destPort) + + defer dcm.StopServer() + + // trigger getOCIRefs error + getReferrersURL := srcBaseURL + path.Join("/v2/", repoName, "referrers", digest.String()) + + resp, err := resty.R(). + SetHeader("Content-Type", "application/json"). + SetQueryParam("artifactType", "application/vnd.cncf.notary.signature"). + Get(getReferrersURL) + + So(err, ShouldBeNil) + So(resp, ShouldNotBeEmpty) + + var referrers ispec.Index + + err = json.Unmarshal(resp.Body(), &referrers) + So(err, ShouldBeNil) + + for _, ref := range referrers.Manifests { + refPath := path.Join(srcDir, repoName, "blobs", string(ref.Digest.Algorithm()), ref.Digest.Encoded()) + err := os.Remove(refPath) + So(err, ShouldBeNil) + } + + resp, err = resty.R().Get(destBaseURL + "/v2/" + testSignedImage + "/manifests/1.0") + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + time.Sleep(3 * time.Second) + + body, err := os.ReadFile(path.Join(destDir, "sync.log")) + if err != nil { + log.Fatalf("unable to read file: %v", err) + } + + So(string(body), ShouldContainSubstring, "couldn't find any oci reference") + So(string(body), ShouldContainSubstring, "couldn't find upstream referrer") + }) } func TestOnlySignaturesOnDemand(t *testing.T) { @@ -3641,12 +3894,8 @@ func TestOnlySignaturesOnDemand(t *testing.T) { // sync signature on demand when upstream doesn't have the signature image := fmt.Sprintf("localhost:%s/%s:%s", destPort, repoName, "1.0") - cmd := exec.Command("notation", "verify", "--cert", "good", "--plain-http", image) - out, err := cmd.CombinedOutput() + err = test.VerifyWithNotation(image, tdir) So(err, ShouldNotBeNil) - msg := string(out) - So(msg, ShouldNotBeEmpty) - So(strings.Contains(msg, "signature failure"), ShouldBeTrue) // cosign verify the synced image vrfy := verify.VerifyCommand{ @@ -3664,12 +3913,8 @@ func TestOnlySignaturesOnDemand(t *testing.T) { // now it should sync signatures on demand, even if we already have the image image = fmt.Sprintf("localhost:%s/%s:%s", destPort, repoName, "1.0") - cmd = exec.Command("notation", "verify", "--cert", "good", "--plain-http", image) - out, err = cmd.CombinedOutput() + err = test.VerifyWithNotation(image, tdir) So(err, ShouldBeNil) - msg = string(out) - So(msg, ShouldNotBeEmpty) - So(strings.Contains(msg, "verification failure"), ShouldBeFalse) // cosign verify the synced image vrfy = verify.VerifyCommand{ @@ -4002,14 +4247,9 @@ func TestSyncSignaturesDiff(t *testing.T) { // notation verify the image image := fmt.Sprintf("localhost:%s/%s:%s", destPort, repoName, "1.0") - cmd := exec.Command("notation", "verify", "--cert", "good", "--plain-http", image) - out, err := cmd.CombinedOutput() + err = test.VerifyWithNotation(image, tdir) So(err, ShouldBeNil) - msg := string(out) - So(msg, ShouldNotBeEmpty) - So(strings.Contains(msg, "verification failure"), ShouldBeFalse) - // cosign verify the image vrfy := verify.VerifyCommand{ RegistryOptions: options.RegistryOptions{AllowInsecure: true}, @@ -4033,14 +4273,9 @@ func TestSyncSignaturesDiff(t *testing.T) { // notation verify the image image = fmt.Sprintf("localhost:%s/%s:%s", destPort, repoName, "1.0") - cmd = exec.Command("notation", "verify", "--cert", "good", "--plain-http", image) - out, err = cmd.CombinedOutput() + err = test.VerifyWithNotation(image, tdir) So(err, ShouldBeNil) - msg = string(out) - So(msg, ShouldNotBeEmpty) - So(strings.Contains(msg, "verification failure"), ShouldBeFalse) - // cosign verify the image vrfy = verify.VerifyCommand{ RegistryOptions: options.RegistryOptions{AllowInsecure: true}, @@ -4721,18 +4956,12 @@ func generateKeyPairs(tdir string) { } } - // "notation" (notaryv2) doesn't yet support exported apis, so use the binary instead - _, err := exec.LookPath("notation") - if err != nil { - panic(err) - } + test.NotationPathLock.Lock() + defer test.NotationPathLock.Unlock() - os.Setenv("XDG_CONFIG_HOME", tdir) + test.LoadNotationPath(tdir) - // generate a keypair - cmd := exec.Command("notation", "cert", "generate-test", "--trust", "good") - - err = cmd.Run() + err := test.GenerateNotationCerts(tdir, "good") if err != nil { panic(err) } @@ -4771,23 +5000,23 @@ func signImage(tdir, port, repoName string, digest godigest.Digest) { panic(err) } + test.NotationPathLock.Lock() + defer test.NotationPathLock.Unlock() + + test.LoadNotationPath(tdir) + // sign the image image := fmt.Sprintf("localhost:%s/%s:%s", port, repoName, "1.0") - cmd := exec.Command("notation", "sign", "--key", "good", "--plain-http", image) - err = cmd.Run() + err = test.SignWithNotation("good", image, tdir) if err != nil { panic(err) } - // verify the image - cmd = exec.Command("notation", "verify", "--cert", "good", "--plain-http", image) - out, err := cmd.CombinedOutput() - So(err, ShouldBeNil) - - msg := string(out) - So(msg, ShouldNotBeEmpty) - So(strings.Contains(msg, "verification failure"), ShouldBeFalse) + err = test.VerifyWithNotation(image, tdir) + if err != nil { + panic(err) + } } func pushRepo(url, repoName string) godigest.Digest { diff --git a/pkg/storage/common.go b/pkg/storage/common.go index 1e62e60b..2ce1d960 100644 --- a/pkg/storage/common.go +++ b/pkg/storage/common.go @@ -3,13 +3,10 @@ package storage import ( "encoding/json" "errors" - "os" "path" "strings" - "github.com/docker/distribution/registry/storage/driver" "github.com/gobwas/glob" - "github.com/notaryproject/notation-go" godigest "github.com/opencontainers/go-digest" imeta "github.com/opencontainers/image-spec/specs-go" ispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -95,7 +92,7 @@ func ValidateManifest(imgStore ImageStore, repo, reference, mediaType string, bo } } case oras.MediaTypeArtifactManifest: - var m notation.Descriptor + var m oras.Descriptor if err := json.Unmarshal(body, &m); err != nil { log.Error().Err(err).Msg("unable to unmarshal JSON") @@ -580,7 +577,7 @@ func GetReferrers(imgStore ImageStore, repo string, gdigest godigest.Digest, art if err != nil { log.Error().Err(err).Str("blob", imgStore.BlobPath(repo, manifest.Digest)).Msg("failed to read manifest") - if os.IsNotExist(err) || errors.Is(err, driver.PathNotFoundError{}) { + if errors.Is(err, zerr.ErrBlobNotFound) { return nilIndex, zerr.ErrManifestNotFound } @@ -691,7 +688,7 @@ func GetOrasManifestByDigest(imgStore ImageStore, repo string, digest godigest.D if err != nil { log.Error().Err(err).Str("blob", blobPath).Msg("failed to read manifest") - if os.IsNotExist(err) || errors.Is(err, driver.PathNotFoundError{}) { + if errors.Is(err, zerr.ErrBlobNotFound) { return artManifest, zerr.ErrManifestNotFound } diff --git a/pkg/storage/common_test.go b/pkg/storage/common_test.go index e943459f..097cd000 100644 --- a/pkg/storage/common_test.go +++ b/pkg/storage/common_test.go @@ -7,7 +7,6 @@ import ( "path" "testing" - "github.com/docker/distribution/registry/storage/driver" godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1" @@ -169,7 +168,7 @@ func TestGetReferrersErrors(t *testing.T) { return indexBuf, nil }, GetBlobContentFn: func(repo string, digest godigest.Digest) ([]byte, error) { - return []byte{}, driver.PathNotFoundError{} + return []byte{}, errors.ErrBlobNotFound }, } @@ -195,6 +194,10 @@ func TestGetReferrersErrors(t *testing.T) { _, err = storage.GetReferrers(imgStore, "zot-test", validDigest, []string{artifactType}, log.With().Caller().Logger()) So(err, ShouldNotBeNil) + + _, err = storage.GetOrasReferrers(imgStore, "zot-test", validDigest, + artifactType, log.With().Caller().Logger()) + So(err, ShouldNotBeNil) }) Convey("Trigger continue on different artifactType", func(c C) { @@ -226,6 +229,20 @@ func TestGetReferrersErrors(t *testing.T) { So(err, ShouldNotBeNil) }) + Convey("Unmarshal oras artifact error", func(c C) { + imgStore = &mocks.MockedImageStore{ + GetIndexContentFn: func(repo string) ([]byte, error) { + return indexBuf, nil + }, + GetBlobContentFn: func(repo string, digest godigest.Digest) ([]byte, error) { + return []byte("wrong content"), nil + }, + } + + _, err = storage.GetOrasReferrers(imgStore, "zot-test", validDigest, artifactType, log.With().Caller().Logger()) + So(err, ShouldNotBeNil) + }) + Convey("Trigger unmarshal error on manifest image mediaType", func(c C) { index = ispec.Index{ Manifests: []ispec.Descriptor{ diff --git a/pkg/storage/local/local.go b/pkg/storage/local/local.go index f791e252..6a5a0cfd 100644 --- a/pkg/storage/local/local.go +++ b/pkg/storage/local/local.go @@ -1476,7 +1476,7 @@ func (is *ImageStoreLocal) garbageCollect(dir string, repo string) error { cosignDescriptors = append(cosignDescriptors, desc) } } - case oras.MediaTypeArtifactManifest: + case ispec.MediaTypeArtifactManifest: notationDescriptors = append(notationDescriptors, desc) } } @@ -1615,7 +1615,9 @@ func gcNotationSignatures(imgStore *ImageStoreLocal, oci casext.Engine, index *i for _, notationDesc := range notationDescriptors { foundSubject := false // check if we can find the manifest which the signature points to - artManifest, err := storage.GetOrasManifestByDigest(imgStore, repo, notationDesc.Digest, imgStore.log) + var artManifest ispec.Artifact + + buf, err := imgStore.GetBlobContent(repo, notationDesc.Digest) if err != nil { imgStore.log.Error().Err(err).Str("repo", repo).Str("digest", notationDesc.Digest.String()). Msg("gc: failed to get oras artifact manifest") @@ -1623,7 +1625,14 @@ func gcNotationSignatures(imgStore *ImageStoreLocal, oci casext.Engine, index *i return err } - // skip oras artifacts which are not signatures + if err := json.Unmarshal(buf, &artManifest); err != nil { + imgStore.log.Error().Err(err).Str("repo", repo).Str("digest", notationDesc.Digest.String()). + Msg("gc: failed to get oras artifact manifest") + + return err + } + + // skip oci artifacts which are not signatures if artManifest.ArtifactType != notreg.ArtifactTypeNotation { continue } diff --git a/pkg/test/common.go b/pkg/test/common.go index c9192229..c5c67702 100644 --- a/pkg/test/common.go +++ b/pkg/test/common.go @@ -4,20 +4,34 @@ import ( "bytes" "context" "crypto/rand" + "crypto/rsa" + "crypto/x509" "encoding/json" + "encoding/pem" "errors" "fmt" "io" + "io/fs" "log" + "math" "math/big" "net/http" "net/url" "os" - "os/exec" "path" + "path/filepath" "strings" + "sync" "time" + "github.com/notaryproject/notation-core-go/signature/jws" + "github.com/notaryproject/notation-core-go/testhelper" + "github.com/notaryproject/notation-go" + notconfig "github.com/notaryproject/notation-go/config" + "github.com/notaryproject/notation-go/dir" + notreg "github.com/notaryproject/notation-go/registry" + "github.com/notaryproject/notation-go/signer" + "github.com/notaryproject/notation-go/verifier" godigest "github.com/opencontainers/go-digest" "github.com/opencontainers/image-spec/specs-go" ispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -27,6 +41,9 @@ import ( "github.com/sigstore/cosign/cmd/cosign/cli/options" "github.com/sigstore/cosign/cmd/cosign/cli/sign" "gopkg.in/resty.v1" + "oras.land/oras-go/v2/registry" + "oras.land/oras-go/v2/registry/remote" + "oras.land/oras-go/v2/registry/remote/auth" "zotregistry.io/zot/pkg/storage" ) @@ -37,6 +54,8 @@ const ( SleepTime = 100 * time.Millisecond ) +var NotationPathLock = new(sync.Mutex) //nolint: gochecknoglobals + // which: manifest, config, layer func GetTestBlobDigest(image, which string) godigest.Digest { prePath := "../test/data" @@ -61,8 +80,11 @@ func GetTestBlobDigest(image, which string) godigest.Digest { } var ( - ErrPostBlob = errors.New("can't post blob") - ErrPutBlob = errors.New("can't put blob") + ErrPostBlob = errors.New("can't post blob") + ErrPutBlob = errors.New("can't put blob") + ErrAlreadyExists = errors.New("already exists") + ErrKeyNotFound = errors.New("key not found") + ErrSignatureVerification = errors.New("signature verification failed") ) type Image struct { @@ -667,11 +689,15 @@ func UploadImage(img Image, baseURL, repo string) error { return err } - _, err = resty.R(). + resp, err = resty.R(). SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json"). SetBody(manifestBlob). Put(baseURL + "/v2/" + repo + "/manifests/" + img.Tag) + if ErrStatusCode(resp.StatusCode()) != http.StatusCreated { + return ErrPutBlob + } + return err } @@ -745,6 +771,469 @@ func ReadLogFileAndSearchString(logPath string, stringToMatch string, timeout ti } } +func CopyFile(sourceFilePath, destFilePath string) error { + destFile, err := os.Create(destFilePath) + if err != nil { + return err + } + defer destFile.Close() + + sourceFile, err := os.Open(sourceFilePath) + if err != nil { + return err + } + defer sourceFile.Close() + + if _, err = io.Copy(destFile, sourceFile); err != nil { + return err + } + + return nil +} + +func LoadNotationPath(tdir string) { + dir.UserConfigDir = filepath.Join(tdir, "notation") + + // set user libexec + dir.UserLibexecDir = dir.UserConfigDir +} + +func GenerateNotationCerts(tdir string, certName string) error { + // generate RSA private key + bits := 2048 + + key, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return err + } + + keyBytes, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + return err + } + + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes}) + + rsaCertTuple := testhelper.GetRSASelfSignedCertTupleWithPK(key, "cert") + + certBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rsaCertTuple.Cert.Raw}) + + // write private key + relativeKeyPath, relativeCertPath := dir.LocalKeyPath(certName) + + configFS := dir.ConfigFS() + + keyPath, err := configFS.SysPath(relativeKeyPath) + if err != nil { + return err + } + + certPath, err := configFS.SysPath(relativeCertPath) + if err != nil { + return err + } + + if err := WriteFileWithPermission(keyPath, keyPEM, 0o600, false); err != nil { //nolint:gomnd + return fmt.Errorf("failed to write key file: %w", err) + } + + // write self-signed certificate + if err := WriteFileWithPermission(certPath, certBytes, 0o644, false); err != nil { //nolint:gomnd + return fmt.Errorf("failed to write certificate file: %w", err) + } + + signingKeys, err := notconfig.LoadSigningKeys() + if err != nil { + return err + } + + keySuite := notconfig.KeySuite{ + Name: certName, + X509KeyPair: ¬config.X509KeyPair{ + KeyPath: keyPath, + CertificatePath: certPath, + }, + } + + // addKeyToSigningKeys + if Contains(signingKeys.Keys, keySuite.Name) { + return ErrAlreadyExists + } + + signingKeys.Keys = append(signingKeys.Keys, keySuite) + + // Add to the trust store + trustStorePath := path.Join(tdir, fmt.Sprintf("notation/truststore/x509/ca/%s", certName)) + + if _, err := os.Stat(filepath.Join(trustStorePath, filepath.Base(certPath))); err == nil { + return ErrAlreadyExists + } + + if err := os.MkdirAll(trustStorePath, 0o755); err != nil { //nolint:gomnd + return fmt.Errorf("GenerateNotationCerts os.MkdirAll failed: %w", err) + } + + trustCertPath := path.Join(trustStorePath, fmt.Sprintf("%s%s", certName, dir.LocalCertificateExtension)) + + err = CopyFile(certPath, trustCertPath) + if err != nil { + return err + } + + // Save to the SigningKeys.json + if err := signingKeys.Save(); err != nil { + return err + } + + return nil +} + +func SignWithNotation(keyName string, reference string, tdir string) error { + ctx := context.TODO() + + // getSigner + var newSigner notation.Signer + + mediaType := jws.MediaTypeEnvelope + + // ResolveKey + signingKeys, err := LoadNotationSigningkeys(tdir) + if err != nil { + return err + } + + idx := Index(signingKeys.Keys, keyName) + if idx < 0 { + return ErrKeyNotFound + } + + key := signingKeys.Keys[idx] + + if key.X509KeyPair != nil { + newSigner, err = signer.NewFromFiles(key.X509KeyPair.KeyPath, key.X509KeyPair.CertificatePath) + if err != nil { + return err + } + } + + // prepareSigningContent + // getRepositoryClient + authClient := &auth.Client{ + Credential: func(ctx context.Context, reg string) (auth.Credential, error) { + return auth.EmptyCredential, nil + }, + Cache: auth.NewCache(), + ClientID: "notation", + } + + authClient.SetUserAgent("notation/zot_tests") + + plainHTTP := true + + // Resolve referance + ref, err := registry.ParseReference(reference) + if err != nil { + return err + } + + remoteRepo := &remote.Repository{ + Client: authClient, + Reference: ref, + PlainHTTP: plainHTTP, + } + + sigRepo := notreg.NewRepository(remoteRepo) + + sigOpts := notation.SignOptions{ + ArtifactReference: ref.String(), + SignatureMediaType: mediaType, + PluginConfig: map[string]string{}, + } + + _, err = notation.Sign(ctx, newSigner, sigRepo, sigOpts) + if err != nil { + return err + } + + return nil +} + +func VerifyWithNotation(reference string, tdir string) error { + // check if trustpolicy.json exists + trustpolicyPath := path.Join(tdir, "notation/trustpolicy.json") + + if _, err := os.Stat(trustpolicyPath); errors.Is(err, os.ErrNotExist) { + trustPolicy := ` + { + "version": "1.0", + "trustPolicies": [ + { + "name": "good", + "registryScopes": [ "*" ], + "signatureVerification": { + "level" : "audit" + }, + "trustStores": ["ca:good"], + "trustedIdentities": [ + "*" + ] + } + ] + }` + + file, err := os.Create(trustpolicyPath) + if err != nil { + return err + } + + defer file.Close() + + _, err = file.WriteString(trustPolicy) + if err != nil { + return err + } + } + + // start verifying signatures + ctx := context.TODO() + + // getRepositoryClient + authClient := &auth.Client{ + Credential: func(ctx context.Context, reg string) (auth.Credential, error) { + return auth.EmptyCredential, nil + }, + Cache: auth.NewCache(), + ClientID: "notation", + } + + authClient.SetUserAgent("notation/zot_tests") + + plainHTTP := true + + // Resolve referance + ref, err := registry.ParseReference(reference) + if err != nil { + return err + } + + remoteRepo := &remote.Repository{ + Client: authClient, + Reference: ref, + PlainHTTP: plainHTTP, + } + + repo := notreg.NewRepository(remoteRepo) + + manifestDesc, err := repo.Resolve(ctx, ref.Reference) + if err != nil { + return err + } + + if err := ref.ValidateReferenceAsDigest(); err != nil { + ref.Reference = manifestDesc.Digest.String() + } + + // getVerifier + newVerifier, err := verifier.NewFromConfig() + if err != nil { + return err + } + + remoteRepo = &remote.Repository{ + Client: authClient, + Reference: ref, + PlainHTTP: plainHTTP, + } + + repo = notreg.NewRepository(remoteRepo) + + configs := map[string]string{} + + verifyOpts := notation.RemoteVerifyOptions{ + ArtifactReference: ref.String(), + PluginConfig: configs, + MaxSignatureAttempts: math.MaxInt64, + } + + _, outcomes, err := notation.Verify(ctx, newVerifier, repo, verifyOpts) + if err != nil || len(outcomes) == 0 { + return ErrSignatureVerification + } + + return nil +} + +func ListNotarySignatures(reference string, tdir string) ([]godigest.Digest, error) { + signatures := []godigest.Digest{} + + ctx := context.TODO() + + // getSignatureRepository + ref, err := registry.ParseReference(reference) + if err != nil { + return signatures, err + } + + plainHTTP := true + + // getRepositoryClient + authClient := &auth.Client{ + Credential: func(ctx context.Context, registry string) (auth.Credential, error) { + return auth.EmptyCredential, nil + }, + Cache: auth.NewCache(), + ClientID: "notation", + } + + authClient.SetUserAgent("notation/zot_tests") + + remoteRepo := &remote.Repository{ + Client: authClient, + Reference: ref, + PlainHTTP: plainHTTP, + } + + sigRepo := notreg.NewRepository(remoteRepo) + + artifectDesc, err := sigRepo.Resolve(ctx, reference) + if err != nil { + return signatures, err + } + + err = sigRepo.ListSignatures(ctx, artifectDesc, func(signatureManifests []ispec.Descriptor) error { + for _, sigManifestDesc := range signatureManifests { + signatures = append(signatures, sigManifestDesc.Digest) + } + + return nil + }) + + return signatures, err +} + +func LoadNotationSigningkeys(tdir string) (*notconfig.SigningKeys, error) { + var err error + + var signingKeysInfo *notconfig.SigningKeys + + filePath := path.Join(tdir, "notation/signingkeys.json") + + file, err := os.Open(filePath) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + // create file + newSigningKeys := notconfig.NewSigningKeys() + + newFile, err := os.Create(filePath) + if err != nil { + return newSigningKeys, err + } + + defer newFile.Close() + + encoder := json.NewEncoder(newFile) + encoder.SetIndent("", " ") + + err = encoder.Encode(newSigningKeys) + + return newSigningKeys, err + } + + return nil, err + } + + defer file.Close() + + err = json.NewDecoder(file).Decode(&signingKeysInfo) + + return signingKeysInfo, err +} + +func LoadNotationConfig(tdir string) (*notconfig.Config, error) { + var configInfo *notconfig.Config + + filePath := path.Join(tdir, "notation/signingkeys.json") + + file, err := os.Open(filePath) + if err != nil { + return configInfo, err + } + + defer file.Close() + + err = json.NewDecoder(file).Decode(&configInfo) + if err != nil { + return configInfo, err + } + + // set default value + configInfo.SignatureFormat = strings.ToLower(configInfo.SignatureFormat) + if configInfo.SignatureFormat == "" { + configInfo.SignatureFormat = "jws" + } + + return configInfo, nil +} + +func WriteFileWithPermission(path string, data []byte, perm fs.FileMode, overwrite bool) error { + if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil { + return err + } + flag := os.O_WRONLY | os.O_CREATE + + if overwrite { + flag |= os.O_TRUNC + } else { + flag |= os.O_EXCL + } + + file, err := os.OpenFile(path, flag, perm) + if err != nil { + return err + } + + _, err = file.Write(data) + if err != nil { + file.Close() + + return err + } + + return file.Close() +} + +func IsDigestReference(ref string) bool { + parts := strings.SplitN(ref, "/", 2) //nolint:gomnd + if len(parts) == 1 { + return false + } + + index := strings.Index(parts[1], "@") + + return index != -1 +} + +type isser interface { + Is(string) bool +} + +// Index returns the index of the first occurrence of name in s, +// or -1 if not present. +func Index[E isser](s []E, name string) int { + for i, v := range s { + if v.Is(name) { + return i + } + } + + return -1 +} + +// Contains reports whether name is present in s. +func Contains[E isser](s []E, name string) bool { + return Index(s, name) >= 0 +} + func UploadImageWithBasicAuth(img Image, baseURL, repo, user, password string) error { for _, blob := range img.Layers { resp, err := resty.R(). @@ -883,17 +1372,13 @@ func SignImageUsingNotary(repoTag, port string) error { _ = os.Chdir(tdir) - _, err = exec.LookPath("notation") - if err != nil { - return err - } + NotationPathLock.Lock() + defer NotationPathLock.Unlock() - os.Setenv("XDG_CONFIG_HOME", tdir) + LoadNotationPath(tdir) // generate a keypair - cmd := exec.Command("notation", "cert", "generate-test", "--trust", "notation-sign-test") - - err = cmd.Run() + err = GenerateNotationCerts(tdir, "notation-sign-test") if err != nil { return err } @@ -901,7 +1386,7 @@ func SignImageUsingNotary(repoTag, port string) error { // sign the image image := fmt.Sprintf("localhost:%s/%s", port, repoTag) - cmd = exec.Command("notation", "sign", "--key", "notation-sign-test", "--plain-http", image) + err = SignWithNotation("notation-sign-test", image, tdir) - return cmd.Run() + return err } diff --git a/pkg/test/common_test.go b/pkg/test/common_test.go index 07c99807..b922bda1 100644 --- a/pkg/test/common_test.go +++ b/pkg/test/common_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + notconfig "github.com/notaryproject/notation-go/config" godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" . "github.com/smartystreets/goconvey/convey" @@ -453,7 +454,7 @@ func TestUploadImage(t *testing.T) { } err := test.UploadImage(img, baseURL, "test") - So(err, ShouldBeNil) + So(err, ShouldNotBeNil) }) Convey("Upload image with authentification", t, func() { @@ -792,3 +793,481 @@ func TestInjectUploadImageWithBasicAuth(t *testing.T) { }) }) } + +func TestCopyFile(t *testing.T) { + Convey("destFilePath does not exist", t, func() { + err := test.CopyFile("/path/to/srcFile", "~/path/to/some/unexisting/destDir/file") + So(err, ShouldNotBeNil) + }) + + Convey("sourceFile does not exist", t, func() { + err := test.CopyFile("/path/to/some/unexisting/file", path.Join(t.TempDir(), "destFile.txt")) + So(err, ShouldNotBeNil) + }) +} + +func TestIsDigestReference(t *testing.T) { + Convey("not digest reference", t, func() { + res := test.IsDigestReference("notDigestReference/input") + So(res, ShouldBeFalse) + }) + + Convey("wrong input format", t, func() { + res := test.IsDigestReference("wrongInput") + So(res, ShouldBeFalse) + }) +} + +func TestLoadNotationSigningkeys(t *testing.T) { + Convey("notation directory doesn't exist", t, func() { + _, err := test.LoadNotationSigningkeys(t.TempDir()) + So(err, ShouldNotBeNil) + }) + + Convey("wrong content of signingkeys.json", t, func() { + tempDir := t.TempDir() + dir := path.Join(tempDir, "notation") + err := os.Mkdir(dir, 0o777) + So(err, ShouldBeNil) + + filePath := path.Join(dir, "signingkeys.json") + err = os.WriteFile(filePath, []byte("some dummy file content"), 0o666) //nolint: gosec + So(err, ShouldBeNil) + + _, err = test.LoadNotationSigningkeys(tempDir) + So(err, ShouldNotBeNil) + }) + + Convey("not enough permissions to access signingkeys.json", t, func() { + tempDir := t.TempDir() + dir := path.Join(tempDir, "notation") + err := os.Mkdir(dir, 0o777) + So(err, ShouldBeNil) + + filePath := path.Join(dir, "signingkeys.json") + err = os.WriteFile(filePath, []byte("some dummy file content"), 0o300) //nolint: gosec + So(err, ShouldBeNil) + + _, err = test.LoadNotationSigningkeys(tempDir) + So(err, ShouldNotBeNil) + }) + + Convey("signingkeys.json not exists so it is created successfully", t, func() { + tempDir := t.TempDir() + dir := path.Join(tempDir, "notation") + err := os.Mkdir(dir, 0o777) + So(err, ShouldBeNil) + + _, err = test.LoadNotationSigningkeys(tempDir) + So(err, ShouldBeNil) + }) + + Convey("signingkeys.json not exists - error trying to create it", t, func() { + tempDir := t.TempDir() + dir := path.Join(tempDir, "notation") + // create notation directory without write permissions + err := os.Mkdir(dir, 0o555) + So(err, ShouldBeNil) + + _, err = test.LoadNotationSigningkeys(tempDir) + So(err, ShouldNotBeNil) + }) +} + +func TestLoadNotationConfig(t *testing.T) { + Convey("directory doesn't exist", t, func() { + _, err := test.LoadNotationConfig(t.TempDir()) + So(err, ShouldNotBeNil) + }) + + Convey("wrong content of signingkeys.json", t, func() { + tempDir := t.TempDir() + dir := path.Join(tempDir, "notation") + err := os.Mkdir(dir, 0o777) + So(err, ShouldBeNil) + + filePath := path.Join(dir, "signingkeys.json") + err = os.WriteFile(filePath, []byte("some dummy file content"), 0o666) //nolint: gosec + So(err, ShouldBeNil) + + _, err = test.LoadNotationConfig(tempDir) + So(err, ShouldNotBeNil) + }) + + Convey("check default value of signature format", t, func() { + tempDir := t.TempDir() + dir := path.Join(tempDir, "notation") + err := os.Mkdir(dir, 0o777) + So(err, ShouldBeNil) + + filePath := path.Join(dir, "signingkeys.json") + err = os.WriteFile(filePath, []byte("{\"SignatureFormat\": \"\"}"), 0o666) //nolint: gosec + So(err, ShouldBeNil) + + configInfo, err := test.LoadNotationConfig(tempDir) + So(err, ShouldBeNil) + So(configInfo.SignatureFormat, ShouldEqual, "jws") + }) +} + +func TestSignWithNotation(t *testing.T) { + Convey("notation directory doesn't exist", t, func() { + err := test.SignWithNotation("key", "reference", t.TempDir()) + So(err, ShouldNotBeNil) + }) + + Convey("key not found", t, func() { + tempDir := t.TempDir() + dir := path.Join(tempDir, "notation") + err := os.Mkdir(dir, 0o777) + So(err, ShouldBeNil) + + filePath := path.Join(dir, "signingkeys.json") + err = os.WriteFile(filePath, []byte("{}"), 0o666) //nolint: gosec + So(err, ShouldBeNil) + + err = test.SignWithNotation("key", "reference", tempDir) + So(err, ShouldEqual, test.ErrKeyNotFound) + }) + + Convey("not enough permissions to access notation/localkeys dir", t, func() { + cwd, err := os.Getwd() + So(err, ShouldBeNil) + defer func() { _ = os.Chdir(cwd) }() + tdir := t.TempDir() + _ = os.Chdir(tdir) + + test.NotationPathLock.Lock() + defer test.NotationPathLock.Unlock() + + test.LoadNotationPath(tdir) + + err = test.GenerateNotationCerts(tdir, "key") + So(err, ShouldBeNil) + + err = os.Chmod(path.Join(tdir, "notation", "localkeys"), 0o000) + So(err, ShouldBeNil) + + err = test.SignWithNotation("key", "reference", tdir) + So(err, ShouldNotBeNil) + + err = os.Chmod(path.Join(tdir, "notation", "localkeys"), 0o755) + So(err, ShouldBeNil) + }) + + Convey("error parsing reference", t, func() { + cwd, err := os.Getwd() + So(err, ShouldBeNil) + defer func() { _ = os.Chdir(cwd) }() + tdir := t.TempDir() + _ = os.Chdir(tdir) + + test.NotationPathLock.Lock() + defer test.NotationPathLock.Unlock() + + test.LoadNotationPath(tdir) + + err = test.GenerateNotationCerts(tdir, "key") + So(err, ShouldBeNil) + + err = test.SignWithNotation("key", "invalidReference", tdir) + So(err, ShouldNotBeNil) + }) + + Convey("error signing", t, func() { + cwd, err := os.Getwd() + So(err, ShouldBeNil) + defer func() { _ = os.Chdir(cwd) }() + tdir := t.TempDir() + _ = os.Chdir(tdir) + + test.NotationPathLock.Lock() + defer test.NotationPathLock.Unlock() + + test.LoadNotationPath(tdir) + + err = test.GenerateNotationCerts(tdir, "key") + So(err, ShouldBeNil) + + err = test.SignWithNotation("key", "localhost:8080/invalidreference:1.0", tdir) + So(err, ShouldNotBeNil) + }) +} + +func TestVerifyWithNotation(t *testing.T) { + Convey("notation directory doesn't exist", t, func() { + err := test.VerifyWithNotation("reference", t.TempDir()) + So(err, ShouldNotBeNil) + }) + + Convey("error parsing reference", t, func() { + cwd, err := os.Getwd() + So(err, ShouldBeNil) + defer func() { _ = os.Chdir(cwd) }() + tdir := t.TempDir() + _ = os.Chdir(tdir) + + test.NotationPathLock.Lock() + defer test.NotationPathLock.Unlock() + + test.LoadNotationPath(tdir) + + err = test.GenerateNotationCerts(tdir, "key") + So(err, ShouldBeNil) + + err = test.VerifyWithNotation("invalidReference", tdir) + So(err, ShouldNotBeNil) + }) + + Convey("error trying to get manifest", t, func() { + cwd, err := os.Getwd() + So(err, ShouldBeNil) + defer func() { _ = os.Chdir(cwd) }() + tdir := t.TempDir() + _ = os.Chdir(tdir) + + test.NotationPathLock.Lock() + defer test.NotationPathLock.Unlock() + + test.LoadNotationPath(tdir) + + err = test.GenerateNotationCerts(tdir, "key") + So(err, ShouldBeNil) + + err = test.VerifyWithNotation("localhost:8080/invalidreference:1.0", tdir) + So(err, ShouldNotBeNil) + }) + + Convey("invalid content of trustpolicy.json", t, func() { + // start a new server + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + dir := t.TempDir() + + conf := config.New() + conf.HTTP.Port = port + conf.Storage.RootDirectory = dir + + ctlr := api.NewController(conf) + cm := test.NewControllerManager(ctlr) + // this blocks + cm.StartAndWait(port) + defer cm.StopServer() + + repoName := "signed-repo" + tag := "1.0" + cfg, layers, manifest, err := test.GetImageComponents(2) + So(err, ShouldBeNil) + + err = test.UploadImage( + test.Image{ + Config: cfg, + Layers: layers, + Manifest: manifest, + Tag: tag, + }, baseURL, repoName) + So(err, ShouldBeNil) + + content, err := json.Marshal(manifest) + So(err, ShouldBeNil) + digest := godigest.FromBytes(content) + So(digest, ShouldNotBeNil) + + tempDir := t.TempDir() + notationDir := path.Join(tempDir, "notation") + err = os.Mkdir(notationDir, 0o777) + So(err, ShouldBeNil) + + filePath := path.Join(notationDir, "trustpolicy.json") + err = os.WriteFile(filePath, []byte("some dummy file content"), 0o666) //nolint: gosec + So(err, ShouldBeNil) + + test.NotationPathLock.Lock() + defer test.NotationPathLock.Unlock() + + test.LoadNotationPath(tempDir) + + err = test.VerifyWithNotation(fmt.Sprintf("localhost:%s/%s:%s", port, repoName, tag), tempDir) + So(err, ShouldNotBeNil) + }) +} + +func TestListNotarySignatures(t *testing.T) { + Convey("error parsing reference", t, func() { + cwd, err := os.Getwd() + So(err, ShouldBeNil) + defer func() { _ = os.Chdir(cwd) }() + tdir := t.TempDir() + _ = os.Chdir(tdir) + + _, err = test.ListNotarySignatures("invalidReference", tdir) + So(err, ShouldNotBeNil) + }) + + Convey("error trying to get manifest", t, func() { + cwd, err := os.Getwd() + So(err, ShouldBeNil) + defer func() { _ = os.Chdir(cwd) }() + tdir := t.TempDir() + _ = os.Chdir(tdir) + + _, err = test.ListNotarySignatures("localhost:8080/invalidreference:1.0", tdir) + So(err, ShouldNotBeNil) + }) +} + +func TestGenerateNotationCerts(t *testing.T) { + Convey("write key file with permission", t, func() { + tempDir := t.TempDir() + + notationDir := path.Join(tempDir, "notation") + err := os.Mkdir(notationDir, 0o777) + So(err, ShouldBeNil) + + filePath := path.Join(notationDir, "localkeys") + err = os.WriteFile(filePath, []byte("{}"), 0o666) //nolint: gosec + So(err, ShouldBeNil) + + test.NotationPathLock.Lock() + defer test.NotationPathLock.Unlock() + + test.LoadNotationPath(tempDir) + + err = test.GenerateNotationCerts(t.TempDir(), "cert") + So(err, ShouldNotBeNil) + }) + + Convey("write cert file with permission", t, func() { + tempDir := t.TempDir() + + notationDir := path.Join(tempDir, "notation", "localkeys") + err := os.MkdirAll(notationDir, 0o777) + So(err, ShouldBeNil) + + filePath := path.Join(notationDir, "cert.crt") + err = os.WriteFile(filePath, []byte("{}"), 0o666) //nolint: gosec + So(err, ShouldBeNil) + + err = os.Chmod(filePath, 0o000) + So(err, ShouldBeNil) + + test.NotationPathLock.Lock() + defer test.NotationPathLock.Unlock() + + test.LoadNotationPath(tempDir) + + err = test.GenerateNotationCerts(t.TempDir(), "cert") + So(err, ShouldNotBeNil) + + err = os.Chmod(filePath, 0o755) + So(err, ShouldBeNil) + }) + + Convey("signingkeys.json file - not enough permission", t, func() { + tempDir := t.TempDir() + + notationDir := path.Join(tempDir, "notation") + err := os.Mkdir(notationDir, 0o777) + So(err, ShouldBeNil) + + filePath := path.Join(notationDir, "signingkeys.json") + _, err = os.Create(filePath) //nolint: gosec + So(err, ShouldBeNil) + err = os.Chmod(filePath, 0o000) + So(err, ShouldBeNil) + + test.NotationPathLock.Lock() + defer test.NotationPathLock.Unlock() + + test.LoadNotationPath(tempDir) + + err = test.GenerateNotationCerts(t.TempDir(), "cert") + So(err, ShouldNotBeNil) + + err = os.Remove(filePath) + So(err, ShouldBeNil) + err = os.RemoveAll(path.Join(notationDir, "localkeys")) + So(err, ShouldBeNil) + signingKeysBuf, err := json.Marshal(notconfig.SigningKeys{}) + So(err, ShouldBeNil) + err = os.WriteFile(filePath, signingKeysBuf, 0o555) + So(err, ShouldBeNil) + err = test.GenerateNotationCerts(t.TempDir(), "cert") + So(err, ShouldNotBeNil) + }) + Convey("keysuite already exists in signingkeys.json", t, func() { + tempDir := t.TempDir() + + notationDir := path.Join(tempDir, "notation") + err := os.Mkdir(notationDir, 0o777) + So(err, ShouldBeNil) + + certName := "cert-test" + filePath := path.Join(notationDir, "signingkeys.json") + keyPath := path.Join(notationDir, "localkeys", certName+".key") + certPath := path.Join(notationDir, "localkeys", certName+".crt") + signingKeys := notconfig.SigningKeys{} + keySuite := notconfig.KeySuite{ + Name: certName, + X509KeyPair: ¬config.X509KeyPair{ + KeyPath: keyPath, + CertificatePath: certPath, + }, + } + signingKeys.Keys = []notconfig.KeySuite{keySuite} + signingKeysBuf, err := json.Marshal(signingKeys) + So(err, ShouldBeNil) + err = os.WriteFile(filePath, signingKeysBuf, 0o600) + So(err, ShouldBeNil) + + test.NotationPathLock.Lock() + defer test.NotationPathLock.Unlock() + + test.LoadNotationPath(tempDir) + + err = test.GenerateNotationCerts(t.TempDir(), certName) + So(err, ShouldNotBeNil) + }) + Convey("truststore files", t, func() { + tempDir := t.TempDir() + + notationDir := path.Join(tempDir, "notation") + err := os.Mkdir(notationDir, 0o777) + So(err, ShouldBeNil) + + certName := "cert-test" + trustStorePath := path.Join(notationDir, fmt.Sprintf("truststore/x509/ca/%s", certName)) + err = os.MkdirAll(trustStorePath, 0o755) + So(err, ShouldBeNil) + err = os.Chmod(path.Join(notationDir, "truststore/x509"), 0o000) + So(err, ShouldBeNil) + + test.NotationPathLock.Lock() + defer test.NotationPathLock.Unlock() + + test.LoadNotationPath(tempDir) + + err = test.GenerateNotationCerts(tempDir, certName) + So(err, ShouldNotBeNil) + + err = os.RemoveAll(path.Join(notationDir, "localkeys")) + So(err, ShouldBeNil) + err = os.Chmod(path.Join(notationDir, "truststore/x509"), 0o755) + So(err, ShouldBeNil) + _, err = os.Create(path.Join(trustStorePath, "cert-test.crt")) + So(err, ShouldBeNil) + + err = test.GenerateNotationCerts(tempDir, certName) + So(err, ShouldNotBeNil) + + err = os.RemoveAll(path.Join(notationDir, "localkeys")) + So(err, ShouldBeNil) + err = os.Remove(path.Join(trustStorePath, "cert-test.crt")) + So(err, ShouldBeNil) + err = os.Chmod(path.Join(notationDir, "truststore/x509/ca", certName), 0o555) + So(err, ShouldBeNil) + + err = test.GenerateNotationCerts(tempDir, certName) + So(err, ShouldNotBeNil) + }) +} diff --git a/test/blackbox/annotations.bats b/test/blackbox/annotations.bats index d5dad2b5..588edbd8 100644 --- a/test/blackbox/annotations.bats +++ b/test/blackbox/annotations.bats @@ -119,11 +119,33 @@ function teardown_file() { [ $(echo "${lines[-1]}" | jq '.data.ImageList[0].RepoName') = '"annotations"' ] [ "$status" -eq 0 ] - run notation cert generate-test --trust "notation-sign-test" + run notation cert generate-test "notation-sign-test" [ "$status" -eq 0 ] + + local trust_policy_file=${HOME}/.config/notation/trustpolicy.json + + cat >${trust_policy_file} <${trust_policy_file} <