s3api: allow unsigned SSE-C customer key headers on presigned requests (#11578)

* s3api: allow unsigned SSE-C customer key headers on presigned requests

AWS requires only x-amz-server-side-encryption-customer-algorithm to be signed on presigned URLs; the key and key-MD5 headers are supplied at request time. Since #9121 rejected any x-amz-* header outside SignedHeaders, SDK-generated presigned SSE-C requests (e.g. .NET GetPreSignedUrlRequest) fail with SignatureDoesNotMatch. Exempt the customer key and copy-source key headers for presigned requests only.

* s3api: test presigned SSE-C requests carrying unsigned key headers
This commit is contained in:
Chris Lu authored and GitHub committed 2026-10-03 12:33:52 +08:00
1 parent 0ca484c354
commit 793ce06b10
2 files changed
+80

No files matched your search

+13
View File
@@ -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
@@ -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