s3: validate per-part checksums in CompleteMultipartUpload

Parse the Checksum* elements of each completed part and enforce what AWS
does for uploads created with x-amz-checksum-algorithm: every part must
carry a checksum in the complete request (InvalidRequest when missing,
BadDigest when it differs from the stored part checksum), and an
x-amz-checksum-type header must match the upload resolved checksum type
(BadDigest). Add the issue-11401 reproduction as a regression test.
This commit is contained in:
Chris Lu committed 2026-09-19 22:18:13 -07:00
1 parent 4eb30f3b5b
commit e9d2bdd02b
4 files changed
+168 -2

No files matched your search

+128
View File
@@ -3,6 +3,9 @@ package checksum_test
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"errors"
"testing"
"github.com/aws/aws-sdk-go-v2/aws"
@@ -10,6 +13,7 @@ import (
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/aws/smithy-go"
"github.com/stretchr/testify/require"
)
@@ -83,3 +87,127 @@ func TestMultipartPartInheritsChecksumAlgorithm(t *testing.T) {
require.Equal(t, types.ChecksumTypeComposite, done.ChecksumType)
require.NotEmpty(t, aws.ToString(done.ChecksumSHA256))
}
// Mimics the .NET repro in https://github.com/seaweedfs/seaweedfs/issues/11401:
// UploadPart carries an explicit client-computed x-amz-checksum-sha256 value,
// CompleteMultipartUpload echoes it back per part, and HeadObject returns the
// object checksum when x-amz-checksum-mode: ENABLED is sent (same as AWS).
func TestIssue11401(t *testing.T) {
client := getS3Client(t)
bucket := uniqueBucket()
createBucket(t, client, bucket)
defer cleanupBucket(t, client, bucket)
body := bytes.Repeat([]byte("x"), 1024)
checksum := base64.StdEncoding.EncodeToString(func() []byte { s := sha256.Sum256(body); return s[:] }())
key := "issue-11401"
create, err := client.CreateMultipartUpload(context.Background(), &s3.CreateMultipartUploadInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
ContentType: aws.String("application/octet-stream"),
ChecksumAlgorithm: types.ChecksumAlgorithmSha256,
})
require.NoError(t, err)
part, err := client.UploadPart(context.Background(), &s3.UploadPartInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
UploadId: create.UploadId,
PartNumber: aws.Int32(1),
Body: bytes.NewReader(body),
ChecksumSHA256: aws.String(checksum),
})
require.NoError(t, err)
require.Equal(t, checksum, aws.ToString(part.ChecksumSHA256))
done, err := client.CompleteMultipartUpload(context.Background(), &s3.CompleteMultipartUploadInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
UploadId: create.UploadId,
MultipartUpload: &types.CompletedMultipartUpload{Parts: []types.CompletedPart{{
ETag: part.ETag,
PartNumber: aws.Int32(1),
ChecksumSHA256: part.ChecksumSHA256,
}}},
})
require.NoError(t, err)
require.Equal(t, types.ChecksumTypeComposite, done.ChecksumType)
require.NotEmpty(t, aws.ToString(done.ChecksumSHA256))
head, err := client.HeadObject(context.Background(), &s3.HeadObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
ChecksumMode: types.ChecksumModeEnabled,
})
require.NoError(t, err)
require.Equal(t, aws.ToString(done.ChecksumSHA256), aws.ToString(head.ChecksumSHA256))
headNoMode, err := client.HeadObject(context.Background(), &s3.HeadObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
})
require.NoError(t, err)
require.Empty(t, aws.ToString(headNoMode.ChecksumSHA256))
}
// CompleteMultipartUpload validates the per-part checksums it is given, like
// AWS: missing checksums fail with InvalidRequest and wrong ones with
// BadDigest.
func TestCompleteMultipartUploadValidatesPartChecksums(t *testing.T) {
client := newWhenRequiredChecksumClient(t)
bucket := uniqueBucket()
createBucket(t, client, bucket)
defer cleanupBucket(t, client, bucket)
body := bytes.Repeat([]byte("z"), 1024)
key := "validate-parts"
create, err := client.CreateMultipartUpload(context.Background(), &s3.CreateMultipartUploadInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
ChecksumAlgorithm: types.ChecksumAlgorithmSha256,
})
require.NoError(t, err)
part, err := client.UploadPart(context.Background(), &s3.UploadPartInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
UploadId: create.UploadId,
PartNumber: aws.Int32(1),
Body: bytes.NewReader(body),
})
require.NoError(t, err)
require.NotEmpty(t, aws.ToString(part.ChecksumSHA256))
complete := func(checksum *string) error {
_, err := client.CompleteMultipartUpload(context.Background(), &s3.CompleteMultipartUploadInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
UploadId: create.UploadId,
MultipartUpload: &types.CompletedMultipartUpload{Parts: []types.CompletedPart{{
ETag: part.ETag,
PartNumber: aws.Int32(1),
ChecksumSHA256: checksum,
}}},
})
return err
}
var apiErr smithy.APIError
err = complete(nil)
require.Error(t, err)
require.True(t, errors.As(err, &apiErr))
require.Equal(t, "InvalidRequest", apiErr.ErrorCode())
wrong := base64.StdEncoding.EncodeToString(func() []byte { s := sha256.Sum256([]byte("other")); return s[:] }())
err = complete(aws.String(wrong))
require.Error(t, err)
require.True(t, errors.As(err, &apiErr))
require.Equal(t, "BadDigest", apiErr.ErrorCode())
require.NoError(t, complete(part.ChecksumSHA256))
}