mirror of
https://github.com/minio/minio.git
synced 2025-11-10 05:59:43 -05:00
fix object rebinding SSE-C security guarantee violation (#6121)
This commit fixes a weakness of the key-encryption-key derivation for SSE-C encrypted objects. Before this change the key-encryption-key was not bound to / didn't depend on the object path. This allows an attacker to repalce objects - encrypted with the same client-key - with each other. This change fixes this issue by updating the key-encryption-key derivation to include: - the domain (in this case SSE-C) - a canonical object path representation - the encryption & key derivation algorithm Changing the object path now causes the KDF to derive a different key-encryption-key such that the object-key unsealing fails. Including the domain (SSE-C) and encryption & key derivation algorithm is not directly neccessary for this fix. However, both will be included for the SSE-S3 KDF. So they are included here to avoid updating the KDF again when we add SSE-S3. The leagcy KDF 'DARE-SHA256' is only used for existing objects and never for new objects / key rotation.
This commit is contained in:
committed by
kannappanr
parent
4ddc222f46
commit
b181a693fb
@@ -28,6 +28,7 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
@@ -144,9 +145,20 @@ const (
|
||||
ServerSideEncryptionSealedKey = ReservedMetadataPrefix + "Server-Side-Encryption-Sealed-Key"
|
||||
)
|
||||
|
||||
// SSESealAlgorithmDareSha256 specifies DARE as authenticated en/decryption scheme and SHA256 as cryptographic
|
||||
// hash function.
|
||||
const SSESealAlgorithmDareSha256 = "DARE-SHA256"
|
||||
const (
|
||||
// SSESealAlgorithmDareSha256 specifies DARE as authenticated en/decryption scheme and SHA256 as cryptographic
|
||||
// hash function. The key derivation of DARE-SHA256 is not optimal and does not include the object path.
|
||||
// It is considered legacy and should not be used anymore.
|
||||
SSESealAlgorithmDareSha256 = "DARE-SHA256"
|
||||
|
||||
// SSESealAlgorithmDareV2HmacSha256 specifies DAREv2 as authenticated en/decryption scheme and SHA256 as cryptographic
|
||||
// hash function for the HMAC PRF.
|
||||
SSESealAlgorithmDareV2HmacSha256 = "DAREv2-HMAC-SHA256"
|
||||
|
||||
// SSEDomain specifies the domain for the derived key - in this case the
|
||||
// key should be used for SSE-C.
|
||||
SSEDomain = "SSE-C"
|
||||
)
|
||||
|
||||
// hasSSECustomerHeader returns true if the given HTTP header
|
||||
// contains server-side-encryption with customer provided key fields.
|
||||
@@ -250,10 +262,11 @@ func ParseSSECustomerHeader(header http.Header) (key []byte, err error) {
|
||||
}
|
||||
|
||||
// This function rotates old to new key.
|
||||
func rotateKey(oldKey []byte, newKey []byte, metadata map[string]string) error {
|
||||
func rotateKey(oldKey []byte, newKey []byte, bucket, object string, metadata map[string]string) error {
|
||||
delete(metadata, SSECustomerKey) // make sure we do not save the key by accident
|
||||
|
||||
if metadata[ServerSideEncryptionSealAlgorithm] != SSESealAlgorithmDareSha256 { // currently DARE-SHA256 is the only option
|
||||
algorithm := metadata[ServerSideEncryptionSealAlgorithm]
|
||||
if algorithm != SSESealAlgorithmDareSha256 && algorithm != SSESealAlgorithmDareV2HmacSha256 {
|
||||
return errObjectTampered
|
||||
}
|
||||
iv, err := base64.StdEncoding.DecodeString(metadata[ServerSideEncryptionIV])
|
||||
@@ -265,14 +278,33 @@ func rotateKey(oldKey []byte, newKey []byte, metadata map[string]string) error {
|
||||
return errObjectTampered
|
||||
}
|
||||
|
||||
sha := sha256.New() // derive key encryption key
|
||||
sha.Write(oldKey)
|
||||
sha.Write(iv)
|
||||
keyEncryptionKey := sha.Sum(nil)
|
||||
var (
|
||||
minDAREVersion byte
|
||||
keyEncryptionKey [32]byte
|
||||
)
|
||||
switch algorithm {
|
||||
default:
|
||||
return errObjectTampered
|
||||
case SSESealAlgorithmDareSha256: // legacy key-encryption-key derivation
|
||||
minDAREVersion = sio.Version10
|
||||
sha := sha256.New()
|
||||
sha.Write(oldKey)
|
||||
sha.Write(iv)
|
||||
sha.Sum(keyEncryptionKey[:0])
|
||||
case SSESealAlgorithmDareV2HmacSha256: // key-encryption-key derivation - See: crypto/doc.go
|
||||
minDAREVersion = sio.Version20
|
||||
mac := hmac.New(sha256.New, oldKey)
|
||||
mac.Write(iv)
|
||||
mac.Write([]byte(SSEDomain))
|
||||
mac.Write([]byte(SSESealAlgorithmDareV2HmacSha256))
|
||||
mac.Write([]byte(path.Join(bucket, object)))
|
||||
mac.Sum(keyEncryptionKey[:0])
|
||||
}
|
||||
|
||||
objectEncryptionKey := bytes.NewBuffer(nil) // decrypt object encryption key
|
||||
n, err := sio.Decrypt(objectEncryptionKey, bytes.NewReader(sealedKey), sio.Config{
|
||||
Key: keyEncryptionKey,
|
||||
MinVersion: minDAREVersion,
|
||||
Key: keyEncryptionKey[:],
|
||||
})
|
||||
if n != 32 || err != nil { // Either the provided key does not match or the object was tampered.
|
||||
if subtle.ConstantTimeCompare(oldKey, newKey) == 1 {
|
||||
@@ -280,46 +312,34 @@ func rotateKey(oldKey []byte, newKey []byte, metadata map[string]string) error {
|
||||
}
|
||||
return errSSEKeyMismatch // To provide strict AWS S3 compatibility we return: access denied.
|
||||
}
|
||||
if subtle.ConstantTimeCompare(oldKey, newKey) == 1 {
|
||||
return nil // we don't need to rotate keys if newKey == oldKey
|
||||
if subtle.ConstantTimeCompare(oldKey, newKey) == 1 && algorithm != SSESealAlgorithmDareSha256 {
|
||||
return nil // we don't need to rotate keys if newKey == oldKey but we may have to upgrade KDF algorithm
|
||||
}
|
||||
|
||||
nonce := make([]byte, 32) // generate random values for key derivation
|
||||
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
niv := sha256.Sum256(nonce[:]) // derive key encryption key
|
||||
sha = sha256.New()
|
||||
sha.Write(newKey)
|
||||
sha.Write(niv[:])
|
||||
keyEncryptionKey = sha.Sum(nil)
|
||||
mac := hmac.New(sha256.New, newKey) // key-encryption-key derivation - See: crypto/doc.go
|
||||
mac.Write(iv)
|
||||
mac.Write([]byte(SSEDomain))
|
||||
mac.Write([]byte(SSESealAlgorithmDareV2HmacSha256))
|
||||
mac.Write([]byte(path.Join(bucket, object)))
|
||||
mac.Sum(keyEncryptionKey[:0])
|
||||
|
||||
sealedKeyW := bytes.NewBuffer(nil) // sealedKey := 16 byte header + 32 byte payload + 16 byte tag
|
||||
n, err = sio.Encrypt(sealedKeyW, bytes.NewReader(objectEncryptionKey.Bytes()), sio.Config{
|
||||
Key: keyEncryptionKey,
|
||||
Key: keyEncryptionKey[:],
|
||||
})
|
||||
if n != 64 || err != nil {
|
||||
return errors.New("failed to seal object encryption key") // if this happens there's a bug in the code (may panic ?)
|
||||
}
|
||||
|
||||
metadata[ServerSideEncryptionIV] = base64.StdEncoding.EncodeToString(niv[:])
|
||||
metadata[ServerSideEncryptionSealAlgorithm] = SSESealAlgorithmDareSha256
|
||||
metadata[ServerSideEncryptionIV] = base64.StdEncoding.EncodeToString(iv[:])
|
||||
metadata[ServerSideEncryptionSealAlgorithm] = SSESealAlgorithmDareV2HmacSha256
|
||||
metadata[ServerSideEncryptionSealedKey] = base64.StdEncoding.EncodeToString(sealedKeyW.Bytes())
|
||||
return nil
|
||||
}
|
||||
|
||||
func newEncryptMetadata(key []byte, metadata map[string]string) ([]byte, error) {
|
||||
func newEncryptMetadata(key []byte, bucket, object string, metadata map[string]string) ([]byte, error) {
|
||||
delete(metadata, SSECustomerKey) // make sure we do not save the key by accident
|
||||
|
||||
// security notice:
|
||||
// - If the first 32 bytes of the random value are ever repeated under the same client-provided
|
||||
// key the encrypted object will not be tamper-proof. [ P(coll) ~= 1 / 2^(256 / 2)]
|
||||
// - If the last 32 bytes of the random value are ever repeated under the same client-provided
|
||||
// key an adversary may be able to extract the object encryption key. This depends on the
|
||||
// authenticated en/decryption scheme. The DARE format will generate an 8 byte nonce which must
|
||||
// be repeated in addition to reveal the object encryption key.
|
||||
// [ P(coll) ~= 1 / 2^((256 + 64) / 2) ]
|
||||
// See crypto/doc.go for detailed description
|
||||
nonce := make([]byte, 32+SSEIVSize) // generate random values for key derivation
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, err
|
||||
@@ -329,11 +349,13 @@ func newEncryptMetadata(key []byte, metadata map[string]string) ([]byte, error)
|
||||
sha.Write(nonce[:32])
|
||||
objectEncryptionKey := sha.Sum(nil)
|
||||
|
||||
iv := sha256.Sum256(nonce[32:]) // derive key encryption key
|
||||
sha = sha256.New()
|
||||
sha.Write(key)
|
||||
sha.Write(iv[:])
|
||||
keyEncryptionKey := sha.Sum(nil)
|
||||
iv := sha256.Sum256(nonce[32:]) // key-encryption-key derivation - See: crypto/doc.go
|
||||
mac := hmac.New(sha256.New, key)
|
||||
mac.Write(iv[:])
|
||||
mac.Write([]byte(SSEDomain))
|
||||
mac.Write([]byte(SSESealAlgorithmDareV2HmacSha256))
|
||||
mac.Write([]byte(path.Join(bucket, object)))
|
||||
keyEncryptionKey := mac.Sum(nil)
|
||||
|
||||
sealedKey := bytes.NewBuffer(nil) // sealedKey := 16 byte header + 32 byte payload + 16 byte tag
|
||||
n, err := sio.Encrypt(sealedKey, bytes.NewReader(objectEncryptionKey), sio.Config{
|
||||
@@ -344,14 +366,14 @@ func newEncryptMetadata(key []byte, metadata map[string]string) ([]byte, error)
|
||||
}
|
||||
|
||||
metadata[ServerSideEncryptionIV] = base64.StdEncoding.EncodeToString(iv[:])
|
||||
metadata[ServerSideEncryptionSealAlgorithm] = SSESealAlgorithmDareSha256
|
||||
metadata[ServerSideEncryptionSealAlgorithm] = SSESealAlgorithmDareV2HmacSha256
|
||||
metadata[ServerSideEncryptionSealedKey] = base64.StdEncoding.EncodeToString(sealedKey.Bytes())
|
||||
|
||||
return objectEncryptionKey, nil
|
||||
}
|
||||
|
||||
func newEncryptReader(content io.Reader, key []byte, metadata map[string]string) (io.Reader, error) {
|
||||
objectEncryptionKey, err := newEncryptMetadata(key, metadata)
|
||||
func newEncryptReader(content io.Reader, key []byte, bucket, object string, metadata map[string]string) (io.Reader, error) {
|
||||
objectEncryptionKey, err := newEncryptMetadata(key, bucket, object, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -367,29 +389,26 @@ func newEncryptReader(content io.Reader, key []byte, metadata map[string]string)
|
||||
// EncryptRequest takes the client provided content and encrypts the data
|
||||
// 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, metadata map[string]string) (io.Reader, error) {
|
||||
func EncryptRequest(content io.Reader, r *http.Request, bucket, object string, metadata map[string]string) (io.Reader, error) {
|
||||
key, err := ParseSSECustomerRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newEncryptReader(content, key, metadata)
|
||||
return newEncryptReader(content, key, bucket, object, metadata)
|
||||
}
|
||||
|
||||
// DecryptCopyRequest decrypts the object with the client provided key. It also removes
|
||||
// the client-side-encryption metadata from the object and sets the correct headers.
|
||||
func DecryptCopyRequest(client io.Writer, r *http.Request, metadata map[string]string) (io.WriteCloser, error) {
|
||||
func DecryptCopyRequest(client io.Writer, r *http.Request, bucket, object string, metadata map[string]string) (io.WriteCloser, error) {
|
||||
key, err := ParseSSECopyCustomerRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
delete(metadata, SSECopyCustomerKey) // make sure we do not save the key by accident
|
||||
return newDecryptWriter(client, key, 0, metadata)
|
||||
return newDecryptWriter(client, key, bucket, object, 0, metadata)
|
||||
}
|
||||
|
||||
func decryptObjectInfo(key []byte, metadata map[string]string) ([]byte, error) {
|
||||
if metadata[ServerSideEncryptionSealAlgorithm] != SSESealAlgorithmDareSha256 { // currently DARE-SHA256 is the only option
|
||||
return nil, errObjectTampered
|
||||
}
|
||||
func decryptObjectInfo(key []byte, bucket, object string, metadata map[string]string) ([]byte, error) {
|
||||
iv, err := base64.StdEncoding.DecodeString(metadata[ServerSideEncryptionIV])
|
||||
if err != nil || len(iv) != SSEIVSize {
|
||||
return nil, errObjectTampered
|
||||
@@ -399,14 +418,33 @@ func decryptObjectInfo(key []byte, metadata map[string]string) ([]byte, error) {
|
||||
return nil, errObjectTampered
|
||||
}
|
||||
|
||||
sha := sha256.New() // derive key encryption key
|
||||
sha.Write(key)
|
||||
sha.Write(iv)
|
||||
keyEncryptionKey := sha.Sum(nil)
|
||||
var (
|
||||
minDAREVersion byte
|
||||
keyEncryptionKey [32]byte
|
||||
)
|
||||
switch algorithm := metadata[ServerSideEncryptionSealAlgorithm]; algorithm {
|
||||
default:
|
||||
return nil, errObjectTampered
|
||||
case SSESealAlgorithmDareSha256: // legacy key-encryption-key derivation
|
||||
minDAREVersion = sio.Version10
|
||||
sha := sha256.New()
|
||||
sha.Write(key)
|
||||
sha.Write(iv)
|
||||
sha.Sum(keyEncryptionKey[:0])
|
||||
case SSESealAlgorithmDareV2HmacSha256: // key-encryption-key derivation - See: crypto/doc.go
|
||||
minDAREVersion = sio.Version20
|
||||
mac := hmac.New(sha256.New, key)
|
||||
mac.Write(iv)
|
||||
mac.Write([]byte(SSEDomain))
|
||||
mac.Write([]byte(SSESealAlgorithmDareV2HmacSha256))
|
||||
mac.Write([]byte(path.Join(bucket, object)))
|
||||
mac.Sum(keyEncryptionKey[:0])
|
||||
}
|
||||
|
||||
objectEncryptionKey := bytes.NewBuffer(nil) // decrypt object encryption key
|
||||
n, err := sio.Decrypt(objectEncryptionKey, bytes.NewReader(sealedKey), sio.Config{
|
||||
Key: keyEncryptionKey,
|
||||
MinVersion: minDAREVersion,
|
||||
Key: keyEncryptionKey[:],
|
||||
})
|
||||
if n != 32 || err != nil {
|
||||
// Either the provided key does not match or the object was tampered.
|
||||
@@ -416,11 +454,10 @@ func decryptObjectInfo(key []byte, metadata map[string]string) ([]byte, error) {
|
||||
return objectEncryptionKey.Bytes(), nil
|
||||
}
|
||||
|
||||
func newDecryptWriter(client io.Writer, key []byte, seqNumber uint32, metadata map[string]string) (io.WriteCloser, error) {
|
||||
objectEncryptionKey, err := decryptObjectInfo(key, metadata)
|
||||
func newDecryptWriter(client io.Writer, key []byte, bucket, object string, seqNumber uint32, metadata map[string]string) (io.WriteCloser, error) {
|
||||
objectEncryptionKey, err := decryptObjectInfo(key, bucket, object, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
}
|
||||
return newDecryptWriterWithObjectKey(client, objectEncryptionKey, seqNumber, metadata)
|
||||
}
|
||||
@@ -443,19 +480,19 @@ func newDecryptWriterWithObjectKey(client io.Writer, objectEncryptionKey []byte,
|
||||
|
||||
// DecryptRequestWithSequenceNumber decrypts the object with the client provided key. It also removes
|
||||
// the client-side-encryption metadata from the object and sets the correct headers.
|
||||
func DecryptRequestWithSequenceNumber(client io.Writer, r *http.Request, seqNumber uint32, metadata map[string]string) (io.WriteCloser, error) {
|
||||
func DecryptRequestWithSequenceNumber(client io.Writer, r *http.Request, bucket, object string, seqNumber uint32, metadata map[string]string) (io.WriteCloser, error) {
|
||||
key, err := ParseSSECustomerRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
delete(metadata, SSECustomerKey) // make sure we do not save the key by accident
|
||||
return newDecryptWriter(client, key, seqNumber, metadata)
|
||||
return newDecryptWriter(client, key, bucket, object, seqNumber, metadata)
|
||||
}
|
||||
|
||||
// DecryptRequest decrypts the object with the client provided key. It also removes
|
||||
// the client-side-encryption metadata from the object and sets the correct headers.
|
||||
func DecryptRequest(client io.Writer, r *http.Request, metadata map[string]string) (io.WriteCloser, error) {
|
||||
return DecryptRequestWithSequenceNumber(client, r, 0, metadata)
|
||||
func DecryptRequest(client io.Writer, r *http.Request, bucket, object string, metadata map[string]string) (io.WriteCloser, error) {
|
||||
return DecryptRequestWithSequenceNumber(client, r, bucket, object, 0, metadata)
|
||||
}
|
||||
|
||||
// DecryptBlocksWriter - decrypts multipart parts, while implementing a io.Writer compatible interface.
|
||||
@@ -469,9 +506,10 @@ type DecryptBlocksWriter struct {
|
||||
// Current part index
|
||||
partIndex int
|
||||
// Parts information
|
||||
parts []objectPartInfo
|
||||
req *http.Request
|
||||
metadata map[string]string
|
||||
parts []objectPartInfo
|
||||
req *http.Request
|
||||
bucket, object string
|
||||
metadata map[string]string
|
||||
|
||||
partEncRelOffset int64
|
||||
|
||||
@@ -499,7 +537,7 @@ func (w *DecryptBlocksWriter) buildDecrypter(partID int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
objectEncryptionKey, err := decryptObjectInfo(key, m)
|
||||
objectEncryptionKey, err := decryptObjectInfo(key, w.bucket, w.object, m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -594,14 +632,14 @@ func (w *DecryptBlocksWriter) Close() error {
|
||||
// DecryptAllBlocksCopyRequest - setup a struct which can decrypt many concatenated encrypted data
|
||||
// parts information helps to know the boundaries of each encrypted data block, this function decrypts
|
||||
// all parts starting from part-1.
|
||||
func DecryptAllBlocksCopyRequest(client io.Writer, r *http.Request, objInfo ObjectInfo) (io.WriteCloser, int64, error) {
|
||||
w, _, size, err := DecryptBlocksRequest(client, r, 0, objInfo.Size, objInfo, true)
|
||||
func DecryptAllBlocksCopyRequest(client io.Writer, r *http.Request, bucket, object string, objInfo ObjectInfo) (io.WriteCloser, int64, error) {
|
||||
w, _, size, err := DecryptBlocksRequest(client, r, bucket, object, 0, objInfo.Size, objInfo, true)
|
||||
return w, size, err
|
||||
}
|
||||
|
||||
// DecryptBlocksRequest - setup a struct which can decrypt many concatenated encrypted data
|
||||
// parts information helps to know the boundaries of each encrypted data block.
|
||||
func DecryptBlocksRequest(client io.Writer, r *http.Request, startOffset, length int64, objInfo ObjectInfo, copySource bool) (io.WriteCloser, int64, int64, error) {
|
||||
func DecryptBlocksRequest(client io.Writer, r *http.Request, bucket, object string, startOffset, length int64, objInfo ObjectInfo, copySource bool) (io.WriteCloser, int64, int64, error) {
|
||||
seqNumber, encStartOffset, encLength := getEncryptedStartOffset(startOffset, length)
|
||||
|
||||
// Encryption length cannot be bigger than the file size, if it is
|
||||
@@ -614,9 +652,9 @@ func DecryptBlocksRequest(client io.Writer, r *http.Request, startOffset, length
|
||||
var writer io.WriteCloser
|
||||
var err error
|
||||
if copySource {
|
||||
writer, err = DecryptCopyRequest(client, r, objInfo.UserDefined)
|
||||
writer, err = DecryptCopyRequest(client, r, bucket, object, objInfo.UserDefined)
|
||||
} else {
|
||||
writer, err = DecryptRequestWithSequenceNumber(client, r, seqNumber, objInfo.UserDefined)
|
||||
writer, err = DecryptRequestWithSequenceNumber(client, r, bucket, object, seqNumber, objInfo.UserDefined)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
@@ -656,6 +694,8 @@ func DecryptBlocksRequest(client io.Writer, r *http.Request, startOffset, length
|
||||
parts: objInfo.Parts,
|
||||
partIndex: partStartIndex,
|
||||
req: r,
|
||||
bucket: bucket,
|
||||
object: object,
|
||||
customerKeyHeader: r.Header.Get(SSECustomerKey),
|
||||
copySource: copySource,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user