Merge pull request #1420 from dmcgowan/configurable-trust-key
Add option to disable signaturesmaster
						commit
						a3213ff331
					
				|  | @ -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.
 | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
| <table> | ||||
|   <tr> | ||||
|     <th>Parameter</th> | ||||
|     <th>Required</th> | ||||
|     <th>Description</th> | ||||
|   </tr> | ||||
|   <tr> | ||||
|     <td> | ||||
|       <code>signingkeyfile</code> | ||||
|     </td> | ||||
|     <td> | ||||
|       no | ||||
|     </td> | ||||
|     <td> | ||||
|      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. | ||||
|     </td> | ||||
|   </tr> | ||||
|   <tr> | ||||
|     <td> | ||||
|       <code>disablesignaturestore</code> | ||||
|     </td> | ||||
|     <td> | ||||
|       no | ||||
|     </td> | ||||
|     <td> | ||||
|      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. | ||||
|     </td> | ||||
|   </tr> | ||||
| </table> | ||||
| 
 | ||||
| ## Example: Development configuration | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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"] | ||||
|  |  | |||
|  | @ -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])) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue