diff --git a/cmd/erasure-multipart-fullobject_test.go b/cmd/erasure-multipart-fullobject_test.go index 0bcb14c4f..b9d71aa21 100644 --- a/cmd/erasure-multipart-fullobject_test.go +++ b/cmd/erasure-multipart-fullobject_test.go @@ -420,8 +420,74 @@ func testAPICompleteMultipartChecksumTypeMismatch(obj ObjectLayer, instanceType, t.Fatalf("%s: object was created despite a rejected checksum type", instanceType) } }) + + t.Run(test.name+"-type-only", func(t *testing.T) { + objectName := "type-mismatch/type-only-" + test.name + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, + typ.String(), test.createdType) + etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, typ, partData) + partCS := []string{mustChecksum(t, typ, partData[0]), mustChecksum(t, typ, partData[1])} + rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, etags, partCS, + map[string]string{xhttp.AmzChecksumType: test.providedType}) + if rec.Code != http.StatusBadRequest { + t.Fatalf("%s: checksum type-only mismatch returned %d, want 400", instanceType, rec.Code) + } + apiErr := apiError(t, rec) + if apiErr.Code != "BadDigest" { + t.Fatalf("%s: expected BadDigest, got %q", instanceType, apiErr.Code) + } + wantMessage := fmt.Sprintf("The checksum type %s does not match the multipart upload checksum type %s.", test.providedType, test.createdType) + if apiErr.Message != wantMessage { + t.Fatalf("%s: expected message %q, got %q", instanceType, wantMessage, apiErr.Message) + } + if _, err := obj.GetObjectInfo(t.Context(), bucketName, objectName, ObjectOptions{}); err == nil { + t.Fatalf("%s: object was created despite a rejected checksum type-only assertion", instanceType) + } + }) } + for _, test := range []struct { + name string + providedType string + withChecksum bool + }{ + {name: "unknown-type-only", providedType: "NOT_A_TYPE"}, + {name: "lowercase-type-only", providedType: "full_object"}, + {name: "unknown-with-checksum", providedType: "NOT_A_TYPE", withChecksum: true}, + } { + t.Run(test.name, func(t *testing.T) { + objectName := "type-mismatch/invalid-" + test.name + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, + typ.String(), xhttp.AmzChecksumTypeComposite) + etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, typ, partData) + partCS := []string{mustChecksum(t, typ, partData[0]), mustChecksum(t, typ, partData[1])} + headers := map[string]string{xhttp.AmzChecksumType: test.providedType} + if test.withChecksum { + headers[typ.Key()] = mustChecksum(t, typ, full) + } + rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, etags, partCS, headers) + if rec.Code != http.StatusBadRequest || apiErrorCode(t, rec) != "InvalidArgument" { + t.Fatalf("%s: invalid checksum type returned %d %s", instanceType, rec.Code, rec.Body.String()) + } + if _, err := obj.GetObjectInfo(t.Context(), bucketName, objectName, ObjectOptions{}); err == nil { + t.Fatalf("%s: object was created despite an invalid checksum type", instanceType) + } + }) + } + + t.Run("matching-type-only", func(t *testing.T) { + objectName := "type-mismatch/matching-type-only" + uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, + typ.String(), xhttp.AmzChecksumTypeComposite) + etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, typ, partData) + partCS := []string{mustChecksum(t, typ, partData[0]), mustChecksum(t, typ, partData[1])} + rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, etags, partCS, + map[string]string{xhttp.AmzChecksumType: xhttp.AmzChecksumTypeComposite}) + if rec.Code != http.StatusOK { + t.Fatalf("%s: matching checksum type-only assertion returned %d %s", instanceType, rec.Code, rec.Body.String()) + } + }) + t.Run("omitted-type-is-not-composite", func(t *testing.T) { objectName := "type-mismatch/omitted-type" uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, diff --git a/cmd/erasure-multipart.go b/cmd/erasure-multipart.go index 222c458b5..d99492368 100644 --- a/cmd/erasure-multipart.go +++ b/cmd/erasure-multipart.go @@ -1173,9 +1173,9 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str var checksumType hash.ChecksumType if cs := fi.Metadata[hash.MinIOMultipartChecksum]; cs != "" { checksumType = hash.NewChecksumType(cs, fi.Metadata[hash.MinIOMultipartChecksumType]) + expectedType := checksumType | hash.ChecksumMultipart | hash.ChecksumIncludesMultipart if opts.WantChecksum != nil { providedType := opts.WantChecksum.Type | hash.ChecksumMultipart | hash.ChecksumIncludesMultipart - expectedType := checksumType | hash.ChecksumMultipart | hash.ChecksumIncludesMultipart if providedType.Base() != expectedType.Base() { return oi, InvalidArgument{ Bucket: bucket, @@ -1183,8 +1183,16 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str Err: fmt.Errorf("checksum algorithm mismatch. got %q expected %q", providedType.String(), expectedType.String()), } } - if opts.wantChecksumTypeSet && providedType.ObjType() != expectedType.ObjType() { - return oi, completeMultipartChecksumTypeMismatch(providedType.ObjType(), expectedType.ObjType()) + } + if opts.wantChecksumType != "" { + providedObjectType := opts.wantChecksumType + // CRC64NVME is always canonicalized to FULL_OBJECT. Preserve this + // behavior until its exact AWS wire semantics have been probed. + if checksumType.Base().Is(hash.ChecksumCRC64NVME) { + providedObjectType = xhttp.AmzChecksumTypeFullObject + } + if providedObjectType != expectedType.ObjType() { + return oi, completeMultipartChecksumTypeMismatch(opts.wantChecksumType, expectedType.ObjType()) } } checksumType |= hash.ChecksumMultipart | hash.ChecksumIncludesMultipart diff --git a/cmd/object-api-interface.go b/cmd/object-api-interface.go index 35d353ca6..f8664310d 100644 --- a/cmd/object-api-interface.go +++ b/cmd/object-api-interface.go @@ -84,8 +84,8 @@ type ObjectOptions struct { Expiration ExpirationOptions LifecycleAuditEvent lcAuditEvent - WantChecksum *hash.Checksum // x-amz-checksum-XXX checksum sent to PutObject/ CompleteMultipartUpload. - wantChecksumTypeSet bool // x-amz-checksum-type was explicitly set on CompleteMultipartUpload. + WantChecksum *hash.Checksum // x-amz-checksum-XXX checksum sent to PutObject/ CompleteMultipartUpload. + wantChecksumType string // explicit x-amz-checksum-type value on CompleteMultipartUpload. WantServerSideChecksumType hash.ChecksumType // if set, we compute a server-side checksum of this type diff --git a/cmd/object-api-options.go b/cmd/object-api-options.go index 828a8ff00..0d496e811 100644 --- a/cmd/object-api-options.go +++ b/cmd/object-api-options.go @@ -469,11 +469,17 @@ func completeMultipartOpts(ctx context.Context, r *http.Request, bucket, object } } + opts.wantChecksumType = r.Header.Get(xhttp.AmzChecksumType) + switch opts.wantChecksumType { + case "", xhttp.AmzChecksumTypeComposite, xhttp.AmzChecksumTypeFullObject: + default: + return opts, hash.ErrInvalidChecksum + } + opts.WantChecksum, err = hash.GetContentChecksum(r.Header) if err != nil { return opts, err } - opts.wantChecksumTypeSet = r.Header.Get(xhttp.AmzChecksumType) != "" opts.MTime = mtime opts.UserDefined = make(map[string]string) // Transfer SSEC key in opts.EncryptFn