diff --git a/configuration/configuration.go b/configuration/configuration.go
index 3dff32f84..62d914cd1 100644
--- a/configuration/configuration.go
+++ b/configuration/configuration.go
@@ -145,6 +145,21 @@ type Configuration struct {
Health Health `yaml:"health,omitempty"`
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
+ Schema1 struct {
+ // TrustKey is the signing key to use for adding the signature to
+ // schema1 manifests.
+ TrustKey string `yaml:"signingkeyfile,omitempty"`
+
+ // DisableSignatureStore will cause all signatures attached to schema1 manifests
+ // to be ignored. Signatures will be generated on all schema1 manifest requests
+ // rather than only requests which converted schema2 to schema1.
+ DisableSignatureStore bool `yaml:"disablesignaturestore,omitempty"`
+ } `yaml:"schema1,omitempty"`
+ } `yaml:"compatibility,omitempty"`
}
// LogHook is composed of hook Level and Type.
diff --git a/docs/configuration.md b/docs/configuration.md
index 89daaa200..ef01d2f6e 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -235,6 +235,10 @@ information about each option that appears later in this page.
remoteurl: https://registry-1.docker.io
username: [username]
password: [password]
+ compatibility:
+ schema1:
+ signingkeyfile: /etc/registry/key.json
+ disablesignaturestore: true
In some instances a configuration option is **optional** but it contains child
options marked as **required**. This indicates that you can omit the parent with
@@ -1732,6 +1736,55 @@ Proxy enables a registry to be configured as a pull through cache to the officia
To enable pulling private repositories (e.g. `batman/robin`) a username and password for user `batman` must be specified. Note: These private repositories will be stored in the proxy cache's storage and relevant measures should be taken to protect access to this.
+## Compatibility
+
+ compatibility:
+ schema1:
+ signingkeyfile: /etc/registry/key.json
+ disablesignaturestore: true
+
+Configure handling of older and deprecated features. Each subsection
+defines a such a feature with configurable behavior.
+
+### Schema1
+
+
+
+ Parameter |
+ Required |
+ Description |
+
+
+
+ signingkeyfile
+ |
+
+ no
+ |
+
+ The signing private key used for adding signatures to schema1 manifests.
+ If no signing key is provided, a new ECDSA key will be generated on
+ startup.
+ |
+
+
+
+ disablesignaturestore
+ |
+
+ no
+ |
+
+ Disables storage of signatures attached to schema1 manifests. By default
+ signatures are detached from schema1 manifests, stored, and reattached
+ when the manifest is requested. When this is true, the storage is disabled
+ and a new signature is always generated for schema1 manifests using the
+ schema1 signing key. Disabling signature storage will cause all newly
+ uploaded signatures to be discarded. Existing stored signatures will not
+ be removed but they will not be re-attached to the corresponding manifest.
+ |
+
+
## Example: Development configuration
diff --git a/manifest/schema1/manifest.go b/manifest/schema1/manifest.go
index 160f9cd99..bff47bde0 100644
--- a/manifest/schema1/manifest.go
+++ b/manifest/schema1/manifest.go
@@ -102,7 +102,7 @@ type SignedManifest struct {
Canonical []byte `json:"-"`
// all contains the byte representation of the Manifest including signatures
- // and is retuend by Payload()
+ // and is returned by Payload()
all []byte
}
diff --git a/registry/handlers/app.go b/registry/handlers/app.go
index ed925a45f..370f63ef2 100644
--- a/registry/handlers/app.go
+++ b/registry/handlers/app.go
@@ -155,11 +155,18 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App {
app.configureRedis(config)
app.configureLogHook(config)
- // Generate an ephemeral key to be used for signing converted manifests
- // for clients that don't support schema2.
- app.trustKey, err = libtrust.GenerateECP256PrivateKey()
- if err != nil {
- panic(err)
+ if config.Compatibility.Schema1.TrustKey != "" {
+ app.trustKey, err = libtrust.LoadKeyFile(config.Compatibility.Schema1.TrustKey)
+ if err != nil {
+ panic(fmt.Sprintf(`could not load schema1 "signingkey" parameter: %v`, err))
+ }
+ } else {
+ // Generate an ephemeral key to be used for signing converted manifests
+ // for clients that don't support schema2.
+ app.trustKey, err = libtrust.GenerateECP256PrivateKey()
+ if err != nil {
+ panic(err)
+ }
}
if config.HTTP.Host != "" {
@@ -176,6 +183,11 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App {
options = append(options, storage.DisableDigestResumption)
}
+ if config.Compatibility.Schema1.DisableSignatureStore {
+ options = append(options, storage.DisableSchema1Signatures)
+ options = append(options, storage.Schema1SigningKey(app.trustKey))
+ }
+
// configure deletion
if d, ok := config.Storage["delete"]; ok {
e, ok := d["enabled"]
diff --git a/registry/storage/manifeststore_test.go b/registry/storage/manifeststore_test.go
index 7885c4662..fcb5adf9a 100644
--- a/registry/storage/manifeststore_test.go
+++ b/registry/storage/manifeststore_test.go
@@ -28,11 +28,10 @@ type manifestStoreTestEnv struct {
tag string
}
-func newManifestStoreTestEnv(t *testing.T, name reference.Named, tag string) *manifestStoreTestEnv {
+func newManifestStoreTestEnv(t *testing.T, name reference.Named, tag string, options ...RegistryOption) *manifestStoreTestEnv {
ctx := context.Background()
driver := inmemory.New()
- registry, err := NewRegistry(ctx, driver, BlobDescriptorCacheProvider(
- memory.NewInMemoryBlobDescriptorCacheProvider()), EnableDelete, EnableRedirect)
+ registry, err := NewRegistry(ctx, driver, options...)
if err != nil {
t.Fatalf("error creating registry: %v", err)
}
@@ -53,13 +52,26 @@ func newManifestStoreTestEnv(t *testing.T, name reference.Named, tag string) *ma
}
func TestManifestStorage(t *testing.T) {
+ testManifestStorage(t, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableDelete, EnableRedirect)
+}
+
+func TestManifestStorageDisabledSignatures(t *testing.T) {
+ k, err := libtrust.GenerateECP256PrivateKey()
+ if err != nil {
+ t.Fatal(err)
+ }
+ testManifestStorage(t, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableDelete, EnableRedirect, DisableSchema1Signatures, Schema1SigningKey(k))
+}
+
+func testManifestStorage(t *testing.T, options ...RegistryOption) {
repoName, _ := reference.ParseNamed("foo/bar")
- env := newManifestStoreTestEnv(t, repoName, "thetag")
+ env := newManifestStoreTestEnv(t, repoName, "thetag", options...)
ctx := context.Background()
ms, err := env.repository.Manifests(ctx)
if err != nil {
t.Fatal(err)
}
+ equalSignatures := env.registry.(*registry).schema1SignaturesEnabled
m := schema1.Manifest{
Versioned: manifest.Versioned{
@@ -159,8 +171,14 @@ func TestManifestStorage(t *testing.T) {
t.Fatalf("unexpected manifest type from signedstore")
}
- if !reflect.DeepEqual(fetchedManifest, sm) {
- t.Fatalf("fetched manifest not equal: %#v != %#v", fetchedManifest, sm)
+ if !bytes.Equal(fetchedManifest.Canonical, sm.Canonical) {
+ t.Fatalf("fetched payload does not match original payload: %q != %q", fetchedManifest.Canonical, sm.Canonical)
+ }
+
+ if equalSignatures {
+ if !reflect.DeepEqual(fetchedManifest, sm) {
+ t.Fatalf("fetched manifest not equal: %#v != %#v", fetchedManifest.Manifest, sm.Manifest)
+ }
}
_, pl, err := fetchedManifest.Payload()
@@ -196,8 +214,19 @@ func TestManifestStorage(t *testing.T) {
t.Fatalf("unexpected error fetching manifest by digest: %v", err)
}
- if !reflect.DeepEqual(fetchedByDigest, fetchedManifest) {
- t.Fatalf("fetched manifest not equal: %#v != %#v", fetchedByDigest, fetchedManifest)
+ byDigestManifest, ok := fetchedByDigest.(*schema1.SignedManifest)
+ if !ok {
+ t.Fatalf("unexpected manifest type from signedstore")
+ }
+
+ if !bytes.Equal(byDigestManifest.Canonical, fetchedManifest.Canonical) {
+ t.Fatalf("fetched manifest not equal: %q != %q", byDigestManifest.Canonical, fetchedManifest.Canonical)
+ }
+
+ if equalSignatures {
+ if !reflect.DeepEqual(fetchedByDigest, fetchedManifest) {
+ t.Fatalf("fetched manifest not equal: %#v != %#v", fetchedByDigest, fetchedManifest)
+ }
}
sigs, err := fetchedJWS.Signatures()
@@ -286,14 +315,16 @@ func TestManifestStorage(t *testing.T) {
t.Fatalf("payloads are not equal")
}
- receivedSigs, err := receivedJWS.Signatures()
- if err != nil {
- t.Fatalf("error getting signatures: %v", err)
- }
+ if equalSignatures {
+ receivedSigs, err := receivedJWS.Signatures()
+ if err != nil {
+ t.Fatalf("error getting signatures: %v", err)
+ }
- for i, sig := range receivedSigs {
- if !bytes.Equal(sig, expectedSigs[i]) {
- t.Fatalf("mismatched signatures from remote: %v != %v", string(sig), string(expectedSigs[i]))
+ for i, sig := range receivedSigs {
+ if !bytes.Equal(sig, expectedSigs[i]) {
+ t.Fatalf("mismatched signatures from remote: %v != %v", string(sig), string(expectedSigs[i]))
+ }
}
}
diff --git a/registry/storage/registry.go b/registry/storage/registry.go
index 1870e698a..9c74ebbc7 100644
--- a/registry/storage/registry.go
+++ b/registry/storage/registry.go
@@ -6,6 +6,7 @@ import (
"github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/storage/cache"
storagedriver "github.com/docker/distribution/registry/storage/driver"
+ "github.com/docker/libtrust"
)
// registry is the top-level implementation of Registry for use in the storage
@@ -17,6 +18,8 @@ type registry struct {
blobDescriptorCacheProvider cache.BlobDescriptorCacheProvider
deleteEnabled bool
resumableDigestEnabled bool
+ schema1SignaturesEnabled bool
+ schema1SigningKey libtrust.PrivateKey
}
// RegistryOption is the type used for functional options for NewRegistry.
@@ -43,6 +46,24 @@ func DisableDigestResumption(registry *registry) error {
return nil
}
+// DisableSchema1Signatures is a functional option for NewRegistry. It disables
+// signature storage and ensures all schema1 manifests will only be returned
+// with a signature from a provided signing key.
+func DisableSchema1Signatures(registry *registry) error {
+ registry.schema1SignaturesEnabled = false
+ return nil
+}
+
+// Schema1SigningKey returns a functional option for NewRegistry. It sets the
+// signing key for adding a signature to all schema1 manifests. This should be
+// used in conjunction with disabling signature store.
+func Schema1SigningKey(key libtrust.PrivateKey) RegistryOption {
+ return func(registry *registry) error {
+ registry.schema1SigningKey = key
+ return nil
+ }
+}
+
// BlobDescriptorCacheProvider returns a functional option for
// NewRegistry. It creates a cached blob statter for use by the
// registry.
@@ -85,8 +106,9 @@ func NewRegistry(ctx context.Context, driver storagedriver.StorageDriver, option
statter: statter,
pathFn: bs.path,
},
- statter: statter,
- resumableDigestEnabled: true,
+ statter: statter,
+ resumableDigestEnabled: true,
+ schema1SignaturesEnabled: true,
}
for _, option := range options {
diff --git a/registry/storage/signedmanifesthandler.go b/registry/storage/signedmanifesthandler.go
index 026632268..8e13dd932 100644
--- a/registry/storage/signedmanifesthandler.go
+++ b/registry/storage/signedmanifesthandler.go
@@ -25,10 +25,17 @@ var _ ManifestHandler = &signedManifestHandler{}
func (ms *signedManifestHandler) Unmarshal(ctx context.Context, dgst digest.Digest, content []byte) (distribution.Manifest, error) {
context.GetLogger(ms.ctx).Debug("(*signedManifestHandler).Unmarshal")
- // Fetch the signatures for the manifest
- signatures, err := ms.signatures.Get(dgst)
- if err != nil {
- return nil, err
+
+ var (
+ signatures [][]byte
+ err error
+ )
+ if ms.repository.schema1SignaturesEnabled {
+ // Fetch the signatures for the manifest
+ signatures, err = ms.signatures.Get(dgst)
+ if err != nil {
+ return nil, err
+ }
}
jsig, err := libtrust.NewJSONSignature(content, signatures...)
@@ -36,6 +43,14 @@ func (ms *signedManifestHandler) Unmarshal(ctx context.Context, dgst digest.Dige
return nil, err
}
+ if ms.repository.schema1SigningKey != nil {
+ if err := jsig.Sign(ms.repository.schema1SigningKey); err != nil {
+ return nil, err
+ }
+ } else if !ms.repository.schema1SignaturesEnabled {
+ return nil, fmt.Errorf("missing signing key with signature store disabled")
+ }
+
// Extract the pretty JWS
raw, err := jsig.PrettySignature("signatures")
if err != nil {
@@ -75,14 +90,16 @@ func (ms *signedManifestHandler) Put(ctx context.Context, manifest distribution.
return "", err
}
- // Grab each json signature and store them.
- signatures, err := sm.Signatures()
- if err != nil {
- return "", err
- }
+ if ms.repository.schema1SignaturesEnabled {
+ // Grab each json signature and store them.
+ signatures, err := sm.Signatures()
+ if err != nil {
+ return "", err
+ }
- if err := ms.signatures.Put(revision.Digest, signatures...); err != nil {
- return "", err
+ if err := ms.signatures.Put(revision.Digest, signatures...); err != nil {
+ return "", err
+ }
}
return revision.Digest, nil