diff --git a/cmd/erasure-multipart-fullobject_test.go b/cmd/erasure-multipart-fullobject_test.go index c8f3dde11..bb196acda 100644 --- a/cmd/erasure-multipart-fullobject_test.go +++ b/cmd/erasure-multipart-fullobject_test.go @@ -516,17 +516,33 @@ func testAPICompleteMultipartChecksumTypeMismatch(obj ObjectLayer, instanceType, } }) - t.Run("crc64nvme-composite-completion-is-rejected", func(t *testing.T) { + t.Run("crc64nvme-composite-is-rejected", func(t *testing.T) { crc64Type := hash.ChecksumCRC64NVME objectName := "type-mismatch/crc64nvme-composite" - uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, - crc64Type.String(), xhttp.AmzChecksumTypeComposite) - etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, crc64Type, partData) - rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, etags, nil, - map[string]string{ - crc64Type.Key(): mustChecksum(t, crc64Type, full), + req, err := newTestSignedRequestV4(http.MethodPost, getNewMultipartURL("", bucketName, objectName), + 0, nil, credentials.AccessKey, credentials.SecretKey, map[string]string{ + xhttp.AmzChecksumAlgo: crc64Type.String(), xhttp.AmzChecksumType: xhttp.AmzChecksumTypeComposite, }) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusBadRequest || apiErrorCode(t, rec) != "InvalidArgument" { + t.Fatalf("%s: CRC64NVME/COMPOSITE returned %d %s", instanceType, rec.Code, rec.Body.String()) + } + }) + + t.Run("crc64nvme-composite-completion-is-rejected", func(t *testing.T) { + crc64Type := hash.ChecksumCRC64NVME + objectName := "type-mismatch/crc64nvme-composite-completion" + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, + crc64Type.String(), xhttp.AmzChecksumTypeFullObject) + etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, crc64Type, partData) + partCS := []string{mustChecksum(t, crc64Type, partData[0]), mustChecksum(t, crc64Type, partData[1])} + rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, etags, partCS, + map[string]string{xhttp.AmzChecksumType: xhttp.AmzChecksumTypeComposite}) if rec.Code != http.StatusBadRequest || apiErrorCode(t, rec) != "BadDigest" { t.Fatalf("%s: CRC64NVME composite completion returned %d %s", instanceType, rec.Code, rec.Body.String()) } diff --git a/cmd/object-crc64-composite_test.go b/cmd/object-crc64-composite_test.go new file mode 100644 index 000000000..70a80370d --- /dev/null +++ b/cmd/object-crc64-composite_test.go @@ -0,0 +1,79 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + "github.com/minio/minio/internal/auth" + "github.com/minio/minio/internal/hash" + xhttp "github.com/minio/minio/internal/http" +) + +func TestAPIPutObjectRejectsCRC64Composite(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIPutObjectRejectsCRC64Composite, + endpoints: []string{"PutObject"}, + }) +} + +func testAPIPutObjectRejectsCRC64Composite(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + data := []byte("crc64-composite") + object := "checksums/crc64-composite" + req, err := newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", bucketName, object), + int64(len(data)), bytes.NewReader(data), credentials.AccessKey, credentials.SecretKey, map[string]string{ + xhttp.AmzChecksumCRC64NVME: mustChecksum(t, hash.ChecksumCRC64NVME, data), + xhttp.AmzChecksumType: xhttp.AmzChecksumTypeComposite, + }) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusBadRequest || apiErrorCode(t, rec) != "InvalidArgument" { + t.Fatalf("%s: CRC64NVME/COMPOSITE PutObject returned %d %s", instanceType, rec.Code, rec.Body.String()) + } + if _, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}); !isErrObjectNotFound(err) { + t.Fatalf("%s: rejected PutObject stored an object: %v", instanceType, err) + } + + trailerObject := "checksums/crc64-composite-trailer" + req, err = newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", bucketName, trailerObject), + int64(len(data)), bytes.NewReader(data), credentials.AccessKey, credentials.SecretKey, map[string]string{ + xhttp.AmzTrailer: xhttp.AmzChecksumCRC64NVME, + xhttp.AmzChecksumType: xhttp.AmzChecksumTypeComposite, + }) + if err != nil { + t.Fatal(err) + } + rec = httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusBadRequest || apiErrorCode(t, rec) != "InvalidArgument" { + t.Fatalf("%s: trailing CRC64NVME/COMPOSITE PutObject returned %d %s", instanceType, rec.Code, rec.Body.String()) + } + if _, err := obj.GetObjectInfo(t.Context(), bucketName, trailerObject, ObjectOptions{}); !isErrObjectNotFound(err) { + t.Fatalf("%s: rejected trailing PutObject stored an object: %v", instanceType, err) + } +} diff --git a/internal/hash/checksum.go b/internal/hash/checksum.go index 32d106d90..e4ced2fba 100644 --- a/internal/hash/checksum.go +++ b/internal/hash/checksum.go @@ -157,7 +157,6 @@ func ChecksumStringToType(alg string) ChecksumType { case "SHA256": return ChecksumSHA256 case "CRC64NVME": - // AWS seems to ignore full value, and just assume it. return ChecksumCRC64NVME case "": return ChecksumNone @@ -192,7 +191,9 @@ func NewChecksumType(alg, objType string) ChecksumType { } return ChecksumSHA256 case "CRC64NVME": - // AWS seems to ignore full value, and just assume it. + if objType == xhttp.AmzChecksumTypeComposite { + return ChecksumInvalid + } return ChecksumCRC64NVME case "": if full != 0 { @@ -715,7 +716,11 @@ func GetContentChecksum(h http.Header) (*Checksum, error) { return nil, ErrInvalidChecksum } res.Type |= ChecksumFullObject - case xhttp.AmzChecksumTypeComposite, "": + case xhttp.AmzChecksumTypeComposite: + if res.Type.Base().Is(ChecksumCRC64NVME) { + return nil, ErrInvalidChecksum + } + case "": default: return nil, ErrInvalidChecksum } @@ -781,5 +786,8 @@ func getContentChecksum(h http.Header) (t ChecksumType, s string) { for _, t := range BaseChecksumTypes { checkType(t) } + if t.Base().Is(ChecksumCRC64NVME) && h.Get(xhttp.AmzChecksumType) == xhttp.AmzChecksumTypeComposite { + return ChecksumInvalid, "" + } return t, s } diff --git a/internal/hash/checksum_test.go b/internal/hash/checksum_test.go index 9ea81c967..595302818 100644 --- a/internal/hash/checksum_test.go +++ b/internal/hash/checksum_test.go @@ -67,6 +67,9 @@ func TestGetContentChecksumRejectsUnsupportedHeaders(t *testing.T) { // TestChecksumAddToHeader tests that adding and retrieving a checksum on a header works func TestChecksumAddToHeader(t *testing.T) { + if got := NewChecksumType("CRC64NVME", xhttp.AmzChecksumTypeComposite); !got.Is(ChecksumInvalid) { + t.Fatalf("CRC64NVME/COMPOSITE = %s, want invalid", got.StringFull()) + } tests := []struct { name string checksum ChecksumType @@ -147,6 +150,16 @@ func TestChecksumAddToHeader(t *testing.T) { } } +func TestCRC64NVMECompositeTrailerIsInvalid(t *testing.T) { + h := http.Header{} + h.Set(xhttp.AmzTrailer, ChecksumCRC64NVME.Key()) + h.Set(xhttp.AmzChecksumType, xhttp.AmzChecksumTypeComposite) + _, err := GetContentChecksum(h) + if !errors.Is(err, ErrInvalidChecksum) { + t.Fatalf("CRC64NVME/COMPOSITE trailer error = %v, want ErrInvalidChecksum", err) + } +} + // TestChecksumSerializeDeserialize checks AppendTo can be reversed by ChecksumFromBytes func TestChecksumSerializeDeserialize(t *testing.T) { myData := []byte("this-is-a-checksum-data-test")