From ec43f4ca00c5d295ba6058aa3d178a53aa848d69 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Wed, 28 Oct 2026 23:01:48 -0700 Subject: [PATCH] s3: accept bare composite checksum on CompleteMultipartUpload (#12194) * s3: accept bare composite checksum on CompleteMultipartUpload Clients such as minio-go send the composite object checksum without the "-" suffix. Compare against the base64 part of the computed composite value as well as the full form. * s3: test object checksum matching with and without part suffix --- weed/s3api/filer_multipart.go | 14 ++++++++++++- weed/s3api/filer_multipart_checksum_test.go | 22 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/weed/s3api/filer_multipart.go b/weed/s3api/filer_multipart.go index 306f7f97a7..710b9db959 100644 --- a/weed/s3api/filer_multipart.go +++ b/weed/s3api/filer_multipart.go @@ -625,7 +625,7 @@ func (s3a *S3ApiServer) prepareMultipartCompletionState(r *http.Request, input * glog.Errorf("completeMultipartUpload: %s checksum computation failed: %v", checksumType, checksumErr) return nil, nil, s3err.ErrInvalidPart } - if objectChecksum := lookupHeaderOrQuery(r, query, checksumHeaderName); objectChecksum != "" && objectChecksum != checksumValue { + if objectChecksum := lookupHeaderOrQuery(r, query, checksumHeaderName); objectChecksum != "" && !matchesObjectChecksum(objectChecksum, checksumValue) { return nil, nil, s3err.ErrBadDigest } } @@ -1858,6 +1858,18 @@ func decodePartChecksum(partNumber int, entries []*filer_pb.Entry, checksumHeade return raw, entry, nil } +// matchesObjectChecksum accepts the computed checksum verbatim or, for a +// composite checksum, its bare base64 form without the "-N" part-count suffix. +func matchesObjectChecksum(provided, computed string) bool { + if provided == computed { + return true + } + if i := strings.LastIndexByte(computed, '-'); i > 0 { + return provided == computed[:i] + } + return false +} + // computeCompositeChecksum computes a composite checksum from per-part checksums. // It concatenates the raw (decoded) per-part checksums, hashes the result with the // same algorithm, and returns the value as "base64-N" where N is the part count. diff --git a/weed/s3api/filer_multipart_checksum_test.go b/weed/s3api/filer_multipart_checksum_test.go index 02a0f738b3..bdad59cadf 100644 --- a/weed/s3api/filer_multipart_checksum_test.go +++ b/weed/s3api/filer_multipart_checksum_test.go @@ -204,3 +204,25 @@ func TestCreateMultipartUploadResultChecksumHeaders(t *testing.T) { t.Fatalf("response %q carries checksum members in the XML body", encoded) } } + +// A client may send the composite object checksum with or without the "-N" +// part-count suffix (minio-go sends the bare form); both must match. +func TestMatchesObjectChecksum(t *testing.T) { + for _, tc := range []struct { + provided string + computed string + want bool + }{ + {"hSEgHA==-1", "hSEgHA==-1", true}, + {"hSEgHA==", "hSEgHA==-1", true}, + {"hSEgHA==-2", "hSEgHA==-1", false}, + {"AAAAAA==", "hSEgHA==-1", false}, + {"fx4FQw==", "fx4FQw==", true}, + {"fx4FQw==-1", "fx4FQw==", false}, + {"", "hSEgHA==-1", false}, + } { + if got := matchesObjectChecksum(tc.provided, tc.computed); got != tc.want { + t.Errorf("matchesObjectChecksum(%q, %q) = %v, want %v", tc.provided, tc.computed, got, tc.want) + } + } +}