Repository: minio/minio (language: go)

Issue:
Checksum not retrievable when provided as a trailing checksum via the AWS S3 SDK
## Expected Behavior
When putting an object via PutObject using the AWS S3 .NET SDK and requesting a trailing checksum by setting ChecksumAlgorithm (X-Amz-Sdk-Checksum-Algorithm) to "SHA256", I expect to be able to retrieve the checksum at a later point from either:

- GetObjectAttributes (by setting X-Amz-Object-Attributes: Checksum) - the checksum value should be included in the GetObjectAttributesResponse response body
- GetObject (by setting X-Amz-Checksum-Mode: ENABLED) - the checksum value should be included in the X-Amz-Checksum-SHA256 response header

The ChecksumAlgorithm should also be included in the ListObjectsV2 result but it too is missing.

Note: when providing a precalculated checksum to PutObject, the checksum value is able to be retrieved later via GetObject and GetObjectAttributes, however ChecksumAlgoritm is still missing from the ListObjectsV2 result.

## Current Behavior
When I upload a file via PutObject using a trailing checksum, I can see the X-Amz-Trailer request header correctly provided and I also see the trailing checksum correctly appended to the end of the upload request.  The PutObject response includes the X-Amz-Checksum-Sha256 response header as expected.

However, subsequent calls to ListObjectsV2, GetObjectAttributes and GetObject fail to include the checksum value (or the checksum algorithm in the case of ListObjectsV2) in their respective responses.

Running the same SDK code against an AWS S3 endpoint yields expected behaviour, that is:
- ListObjectsV2 includes 'ChecksumAlgorithm' in its xml response body
- GetObjectAttributes includes 'Checksum/ChecksumSHA256' in its xml response body
- GetObject includes the 'X-Amz-Checksum-Sha256' response header

## Environment
Version: RELEASE.2024-09-13T20-26-02Z (go1.22.7 linux/amd64)
Server setup and configuration: Docker on Ubuntu 20.04.5



Discussion / hints:
Got a PutObject repro.

ListObjectsV2 must have been added recently. Do not see it in [API docs](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html). Do you have an example of this working?

Fix patch:
diff --git a/cmd/erasure-object.go b/cmd/erasure-object.go
index de0c4082ff711..6c84c96eb0467 100644
--- a/cmd/erasure-object.go
+++ b/cmd/erasure-object.go
@@ -1329,10 +1329,6 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
 	}
 
 	fi.DataDir = mustGetUUID()
-	fi.Checksum = opts.WantChecksum.AppendTo(nil, nil)
-	if opts.EncryptFn != nil {
-		fi.Checksum = opts.EncryptFn("object-checksum", fi.Checksum)
-	}
 	if ckSum := userDefined[ReplicationSsecChecksumHeader]; ckSum != "" {
 		if v, err := base64.StdEncoding.DecodeString(ckSum); err == nil {
 			fi.Checksum = v
@@ -1460,7 +1456,13 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
 			actualSize = n
 		}
 	}
-
+	if fi.Checksum == nil {
+		// Trailing headers checksums should now be filled.
+		fi.Checksum = opts.WantChecksum.AppendTo(nil, nil)
+		if opts.EncryptFn != nil {
+			fi.Checksum = opts.EncryptFn("object-checksum", fi.Checksum)
+		}
+	}
 	for i, w := range writers {
 		if w == nil {
 			onlineDisks[i] = nil
@@ -1474,6 +1476,7 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
 		// No need to add checksum to part. We already have it on the object.
 		partsMetadata[i].AddObjectPart(1, "", n, actualSize, modTime, compIndex, nil)
 		partsMetadata[i].Versioned = opts.Versioned || opts.VersionSuspended
+		partsMetadata[i].Checksum = fi.Checksum
 	}
 
 	userDefined["etag"] = r.MD5CurrentHexString()
diff --git a/cmd/object-handlers.go b/cmd/object-handlers.go
index 00e74d9c5ae7c..3443ecd41a13c 100644
--- a/cmd/object-handlers.go
+++ b/cmd/object-handlers.go
@@ -1960,6 +1960,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
 		return
 	}
 	opts.IndexCB = idxCb
+	opts.WantChecksum = hashReader.Checksum()
 
 	if opts.PreserveETag != "" ||
 		r.Header.Get(xhttp.IfMatch) != "" ||
diff --git a/internal/hash/checksum.go b/internal/hash/checksum.go
index 1466cec137551..a169e5acf36c5 100644
--- a/internal/hash/checksum.go
+++ b/internal/hash/checksum.go
@@ -335,6 +335,10 @@ func (c *Checksum) AppendTo(b []byte, parts []byte) []byte {
 	var tmp [binary.MaxVarintLen32]byte
 	n := binary.PutUvarint(tmp[:], uint64(c.Type))
 	crc := c.Raw
+	if c.Type.Trailing() {
+		// When we serialize we don't care if it was trailing.
+		c.Type ^= ChecksumTrailing
+	}
 	if len(crc) != c.Type.RawByteLen() {
 		return b
 	}
diff --git a/internal/hash/reader.go b/internal/hash/reader.go
index f849a69f0fa53..272452f06a23b 100644
--- a/internal/hash/reader.go
+++ b/internal/hash/reader.go
@@ -366,6 +366,14 @@ func (r *Reader) ContentCRC() map[string]string {
 	return map[string]string{r.contentHash.Type.String(): r.contentHash.Encoded}
 }
 
+// Checksum returns the content checksum if set.
+func (r *Reader) Checksum() *Checksum {
+	if !r.contentHash.Type.IsSet() || !r.contentHash.Valid() {
+		return nil
+	}
+	return &r.contentHash
+}
+
 var _ io.Closer = (*Reader)(nil) // compiler check
 
 // Close and release resources.


Test patch: