diff --git a/weed/s3api/auth_signature_v4.go b/weed/s3api/auth_signature_v4.go index 7a9f55768..b2067d5a0 100644 --- a/weed/s3api/auth_signature_v4.go +++ b/weed/s3api/auth_signature_v4.go @@ -848,6 +848,16 @@ var presignedSigV4ProtocolHeaders = map[string]struct{}{ "x-amz-signature": {}, } +// presignedUnsignedSSECHeaders exempts SSE-C key material that AWS requires to +// be sent but not signed on presigned requests: only the algorithm header is +// part of the signature calculation. +var presignedUnsignedSSECHeaders = map[string]struct{}{ + strings.ToLower(s3_constants.AmzServerSideEncryptionCustomerKey): {}, + strings.ToLower(s3_constants.AmzServerSideEncryptionCustomerKeyMD5): {}, + strings.ToLower(s3_constants.AmzCopySourceServerSideEncryptionCustomerKey): {}, + strings.ToLower(s3_constants.AmzCopySourceServerSideEncryptionCustomerKeyMD5): {}, +} + // verifySignedHeadersCoverage rejects requests that carry x-amz-* headers // outside of the SignedHeaders list. AWS SigV4 requires every x-amz-* header // present in the request to be covered by the signature; without this check a @@ -874,6 +884,9 @@ func verifySignedHeadersCoverage(r *http.Request, signedHeaders []string, isPres if _, exempt := presignedSigV4ProtocolHeaders[lower]; exempt { continue } + if _, exempt := presignedUnsignedSSECHeaders[lower]; exempt { + continue + } } glog.V(2).Infof("reject %s %s: unsigned header %q not in SignedHeaders", r.Method, r.URL.Path, name) return s3err.ErrSignatureDoesNotMatch diff --git a/weed/s3api/auth_signature_v4_unsigned_headers_test.go b/weed/s3api/auth_signature_v4_unsigned_headers_test.go index a0397fad3..a9ccdb72f 100644 --- a/weed/s3api/auth_signature_v4_unsigned_headers_test.go +++ b/weed/s3api/auth_signature_v4_unsigned_headers_test.go @@ -142,6 +142,50 @@ func TestVerifySignedHeadersCoverage_Unit(t *testing.T) { signedHeaders: []string{"HOST", "X-Amz-Tagging"}, want: s3err.ErrNone, }, + { + name: "presigned exempts unsigned SSE-C key headers", + headers: map[string]string{ + "X-Amz-Server-Side-Encryption-Customer-Algorithm": "AES256", + "X-Amz-Server-Side-Encryption-Customer-Key": "key", + "X-Amz-Server-Side-Encryption-Customer-Key-MD5": "md5", + }, + signedHeaders: []string{"host", "x-amz-server-side-encryption-customer-algorithm"}, + isPresigned: true, + want: s3err.ErrNone, + }, + { + name: "presigned exempts unsigned SSE-C copy-source key headers", + headers: map[string]string{ + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": "AES256", + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": "key", + "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-MD5": "md5", + }, + signedHeaders: []string{"host", "x-amz-copy-source-server-side-encryption-customer-algorithm"}, + isPresigned: true, + want: s3err.ErrNone, + }, + { + name: "presigned still requires SSE-C algorithm to be signed", + headers: map[string]string{ + "X-Amz-Server-Side-Encryption-Customer-Algorithm": "AES256", + "X-Amz-Server-Side-Encryption-Customer-Key": "key", + "X-Amz-Server-Side-Encryption-Customer-Key-MD5": "md5", + }, + signedHeaders: []string{"host"}, + isPresigned: true, + want: s3err.ErrSignatureDoesNotMatch, + }, + { + name: "header-based does NOT exempt unsigned SSE-C key headers", + headers: map[string]string{ + "X-Amz-Server-Side-Encryption-Customer-Algorithm": "AES256", + "X-Amz-Server-Side-Encryption-Customer-Key": "key", + "X-Amz-Server-Side-Encryption-Customer-Key-MD5": "md5", + }, + signedHeaders: []string{"host", "x-amz-server-side-encryption-customer-algorithm"}, + isPresigned: false, + want: s3err.ErrSignatureDoesNotMatch, + }, } for _, tt := range tests { @@ -203,6 +247,29 @@ func TestPresignedPutAcceptsSignedTagging(t *testing.T) { } } +// TestPresignedGetAcceptsUnsignedSSECKeyHeaders mirrors the AWS SDK presign +// flow for SSE-C objects: only the algorithm header is signed while the key +// and key-MD5 headers are attached to the request unsigned. +func TestPresignedGetAcceptsUnsignedSSECKeyHeaders(t *testing.T) { + iam := newTestIAM() + + req, err := newTestRequest(http.MethodGet, "http://127.0.0.1:9000/bucket/key", 0, nil) + if err != nil { + t.Fatalf("newTestRequest: %v", err) + } + req.Header.Set("X-Amz-Server-Side-Encryption-Customer-Algorithm", "AES256") + if err := preSignV4WithHeaders(iam, req, "AKIAIOSFODNN7EXAMPLE", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", 600, []string{"host", "x-amz-server-side-encryption-customer-algorithm"}); err != nil { + t.Fatalf("preSignV4WithHeaders: %v", err) + } + req.Header.Set("X-Amz-Server-Side-Encryption-Customer-Key", "MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE=") + req.Header.Set("X-Amz-Server-Side-Encryption-Customer-Key-MD5", "md5") + + _, errCode := iam.reqSignatureV4Verify(req) + if errCode != s3err.ErrNone { + t.Fatalf("expected ErrNone for presigned SSE-C with unsigned key headers, got %v", errCode) + } +} + // preSignV4WithHeaders is a test helper that builds a presigned URL whose // SignedHeaders list covers the specified headers (which must already be set // on the request), then computes the signature over those headers and