Repository: distribution/distribution
Pull request #4013 (2023-08-24 07:16:11)

Dont parse errors as JSON unless Content-Type is set to JSON
Client attempts to parse the body of every error it receives as `JSON` regardless of the `Content-Type`.

This PR rectifies that by only parsing the error body as `JSON` if the `Content-Type header` is set to either `application/json` or `application/vnd.api+json`.

Closes https://github.com/distribution/distribution/issues/3962

codecov-commenter: ## [Codecov](https://app.codecov.io/gh/distribution/distribution/pull/4013?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
Patch coverage: **`66.66%`** and project coverage change: **`+0.03%`** :tada:
> Comparison is base [(`4f7424c`)](https://app.codecov.io/gh/distribution/distribution/commit/4f7424c8eb41431f4ec893ba75d808acc9f4e103?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) 57.76% compared to head [(`158dc67`)](https://app.codecov.io/gh/distribution/distribution/pull/4013?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) 57.79%.

:exclamation: Your organization is not using the GitHub App Integration. As a result you may experience degraded service beginning May 15th. Please [install the Github App Integration](https://github.com/apps/codecov) for your organization. [Read more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #4013      +/-   ##
==========================================
+ Coverage   57.76%   57.79%   +0.03%     
==========================================
  Files         108      108              
  Lines       10500    10507       +7     
==========================================
+ Hits         6065     6073       +8     
+ Misses       3762     3761       -1     
  Partials      673      673              
```


| [Files Changed](https://app.codecov.io/gh/distribution/distribution/pull/4013?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [registry/client/errors.go](https://app.codecov.io/gh/distribution/distribution/pull/4013?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cmVnaXN0cnkvY2xpZW50L2Vycm9ycy5nbw==) | `61.90% <66.66%> (+4.76%)` | :arrow_up: |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/distribution/distribution/pull/4013?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).

milosgajdos: Addressed your comments, PTAL @thaJeztah 
milosgajdos: DCO is moaning now, but I've no idea why 🤨

Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index d967624c..bede5814 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -26,8 +26,8 @@ jobs:
       fail-fast: false
       matrix:
         go:
-          - 1.20.7
-          - 1.21.0
+          - 1.18
+          - 1.19.10
     steps:
       -
         name: Checkout
diff --git a/Dockerfile b/Dockerfile
index 2ec7b5ec..13c2ec29 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
 # syntax=docker/dockerfile:1
 
-ARG GO_VERSION=1.20.7
+ARG GO_VERSION=1.19.10
 ARG ALPINE_VERSION=3.18
 ARG XX_VERSION=1.2.1
 
diff --git a/MAINTAINERS b/MAINTAINERS
index 9e0a60c8..ad7464b8 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -12,7 +12,7 @@
 "joaodrp","João Pereira","jpereira@gitlab.com"
 "justincormack","Justin Cormack","justin.cormack@docker.com"
 "squizzi","Kyle Squizzato","ksquizzato@mirantis.com"
-"milosgajdos","Milos Gajdos","milosthegajdos@gmail.com"
+"milosgajdos","Milos Gajdos","milos.gajdos@docker.com"
 "sargun","Sargun Dhillon","sargun@sargun.me"
 "wy65701436","Wang Yan","wangyan@vmware.com"
 "stevelasker","Steve Lasker","steve.lasker@microsoft.com"
diff --git a/Makefile b/Makefile
index d2059afe..fd67ec2b 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,3 @@
-.DEFAULT_GOAL := help
-
 # Root directory of the project (absolute path).
 ROOTDIR=$(dir $(abspath $(lastword $(MAKEFILE_LIST))))
 
@@ -15,9 +13,6 @@ PACKAGES=$(shell go list -tags "${BUILDTAGS}" ./... | grep -v /vendor/)
 INTEGRATION_PACKAGE=${PKG}
 COVERAGE_PACKAGES=$(filter-out ${PKG}/registry/storage/driver/%,${PACKAGES})
 
-IMAGE_REPO ?= distribution/distribution
-IMAGE_TAG ?= latest
-IMAGE_NAME ?= $(IMAGE_REPO):$(IMAGE_TAG)
 
 # Project binaries.
 COMMANDS=registry digest registry-api-descriptor-template
@@ -43,49 +38,16 @@ BINARIES=$(addprefix bin/,$(COMMANDS))
 TESTFLAGS ?= -v $(TESTFLAGS_RACE)
 TESTFLAGS_PARALLEL ?= 8
 
-.PHONY: all build binaries clean test test-race test-full integration coverage validate lint validate-git validate-vendor vendor mod-outdated image
+.PHONY: all build binaries clean test test-race test-full integration coverage validate lint validate-git validate-vendor vendor mod-outdated
 .DEFAULT: all
 
-.PHONY: FORCE
-FORCE:
-
-##@ Build
+all: binaries
 
 # This only needs to be generated by hand when cutting full releases.
 version/version.go:
 	@echo "$(WHALE) $@"
 	./version/version.sh > $@
 
-bin/%: cmd/% FORCE ## build individual binary
-	@echo "$(WHALE) $@${BINARY_SUFFIX}"
-	@go build -buildmode=pie ${GO_GCFLAGS} ${GO_BUILD_FLAGS} -o $@${BINARY_SUFFIX} ${GO_LDFLAGS} --ldflags '-extldflags "-Wl,-z,now" -s' ${GO_TAGS}  ./$<
-
-binaries: $(BINARIES) ## build binaries
-	@echo "$(WHALE) $@"
-
-build: ## build go packages
-	@echo "$(WHALE) $@"
-	@go build -buildmode=pie ${GO_GCFLAGS} ${GO_BUILD_FLAGS} ${GO_LDFLAGS} --ldflags '-extldflags "-Wl,-z,now" -s' ${GO_TAGS} $(PACKAGES)
-
-image: ## build docker image IMAGE_NAME=<name>
-	docker buildx bake --set "*.tags=${IMAGE_NAME}" image-local
-
-clean: ## clean up binaries
-	@echo "$(WHALE) $@"
-	@rm -f $(BINARIES)
-
-vendor: ## update vendor
-	$(eval $@_TMP_OUT := $(shell mktemp -d -t buildx-output.XXXXXXXXXX))
-	docker buildx bake --set "*.output=$($@_TMP_OUT)" update-vendor
-	rm -rf ./vendor
-	cp -R "$($@_TMP_OUT)"/out/* .
-	rm -rf $($@_TMP_OUT)/*
-
-mod-outdated: ## check outdated dependencies
-	docker buildx bake $@
-
-##@ Test
-
 test: ## run tests, except integration test with test.short
 	@echo "$(WHALE) $@"
 	@go test ${GO_TAGS} -test.short ${TESTFLAGS} $(filter-out ${INTEGRATION_PACKAGE},${PACKAGES})
@@ -117,23 +79,42 @@ coverage: ## generate coverprofiles from the unit tests
 		fi; \
 	done )
 
-##@ Validate
+FORCE:
 
-lint: ## run all linters
-	docker buildx bake $@
+# Build a binary from a cmd.
+bin/%: cmd/% FORCE
+	@echo "$(WHALE) $@${BINARY_SUFFIX}"
+	@go build -buildmode=pie ${GO_GCFLAGS} ${GO_BUILD_FLAGS} -o $@${BINARY_SUFFIX} ${GO_LDFLAGS} --ldflags '-extldflags "-Wl,-z,now" -s' ${GO_TAGS}  ./$<
+
+binaries: $(BINARIES) ## build binaries
+	@echo "$(WHALE) $@"
+
+build:
+	@echo "$(WHALE) $@"
+	@go build -buildmode=pie ${GO_GCFLAGS} ${GO_BUILD_FLAGS} ${GO_LDFLAGS} --ldflags '-extldflags "-Wl,-z,now" -s' ${GO_TAGS} $(PACKAGES)
+
+clean: ## clean up binaries
+	@echo "$(WHALE) $@"
+	@rm -f $(BINARIES)
 
 validate: ## run all validators
 	docker buildx bake $@
 
+lint: ## run all linters
+	docker buildx bake $@
+
 validate-git: ## validate git
 	docker buildx bake $@
 
 validate-vendor: ## validate vendor
 	docker buildx bake $@
 
-.PHONY: help
-help:
-	@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n  make \033[36m\033[0m\n"} /^[a-zA-Z_\/%-]+:.*?##/ { printf "  \033[36m%-27s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
-	@echo ""
-	@echo "Go binaries:   $(BINARIES)"
-	@echo "Docker image: $(IMAGE_NAME)"
+vendor: ## update vendor
+	$(eval $@_TMP_OUT := $(shell mktemp -d -t buildx-output.XXXXXXXXXX))
+	docker buildx bake --set "*.output=$($@_TMP_OUT)" update-vendor
+	rm -rf ./vendor
+	cp -R "$($@_TMP_OUT)"/out/* .
+	rm -rf $($@_TMP_OUT)/*
+
+mod-outdated: ## check outdated dependencies
+	docker buildx bake $@
diff --git a/README.md b/README.md
index 9654b797..01e73be1 100644
--- a/README.md
+++ b/README.md
@@ -8,9 +8,7 @@
 [![codecov](https://codecov.io/gh/distribution/distribution/branch/main/graph/badge.svg)](https://codecov.io/gh/distribution/distribution)
 [![FOSSA Status](https://app.fossa.com/api/projects/custom%2B162%2Fgithub.com%2Fdistribution%2Fdistribution.svg?type=shield)](https://app.fossa.com/projects/custom%2B162%2Fgithub.com%2Fdistribution%2Fdistribution?ref=badge_shield)
 [![OCI Conformance](https://github.com/distribution/distribution/workflows/conformance/badge.svg)](https://github.com/distribution/distribution/actions?query=workflow%3Aconformance)
-[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/distribution/distribution/badge)](https://securityscorecards.dev/viewer/?uri=github.com/distribution/distribution)
-
-
+[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/distribution/distribution/badge)](https://api.securityscorecards.dev/projects/github.com/distribution/distribution)
 
 The toolset to pack, ship, store, and deliver content.
 
diff --git a/dockerfiles/git.Dockerfile b/dockerfiles/git.Dockerfile
index 09d9116b..47cab54e 100644
--- a/dockerfiles/git.Dockerfile
+++ b/dockerfiles/git.Dockerfile
@@ -1,6 +1,6 @@
 # syntax=docker/dockerfile:1
 
-ARG GO_VERSION=1.20.7
+ARG GO_VERSION=1.19.10
 ARG ALPINE_VERSION=3.18
 
 FROM alpine:${ALPINE_VERSION} AS base
diff --git a/dockerfiles/lint.Dockerfile b/dockerfiles/lint.Dockerfile
index 54656e69..f69ad37f 100644
--- a/dockerfiles/lint.Dockerfile
+++ b/dockerfiles/lint.Dockerfile
@@ -1,8 +1,8 @@
 # syntax=docker/dockerfile:1
 
-ARG GO_VERSION=1.20.7
+ARG GO_VERSION=1.19.10
 ARG ALPINE_VERSION=3.18
-ARG GOLANGCI_LINT_VERSION=v1.54.2
+ARG GOLANGCI_LINT_VERSION=v1.52
 ARG BUILDTAGS="include_gcs"
 
 FROM golangci/golangci-lint:${GOLANGCI_LINT_VERSION}-alpine AS golangci-lint
diff --git a/dockerfiles/vendor.Dockerfile b/dockerfiles/vendor.Dockerfile
index aff25160..3377e897 100644
--- a/dockerfiles/vendor.Dockerfile
+++ b/dockerfiles/vendor.Dockerfile
@@ -1,6 +1,6 @@
 # syntax=docker/dockerfile:1
 
-ARG GO_VERSION=1.20.7
+ARG GO_VERSION=1.19.10
 ARG ALPINE_VERSION=3.18
 ARG MODOUTDATED_VERSION=v0.8.0
 
diff --git a/go.mod b/go.mod
index 45092e6a..c61f2a9e 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/distribution/distribution/v3
 
-go 1.20
+go 1.18
 
 require (
 	cloud.google.com/go/storage v1.30.1
diff --git a/registry/client/errors.go b/registry/client/errors.go
index 8394ee0c..30c76926 100644
--- a/registry/client/errors.go
+++ b/registry/client/errors.go
@@ -5,6 +5,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"mime"
 	"net/http"
 
 	"github.com/distribution/distribution/v3/registry/api/errcode"
@@ -37,13 +38,29 @@ func (e *UnexpectedHTTPResponseError) Error() string {
 	return fmt.Sprintf("error parsing HTTP %d response body: %s: %q", e.StatusCode, e.ParseErr.Error(), string(e.Response))
 }
 
-func parseHTTPErrorResponse(statusCode int, r io.Reader) error {
+func parseHTTPErrorResponse(resp *http.Response) error {
 	var errors errcode.Errors
-	body, err := io.ReadAll(r)
+	body, err := io.ReadAll(resp.Body)
 	if err != nil {
 		return err
 	}
 
+	statusCode := resp.StatusCode
+	ctHeader := resp.Header.Get("Content-Type")
+
+	if ctHeader == "" {
+		return makeError(statusCode, string(body))
+	}
+
+	contentType, _, err := mime.ParseMediaType(ctHeader)
+	if err != nil {
+		return fmt.Errorf("failed parsing content-type: %w", err)
+	}
+
+	if contentType != "application/json" && contentType != "application/vnd.api+json" {
+		return makeError(statusCode, string(body))
+	}
+
 	// For backward compatibility, handle irregularly formatted
 	// messages that contain a "details" field.
 	var detailsErr struct {
@@ -51,16 +68,7 @@ func parseHTTPErrorResponse(statusCode int, r io.Reader) error {
 	}
 	err = json.Unmarshal(body, &detailsErr)
 	if err == nil && detailsErr.Details != "" {
-		switch statusCode {
-		case http.StatusUnauthorized:
-			return errcode.ErrorCodeUnauthorized.WithMessage(detailsErr.Details)
-		case http.StatusForbidden:
-			return errcode.ErrorCodeDenied.WithMessage(detailsErr.Details)
-		case http.StatusTooManyRequests:
-			return errcode.ErrorCodeTooManyRequests.WithMessage(detailsErr.Details)
-		default:
-			return errcode.ErrorCodeUnknown.WithMessage(detailsErr.Details)
-		}
+		return makeError(statusCode, detailsErr.Details)
 	}
 
 	if err := json.Unmarshal(body, &errors); err != nil {
@@ -84,6 +92,19 @@ func parseHTTPErrorResponse(statusCode int, r io.Reader) error {
 	return errors
 }
 
+func makeError(statusCode int, details string) error {
+	switch statusCode {
+	case http.StatusUnauthorized:
+		return errcode.ErrorCodeUnauthorized.WithMessage(details)
+	case http.StatusForbidden:
+		return errcode.ErrorCodeDenied.WithMessage(details)
+	case http.StatusTooManyRequests:
+		return errcode.ErrorCodeTooManyRequests.WithMessage(details)
+	default:
+		return errcode.ErrorCodeUnknown.WithMessage(details)
+	}
+}
+
 func makeErrorList(err error) []error {
 	if errL, ok := err.(errcode.Errors); ok {
 		return []error(errL)
@@ -120,11 +141,10 @@ func HandleErrorResponse(resp *http.Response) error {
 				} else {
 					err.Message = err.Code.Message()
 				}
-
-				return mergeErrors(err, parseHTTPErrorResponse(resp.StatusCode, resp.Body))
+				return mergeErrors(err, parseHTTPErrorResponse(resp))
 			}
 		}
-		err := parseHTTPErrorResponse(resp.StatusCode, resp.Body)
+		err := parseHTTPErrorResponse(resp)
 		if uErr, ok := err.(*UnexpectedHTTPResponseError); ok && resp.StatusCode == 401 {
 			return errcode.ErrorCodeUnauthorized.WithDetail(uErr.Response)
 		}
diff --git a/registry/root.go b/registry/root.go
index 51c8c7ff..49d10946 100644
--- a/registry/root.go
+++ b/registry/root.go
@@ -8,6 +8,7 @@ import (
 	"github.com/distribution/distribution/v3/registry/storage"
 	"github.com/distribution/distribution/v3/registry/storage/driver/factory"
 	"github.com/distribution/distribution/v3/version"
+	"github.com/docker/libtrust"
 	"github.com/spf13/cobra"
 )
 
@@ -66,7 +67,13 @@ var GCCmd = &cobra.Command{
 			os.Exit(1)
 		}
 
-		registry, err := storage.NewRegistry(ctx, driver)
+		k, err := libtrust.GenerateECP256PrivateKey()
+		if err != nil {
+			fmt.Fprint(os.Stderr, err)
+			os.Exit(1)
+		}
+
+		registry, err := storage.NewRegistry(ctx, driver, storage.Schema1SigningKey(k))
 		if err != nil {
 			fmt.Fprintf(os.Stderr, "failed to construct registry: %v", err)
 			os.Exit(1)
diff --git a/registry/storage/manifeststore.go b/registry/storage/manifeststore.go
index 37c86495..1d55a080 100644
--- a/registry/storage/manifeststore.go
+++ b/registry/storage/manifeststore.go
@@ -10,6 +10,7 @@ import (
 	"github.com/distribution/distribution/v3/manifest"
 	"github.com/distribution/distribution/v3/manifest/manifestlist"
 	"github.com/distribution/distribution/v3/manifest/ocischema"
+	"github.com/distribution/distribution/v3/manifest/schema1" //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
 	"github.com/distribution/distribution/v3/manifest/schema2"
 	"github.com/opencontainers/go-digest"
 	v1 "github.com/opencontainers/image-spec/specs-go/v1"
@@ -47,6 +48,7 @@ type manifestStore struct {
 
 	skipDependencyVerification bool
 
+	schema1Handler        ManifestHandler
 	schema2Handler        ManifestHandler
 	manifestListHandler   ManifestHandler
 	ocischemaHandler      ManifestHandler
@@ -94,6 +96,8 @@ func (ms *manifestStore) Get(ctx context.Context, dgst digest.Digest, options ..
 	}
 
 	switch versioned.SchemaVersion {
+	case 1:
+		return ms.schema1Handler.Unmarshal(ctx, dgst, content)
 	case 2:
 		// This can be an image manifest or a manifest list
 		switch versioned.MediaType {
@@ -129,6 +133,8 @@ func (ms *manifestStore) Put(ctx context.Context, manifest distribution.Manifest
 	dcontext.GetLogger(ms.ctx).Debug("(*manifestStore).Put")
 
 	switch manifest.(type) {
+	case *schema1.SignedManifest: //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
+		return ms.schema1Handler.Put(ctx, manifest, ms.skipDependencyVerification)
 	case *schema2.DeserializedManifest:
 		return ms.schema2Handler.Put(ctx, manifest, ms.skipDependencyVerification)
 	case *ocischema.DeserializedManifest:
diff --git a/registry/storage/registry.go b/registry/storage/registry.go
index 6c263e7c..7bf24487 100644
--- a/registry/storage/registry.go
+++ b/registry/storage/registry.go
@@ -8,6 +8,7 @@ import (
 	"github.com/distribution/distribution/v3/reference"
 	"github.com/distribution/distribution/v3/registry/storage/cache"
 	storagedriver "github.com/distribution/distribution/v3/registry/storage/driver"
+	"github.com/docker/libtrust"
 )
 
 // registry is the top-level implementation of Registry for use in the storage
@@ -18,7 +19,9 @@ type registry struct {
 	statter                      *blobStatter // global statter service.
 	blobDescriptorCacheProvider  cache.BlobDescriptorCacheProvider
 	deleteEnabled                bool
+	schema1Enabled               bool
 	resumableDigestEnabled       bool
+	schema1SigningKey            libtrust.PrivateKey
 	blobDescriptorServiceFactory distribution.BlobDescriptorServiceFactory
 	manifestURLs                 manifestURLs
 	driver                       storagedriver.StorageDriver
@@ -47,6 +50,13 @@ func EnableDelete(registry *registry) error {
 	return nil
 }
 
+// EnableSchema1 is a functional option for NewRegistry. It enables pushing of
+// schema1 manifests.
+func EnableSchema1(registry *registry) error {
+	registry.schema1Enabled = true
+	return nil
+}
+
 // DisableDigestResumption is a functional option for NewRegistry. It should be
 // used if the registry is acting as a caching proxy.
 func DisableDigestResumption(registry *registry) error {
@@ -70,6 +80,15 @@ func ManifestURLsDenyRegexp(r *regexp.Regexp) RegistryOption {
 	}
 }
 
+// Schema1SigningKey returns a functional option for NewRegistry. It sets the
+// key for signing  all schema1 manifests.
+func Schema1SigningKey(key libtrust.PrivateKey) RegistryOption {
+	return func(registry *registry) error {
+		registry.schema1SigningKey = key
+		return nil
+	}
+}
+
 // BlobDescriptorServiceFactory returns a functional option for NewRegistry. It sets the
 // factory to create BlobDescriptorServiceFactory middleware.
 func BlobDescriptorServiceFactory(factory distribution.BlobDescriptorServiceFactory) RegistryOption {
@@ -221,6 +240,25 @@ func (repo *repository) Manifests(ctx context.Context, options ...distribution.M
 		linkDirectoryPathSpec: manifestDirectoryPathSpec,
 	}
 
+	var v1Handler ManifestHandler
+	if repo.schema1Enabled {
+		v1Handler = &signedManifestHandler{
+			ctx:               ctx,
+			schema1SigningKey: repo.schema1SigningKey,
+			repository:        repo,
+			blobStore:         blobStore,
+		}
+	} else {
+		v1Handler = &v1UnsupportedHandler{
+			innerHandler: &signedManifestHandler{
+				ctx:               ctx,
+				schema1SigningKey: repo.schema1SigningKey,
+				repository:        repo,
+				blobStore:         blobStore,
+			},
+		}
+	}
+
 	manifestListHandler := &manifestListHandler{
 		ctx:        ctx,
 		repository: repo,
@@ -228,9 +266,10 @@ func (repo *repository) Manifests(ctx context.Context, options ...distribution.M
 	}
 
 	ms := &manifestStore{
-		ctx:        ctx,
-		repository: repo,
-		blobStore:  blobStore,
+		ctx:            ctx,
+		repository:     repo,
+		blobStore:      blobStore,
+		schema1Handler: v1Handler,
 		schema2Handler: &schema2ManifestHandler{
 			ctx:          ctx,
 			repository:   repo,
diff --git a/registry/storage/schema2manifesthandler.go b/registry/storage/schema2manifesthandler.go
index aa8980ec..7716289a 100644
--- a/registry/storage/schema2manifesthandler.go
+++ b/registry/storage/schema2manifesthandler.go
@@ -8,6 +8,7 @@ import (
 
 	"github.com/distribution/distribution/v3"
 	dcontext "github.com/distribution/distribution/v3/context"
+	"github.com/distribution/distribution/v3/manifest/schema1" //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
 	"github.com/distribution/distribution/v3/manifest/schema2"
 	"github.com/opencontainers/go-digest"
 )
@@ -109,7 +110,7 @@ func (ms *schema2ManifestHandler) verifyManifest(ctx context.Context, mnfst sche
 					break
 				}
 			}
-		case schema2.MediaTypeManifest:
+		case schema2.MediaTypeManifest, schema1.MediaTypeManifest: //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
 			var exists bool
 			exists, err = manifestService.Exists(ctx, descriptor.Digest)
 			if err != nil || !exists {
diff --git a/registry/storage/signedmanifesthandler.go b/registry/storage/signedmanifesthandler.go
new file mode 100644
index 00000000..edb32e07
--- /dev/null
+++ b/registry/storage/signedmanifesthandler.go
@@ -0,0 +1,142 @@
+package storage
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+
+	"github.com/distribution/distribution/v3"
+	dcontext "github.com/distribution/distribution/v3/context"
+	"github.com/distribution/distribution/v3/manifest/schema1" //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
+	"github.com/distribution/distribution/v3/reference"
+	"github.com/docker/libtrust"
+	"github.com/opencontainers/go-digest"
+)
+
+// signedManifestHandler is a ManifestHandler that covers schema1 manifests. It
+// can unmarshal and put schema1 manifests that have been signed by libtrust.
+type signedManifestHandler struct {
+	repository        distribution.Repository
+	schema1SigningKey libtrust.PrivateKey
+	blobStore         distribution.BlobStore
+	ctx               context.Context
+}
+
+var _ ManifestHandler = &signedManifestHandler{}
+
+func (ms *signedManifestHandler) Unmarshal(ctx context.Context, dgst digest.Digest, content []byte) (distribution.Manifest, error) {
+	dcontext.GetLogger(ms.ctx).Debug("(*signedManifestHandler).Unmarshal")
+
+	var (
+		signatures [][]byte
+		err        error
+	)
+
+	jsig, err := libtrust.NewJSONSignature(content, signatures...)
+	if err != nil {
+		return nil, err
+	}
+
+	if ms.schema1SigningKey != nil {
+		if err := jsig.Sign(ms.schema1SigningKey); err != nil {
+			return nil, err
+		}
+	}
+
+	// Extract the pretty JWS
+	raw, err := jsig.PrettySignature("signatures")
+	if err != nil {
+		return nil, err
+	}
+
+	var sm schema1.SignedManifest //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
+	if err := json.Unmarshal(raw, &sm); err != nil {
+		return nil, err
+	}
+	return &sm, nil
+}
+
+func (ms *signedManifestHandler) Put(ctx context.Context, manifest distribution.Manifest, skipDependencyVerification bool) (digest.Digest, error) {
+	dcontext.GetLogger(ms.ctx).Debug("(*signedManifestHandler).Put")
+
+	sm, ok := manifest.(*schema1.SignedManifest) //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
+	if !ok {
+		return "", fmt.Errorf("non-schema1 manifest put to signedManifestHandler: %T", manifest)
+	}
+
+	if err := ms.verifyManifest(ms.ctx, *sm, skipDependencyVerification); err != nil {
+		return "", err
+	}
+
+	mt := schema1.MediaTypeManifest //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
+	payload := sm.Canonical
+
+	revision, err := ms.blobStore.Put(ctx, mt, payload)
+	if err != nil {
+		dcontext.GetLogger(ctx).Errorf("error putting payload into blobstore: %v", err)
+		return "", err
+	}
+
+	return revision.Digest, nil
+}
+
+// verifyManifest ensures that the manifest content is valid from the
+// perspective of the registry. It ensures that the signature is valid for the
+// enclosed payload. As a policy, the registry only tries to store valid
+// content, leaving trust policies of that content up to consumers.
+func (ms *signedManifestHandler) verifyManifest(ctx context.Context, mnfst schema1.SignedManifest, skipDependencyVerification bool) error { //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
+	var errs distribution.ErrManifestVerification
+
+	if len(mnfst.Name) > reference.NameTotalLengthMax {
+		errs = append(errs,
+			distribution.ErrManifestNameInvalid{
+				Name:   mnfst.Name,
+				Reason: fmt.Errorf("manifest name must not be more than %v characters", reference.NameTotalLengthMax),
+			})
+	}
+
+	if !reference.NameRegexp.MatchString(mnfst.Name) {
+		errs = append(errs,
+			distribution.ErrManifestNameInvalid{
+				Name:   mnfst.Name,
+				Reason: fmt.Errorf("invalid manifest name format"),
+			})
+	}
+
+	if len(mnfst.History) != len(mnfst.FSLayers) {
+		errs = append(errs, fmt.Errorf("mismatched history and fslayer cardinality %d != %d",
+			len(mnfst.History), len(mnfst.FSLayers)))
+	}
+
+	if _, err := schema1.Verify(&mnfst); err != nil { //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
+		switch err {
+		case libtrust.ErrMissingSignatureKey, libtrust.ErrInvalidJSONContent, libtrust.ErrMissingSignatureKey:
+			errs = append(errs, distribution.ErrManifestUnverified{})
+		default:
+			if err.Error() == "invalid signature" { // TODO(stevvooe): This should be exported by libtrust
+				errs = append(errs, distribution.ErrManifestUnverified{})
+			} else {
+				errs = append(errs, err)
+			}
+		}
+	}
+
+	if !skipDependencyVerification {
+		for _, fsLayer := range mnfst.References() { //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
+			_, err := ms.repository.Blobs(ctx).Stat(ctx, fsLayer.Digest)
+			if err != nil {
+				if err != distribution.ErrBlobUnknown {
+					errs = append(errs, err)
+				}
+
+				// On error here, we always append unknown blob errors.
+				errs = append(errs, distribution.ErrManifestBlobUnknown{Digest: fsLayer.Digest})
+			}
+		}
+	}
+	if len(errs) != 0 {
+		return errs
+	}
+
+	return nil
+}
diff --git a/registry/storage/v1unsupportedhandler.go b/registry/storage/v1unsupportedhandler.go
new file mode 100644
index 00000000..d6ed95fe
--- /dev/null
+++ b/registry/storage/v1unsupportedhandler.go
@@ -0,0 +1,24 @@
+package storage
+
+import (
+	"context"
+
+	"github.com/distribution/distribution/v3"
+	digest "github.com/opencontainers/go-digest"
+)
+
+// signedManifestHandler is a ManifestHandler that unmarshals v1 manifests but
+// refuses to Put v1 manifests
+type v1UnsupportedHandler struct {
+	innerHandler ManifestHandler
+}
+
+var _ ManifestHandler = &v1UnsupportedHandler{}
+
+func (v *v1UnsupportedHandler) Unmarshal(ctx context.Context, dgst digest.Digest, content []byte) (distribution.Manifest, error) {
+	return v.innerHandler.Unmarshal(ctx, dgst, content)
+}
+
+func (v *v1UnsupportedHandler) Put(ctx context.Context, manifest distribution.Manifest, skipDependencyVerification bool) (digest.Digest, error) {
+	return digest.Digest(""), distribution.ErrSchemaV1Unsupported
+}


Test patch:
diff --git a/registry/api/v2/routes_test.go b/registry/api/v2/routes_test.go
index b2a0c830..bdd0ef48 100644
--- a/registry/api/v2/routes_test.go
+++ b/registry/api/v2/routes_test.go
@@ -9,6 +9,7 @@ import (
 	"reflect"
 	"strings"
 	"testing"
+	"time"
 
 	"github.com/gorilla/mux"
 )
@@ -220,6 +221,7 @@ func TestRouterWithBadCharacters(t *testing.T) {
 		// with random UTF8 characters not in the 128 bit ASCII range.
 		// These are not valid characters for the router and we expect
 		// 404s on every test.
+		rand.Seed(time.Now().UTC().UnixNano())
 		tests := make([]routeTestCase, 1000)
 		for idx := range tests {
 			tests[idx] = routeTestCase{
diff --git a/registry/client/blob_writer_test.go b/registry/client/blob_writer_test.go
index 0eb0b39b..d9947aae 100644
--- a/registry/client/blob_writer_test.go
+++ b/registry/client/blob_writer_test.go
@@ -86,6 +86,7 @@ func TestUploadReadFrom(t *testing.T) {
 			},
 			Response: testutil.Response{
 				StatusCode: http.StatusBadRequest,
+				Headers:    http.Header{"Content-Type": []string{"application/json; charset=utf-8"}},
 				Body: []byte(`
 					{ "errors":
 						[
@@ -107,6 +108,7 @@ func TestUploadReadFrom(t *testing.T) {
 			},
 			Response: testutil.Response{
 				StatusCode: http.StatusBadRequest,
+				Headers:    http.Header{"Content-Type": []string{"application/json"}},
 				Body:       []byte("something bad happened"),
 			},
 		},
@@ -372,6 +374,7 @@ func TestUploadWrite(t *testing.T) {
 			},
 			Response: testutil.Response{
 				StatusCode: http.StatusBadRequest,
+				Headers:    http.Header{"Content-Type": []string{"application/json; charset=utf-8"}},
 				Body: []byte(`
 					{ "errors":
 						[
@@ -393,6 +396,7 @@ func TestUploadWrite(t *testing.T) {
 			},
 			Response: testutil.Response{
 				StatusCode: http.StatusBadRequest,
+				Headers:    http.Header{"Content-Type": []string{"application/json"}},
 				Body:       []byte("something bad happened"),
 			},
 		},
diff --git a/registry/client/errors_test.go b/registry/client/errors_test.go
index 6be399d0..135608c7 100644
--- a/registry/client/errors_test.go
+++ b/registry/client/errors_test.go
@@ -15,17 +15,18 @@ type nopCloser struct {
 func (nopCloser) Close() error { return nil }
 
 func TestHandleErrorResponse401ValidBody(t *testing.T) {
-	json := "{\"errors\":[{\"code\":\"UNAUTHORIZED\",\"message\":\"action requires authentication\"}]}"
+	json := `{"errors":[{"code":"UNAUTHORIZED","message":"action requires authentication"}]}`
 	response := &http.Response{
 		Status:     "401 Unauthorized",
 		StatusCode: 401,
 		Body:       nopCloser{bytes.NewBufferString(json)},
+		Header:     http.Header{"Content-Type": []string{"application/json; charset=utf-8"}},
 	}
 	err := HandleErrorResponse(response)
 
 	expectedMsg := "unauthorized: action requires authentication"
 	if !strings.Contains(err.Error(), expectedMsg) {
-		t.Errorf("Expected \"%s\", got: \"%s\"", expectedMsg, err.Error())
+		t.Errorf("Expected %q, got: %q", expectedMsg, err.Error())
 	}
 }
 
@@ -35,27 +36,29 @@ func TestHandleErrorResponse401WithInvalidBody(t *testing.T) {
 		Status:     "401 Unauthorized",
 		StatusCode: 401,
 		Body:       nopCloser{bytes.NewBufferString(json)},
+		Header:     http.Header{"Content-Type": []string{"application/json; charset=utf-8"}},
 	}
 	err := HandleErrorResponse(response)
 
 	expectedMsg := "unauthorized: authentication required"
 	if !strings.Contains(err.Error(), expectedMsg) {
-		t.Errorf("Expected \"%s\", got: \"%s\"", expectedMsg, err.Error())
+		t.Errorf("Expected %q, got: %q", expectedMsg, err.Error())
 	}
 }
 
 func TestHandleErrorResponseExpectedStatusCode400ValidBody(t *testing.T) {
-	json := "{\"errors\":[{\"code\":\"DIGEST_INVALID\",\"message\":\"provided digest does not match\"}]}"
+	json := `{"errors":[{"code":"DIGEST_INVALID","message":"provided digest does not match"}]}`
 	response := &http.Response{
 		Status:     "400 Bad Request",
 		StatusCode: 400,
 		Body:       nopCloser{bytes.NewBufferString(json)},
+		Header:     http.Header{"Content-Type": []string{"application/json"}},
 	}
 	err := HandleErrorResponse(response)
 
 	expectedMsg := "digest invalid: provided digest does not match"
 	if !strings.Contains(err.Error(), expectedMsg) {
-		t.Errorf("Expected \"%s\", got: \"%s\"", expectedMsg, err.Error())
+		t.Errorf("Expected %q, got: %q", expectedMsg, err.Error())
 	}
 }
 
@@ -65,12 +68,13 @@ func TestHandleErrorResponseExpectedStatusCode404EmptyErrorSlice(t *testing.T) {
 		Status:     "404 Not Found",
 		StatusCode: 404,
 		Body:       nopCloser{bytes.NewBufferString(json)},
+		Header:     http.Header{"Content-Type": []string{"application/json; charset=utf-8"}},
 	}
 	err := HandleErrorResponse(response)
 
 	expectedMsg := `error parsing HTTP 404 response body: no error details found in HTTP response body: "{\"randomkey\": \"randomvalue\"}"`
 	if !strings.Contains(err.Error(), expectedMsg) {
-		t.Errorf("Expected \"%s\", got: \"%s\"", expectedMsg, err.Error())
+		t.Errorf("Expected %q, got: %q", expectedMsg, err.Error())
 	}
 }
 
@@ -80,12 +84,13 @@ func TestHandleErrorResponseExpectedStatusCode404InvalidBody(t *testing.T) {
 		Status:     "404 Not Found",
 		StatusCode: 404,
 		Body:       nopCloser{bytes.NewBufferString(json)},
+		Header:     http.Header{"Content-Type": []string{"application/json"}},
 	}
 	err := HandleErrorResponse(response)
 
 	expectedMsg := "error parsing HTTP 404 response body: invalid character 'i' looking for beginning of object key string: \"{invalid json}\""
 	if !strings.Contains(err.Error(), expectedMsg) {
-		t.Errorf("Expected \"%s\", got: \"%s\"", expectedMsg, err.Error())
+		t.Errorf("Expected %q, got: %q", expectedMsg, err.Error())
 	}
 }
 
@@ -94,12 +99,13 @@ func TestHandleErrorResponseUnexpectedStatusCode501(t *testing.T) {
 		Status:     "501 Not Implemented",
 		StatusCode: 501,
 		Body:       nopCloser{bytes.NewBufferString("{\"Error Encountered\" : \"Function not implemented.\"}")},
+		Header:     http.Header{"Content-Type": []string{"application/json"}},
 	}
 	err := HandleErrorResponse(response)
 
 	expectedMsg := "received unexpected HTTP status: 501 Not Implemented"
 	if !strings.Contains(err.Error(), expectedMsg) {
-		t.Errorf("Expected \"%s\", got: \"%s\"", expectedMsg, err.Error())
+		t.Errorf("Expected %q, got: %q", expectedMsg, err.Error())
 	}
 }
 
@@ -109,11 +115,26 @@ func TestHandleErrorResponseInsufficientPrivileges403(t *testing.T) {
 		Status:     "403 Forbidden",
 		StatusCode: 403,
 		Body:       nopCloser{bytes.NewBufferString(json)},
+		Header:     http.Header{"Content-Type": []string{"application/json"}},
 	}
 	err := HandleErrorResponse(response)
 
 	expectedMsg := "denied: requesting higher privileges than access token allows"
 	if !strings.Contains(err.Error(), expectedMsg) {
-		t.Errorf("Expected \"%s\", got: \"%s\"", expectedMsg, err.Error())
+		t.Errorf("Expected %q, got: %q", expectedMsg, err.Error())
+	}
+}
+
+func TestHandleErrorResponseNonJson(t *testing.T) {
+	msg := `{"details":"requesting higher privileges than access token allows"}`
+	response := &http.Response{
+		Status:     "403 Forbidden",
+		StatusCode: 403,
+		Body:       nopCloser{bytes.NewBufferString(msg)},
+	}
+	err := HandleErrorResponse(response)
+
+	if !strings.Contains(err.Error(), msg) {
+		t.Errorf("Expected %q, got: %q", msg, err.Error())
 	}
 }
diff --git a/registry/client/repository_test.go b/registry/client/repository_test.go
index 838c1621..56681c0f 100644
--- a/registry/client/repository_test.go
+++ b/registry/client/repository_test.go
@@ -1603,6 +1603,7 @@ func TestManifestUnauthorized(t *testing.T) {
 		},
 		Response: testutil.Response{
 			StatusCode: http.StatusUnauthorized,
+			Headers:    http.Header{"Content-Type": []string{"application/json; charset=utf-8"}},
 			Body:       []byte("<html>garbage</html>"),
 		},
 	})
diff --git a/registry/proxy/proxyblobstore_test.go b/registry/proxy/proxyblobstore_test.go
index d90d2384..1531ba38 100644
--- a/registry/proxy/proxyblobstore_test.go
+++ b/registry/proxy/proxyblobstore_test.go
@@ -21,7 +21,6 @@ import (
 )
 
 var sbsMu sync.Mutex
-var randSource rand.Rand
 
 type statsBlobStore struct {
 	stats map[string]int
@@ -190,13 +189,13 @@ func makeTestEnv(t *testing.T, name string) *testEnv {
 func makeBlob(size int) []byte {
 	blob := make([]byte, size)
 	for i := 0; i < size; i++ {
-		blob[i] = byte('A' + randSource.Int()%48)
+		blob[i] = byte('A' + rand.Int()%48)
 	}
 	return blob
 }
 
 func init() {
-	randSource = *rand.New(rand.NewSource(42))
+	rand.Seed(42)
 }
 
 func populate(t *testing.T, te *testEnv, blobCount, size, numUnique int) {
diff --git a/registry/storage/catalog_test.go b/registry/storage/catalog_test.go
index 0e3e0361..14ba4e83 100644
--- a/registry/storage/catalog_test.go
+++ b/registry/storage/catalog_test.go
@@ -26,7 +26,7 @@ type setupEnv struct {
 func setupFS(t *testing.T) *setupEnv {
 	d := inmemory.New()
 	ctx := context.Background()
-	registry, err := NewRegistry(ctx, d, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider(memory.UnlimitedSize)), EnableRedirect)
+	registry, err := NewRegistry(ctx, d, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider(memory.UnlimitedSize)), EnableRedirect, EnableSchema1)
 	if err != nil {
 		t.Fatalf("error creating registry: %v", err)
 	}
@@ -81,18 +81,19 @@ func makeRepo(ctx context.Context, t *testing.T, name string, reg distribution.N
 		t.Fatal(err)
 	}
 
-	// upload the layers
 	err = testutil.UploadBlobs(repo, layers)
 	if err != nil {
 		t.Fatalf("failed to upload layers: %v", err)
 	}
 
-	digests := []digest.Digest{}
-	for digest := range layers {
-		digests = append(digests, digest)
+	getKeys := func(digests map[digest.Digest]io.ReadSeeker) (ds []digest.Digest) {
+		for d := range digests {
+			ds = append(ds, d)
+		}
+		return
 	}
 
-	manifest, err := testutil.MakeSchema2Manifest(repo, digests)
+	manifest, err := testutil.MakeSchema1Manifest(getKeys(layers)) //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -205,7 +206,7 @@ func testEq(a, b []string, size int) bool {
 func setupBadWalkEnv(t *testing.T) *setupEnv {
 	d := newBadListDriver()
 	ctx := context.Background()
-	registry, err := NewRegistry(ctx, d, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider(memory.UnlimitedSize)), EnableRedirect)
+	registry, err := NewRegistry(ctx, d, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider(memory.UnlimitedSize)), EnableRedirect, EnableSchema1)
 	if err != nil {
 		t.Fatalf("error creating registry: %v", err)
 	}
diff --git a/registry/storage/driver/s3-aws/s3_test.go b/registry/storage/driver/s3-aws/s3_test.go
index 060ed2d8..0330eafc 100644
--- a/registry/storage/driver/s3-aws/s3_test.go
+++ b/registry/storage/driver/s3-aws/s3_test.go
@@ -2,9 +2,9 @@ package s3
 
 import (
 	"bytes"
-	"crypto/rand"
 	"errors"
 	"fmt"
+	"math/rand"
 	"os"
 	"path"
 	"reflect"
diff --git a/registry/storage/driver/testsuites/testsuites.go b/registry/storage/driver/testsuites/testsuites.go
index 2ee46f5d..8e54be32 100644
--- a/registry/storage/driver/testsuites/testsuites.go
+++ b/registry/storage/driver/testsuites/testsuites.go
@@ -3,7 +3,6 @@ package testsuites
 import (
 	"bytes"
 	"context"
-	crand "crypto/rand"
 	"crypto/sha256"
 	"io"
 	"math/rand"
@@ -1233,7 +1232,7 @@ func randomFilename(length int64) string {
 var randomBytes = make([]byte, 128<<20)
 
 func init() {
-	_, _ = crand.Read(randomBytes) // always returns len(randomBytes) and nil error
+	_, _ = rand.Read(randomBytes) // always returns len(randomBytes) and nil error
 }
 
 func randomContents(length int64) []byte {
diff --git a/registry/storage/filereader_test.go b/registry/storage/filereader_test.go
index 3f05582e..51c88c36 100644
--- a/registry/storage/filereader_test.go
+++ b/registry/storage/filereader_test.go
@@ -2,7 +2,6 @@ package storage
 
 import (
 	"bytes"
-	crand "crypto/rand"
 	"io"
 	mrand "math/rand"
 	"testing"
@@ -15,7 +14,7 @@ import (
 func TestSimpleRead(t *testing.T) {
 	ctx := context.Background()
 	content := make([]byte, 1<<20)
-	n, err := crand.Read(content)
+	n, err := mrand.Read(content)
 	if err != nil {
 		t.Fatalf("unexpected error building random data: %v", err)
 	}
diff --git a/registry/storage/garbagecollect_test.go b/registry/storage/garbagecollect_test.go
index 695dbe9d..bdbf9ab2 100644
--- a/registry/storage/garbagecollect_test.go
+++ b/registry/storage/garbagecollect_test.go
@@ -11,6 +11,7 @@ import (
 	"github.com/distribution/distribution/v3/registry/storage/driver"
 	"github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
 	"github.com/distribution/distribution/v3/testutil"
+	"github.com/docker/libtrust"
 	"github.com/opencontainers/go-digest"
 )
 
@@ -22,7 +23,11 @@ type image struct {
 
 func createRegistry(t *testing.T, driver driver.StorageDriver, options ...RegistryOption) distribution.Namespace {
 	ctx := context.Background()
-	options = append(options, EnableDelete)
+	k, err := libtrust.GenerateECP256PrivateKey()
+	if err != nil {
+		t.Fatal(err)
+	}
+	options = append([]RegistryOption{EnableDelete, Schema1SigningKey(k), EnableSchema1}, options...)
 	registry, err := NewRegistry(ctx, driver, options...)
 	if err != nil {
 		t.Fatalf("Failed to construct namespace")
@@ -105,6 +110,30 @@ func uploadImage(t *testing.T, repository distribution.Repository, im image) dig
 	return manifestDigest
 }
 
+func uploadRandomSchema1Image(t *testing.T, repository distribution.Repository) image {
+	randomLayers, err := testutil.CreateRandomLayers(2)
+	if err != nil {
+		t.Fatalf("%v", err)
+	}
+
+	digests := []digest.Digest{}
+	for digest := range randomLayers {
+		digests = append(digests, digest)
+	}
+
+	manifest, err := testutil.MakeSchema1Manifest(digests) //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
+	if err != nil {
+		t.Fatalf("%v", err)
+	}
+
+	manifestDigest := uploadImage(t, repository, image{manifest: manifest, layers: randomLayers})
+	return image{
+		manifest:       manifest,
+		manifestDigest: manifestDigest,
+		layers:         randomLayers,
+	}
+}
+
 func uploadRandomSchema2Image(t *testing.T, repository distribution.Repository) image {
 	randomLayers, err := testutil.CreateRandomLayers(2)
 	if err != nil {
@@ -137,8 +166,8 @@ func TestNoDeletionNoEffect(t *testing.T) {
 	repo := makeRepository(t, registry, "palailogos")
 	manifestService, _ := repo.Manifests(ctx)
 
-	image1 := uploadRandomSchema2Image(t, repo)
-	image2 := uploadRandomSchema2Image(t, repo)
+	image1 := uploadRandomSchema1Image(t, repo)
+	image2 := uploadRandomSchema1Image(t, repo)
 	uploadRandomSchema2Image(t, repo)
 
 	// construct manifestlist for fun.
@@ -203,12 +232,12 @@ func TestDeleteManifestIfTagNotFound(t *testing.T) {
 	}
 
 	// Construct manifests
-	manifest1, err := testutil.MakeSchema2Manifest(repo, getKeys(randomLayers1))
+	manifest1, err := testutil.MakeSchema1Manifest(getKeys(randomLayers1)) //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
 	if err != nil {
 		t.Fatalf("failed to make manifest: %v", err)
 	}
 
-	manifest2, err := testutil.MakeSchema2Manifest(repo, getKeys(randomLayers2))
+	manifest2, err := testutil.MakeSchema1Manifest(getKeys(randomLayers2)) //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
 	if err != nil {
 		t.Fatalf("failed to make manifest: %v", err)
 	}
@@ -274,7 +303,7 @@ func TestGCWithMissingManifests(t *testing.T) {
 
 	registry := createRegistry(t, d)
 	repo := makeRepository(t, registry, "testrepo")
-	uploadRandomSchema2Image(t, repo)
+	uploadRandomSchema1Image(t, repo)
 
 	// Simulate a missing _manifests directory
 	revPath, err := pathFor(manifestRevisionsPathSpec{"testrepo"})
@@ -310,8 +339,8 @@ func TestDeletionHasEffect(t *testing.T) {
 	repo := makeRepository(t, registry, "komnenos")
 	manifests, _ := repo.Manifests(ctx)
 
-	image1 := uploadRandomSchema2Image(t, repo)
-	image2 := uploadRandomSchema2Image(t, repo)
+	image1 := uploadRandomSchema1Image(t, repo)
+	image2 := uploadRandomSchema1Image(t, repo)
 	image3 := uploadRandomSchema2Image(t, repo)
 
 	manifests.Delete(ctx, image2.manifestDigest)
@@ -397,7 +426,7 @@ func TestDeletionWithSharedLayer(t *testing.T) {
 	}
 
 	// Construct manifests
-	manifest1, err := testutil.MakeSchema2Manifest(repo, getKeys(randomLayers1))
+	manifest1, err := testutil.MakeSchema1Manifest(getKeys(randomLayers1)) //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
 	if err != nil {
 		t.Fatalf("failed to make manifest: %v", err)
 	}
diff --git a/registry/storage/manifeststore_test.go b/registry/storage/manifeststore_test.go
index 3285d541..d40315a9 100644
--- a/registry/storage/manifeststore_test.go
+++ b/registry/storage/manifeststore_test.go
@@ -11,12 +11,13 @@ import (
 	"github.com/distribution/distribution/v3"
 	"github.com/distribution/distribution/v3/manifest"
 	"github.com/distribution/distribution/v3/manifest/ocischema"
-	"github.com/distribution/distribution/v3/manifest/schema2"
+	"github.com/distribution/distribution/v3/manifest/schema1" //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
 	"github.com/distribution/distribution/v3/reference"
 	"github.com/distribution/distribution/v3/registry/storage/cache/memory"
 	"github.com/distribution/distribution/v3/registry/storage/driver"
 	"github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
 	"github.com/distribution/distribution/v3/testutil"
+	"github.com/docker/libtrust"
 	"github.com/opencontainers/go-digest"
 	v1 "github.com/opencontainers/image-spec/specs-go/v1"
 )
@@ -54,55 +55,36 @@ func newManifestStoreTestEnv(t *testing.T, name reference.Named, tag string, opt
 }
 
 func TestManifestStorage(t *testing.T) {
-	testManifestStorage(t, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider(memory.UnlimitedSize)), EnableDelete, EnableRedirect)
+	k, err := libtrust.GenerateECP256PrivateKey()
+	if err != nil {
+		t.Fatal(err)
+	}
+	testManifestStorage(t, true, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider(memory.UnlimitedSize)), EnableDelete, EnableRedirect, Schema1SigningKey(k), EnableSchema1)
 }
 
-func testManifestStorage(t *testing.T, options ...RegistryOption) {
-	repoName, _ := reference.WithName("foo/bar")
-	env := newManifestStoreTestEnv(t, repoName, "thetag", options...)
-	ctx := context.Background()
-	ms, err := env.repository.Manifests(ctx)
+func TestManifestStorageV1Unsupported(t *testing.T) {
+	k, err := libtrust.GenerateECP256PrivateKey()
 	if err != nil {
 		t.Fatal(err)
 	}
+	testManifestStorage(t, false, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider(memory.UnlimitedSize)), EnableDelete, EnableRedirect, Schema1SigningKey(k))
+}
 
-	// Push a config, and reference it in the manifest
-	sampleConfig := []byte(`{
-		"architecture": "amd64",
-		"history": [
-		  {
-		    "created": "2015-10-31T22:22:54.690851953Z",
-		    "created_by": "/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /"
-		  },
-		],
-		"rootfs": {
-		  "diff_ids": [
-		    "sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1",
-		  ],
-		  "type": "layers"
-		}
-	}`)
-
-	// Build a manifest and store it and its layers in the registry
-
-	blobStore := env.repository.Blobs(ctx)
-	d, err := blobStore.Put(ctx, schema2.MediaTypeImageConfig, sampleConfig)
+func testManifestStorage(t *testing.T, schema1Enabled bool, options ...RegistryOption) {
+	repoName, _ := reference.WithName("foo/bar")
+	env := newManifestStoreTestEnv(t, repoName, "thetag", options...)
+	ctx := context.Background()
+	ms, err := env.repository.Manifests(ctx)
 	if err != nil {
 		t.Fatal(err)
 	}
-	builder := schema2.NewManifestBuilder(d, sampleConfig)
 
-	m := &schema2.Manifest{
+	m := schema1.Manifest{ //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
 		Versioned: manifest.Versioned{
-			SchemaVersion: 2,
-			MediaType:     schema2.MediaTypeManifest,
-		},
-		Config: distribution.Descriptor{
-			Digest:    digest.FromBytes(sampleConfig),
-			Size:      int64(len(sampleConfig)),
-			MediaType: schema2.MediaTypeImageConfig,
+			SchemaVersion: 1,
 		},
-		Layers: []distribution.Descriptor{},
+		Name: env.name.Name(),
+		Tag:  env.tag,
 	}
 
 	// Build up some test layers and add them to the manifest, saving the
@@ -115,12 +97,52 @@ func testManifestStorage(t *testing.T, options ...RegistryOption) {
 		}
 
 		testLayers[dgst] = rs
-		layer := distribution.Descriptor{
-			Digest:    dgst,
-			Size:      6323,
-			MediaType: schema2.MediaTypeLayer,
+		m.FSLayers = append(m.FSLayers, schema1.FSLayer{ //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
+			BlobSum: dgst,
+		})
+		m.History = append(m.History, schema1.History{ //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
+			V1Compatibility: "",
+		})
+
+	}
+
+	pk, err := libtrust.GenerateECP256PrivateKey()
+	if err != nil {
+		t.Fatalf("unexpected error generating private key: %v", err)
+	}
+
+	sm, merr := schema1.Sign(&m, pk) //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
+	if merr != nil {
+		t.Fatalf("error signing manifest: %v", err)
+	}
+
+	_, err = ms.Put(ctx, sm)
+	if err == nil {
+		t.Fatalf("expected errors putting manifest with full verification")
+	}
+
+	// If schema1 is not enabled, do a short version of this test, just checking
+	// if we get the right error when we Put
+	if !schema1Enabled {
+		if err != distribution.ErrSchemaV1Unsupported {
+			t.Fatalf("got the wrong error when schema1 is disabled: %s", err)
 		}
-		m.Layers = append(m.Layers, layer)
+		return
+	}
+
+	switch err := err.(type) {
+	case distribution.ErrManifestVerification:
+		if len(err) != 2 {
+			t.Fatalf("expected 2 verification errors: %#v", err)
+		}
+
+		for _, err := range err {
+			if _, ok := err.(distribution.ErrManifestBlobUnknown); !ok {
+				t.Fatalf("unexpected error type: %v", err)
+			}
+		}
+	default:
+		t.Fatalf("unexpected error verifying manifest: %v", err)
 	}
 
 	// Now, upload the layers that were missing!
@@ -137,12 +159,6 @@ func testManifestStorage(t *testing.T, options ...RegistryOption) {
 		if _, err := wr.Commit(env.ctx, distribution.Descriptor{Digest: dgst}); err != nil {
 			t.Fatalf("unexpected error finishing upload: %v", err)
 		}
-		builder.AppendReference(distribution.Descriptor{Digest: dgst})
-	}
-
-	sm, err := builder.Build(ctx)
-	if err != nil {
-		t.Fatalf("%s: unexpected error generating manifest: %v", repoName, err)
 	}
 
 	var manifestDigest digest.Digest
@@ -164,19 +180,34 @@ func testManifestStorage(t *testing.T, options ...RegistryOption) {
 		t.Fatalf("unexpected error fetching manifest: %v", err)
 	}
 
-	fetchedManifest, ok := fromStore.(*schema2.DeserializedManifest)
+	fetchedManifest, ok := fromStore.(*schema1.SignedManifest) //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
 	if !ok {
 		t.Fatalf("unexpected manifest type from signedstore")
 	}
-	_, pl, err := fetchedManifest.Payload()
+
+	if !bytes.Equal(fetchedManifest.Canonical, sm.Canonical) {
+		t.Fatalf("fetched payload does not match original payload: %q != %q", fetchedManifest.Canonical, sm.Canonical)
+	}
+
+	_, pl, err := fetchedManifest.Payload() //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
+	if err != nil {
+		t.Fatalf("error getting payload %#v", err)
+	}
+
+	fetchedJWS, err := libtrust.ParsePrettySignature(pl, "signatures")
+	if err != nil {
+		t.Fatalf("unexpected error parsing jws: %v", err)
+	}
+
+	payload, err := fetchedJWS.Payload()
 	if err != nil {
-		t.Fatalf("could not get manifest payload: %v", err)
+		t.Fatalf("unexpected error extracting payload: %v", err)
 	}
 
 	// Now that we have a payload, take a moment to check that the manifest is
 	// return by the payload digest.
 
-	dgst := digest.FromBytes(pl)
+	dgst := digest.FromBytes(payload)
 	exists, err = ms.Exists(ctx, dgst)
 	if err != nil {
 		t.Fatalf("error checking manifest existence by digest: %v", err)
@@ -191,18 +222,55 @@ func testManifestStorage(t *testing.T, options ...RegistryOption) {
 		t.Fatalf("unexpected error fetching manifest by digest: %v", err)
 	}
 
-	byDigestManifest, ok := fetchedByDigest.(*schema2.DeserializedManifest)
+	byDigestManifest, ok := fetchedByDigest.(*schema1.SignedManifest) //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
 	if !ok {
 		t.Fatalf("unexpected manifest type from signedstore")
 	}
 
-	_, byDigestCanonical, err := byDigestManifest.Payload()
+	if !bytes.Equal(byDigestManifest.Canonical, fetchedManifest.Canonical) {
+		t.Fatalf("fetched manifest not equal: %q != %q", byDigestManifest.Canonical, fetchedManifest.Canonical)
+	}
+
+	sigs, err := fetchedJWS.Signatures()
+	if err != nil {
+		t.Fatalf("unable to extract signatures: %v", err)
+	}
+
+	if len(sigs) != 1 {
+		t.Fatalf("unexpected number of signatures: %d != %d", len(sigs), 1)
+	}
+
+	// Now, push the same manifest with a different key
+	pk2, err := libtrust.GenerateECP256PrivateKey()
+	if err != nil {
+		t.Fatalf("unexpected error generating private key: %v", err)
+	}
+
+	sm2, err := schema1.Sign(&m, pk2) //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
 	if err != nil {
-		t.Fatalf("could not get manifest payload: %v", err)
+		t.Fatalf("unexpected error signing manifest: %v", err)
+	}
+	_, pl, err = sm2.Payload() //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
+	if err != nil {
+		t.Fatalf("error getting payload %#v", err)
+	}
+
+	jws2, err := libtrust.ParsePrettySignature(pl, "signatures")
+	if err != nil {
+		t.Fatalf("error parsing signature: %v", err)
+	}
+
+	sigs2, err := jws2.Signatures()
+	if err != nil {
+		t.Fatalf("unable to extract signatures: %v", err)
+	}
+
+	if len(sigs2) != 1 {
+		t.Fatalf("unexpected number of signatures: %d != %d", len(sigs2), 1)
 	}
 
-	if !bytes.Equal(byDigestCanonical, pl) {
-		t.Fatalf("fetched manifest not equal: %q != %q", byDigestCanonical, pl)
+	if manifestDigest, err = ms.Put(ctx, sm2); err != nil {
+		t.Fatalf("unexpected error putting manifest: %v", err)
 	}
 
 	fromStore, err = ms.Get(ctx, manifestDigest)
@@ -210,17 +278,31 @@ func testManifestStorage(t *testing.T, options ...RegistryOption) {
 		t.Fatalf("unexpected error fetching manifest: %v", err)
 	}
 
-	fetched, ok := fromStore.(*schema2.DeserializedManifest)
+	fetched, ok := fromStore.(*schema1.SignedManifest) //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
 	if !ok {
 		t.Fatalf("unexpected type from signed manifeststore : %T", fetched)
 	}
 
-	_, receivedPL, err := fetched.Payload()
+	if _, err := schema1.Verify(fetched); err != nil { //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
+		t.Fatalf("unexpected error verifying manifest: %v", err)
+	}
+
+	_, pl, err = fetched.Payload() //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
 	if err != nil {
 		t.Fatalf("error getting payload %#v", err)
 	}
 
-	if !bytes.Equal(receivedPL, pl) {
+	receivedJWS, err := libtrust.ParsePrettySignature(pl, "signatures")
+	if err != nil {
+		t.Fatalf("unexpected error parsing jws: %v", err)
+	}
+
+	receivedPayload, err := receivedJWS.Payload()
+	if err != nil {
+		t.Fatalf("unexpected error extracting received payload: %v", err)
+	}
+
+	if !bytes.Equal(receivedPayload, payload) {
 		t.Fatalf("payloads are not equal")
 	}
 
diff --git a/testutil/manifests.go b/testutil/manifests.go
index 9501aace..3a6a333b 100644
--- a/testutil/manifests.go
+++ b/testutil/manifests.go
@@ -5,8 +5,11 @@ import (
 
 	"github.com/distribution/distribution/v3"
 	"github.com/distribution/distribution/v3/context"
+	"github.com/distribution/distribution/v3/manifest"
 	"github.com/distribution/distribution/v3/manifest/manifestlist"
+	"github.com/distribution/distribution/v3/manifest/schema1" //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility.
 	"github.com/distribution/distribution/v3/manifest/schema2"
+	"github.com/docker/libtrust"
 	"github.com/opencontainers/go-digest"
 )
 
@@ -36,6 +39,38 @@ func MakeManifestList(blobstatter distribution.BlobStatter, manifestDigests []di
 	return manifestlist.FromDescriptors(manifestDescriptors)
 }
 
+// MakeSchema1Manifest constructs a schema 1 manifest from a given list of digests and returns
+// the digest of the manifest.
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
+func MakeSchema1Manifest(digests []digest.Digest) (*schema1.SignedManifest, error) {
+	mfst := schema1.Manifest{
+		Versioned: manifest.Versioned{
+			SchemaVersion: 1,
+		},
+		Name: "who",
+		Tag:  "cares",
+	}
+
+	for _, d := range digests {
+		mfst.FSLayers = append(mfst.FSLayers, schema1.FSLayer{BlobSum: d})
+		mfst.History = append(mfst.History, schema1.History{V1Compatibility: ""})
+	}
+
+	pk, err := libtrust.GenerateECP256PrivateKey()
+	if err != nil {
+		return nil, fmt.Errorf("unexpected error generating private key: %v", err)
+	}
+
+	signedManifest, err := schema1.Sign(&mfst, pk)
+	if err != nil {
+		return nil, fmt.Errorf("error signing manifest: %v", err)
+	}
+
+	return signedManifest, nil
+}
+
 // MakeSchema2Manifest constructs a schema 2 manifest from a given list of digests and returns
 // the digest of the manifest
 func MakeSchema2Manifest(repository distribution.Repository, digests []digest.Digest) (distribution.Manifest, error) {
diff --git a/testutil/tarfile.go b/testutil/tarfile.go
index 2b30a24d..e0782b73 100644
--- a/testutil/tarfile.go
+++ b/testutil/tarfile.go
@@ -3,7 +3,6 @@ package testutil
 import (
 	"archive/tar"
 	"bytes"
-	crand "crypto/rand"
 	"fmt"
 	"io"
 	mrand "math/rand"
@@ -46,7 +45,7 @@ func CreateRandomTarFile() (rs io.ReadSeeker, dgst digest.Digest, err error) {
 		randomData := make([]byte, fileSize)
 
 		// Fill up the buffer with some random data.
-		n, err := crand.Read(randomData)
+		n, err := mrand.Read(randomData)
 
 		if n != len(randomData) {
 			return nil, "", fmt.Errorf("short read creating random reader: %v bytes != %v bytes", n, len(randomData))