mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-11 16:57:45 +02:00
* s3api: reject SSE headers PutObject cannot honor, as CopyObject does PutObject and CreateMultipartUpload did not check the server-side encryption headers they were given. An x-amz-server-side-encryption value that names no method, such as "aes:kms", matched none of the SSE paths, so the object was stored unencrypted and the request answered 200. SSE-C together with x-amz-server-side-encryption was also accepted, and one of the two silently won. CopyObject already rejects both through validateEncryptionCompatibility. Run the same check, with the same error codes, before PutObject and CreateMultipartUpload store anything. * s3api: answer rejected SSE headers with InvalidArgument, as S3 does S3 rejects an unknown x-amz-server-side-encryption value and SSE-C combined with another method with InvalidArgument. PutObject and CreateMultipartUpload now return that code, with S3's messages, through two new error codes; CopyObject keeps its own. Also run ceph/s3-tests' test_put_obj_enc_conflict_c_s3, _c_kms and _bad_enc_kms in CI, which check both handlers' responses end to end. * s3api: close the remaining ways a PUT could skip requested encryption - A repeated x-amz-server-side-encryption header is rejected: the encryption paths apply only the first value, so extra values could hide the method the client asked for. - KMS options (key id, encryption context, bucket key) are rejected unless the method is aws:kms, matching the S3 InvalidArgument error. - CreateMultipartUpload validates before auto-create, so a refused upload cannot leave a bucket behind. - Directory markers encrypt their inline content through the shared SSE path and record the same entry metadata as regular objects, instead of storing requested-encrypted bytes in plaintext. * s3api: read back what the SSE marker write stores - Repeated SSE-C and KMS option headers are rejected alongside a repeated x-amz-server-side-encryption, closing the same first-value bypass for customer-key and KMS fields. - Algorithm validity is checked before KMS options so an unsupported value keeps the "not supported" error. - serveDirectoryContent decrypts marker content with the stored SSE metadata and returns the SSE headers, so an encrypted marker reads back what was written; its Content-Length now reflects the bytes actually served. * s3api: HEAD of a marker skips decryption and keeps the stored size HEAD returns no body, so it now runs only the SSE-C key check instead of decrypting — matching HeadObjectHandler and avoiding a KMS round-trip — and chunk-backed directory entries report Attributes.FileSize again rather than the length of their (empty) inline content. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3api: keep GET Content-Length to the bytes serveDirectoryContent writes The FileSize override described chunk-backed markers on HEAD, but GET sends only the inline content, so it promised bytes it never wrote. * s3api: stream a chunk-backed directory on GET like any object A directory promoted over an uploaded object keeps the object chunks with empty inline content, so serving only entry.Content made GET deliver nothing while HEAD reported FileSize. GET now routes those entries through the regular volume-server stream, keeping the two methods consistent. * s3api: answer a busy volume read with RequestBytesExceed --------- Co-authored-by: Chris Lu <chris.lu@gmail.com> Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
108 lines
5.0 KiB
Go
108 lines
5.0 KiB
Go
package s3api
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// TestValidateRequestEncryption verifies that a PutObject or
|
|
// CreateMultipartUpload request asking for an encryption method that cannot be
|
|
// honored is rejected instead of being stored unencrypted
|
|
func TestValidateRequestEncryption(t *testing.T) {
|
|
ssec := map[string]string{
|
|
s3_constants.AmzServerSideEncryptionCustomerAlgorithm: "AES256",
|
|
s3_constants.AmzServerSideEncryptionCustomerKey: "a2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2s=",
|
|
s3_constants.AmzServerSideEncryptionCustomerKeyMD5: "f6OQvGsmFBq4WOqaVcuO5w==",
|
|
}
|
|
testCases := []struct {
|
|
name string
|
|
headers map[string]string
|
|
sse string
|
|
sseValues []string
|
|
dup string
|
|
want s3err.ErrorCode
|
|
}{
|
|
{name: "no encryption", want: s3err.ErrNone},
|
|
{name: "SSE-S3", sse: s3_constants.SSEAlgorithmAES256, want: s3err.ErrNone},
|
|
{name: "SSE-KMS", sse: s3_constants.SSEAlgorithmKMS, want: s3err.ErrNone},
|
|
{name: "SSE-C", headers: ssec, want: s3err.ErrNone},
|
|
{name: "unknown algorithm", sse: "aes:kms", want: s3err.ErrInvalidEncryptionMethod},
|
|
{name: "misspelled AES256", sse: "AES-256", want: s3err.ErrInvalidEncryptionMethod},
|
|
{name: "SSE-C and SSE-S3", headers: ssec, sse: s3_constants.SSEAlgorithmAES256, want: s3err.ErrIncompatibleEncryptionMethod},
|
|
{name: "SSE-C and SSE-KMS", headers: ssec, sse: s3_constants.SSEAlgorithmKMS, want: s3err.ErrIncompatibleEncryptionMethod},
|
|
{name: "repeated algorithm header", sseValues: []string{"AES256", "aws:kms"}, want: s3err.ErrIncompatibleEncryptionMethod},
|
|
{name: "repeated identical algorithm", sseValues: []string{"AES256", "AES256"}, want: s3err.ErrIncompatibleEncryptionMethod},
|
|
{name: "empty value before KMS", sseValues: []string{"", "aws:kms"}, want: s3err.ErrIncompatibleEncryptionMethod},
|
|
{name: "SSE-S3 hidden behind empty value", sseValues: []string{"", "AES256"}, headers: ssec, want: s3err.ErrIncompatibleEncryptionMethod},
|
|
{name: "repeated SSE-C key header", headers: ssec, dup: s3_constants.AmzServerSideEncryptionCustomerKey, want: s3err.ErrIncompatibleEncryptionMethod},
|
|
{name: "repeated KMS key id", sse: s3_constants.SSEAlgorithmKMS, dup: s3_constants.AmzServerSideEncryptionAwsKmsKeyId, want: s3err.ErrIncompatibleEncryptionMethod},
|
|
{name: "SSE-S3 with KMS key id", sse: s3_constants.SSEAlgorithmAES256, headers: map[string]string{s3_constants.AmzServerSideEncryptionAwsKmsKeyId: "key-id"}, want: s3err.ErrIncompatibleEncryptionMethod},
|
|
{name: "KMS key id without method", headers: map[string]string{s3_constants.AmzServerSideEncryptionAwsKmsKeyId: "key-id"}, want: s3err.ErrIncompatibleEncryptionMethod},
|
|
{name: "KMS key id with aws:kms", sse: s3_constants.SSEAlgorithmKMS, headers: map[string]string{s3_constants.AmzServerSideEncryptionAwsKmsKeyId: "key-id"}, want: s3err.ErrNone},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
h := http.Header{}
|
|
for k, v := range tc.headers {
|
|
h.Set(k, v)
|
|
}
|
|
if tc.sse != "" {
|
|
h.Set(s3_constants.AmzServerSideEncryption, tc.sse)
|
|
}
|
|
for _, v := range tc.sseValues {
|
|
h.Add(s3_constants.AmzServerSideEncryption, v)
|
|
}
|
|
if tc.dup != "" {
|
|
h.Add(tc.dup, "first")
|
|
h.Add(tc.dup, "second")
|
|
}
|
|
assert.Equal(t, tc.want, ValidateRequestEncryption(h))
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDirectoryMarkerSSEC verifies marker content stored under SSE-C encrypts
|
|
// on write and decrypts on read through the stored entry metadata
|
|
func TestDirectoryMarkerSSEC(t *testing.T) {
|
|
ssecHeaders := func(r *http.Request) {
|
|
r.Header.Set(s3_constants.AmzServerSideEncryptionCustomerAlgorithm, "AES256")
|
|
r.Header.Set(s3_constants.AmzServerSideEncryptionCustomerKey, "a2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2s=")
|
|
r.Header.Set(s3_constants.AmzServerSideEncryptionCustomerKeyMD5, "mT2HRsMGJ5IX5C+0rreZ8Q==")
|
|
}
|
|
plaintext := []byte("directory marker content")
|
|
s3a := &S3ApiServer{}
|
|
|
|
putReq := httptest.NewRequest(http.MethodPut, "/bucket/dir/", nil)
|
|
ssecHeaders(putReq)
|
|
sseResult, errCode := s3a.handleAllSSEEncryption(putReq, bytes.NewReader(plaintext), 0)
|
|
assert.Equal(t, s3err.ErrNone, errCode)
|
|
encrypted, err := io.ReadAll(sseResult.DataReader)
|
|
assert.NoError(t, err)
|
|
assert.NotEqual(t, plaintext, encrypted)
|
|
|
|
entry := &filer_pb.Entry{Extended: map[string][]byte{}, Content: encrypted}
|
|
storeSSEMetadata(entry, sseResult)
|
|
|
|
sseType := s3a.detectPrimarySSEType(entry)
|
|
assert.Equal(t, s3_constants.SSETypeC, sseType)
|
|
|
|
getReq := httptest.NewRequest(http.MethodGet, "/bucket/dir/", nil)
|
|
ssecHeaders(getReq)
|
|
decrypted, errCode := s3a.decryptDirectoryContent(getReq, entry, sseType)
|
|
assert.Equal(t, s3err.ErrNone, errCode)
|
|
assert.Equal(t, plaintext, decrypted)
|
|
|
|
bareReq := httptest.NewRequest(http.MethodGet, "/bucket/dir/", nil)
|
|
_, errCode = s3a.decryptDirectoryContent(bareReq, entry, sseType)
|
|
assert.Equal(t, s3err.ErrSSECustomerKeyMissing, errCode)
|
|
}
|