diff --git a/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/aes/aes.go b/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/aes/aes.go new file mode 100644 index 00000000000..8a180eb3c4b --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/aes/aes.go @@ -0,0 +1,80 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package aes transforms values for storage at rest using AES-GCM. +package aes + +import ( + "crypto/cipher" + "crypto/rand" + "fmt" + + "k8s.io/apiserver/pkg/storage/value" +) + +// gcm implements AEAD encryption of the provided values given a cipher.Block algorithm. +// The authenticated data provided as part of the value.Context method must match when the same +// value is set to and loaded from storage. In order to ensure that values cannot be copied by +// an attacker from a location under their control, use characteristics of the storage location +// (such as the etcd key) as part of the authenticated data. +// +// Because this mode requires a generated IV and IV reuse is a known weakness of AES-GCM, keys +// must be rotated before a birthday attack becomes feasible. NIST SP 800-38D +// (http://csrc.nist.gov/publications/nistpubs/800-38D/SP-800-38D.pdf) recommends using the same +// key with random 96-bit nonces (the default nonce length) no more than 2^32 times, and +// therefore transformers using this implementation *must* ensure they allow for frequent key +// rotation. Future work should include investigation of AES-GCM-SIV as an alternative to +// random nonces. +type gcm struct { + block cipher.Block +} + +// NewGCMTransformer takes the given block cipher and performs encryption and decryption on the given +// data. +func NewGCMTransformer(block cipher.Block) value.Transformer { + return &gcm{block: block} +} + +func (t *gcm) TransformFromStorage(data []byte, context value.Context) ([]byte, bool, error) { + aead, err := cipher.NewGCM(t.block) + if err != nil { + return nil, false, err + } + nonceSize := aead.NonceSize() + if len(data) < nonceSize { + return nil, false, fmt.Errorf("the stored data was shorter than the required size") + } + result, err := aead.Open(nil, data[:nonceSize], data[nonceSize:], context.AuthenticatedData()) + return result, false, err +} + +func (t *gcm) TransformToStorage(data []byte, context value.Context) ([]byte, error) { + aead, err := cipher.NewGCM(t.block) + if err != nil { + return nil, err + } + nonceSize := aead.NonceSize() + result := make([]byte, nonceSize+aead.Overhead()+len(data)) + n, err := rand.Read(result[:nonceSize]) + if err != nil { + return nil, err + } + if n != nonceSize { + return nil, fmt.Errorf("unable to read sufficient random bytes") + } + cipherText := aead.Seal(result[nonceSize:nonceSize], result[:nonceSize], data, context.AuthenticatedData()) + return result[:nonceSize+len(cipherText)], nil +} diff --git a/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/aes/aes_test.go b/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/aes/aes_test.go new file mode 100644 index 00000000000..ea2ea3936f1 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/aes/aes_test.go @@ -0,0 +1,172 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package aes + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "fmt" + "testing" + + "k8s.io/apiserver/pkg/storage/value" +) + +func TestGCMDataStable(t *testing.T) { + block, err := aes.NewCipher([]byte("0123456789abcdef")) + if err != nil { + t.Fatal(err) + } + aead, err := cipher.NewGCM(block) + if err != nil { + t.Fatal(err) + } + // IMPORTANT: If you must fix this test, then all previously encrypted data from previously compiled versions is broken unless you hardcode the nonce size to 12 + if aead.NonceSize() != 12 { + t.Fatalf("The underlying Golang crypto size has changed, old version of AES on disk will not be readable unless the AES implementation is changed to hardcode nonce size.") + } +} + +func TestGCMKeyRotation(t *testing.T) { + testErr := fmt.Errorf("test error") + block1, err := aes.NewCipher([]byte("abcdefghijklmnop")) + if err != nil { + t.Fatal(err) + } + block2, err := aes.NewCipher([]byte("0123456789abcdef")) + if err != nil { + t.Fatal(err) + } + + context := value.DefaultContext([]byte("authenticated_data")) + + p := value.NewPrefixTransformers(testErr, + value.PrefixTransformer{Prefix: []byte("first:"), Transformer: NewGCMTransformer(block1)}, + value.PrefixTransformer{Prefix: []byte("second:"), Transformer: NewGCMTransformer(block2)}, + ) + out, err := p.TransformToStorage([]byte("firstvalue"), context) + if err != nil { + t.Fatal(err) + } + if !bytes.HasPrefix(out, []byte("first:")) { + t.Fatalf("unexpected prefix: %q", out) + } + from, stale, err := p.TransformFromStorage(out, context) + if err != nil { + t.Fatal(err) + } + if stale || !bytes.Equal([]byte("firstvalue"), from) { + t.Fatalf("unexpected data: %t %q", stale, from) + } + + // verify changing the context fails storage + from, stale, err = p.TransformFromStorage(out, value.DefaultContext([]byte("incorrect_context"))) + if err == nil { + t.Fatalf("expected unauthenticated data") + } + + // reverse the order, use the second key + p = value.NewPrefixTransformers(testErr, + value.PrefixTransformer{Prefix: []byte("second:"), Transformer: NewGCMTransformer(block2)}, + value.PrefixTransformer{Prefix: []byte("first:"), Transformer: NewGCMTransformer(block1)}, + ) + from, stale, err = p.TransformFromStorage(out, context) + if err != nil { + t.Fatal(err) + } + if !stale || !bytes.Equal([]byte("firstvalue"), from) { + t.Fatalf("unexpected data: %t %q", stale, from) + } +} + +func BenchmarkGCMRead_16_1024(b *testing.B) { benchmarkGCMRead(b, 16, 1024, false) } +func BenchmarkGCMRead_32_1024(b *testing.B) { benchmarkGCMRead(b, 32, 1024, false) } +func BenchmarkGCMRead_32_16384(b *testing.B) { benchmarkGCMRead(b, 32, 16384, false) } +func BenchmarkGCMRead_32_16384_Stale(b *testing.B) { benchmarkGCMRead(b, 32, 16384, true) } + +func BenchmarkGCMWrite_16_1024(b *testing.B) { benchmarkGCMWrite(b, 16, 1024) } +func BenchmarkGCMWrite_32_1024(b *testing.B) { benchmarkGCMWrite(b, 32, 1024) } +func BenchmarkGCMWrite_32_16384(b *testing.B) { benchmarkGCMWrite(b, 32, 16384) } + +func benchmarkGCMRead(b *testing.B, keyLength int, valueLength int, stale bool) { + block1, err := aes.NewCipher(bytes.Repeat([]byte("a"), keyLength)) + if err != nil { + b.Fatal(err) + } + block2, err := aes.NewCipher(bytes.Repeat([]byte("b"), keyLength)) + if err != nil { + b.Fatal(err) + } + p := value.NewPrefixTransformers(nil, + value.PrefixTransformer{Prefix: []byte("first:"), Transformer: NewGCMTransformer(block1)}, + value.PrefixTransformer{Prefix: []byte("second:"), Transformer: NewGCMTransformer(block2)}, + ) + + context := value.DefaultContext([]byte("authenticated_data")) + v := bytes.Repeat([]byte("0123456789abcdef"), valueLength/16) + + out, err := p.TransformToStorage(v, context) + if err != nil { + b.Fatal(err) + } + // reverse the key order if stale + if stale { + p = value.NewPrefixTransformers(nil, + value.PrefixTransformer{Prefix: []byte("first:"), Transformer: NewGCMTransformer(block1)}, + value.PrefixTransformer{Prefix: []byte("second:"), Transformer: NewGCMTransformer(block2)}, + ) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + from, stale, err := p.TransformFromStorage(out, context) + if err != nil { + b.Fatal(err) + } + if stale { + b.Fatalf("unexpected data: %t %q", stale, from) + } + } + b.StopTimer() +} + +func benchmarkGCMWrite(b *testing.B, keyLength int, valueLength int) { + block1, err := aes.NewCipher(bytes.Repeat([]byte("a"), keyLength)) + if err != nil { + b.Fatal(err) + } + block2, err := aes.NewCipher(bytes.Repeat([]byte("b"), keyLength)) + if err != nil { + b.Fatal(err) + } + p := value.NewPrefixTransformers(nil, + value.PrefixTransformer{Prefix: []byte("first:"), Transformer: NewGCMTransformer(block1)}, + value.PrefixTransformer{Prefix: []byte("second:"), Transformer: NewGCMTransformer(block2)}, + ) + + context := value.DefaultContext([]byte("authenticated_data")) + v := bytes.Repeat([]byte("0123456789abcdef"), valueLength/16) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := p.TransformToStorage(v, context) + if err != nil { + b.Fatal(err) + } + } + b.StopTimer() +} diff --git a/staging/src/k8s.io/apiserver/pkg/storage/value/transformer.go b/staging/src/k8s.io/apiserver/pkg/storage/value/transformer.go new file mode 100644 index 00000000000..ab5d4af959b --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/storage/value/transformer.go @@ -0,0 +1,146 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package value contains methods for assisting with transformation of values in storage. +package value + +import ( + "bytes" + "fmt" + "sync" +) + +// Context is additional information that a storage transformation may need to verify the data at rest. +type Context interface { + // AuthenticatedData should return an array of bytes that describes the current value. If the value changes, + // the transformer may report the value as unreadable or tampered. This may be nil if no such description exists + // or is needed. For additional verification, set this to data that strongly identifies the value, such as + // the key and creation version of the stored data. + AuthenticatedData() []byte +} + +// Transformer allows a value to be transformed before being read from or written to the underlying store. The methods +// must be able to undo the transformation caused by the other. +type Transformer interface { + // TransformFromStorage may transform the provided data from its underlying storage representation or return an error. + // Stale is true if the object on disk is stale and a write to etcd should be issued, even if the contents of the object + // have not changed. + TransformFromStorage(data []byte, context Context) (out []byte, stale bool, err error) + // TransformToStorage may transform the provided data into the appropriate form in storage or return an error. + TransformToStorage(data []byte, context Context) (out []byte, err error) +} + +type identityTransformer struct{} + +// IdentityTransformer performs no transformation of the provided data. +var IdentityTransformer Transformer = identityTransformer{} + +func (identityTransformer) TransformFromStorage(b []byte, ctx Context) ([]byte, bool, error) { + return b, false, nil +} +func (identityTransformer) TransformToStorage(b []byte, ctx Context) ([]byte, error) { + return b, nil +} + +// DefaultContext is a simple implementation of Context for a slice of bytes. +type DefaultContext []byte + +// AuthenticatedData returns itself. +func (c DefaultContext) AuthenticatedData() []byte { return []byte(c) } + +// MutableTransformer allows a transformer to be changed safely at runtime. +type MutableTransformer struct { + lock sync.RWMutex + transformer Transformer +} + +// NewMutableTransformer creates a transformer that can be updated at any time by calling Set() +func NewMutableTransformer(transformer Transformer) *MutableTransformer { + return &MutableTransformer{transformer: transformer} +} + +// Set updates the nested transformer. +func (t *MutableTransformer) Set(transformer Transformer) { + t.lock.Lock() + t.transformer = transformer + t.lock.Unlock() +} + +func (t *MutableTransformer) TransformFromStorage(data []byte, context Context) (out []byte, stale bool, err error) { + t.lock.RLock() + transformer := t.transformer + t.lock.RUnlock() + return transformer.TransformFromStorage(data, context) +} +func (t *MutableTransformer) TransformToStorage(data []byte, context Context) (out []byte, err error) { + t.lock.RLock() + transformer := t.transformer + t.lock.RUnlock() + return transformer.TransformToStorage(data, context) +} + +// PrefixTransformer holds a transformer interface and the prefix that the transformation is located under. +type PrefixTransformer struct { + Prefix []byte + Transformer Transformer +} + +type prefixTransformers struct { + transformers []PrefixTransformer + err error +} + +var _ Transformer = &prefixTransformers{} + +// NewPrefixTransformers supports the Transformer interface by checking the incoming data against the provided +// prefixes in order. The first matching prefix will be used to transform the value (the prefix is stripped +// before the Transformer interface is invoked). The first provided transformer will be used when writing to +// the store. +func NewPrefixTransformers(err error, transformers ...PrefixTransformer) Transformer { + if err == nil { + err = fmt.Errorf("the provided value does not match any of the supported transformers") + } + return &prefixTransformers{ + transformers: transformers, + err: err, + } +} + +// TransformFromStorage finds the first transformer with a prefix matching the provided data and returns +// the result of transforming the value. It will always mark any transformation as stale that is not using +// the first transformer. +func (t *prefixTransformers) TransformFromStorage(data []byte, context Context) ([]byte, bool, error) { + for i, transformer := range t.transformers { + if bytes.HasPrefix(data, transformer.Prefix) { + result, stale, err := transformer.Transformer.TransformFromStorage(data[len(transformer.Prefix):], context) + return result, stale || i != 0, err + } + } + return nil, false, t.err +} + +// TransformToStorage uses the first transformer and adds its prefix to the data. +func (t *prefixTransformers) TransformToStorage(data []byte, context Context) ([]byte, error) { + transformer := t.transformers[0] + prefixedData := make([]byte, len(transformer.Prefix), len(data)+len(transformer.Prefix)) + copy(prefixedData, transformer.Prefix) + result, err := transformer.Transformer.TransformToStorage(data, context) + if err != nil { + return nil, err + } + prefixedData = append(prefixedData, result...) + return prefixedData, nil +} diff --git a/staging/src/k8s.io/apiserver/pkg/storage/value/transformer_test.go b/staging/src/k8s.io/apiserver/pkg/storage/value/transformer_test.go new file mode 100644 index 00000000000..63b9e0a7543 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/storage/value/transformer_test.go @@ -0,0 +1,101 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package value + +import ( + "bytes" + "fmt" + "testing" +) + +type testTransformer struct { + from, to []byte + err error + stale bool + receivedFrom, receivedTo []byte +} + +func (t *testTransformer) TransformFromStorage(from []byte, context Context) (data []byte, stale bool, err error) { + t.receivedFrom = from + return t.from, t.stale, t.err +} + +func (t *testTransformer) TransformToStorage(to []byte, context Context) (data []byte, err error) { + t.receivedTo = to + return t.to, t.err +} + +func TestPrefixFrom(t *testing.T) { + testErr := fmt.Errorf("test error") + transformErr := fmt.Errorf("test error") + transformers := []PrefixTransformer{ + {Prefix: []byte("first:"), Transformer: &testTransformer{from: []byte("value1")}}, + {Prefix: []byte("second:"), Transformer: &testTransformer{from: []byte("value2")}}, + {Prefix: []byte("fails:"), Transformer: &testTransformer{err: transformErr}}, + {Prefix: []byte("stale:"), Transformer: &testTransformer{from: []byte("value3"), stale: true}}, + } + p := NewPrefixTransformers(testErr, transformers...) + + testCases := []struct { + input []byte + expect []byte + stale bool + err error + match int + }{ + {[]byte("first:value"), []byte("value1"), false, nil, 0}, + {[]byte("second:value"), []byte("value2"), true, nil, 1}, + {[]byte("third:value"), nil, false, testErr, -1}, + {[]byte("fails:value"), nil, true, transformErr, 2}, + {[]byte("stale:value"), []byte("value3"), true, nil, 3}, + } + for i, test := range testCases { + got, stale, err := p.TransformFromStorage(test.input, nil) + if err != test.err || stale != test.stale || !bytes.Equal(got, test.expect) { + t.Errorf("%d: unexpected out: %q %t %#v", i, string(got), stale, err) + continue + } + if test.match != -1 && !bytes.Equal([]byte("value"), transformers[test.match].Transformer.(*testTransformer).receivedFrom) { + t.Errorf("%d: unexpected value received by transformer: %s", i, transformers[test.match].Transformer.(*testTransformer).receivedFrom) + } + } +} + +func TestPrefixTo(t *testing.T) { + testErr := fmt.Errorf("test error") + transformErr := fmt.Errorf("test error") + testCases := []struct { + transformers []PrefixTransformer + expect []byte + err error + }{ + {[]PrefixTransformer{{Prefix: []byte("first:"), Transformer: &testTransformer{to: []byte("value1")}}}, []byte("first:value1"), nil}, + {[]PrefixTransformer{{Prefix: []byte("second:"), Transformer: &testTransformer{to: []byte("value2")}}}, []byte("second:value2"), nil}, + {[]PrefixTransformer{{Prefix: []byte("fails:"), Transformer: &testTransformer{err: transformErr}}}, nil, transformErr}, + } + for i, test := range testCases { + p := NewPrefixTransformers(testErr, test.transformers...) + got, err := p.TransformToStorage([]byte("value"), nil) + if err != test.err || !bytes.Equal(got, test.expect) { + t.Errorf("%d: unexpected out: %q %#v", i, string(got), err) + continue + } + if !bytes.Equal([]byte("value"), test.transformers[0].Transformer.(*testTransformer).receivedTo) { + t.Errorf("%d: unexpected value received by transformer: %s", i, test.transformers[0].Transformer.(*testTransformer).receivedTo) + } + } +}