crypto: add a basic KMS implementation (#6161)
This commit adds a basic KMS implementation for an operator-specified SSE-S3 master key. The master key is wrapped as KMS such that using SSE-S3 with master key and SSE-S3 with KMS can use the same code. Bindings for a remote / true KMS (like hashicorp vault) will be added later on.master
parent
0c9f4c9092
commit
6c93c60424
@ -0,0 +1,139 @@ |
|||||||
|
// Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
|
||||||
|
//
|
||||||
|
// 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 crypto |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"context" |
||||||
|
"crypto/hmac" |
||||||
|
"crypto/rand" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"sort" |
||||||
|
|
||||||
|
"github.com/minio/minio/cmd/logger" |
||||||
|
sha256 "github.com/minio/sha256-simd" |
||||||
|
"github.com/minio/sio" |
||||||
|
) |
||||||
|
|
||||||
|
// Context is a list of key-value pairs cryptographically
|
||||||
|
// associated with a certain object.
|
||||||
|
type Context map[string]string |
||||||
|
|
||||||
|
// WriteTo writes the context in a canonical from to w.
|
||||||
|
// It returns the number of bytes and the first error
|
||||||
|
// encounter during writing to w, if any.
|
||||||
|
//
|
||||||
|
// WriteTo sorts the context keys and writes the sorted
|
||||||
|
// key-value pairs as canonical JSON object to w.
|
||||||
|
func (c Context) WriteTo(w io.Writer) (n int64, err error) { |
||||||
|
sortedKeys := make(sort.StringSlice, 0, len(c)) |
||||||
|
for k := range c { |
||||||
|
sortedKeys = append(sortedKeys, k) |
||||||
|
} |
||||||
|
sort.Sort(sortedKeys) |
||||||
|
|
||||||
|
nn, err := io.WriteString(w, "{") |
||||||
|
if err != nil { |
||||||
|
return n + int64(nn), err |
||||||
|
} |
||||||
|
n += int64(nn) |
||||||
|
for i, k := range sortedKeys { |
||||||
|
s := fmt.Sprintf("\"%s\":\"%s\",", k, c[k]) |
||||||
|
if i == len(sortedKeys)-1 { |
||||||
|
s = s[:len(s)-1] // remove last ','
|
||||||
|
} |
||||||
|
|
||||||
|
nn, err = io.WriteString(w, s) |
||||||
|
if err != nil { |
||||||
|
return n + int64(nn), err |
||||||
|
} |
||||||
|
n += int64(nn) |
||||||
|
} |
||||||
|
nn, err = io.WriteString(w, "}") |
||||||
|
return n + int64(nn), err |
||||||
|
} |
||||||
|
|
||||||
|
// KMS represents an active and authenticted connection
|
||||||
|
// to a Key-Management-Service. It supports generating
|
||||||
|
// data key generation and unsealing of KMS-generated
|
||||||
|
// data keys.
|
||||||
|
type KMS interface { |
||||||
|
// GenerateKey generates a new random data key using
|
||||||
|
// the master key referenced by the keyID. It returns
|
||||||
|
// the plaintext key and the sealed plaintext key
|
||||||
|
// on success.
|
||||||
|
//
|
||||||
|
// The context is cryptographically bound to the
|
||||||
|
// generated key. The same context must be provided
|
||||||
|
// again to unseal the generated key.
|
||||||
|
GenerateKey(keyID string, context Context) (key, sealedKey []byte, err error) |
||||||
|
|
||||||
|
// UnsealKey unseals the sealedKey using the master key
|
||||||
|
// referenced by the keyID. The provided context must
|
||||||
|
// match the context used to generate the sealed key.
|
||||||
|
UnsealKey(keyID string, sealedKey []byte, context Context) (key []byte, err error) |
||||||
|
} |
||||||
|
|
||||||
|
type masterKeyKMS struct { |
||||||
|
masterKey [32]byte |
||||||
|
} |
||||||
|
|
||||||
|
// NewKMS returns a basic KMS implementation from a single 256 bit master key.
|
||||||
|
//
|
||||||
|
// The KMS accepts any keyID but binds the keyID and context cryptographically
|
||||||
|
// to the generated keys.
|
||||||
|
func NewKMS(key [32]byte) KMS { return &masterKeyKMS{masterKey: key} } |
||||||
|
|
||||||
|
func (kms *masterKeyKMS) GenerateKey(keyID string, ctx Context) (key, sealedKey []byte, err error) { |
||||||
|
key = make([]byte, 32) |
||||||
|
if _, err = io.ReadFull(rand.Reader, key); err != nil { |
||||||
|
logger.CriticalIf(context.Background(), errOutOfEntropy) |
||||||
|
} |
||||||
|
|
||||||
|
var ( |
||||||
|
buffer bytes.Buffer |
||||||
|
derivedKey = kms.deriveKey(keyID, ctx) |
||||||
|
) |
||||||
|
if n, err := sio.Encrypt(&buffer, bytes.NewReader(key), sio.Config{Key: derivedKey[:]}); err != nil || n != 64 { |
||||||
|
logger.CriticalIf(context.Background(), errors.New("KMS: unable to encrypt data key")) |
||||||
|
} |
||||||
|
sealedKey = buffer.Bytes() |
||||||
|
return key, sealedKey, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (kms *masterKeyKMS) UnsealKey(keyID string, sealedKey []byte, ctx Context) (key []byte, err error) { |
||||||
|
var ( |
||||||
|
buffer bytes.Buffer |
||||||
|
derivedKey = kms.deriveKey(keyID, ctx) |
||||||
|
) |
||||||
|
if n, err := sio.Decrypt(&buffer, bytes.NewReader(sealedKey), sio.Config{Key: derivedKey[:]}); err != nil || n != 32 { |
||||||
|
return nil, err // TODO(aead): upgrade sio to use sio.Error
|
||||||
|
} |
||||||
|
key = buffer.Bytes() |
||||||
|
return key, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (kms *masterKeyKMS) deriveKey(keyID string, context Context) (key [32]byte) { |
||||||
|
if context == nil { |
||||||
|
context = Context{} |
||||||
|
} |
||||||
|
mac := hmac.New(sha256.New, kms.masterKey[:]) |
||||||
|
mac.Write([]byte(keyID)) |
||||||
|
context.WriteTo(mac) |
||||||
|
mac.Sum(key[:0]) |
||||||
|
return key |
||||||
|
} |
@ -0,0 +1,84 @@ |
|||||||
|
// Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
|
||||||
|
//
|
||||||
|
// 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 crypto |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"path" |
||||||
|
"strings" |
||||||
|
"testing" |
||||||
|
) |
||||||
|
|
||||||
|
var masterKeyKMSTests = []struct { |
||||||
|
GenKeyID, UnsealKeyID string |
||||||
|
GenContext, UnsealContext Context |
||||||
|
|
||||||
|
ShouldFail bool |
||||||
|
}{ |
||||||
|
{GenKeyID: "", UnsealKeyID: "", GenContext: Context{}, UnsealContext: nil, ShouldFail: false}, // 0
|
||||||
|
{GenKeyID: "ac47be7f", UnsealKeyID: "ac47be7f", GenContext: Context{}, UnsealContext: Context{}, ShouldFail: false}, // 1
|
||||||
|
{GenKeyID: "ac47be7f", UnsealKeyID: "ac47be7f", GenContext: Context{"bucket": "object"}, UnsealContext: Context{"bucket": "object"}, ShouldFail: false}, // 2
|
||||||
|
{GenKeyID: "", UnsealKeyID: "", GenContext: Context{"bucket": path.Join("bucket", "object")}, UnsealContext: Context{"bucket": path.Join("bucket", "object")}, ShouldFail: false}, // 3
|
||||||
|
{GenKeyID: "", UnsealKeyID: "", GenContext: Context{"a": "a", "0": "0", "b": "b"}, UnsealContext: Context{"b": "b", "a": "a", "0": "0"}, ShouldFail: false}, // 4
|
||||||
|
|
||||||
|
{GenKeyID: "ac47be7f", UnsealKeyID: "ac47be7e", GenContext: Context{}, UnsealContext: Context{}, ShouldFail: true}, // 5
|
||||||
|
{GenKeyID: "ac47be7f", UnsealKeyID: "ac47be7f", GenContext: Context{"bucket": "object"}, UnsealContext: Context{"Bucket": "object"}, ShouldFail: true}, // 6
|
||||||
|
{GenKeyID: "", UnsealKeyID: "", GenContext: Context{"bucket": path.Join("bucket", "Object")}, UnsealContext: Context{"bucket": path.Join("bucket", "object")}, ShouldFail: true}, // 7
|
||||||
|
{GenKeyID: "", UnsealKeyID: "", GenContext: Context{"a": "a", "0": "1", "b": "b"}, UnsealContext: Context{"b": "b", "a": "a", "0": "0"}, ShouldFail: true}, // 8
|
||||||
|
} |
||||||
|
|
||||||
|
func TestMasterKeyKMS(t *testing.T) { |
||||||
|
kms := NewKMS([32]byte{}) |
||||||
|
for i, test := range masterKeyKMSTests { |
||||||
|
key, sealedKey, err := kms.GenerateKey(test.GenKeyID, test.GenContext) |
||||||
|
if err != nil { |
||||||
|
t.Errorf("Test %d: KMS failed to generate key: %v", i, err) |
||||||
|
} |
||||||
|
unsealedKey, err := kms.UnsealKey(test.UnsealKeyID, sealedKey, test.UnsealContext) |
||||||
|
if err != nil && !test.ShouldFail { |
||||||
|
t.Errorf("Test %d: KMS failed to unseal the generated key: %v", i, err) |
||||||
|
} |
||||||
|
if err == nil && test.ShouldFail { |
||||||
|
t.Errorf("Test %d: KMS unsealed the generated successfully but should have failed", i) |
||||||
|
} |
||||||
|
if !test.ShouldFail && !bytes.Equal(key, unsealedKey) { |
||||||
|
t.Errorf("Test %d: The generated and unsealed key differ", i) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var contextWriteToTests = []struct { |
||||||
|
Context Context |
||||||
|
ExpectedJSON string |
||||||
|
}{ |
||||||
|
{Context: Context{}, ExpectedJSON: "{}"}, // 0
|
||||||
|
{Context: Context{"a": "b"}, ExpectedJSON: `{"a":"b"}`}, // 1
|
||||||
|
{Context: Context{"a": "b", "c": "d"}, ExpectedJSON: `{"a":"b","c":"d"}`}, // 2
|
||||||
|
{Context: Context{"c": "d", "a": "b"}, ExpectedJSON: `{"a":"b","c":"d"}`}, // 3
|
||||||
|
{Context: Context{"0": "1", "-": "2", ".": "#"}, ExpectedJSON: `{"-":"2",".":"#","0":"1"}`}, // 4
|
||||||
|
} |
||||||
|
|
||||||
|
func TestContextWriteTo(t *testing.T) { |
||||||
|
for i, test := range contextWriteToTests { |
||||||
|
var jsonContext strings.Builder |
||||||
|
if _, err := test.Context.WriteTo(&jsonContext); err != nil { |
||||||
|
t.Errorf("Test %d: Failed to encode context: %v", i, err) |
||||||
|
continue |
||||||
|
} |
||||||
|
if s := jsonContext.String(); s != test.ExpectedJSON { |
||||||
|
t.Errorf("Test %d: JSON representation differ - got: '%s' want: '%s'", i, s, test.ExpectedJSON) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue