diff --git a/Makefile b/Makefile index 017fe523..e959ec3d 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ STACKER := $(shell which stacker) GOLINTER := $(TOOLSDIR)/bin/golangci-lint GOLINTER_VERSION := v1.52.2 NOTATION := $(TOOLSDIR)/bin/notation -NOTATION_VERSION := 1.0.0-rc.4 +NOTATION_VERSION := 1.0.0 COSIGN := $(TOOLSDIR)/bin/cosign COSIGN_VERSION := 2.2.0 HELM := $(TOOLSDIR)/bin/helm diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index 05b53f12..0f446eaa 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -5233,7 +5233,7 @@ func TestImageSignatures(t *testing.T) { So(err, ShouldBeNil) image := fmt.Sprintf("localhost:%s/%s:%s", port, repoName, "1.0") - err = signature.SignWithNotation("good", image, tdir) + err = signature.SignWithNotation("good", image, tdir, true) So(err, ShouldBeNil) err = signature.VerifyWithNotation(image, tdir) @@ -7806,7 +7806,7 @@ func TestGCSignaturesAndUntaggedManifestsWithMetaDB(t *testing.T) { So(err, ShouldBeNil) // sign the image - err = signature.SignWithNotation("good", image, tdir) + err = signature.SignWithNotation("good", image, tdir, true) So(err, ShouldBeNil) // get cosign signature manifest diff --git a/pkg/cli/client/image_cmd_test.go b/pkg/cli/client/image_cmd_test.go index 90ba24a1..00561818 100644 --- a/pkg/cli/client/image_cmd_test.go +++ b/pkg/cli/client/image_cmd_test.go @@ -139,7 +139,7 @@ func TestSignature(t *testing.T) { err = UploadImage(CreateDefaultImage(), url, repoName, "0.0.1") So(err, ShouldBeNil) - err = signature.SignImageUsingNotary("repo7:0.0.1", port) + err = signature.SignImageUsingNotary("repo7:0.0.1", port, true) So(err, ShouldBeNil) searchConfig := getTestSearchConfig(url, client.NewSearchService()) diff --git a/pkg/common/common.go b/pkg/common/common.go index 1aa354a4..6081ab7d 100644 --- a/pkg/common/common.go +++ b/pkg/common/common.go @@ -6,6 +6,7 @@ import ( "fmt" "io/fs" "os" + "regexp" "strings" "syscall" "time" @@ -119,3 +120,11 @@ func ContainsStringIgnoreCase(strSlice []string, str string) bool { return false } + +// this function will check if tag is a referrers tag +// (https://github.com/opencontainers/distribution-spec/blob/main/spec.md#referrers-tag-schema). +func IsReferrersTag(tag string) bool { + referrersTagRule := regexp.MustCompile(`sha256\-[A-Za-z0-9]*$`) + + return referrersTagRule.MatchString(tag) +} diff --git a/pkg/extensions/extension_image_trust_test.go b/pkg/extensions/extension_image_trust_test.go index 654a9cb6..af3b24fc 100644 --- a/pkg/extensions/extension_image_trust_test.go +++ b/pkg/extensions/extension_image_trust_test.go @@ -366,7 +366,7 @@ func RunSignatureUploadAndVerificationTests(t *testing.T, cacheDriverParams map[ // sign the image imageURL := fmt.Sprintf("localhost:%s/%s", port, fmt.Sprintf("%s:%s", repo, tag)) - err = signature.SignWithNotation(certName, imageURL, rootDir) + err = signature.SignWithNotation(certName, imageURL, rootDir, true) So(err, ShouldBeNil) found, err = test.ReadLogFileAndSearchString(logFile.Name(), "update signatures validity", 10*time.Second) @@ -499,7 +499,7 @@ func RunSignatureUploadAndVerificationTests(t *testing.T, cacheDriverParams map[ // sign the image imageURL := fmt.Sprintf("localhost:%s/%s", port, fmt.Sprintf("%s:%s", repo, tag)) - err = signature.SignWithNotation(certName, imageURL, rootDir) + err = signature.SignWithNotation(certName, imageURL, rootDir, false) So(err, ShouldBeNil) found, err = test.ReadLogFileAndSearchString(logFile.Name(), "update signatures validity", 10*time.Second) diff --git a/pkg/extensions/imagetrust/image_trust_test.go b/pkg/extensions/imagetrust/image_trust_test.go index de204474..d7bd53a2 100644 --- a/pkg/extensions/imagetrust/image_trust_test.go +++ b/pkg/extensions/imagetrust/image_trust_test.go @@ -437,7 +437,7 @@ func TestVerifySignatures(t *testing.T) { // sign the image image := fmt.Sprintf("localhost:%s/%s", port, fmt.Sprintf("%s:%s", repo, tag)) - err = signature.SignWithNotation("notation-sign-test", image, notationDir) + err = signature.SignWithNotation("notation-sign-test", image, notationDir, true) So(err, ShouldBeNil) err = test.CopyFiles(path.Join(notationDir, "notation", "truststore"), path.Join(notationDir, "truststore")) @@ -1271,7 +1271,7 @@ func RunVerificationTests(t *testing.T, dbDriverParams map[string]interface{}) { // sign the image imageURL := fmt.Sprintf("localhost:%s/%s", port, fmt.Sprintf("%s:%s", repo, tag)) - err = signature.SignWithNotation(certName, imageURL, notationDir) + err = signature.SignWithNotation(certName, imageURL, notationDir, false) So(err, ShouldBeNil) indexContent, err := ctlr.StoreController.DefaultStore.GetIndexContent(repo) diff --git a/pkg/extensions/search/search_test.go b/pkg/extensions/search/search_test.go index 1ed41016..f704e48c 100644 --- a/pkg/extensions/search/search_test.go +++ b/pkg/extensions/search/search_test.go @@ -4422,7 +4422,7 @@ func TestMetaDBWhenSigningImages(t *testing.T) { }) Convey("Sign with notation", func() { - err = signature.SignImageUsingNotary("repo1:1.0.1", port) + err = signature.SignImageUsingNotary("repo1:1.0.1", port, true) So(err, ShouldBeNil) resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(queryImage1)) @@ -4439,7 +4439,7 @@ func TestMetaDBWhenSigningImages(t *testing.T) { }) Convey("Sign with notation index", func() { - err = signature.SignImageUsingNotary("repo1:index", port) + err = signature.SignImageUsingNotary("repo1:index", port, false) So(err, ShouldBeNil) resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(queryIndex)) @@ -5438,7 +5438,7 @@ func TestMetaDBWhenDeletingImages(t *testing.T) { Convey("Delete a notary signature", func() { repo := "repo1" - err := signature.SignImageUsingNotary("repo1:1.0.1", port) + err := signature.SignImageUsingNotary("repo1:1.0.1", port, true) So(err, ShouldBeNil) query := ` diff --git a/pkg/extensions/sync/constants/consts.go b/pkg/extensions/sync/constants/consts.go index 69484d24..cd7247be 100644 --- a/pkg/extensions/sync/constants/consts.go +++ b/pkg/extensions/sync/constants/consts.go @@ -5,5 +5,6 @@ const ( Oras = "OrasReference" Cosign = "CosignSignature" OCI = "OCIReference" + Tag = "TagReference" SyncBlobUploadDir = ".sync" ) diff --git a/pkg/extensions/sync/references/cosign.go b/pkg/extensions/sync/references/cosign.go index 74d399f9..1b842b6e 100644 --- a/pkg/extensions/sync/references/cosign.go +++ b/pkg/extensions/sync/references/cosign.go @@ -161,10 +161,8 @@ func (ref CosignReference) SyncReferences(ctx context.Context, localRepo, remote } if isSig { - err = ref.metaDB.AddManifestSignature(localRepo, signedManifestDig, mTypes.SignatureMetadata{ - SignatureType: sigType, - SignatureDigest: referenceDigest.String(), - }) + err = addSigToMeta(ref.metaDB, localRepo, sigType, cosignTag, signedManifestDig, referenceDigest, + manifestBuf, imageStore, ref.log) } else { err = meta.SetImageMetaFromInput(localRepo, cosignTag, ispec.MediaTypeImageManifest, referenceDigest, manifestBuf, ref.storeController.GetImageStore(localRepo), diff --git a/pkg/extensions/sync/references/oci.go b/pkg/extensions/sync/references/oci.go index 4a98a33e..beac3031 100644 --- a/pkg/extensions/sync/references/oci.go +++ b/pkg/extensions/sync/references/oci.go @@ -145,10 +145,8 @@ func (ref OciReferences) SyncReferences(ctx context.Context, localRepo, remoteRe } if isSig { - err = ref.metaDB.AddManifestSignature(localRepo, signedManifestDig, mTypes.SignatureMetadata{ - SignatureType: sigType, - SignatureDigest: referenceDigest.String(), - }) + err = addSigToMeta(ref.metaDB, localRepo, sigType, referrer.Digest.String(), signedManifestDig, referenceDigest, + referenceBuf, imageStore, ref.log) } else { err = meta.SetImageMetaFromInput(localRepo, referenceDigest.String(), referrer.MediaType, referenceDigest, referenceBuf, ref.storeController.GetImageStore(localRepo), diff --git a/pkg/extensions/sync/references/references.go b/pkg/extensions/sync/references/references.go index ce1acf29..9e448100 100644 --- a/pkg/extensions/sync/references/references.go +++ b/pkg/extensions/sync/references/references.go @@ -17,6 +17,7 @@ import ( "zotregistry.io/zot/pkg/common" client "zotregistry.io/zot/pkg/extensions/sync/httpclient" "zotregistry.io/zot/pkg/log" + "zotregistry.io/zot/pkg/meta" mTypes "zotregistry.io/zot/pkg/meta/types" "zotregistry.io/zot/pkg/storage" storageTypes "zotregistry.io/zot/pkg/storage/types" @@ -42,6 +43,7 @@ func NewReferences(httpClient *client.Client, storeController storage.StoreContr refs := References{log: log} refs.referenceList = append(refs.referenceList, NewCosignReference(httpClient, storeController, metaDB, log)) + refs.referenceList = append(refs.referenceList, NewTagReferences(httpClient, storeController, metaDB, log)) refs.referenceList = append(refs.referenceList, NewOciReferences(httpClient, storeController, metaDB, log)) refs.referenceList = append(refs.referenceList, NewORASReferences(httpClient, storeController, metaDB, log)) @@ -215,3 +217,21 @@ func getNotationManifestsFromOCIRefs(ociRefs ispec.Index) []ispec.Descriptor { return notaryManifests } + +func addSigToMeta( + metaDB mTypes.MetaDB, repo, sigType, tag string, signedManifestDig, referenceDigest godigest.Digest, + referenceBuf []byte, imageStore storageTypes.ImageStore, log log.Logger, +) error { + layersInfo, errGetLayers := meta.GetSignatureLayersInfo(repo, tag, referenceDigest.String(), + sigType, referenceBuf, imageStore, log) + + if errGetLayers != nil { + return errGetLayers + } + + return metaDB.AddManifestSignature(repo, signedManifestDig, mTypes.SignatureMetadata{ + SignatureType: sigType, + SignatureDigest: referenceDigest.String(), + LayersInfo: layersInfo, + }) +} diff --git a/pkg/extensions/sync/references/references_internal_test.go b/pkg/extensions/sync/references/references_internal_test.go index 2f968906..7a85c2fd 100644 --- a/pkg/extensions/sync/references/references_internal_test.go +++ b/pkg/extensions/sync/references/references_internal_test.go @@ -86,6 +86,54 @@ func TestOci(t *testing.T) { }) } +func TestReferrersTag(t *testing.T) { + Convey("trigger errors", t, func() { + cfg := client.Config{ + URL: "url", + TLSVerify: false, + } + + client, err := client.New(cfg, log.NewLogger("debug", "")) + So(err, ShouldBeNil) + + referrersTag := NewTagReferences(client, storage.StoreController{DefaultStore: mocks.MockedImageStore{ + GetImageManifestFn: func(repo, reference string) ([]byte, godigest.Digest, string, error) { + return []byte{}, "", "", errRef + }, + }}, nil, log.NewLogger("debug", "")) + + ok := referrersTag.IsSigned(context.Background(), "repo", "") + So(ok, ShouldBeFalse) + + // trigger GetImageManifest err + ok, err = referrersTag.canSkipReferences("repo", "subjectdigest", "digest") + So(err, ShouldNotBeNil) + So(ok, ShouldBeFalse) + + referrersTag = NewTagReferences(client, storage.StoreController{DefaultStore: mocks.MockedImageStore{ + GetImageManifestFn: func(repo, reference string) ([]byte, godigest.Digest, string, error) { + return []byte{}, "", "", zerr.ErrManifestNotFound + }, + }}, nil, log.NewLogger("debug", "")) + + // trigger GetImageManifest err + ok, err = referrersTag.canSkipReferences("repo", "subjectdigest", "digest") + So(err, ShouldBeNil) + So(ok, ShouldBeFalse) + + referrersTag = NewTagReferences(client, storage.StoreController{DefaultStore: mocks.MockedImageStore{ + GetImageManifestFn: func(repo, reference string) ([]byte, godigest.Digest, string, error) { + return []byte{}, "digest", "", nil + }, + }}, nil, log.NewLogger("debug", "")) + + // different digest + ok, err = referrersTag.canSkipReferences("repo", "subjectdigest", "newdigest") + So(err, ShouldBeNil) + So(ok, ShouldBeFalse) + }) +} + func TestORAS(t *testing.T) { Convey("trigger errors", t, func() { cfg := client.Config{ @@ -392,3 +440,14 @@ func TestCompareArtifactRefs(t *testing.T) { } }) } + +func TestAddSigToMeta(t *testing.T) { + Convey("Test addSigToMeta", t, func() { + imageStore := mocks.MockedImageStore{} + metaDB := mocks.MetaDBMock{} + + err := addSigToMeta(metaDB, "repo", "cosign", "tag", godigest.FromString("signedmanifest"), + godigest.FromString("reference"), []byte("bad"), imageStore, log.Logger{}) + So(err, ShouldNotBeNil) + }) +} diff --git a/pkg/extensions/sync/references/referrers_tag.go b/pkg/extensions/sync/references/referrers_tag.go new file mode 100644 index 00000000..4628add3 --- /dev/null +++ b/pkg/extensions/sync/references/referrers_tag.go @@ -0,0 +1,185 @@ +//go:build sync +// +build sync + +package references + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + + godigest "github.com/opencontainers/go-digest" + ispec "github.com/opencontainers/image-spec/specs-go/v1" + + zerr "zotregistry.io/zot/errors" + "zotregistry.io/zot/pkg/common" + "zotregistry.io/zot/pkg/extensions/sync/constants" + client "zotregistry.io/zot/pkg/extensions/sync/httpclient" + "zotregistry.io/zot/pkg/log" + "zotregistry.io/zot/pkg/meta" + mTypes "zotregistry.io/zot/pkg/meta/types" + "zotregistry.io/zot/pkg/storage" +) + +type TagReferences struct { + client *client.Client + storeController storage.StoreController + metaDB mTypes.MetaDB + log log.Logger +} + +func NewTagReferences(httpClient *client.Client, storeController storage.StoreController, + metaDB mTypes.MetaDB, log log.Logger, +) TagReferences { + return TagReferences{ + client: httpClient, + storeController: storeController, + metaDB: metaDB, + log: log, + } +} + +func (ref TagReferences) Name() string { + return constants.Tag +} + +func (ref TagReferences) IsSigned(ctx context.Context, remoteRepo, subjectDigestStr string) bool { + return false +} + +func (ref TagReferences) canSkipReferences(localRepo, subjectDigestStr, digest string) (bool, error) { + imageStore := ref.storeController.GetImageStore(localRepo) + + _, localDigest, _, err := imageStore.GetImageManifest(localRepo, getReferrersTagFromSubjectDigest(subjectDigestStr)) + if err != nil { + if errors.Is(err, zerr.ErrManifestNotFound) { + return false, nil + } + + ref.log.Error().Str("errorType", common.TypeOf(err)). + Str("repository", localRepo).Str("subject", subjectDigestStr). + Err(err).Msg("couldn't get local index with referrers tag for image") + + return false, err + } + + if localDigest.String() != digest { + return false, nil + } + + ref.log.Info().Str("repository", localRepo).Str("reference", subjectDigestStr). + Msg("skipping index with referrers tag for image, already synced") + + return true, nil +} + +func (ref TagReferences) SyncReferences(ctx context.Context, localRepo, remoteRepo, subjectDigestStr string) ( + []godigest.Digest, error, +) { + refsDigests := make([]godigest.Digest, 0, 10) + + index, indexContent, err := ref.getIndex(ctx, remoteRepo, subjectDigestStr) + if err != nil { + return refsDigests, err + } + + skipTagRefs, err := ref.canSkipReferences(localRepo, subjectDigestStr, string(godigest.FromBytes(indexContent))) + if err != nil { + ref.log.Error().Err(err).Str("repository", localRepo).Str("subject", subjectDigestStr). + Msg("couldn't check if the upstream index with referrers tag for image can be skipped") + } + + if skipTagRefs { + return refsDigests, nil + } + + imageStore := ref.storeController.GetImageStore(localRepo) + + ref.log.Info().Str("repository", localRepo).Str("subject", subjectDigestStr). + Msg("syncing oci references for image") + + for _, referrer := range index.Manifests { + referenceBuf, referenceDigest, err := syncManifest(ctx, ref.client, imageStore, localRepo, remoteRepo, + referrer, subjectDigestStr, ref.log) + if err != nil { + return refsDigests, err + } + + refsDigests = append(refsDigests, referenceDigest) + + if ref.metaDB != nil { + ref.log.Debug().Str("repository", localRepo).Str("subject", subjectDigestStr). + Msg("metaDB: trying to add oci references for image") + + isSig, sigType, signedManifestDig, err := storage.CheckIsImageSignature(localRepo, referenceBuf, + referrer.Digest.String()) + if err != nil { + return refsDigests, fmt.Errorf("failed to check if oci reference '%s@%s' is a signature: %w", localRepo, + referrer.Digest.String(), err) + } + + if isSig { + err = addSigToMeta(ref.metaDB, localRepo, sigType, referrer.Digest.String(), signedManifestDig, referenceDigest, + referenceBuf, imageStore, ref.log) + } else { + err = meta.SetImageMetaFromInput(localRepo, referenceDigest.String(), referrer.MediaType, + referenceDigest, referenceBuf, ref.storeController.GetImageStore(localRepo), + ref.metaDB, ref.log) + } + + if err != nil { + return refsDigests, fmt.Errorf("failed to set metadata for oci reference in '%s@%s': %w", + localRepo, subjectDigestStr, err) + } + + ref.log.Info().Str("repository", localRepo).Str("subject", subjectDigestStr). + Msg("metaDB: successfully added oci references to MetaDB for image") + } + } + + ref.log.Info().Str("repository", localRepo).Str("subject", subjectDigestStr). + Msg("syncing index with referrers tag for image") + + referrersTag := getReferrersTagFromSubjectDigest(subjectDigestStr) + + _, _, err = imageStore.PutImageManifest(localRepo, referrersTag, index.MediaType, indexContent) + if err != nil { + ref.log.Error().Str("errorType", common.TypeOf(err)). + Str("repository", localRepo).Str("subject", subjectDigestStr). + Err(err).Msg("couldn't upload index with referrers tag for image") + + return refsDigests, err + } + + ref.log.Info().Str("repository", localRepo).Str("subject", subjectDigestStr). + Msg("successfully synced index with referrers tag for image") + + return refsDigests, nil +} + +func (ref TagReferences) getIndex( + ctx context.Context, repo, subjectDigestStr string, +) (ispec.Index, []byte, error) { + var index ispec.Index + + content, _, statusCode, err := ref.client.MakeGetRequest(ctx, &index, ispec.MediaTypeImageIndex, + "v2", repo, "manifests", getReferrersTagFromSubjectDigest(subjectDigestStr)) + if err != nil { + if statusCode == http.StatusNotFound { + ref.log.Debug().Str("repository", repo).Str("subject", subjectDigestStr). + Msg("couldn't find any index with referrers tag for image, skipping") + + return index, []byte{}, zerr.ErrSyncReferrerNotFound + } + + return index, []byte{}, err + } + + return index, content, nil +} + +func getReferrersTagFromSubjectDigest(digestStr string) string { + return strings.Replace(digestStr, ":", "-", 1) +} diff --git a/pkg/extensions/sync/service.go b/pkg/extensions/sync/service.go index f384b2b0..a121c7ce 100644 --- a/pkg/extensions/sync/service.go +++ b/pkg/extensions/sync/service.go @@ -298,7 +298,7 @@ func (service *BaseService) SyncRepo(ctx context.Context, repo string) error { default: } - if references.IsCosignTag(tag) { + if references.IsCosignTag(tag) || common.IsReferrersTag(tag) { continue } @@ -374,7 +374,8 @@ func (service *BaseService) syncTag(ctx context.Context, localRepo, remoteRepo, return "", zerr.ErrMediaTypeNotSupported } - if service.config.OnlySigned != nil && *service.config.OnlySigned && !references.IsCosignTag(tag) { + if service.config.OnlySigned != nil && *service.config.OnlySigned && + !references.IsCosignTag(tag) && !common.IsReferrersTag(tag) { signed := service.references.IsSigned(ctx, remoteRepo, manifestDigest.String()) if !signed { // skip unsigned images diff --git a/pkg/extensions/sync/sync_test.go b/pkg/extensions/sync/sync_test.go index 20a57c01..52b5dcd7 100644 --- a/pkg/extensions/sync/sync_test.go +++ b/pkg/extensions/sync/sync_test.go @@ -898,6 +898,146 @@ func TestOnDemand(t *testing.T) { So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusOK) }) + + Convey("Sync referrers tag errors", func() { + // start upstream server + rootDir := t.TempDir() + port := test.GetFreePort() + srcBaseURL := test.GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + conf.Storage.GC = false + ctlr := api.NewController(conf) + ctlr.Config.Storage.RootDirectory = rootDir + + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(conf.HTTP.Port) + defer cm.StopServer() + + image := CreateRandomImage() + manifestBlob := image.ManifestDescriptor.Data + manifestDigest := image.ManifestDescriptor.Digest + + err := UploadImage(image, srcBaseURL, "remote-repo", "test") + So(err, ShouldBeNil) + + subjectDesc := ispec.Descriptor{ + MediaType: ispec.MediaTypeImageManifest, + Digest: manifestDigest, + Size: int64(len(manifestBlob)), + } + + ociRefImage := CreateDefaultImageWith().Subject(&subjectDesc).Build() + + err = UploadImage(ociRefImage, srcBaseURL, "remote-repo", ociRefImage.ManifestDescriptor.Digest.String()) + So(err, ShouldBeNil) + + tag := strings.Replace(manifestDigest.String(), ":", "-", 1) + + // add index with referrers tag + tagRefIndex := ispec.Index{ + MediaType: ispec.MediaTypeImageIndex, + Manifests: []ispec.Descriptor{ + { + MediaType: ispec.MediaTypeImageManifest, + Digest: ociRefImage.ManifestDescriptor.Digest, + Size: int64(len(ociRefImage.ManifestDescriptor.Data)), + }, + }, + Annotations: map[string]string{ispec.AnnotationRefName: tag}, + } + + tagRefIndex.SchemaVersion = 2 + + tagRefIndexBlob, err := json.Marshal(tagRefIndex) + So(err, ShouldBeNil) + + resp, err := resty.R(). + SetHeader("Content-type", ispec.MediaTypeImageIndex). + SetBody(tagRefIndexBlob). + Put(srcBaseURL + "/v2/remote-repo/manifests/" + tag) + + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusCreated) + + //------- Start downstream server + + var tlsVerify bool + + regex := ".*" + semver := true + + destPort := test.GetFreePort() + destConfig := config.New() + + destBaseURL := test.GetBaseURL(destPort) + + hostname, err := os.Hostname() + So(err, ShouldBeNil) + + syncRegistryConfig := syncconf.RegistryConfig{ + Content: []syncconf.Content{ + { + Prefix: "remote-repo", + Tags: &syncconf.Tags{ + Regex: ®ex, + Semver: &semver, + }, + }, + }, + // include self url, should be ignored + URLs: []string{ + fmt.Sprintf("http://%s", hostname), destBaseURL, + srcBaseURL, fmt.Sprintf("http://localhost:%s", destPort), + }, + TLSVerify: &tlsVerify, + CertDir: "", + OnDemand: true, + } + + defaultVal := true + syncConfig := &syncconf.Config{ + Enable: &defaultVal, + Registries: []syncconf.RegistryConfig{syncRegistryConfig}, + } + + 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.Sync = syncConfig + + dctlr := api.NewController(destConfig) + + // metadb fails for syncReferrersTag" + dctlr.MetaDB = mocks.MetaDBMock{ + SetManifestDataFn: func(manifestDigest godigest.Digest, mm mTypes.ManifestData) error { + if manifestDigest.String() == ociRefImage.ManifestDescriptor.Digest.String() { + return sync.ErrTestError + } + + return nil + }, + } + + dcm := test.NewControllerManager(dctlr) + dcm.StartAndWait(destPort) + defer dcm.StopServer() + + resp, err = resty.R().Get(destBaseURL + "/v2/remote-repo/manifests/test") + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + resp, err = resty.R().Get(destBaseURL + "/v2/remote-repo/manifests/" + tag) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) + }) }) } @@ -3818,7 +3958,10 @@ func TestPeriodicallySignaturesErr(t *testing.T) { So(found, ShouldBeTrue) // should not be synced nor sync on demand - resp, err = resty.R().Get(destBaseURL + artifactURLPath) + resp, err = resty.R(). + SetHeader("Content-Type", "application/json"). + SetQueryParam("artifactType", "application/vnd.cncf.icecream"). + Get(destBaseURL + artifactURLPath) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusOK) @@ -3871,7 +4014,10 @@ func TestPeriodicallySignaturesErr(t *testing.T) { So(found, ShouldBeTrue) // should not be synced nor sync on demand - resp, err = resty.R().Get(destBaseURL + artifactURLPath) + resp, err = resty.R(). + SetHeader("Content-Type", "application/json"). + SetQueryParam("artifactType", "application/vnd.cncf.icecream"). + Get(destBaseURL + artifactURLPath) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusOK) @@ -4475,7 +4621,7 @@ func TestSyncedSignaturesMetaDB(t *testing.T) { err := UploadImage(signedImage, srcBaseURL, repoName, tag) So(err, ShouldBeNil) - err = signature.SignImageUsingNotary(repoName+":"+tag, srcPort) + err = signature.SignImageUsingNotary(repoName+":"+tag, srcPort, true) So(err, ShouldBeNil) err = signature.SignImageUsingCosign(repoName+":"+tag, srcPort) @@ -5715,7 +5861,7 @@ func TestSyncSignaturesDiff(t *testing.T) { So(func() { signImage(tdir, srcPort, repoName, digest) }, ShouldNotPanic) // wait for signatures - time.Sleep(10 * time.Second) + time.Sleep(12 * time.Second) // notation verify the image image = fmt.Sprintf("localhost:%s/%s:%s", destPort, repoName, testImageTag) @@ -6301,8 +6447,8 @@ func signImage(tdir, port, repoName string, digest godigest.Digest) { // sign the image image := fmt.Sprintf("localhost:%s/%s@%s", port, repoName, digest.String()) - err = signature.SignWithNotation("good", image, tdir) - if err != nil { + err = signature.SignWithNotation("good", image, tdir, false) + if err != nil && !strings.Contains(err.Error(), "failed to delete dangling referrers index") { panic(err) } diff --git a/pkg/meta/hooks.go b/pkg/meta/hooks.go index 3a37814e..3817f2c9 100644 --- a/pkg/meta/hooks.go +++ b/pkg/meta/hooks.go @@ -3,6 +3,7 @@ package meta import ( godigest "github.com/opencontainers/go-digest" + zcommon "zotregistry.io/zot/pkg/common" "zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/meta/common" mTypes "zotregistry.io/zot/pkg/meta/types" @@ -15,6 +16,10 @@ import ( func OnUpdateManifest(repo, reference, mediaType string, digest godigest.Digest, body []byte, storeController storage.StoreController, metaDB mTypes.MetaDB, log log.Logger, ) error { + if zcommon.IsReferrersTag(reference) { + return nil + } + imgStore := storeController.GetImageStore(repo) // check if image is a signature @@ -87,6 +92,10 @@ func OnUpdateManifest(repo, reference, mediaType string, digest godigest.Digest, func OnDeleteManifest(repo, reference, mediaType string, digest godigest.Digest, manifestBlob []byte, storeController storage.StoreController, metaDB mTypes.MetaDB, log log.Logger, ) error { + if zcommon.IsReferrersTag(reference) { + return nil + } + imgStore := storeController.GetImageStore(repo) isSignature, signatureType, signedManifestDigest, err := storage.CheckIsImageSignature(repo, manifestBlob, @@ -154,7 +163,7 @@ func OnGetManifest(name, reference string, body []byte, return err } - if !isSignature { + if !isSignature && !zcommon.IsReferrersTag(reference) { err := metaDB.IncrementImageDownloads(name, reference) if err != nil { log.Error().Err(err).Str("repository", name).Str("reference", reference). diff --git a/pkg/meta/hooks_test.go b/pkg/meta/hooks_test.go index 0530f3e4..5f697905 100644 --- a/pkg/meta/hooks_test.go +++ b/pkg/meta/hooks_test.go @@ -108,6 +108,12 @@ func TestUpdateErrors(t *testing.T) { So(err, ShouldNotBeNil) }) + Convey("IsReferrersTag true", func() { + err := meta.OnUpdateManifest("repo", "sha256-123", "digest", "media", []byte("bad"), + storeController, metaDB, log) + So(err, ShouldBeNil) + }) + Convey("GetSignatureLayersInfo errors", func() { // get notation signature layers info badNotationManifestContent := ispec.Manifest{ @@ -180,6 +186,12 @@ func TestUpdateErrors(t *testing.T) { So(err, ShouldNotBeNil) }) + Convey("IsReferrersTag true", func() { + err := meta.OnDeleteManifest("repo", "sha256-123", "digest", "media", []byte("bad"), + storeController, metaDB, log) + So(err, ShouldBeNil) + }) + Convey("DeleteReferrers errors", func() { metaDB.DeleteReferrerFn = func(repo string, referredDigest, referrerDigest godigest.Digest) error { return ErrTestError diff --git a/pkg/meta/parse.go b/pkg/meta/parse.go index d9317f65..6613d29b 100644 --- a/pkg/meta/parse.go +++ b/pkg/meta/parse.go @@ -80,6 +80,10 @@ func ParseRepo(repo string, metaDB mTypes.MetaDB, storeController storage.StoreC for _, descriptor := range indexContent.Manifests { tag := descriptor.Annotations[ispec.AnnotationRefName] + if zcommon.IsReferrersTag(tag) { + continue + } + descriptorBlob, err := getCachedBlob(repo, descriptor, metaDB, imageStore, log) if err != nil { log.Error().Err(err).Msg("load-repo: error checking manifestMeta in MetaDB") @@ -299,6 +303,11 @@ func getNotationSignatureLayersInfo( return layers, err } + // skip if is a notation index + if manifestContent.MediaType == ispec.MediaTypeImageIndex { + return []mTypes.LayerInfo{}, nil + } + if len(manifestContent.Layers) != 1 { log.Error().Err(zerr.ErrBadManifest).Str("repository", repo).Str("reference", manifestDigest). Msg("load-repo: notation signature manifest requires exactly one layer but it does not") diff --git a/pkg/meta/parse_test.go b/pkg/meta/parse_test.go index 21ae0020..103d847e 100644 --- a/pkg/meta/parse_test.go +++ b/pkg/meta/parse_test.go @@ -347,6 +347,33 @@ func TestParseStorageErrors(t *testing.T) { err = meta.ParseRepo("repo", metaDB, storeController, log) So(err, ShouldNotBeNil) }) + + Convey("IsReferrersTag -> true", func() { + indexContent := ispec.Index{ + Manifests: []ispec.Descriptor{ + { + Digest: godigest.FromString("indx1"), + MediaType: ispec.MediaTypeImageIndex, + Annotations: map[string]string{ + ispec.AnnotationRefName: "sha256-123", + }, + }, + }, + } + indexBlob, err := json.Marshal(indexContent) + So(err, ShouldBeNil) + + imageStore.GetIndexContentFn = func(repo string) ([]byte, error) { + return indexBlob, nil + } + + metaDB.SetIndexDataFn = func(digest godigest.Digest, indexData mTypes.IndexData) error { + return ErrTestError + } + + err = meta.ParseRepo("repo", metaDB, storeController, log) + So(err, ShouldBeNil) + }) }) }) } @@ -605,6 +632,19 @@ func TestGetSignatureLayersInfo(t *testing.T) { So(layers, ShouldBeEmpty) }) + Convey("notation index", t, func() { + notationIndex := ispec.Index{ + MediaType: ispec.MediaTypeImageIndex, + } + + notationIndexBlob, err := json.Marshal(notationIndex) + So(err, ShouldBeNil) + layers, err := meta.GetSignatureLayersInfo("repo", "tag", "123", zcommon.NotationSignature, notationIndexBlob, + nil, log.NewLogger("debug", "")) + So(err, ShouldBeNil) + So(layers, ShouldBeEmpty) + }) + Convey("error while unmarshaling manifest content", t, func() { _, err := meta.GetSignatureLayersInfo("repo", "tag", "123", zcommon.CosignSignature, []byte("bad manifest"), nil, log.NewLogger("debug", "")) diff --git a/pkg/test/oci-utils/oci_layout_test.go b/pkg/test/oci-utils/oci_layout_test.go index 7fcee6fe..31319a71 100644 --- a/pkg/test/oci-utils/oci_layout_test.go +++ b/pkg/test/oci-utils/oci_layout_test.go @@ -325,7 +325,7 @@ func TestBaseOciLayoutUtils(t *testing.T) { isSigned := olu.CheckManifestSignature(repo, manifestList[0].Digest) So(isSigned, ShouldBeFalse) - err = signature.SignImageUsingNotary(fmt.Sprintf("%s:%s", repo, tag), port) + err = signature.SignImageUsingNotary(fmt.Sprintf("%s:%s", repo, tag), port, true) So(err, ShouldBeNil) isSigned = olu.CheckManifestSignature(repo, manifestList[0].Digest) diff --git a/pkg/test/signature/notation.go b/pkg/test/signature/notation.go index dcfa899d..7b85e888 100644 --- a/pkg/test/signature/notation.go +++ b/pkg/test/signature/notation.go @@ -139,7 +139,7 @@ func GenerateNotationCerts(tdir string, certName string) error { return nil } -func SignWithNotation(keyName string, reference string, tdir string) error { +func SignWithNotation(keyName, reference, tdir string, referrersCapability bool) error { ctx := context.TODO() // getSigner @@ -193,6 +193,10 @@ func SignWithNotation(keyName string, reference string, tdir string) error { PlainHTTP: plainHTTP, } + if !referrersCapability { + _ = remoteRepo.SetReferrersCapability(false) + } + repositoryOpts := notreg.RepositoryOptions{} sigRepo := notreg.NewRepositoryWithOptions(remoteRepo, repositoryOpts) @@ -432,7 +436,7 @@ func LoadNotationConfig(tdir string) (*notconfig.Config, error) { return configInfo, nil } -func SignImageUsingNotary(repoTag, port string) error { +func SignImageUsingNotary(repoTag, port string, referrersCapability bool) error { cwd, err := os.Getwd() if err != nil { return err @@ -463,7 +467,7 @@ func SignImageUsingNotary(repoTag, port string) error { // sign the image image := fmt.Sprintf("localhost:%s/%s", port, repoTag) - err = SignWithNotation("notation-sign-test", image, tdir) + err = SignWithNotation("notation-sign-test", image, tdir, referrersCapability) return err } diff --git a/pkg/test/signature/notation_test.go b/pkg/test/signature/notation_test.go index 0a4089c8..39596531 100644 --- a/pkg/test/signature/notation_test.go +++ b/pkg/test/signature/notation_test.go @@ -114,7 +114,7 @@ func TestLoadNotationConfig(t *testing.T) { func TestSignWithNotation(t *testing.T) { Convey("notation directory doesn't exist", t, func() { - err := signature.SignWithNotation("key", "reference", t.TempDir()) + err := signature.SignWithNotation("key", "reference", t.TempDir(), true) So(err, ShouldNotBeNil) }) @@ -128,7 +128,7 @@ func TestSignWithNotation(t *testing.T) { err = os.WriteFile(filePath, []byte("{}"), 0o666) //nolint: gosec So(err, ShouldBeNil) - err = signature.SignWithNotation("key", "reference", tempDir) + err = signature.SignWithNotation("key", "reference", tempDir, true) So(err, ShouldEqual, signature.ErrKeyNotFound) }) @@ -150,7 +150,7 @@ func TestSignWithNotation(t *testing.T) { err = os.Chmod(path.Join(tdir, "notation", "localkeys"), 0o000) So(err, ShouldBeNil) - err = signature.SignWithNotation("key", "reference", tdir) + err = signature.SignWithNotation("key", "reference", tdir, true) So(err, ShouldNotBeNil) err = os.Chmod(path.Join(tdir, "notation", "localkeys"), 0o755) @@ -172,7 +172,7 @@ func TestSignWithNotation(t *testing.T) { err = signature.GenerateNotationCerts(tdir, "key") So(err, ShouldBeNil) - err = signature.SignWithNotation("key", "invalidReference", tdir) + err = signature.SignWithNotation("key", "invalidReference", tdir, true) So(err, ShouldNotBeNil) }) @@ -191,7 +191,7 @@ func TestSignWithNotation(t *testing.T) { err = signature.GenerateNotationCerts(tdir, "key") So(err, ShouldBeNil) - err = signature.SignWithNotation("key", "localhost:8080/invalidreference:1.0", tdir) + err = signature.SignWithNotation("key", "localhost:8080/invalidreference:1.0", tdir, true) So(err, ShouldNotBeNil) }) } diff --git a/test/blackbox/annotations.bats b/test/blackbox/annotations.bats index dc9f9e3a..bfada64b 100644 --- a/test/blackbox/annotations.bats +++ b/test/blackbox/annotations.bats @@ -163,10 +163,50 @@ function teardown_file() { } EOF - run notation sign --key "notation-sign-test" --plain-http localhost:8080/annotations:latest + run notation sign --key "notation-sign-test" --insecure-registry localhost:8080/annotations:latest [ "$status" -eq 0 ] - run notation verify --plain-http localhost:8080/annotations:latest + run notation verify --insecure-registry localhost:8080/annotations:latest [ "$status" -eq 0 ] - run notation list --plain-http localhost:8080/annotations:latest + run notation list --insecure-registry localhost:8080/annotations:latest [ "$status" -eq 0 ] } + +@test "sign/verify with notation( NOTATION_EXPERIMENTAL=1 and --allow-referrers-api )" { + run curl -X POST -H "Content-Type: application/json" --data '{ "query": "{ ImageList(repo: \"annotations\") { Results { RepoName Tag Manifests {Digest ConfigDigest Size Layers { Size Digest }} Vendor Licenses }}}"}' http://localhost:8080/v2/_zot/ext/search + [ "$status" -eq 0 ] + [ $(echo "${lines[-1]}" | jq '.data.ImageList.Results[0].RepoName') = '"annotations"' ] + [ "$status" -eq 0 ] + + run notation cert generate-test "notation-sign-test-experimental" + [ "$status" -eq 0 ] + + local trust_policy_file=${HOME}/.config/notation/trustpolicy.json + + cat >${trust_policy_file} <