Merge pull request #1420 from dmcgowan/configurable-trust-key

Add option to disable signatures
This commit is contained in:
Aaron Lehmann 2016-02-10 16:15:59 -08:00
commit a3213ff331
7 changed files with 184 additions and 34 deletions

View file

@ -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.

View file

@ -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

View file

@ -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
} }

View file

@ -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"]

View file

@ -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]))
}
} }
} }

View file

@ -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 {

View file

@ -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