Repository: localstack/localstack

Issue:
S3 HeadObject does not respect ChecksumMode
As reported in https://github.com/localstack/localstack/issues/6659#issuecomment-1630932643_, `HeadObject` does not respect `ChecksumMode`. 


```shell
at 09:31:57 ➜ AWS_DEFAULT_REGION=us-west-1 aws --endpoint-url=http://localhost:4566 s3api create-bucket --bucket test-bucket --create-bucket-configuration LocationConstraint=us-west-1
{
    "Location": "http://test-bucket.s3.localhost.localstack.cloud:4566/"
}

at 09:32:13 ➜ aws --endpoint-url=http://localhost:4566 s3api put-object --bucket test-bucket --key file.json --body README.md --checksum-algorithm SHA256
{
    "ETag": "\"4bb2f1387449d690bff6336ea255909c\"",
    "ChecksumSHA256": "bzOikXXMUqRTCrpVq7Pt7LjJ0fjR3ccUfM19S/gnfqA="
}

# SHA256 missing here
at 09:32:20 ➜ aws --endpoint-url=http://localhost:4566 s3api head-object --bucket test-bucket --key file.json --checksum-mode Enabled
{
    "AcceptRanges": "bytes",
    "LastModified": "2023-07-11T13:32:15+00:00",
    "ContentLength": 1455,
    "ETag": "\"4bb2f1387449d690bff6336ea255909c\"",
    "ContentType": "binary/octet-stream",
    "Metadata": {}
}
```

Note the equivalent response from S3 actual includes the SHA256 value

```shell
at 10:21:23 ➜ aws s3api head-object --bucket test-bucket --key file.json --checksum-mode Enabled
{
    "AcceptRanges": "bytes",
    "Expiration": "expiry-date=\"Thu, 13 Jul 2023 00:00:00 GMT\", rule-id=\"all\"",
    "LastModified": "2023-07-11T14:21:09+00:00",
    "ContentLength": 1956,
    "ChecksumSHA256": "bj7fqCqtaF95M1ULwQRDJuHbL/Xd9NVMH6pua9OINcc=",
    "ETag": "\"f4be535d45019a4408dd5369986919f8\"",
    "ContentEncoding": "",
    "ContentType": "binary/octet-stream",
   
    "Metadata": {},
    
}
```

_Originally posted by @bdurrani in https://github.com/localstack/localstack/issues/6659#issuecomment-1630932643_
            


Discussion:

Resolving patch:
diff --git a/localstack/services/s3/provider.py b/localstack/services/s3/provider.py
--- a/localstack/services/s3/provider.py
+++ b/localstack/services/s3/provider.py
@@ -422,12 +422,20 @@ def head_object(
         response: HeadObjectOutput = call_moto(context)
         response["AcceptRanges"] = "bytes"
 
+        key = request["Key"]
+        bucket = request["Bucket"]
+        moto_backend = get_moto_s3_backend(context)
+        moto_bucket = get_bucket_from_moto(moto_backend, bucket=bucket)
+        key_object = get_key_from_moto_bucket(moto_bucket, key=key)
+
+        if checksum_algorithm := key_object.checksum_algorithm:
+            # this is a bug in AWS: it sets the content encoding header to an empty string (parity tested)
+            response["ContentEncoding"] = ""
+
+        if request.get("ChecksumMode") == "ENABLED" and checksum_algorithm:
+            response[f"Checksum{checksum_algorithm.upper()}"] = key_object.checksum_value  # noqa
+
         if not request.get("VersionId"):
-            key = request["Key"]
-            bucket = request["Bucket"]
-            moto_backend = get_moto_s3_backend(context)
-            moto_bucket = get_bucket_from_moto(moto_backend, bucket=bucket)
-            key_object = get_key_from_moto_bucket(moto_bucket, key=key)
             store = self.get_store(context)
             if (
                 bucket_lifecycle_config := store.bucket_lifecycle_configuration.get(


Test patch:
diff --git a/tests/integration/s3/test_s3.py b/tests/integration/s3/test_s3.py
--- a/tests/integration/s3/test_s3.py
+++ b/tests/integration/s3/test_s3.py
@@ -1132,7 +1132,7 @@ def test_put_object_checksum(self, s3_create_bucket, algorithm, snapshot, aws_cl
         response = aws_client.s3.put_object(**params)
         snapshot.match("put-object-generated", response)
         assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
-        if is_aws_cloud() or is_asf_provider():
+        if is_aws_cloud() or not is_old_provider():
             # get_object_attributes is not implemented in moto
             object_attrs = aws_client.s3.get_object_attributes(
                 Bucket=bucket,
@@ -1180,6 +1180,11 @@ def test_s3_get_object_checksum(self, s3_bucket, snapshot, algorithm, aws_client
         )
         snapshot.match("get-object-with-checksum", get_object_with_checksum)
 
+        head_object_with_checksum = aws_client.s3.get_object(
+            Bucket=s3_bucket, Key=key, ChecksumMode="ENABLED"
+        )
+        snapshot.match("head-object-with-checksum", head_object_with_checksum)
+
         object_attrs = aws_client.s3.get_object_attributes(
             Bucket=s3_bucket,
             Key=key,
diff --git a/tests/integration/s3/test_s3.snapshot.json b/tests/integration/s3/test_s3.snapshot.json
--- a/tests/integration/s3/test_s3.snapshot.json
+++ b/tests/integration/s3/test_s3.snapshot.json
@@ -4544,7 +4544,7 @@
     }
   },
   "tests/integration/s3/test_s3.py::TestS3::test_s3_get_object_checksum[SHA256]": {
-    "recorded-date": "10-05-2023, 12:34:01",
+    "recorded-date": "11-07-2023, 16:50:49",
     "recorded-content": {
       "put-object": {
         "ChecksumSHA256": "1YQo81vx2VFUl0q5ccWISq8AkSBQQ0WO80S82TmfdIQ=",
@@ -4586,6 +4586,22 @@
           "HTTPStatusCode": 200
         }
       },
+      "head-object-with-checksum": {
+        "AcceptRanges": "bytes",
+        "Body": "test-checksum",
+        "ChecksumSHA256": "1YQo81vx2VFUl0q5ccWISq8AkSBQQ0WO80S82TmfdIQ=",
+        "ContentEncoding": "",
+        "ContentLength": 13,
+        "ContentType": "binary/octet-stream",
+        "ETag": "\"f2081dd61dfa700a0fd5e29b9c3cc23d\"",
+        "LastModified": "datetime",
+        "Metadata": {},
+        "ServerSideEncryption": "AES256",
+        "ResponseMetadata": {
+          "HTTPHeaders": {},
+          "HTTPStatusCode": 200
+        }
+      },
       "get-object-attrs": {
         "Checksum": {
           "ChecksumSHA256": "1YQo81vx2VFUl0q5ccWISq8AkSBQQ0WO80S82TmfdIQ="
@@ -4599,7 +4615,7 @@
     }
   },
   "tests/integration/s3/test_s3.py::TestS3::test_s3_get_object_checksum[None]": {
-    "recorded-date": "10-05-2023, 12:34:06",
+    "recorded-date": "11-07-2023, 16:50:53",
     "recorded-content": {
       "put-object": {
         "ETag": "\"f2081dd61dfa700a0fd5e29b9c3cc23d\"",
@@ -4637,6 +4653,20 @@
           "HTTPStatusCode": 200
         }
       },
+      "head-object-with-checksum": {
+        "AcceptRanges": "bytes",
+        "Body": "test-checksum",
+        "ContentLength": 13,
+        "ContentType": "binary/octet-stream",
+        "ETag": "\"f2081dd61dfa700a0fd5e29b9c3cc23d\"",
+        "LastModified": "datetime",
+        "Metadata": {},
+        "ServerSideEncryption": "AES256",
+        "ResponseMetadata": {
+          "HTTPHeaders": {},
+          "HTTPStatusCode": 200
+        }
+      },
       "get-object-attrs": {
         "LastModified": "datetime",
         "ResponseMetadata": {
@@ -6598,7 +6628,7 @@
     }
   },
   "tests/integration/s3/test_s3.py::TestS3::test_s3_get_object_checksum[CRC32]": {
-    "recorded-date": "10-05-2023, 12:33:43",
+    "recorded-date": "11-07-2023, 16:50:37",
     "recorded-content": {
       "put-object": {
         "ChecksumCRC32": "lVk/nw==",
@@ -6640,6 +6670,22 @@
           "HTTPStatusCode": 200
         }
       },
+      "head-object-with-checksum": {
+        "AcceptRanges": "bytes",
+        "Body": "test-checksum",
+        "ChecksumCRC32": "lVk/nw==",
+        "ContentEncoding": "",
+        "ContentLength": 13,
+        "ContentType": "binary/octet-stream",
+        "ETag": "\"f2081dd61dfa700a0fd5e29b9c3cc23d\"",
+        "LastModified": "datetime",
+        "Metadata": {},
+        "ServerSideEncryption": "AES256",
+        "ResponseMetadata": {
+          "HTTPHeaders": {},
+          "HTTPStatusCode": 200
+        }
+      },
       "get-object-attrs": {
         "Checksum": {
           "ChecksumCRC32": "lVk/nw=="
@@ -6653,7 +6699,7 @@
     }
   },
   "tests/integration/s3/test_s3.py::TestS3::test_s3_get_object_checksum[CRC32C]": {
-    "recorded-date": "10-05-2023, 12:33:49",
+    "recorded-date": "11-07-2023, 16:50:41",
     "recorded-content": {
       "put-object": {
         "ChecksumCRC32C": "Fz3epA==",
@@ -6695,6 +6741,22 @@
           "HTTPStatusCode": 200
         }
       },
+      "head-object-with-checksum": {
+        "AcceptRanges": "bytes",
+        "Body": "test-checksum",
+        "ChecksumCRC32C": "Fz3epA==",
+        "ContentEncoding": "",
+        "ContentLength": 13,
+        "ContentType": "binary/octet-stream",
+        "ETag": "\"f2081dd61dfa700a0fd5e29b9c3cc23d\"",
+        "LastModified": "datetime",
+        "Metadata": {},
+        "ServerSideEncryption": "AES256",
+        "ResponseMetadata": {
+          "HTTPHeaders": {},
+          "HTTPStatusCode": 200
+        }
+      },
       "get-object-attrs": {
         "Checksum": {
           "ChecksumCRC32C": "Fz3epA=="
@@ -6708,7 +6770,7 @@
     }
   },
   "tests/integration/s3/test_s3.py::TestS3::test_s3_get_object_checksum[SHA1]": {
-    "recorded-date": "10-05-2023, 12:33:55",
+    "recorded-date": "11-07-2023, 16:50:45",
     "recorded-content": {
       "put-object": {
         "ChecksumSHA1": "jbXkHAsXUrubtL3dqDQ4w+7WXc0=",
@@ -6750,6 +6812,22 @@
           "HTTPStatusCode": 200
         }
       },
+      "head-object-with-checksum": {
+        "AcceptRanges": "bytes",
+        "Body": "test-checksum",
+        "ChecksumSHA1": "jbXkHAsXUrubtL3dqDQ4w+7WXc0=",
+        "ContentEncoding": "",
+        "ContentLength": 13,
+        "ContentType": "binary/octet-stream",
+        "ETag": "\"f2081dd61dfa700a0fd5e29b9c3cc23d\"",
+        "LastModified": "datetime",
+        "Metadata": {},
+        "ServerSideEncryption": "AES256",
+        "ResponseMetadata": {
+          "HTTPHeaders": {},
+          "HTTPStatusCode": 200
+        }
+      },
       "get-object-attrs": {
         "Checksum": {
           "ChecksumSHA1": "jbXkHAsXUrubtL3dqDQ4w+7WXc0="