add SSE-KMS support and use SSE-KMS for auto encryption (#12237)

This commit adds basic SSE-KMS support.
Now, a client can specify the SSE-KMS headers
(algorithm, optional key-id, optional context)
such that the object gets encrypted using the
SSE-KMS method. Further, auto-encryption now
defaults to SSE-KMS.

This commit does not try to do any refactoring
and instead tries to implement SSE-KMS as a minimal
change to the code base. However, refactoring the entire
crypto-related code is planned - but needs a separate
effort.

Signed-off-by: Andreas Auernhammer <aead@mail.de>
This commit is contained in:
Andreas Auernhammer
2021-05-07 00:24:01 +02:00
committed by GitHub
parent 989e394a32
commit af0c65be93
7 changed files with 229 additions and 94 deletions

View File

@@ -26,6 +26,7 @@ import (
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"path"
@@ -36,6 +37,7 @@ import (
xhttp "github.com/minio/minio/cmd/http"
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/fips"
"github.com/minio/minio/pkg/kms"
"github.com/minio/sio"
)
@@ -116,11 +118,71 @@ func ParseSSECustomerHeader(header http.Header) (key []byte, err error) {
}
// This function rotates old to new key.
func rotateKey(oldKey []byte, newKey []byte, bucket, object string, metadata map[string]string) error {
switch {
default:
return errObjectTampered
case crypto.SSEC.IsEncrypted(metadata):
func rotateKey(oldKey []byte, newKeyID string, newKey []byte, bucket, object string, metadata map[string]string, ctx crypto.Context) error {
kind, _ := crypto.IsEncrypted(metadata)
switch kind {
case crypto.S3:
if GlobalKMS == nil {
return errKMSNotConfigured
}
keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(metadata)
if err != nil {
return err
}
oldKey, err := GlobalKMS.DecryptKey(keyID, kmsKey, kms.Context{bucket: path.Join(bucket, object)})
if err != nil {
return err
}
var objectKey crypto.ObjectKey
if err = objectKey.Unseal(oldKey, sealedKey, crypto.S3.String(), bucket, object); err != nil {
return err
}
newKey, err := GlobalKMS.GenerateKey("", kms.Context{bucket: path.Join(bucket, object)})
if err != nil {
return err
}
sealedKey = objectKey.Seal(newKey.Plaintext, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, object)
crypto.S3.CreateMetadata(metadata, newKey.KeyID, newKey.Ciphertext, sealedKey)
return nil
case crypto.S3KMS:
if GlobalKMS == nil {
return errKMSNotConfigured
}
objectKey, err := crypto.S3KMS.UnsealObjectKey(GlobalKMS, metadata, bucket, object)
if err != nil {
return err
}
if len(ctx) == 0 {
_, _, _, ctx, err = crypto.S3KMS.ParseMetadata(metadata)
if err != nil {
return err
}
}
// If the context does not contain the bucket key
// we must add it for key generation. However,
// the context must be stored exactly like the
// client provided it. Therefore, we create a copy
// of the client provided context and add the bucket
// key, if not present.
var kmsCtx = kms.Context{}
for k, v := range ctx {
kmsCtx[k] = v
}
if _, ok := kmsCtx[bucket]; !ok {
kmsCtx[bucket] = path.Join(bucket, object)
}
newKey, err := GlobalKMS.GenerateKey(newKeyID, kmsCtx)
if err != nil {
return err
}
sealedKey := objectKey.Seal(newKey.Plaintext, crypto.GenerateIV(rand.Reader), crypto.S3KMS.String(), bucket, object)
crypto.S3KMS.CreateMetadata(metadata, newKey.KeyID, newKey.Ciphertext, sealedKey, ctx)
return nil
case crypto.SSEC:
sealedKey, err := crypto.SSEC.ParseMetadata(metadata)
if err != nil {
return err
@@ -140,40 +202,19 @@ func rotateKey(oldKey []byte, newKey []byte, bucket, object string, metadata map
sealedKey = objectKey.Seal(newKey, sealedKey.IV, crypto.SSEC.String(), bucket, object)
crypto.SSEC.CreateMetadata(metadata, sealedKey)
return nil
case crypto.S3.IsEncrypted(metadata):
if GlobalKMS == nil {
return errKMSNotConfigured
}
keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(metadata)
if err != nil {
return err
}
oldKey, err := GlobalKMS.DecryptKey(keyID, kmsKey, crypto.Context{bucket: path.Join(bucket, object)})
if err != nil {
return err
}
var objectKey crypto.ObjectKey
if err = objectKey.Unseal(oldKey, sealedKey, crypto.S3.String(), bucket, object); err != nil {
return err
}
newKey, err := GlobalKMS.GenerateKey("", crypto.Context{bucket: path.Join(bucket, object)})
if err != nil {
return err
}
sealedKey = objectKey.Seal(newKey.Plaintext, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, object)
crypto.S3.CreateMetadata(metadata, newKey.KeyID, newKey.Ciphertext, sealedKey)
return nil
default:
return errObjectTampered
}
}
func newEncryptMetadata(key []byte, bucket, object string, metadata map[string]string, sseS3 bool) (crypto.ObjectKey, error) {
func newEncryptMetadata(kind crypto.Type, keyID string, key []byte, bucket, object string, metadata map[string]string, ctx kms.Context) (crypto.ObjectKey, error) {
var sealedKey crypto.SealedKey
if sseS3 {
switch kind {
case crypto.S3:
if GlobalKMS == nil {
return crypto.ObjectKey{}, errKMSNotConfigured
}
key, err := GlobalKMS.GenerateKey("", crypto.Context{bucket: path.Join(bucket, object)})
key, err := GlobalKMS.GenerateKey("", kms.Context{bucket: path.Join(bucket, object)})
if err != nil {
return crypto.ObjectKey{}, err
}
@@ -182,15 +223,45 @@ func newEncryptMetadata(key []byte, bucket, object string, metadata map[string]s
sealedKey = objectKey.Seal(key.Plaintext, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, object)
crypto.S3.CreateMetadata(metadata, key.KeyID, key.Ciphertext, sealedKey)
return objectKey, nil
case crypto.S3KMS:
if GlobalKMS == nil {
return crypto.ObjectKey{}, errKMSNotConfigured
}
// If the context does not contain the bucket key
// we must add it for key generation. However,
// the context must be stored exactly like the
// client provided it. Therefore, we create a copy
// of the client provided context and add the bucket
// key, if not present.
var kmsCtx = kms.Context{}
for k, v := range ctx {
kmsCtx[k] = v
}
if _, ok := kmsCtx[bucket]; !ok {
kmsCtx[bucket] = path.Join(bucket, object)
}
key, err := GlobalKMS.GenerateKey(keyID, kmsCtx)
if err != nil {
return crypto.ObjectKey{}, err
}
objectKey := crypto.GenerateKey(key.Plaintext, rand.Reader)
sealedKey = objectKey.Seal(key.Plaintext, crypto.GenerateIV(rand.Reader), crypto.S3KMS.String(), bucket, object)
crypto.S3KMS.CreateMetadata(metadata, key.KeyID, key.Ciphertext, sealedKey, ctx)
return objectKey, nil
case crypto.SSEC:
objectKey := crypto.GenerateKey(key, rand.Reader)
sealedKey = objectKey.Seal(key, crypto.GenerateIV(rand.Reader), crypto.SSEC.String(), bucket, object)
crypto.SSEC.CreateMetadata(metadata, sealedKey)
return objectKey, nil
default:
return crypto.ObjectKey{}, fmt.Errorf("encryption type '%v' not supported", kind)
}
objectKey := crypto.GenerateKey(key, rand.Reader)
sealedKey = objectKey.Seal(key, crypto.GenerateIV(rand.Reader), crypto.SSEC.String(), bucket, object)
crypto.SSEC.CreateMetadata(metadata, sealedKey)
return objectKey, nil
}
func newEncryptReader(content io.Reader, key []byte, bucket, object string, metadata map[string]string, sseS3 bool) (io.Reader, crypto.ObjectKey, error) {
objectEncryptionKey, err := newEncryptMetadata(key, bucket, object, metadata, sseS3)
func newEncryptReader(content io.Reader, kind crypto.Type, keyID string, key []byte, bucket, object string, metadata map[string]string, ctx crypto.Context) (io.Reader, crypto.ObjectKey, error) {
objectEncryptionKey, err := newEncryptMetadata(kind, keyID, key, bucket, object, metadata, ctx)
if err != nil {
return nil, crypto.ObjectKey{}, err
}
@@ -207,15 +278,24 @@ func newEncryptReader(content io.Reader, key []byte, bucket, object string, meta
// SSE-S3
func setEncryptionMetadata(r *http.Request, bucket, object string, metadata map[string]string) (err error) {
var (
key []byte
key []byte
keyID string
ctx crypto.Context
)
if crypto.SSEC.IsRequested(r.Header) {
kind, _ := crypto.IsRequested(r.Header)
switch kind {
case crypto.SSEC:
key, err = ParseSSECustomerRequest(r)
if err != nil {
return
return err
}
case crypto.S3KMS:
keyID, ctx, err = crypto.S3KMS.ParseHTTP(r.Header)
if err != nil {
return err
}
}
_, err = newEncryptMetadata(key, bucket, object, metadata, crypto.S3.IsRequested(r.Header))
_, err = newEncryptMetadata(kind, keyID, key, bucket, object, metadata, ctx)
return
}
@@ -223,24 +303,32 @@ func setEncryptionMetadata(r *http.Request, bucket, object string, metadata map[
// with the client provided key. It also marks the object as client-side-encrypted
// and sets the correct headers.
func EncryptRequest(content io.Reader, r *http.Request, bucket, object string, metadata map[string]string) (io.Reader, crypto.ObjectKey, error) {
if crypto.S3.IsRequested(r.Header) && crypto.SSEC.IsRequested(r.Header) {
return nil, crypto.ObjectKey{}, crypto.ErrIncompatibleEncryptionMethod
}
if r.ContentLength > encryptBufferThreshold {
// The encryption reads in blocks of 64KB.
// We add a buffer on bigger files to reduce the number of syscalls upstream.
content = bufio.NewReaderSize(content, encryptBufferSize)
}
var key []byte
if crypto.SSEC.IsRequested(r.Header) {
var err error
var (
key []byte
keyID string
ctx crypto.Context
err error
)
kind, _ := crypto.IsRequested(r.Header)
if kind == crypto.SSEC {
key, err = ParseSSECustomerRequest(r)
if err != nil {
return nil, crypto.ObjectKey{}, err
}
}
return newEncryptReader(content, key, bucket, object, metadata, crypto.S3.IsRequested(r.Header))
if kind == crypto.S3KMS {
keyID, ctx, err = crypto.S3KMS.ParseHTTP(r.Header)
if err != nil {
return nil, crypto.ObjectKey{}, err
}
}
return newEncryptReader(content, kind, keyID, key, bucket, object, metadata, ctx)
}
func decryptObjectInfo(key []byte, bucket, object string, metadata map[string]string) ([]byte, error) {
@@ -590,7 +678,7 @@ func getDecryptedETag(headers http.Header, objInfo ObjectInfo, copySource bool)
// Since server side copy with same source and dest just replaces the ETag, we save
// encrypted content MD5Sum as ETag for both SSE-C and SSE-S3, we standardize the ETag
// encryption across SSE-C and SSE-S3, and only return last 32 bytes for SSE-C
if crypto.SSEC.IsEncrypted(objInfo.UserDefined) && !copySource {
if (crypto.SSEC.IsEncrypted(objInfo.UserDefined) || crypto.S3KMS.IsEncrypted(objInfo.UserDefined)) && !copySource {
return objInfo.ETag[len(objInfo.ETag)-32:]
}
@@ -774,7 +862,7 @@ func DecryptObjectInfo(info *ObjectInfo, r *http.Request) (encrypted bool, err e
// disallow X-Amz-Server-Side-Encryption header on HEAD and GET
switch r.Method {
case http.MethodGet, http.MethodHead:
if crypto.S3.IsRequested(headers) {
if crypto.S3.IsRequested(headers) || crypto.S3KMS.IsRequested(headers) {
return false, errInvalidEncryptionParameters
}
}
@@ -797,6 +885,12 @@ func DecryptObjectInfo(info *ObjectInfo, r *http.Request) (encrypted bool, err e
}
}
if crypto.S3KMS.IsEncrypted(info.UserDefined) && r.Header.Get(xhttp.AmzCopySource) == "" {
if crypto.SSEC.IsRequested(headers) || crypto.SSECopy.IsRequested(headers) {
return encrypted, errEncryptedObject
}
}
if _, err = info.DecryptedSize(); err != nil {
return encrypted, err
}