Repository: google/go-containerregistry
Pull request #1546 (2023-01-27 22:46:57)

Support for OCI 1.1+ referrers via API
Follow-up to #1543, use the referrers API endpoint if the registry supports it, otherwise fallback to digest tag

codecov-commenter: # [Codecov](https://codecov.io/gh/google/go-containerregistry/pull/1546?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) Report
> Merging [#1546](https://codecov.io/gh/google/go-containerregistry/pull/1546?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) (224b9c7) into [main](https://codecov.io/gh/google/go-containerregistry/commit/da1008fb1ac8dfc6f74bdf380c56719b0b7b5032?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) (da1008f) will **decrease** coverage by `0.49%`.
> The diff coverage is `48.96%`.

```diff
@@            Coverage Diff             @@
##             main    #1546      +/-   ##
==========================================
- Coverage   73.07%   72.59%   -0.49%     
==========================================
  Files         117      118       +1     
  Lines        9082     9276     +194     
==========================================
+ Hits         6637     6734      +97     
- Misses       1780     1850      +70     
- Partials      665      692      +27     
```


| [Impacted Files](https://codecov.io/gh/google/go-containerregistry/pull/1546?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) | Coverage Δ | |
|---|---|---|
| [pkg/v1/manifest.go](https://codecov.io/gh/google/go-containerregistry/pull/1546?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#diff-cGtnL3YxL21hbmlmZXN0Lmdv) | `100.00% <ø> (ø)` | |
| [pkg/v1/remote/delete.go](https://codecov.io/gh/google/go-containerregistry/pull/1546?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#diff-cGtnL3YxL3JlbW90ZS9kZWxldGUuZ28=) | `58.62% <ø> (ø)` | |
| [pkg/v1/mutate/mutate.go](https://codecov.io/gh/google/go-containerregistry/pull/1546?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#diff-cGtnL3YxL211dGF0ZS9tdXRhdGUuZ28=) | `69.01% <10.52%> (-3.80%)` | :arrow_down: |
| [pkg/v1/remote/referrers.go](https://codecov.io/gh/google/go-containerregistry/pull/1546?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#diff-cGtnL3YxL3JlbW90ZS9yZWZlcnJlcnMuZ28=) | `40.00% <40.00%> (ø)` | |
| [pkg/v1/remote/descriptor.go](https://codecov.io/gh/google/go-containerregistry/pull/1546?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#diff-cGtnL3YxL3JlbW90ZS9kZXNjcmlwdG9yLmdv) | `71.12% <50.94%> (-3.16%)` | :arrow_down: |
| [pkg/v1/remote/write.go](https://codecov.io/gh/google/go-containerregistry/pull/1546?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#diff-cGtnL3YxL3JlbW90ZS93cml0ZS5nbw==) | `62.29% <51.45%> (-1.83%)` | :arrow_down: |
| [pkg/v1/mutate/image.go](https://codecov.io/gh/google/go-containerregistry/pull/1546?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#diff-cGtnL3YxL211dGF0ZS9pbWFnZS5nbw==) | `69.56% <100.00%> (+0.33%)` | :arrow_up: |
| [pkg/v1/mutate/index.go](https://codecov.io/gh/google/go-containerregistry/pull/1546?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#diff-cGtnL3YxL211dGF0ZS9pbmRleC5nbw==) | `79.33% <100.00%> (+0.34%)` | :arrow_up: |
| [pkg/v1/remote/options.go](https://codecov.io/gh/google/go-containerregistry/pull/1546?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#diff-cGtnL3YxL3JlbW90ZS9vcHRpb25zLmdv) | `77.77% <100.00%> (+1.41%)` | :arrow_up: |
| ... and [2 more](https://codecov.io/gh/google/go-containerregistry/pull/1546?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google)


Patch:
diff --git a/cmd/crane/README.md b/cmd/crane/README.md
index 3d6780f5..76a7fb09 100644
--- a/cmd/crane/README.md
+++ b/cmd/crane/README.md
@@ -10,8 +10,9 @@ A collection of useful things you can do with `crane` is [here](recipes.md).
 ## Installation
 
 ### Install from Releases
-Download [latest release](https://github.com/google/go-containerregistry/releases/latest):
-```
+1. Download [latest release](https://github.com/google/go-containerregistry/releases/latest):
+
+```sh
 $ OS=Linux       # or Darwin, Windows
 $ ARCH=x86_64    # or arm64, x86_64, armv6, i386, s390x
 $ curl -sL "https://github.com/google/go-containerregistry/releases/latest/download/go-containerregistry_${OS}_${ARCH}.tar.gz" > go-containerregistry.tar.gz
@@ -25,18 +26,25 @@ $ ARCH=x86_64    # or arm64, x86_64, armv6, i386, s390x
 $ curl -sL "https://github.com/google/go-containerregistry/releases/download/v${VERSION}/go-containerregistry_${OS}_${ARCH}.tar.gz" > go-containerregistry.tar.gz
 ```
 
+2. Verify the signature
 We generate [SLSA 3 provenance](https://slsa.dev) using the OpenSSF's [slsa-framework/slsa-github-generator](https://github.com/slsa-framework/slsa-github-generator). To verify our release, install the verification tool from [slsa-framework/slsa-verifier#installation](https://github.com/slsa-framework/slsa-verifier#installation) and verify as follows:
 
-```
+```sh
 $ curl -sL https://github.com/google/go-containerregistry/releases/download/v${VERSION}/attestation.intoto.jsonl > provenance.intoto.jsonl
 $ slsa-verifier -artifact-path go-containerregistry.tar.gz -provenance provenance.intoto.jsonl -source github.com/google/go-containerregistry -tag "v${VERSION}"
   PASSED: Verified SLSA provenance
 ```
 
+3. Unpack it in the PATH.
+
+```sh
+$ tar -zxvf go-containerregistry.tar.gz -C /usr/local/bin/ crane
+```
+
 ### Install manually
 Install manually:
 
-```
+```sh
 go install github.com/google/go-containerregistry/cmd/crane@latest
 ```
 
diff --git a/internal/cmd/edit.go b/internal/cmd/edit.go
index b54ba933..d0e4d0de 100644
--- a/internal/cmd/edit.go
+++ b/internal/cmd/edit.go
@@ -84,18 +84,18 @@ func NewCmdEditConfig(options *[]crane.Option) *cobra.Command {
 
 // NewCmdManifest creates a new cobra.Command for the manifest subcommand.
 func NewCmdEditManifest(options *[]crane.Option) *cobra.Command {
-	var dst string
+	var (
+		dst string
+		mt  string
+	)
 	cmd := &cobra.Command{
 		Use:   "manifest",
 		Short: "Edit an image's manifest.",
-		Example: `  # Edit ubuntu's config file
-  crane edit config ubuntu
-
-  # Overwrite ubuntu's config file with '{}'
-  echo '{}' | crane edit config ubuntu`,
+		Example: `  # Edit ubuntu's manifest
+  crane edit manifest ubuntu`,
 		Args: cobra.ExactArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
-			ref, err := editManifest(cmd.InOrStdin(), cmd.OutOrStdout(), args[0], dst, *options...)
+			ref, err := editManifest(cmd.InOrStdin(), cmd.OutOrStdout(), args[0], dst, mt, *options...)
 			if err != nil {
 				return fmt.Errorf("editing manifest: %w", err)
 			}
@@ -104,6 +104,7 @@ func NewCmdEditManifest(options *[]crane.Option) *cobra.Command {
 		},
 	}
 	cmd.Flags().StringVarP(&dst, "tag", "t", "", "New tag reference to apply to mutated image. If not provided, uses original tag or pushes a new digest.")
+	cmd.Flags().StringVarP(&mt, "media-type", "m", "", "Override the mediaType used as the Content-Type for PUT")
 
 	return cmd
 }
@@ -233,7 +234,7 @@ func editConfig(in io.Reader, out io.Writer, src, dst string, options ...crane.O
 	return dstRef, nil
 }
 
-func editManifest(in io.Reader, out io.Writer, src string, dst string, options ...crane.Option) (name.Reference, error) {
+func editManifest(in io.Reader, out io.Writer, src string, dst string, mt string, options ...crane.Option) (name.Reference, error) {
 	o := crane.GetOptions(options...)
 
 	ref, err := name.ParseReference(src, o.Name...)
@@ -276,9 +277,22 @@ func editManifest(in io.Reader, out io.Writer, src string, dst string, options .
 		return nil, err
 	}
 
+	if mt == "" {
+		// If --media-type is unset, use Content-Type by default.
+		mt = string(desc.MediaType)
+
+		// If document contains mediaType, default to that.
+		wmt := withMediaType{}
+		if err := json.Unmarshal(edited, &wmt); err == nil {
+			if wmt.MediaType != "" {
+				mt = wmt.MediaType
+			}
+		}
+	}
+
 	rm := &rawManifest{
 		body:      edited,
-		mediaType: desc.MediaType,
+		mediaType: types.MediaType(mt),
 	}
 
 	if err := remote.Put(dstRef, rm, o.Remote...); err != nil {
@@ -421,6 +435,10 @@ func normalize(name string) string {
 	return filepath.Clean("/" + name)
 }
 
+type withMediaType struct {
+	MediaType string `json:"mediaType,omitempty"`
+}
+
 type rawManifest struct {
 	body      []byte
 	mediaType types.MediaType
diff --git a/pkg/registry/manifest.go b/pkg/registry/manifest.go
index 1df3f58a..cd788f7b 100644
--- a/pkg/registry/manifest.go
+++ b/pkg/registry/manifest.go
@@ -79,6 +79,16 @@ func isCatalog(req *http.Request) bool {
 	return elems[len(elems)-1] == "_catalog"
 }
 
+// Returns whether this url should be handled by the referrers handler
+func isReferrers(req *http.Request) bool {
+	elems := strings.Split(req.URL.Path, "/")
+	elems = elems[1:]
+	if len(elems) < 4 {
+		return false
+	}
+	return elems[len(elems)-2] == "referrers"
+}
+
 // https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pulling-an-image-manifest
 // https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pushing-an-image
 func (m *manifests) handle(resp http.ResponseWriter, req *http.Request) *regError {
@@ -339,3 +349,82 @@ func (m *manifests) handleCatalog(resp http.ResponseWriter, req *http.Request) *
 		Message: "We don't understand your method + url",
 	}
 }
+
+// TODO: implement handling of artifactType querystring
+func (m *manifests) handleReferrers(resp http.ResponseWriter, req *http.Request) *regError {
+	// Ensure this is a GET request
+	if req.Method != "GET" {
+		return &regError{
+			Status:  http.StatusBadRequest,
+			Code:    "METHOD_UNKNOWN",
+			Message: "We don't understand your method + url",
+		}
+	}
+
+	elem := strings.Split(req.URL.Path, "/")
+	elem = elem[1:]
+	target := elem[len(elem)-1]
+	repo := strings.Join(elem[1:len(elem)-2], "/")
+
+	// Validate that incoming target is a valid digest
+	if _, err := v1.NewHash(target); err != nil {
+		return &regError{
+			Status:  http.StatusBadRequest,
+			Code:    "UNSUPPORTED",
+			Message: "Target must be a valid digest",
+		}
+	}
+
+	m.lock.Lock()
+	defer m.lock.Unlock()
+
+	digestToManifestMap, repoExists := m.manifests[repo]
+	if !repoExists {
+		return &regError{
+			Status:  http.StatusNotFound,
+			Code:    "NAME_UNKNOWN",
+			Message: "Unknown name",
+		}
+	}
+
+	im := v1.IndexManifest{
+		SchemaVersion: 2,
+		MediaType:     types.OCIImageIndex,
+		Manifests:     []v1.Descriptor{},
+	}
+	for digest, manifest := range digestToManifestMap {
+		h, err := v1.NewHash(digest)
+		if err != nil {
+			continue
+		}
+		var refPointer struct {
+			Subject *v1.Descriptor `json:"subject"`
+		}
+		json.Unmarshal(manifest.blob, &refPointer)
+		if refPointer.Subject == nil {
+			continue
+		}
+		referenceDigest := refPointer.Subject.Digest
+		if referenceDigest.String() != target {
+			continue
+		}
+		// At this point, we know the current digest references the target
+		var imageAsArtifact struct {
+			Config struct {
+				MediaType string `json:"mediaType"`
+			} `json:"config"`
+		}
+		json.Unmarshal(manifest.blob, &imageAsArtifact)
+		im.Manifests = append(im.Manifests, v1.Descriptor{
+			MediaType:    types.MediaType(manifest.contentType),
+			Size:         int64(len(manifest.blob)),
+			Digest:       h,
+			ArtifactType: imageAsArtifact.Config.MediaType,
+		})
+	}
+	msg, _ := json.Marshal(&im)
+	resp.Header().Set("Content-Length", fmt.Sprint(len(msg)))
+	resp.WriteHeader(http.StatusOK)
+	io.Copy(resp, bytes.NewReader([]byte(msg)))
+	return nil
+}
diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go
index e6129092..303e6e7e 100644
--- a/pkg/registry/registry.go
+++ b/pkg/registry/registry.go
@@ -30,9 +30,10 @@ import (
 )
 
 type registry struct {
-	log       *log.Logger
-	blobs     blobs
-	manifests manifests
+	log              *log.Logger
+	blobs            blobs
+	manifests        manifests
+	referrersEnabled bool
 }
 
 // https://docs.docker.com/registry/spec/api/#api-version-check
@@ -50,6 +51,9 @@ func (r *registry) v2(resp http.ResponseWriter, req *http.Request) *regError {
 	if isCatalog(req) {
 		return r.manifests.handleCatalog(resp, req)
 	}
+	if r.referrersEnabled && isReferrers(req) {
+		return r.manifests.handleReferrers(resp, req)
+	}
 	resp.Header().Set("Docker-Distribution-API-Version", "registry/2.0")
 	if req.URL.Path != "/v2/" && req.URL.Path != "/v2" {
 		return &regError{
@@ -104,3 +108,10 @@ func Logger(l *log.Logger) Option {
 		r.blobs.log = l
 	}
 }
+
+// WithReferrersSupport enables the referrers API endpoint (OCI 1.1+)
+func WithReferrersSupport(enabled bool) Option {
+	return func(r *registry) {
+		r.referrersEnabled = enabled
+	}
+}
diff --git a/pkg/v1/remote/descriptor.go b/pkg/v1/remote/descriptor.go
index 878d1654..78919d7a 100644
--- a/pkg/v1/remote/descriptor.go
+++ b/pkg/v1/remote/descriptor.go
@@ -245,7 +245,32 @@ func fallbackTag(d name.Digest) name.Tag {
 }
 
 func (f *fetcher) fetchReferrers(ctx context.Context, filter map[string]string, d name.Digest) (*v1.IndexManifest, error) {
-	// Assume the registry doesn't support the Referrers API endpoint, so we'll use the fallback tag scheme.
+	// Check the Referrers API endpoint first.
+	u := f.url("referrers", d.DigestStr())
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Set("Accept", string(types.OCIImageIndex))
+
+	resp, err := f.Client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	if err := transport.CheckError(resp, http.StatusOK, http.StatusNotFound, http.StatusBadRequest); err != nil {
+		return nil, err
+	}
+	if resp.StatusCode == http.StatusOK {
+		var im v1.IndexManifest
+		if err := json.NewDecoder(resp.Body).Decode(&im); err != nil {
+			return nil, err
+		}
+		return filterReferrersResponse(filter, &im), nil
+	}
+
+	// The registry doesn't support the Referrers API endpoint, so we'll use the fallback tag scheme.
 	b, _, err := f.fetchManifest(fallbackTag(d), []types.MediaType{types.OCIImageIndex})
 	if err != nil {
 		return nil, err
@@ -261,21 +286,7 @@ func (f *fetcher) fetchReferrers(ctx context.Context, filter map[string]string,
 		return nil, err
 	}
 
-	// If filter applied, filter out by artifactType and add annotation
-	// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers
-	if filter != nil {
-		if v, ok := filter["artifactType"]; ok {
-			tmp := []v1.Descriptor{}
-			for _, desc := range im.Manifests {
-				if desc.ArtifactType == v {
-					tmp = append(tmp, desc)
-				}
-			}
-			im.Manifests = tmp
-		}
-	}
-
-	return &im, nil
+	return filterReferrersResponse(filter, &im), nil
 }
 
 func (f *fetcher) fetchManifest(ref name.Reference, acceptable []types.MediaType) ([]byte, *v1.Descriptor, error) {
@@ -479,3 +490,22 @@ func (f *fetcher) blobExists(h v1.Hash) (bool, error) {
 
 	return resp.StatusCode == http.StatusOK, nil
 }
+
+// If filter applied, filter out by artifactType.
+// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers
+func filterReferrersResponse(filter map[string]string, origIndex *v1.IndexManifest) *v1.IndexManifest {
+	newIndex := origIndex
+	if filter == nil {
+		return newIndex
+	}
+	if v, ok := filter["artifactType"]; ok {
+		tmp := []v1.Descriptor{}
+		for _, desc := range newIndex.Manifests {
+			if desc.ArtifactType == v {
+				tmp = append(tmp, desc)
+			}
+		}
+		newIndex.Manifests = tmp
+	}
+	return newIndex
+}
diff --git a/pkg/v1/remote/write.go b/pkg/v1/remote/write.go
index d8d86777..5dbaa7c2 100644
--- a/pkg/v1/remote/write.go
+++ b/pkg/v1/remote/write.go
@@ -580,19 +580,40 @@ func unpackTaggable(t Taggable) ([]byte, *v1.Descriptor, error) {
 }
 
 // commitSubjectReferrers is responsible for updating the fallback tag manifest to track descriptors referring to a subject for registries that don't yet support the Referrers API.
-// TODO: this method only uses the fallback tag for now
 // TODO: use conditional requests to avoid race conditions
 func (w *writer) commitSubjectReferrers(ctx context.Context, sub name.Digest, add v1.Descriptor) error {
+	// Check if the registry supports Referrers API.
+	// TODO: This should be done once per registry, not once per subject.
+	u := w.url(fmt.Sprintf("/v2/%s/referrers/%s", w.repo.RepositoryStr(), sub.DigestStr()))
+	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
+	if err != nil {
+		return err
+	}
+	req.Header.Set("Accept", string(types.OCIImageIndex))
+	resp, err := w.client.Do(req.WithContext(ctx))
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+
+	if err := transport.CheckError(resp, http.StatusOK, http.StatusNotFound, http.StatusBadRequest); err != nil {
+		return err
+	}
+	if resp.StatusCode == http.StatusOK {
+		// The registry supports Referrers API. The registry is responsible for updating the referrers list.
+		return nil
+	}
+
 	// The registry doesn't support Referrers API, we need to update the manifest tagged with the fallback tag.
 	// Make the request to GET the current manifest.
 	t := fallbackTag(sub)
-	u := w.url(fmt.Sprintf("/v2/%s/manifests/%s", w.repo.RepositoryStr(), t.Identifier()))
-	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
+	u = w.url(fmt.Sprintf("/v2/%s/manifests/%s", w.repo.RepositoryStr(), t.Identifier()))
+	req, err = http.NewRequest(http.MethodGet, u.String(), nil)
 	if err != nil {
 		return err
 	}
 	req.Header.Set("Accept", string(types.OCIImageIndex))
-	resp, err := w.client.Do(req.WithContext(ctx))
+	resp, err = w.client.Do(req.WithContext(ctx))
 	if err != nil {
 		return err
 	}


Test patch:
diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go
index f745c198..0ee492ed 100644
--- a/pkg/registry/registry_test.go
+++ b/pkg/registry/registry_test.go
@@ -437,6 +437,53 @@ func TestCalls(t *testing.T) {
 			URL:         "/v2/_catalog?n=1000",
 			Code:        http.StatusOK,
 		},
+		{
+			Description: "fetch references",
+			Method:      "GET",
+			URL:         "/v2/foo/referrers/sha256:" + sha256String("foo"),
+			Code:        http.StatusOK,
+			Manifests: map[string]string{
+				"foo/manifests/image":           "foo",
+				"foo/manifests/points-to-image": "{\"subject\": {\"digest\": \"sha256:" + sha256String("foo") + "\"}}",
+			},
+		},
+		{
+			Description: "fetch references, subject pointing elsewhere",
+			Method:      "GET",
+			URL:         "/v2/foo/referrers/sha256:" + sha256String("foo"),
+			Code:        http.StatusOK,
+			Manifests: map[string]string{
+				"foo/manifests/image":           "foo",
+				"foo/manifests/points-to-image": "{\"subject\": {\"digest\": \"sha256:" + sha256String("nonexistant") + "\"}}",
+			},
+		},
+		{
+			Description: "fetch references, no results",
+			Method:      "GET",
+			URL:         "/v2/foo/referrers/sha256:" + sha256String("foo"),
+			Code:        http.StatusOK,
+			Manifests: map[string]string{
+				"foo/manifests/image": "foo",
+			},
+		},
+		{
+			Description: "fetch references, missing repo",
+			Method:      "GET",
+			URL:         "/v2/does-not-exist/referrers/sha256:" + sha256String("foo"),
+			Code:        http.StatusNotFound,
+		},
+		{
+			Description: "fetch references, bad target (tag vs. digest)",
+			Method:      "GET",
+			URL:         "/v2/foo/referrers/latest",
+			Code:        http.StatusBadRequest,
+		},
+		{
+			Description: "fetch references, bad method",
+			Method:      "POST",
+			URL:         "/v2/foo/referrers/sha256:" + sha256String("foo"),
+			Code:        http.StatusBadRequest,
+		},
 	}
 
 	for _, tc := range tcs {
@@ -444,10 +491,11 @@ func TestCalls(t *testing.T) {
 		var logger *log.Logger
 		testf := func(t *testing.T) {
 
-			r := registry.New()
+			opts := []registry.Option{registry.WithReferrersSupport(true)}
 			if logger != nil {
-				r = registry.New(registry.Logger(logger))
+				opts = append(opts, registry.Logger(logger))
 			}
+			r := registry.New(opts...)
 			s := httptest.NewServer(r)
 			defer s.Close()
 
diff --git a/pkg/v1/remote/referrers_test.go b/pkg/v1/remote/referrers_test.go
index 7a0969dd..91f9edcc 100644
--- a/pkg/v1/remote/referrers_test.go
+++ b/pkg/v1/remote/referrers_test.go
@@ -30,134 +30,154 @@ import (
 	"github.com/google/go-containerregistry/pkg/v1/types"
 )
 
-func TestReferrers_FallbackTag(t *testing.T) {
-	// Set up a fake registry that doesn't support the Referrers API.
-	s := httptest.NewServer(registry.New())
-	defer s.Close()
-	u, err := url.Parse(s.URL)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	descriptor := func(img v1.Image) v1.Descriptor {
-		d, err := img.Digest()
+func TestReferrers(t *testing.T) {
+	// Run all tests against:
+	//
+	//   (1) A OCI 1.0 registry (without referrers API)
+	//   (2) An OCI 1.1+ registry (with referrers API)
+	//
+	for _, leg := range []struct {
+		server      *httptest.Server
+		tryFallback bool
+	}{
+		{
+			server:      httptest.NewServer(registry.New(registry.WithReferrersSupport(false))),
+			tryFallback: true,
+		},
+		{
+			server:      httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
+			tryFallback: false,
+		},
+	} {
+		s := leg.server
+		defer s.Close()
+		u, err := url.Parse(s.URL)
 		if err != nil {
 			t.Fatal(err)
 		}
-		sz, err := img.Size()
+
+		descriptor := func(img v1.Image) v1.Descriptor {
+			d, err := img.Digest()
+			if err != nil {
+				t.Fatal(err)
+			}
+			sz, err := img.Size()
+			if err != nil {
+				t.Fatal(err)
+			}
+			mt, err := img.MediaType()
+			if err != nil {
+				t.Fatal(err)
+			}
+			return v1.Descriptor{
+				Digest:       d,
+				Size:         sz,
+				MediaType:    mt,
+				ArtifactType: "application/testing123",
+			}
+		}
+
+		// Push an image we'll attach things to.
+		// We'll copy from src to dst.
+		rootRef, err := name.ParseReference(fmt.Sprintf("%s/repo:root", u.Host))
 		if err != nil {
 			t.Fatal(err)
 		}
-		mt, err := img.MediaType()
+		rootImg, err := random.Image(10, 10)
 		if err != nil {
 			t.Fatal(err)
 		}
-		return v1.Descriptor{
-			Digest:       d,
-			Size:         sz,
-			MediaType:    mt,
-			ArtifactType: "application/testing123",
+		rootImg = mutate.ConfigMediaType(rootImg, types.MediaType("application/testing123"))
+		if err := remote.Write(rootRef, rootImg); err != nil {
+			t.Fatal(err)
 		}
-	}
+		rootDesc := descriptor(rootImg)
+		t.Logf("root image is %s", rootDesc.Digest)
 
-	// Push an image we'll attach things to.
-	// We'll copy from src to dst.
-	rootRef, err := name.ParseReference(fmt.Sprintf("%s/repo:root", u.Host))
-	if err != nil {
-		t.Fatal(err)
-	}
-	rootImg, err := random.Image(10, 10)
-	if err != nil {
-		t.Fatal(err)
-	}
-	rootImg = mutate.ConfigMediaType(rootImg, types.MediaType("application/testing123"))
-	if err := remote.Write(rootRef, rootImg); err != nil {
-		t.Fatal(err)
-	}
-	rootDesc := descriptor(rootImg)
-	t.Logf("root image is %s", rootDesc.Digest)
-
-	// Push an image that refers to the root image as its subject.
-	leafRef, err := name.ParseReference(fmt.Sprintf("%s/repo:leaf", u.Host))
-	if err != nil {
-		t.Fatal(err)
-	}
-	leafImg, err := random.Image(20, 20)
-	if err != nil {
-		t.Fatal(err)
-	}
-	leafImg = mutate.ConfigMediaType(leafImg, types.MediaType("application/testing123"))
-	leafImg = mutate.Subject(leafImg, rootDesc).(v1.Image)
-	if err := remote.Write(leafRef, leafImg); err != nil {
-		t.Fatal(err)
-	}
-	leafDesc := descriptor(leafImg)
-	t.Logf("leaf image is %s", leafDesc.Digest)
+		// Push an image that refers to the root image as its subject.
+		leafRef, err := name.ParseReference(fmt.Sprintf("%s/repo:leaf", u.Host))
+		if err != nil {
+			t.Fatal(err)
+		}
+		leafImg, err := random.Image(20, 20)
+		if err != nil {
+			t.Fatal(err)
+		}
+		leafImg = mutate.ConfigMediaType(leafImg, types.MediaType("application/testing123"))
+		leafImg = mutate.Subject(leafImg, rootDesc).(v1.Image)
+		if err := remote.Write(leafRef, leafImg); err != nil {
+			t.Fatal(err)
+		}
+		leafDesc := descriptor(leafImg)
+		t.Logf("leaf image is %s", leafDesc.Digest)
 
-	// Get the referrers of the root image, by digest.
-	rootRefDigest := rootRef.Context().Digest(rootDesc.Digest.String())
-	index, err := remote.Referrers(rootRefDigest)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if d := cmp.Diff([]v1.Descriptor{leafDesc}, index.Manifests); d != "" {
-		t.Fatalf("referrers diff (-want,+got): %s", d)
-	}
+		// Get the referrers of the root image, by digest.
+		rootRefDigest := rootRef.Context().Digest(rootDesc.Digest.String())
+		index, err := remote.Referrers(rootRefDigest)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if d := cmp.Diff([]v1.Descriptor{leafDesc}, index.Manifests); d != "" {
+			t.Fatalf("referrers diff (-want,+got): %s", d)
+		}
 
-	// Get the referrers by querying the root image's fallback tag directly.
-	tag, err := name.ParseReference(fmt.Sprintf("%s/repo:sha256-%s", u.Host, rootDesc.Digest.Hex))
-	if err != nil {
-		t.Fatal(err)
-	}
-	idx, err := remote.Index(tag)
-	if err != nil {
-		t.Fatal(err)
-	}
-	mf, err := idx.IndexManifest()
-	if err != nil {
-		t.Fatal(err)
-	}
-	if d := cmp.Diff(index.Manifests, mf.Manifests); d != "" {
-		t.Fatalf("fallback tag diff (-want,+got): %s", d)
-	}
+		if leg.tryFallback {
+			// Get the referrers by querying the root image's fallback tag directly.
+			tag, err := name.ParseReference(fmt.Sprintf("%s/repo:sha256-%s", u.Host, rootDesc.Digest.Hex))
+			if err != nil {
+				t.Fatal(err)
+			}
+			idx, err := remote.Index(tag)
+			if err != nil {
+				t.Fatal(err)
+			}
+			mf, err := idx.IndexManifest()
+			if err != nil {
+				t.Fatal(err)
+			}
+			if d := cmp.Diff(index.Manifests, mf.Manifests); d != "" {
+				t.Fatalf("fallback tag diff (-want,+got): %s", d)
+			}
+		}
 
-	// Push the leaf image again, this time with a different tag.
-	// This shouldn't add another item to the root image's referrers,
-	// because it's the same digest.
-	// Push an image that refers to the root image as its subject.
-	leaf2Ref, err := name.ParseReference(fmt.Sprintf("%s/repo:leaf2", u.Host))
-	if err != nil {
-		t.Fatal(err)
-	}
-	if err := remote.Write(leaf2Ref, leafImg); err != nil {
-		t.Fatal(err)
-	}
-	// Get the referrers of the root image again, which should only have one entry.
-	rootRefDigest = rootRef.Context().Digest(rootDesc.Digest.String())
-	index, err = remote.Referrers(rootRefDigest)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if d := cmp.Diff([]v1.Descriptor{leafDesc}, index.Manifests); d != "" {
-		t.Fatalf("referrers diff after second push (-want,+got): %s", d)
-	}
+		// Push the leaf image again, this time with a different tag.
+		// This shouldn't add another item to the root image's referrers,
+		// because it's the same digest.
+		// Push an image that refers to the root image as its subject.
+		leaf2Ref, err := name.ParseReference(fmt.Sprintf("%s/repo:leaf2", u.Host))
+		if err != nil {
+			t.Fatal(err)
+		}
+		if err := remote.Write(leaf2Ref, leafImg); err != nil {
+			t.Fatal(err)
+		}
+		// Get the referrers of the root image again, which should only have one entry.
+		rootRefDigest = rootRef.Context().Digest(rootDesc.Digest.String())
+		index, err = remote.Referrers(rootRefDigest)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if d := cmp.Diff([]v1.Descriptor{leafDesc}, index.Manifests); d != "" {
+			t.Fatalf("referrers diff after second push (-want,+got): %s", d)
+		}
 
-	// Try applying filters and verify number of manifests and and annotations
-	index, err = remote.Referrers(rootRefDigest,
-		remote.WithFilter("artifactType", "application/testing123"))
-	if err != nil {
-		t.Fatal(err)
-	}
-	if numManifests := len(index.Manifests); numManifests == 0 {
-		t.Fatal("index contained 0 manifests")
-	}
+		// Try applying filters and verify number of manifests and and annotations
+		index, err = remote.Referrers(rootRefDigest,
+			remote.WithFilter("artifactType", "application/testing123"))
+		if err != nil {
+			t.Fatal(err)
+		}
+		if numManifests := len(index.Manifests); numManifests == 0 {
+			t.Fatal("index contained 0 manifests")
+		}
 
-	index, err = remote.Referrers(rootRefDigest,
-		remote.WithFilter("artifactType", "application/testing123BADDDD"))
-	if err != nil {
-		t.Fatal(err)
-	}
-	if numManifests := len(index.Manifests); numManifests != 0 {
-		t.Fatalf("expected index to contain 0 manifests, but had %d", numManifests)
+		index, err = remote.Referrers(rootRefDigest,
+			remote.WithFilter("artifactType", "application/testing123BADDDD"))
+		if err != nil {
+			t.Fatal(err)
+		}
+		if numManifests := len(index.Manifests); numManifests != 0 {
+			t.Fatalf("expected index to contain 0 manifests, but had %d", numManifests)
+		}
 	}
 }