Merge pull request #1420 from dmcgowan/configurable-trust-key
Add option to disable signatures
This commit is contained in:
commit
a3213ff331
@ -145,6 +145,21 @@ type Configuration struct {
|
|||||||
Health Health `yaml:"health,omitempty"`
|
Health Health `yaml:"health,omitempty"`
|
||||||
|
|
||||||
Proxy Proxy `yaml:"proxy,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.
|
// 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
|
remoteurl: https://registry-1.docker.io
|
||||||
username: [username]
|
username: [username]
|
||||||
password: [password]
|
password: [password]
|
||||||
|
compatibility:
|
||||||
|
schema1:
|
||||||
|
signingkeyfile: /etc/registry/key.json
|
||||||
|
disablesignaturestore: true
|
||||||
|
|
||||||
In some instances a configuration option is **optional** but it contains child
|
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
|
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.
|
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
|
## Example: Development configuration
|
||||||
|
|
||||||
|
@ -102,7 +102,7 @@ type SignedManifest struct {
|
|||||||
Canonical []byte `json:"-"`
|
Canonical []byte `json:"-"`
|
||||||
|
|
||||||
// all contains the byte representation of the Manifest including signatures
|
// all contains the byte representation of the Manifest including signatures
|
||||||
// and is retuend by Payload()
|
// and is returned by Payload()
|
||||||
all []byte
|
all []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,11 +155,18 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App {
|
|||||||
app.configureRedis(config)
|
app.configureRedis(config)
|
||||||
app.configureLogHook(config)
|
app.configureLogHook(config)
|
||||||
|
|
||||||
// Generate an ephemeral key to be used for signing converted manifests
|
if config.Compatibility.Schema1.TrustKey != "" {
|
||||||
// for clients that don't support schema2.
|
app.trustKey, err = libtrust.LoadKeyFile(config.Compatibility.Schema1.TrustKey)
|
||||||
app.trustKey, err = libtrust.GenerateECP256PrivateKey()
|
if err != nil {
|
||||||
if err != nil {
|
panic(fmt.Sprintf(`could not load schema1 "signingkey" parameter: %v`, err))
|
||||||
panic(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 != "" {
|
if config.HTTP.Host != "" {
|
||||||
@ -176,6 +183,11 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App {
|
|||||||
options = append(options, storage.DisableDigestResumption)
|
options = append(options, storage.DisableDigestResumption)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.Compatibility.Schema1.DisableSignatureStore {
|
||||||
|
options = append(options, storage.DisableSchema1Signatures)
|
||||||
|
options = append(options, storage.Schema1SigningKey(app.trustKey))
|
||||||
|
}
|
||||||
|
|
||||||
// configure deletion
|
// configure deletion
|
||||||
if d, ok := config.Storage["delete"]; ok {
|
if d, ok := config.Storage["delete"]; ok {
|
||||||
e, ok := d["enabled"]
|
e, ok := d["enabled"]
|
||||||
|
@ -28,11 +28,10 @@ type manifestStoreTestEnv struct {
|
|||||||
tag string
|
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()
|
ctx := context.Background()
|
||||||
driver := inmemory.New()
|
driver := inmemory.New()
|
||||||
registry, err := NewRegistry(ctx, driver, BlobDescriptorCacheProvider(
|
registry, err := NewRegistry(ctx, driver, options...)
|
||||||
memory.NewInMemoryBlobDescriptorCacheProvider()), EnableDelete, EnableRedirect)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error creating registry: %v", err)
|
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) {
|
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")
|
repoName, _ := reference.ParseNamed("foo/bar")
|
||||||
env := newManifestStoreTestEnv(t, repoName, "thetag")
|
env := newManifestStoreTestEnv(t, repoName, "thetag", options...)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ms, err := env.repository.Manifests(ctx)
|
ms, err := env.repository.Manifests(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
equalSignatures := env.registry.(*registry).schema1SignaturesEnabled
|
||||||
|
|
||||||
m := schema1.Manifest{
|
m := schema1.Manifest{
|
||||||
Versioned: manifest.Versioned{
|
Versioned: manifest.Versioned{
|
||||||
@ -159,8 +171,14 @@ func TestManifestStorage(t *testing.T) {
|
|||||||
t.Fatalf("unexpected manifest type from signedstore")
|
t.Fatalf("unexpected manifest type from signedstore")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(fetchedManifest, sm) {
|
if !bytes.Equal(fetchedManifest.Canonical, sm.Canonical) {
|
||||||
t.Fatalf("fetched manifest not equal: %#v != %#v", fetchedManifest, sm)
|
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()
|
_, pl, err := fetchedManifest.Payload()
|
||||||
@ -196,8 +214,19 @@ func TestManifestStorage(t *testing.T) {
|
|||||||
t.Fatalf("unexpected error fetching manifest by digest: %v", err)
|
t.Fatalf("unexpected error fetching manifest by digest: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(fetchedByDigest, fetchedManifest) {
|
byDigestManifest, ok := fetchedByDigest.(*schema1.SignedManifest)
|
||||||
t.Fatalf("fetched manifest not equal: %#v != %#v", fetchedByDigest, fetchedManifest)
|
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()
|
sigs, err := fetchedJWS.Signatures()
|
||||||
@ -286,14 +315,16 @@ func TestManifestStorage(t *testing.T) {
|
|||||||
t.Fatalf("payloads are not equal")
|
t.Fatalf("payloads are not equal")
|
||||||
}
|
}
|
||||||
|
|
||||||
receivedSigs, err := receivedJWS.Signatures()
|
if equalSignatures {
|
||||||
if err != nil {
|
receivedSigs, err := receivedJWS.Signatures()
|
||||||
t.Fatalf("error getting signatures: %v", err)
|
if err != nil {
|
||||||
}
|
t.Fatalf("error getting signatures: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
for i, sig := range receivedSigs {
|
for i, sig := range receivedSigs {
|
||||||
if !bytes.Equal(sig, expectedSigs[i]) {
|
if !bytes.Equal(sig, expectedSigs[i]) {
|
||||||
t.Fatalf("mismatched signatures from remote: %v != %v", string(sig), string(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/reference"
|
||||||
"github.com/docker/distribution/registry/storage/cache"
|
"github.com/docker/distribution/registry/storage/cache"
|
||||||
storagedriver "github.com/docker/distribution/registry/storage/driver"
|
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
|
// registry is the top-level implementation of Registry for use in the storage
|
||||||
@ -17,6 +18,8 @@ type registry struct {
|
|||||||
blobDescriptorCacheProvider cache.BlobDescriptorCacheProvider
|
blobDescriptorCacheProvider cache.BlobDescriptorCacheProvider
|
||||||
deleteEnabled bool
|
deleteEnabled bool
|
||||||
resumableDigestEnabled bool
|
resumableDigestEnabled bool
|
||||||
|
schema1SignaturesEnabled bool
|
||||||
|
schema1SigningKey libtrust.PrivateKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegistryOption is the type used for functional options for NewRegistry.
|
// RegistryOption is the type used for functional options for NewRegistry.
|
||||||
@ -43,6 +46,24 @@ func DisableDigestResumption(registry *registry) error {
|
|||||||
return nil
|
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
|
// BlobDescriptorCacheProvider returns a functional option for
|
||||||
// NewRegistry. It creates a cached blob statter for use by the
|
// NewRegistry. It creates a cached blob statter for use by the
|
||||||
// registry.
|
// registry.
|
||||||
@ -85,8 +106,9 @@ func NewRegistry(ctx context.Context, driver storagedriver.StorageDriver, option
|
|||||||
statter: statter,
|
statter: statter,
|
||||||
pathFn: bs.path,
|
pathFn: bs.path,
|
||||||
},
|
},
|
||||||
statter: statter,
|
statter: statter,
|
||||||
resumableDigestEnabled: true,
|
resumableDigestEnabled: true,
|
||||||
|
schema1SignaturesEnabled: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, option := range options {
|
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) {
|
func (ms *signedManifestHandler) Unmarshal(ctx context.Context, dgst digest.Digest, content []byte) (distribution.Manifest, error) {
|
||||||
context.GetLogger(ms.ctx).Debug("(*signedManifestHandler).Unmarshal")
|
context.GetLogger(ms.ctx).Debug("(*signedManifestHandler).Unmarshal")
|
||||||
// Fetch the signatures for the manifest
|
|
||||||
signatures, err := ms.signatures.Get(dgst)
|
var (
|
||||||
if err != nil {
|
signatures [][]byte
|
||||||
return nil, err
|
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...)
|
jsig, err := libtrust.NewJSONSignature(content, signatures...)
|
||||||
@ -36,6 +43,14 @@ func (ms *signedManifestHandler) Unmarshal(ctx context.Context, dgst digest.Dige
|
|||||||
return nil, err
|
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
|
// Extract the pretty JWS
|
||||||
raw, err := jsig.PrettySignature("signatures")
|
raw, err := jsig.PrettySignature("signatures")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -75,14 +90,16 @@ func (ms *signedManifestHandler) Put(ctx context.Context, manifest distribution.
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab each json signature and store them.
|
if ms.repository.schema1SignaturesEnabled {
|
||||||
signatures, err := sm.Signatures()
|
// Grab each json signature and store them.
|
||||||
if err != nil {
|
signatures, err := sm.Signatures()
|
||||||
return "", err
|
if err != nil {
|
||||||
}
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
if err := ms.signatures.Put(revision.Digest, signatures...); err != nil {
|
if err := ms.signatures.Put(revision.Digest, signatures...); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return revision.Digest, nil
|
return revision.Digest, nil
|
||||||
|
Loading…
Reference in New Issue
Block a user