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
"-<parts>" 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
This commit is contained in:
Chris Lu authored and GitHub committed 2026-10-28 23:01:48 -07:00
1 parent 4ed9595a58
commit ec43f4ca00
2 files changed
+35 -1

No files matched your search

+13 -1
View File
@@ -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.
@@ -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)
}
}
}