Repository: distribution/distribution
Pull request #4028 (2023-08-29 14:39:01)

remove duplicated code
Lines 138-140 are the same as 134-136


Patch:
diff --git a/configuration/configuration.go b/configuration/configuration.go
index e4d4311d..7d3fac0b 100644
--- a/configuration/configuration.go
+++ b/configuration/configuration.go
@@ -174,6 +174,30 @@ type Configuration struct {
 
 	Proxy Proxy `yaml:"proxy,omitempty"`
 
+	// Compatibility is used for configurations of working with older or deprecated features.
+	Compatibility struct {
+		// Schema1 configures how schema1 manifests will be handled.
+		//
+		// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since
+		// 2015. These options should only be used if you need to provide
+		// backward compatibility.
+		Schema1 struct {
+			// TrustKey is the signing key to use for adding the signature to
+			// schema1 manifests.
+			//
+			// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since
+			// 2015. These options should only be used if you need to provide
+			// backward compatibility.
+			TrustKey string `yaml:"signingkeyfile,omitempty"`
+			// Enabled determines if schema1 manifests should be pullable.
+			//
+			// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since
+			// 2015. These options should only be used if you need to provide
+			// backward compatibility.
+			Enabled bool `yaml:"enabled,omitempty"`
+		} `yaml:"schema1,omitempty"`
+	} `yaml:"compatibility,omitempty"`
+
 	// Validation configures validation options for the registry.
 	Validation struct {
 		// Enabled enables the other options in this section. This field is
diff --git a/docs/configuration.md b/docs/configuration.md
index 90eeaa7c..bc900d73 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -276,6 +276,10 @@ proxy:
   username: [username]
   password: [password]
   ttl: 168h
+compatibility:
+  schema1:
+    signingkeyfile: /etc/registry/key.json
+    enabled: true
 validation:
   manifests:
     urls:
@@ -1108,6 +1112,25 @@ username (such as `batman`) and the password for that username.
 > **Note**: These private repositories are stored in the proxy cache's storage.
 > Take appropriate measures to protect access to the proxy cache.
 
+## `compatibility`
+
+```none
+compatibility:
+  schema1:
+    signingkeyfile: /etc/registry/key.json
+    enabled: true
+```
+
+Use the `compatibility` structure to configure handling of older and deprecated
+features. Each subsection defines such a feature with configurable behavior.
+
+### `schema1`
+
+| Parameter | Required | Description                                           |
+|-----------|----------|-------------------------------------------------------|
+| `signingkeyfile` | no | The signing private key used to add signatures to `schema1` manifests. If no signing key is provided, a new ECDSA key is generated when the registry starts. |
+| `enabled` | no | If this is not set to true, `schema1` manifests cannot be pushed. |
+
 ## `validation`
 
 ```none
diff --git a/docs/notifications.md b/docs/notifications.md
index 245d2faa..69ac90d0 100644
--- a/docs/notifications.md
+++ b/docs/notifications.md
@@ -152,16 +152,16 @@ group unrelated events and send them in the same envelope to reduce the total
 number of requests.
 
 The full package has the mediatype
-"application/vnd.docker.distribution.events.v2+json", which is set on the
+"application/vnd.docker.distribution.events.v1+json", which is set on the
 request coming to an endpoint.
 
 An example of a full event may look as follows:
 
 ```http request
 POST /callback HTTP/1.1
-Host: application/vnd.docker.distribution.events.v2+json
+Host: application/vnd.docker.distribution.events.v1+json
 Authorization: Bearer <your token, if needed>
-Content-Type: application/vnd.docker.distribution.events.v2+json
+Content-Type: application/vnd.docker.distribution.events.v1+json
 
 {
   "events": [
@@ -170,7 +170,7 @@ Content-Type: application/vnd.docker.distribution.events.v2+json
       "timestamp": "2006-01-02T15:04:05Z",
       "action": "push",
       "target": {
-        "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+        "mediaType": "application/vnd.docker.distribution.manifest.v1+json",
         "digest": "sha256:fea8895f450959fa676bcc1df0611ea93823a735a01205fd8622846041d0c7cf",
         "length": 1,
         "repository": "library/test",
diff --git a/docs/spec/manifest-v2-1.md b/docs/spec/manifest-v2-1.md
new file mode 100644
index 00000000..4572fb23
--- /dev/null
+++ b/docs/spec/manifest-v2-1.md
@@ -0,0 +1,170 @@
+---
+title: "Image Manifest V 2, Schema 1 "
+description: "image manifest for the Registry."
+keywords: registry, on-prem, images, tags, repository, distribution, api, advanced, manifest
+---
+
+# Image Manifest Version 2, Schema 1
+
+> **Deprecated**
+>
+> Image Manifest v2, Schema 1 is deprecated. Use [Image Manifest v2, Schema 2](manifest-v2-2.md),
+> or the [OCI Image Specification](https://github.com/opencontainers/image-spec).
+> Image Manifest v2, Schema 1 should not be used for purposes other than backward
+> compatibility.
+
+This document outlines the format of the V2 image manifest. The image
+manifest described herein was introduced in the Docker daemon in the [v1.3.0
+release](https://github.com/docker/docker/commit/9f482a66ab37ec396ac61ed0c00d59122ac07453).
+It is a provisional manifest to provide a compatibility with the [V1 Image
+format](https://github.com/docker/docker/blob/master/image/spec/v1.md), as the
+requirements are defined for the [V2 Schema 2
+image](https://github.com/distribution/distribution/pull/62).
+
+
+Image manifests describe the various constituents of a docker image.  Image
+manifests can be serialized to JSON format with the following media types:
+
+Manifest Type  | Media Type
+------------- | -------------
+manifest  | "application/vnd.docker.distribution.manifest.v1+json"
+signed manifest  | "application/vnd.docker.distribution.manifest.v1+prettyjws"
+
+*Note that "application/json" will also be accepted for schema 1.*
+
+References:
+
+ - [Proposal: JSON Registry API V2.1](https://github.com/docker/docker/issues/9015)
+ - [Proposal: Provenance step 1 - Transform images for validation and verification](https://github.com/docker/docker/issues/8093)
+
+## *Manifest* Field Descriptions
+
+Manifest provides the base accessible fields for working with V2 image format
+ in the registry.
+
+- **`name`** *string*
+
+	name is the name of the image's repository
+
+- **`tag`** *string*
+
+	tag is the tag of the image
+
+- **`architecture`** *string*
+
+   architecture is the host architecture on which this image is intended to
+   run.  This is for information purposes and not currently used by the engine
+
+- **`fsLayers`** *array*
+
+   fsLayers is a list of filesystem layer blob sums contained in this image.
+
+   An fsLayer is a struct consisting of the following fields
+      - **`blobSum`** *digest.Digest*
+
+      blobSum is the digest of the referenced filesystem image layer. A
+      digest must be a sha256 hash.
+
+
+- **`history`** *array*
+
+   history is a list of unstructured historical data for v1 compatibility. It
+   contains ID of the image layer and ID of the layer's parent layers.
+
+   history is a struct consisting of the following fields
+   - **`v1Compatibility`** string
+
+      V1Compatibility is the raw V1 compatibility information. This will
+      contain the JSON object describing the V1 of this image.
+
+- **`schemaVersion`** *int*
+
+   SchemaVersion is the image manifest schema that this image follows.
+
+>**Note**:the length of `history` must be equal to the length of `fsLayers` and
+>entries in each are correlated by index.
+
+## Signed Manifests
+
+Signed manifests provides an envelope for a signed image manifest.  A signed
+manifest consists of an image manifest along with an additional field
+containing the signature of the manifest.
+
+The docker client can verify signed manifests and displays a message to the user.
+
+### Signing Manifests
+
+Image manifests can be signed in two different ways: with a *libtrust* private
+ key or an x509 certificate chain.  When signing with an x509 certificate chain,
+ the public key of the first element in the chain must be the public key
+ corresponding with the sign key.
+
+### Signed Manifest Field Description
+
+Signed manifests include an image manifest and a list of signatures generated
+by *libtrust*.  A signature consists of the following fields:
+
+
+- **`header`** *[JOSE](http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31#section-2)*
+
+   A [JSON Web Signature](http://self-issued.info/docs/draft-ietf-jose-json-web-signature.html)
+
+- **`signature`** *string*
+
+	A signature for the image manifest, signed by a *libtrust* private key
+
+- **`protected`** *string*
+
+	The signed protected header
+
+## Example Manifest
+
+*Example showing the official 'hello-world' image manifest.*
+
+```
+{
+   "name": "hello-world",
+   "tag": "latest",
+   "architecture": "amd64",
+   "fsLayers": [
+      {
+         "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
+      },
+      {
+         "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
+      },
+      {
+         "blobSum": "sha256:cc8567d70002e957612902a8e985ea129d831ebe04057d88fb644857caa45d11"
+      },
+      {
+         "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
+      }
+   ],
+   "history": [
+      {
+         "v1Compatibility": "{\"id\":\"e45a5af57b00862e5ef5782a9925979a02ba2b12dff832fd0991335f4a11e5c5\",\"parent\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"created\":\"2014-12-31T22:57:59.178729048Z\",\"container\":\"27b45f8fb11795b52e9605b686159729b0d9ca92f76d40fb4f05a62e19c46b4f\",\"container_config\":{\"Hostname\":\"8ce6509d66e2\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [/hello]\"],\"Image\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"SecurityOpt\":null,\"Labels\":null},\"docker_version\":\"1.4.1\",\"config\":{\"Hostname\":\"8ce6509d66e2\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/hello\"],\"Image\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"SecurityOpt\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
+      },
+      {
+         "v1Compatibility": "{\"id\":\"e45a5af57b00862e5ef5782a9925979a02ba2b12dff832fd0991335f4a11e5c5\",\"parent\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"created\":\"2014-12-31T22:57:59.178729048Z\",\"container\":\"27b45f8fb11795b52e9605b686159729b0d9ca92f76d40fb4f05a62e19c46b4f\",\"container_config\":{\"Hostname\":\"8ce6509d66e2\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [/hello]\"],\"Image\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"SecurityOpt\":null,\"Labels\":null},\"docker_version\":\"1.4.1\",\"config\":{\"Hostname\":\"8ce6509d66e2\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/hello\"],\"Image\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"SecurityOpt\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
+      }
+   ],
+   "schemaVersion": 1,
+   "signatures": [
+      {
+         "header": {
+            "jwk": {
+               "crv": "P-256",
+               "kid": "OD6I:6DRK:JXEJ:KBM4:255X:NSAA:MUSF:E4VM:ZI6W:CUN2:L4Z6:LSF4",
+               "kty": "EC",
+               "x": "3gAwX48IQ5oaYQAYSxor6rYYc_6yjuLCjtQ9LUakg4A",
+               "y": "t72ge6kIA1XOjqjVoEOiPPAURltJFBMGDSQvEGVB010"
+            },
+            "alg": "ES256"
+         },
+         "signature": "XREm0L8WNn27Ga_iE_vRnTxVMhhYY0Zst_FfkKopg6gWSoTOZTuW4rK0fg_IqnKkEKlbD83tD46LKEGi5aIVFg",
+         "protected": "eyJmb3JtYXRMZW5ndGgiOjY2MjgsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNS0wNC0wOFQxODo1Mjo1OVoifQ"
+      }
+   ]
+}
+
+```
diff --git a/docs/spec/manifest-v2-2.md b/docs/spec/manifest-v2-2.md
index 5da1d0ad..81f40a39 100644
--- a/docs/spec/manifest-v2-2.md
+++ b/docs/spec/manifest-v2-2.md
@@ -24,6 +24,7 @@ an ID for the image.
 The following media types are used by the manifest formats described here, and
 the resources they reference:
 
+- `application/vnd.docker.distribution.manifest.v1+json`: schema1 (existing manifest format)
 - `application/vnd.docker.distribution.manifest.v2+json`: New image manifest format (schemaVersion = 2)
 - `application/vnd.docker.distribution.manifest.list.v2+json`: Manifest list, aka "fat manifest"
 - `application/vnd.docker.container.image.v1+json`: Container config JSON
@@ -59,7 +60,9 @@ image manifest based on the Content-Type returned in the HTTP response.
     - **`mediaType`** *string*
 
         The MIME type of the referenced object. This will generally be
-        `application/vnd.docker.distribution.manifest.v2+json`.
+        `application/vnd.docker.distribution.manifest.v2+json`, but it could also
+        be `application/vnd.docker.distribution.manifest.v1+json` if the manifest
+        list references a legacy schema-1 manifest.
 
     - **`size`** *int*
 
diff --git a/manifest/schema1/config_builder.go b/manifest/schema1/config_builder.go
new file mode 100644
index 00000000..77fdfee9
--- /dev/null
+++ b/manifest/schema1/config_builder.go
@@ -0,0 +1,303 @@
+package schema1
+
+import (
+	"context"
+	"crypto/sha512"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"time"
+
+	"github.com/distribution/distribution/v3"
+	"github.com/distribution/distribution/v3/manifest"
+	"github.com/distribution/distribution/v3/reference"
+	"github.com/docker/libtrust"
+	"github.com/opencontainers/go-digest"
+)
+
+type diffID digest.Digest
+
+// gzippedEmptyTar is a gzip-compressed version of an empty tar file
+// (1024 NULL bytes)
+var gzippedEmptyTar = []byte{
+	31, 139, 8, 0, 0, 9, 110, 136, 0, 255, 98, 24, 5, 163, 96, 20, 140, 88,
+	0, 8, 0, 0, 255, 255, 46, 175, 181, 239, 0, 4, 0, 0,
+}
+
+// digestSHA256GzippedEmptyTar is the canonical sha256 digest of
+// gzippedEmptyTar
+const digestSHA256GzippedEmptyTar = digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
+
+// configManifestBuilder is a type for constructing manifests from an image
+// configuration and generic descriptors.
+type configManifestBuilder struct {
+	// bs is a BlobService used to create empty layer tars in the
+	// blob store if necessary.
+	bs distribution.BlobService
+	// pk is the libtrust private key used to sign the final manifest.
+	pk libtrust.PrivateKey
+	// configJSON is configuration supplied when the ManifestBuilder was
+	// created.
+	configJSON []byte
+	// ref contains the name and optional tag provided to NewConfigManifestBuilder.
+	ref reference.Named
+	// descriptors is the set of descriptors referencing the layers.
+	descriptors []distribution.Descriptor
+	// emptyTarDigest is set to a valid digest if an empty tar has been
+	// put in the blob store; otherwise it is empty.
+	emptyTarDigest digest.Digest
+}
+
+// NewConfigManifestBuilder is used to build new manifests for the current
+// schema version from an image configuration and a set of descriptors.
+// It takes a BlobService so that it can add an empty tar to the blob store
+// if the resulting manifest needs empty layers.
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015,
+// use Docker Image Manifest v2, Schema 2, or the OCI Image Specification v1.
+// This package should not be used for purposes other than backward compatibility.
+func NewConfigManifestBuilder(bs distribution.BlobService, pk libtrust.PrivateKey, ref reference.Named, configJSON []byte) distribution.ManifestBuilder {
+	return &configManifestBuilder{
+		bs:         bs,
+		pk:         pk,
+		configJSON: configJSON,
+		ref:        ref,
+	}
+}
+
+// Build produces a final manifest from the given references.
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
+func (mb *configManifestBuilder) Build(ctx context.Context) (m distribution.Manifest, err error) {
+	type imageRootFS struct {
+		Type      string   `json:"type"`
+		DiffIDs   []diffID `json:"diff_ids,omitempty"`
+		BaseLayer string   `json:"base_layer,omitempty"`
+	}
+
+	type imageHistory struct {
+		Created    time.Time `json:"created"`
+		Author     string    `json:"author,omitempty"`
+		CreatedBy  string    `json:"created_by,omitempty"`
+		Comment    string    `json:"comment,omitempty"`
+		EmptyLayer bool      `json:"empty_layer,omitempty"`
+	}
+
+	type imageConfig struct {
+		RootFS       *imageRootFS   `json:"rootfs,omitempty"`
+		History      []imageHistory `json:"history,omitempty"`
+		Architecture string         `json:"architecture,omitempty"`
+	}
+
+	var img imageConfig
+
+	if err := json.Unmarshal(mb.configJSON, &img); err != nil {
+		return nil, err
+	}
+
+	if len(img.History) == 0 {
+		return nil, errors.New("empty history when trying to create schema1 manifest")
+	}
+
+	if len(img.RootFS.DiffIDs) != len(mb.descriptors) {
+		return nil, fmt.Errorf("number of descriptors and number of layers in rootfs must match: len(%v) != len(%v)", img.RootFS.DiffIDs, mb.descriptors)
+	}
+
+	// Generate IDs for each layer
+	// For non-top-level layers, create fake V1Compatibility strings that
+	// fit the format and don't collide with anything else, but don't
+	// result in runnable images on their own.
+	type v1Compatibility struct {
+		ID              string    `json:"id"`
+		Parent          string    `json:"parent,omitempty"`
+		Comment         string    `json:"comment,omitempty"`
+		Created         time.Time `json:"created"`
+		ContainerConfig struct {
+			Cmd []string
+		} `json:"container_config,omitempty"`
+		Author    string `json:"author,omitempty"`
+		ThrowAway bool   `json:"throwaway,omitempty"`
+	}
+
+	fsLayerList := make([]FSLayer, len(img.History))
+	history := make([]History, len(img.History))
+
+	parent := ""
+	layerCounter := 0
+	for i, h := range img.History[:len(img.History)-1] {
+		var blobsum digest.Digest
+		if h.EmptyLayer {
+			if blobsum, err = mb.emptyTar(ctx); err != nil {
+				return nil, err
+			}
+		} else {
+			if len(img.RootFS.DiffIDs) <= layerCounter {
+				return nil, errors.New("too many non-empty layers in History section")
+			}
+			blobsum = mb.descriptors[layerCounter].Digest
+			layerCounter++
+		}
+
+		v1ID := digest.FromBytes([]byte(blobsum.Encoded() + " " + parent)).Encoded()
+
+		if i == 0 && img.RootFS.BaseLayer != "" {
+			// windows-only baselayer setup
+			baseID := sha512.Sum384([]byte(img.RootFS.BaseLayer))
+			parent = fmt.Sprintf("%x", baseID[:32])
+		}
+
+		v1Compat := v1Compatibility{
+			ID:      v1ID,
+			Parent:  parent,
+			Comment: h.Comment,
+			Created: h.Created,
+			Author:  h.Author,
+		}
+		v1Compat.ContainerConfig.Cmd = []string{img.History[i].CreatedBy}
+		if h.EmptyLayer {
+			v1Compat.ThrowAway = true
+		}
+		jsonBytes, err := json.Marshal(&v1Compat)
+		if err != nil {
+			return nil, err
+		}
+
+		reversedIndex := len(img.History) - i - 1
+		history[reversedIndex].V1Compatibility = string(jsonBytes)
+		fsLayerList[reversedIndex] = FSLayer{BlobSum: blobsum}
+
+		parent = v1ID
+	}
+
+	latestHistory := img.History[len(img.History)-1]
+
+	var blobsum digest.Digest
+	if latestHistory.EmptyLayer {
+		if blobsum, err = mb.emptyTar(ctx); err != nil {
+			return nil, err
+		}
+	} else {
+		if len(img.RootFS.DiffIDs) <= layerCounter {
+			return nil, errors.New("too many non-empty layers in History section")
+		}
+		blobsum = mb.descriptors[layerCounter].Digest
+	}
+
+	fsLayerList[0] = FSLayer{BlobSum: blobsum}
+	dgst := digest.FromBytes([]byte(blobsum.Encoded() + " " + parent + " " + string(mb.configJSON)))
+
+	// Top-level v1compatibility string should be a modified version of the
+	// image config.
+	transformedConfig, err := MakeV1ConfigFromConfig(mb.configJSON, dgst.Encoded(), parent, latestHistory.EmptyLayer)
+	if err != nil {
+		return nil, err
+	}
+
+	history[0].V1Compatibility = string(transformedConfig)
+
+	tag := ""
+	if tagged, isTagged := mb.ref.(reference.Tagged); isTagged {
+		tag = tagged.Tag()
+	}
+
+	mfst := Manifest{
+		Versioned: manifest.Versioned{
+			SchemaVersion: 1,
+		},
+		Name:         mb.ref.Name(),
+		Tag:          tag,
+		Architecture: img.Architecture,
+		FSLayers:     fsLayerList,
+		History:      history,
+	}
+
+	return Sign(&mfst, mb.pk)
+}
+
+// emptyTar pushes a compressed empty tar to the blob store if one doesn't
+// already exist, and returns its blobsum.
+func (mb *configManifestBuilder) emptyTar(ctx context.Context) (digest.Digest, error) {
+	if mb.emptyTarDigest != "" {
+		// Already put an empty tar
+		return mb.emptyTarDigest, nil
+	}
+
+	descriptor, err := mb.bs.Stat(ctx, digestSHA256GzippedEmptyTar)
+	switch err {
+	case nil:
+		mb.emptyTarDigest = descriptor.Digest
+		return descriptor.Digest, nil
+	case distribution.ErrBlobUnknown:
+		// nop
+	default:
+		return "", err
+	}
+
+	// Add gzipped empty tar to the blob store
+	descriptor, err = mb.bs.Put(ctx, "", gzippedEmptyTar)
+	if err != nil {
+		return "", err
+	}
+
+	mb.emptyTarDigest = descriptor.Digest
+
+	return descriptor.Digest, nil
+}
+
+// AppendReference adds a reference to the current ManifestBuilder.
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
+func (mb *configManifestBuilder) AppendReference(d distribution.Describable) error {
+	descriptor := d.Descriptor()
+
+	if err := descriptor.Digest.Validate(); err != nil {
+		return err
+	}
+
+	mb.descriptors = append(mb.descriptors, descriptor)
+	return nil
+}
+
+// References returns the current references added to this builder.
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
+func (mb *configManifestBuilder) References() []distribution.Descriptor {
+	return mb.descriptors
+}
+
+// MakeV1ConfigFromConfig creates an legacy V1 image config from image config JSON.
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
+func MakeV1ConfigFromConfig(configJSON []byte, v1ID, parentV1ID string, throwaway bool) ([]byte, error) {
+	// Top-level v1compatibility string should be a modified version of the
+	// image config.
+	var configAsMap map[string]*json.RawMessage
+	if err := json.Unmarshal(configJSON, &configAsMap); err != nil {
+		return nil, err
+	}
+
+	// Delete fields that didn't exist in old manifest
+	delete(configAsMap, "rootfs")
+	delete(configAsMap, "history")
+	configAsMap["id"] = rawJSON(v1ID)
+	if parentV1ID != "" {
+		configAsMap["parent"] = rawJSON(parentV1ID)
+	}
+	if throwaway {
+		configAsMap["throwaway"] = rawJSON(true)
+	}
+
+	return json.Marshal(configAsMap)
+}
+
+func rawJSON(value interface{}) *json.RawMessage {
+	jsonval, err := json.Marshal(value)
+	if err != nil {
+		return nil
+	}
+	return (*json.RawMessage)(&jsonval)
+}
diff --git a/manifest/schema1/manifest.go b/manifest/schema1/manifest.go
new file mode 100644
index 00000000..4faee15d
--- /dev/null
+++ b/manifest/schema1/manifest.go
@@ -0,0 +1,226 @@
+// Package schema1 provides definitions for the deprecated Docker Image
+// Manifest v2, Schema 1 specification.
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification. This package should not be used for purposes
+// other than backward compatibility.
+package schema1
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/distribution/distribution/v3"
+	"github.com/distribution/distribution/v3/manifest"
+	"github.com/docker/libtrust"
+	"github.com/opencontainers/go-digest"
+)
+
+// MediaTypeManifest specifies the mediaType for the current version. Note
+// that for schema version 1, the the media is optionally "application/json".
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
+const MediaTypeManifest = "application/vnd.docker.distribution.manifest.v1+json"
+
+// MediaTypeSignedManifest specifies the mediatype for current SignedManifest version
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
+const MediaTypeSignedManifest = "application/vnd.docker.distribution.manifest.v1+prettyjws"
+
+// MediaTypeManifestLayer specifies the media type for manifest layers
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
+const MediaTypeManifestLayer = "application/vnd.docker.container.image.rootfs.diff+x-gtar"
+
+// SchemaVersion provides a pre-initialized version structure for this
+// packages version 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.
+var SchemaVersion = manifest.Versioned{
+	SchemaVersion: 1,
+}
+
+func init() {
+	schema1Func := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
+		sm := new(SignedManifest)
+		err := sm.UnmarshalJSON(b)
+		if err != nil {
+			return nil, distribution.Descriptor{}, err
+		}
+
+		desc := distribution.Descriptor{
+			MediaType: MediaTypeSignedManifest,
+			Digest:    digest.FromBytes(sm.Canonical),
+			Size:      int64(len(sm.Canonical)),
+		}
+		return sm, desc, err
+	}
+	err := distribution.RegisterManifestSchema(MediaTypeSignedManifest, schema1Func)
+	if err != nil {
+		panic(fmt.Sprintf("Unable to register manifest: %s", err))
+	}
+	err = distribution.RegisterManifestSchema("", schema1Func)
+	if err != nil {
+		panic(fmt.Sprintf("Unable to register manifest: %s", err))
+	}
+	err = distribution.RegisterManifestSchema("application/json", schema1Func)
+	if err != nil {
+		panic(fmt.Sprintf("Unable to register manifest: %s", err))
+	}
+}
+
+// FSLayer is a container struct for BlobSums defined in an image manifest.
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
+type FSLayer struct {
+	// BlobSum is the tarsum of the referenced filesystem image layer
+	BlobSum digest.Digest `json:"blobSum"`
+}
+
+// History stores unstructured v1 compatibility information.
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
+type History struct {
+	// V1Compatibility is the raw v1 compatibility information
+	V1Compatibility string `json:"v1Compatibility"`
+}
+
+// Manifest provides the base accessible fields for working with V2 image
+// format in the registry.
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
+type Manifest struct {
+	manifest.Versioned
+
+	// Name is the name of the image's repository
+	Name string `json:"name"`
+
+	// Tag is the tag of the image specified by this manifest
+	Tag string `json:"tag"`
+
+	// Architecture is the host architecture on which this image is intended to
+	// run
+	Architecture string `json:"architecture"`
+
+	// FSLayers is a list of filesystem layer blobSums contained in this image
+	FSLayers []FSLayer `json:"fsLayers"`
+
+	// History is a list of unstructured historical data for v1 compatibility
+	History []History `json:"history"`
+}
+
+// SignedManifest provides an envelope for a signed image manifest, including
+// the format sensitive raw bytes.
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
+type SignedManifest struct {
+	Manifest
+
+	// Canonical is the canonical byte representation of the ImageManifest,
+	// without any attached signatures. The manifest byte
+	// representation cannot change or it will have to be re-signed.
+	Canonical []byte `json:"-"`
+
+	// all contains the byte representation of the Manifest including signatures
+	// and is returned by Payload()
+	all []byte
+}
+
+// UnmarshalJSON populates a new SignedManifest struct from JSON data.
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
+func (sm *SignedManifest) UnmarshalJSON(b []byte) error {
+	sm.all = make([]byte, len(b))
+	// store manifest and signatures in all
+	copy(sm.all, b)
+
+	jsig, err := libtrust.ParsePrettySignature(b, "signatures")
+	if err != nil {
+		return err
+	}
+
+	// Resolve the payload in the manifest.
+	bytes, err := jsig.Payload()
+	if err != nil {
+		return err
+	}
+
+	// sm.Canonical stores the canonical manifest JSON
+	sm.Canonical = make([]byte, len(bytes))
+	copy(sm.Canonical, bytes)
+
+	// Unmarshal canonical JSON into Manifest object
+	var mfst Manifest
+	if err := json.Unmarshal(sm.Canonical, &mfst); err != nil {
+		return err
+	}
+
+	sm.Manifest = mfst
+
+	return nil
+}
+
+// References returns the descriptors of this manifests references.
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
+func (sm SignedManifest) References() []distribution.Descriptor {
+	dependencies := make([]distribution.Descriptor, len(sm.FSLayers))
+	for i, fsLayer := range sm.FSLayers {
+		dependencies[i] = distribution.Descriptor{
+			MediaType: "application/vnd.docker.container.image.rootfs.diff+x-gtar",
+			Digest:    fsLayer.BlobSum,
+		}
+	}
+
+	return dependencies
+}
+
+// MarshalJSON returns the contents of raw. If Raw is nil, marshals the inner
+// contents. Applications requiring a marshaled signed manifest should simply
+// use Raw directly, since the the content produced by json.Marshal will be
+// compacted and will fail signature checks.
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
+func (sm *SignedManifest) MarshalJSON() ([]byte, error) {
+	if len(sm.all) > 0 {
+		return sm.all, nil
+	}
+
+	// If the raw data is not available, just dump the inner content.
+	return json.Marshal(&sm.Manifest)
+}
+
+// Payload returns the signed content of the signed 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 (sm SignedManifest) Payload() (string, []byte, error) {
+	return MediaTypeSignedManifest, sm.all, nil
+}
+
+// Signatures returns the signatures as provided by
+// (*libtrust.JSONSignature).Signatures. The byte slices are opaque jws
+// signatures.
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
+func (sm *SignedManifest) Signatures() ([][]byte, error) {
+	jsig, err := libtrust.ParsePrettySignature(sm.all, "signatures")
+	if err != nil {
+		return nil, err
+	}
+
+	// Resolve the payload in the manifest.
+	return jsig.Signatures()
+}
diff --git a/manifest/schema1/reference_builder.go b/manifest/schema1/reference_builder.go
new file mode 100644
index 00000000..13713b52
--- /dev/null
+++ b/manifest/schema1/reference_builder.go
@@ -0,0 +1,112 @@
+package schema1
+
+import (
+	"context"
+	"errors"
+	"fmt"
+
+	"github.com/distribution/distribution/v3"
+	"github.com/distribution/distribution/v3/manifest"
+	"github.com/distribution/distribution/v3/reference"
+	"github.com/docker/libtrust"
+	"github.com/opencontainers/go-digest"
+)
+
+// referenceManifestBuilder is a type for constructing manifests from schema1
+// dependencies.
+type referenceManifestBuilder struct {
+	Manifest
+	pk libtrust.PrivateKey
+}
+
+// NewReferenceManifestBuilder is used to build new manifests for the current
+// schema version using schema1 dependencies.
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
+func NewReferenceManifestBuilder(pk libtrust.PrivateKey, ref reference.Named, architecture string) distribution.ManifestBuilder {
+	tag := ""
+	if tagged, isTagged := ref.(reference.Tagged); isTagged {
+		tag = tagged.Tag()
+	}
+
+	return &referenceManifestBuilder{
+		Manifest: Manifest{
+			Versioned: manifest.Versioned{
+				SchemaVersion: 1,
+			},
+			Name:         ref.Name(),
+			Tag:          tag,
+			Architecture: architecture,
+		},
+		pk: pk,
+	}
+}
+
+func (mb *referenceManifestBuilder) Build(ctx context.Context) (distribution.Manifest, error) {
+	m := mb.Manifest
+	if len(m.FSLayers) == 0 {
+		return nil, errors.New("cannot build manifest with zero layers or history")
+	}
+
+	m.FSLayers = make([]FSLayer, len(mb.Manifest.FSLayers))
+	m.History = make([]History, len(mb.Manifest.History))
+	copy(m.FSLayers, mb.Manifest.FSLayers)
+	copy(m.History, mb.Manifest.History)
+
+	return Sign(&m, mb.pk)
+}
+
+// AppendReference adds a reference to the current ManifestBuilder.
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
+func (mb *referenceManifestBuilder) AppendReference(d distribution.Describable) error {
+	r, ok := d.(Reference)
+	if !ok {
+		return fmt.Errorf("unable to add non-reference type to v1 builder")
+	}
+
+	// Entries need to be prepended
+	mb.Manifest.FSLayers = append([]FSLayer{{BlobSum: r.Digest}}, mb.Manifest.FSLayers...)
+	mb.Manifest.History = append([]History{r.History}, mb.Manifest.History...)
+	return nil
+}
+
+// References returns the current references added to this builder.
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
+func (mb *referenceManifestBuilder) References() []distribution.Descriptor {
+	refs := make([]distribution.Descriptor, len(mb.Manifest.FSLayers))
+	for i := range mb.Manifest.FSLayers {
+		layerDigest := mb.Manifest.FSLayers[i].BlobSum
+		history := mb.Manifest.History[i]
+		ref := Reference{layerDigest, 0, history}
+		refs[i] = ref.Descriptor()
+	}
+	return refs
+}
+
+// Reference describes a Manifest v2, schema version 1 dependency.
+// An FSLayer associated with a history entry.
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
+type Reference struct {
+	Digest  digest.Digest
+	Size    int64 // if we know it, set it for the descriptor.
+	History History
+}
+
+// Descriptor describes a reference.
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
+func (r Reference) Descriptor() distribution.Descriptor {
+	return distribution.Descriptor{
+		MediaType: MediaTypeManifestLayer,
+		Digest:    r.Digest,
+		Size:      r.Size,
+	}
+}
diff --git a/manifest/schema1/sign.go b/manifest/schema1/sign.go
new file mode 100644
index 00000000..920fdb2d
--- /dev/null
+++ b/manifest/schema1/sign.go
@@ -0,0 +1,74 @@
+package schema1
+
+import (
+	"crypto/x509"
+	"encoding/json"
+
+	"github.com/docker/libtrust"
+)
+
+// Sign signs the manifest with the provided private key, returning a
+// SignedManifest. This typically won't be used within the registry, except
+// for testing.
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
+func Sign(m *Manifest, pk libtrust.PrivateKey) (*SignedManifest, error) {
+	p, err := json.MarshalIndent(m, "", "   ")
+	if err != nil {
+		return nil, err
+	}
+
+	js, err := libtrust.NewJSONSignature(p)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := js.Sign(pk); err != nil {
+		return nil, err
+	}
+
+	pretty, err := js.PrettySignature("signatures")
+	if err != nil {
+		return nil, err
+	}
+
+	return &SignedManifest{
+		Manifest:  *m,
+		all:       pretty,
+		Canonical: p,
+	}, nil
+}
+
+// SignWithChain signs the manifest with the given private key and x509 chain.
+// The public key of the first element in the chain must be the public key
+// corresponding with the sign key.
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
+func SignWithChain(m *Manifest, key libtrust.PrivateKey, chain []*x509.Certificate) (*SignedManifest, error) {
+	p, err := json.MarshalIndent(m, "", "   ")
+	if err != nil {
+		return nil, err
+	}
+
+	js, err := libtrust.NewJSONSignature(p)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := js.SignWithChain(key, chain); err != nil {
+		return nil, err
+	}
+
+	pretty, err := js.PrettySignature("signatures")
+	if err != nil {
+		return nil, err
+	}
+
+	return &SignedManifest{
+		Manifest:  *m,
+		all:       pretty,
+		Canonical: p,
+	}, nil
+}
diff --git a/manifest/schema1/verify.go b/manifest/schema1/verify.go
new file mode 100644
index 00000000..5d20cc5b
--- /dev/null
+++ b/manifest/schema1/verify.go
@@ -0,0 +1,38 @@
+package schema1
+
+import (
+	"crypto/x509"
+
+	"github.com/docker/libtrust"
+	"github.com/sirupsen/logrus"
+)
+
+// Verify verifies the signature of the signed manifest returning the public
+// keys used during signing.
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
+func Verify(sm *SignedManifest) ([]libtrust.PublicKey, error) {
+	js, err := libtrust.ParsePrettySignature(sm.all, "signatures")
+	if err != nil {
+		logrus.WithField("err", err).Debugf("(*SignedManifest).Verify")
+		return nil, err
+	}
+
+	return js.Verify()
+}
+
+// VerifyChains verifies the signature of the signed manifest against the
+// certificate pool returning the list of verified chains. Signatures without
+// an x509 chain are not checked.
+//
+// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
+// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
+func VerifyChains(sm *SignedManifest, ca *x509.CertPool) ([][]*x509.Certificate, error) {
+	js, err := libtrust.ParsePrettySignature(sm.all, "signatures")
+	if err != nil {
+		return nil, err
+	}
+
+	return js.VerifyChains(ca)
+}
diff --git a/notifications/event.go b/notifications/event.go
index 82c7ba2c..dc794ba2 100644
--- a/notifications/event.go
+++ b/notifications/event.go
@@ -20,7 +20,7 @@ const (
 	// EventsMediaType is the mediatype for the json event envelope. If the
 	// Event, ActorRecord, SourceRecord or Envelope structs change, the version
 	// number should be incremented.
-	EventsMediaType = "application/vnd.docker.distribution.events.v2+json"
+	EventsMediaType = "application/vnd.docker.distribution.events.v1+json"
 	// LayerMediaType is the media type for image rootfs diffs (aka "layers")
 	// used by Docker. We don't expect this to change for quite a while.
 	layerMediaType = "application/vnd.docker.container.image.rootfs.diff+x-gtar"
diff --git a/registry/handlers/app.go b/registry/handlers/app.go
index 04e3a070..8901e196 100644
--- a/registry/handlers/app.go
+++ b/registry/handlers/app.go
@@ -525,7 +525,6 @@ func (app *App) createPool(cfg configuration.Redis) *redis.Client {
 			res := cn.Ping(ctx)
 			return res.Err()
 		},
-		Username:        cfg.Username,
 		Password:        cfg.Password,
 		DB:              cfg.DB,
 		MaxRetries:      3,
diff --git a/registry/storage/cache/redis/redis.go b/registry/storage/cache/redis/redis.go
index 9efbbf22..80286d71 100644
--- a/registry/storage/cache/redis/redis.go
+++ b/registry/storage/cache/redis/redis.go
@@ -135,10 +135,6 @@ func (rbds *redisBlobDescriptorService) SetDescriptor(ctx context.Context, dgst
 		return err
 	}
 
-	if err := cache.ValidateDescriptor(desc); err != nil {
-		return err
-	}
-
 	return rbds.setDescriptor(ctx, rbds.pool, dgst, desc)
 }
 
diff --git a/registry/storage/catalog.go b/registry/storage/catalog.go
index 7c8551fe..55500694 100644
--- a/registry/storage/catalog.go
+++ b/registry/storage/catalog.go
@@ -14,12 +14,12 @@ import (
 // Returns a list, or partial list, of repositories in the registry.
 // Because it's a quite expensive operation, it should only be used when building up
 // an initial set of repositories.
-func (reg *registry) Repositories(ctx context.Context, repos []string, last string) (int, error) {
-	filledBuffer := false
-	foundRepos := 0
+func (reg *registry) Repositories(ctx context.Context, repos []string, last string) (n int, err error) {
+	var finishedWalk bool
+	var foundRepos []string
 
 	if len(repos) == 0 {
-		return 0, errors.New("Attempted to list 0 repositories")
+		return 0, errors.New("no space in slice")
 	}
 
 	root, err := pathFor(repositoriesRootPathSpec{})
@@ -29,34 +29,32 @@ func (reg *registry) Repositories(ctx context.Context, repos []string, last stri
 
 	err = reg.blobStore.driver.Walk(ctx, root, func(fileInfo driver.FileInfo) error {
 		err := handleRepository(fileInfo, root, last, func(repoPath string) error {
-			repos[foundRepos] = repoPath
-			foundRepos += 1
+			foundRepos = append(foundRepos, repoPath)
 			return nil
 		})
 		if err != nil {
 			return err
 		}
 
-		// if we've filled our slice, no need to walk any further
-		if foundRepos == len(repos) {
-			filledBuffer = true
+		// if we've filled our array, no need to walk any further
+		if len(foundRepos) == len(repos) {
+			finishedWalk = true
 			return driver.ErrSkipDir
 		}
 
 		return nil
 	})
 
-	if err != nil {
-		return foundRepos, err
-	}
+	n = copy(repos, foundRepos)
 
-	if filledBuffer {
-		// There are potentially more repositories to list
-		return foundRepos, nil
+	if err != nil {
+		return n, err
+	} else if !finishedWalk {
+		// We didn't fill buffer. No more records are available.
+		return n, io.EOF
 	}
 
-	// We didn't fill the buffer, so that's the end of the list of repos
-	return foundRepos, io.EOF
+	return n, err
 }
 
 // Enumerate applies ingester to each repository


Test patch:
diff --git a/manifest/schema1/config_builder_test.go b/manifest/schema1/config_builder_test.go
new file mode 100644
index 00000000..32b7c1f5
--- /dev/null
+++ b/manifest/schema1/config_builder_test.go
@@ -0,0 +1,262 @@
+package schema1
+
+import (
+	"bytes"
+	"compress/gzip"
+	"context"
+	"io"
+	"reflect"
+	"testing"
+
+	"github.com/distribution/distribution/v3"
+	dcontext "github.com/distribution/distribution/v3/context"
+	"github.com/distribution/distribution/v3/reference"
+	"github.com/docker/libtrust"
+	"github.com/opencontainers/go-digest"
+)
+
+type mockBlobService struct {
+	descriptors map[digest.Digest]distribution.Descriptor
+	distribution.BlobService
+}
+
+func (bs *mockBlobService) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
+	if descriptor, ok := bs.descriptors[dgst]; ok {
+		return descriptor, nil
+	}
+	return distribution.Descriptor{}, distribution.ErrBlobUnknown
+}
+
+func (bs *mockBlobService) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) {
+	d := distribution.Descriptor{
+		MediaType: mediaType,
+		Digest:    digest.FromBytes(p),
+		Size:      int64(len(p)),
+	}
+	bs.descriptors[d.Digest] = d
+	return d, nil
+}
+
+func TestEmptyTar(t *testing.T) {
+	// Confirm that gzippedEmptyTar expands to 1024 NULL bytes.
+	var decompressed [2048]byte
+	gzipReader, err := gzip.NewReader(bytes.NewReader(gzippedEmptyTar))
+	if err != nil {
+		t.Fatalf("NewReader returned error: %v", err)
+	}
+	n, _ := gzipReader.Read(decompressed[:])
+	if n != 1024 {
+		t.Fatalf("read returned %d bytes; expected 1024", n)
+	}
+	n, err = gzipReader.Read(decompressed[1024:])
+	if n != 0 {
+		t.Fatalf("read returned %d bytes; expected 0", n)
+	}
+	if err != io.EOF {
+		t.Fatal("read did not return io.EOF")
+	}
+	gzipReader.Close()
+	for _, b := range decompressed[:1024] {
+		if b != 0 {
+			t.Fatal("nonzero byte in decompressed tar")
+		}
+	}
+
+	// Confirm that digestSHA256EmptyTar is the digest of gzippedEmptyTar.
+	dgst := digest.FromBytes(gzippedEmptyTar)
+	if dgst != digestSHA256GzippedEmptyTar {
+		t.Fatalf("digest mismatch for empty tar: expected %s got %s", digestSHA256GzippedEmptyTar, dgst)
+	}
+}
+
+func TestConfigBuilder(t *testing.T) {
+	imgJSON := `{
+    "architecture": "amd64",
+    "config": {
+        "AttachStderr": false,
+        "AttachStdin": false,
+        "AttachStdout": false,
+        "Cmd": [
+            "/bin/sh",
+            "-c",
+            "echo hi"
+        ],
+        "Domainname": "",
+        "Entrypoint": null,
+        "Env": [
+            "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
+            "derived=true",
+            "asdf=true"
+        ],
+        "Hostname": "23304fc829f9",
+        "Image": "sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246",
+        "Labels": {},
+        "OnBuild": [],
+        "OpenStdin": false,
+        "StdinOnce": false,
+        "Tty": false,
+        "User": "",
+        "Volumes": null,
+        "WorkingDir": ""
+    },
+    "container": "e91032eb0403a61bfe085ff5a5a48e3659e5a6deae9f4d678daa2ae399d5a001",
+    "container_config": {
+        "AttachStderr": false,
+        "AttachStdin": false,
+        "AttachStdout": false,
+        "Cmd": [
+            "/bin/sh",
+            "-c",
+            "#(nop) CMD [\"/bin/sh\" \"-c\" \"echo hi\"]"
+        ],
+        "Domainname": "",
+        "Entrypoint": null,
+        "Env": [
+            "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
+            "derived=true",
+            "asdf=true"
+        ],
+        "Hostname": "23304fc829f9",
+        "Image": "sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246",
+        "Labels": {},
+        "OnBuild": [],
+        "OpenStdin": false,
+        "StdinOnce": false,
+        "Tty": false,
+        "User": "",
+        "Volumes": null,
+        "WorkingDir": ""
+    },
+    "created": "2015-11-04T23:06:32.365666163Z",
+    "docker_version": "1.9.0-dev",
+    "history": [
+        {
+            "created": "2015-10-31T22:22:54.690851953Z",
+            "created_by": "/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /"
+        },
+        {
+            "created": "2015-10-31T22:22:55.613815829Z",
+            "created_by": "/bin/sh -c #(nop) CMD [\"sh\"]"
+        },
+        {
+            "created": "2015-11-04T23:06:30.934316144Z",
+            "created_by": "/bin/sh -c #(nop) ENV derived=true",
+            "empty_layer": true
+        },
+        {
+            "created": "2015-11-04T23:06:31.192097572Z",
+            "created_by": "/bin/sh -c #(nop) ENV asdf=true",
+            "empty_layer": true
+        },
+        {
+            "author": "Alyssa P. Hacker \u003calyspdev@example.com\u003e",
+            "created": "2015-11-04T23:06:32.083868454Z",
+            "created_by": "/bin/sh -c dd if=/dev/zero of=/file bs=1024 count=1024"
+        },
+        {
+            "created": "2015-11-04T23:06:32.365666163Z",
+            "created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\" \"-c\" \"echo hi\"]",
+            "empty_layer": true
+        }
+    ],
+    "os": "linux",
+    "rootfs": {
+        "diff_ids": [
+            "sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1",
+            "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
+            "sha256:13f53e08df5a220ab6d13c58b2bf83a59cbdc2e04d0a3f041ddf4b0ba4112d49"
+        ],
+        "type": "layers"
+    }
+}`
+
+	descriptors := []distribution.Descriptor{
+		{Digest: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
+		{Digest: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")},
+		{Digest: digest.Digest("sha256:b4ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
+	}
+
+	pk, err := libtrust.GenerateECP256PrivateKey()
+	if err != nil {
+		t.Fatalf("could not generate key for testing: %v", err)
+	}
+
+	bs := &mockBlobService{descriptors: make(map[digest.Digest]distribution.Descriptor)}
+
+	ref, err := reference.WithName("testrepo")
+	if err != nil {
+		t.Fatalf("could not parse reference: %v", err)
+	}
+	ref, err = reference.WithTag(ref, "testtag")
+	if err != nil {
+		t.Fatalf("could not add tag: %v", err)
+	}
+
+	builder := NewConfigManifestBuilder(bs, pk, ref, []byte(imgJSON))
+
+	for _, d := range descriptors {
+		if err := builder.AppendReference(d); err != nil {
+			t.Fatalf("AppendReference returned error: %v", err)
+		}
+	}
+
+	signed, err := builder.Build(dcontext.Background())
+	if err != nil {
+		t.Fatalf("Build returned error: %v", err)
+	}
+
+	// Check that the gzipped empty layer tar was put in the blob store
+	_, err = bs.Stat(dcontext.Background(), digestSHA256GzippedEmptyTar)
+	if err != nil {
+		t.Fatal("gzipped empty tar was not put in the blob store")
+	}
+
+	manifest := signed.(*SignedManifest).Manifest
+
+	if manifest.Versioned.SchemaVersion != 1 {
+		t.Fatal("SchemaVersion != 1")
+	}
+	if manifest.Name != "testrepo" {
+		t.Fatal("incorrect name in manifest")
+	}
+	if manifest.Tag != "testtag" {
+		t.Fatal("incorrect tag in manifest")
+	}
+	if manifest.Architecture != "amd64" {
+		t.Fatal("incorrect arch in manifest")
+	}
+
+	expectedFSLayers := []FSLayer{
+		{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
+		{BlobSum: digest.Digest("sha256:b4ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
+		{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
+		{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
+		{BlobSum: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")},
+		{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
+	}
+
+	if len(manifest.FSLayers) != len(expectedFSLayers) {
+		t.Fatalf("wrong number of FSLayers: %d", len(manifest.FSLayers))
+	}
+	if !reflect.DeepEqual(manifest.FSLayers, expectedFSLayers) {
+		t.Fatal("wrong FSLayers list")
+	}
+
+	expectedV1Compatibility := []string{
+		`{"architecture":"amd64","config":{"AttachStderr":false,"AttachStdin":false,"AttachStdout":false,"Cmd":["/bin/sh","-c","echo hi"],"Domainname":"","Entrypoint":null,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","derived=true","asdf=true"],"Hostname":"23304fc829f9","Image":"sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246","Labels":{},"OnBuild":[],"OpenStdin":false,"StdinOnce":false,"Tty":false,"User":"","Volumes":null,"WorkingDir":""},"container":"e91032eb0403a61bfe085ff5a5a48e3659e5a6deae9f4d678daa2ae399d5a001","container_config":{"AttachStderr":false,"AttachStdin":false,"AttachStdout":false,"Cmd":["/bin/sh","-c","#(nop) CMD [\"/bin/sh\" \"-c\" \"echo hi\"]"],"Domainname":"","Entrypoint":null,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","derived=true","asdf=true"],"Hostname":"23304fc829f9","Image":"sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246","Labels":{},"OnBuild":[],"OpenStdin":false,"StdinOnce":false,"Tty":false,"User":"","Volumes":null,"WorkingDir":""},"created":"2015-11-04T23:06:32.365666163Z","docker_version":"1.9.0-dev","id":"69e5c1bfadad697fdb6db59f6326648fa119e0c031a0eda33b8cfadcab54ba7f","os":"linux","parent":"74cf9c92699240efdba1903c2748ef57105d5bedc588084c4e88f3bb1c3ef0b0","throwaway":true}`,
+		`{"id":"74cf9c92699240efdba1903c2748ef57105d5bedc588084c4e88f3bb1c3ef0b0","parent":"178be37afc7c49e951abd75525dbe0871b62ad49402f037164ee6314f754599d","created":"2015-11-04T23:06:32.083868454Z","container_config":{"Cmd":["/bin/sh -c dd if=/dev/zero of=/file bs=1024 count=1024"]},"author":"Alyssa P. Hacker \u003calyspdev@example.com\u003e"}`,
+		`{"id":"178be37afc7c49e951abd75525dbe0871b62ad49402f037164ee6314f754599d","parent":"b449305a55a283538c4574856a8b701f2a3d5ec08ef8aec47f385f20339a4866","created":"2015-11-04T23:06:31.192097572Z","container_config":{"Cmd":["/bin/sh -c #(nop) ENV asdf=true"]},"throwaway":true}`,
+		`{"id":"b449305a55a283538c4574856a8b701f2a3d5ec08ef8aec47f385f20339a4866","parent":"9e3447ca24cb96d86ebd5960cb34d1299b07e0a0e03801d90b9969a2c187dd6e","created":"2015-11-04T23:06:30.934316144Z","container_config":{"Cmd":["/bin/sh -c #(nop) ENV derived=true"]},"throwaway":true}`,
+		`{"id":"9e3447ca24cb96d86ebd5960cb34d1299b07e0a0e03801d90b9969a2c187dd6e","parent":"3690474eb5b4b26fdfbd89c6e159e8cc376ca76ef48032a30fa6aafd56337880","created":"2015-10-31T22:22:55.613815829Z","container_config":{"Cmd":["/bin/sh -c #(nop) CMD [\"sh\"]"]}}`,
+		`{"id":"3690474eb5b4b26fdfbd89c6e159e8cc376ca76ef48032a30fa6aafd56337880","created":"2015-10-31T22:22:54.690851953Z","container_config":{"Cmd":["/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /"]}}`,
+	}
+
+	if len(manifest.History) != len(expectedV1Compatibility) {
+		t.Fatalf("wrong number of history entries: %d", len(manifest.History))
+	}
+	for i := range expectedV1Compatibility {
+		if manifest.History[i].V1Compatibility != expectedV1Compatibility[i] {
+			t.Errorf("wrong V1Compatibility %d. expected:\n%s\ngot:\n%s", i, expectedV1Compatibility[i], manifest.History[i].V1Compatibility)
+		}
+	}
+}
diff --git a/manifest/schema1/manifest_test.go b/manifest/schema1/manifest_test.go
new file mode 100644
index 00000000..ddc2a0b8
--- /dev/null
+++ b/manifest/schema1/manifest_test.go
@@ -0,0 +1,135 @@
+package schema1
+
+import (
+	"bytes"
+	"encoding/json"
+	"reflect"
+	"testing"
+
+	"github.com/docker/libtrust"
+)
+
+type testEnv struct {
+	name, tag     string
+	invalidSigned *SignedManifest
+	signed        *SignedManifest
+	pk            libtrust.PrivateKey
+}
+
+func TestManifestMarshaling(t *testing.T) {
+	env := genEnv(t)
+
+	// Check that the all field is the same as json.MarshalIndent with these
+	// parameters.
+	expected, err := json.MarshalIndent(env.signed, "", "   ")
+	if err != nil {
+		t.Fatalf("error marshaling manifest: %v", err)
+	}
+
+	if !bytes.Equal(expected, env.signed.all) {
+		t.Fatalf("manifest bytes not equal:\nexpected:\n%s\nactual:\n%s\n", string(expected), string(env.signed.all))
+	}
+}
+
+func TestManifestUnmarshaling(t *testing.T) {
+	env := genEnv(t)
+
+	var signed SignedManifest
+	if err := json.Unmarshal(env.signed.all, &signed); err != nil {
+		t.Fatalf("error unmarshaling signed manifest: %v", err)
+	}
+
+	if !reflect.DeepEqual(&signed, env.signed) {
+		t.Fatalf("manifests are different after unmarshaling: %v != %v", signed, env.signed)
+	}
+}
+
+func TestManifestVerification(t *testing.T) {
+	env := genEnv(t)
+
+	publicKeys, err := Verify(env.signed)
+	if err != nil {
+		t.Fatalf("error verifying manifest: %v", err)
+	}
+
+	if len(publicKeys) == 0 {
+		t.Fatalf("no public keys found in signature")
+	}
+
+	var found bool
+	publicKey := env.pk.PublicKey()
+	// ensure that one of the extracted public keys matches the private key.
+	for _, candidate := range publicKeys {
+		if candidate.KeyID() == publicKey.KeyID() {
+			found = true
+			break
+		}
+	}
+
+	if !found {
+		t.Fatalf("expected public key, %v, not found in verified keys: %v", publicKey, publicKeys)
+	}
+
+	// Check that an invalid manifest fails verification
+	_, err = Verify(env.invalidSigned)
+	if err != nil {
+		t.Fatalf("Invalid manifest should not pass Verify()")
+	}
+}
+
+func genEnv(t *testing.T) *testEnv {
+	pk, err := libtrust.GenerateECP256PrivateKey()
+	if err != nil {
+		t.Fatalf("error generating test key: %v", err)
+	}
+
+	name, tag := "foo/bar", "test"
+
+	invalid := Manifest{
+		Versioned: SchemaVersion,
+		Name:      name,
+		Tag:       tag,
+		FSLayers: []FSLayer{
+			{
+				BlobSum: "asdf",
+			},
+			{
+				BlobSum: "qwer",
+			},
+		},
+	}
+
+	valid := Manifest{
+		Versioned: SchemaVersion,
+		Name:      name,
+		Tag:       tag,
+		FSLayers: []FSLayer{
+			{
+				BlobSum: "asdf",
+			},
+		},
+		History: []History{
+			{
+				V1Compatibility: "",
+			},
+		},
+	}
+
+	sm, err := Sign(&valid, pk)
+	if err != nil {
+		t.Fatalf("error signing manifest: %v", err)
+	}
+
+	invalidSigned, err := Sign(&invalid, pk)
+	if err != nil {
+		t.Fatalf("error signing manifest: %v", err)
+	}
+
+	return &testEnv{
+		name:          name,
+		tag:           tag,
+		invalidSigned: invalidSigned,
+		signed:        sm,
+		pk:            pk,
+	}
+}
diff --git a/manifest/schema1/reference_builder_test.go b/manifest/schema1/reference_builder_test.go
new file mode 100644
index 00000000..31fde646
--- /dev/null
+++ b/manifest/schema1/reference_builder_test.go
@@ -0,0 +1,108 @@
+package schema1
+
+import (
+	"testing"
+
+	"github.com/distribution/distribution/v3/context"
+	"github.com/distribution/distribution/v3/manifest"
+	"github.com/distribution/distribution/v3/reference"
+	"github.com/docker/libtrust"
+	"github.com/opencontainers/go-digest"
+)
+
+func makeSignedManifest(t *testing.T, pk libtrust.PrivateKey, refs []Reference) *SignedManifest {
+	u := &Manifest{
+		Versioned: manifest.Versioned{
+			SchemaVersion: 1,
+		},
+		Name:         "foo/bar",
+		Tag:          "latest",
+		Architecture: "amd64",
+	}
+
+	for i := len(refs) - 1; i >= 0; i-- {
+		u.FSLayers = append(u.FSLayers, FSLayer{
+			BlobSum: refs[i].Digest,
+		})
+		u.History = append(u.History, History{
+			V1Compatibility: refs[i].History.V1Compatibility,
+		})
+	}
+
+	signedManifest, err := Sign(u, pk)
+	if err != nil {
+		t.Fatalf("unexpected error signing manifest: %v", err)
+	}
+	return signedManifest
+}
+
+func TestReferenceBuilder(t *testing.T) {
+	pk, err := libtrust.GenerateECP256PrivateKey()
+	if err != nil {
+		t.Fatalf("unexpected error generating private key: %v", err)
+	}
+
+	r1 := Reference{
+		Digest:  "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+		Size:    1,
+		History: History{V1Compatibility: "{\"a\" : 1 }"},
+	}
+	r2 := Reference{
+		Digest:  "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+		Size:    2,
+		History: History{V1Compatibility: "{\"\a\" : 2 }"},
+	}
+
+	handCrafted := makeSignedManifest(t, pk, []Reference{r1, r2})
+
+	ref, err := reference.WithName(handCrafted.Manifest.Name)
+	if err != nil {
+		t.Fatalf("could not parse reference: %v", err)
+	}
+	ref, err = reference.WithTag(ref, handCrafted.Manifest.Tag)
+	if err != nil {
+		t.Fatalf("could not add tag: %v", err)
+	}
+
+	b := NewReferenceManifestBuilder(pk, ref, handCrafted.Manifest.Architecture)
+	_, err = b.Build(context.Background())
+	if err == nil {
+		t.Fatal("Expected error building zero length manifest")
+	}
+
+	err = b.AppendReference(r1)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = b.AppendReference(r2)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	refs := b.References()
+	if len(refs) != 2 {
+		t.Fatalf("Unexpected reference count : %d != %d", 2, len(refs))
+	}
+
+	// Ensure ordering
+	if refs[0].Digest != r2.Digest {
+		t.Fatalf("Unexpected reference : %v", refs[0])
+	}
+
+	m, err := b.Build(context.Background())
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	built, ok := m.(*SignedManifest)
+	if !ok {
+		t.Fatalf("unexpected type from Build() : %T", built)
+	}
+
+	d1 := digest.FromBytes(built.Canonical)
+	d2 := digest.FromBytes(handCrafted.Canonical)
+	if d1 != d2 {
+		t.Errorf("mismatching canonical JSON")
+	}
+}