fix: send the SSE-KMS context as a JSON object on federated copies

putOptsFromReq handed the parsed kms.Context straight to
encrypt.NewSSEKMS. kms.Context implements encoding.TextMarshaler, so the
SDK serialized it as a JSON string, and a request without a context
still produced one because the nil Context is a typed nil inside the
interface value and marshals to "{}". The receiving ParseHTTP rejects
both forms, so every federated CopyObject to an SSE-KMS destination
failed with InvalidArgument once the forwarded stream was correct.

Pass a plain map, or nothing when no context was requested, and cover
SSE-KMS destinations with and without an explicit context.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FodsDpa6VkghaeRE6WjmEe
Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
Feng Ruohang
2026-09-09 17:35:50 +08:00
parent af56d17630
commit cfefc049c1
2 changed files with 25 additions and 10 deletions
+10 -1
View File
@@ -433,7 +433,16 @@ func putOptsFromHeaders(ctx context.Context, hdr http.Header, metadata map[strin
if err != nil {
return ObjectOptions{}, err
}
sseKms, err := encrypt.NewSSEKMS(keyID, context)
// kms.Context implements encoding.TextMarshaler, so handing it to the
// SDK's interface{} parameter would serialize the context as a JSON
// string, which the receiving ParseHTTP rejects; a nil Context is a
// typed nil there and would be sent as "{}". Pass a plain map, or
// nothing when no context was requested.
var sdkContext any
if context != nil {
sdkContext = map[string]string(context)
}
sseKms, err := encrypt.NewSSEKMS(keyID, sdkContext)
if err != nil {
return ObjectOptions{}, err
}
+15 -9
View File
@@ -496,10 +496,11 @@ func testAPIFederatedCopyObjectInheritedChecksum(objectAPI ObjectLayer, instance
const federationTestKMSKeyID = "federation-test-key"
// federationSSEHeaders returns the request headers that select one server-side
// encryption kind: "plain", "s3", "kms" or "c". SSE-C keys derive from keyByte
// so a case can name two distinct customer keys. With copySource the SSE-C key
// is returned in its x-amz-copy-source-* form, the only kind a copy has to
// name for its source; the other kinds then return nothing.
// encryption kind: "plain", "s3", "kms", "kms-context" (SSE-KMS with an
// explicit encryption context) or "c". SSE-C keys derive from keyByte so a
// case can name two distinct customer keys. With copySource the SSE-C key is
// returned in its x-amz-copy-source-* form, the only kind a copy has to name
// for its source; the other kinds then return nothing.
func federationSSEHeaders(kind string, keyByte byte, copySource bool) map[string]string {
h := map[string]string{}
if copySource && kind != "c" {
@@ -509,9 +510,12 @@ func federationSSEHeaders(kind string, keyByte byte, copySource bool) map[string
case "plain":
case "s3":
h[xhttp.AmzServerSideEncryption] = xhttp.AmzEncryptionAES
case "kms":
case "kms", "kms-context":
h[xhttp.AmzServerSideEncryption] = xhttp.AmzEncryptionKMS
h[xhttp.AmzServerSideEncryptionKmsID] = federationTestKMSKeyID
if kind == "kms-context" {
h[xhttp.AmzServerSideEncryptionKmsContext] = base64.StdEncoding.EncodeToString([]byte(`{"tenant":"federation"}`))
}
case "c":
key := bytes.Repeat([]byte{keyByte}, 32)
sum := md5.Sum(key)
@@ -609,6 +613,8 @@ func testAPIFederatedCopyObjectSSE(objectAPI ObjectLayer, instanceType, bucketNa
pairs := []struct{ src, dst string }{
{"plain", "s3"}, {"s3", "plain"}, {"s3", "s3"},
{"plain", "c"}, {"c", "plain"}, {"c", "c"}, {"s3", "c"},
{"plain", "kms"}, {"kms", "plain"}, {"kms", "kms"},
{"plain", "kms-context"}, {"kms-context", "kms-context"},
}
const srcKeyByte, dstKeyByte = 0x11, 0x22
@@ -624,8 +630,8 @@ func testAPIFederatedCopyObjectSSE(objectAPI ObjectLayer, instanceType, bucketNa
if err != nil {
t.Fatalf("%s: GetObjectInfo(source) failed: %v", instanceType, err)
}
if got := federationStoredSSE(before.UserDefined); got != pair.src {
t.Fatalf("%s: source stored as %s, want %s", instanceType, got, pair.src)
if got, want := federationStoredSSE(before.UserDefined), strings.TrimSuffix(pair.src, "-context"); got != want {
t.Fatalf("%s: source stored as %s, want %s", instanceType, got, want)
}
if compressed := body.ext == ".txt" && pair.src != "c"; before.IsCompressed() != compressed {
t.Fatalf("%s: source compressed=%v, want %v", instanceType, before.IsCompressed(), compressed)
@@ -666,8 +672,8 @@ func testAPIFederatedCopyObjectSSE(objectAPI ObjectLayer, instanceType, bucketNa
if err != nil {
t.Fatalf("%s: GetObjectInfo(destination) failed: %v", instanceType, err)
}
if got := federationStoredSSE(after.UserDefined); got != pair.dst {
t.Fatalf("%s: destination stored as %s, want %s", instanceType, got, pair.dst)
if got, want := federationStoredSSE(after.UserDefined), strings.TrimSuffix(pair.dst, "-context"); got != want {
t.Fatalf("%s: destination stored as %s, want %s", instanceType, got, want)
}
if pair.dst != "plain" && !after.IsCompressed() {
once := ObjectInfo{Size: int64(len(body.data))}