diff --git a/.changes/nextrelease/checksumsv2.json b/.changes/nextrelease/checksumsv2.json
new file mode 100644
index 0000000000..a1bd462185
--- /dev/null
+++ b/.changes/nextrelease/checksumsv2.json
@@ -0,0 +1,7 @@
+[
+  {
+    "type": "feature",
+    "category": "S3",
+    "description": "Adds a default checksum of CRC32 to operations that support checksums. Adds additional configuration for request checksum calculation and response checksum validation."
+  }
+]
diff --git a/features/multipart/s3.feature b/features/multipart/s3.feature
index 52f564b685..200d27a7d6 100644
--- a/features/multipart/s3.feature
+++ b/features/multipart/s3.feature
@@ -13,6 +13,20 @@ Feature: S3 Multipart Uploads
       | seekable     | 3           |
       | non-seekable | 3           |
 
+  Scenario Outline: Uploading a stream with checksum algorithm
+    Given I have a <seekable> read stream
+    When I upload the stream to S3 with a checksum algorithm of "<checksumalgorithm>"
+    Then the result should contain a(n) "Checksum<checksumalgorithm>"
+
+    Examples:
+      | seekable     | checksumalgorithm |
+      | seekable     | crc32             |
+      | non-seekable | crc32             |
+      | seekable     | sha256            |
+      | non-seekable | sha256            |
+      | seekable     | sha1              |
+      | non-seekable | sha1              |
+
   Scenario Outline: Copying a file
     Given I have an s3 client and an uploaded file named "<filename>"
     When I call multipartCopy on "<filename>" to a new key in the same bucket
@@ -22,4 +36,3 @@ Feature: S3 Multipart Uploads
       | filename     |
       | the-file     |
       | the-?-file   |
-
diff --git a/src/S3/ApplyChecksumMiddleware.php b/src/S3/ApplyChecksumMiddleware.php
index 085d288a7c..ed3b9bf73a 100644
--- a/src/S3/ApplyChecksumMiddleware.php
+++ b/src/S3/ApplyChecksumMiddleware.php
@@ -2,6 +2,7 @@
 namespace Aws\S3;
 
 use Aws\Api\Service;
+use Aws\Api\Shape;
 use Aws\CommandInterface;
 use Aws\MetricsBuilder;
 use GuzzleHttp\Psr7;
@@ -19,14 +20,28 @@
 class ApplyChecksumMiddleware
 {
     use CalculatesChecksumTrait;
-    private static $sha256AndMd5 = [
-        'PutObject',
-        'UploadPart',
+
+    public const DEFAULT_CALCULATION_MODE = 'when_supported';
+    public const DEFAULT_ALGORITHM = 'crc32';
+
+    /**
+     * @var true[]
+     *
+     * S3 Operations for which pre-calculated SHA256
+     * Checksums can be added to the command
+     */
+    public static $sha256 = [
+        'PutObject' => true,
+        'UploadPart' => true,
     ];
 
     /** @var Service */
     private $api;
 
+    /** @var array */
+    private $config;
+
+    /** @var callable */
     private $nextHandler;
 
     /**
@@ -35,17 +50,22 @@ class ApplyChecksumMiddleware
      * @param Service $api
      * @return callable
      */
-    public static function wrap(Service $api)
+    public static function wrap(Service $api, array $config = [])
     {
-        return function (callable $handler) use ($api) {
-            return new self($handler, $api);
+        return function (callable $handler) use ($api, $config) {
+            return new self($handler, $api, $config);
         };
     }
 
-    public function __construct(callable $nextHandler, Service $api)
+    public function __construct(
+        callable $nextHandler,
+        Service $api, 
+        array $config = []
+    )
     {
         $this->api = $api;
         $this->nextHandler = $nextHandler;
+        $this->config = $config;
     }
 
     public function __invoke(
@@ -55,68 +75,46 @@ public function __invoke(
         $next = $this->nextHandler;
         $name = $command->getName();
         $body = $request->getBody();
-
-        //Checks if AddContentMD5 has been specified for PutObject or UploadPart
-        $addContentMD5 = $command['AddContentMD5'] ?? null;
-
-        $op = $this->api->getOperation($command->getName());
-
-        $checksumInfo = $op['httpChecksum'] ?? [];
-        $checksumMemberName = array_key_exists('requestAlgorithmMember', $checksumInfo)
-            ? $checksumInfo['requestAlgorithmMember']
-            : "";
+        $operation = $this->api->getOperation($name);
+        $mode = $this->config['request_checksum_calculation']
+            ?? self::DEFAULT_CALCULATION_MODE;
+
+        // Trigger warning if AddContentMD5 is specified for PutObject or UploadPart
+        $this->handleDeprecatedAddContentMD5($command);
+
+        $checksumInfo = $operation['httpChecksum'] ?? [];
+        $checksumMemberName = $checksumInfo['requestAlgorithmMember'] ?? '';
+        $checksumMember = !empty($checksumMemberName)
+            ? $operation->getInput()->getMember($checksumMemberName)
+            : null;
+        $checksumRequired = $checksumInfo['requestChecksumRequired'] ?? false;
         $requestedAlgorithm = $command[$checksumMemberName] ?? null;
-        if (!empty($checksumMemberName) && !empty($requestedAlgorithm)) {
-            $requestedAlgorithm = strtolower($requestedAlgorithm);
-            $checksumMember = $op->getInput()->getMember($checksumMemberName);
-            $supportedAlgorithms = isset($checksumMember['enum'])
-                ? array_map('strtolower', $checksumMember['enum'])
-                : null;
-            if (is_array($supportedAlgorithms)
-                && in_array($requestedAlgorithm, $supportedAlgorithms)
-            ) {
-                $request = $this->addAlgorithmHeader($requestedAlgorithm, $request, $body);
-            } else {
-                throw new InvalidArgumentException(
-                    "Unsupported algorithm supplied for input variable {$checksumMemberName}."
-                    . "  Supported checksums for this operation include: "
-                    . implode(", ", $supportedAlgorithms) . "."
-                );
-            }
-
-            $command->getMetricsBuilder()->identifyMetricByValueAndAppend(
-                'request_checksum',
-                $requestedAlgorithm
-            );
 
-            return $next($command, $request);
-        }
+        $shouldAddChecksum = $this->shouldAddChecksum(
+            $mode,
+            $checksumRequired,
+            $checksumMember,
+            $requestedAlgorithm
+        );
+        if ($shouldAddChecksum) {
+            if (!$this->hasAlgorithmHeader($request)) {
+                $supportedAlgorithms =  array_map('strtolower', $checksumMember['enum'] ?? []);
+                $algorithm = $this->determineChecksumAlgorithm(
+                    $supportedAlgorithms,
+                    $requestedAlgorithm,
+                    $checksumMemberName
+                );
+                $request = $this->addAlgorithmHeader($algorithm, $request, $body);
 
-        if (!empty($checksumInfo)) {
-        //if the checksum member is absent, check if it's required
-        $checksumRequired = $checksumInfo['requestChecksumRequired'] ?? null;
-            if ((!empty($checksumRequired))
-                || (in_array($name, self::$sha256AndMd5) && $addContentMD5)
-            ) {
-                //S3Express doesn't support MD5; default to crc32 instead
-                if ($this->isS3Express($command)) {
-                    $request = $this->addAlgorithmHeader('crc32', $request, $body);
-                    $command->getMetricsBuilder()->append(
-                        MetricsBuilder::FLEXIBLE_CHECKSUMS_REQ_CRC32
-                    );
-                } elseif (!$request->hasHeader('Content-MD5')) {
-                    // Set the content MD5 header for operations that require it.
-                    $request = $request->withHeader(
-                        'Content-MD5',
-                        base64_encode(Psr7\Utils::hash($body, 'md5', true))
-                    );
-                }
-                return $next($command, $request);
+                $command->getMetricsBuilder()->identifyMetricByValueAndAppend(
+                    'request_checksum',
+                    $algorithm
+                );
             }
         }
 
-        if (in_array($name, self::$sha256AndMd5) && $command['ContentSHA256']) {
-            // Set the content hash header if provided in the parameters.
+        // Set the content hash header if ContentSHA256 is provided
+        if (isset(self::$sha256[$name]) && $command['ContentSHA256']) {
             $request = $request->withHeader(
                 'X-Amz-Content-Sha256',
                 $command['ContentSHA256']
@@ -129,32 +127,111 @@ public function __invoke(
         return $next($command, $request);
     }
 
+    /**
+     * @param CommandInterface $command
+     *
+     * @return void
+     */
+    private function handleDeprecatedAddContentMD5(CommandInterface $command): void
+    {
+        if (!empty($command['AddContentMD5'])) {
+            trigger_error(
+                'S3 no longer supports MD5 checksums. ' .
+                'A CRC32 checksum will be computed and applied on your behalf.',
+                E_USER_DEPRECATED
+            );
+            $command['ChecksumAlgorithm'] = self::DEFAULT_ALGORITHM;
+        }
+    }
+
+    /**
+     * @param string $mode
+     * @param Shape|null $checksumMember
+     * @param string $name
+     * @param bool $checksumRequired
+     * @param string|null $requestedAlgorithm
+     *
+     * @return bool
+     */
+    private function shouldAddChecksum(
+        string $mode,
+        bool $checksumRequired,
+        ?Shape $checksumMember,
+        ?string $requestedAlgorithm
+    ): bool
+    {
+        return ($mode === 'when_supported' && $checksumMember)
+            || ($mode === 'when_required'
+                && ($checksumRequired || ($checksumMember && $requestedAlgorithm)));
+    }
+
+    /**
+     * @param Shape|null $checksumMember
+     * @param string|null $requestedAlgorithm
+     * @param string|null $checksumMemberName
+     *
+     * @return string
+     */
+    private function determineChecksumAlgorithm(
+        array $supportedAlgorithms,
+        ?string $requestedAlgorithm,
+        ?string $checksumMemberName
+    ): string
+    {
+        $algorithm = self::DEFAULT_ALGORITHM;
+
+        if ($requestedAlgorithm) {
+            $requestedAlgorithm = strtolower($requestedAlgorithm);
+            if (!in_array($requestedAlgorithm, $supportedAlgorithms)) {
+                throw new InvalidArgumentException(
+                    "Unsupported algorithm supplied for input variable {$checksumMemberName}. " .
+                    "Supported checksums for this operation include: "
+                    . implode(", ", $supportedAlgorithms) . "."
+                );
+            }
+            $algorithm = $requestedAlgorithm;
+        }
+
+        return $algorithm;
+    }
+
     /**
      * @param string $requestedAlgorithm
      * @param RequestInterface $request
      * @param StreamInterface $body
+     *
      * @return RequestInterface
      */
     private function addAlgorithmHeader(
         string $requestedAlgorithm,
         RequestInterface $request,
         StreamInterface $body
-    ) {
+    ): RequestInterface
+    {
         $headerName = "x-amz-checksum-{$requestedAlgorithm}";
         if (!$request->hasHeader($headerName)) {
-            $encoded = $this->getEncodedValue($requestedAlgorithm, $body);
+            $encoded = self::getEncodedValue($requestedAlgorithm, $body);
             $request = $request->withHeader($headerName, $encoded);
         }
+
         return $request;
     }
 
     /**
-     * @param CommandInterface $command
+     * @param RequestInterface $request
+     *
      * @return bool
      */
-    private function isS3Express(CommandInterface $command): bool
+    private function hasAlgorithmHeader(RequestInterface $request): bool
     {
-        return isset($command['@context']['signing_service'])
-            && $command['@context']['signing_service'] === 's3express';
+        $headers = $request->getHeaders();
+
+        foreach ($headers as $name => $values) {
+            if (stripos($name, 'x-amz-checksum-') === 0) {
+                return true;
+            }
+        }
+
+        return false;
     }
 }
diff --git a/src/S3/CalculatesChecksumTrait.php b/src/S3/CalculatesChecksumTrait.php
index 3d0179ee64..6b2b19413c 100644
--- a/src/S3/CalculatesChecksumTrait.php
+++ b/src/S3/CalculatesChecksumTrait.php
@@ -8,6 +8,13 @@
 
 trait CalculatesChecksumTrait
 {
+    private static $supportedAlgorithms = [
+        'crc32c' => true,
+        'crc32' => true,
+        'sha256' => true,
+        'sha1' => true
+    ];
+
     /**
      * @param string $requestedAlgorithm  the algorithm to encode with
      * @param string $value               the value to be encoded
@@ -16,35 +23,37 @@ trait CalculatesChecksumTrait
     public static function getEncodedValue($requestedAlgorithm, $value) {
         $requestedAlgorithm = strtolower($requestedAlgorithm);
         $useCrt = extension_loaded('awscrt');
-        if ($useCrt) {
-            $crt = new Crt();
-            switch ($requestedAlgorithm) {
-                case 'crc32c':
-                    return base64_encode(pack('N*',($crt->crc32c($value))));
-                case 'crc32':
-                    return base64_encode(pack('N*',($crt->crc32($value))));
-                case 'sha256':
-                case 'sha1':
-                    return base64_encode(Psr7\Utils::hash($value, $requestedAlgorithm, true));
-                default:
-                    break;
-                throw new InvalidArgumentException(
-                    "Invalid checksum requested: {$requestedAlgorithm}."
-                    . "  Valid algorithms are CRC32C, CRC32, SHA256, and SHA1."
-                );
+
+        if (isset(self::$supportedAlgorithms[$requestedAlgorithm])) {
+            if ($useCrt) {
+                $crt = new Crt();
+                switch ($requestedAlgorithm) {
+                    case 'crc32c':
+                        return base64_encode(pack('N*',($crt::crc32c($value))));
+                    case 'crc32':
+                        return base64_encode(pack('N*',($crt::crc32($value))));
+                    default:
+                        break;
+                }
             }
-        }  else {
-            if ($requestedAlgorithm == 'crc32c') {
+
+            if ($requestedAlgorithm === 'crc32c') {
                 throw new CommonRuntimeException("crc32c is not supported for checksums "
                     . "without use of the common runtime for php.  Please enable the CRT or choose "
                     . "a different algorithm."
                 );
             }
-            if ($requestedAlgorithm == "crc32") {
+
+            if ($requestedAlgorithm === "crc32") {
                 $requestedAlgorithm = "crc32b";
             }
             return base64_encode(Psr7\Utils::hash($value, $requestedAlgorithm, true));
         }
-    }
 
+        $validAlgorithms = implode(', ', array_keys(self::$supportedAlgorithms));
+        throw new InvalidArgumentException(
+            "Invalid checksum requested: {$requestedAlgorithm}."
+            . "  Valid algorithms supported by the runtime are {$validAlgorithms}."
+        );
+    }
 }
diff --git a/src/S3/MultipartUploadingTrait.php b/src/S3/MultipartUploadingTrait.php
index 002bd43c46..097b1b3d1a 100644
--- a/src/S3/MultipartUploadingTrait.php
+++ b/src/S3/MultipartUploadingTrait.php
@@ -54,10 +54,16 @@ protected function handleResult(CommandInterface $command, ResultInterface $resu
         $partData = [];
         $partData['PartNumber'] = $command['PartNumber'];
         $partData['ETag'] = $this->extractETag($result);
+        $commandName = $command->getName();
+        $checksumResult = $commandName === 'UploadPart'
+            ? $result
+            : $result[$commandName . 'Result'];
+
         if (isset($command['ChecksumAlgorithm'])) {
             $checksumMemberName = 'Checksum' . strtoupper($command['ChecksumAlgorithm']);
-            $partData[$checksumMemberName] = $result[$checksumMemberName];
+            $partData[$checksumMemberName] = $checksumResult[$checksumMemberName] ?? null;
         }
+
         $this->getState()->markPartAsUploaded($command['PartNumber'], $partData);
     }
 
diff --git a/src/S3/Parser/ValidateResponseChecksumResultMutator.php b/src/S3/Parser/ValidateResponseChecksumResultMutator.php
index 3a31c4e813..c047898129 100644
--- a/src/S3/Parser/ValidateResponseChecksumResultMutator.php
+++ b/src/S3/Parser/ValidateResponseChecksumResultMutator.php
@@ -17,15 +17,23 @@
 final class ValidateResponseChecksumResultMutator implements S3ResultMutator
 {
     use CalculatesChecksumTrait;
+
+    public const DEFAULT_VALIDATION_MODE = 'when_supported';
+
     /** @var Service $api */
     private $api;
 
+    /** @var array $api */
+    private $config;
+
     /**
      * @param Service $api
+     * @param array $config
      */
-    public function __construct(Service $api)
+    public function __construct(Service $api, array $config = [])
     {
         $this->api = $api;
+        $this->config = $config;
     }
 
     /**
@@ -42,56 +50,40 @@ public function __invoke(
     ): ResultInterface
     {
         $operation = $this->api->getOperation($command->getName());
+
         // Skip this middleware if the operation doesn't have an httpChecksum
-        $checksumInfo = empty($operation['httpChecksum'])
-            ? null
-            : $operation['httpChecksum'];;
-        if (null === $checksumInfo) {
+        $checksumInfo = $operation['httpChecksum'] ?? null;
+        if (is_null($checksumInfo)) {
             return $result;
         }
 
-        // Skip this middleware if the operation doesn't send back a checksum,
-        // or the user doesn't opt in
+        $mode = $this->config['response_checksum_validation'] ?? self::DEFAULT_VALIDATION_MODE;
         $checksumModeEnabledMember = $checksumInfo['requestValidationModeMember'] ?? "";
-        $checksumModeEnabled = $command[$checksumModeEnabledMember] ?? "";
+        $checksumModeEnabled = strtolower($command[$checksumModeEnabledMember] ?? "");
         $responseAlgorithms = $checksumInfo['responseAlgorithms'] ?? [];
-        if (empty($responseAlgorithms)
-            || strtolower($checksumModeEnabled) !== "enabled") {
-            return $result;
-        }
+        $shouldSkipValidation = $this->shouldSkipValidation(
+            $mode,
+            $checksumModeEnabled,
+            $responseAlgorithms
+        );
 
-        if (extension_loaded('awscrt')) {
-            $checksumPriority = ['CRC32C', 'CRC32', 'SHA1', 'SHA256'];
-        } else {
-            $checksumPriority = ['CRC32', 'SHA1', 'SHA256'];
+        if ($shouldSkipValidation) {
+            return $result;
         }
 
-        $checksumsToCheck = array_intersect(
-            $responseAlgorithms,
-            $checksumPriority
+        $checksumPriority = $this->getChecksumPriority();
+        $checksumsToCheck = array_intersect($responseAlgorithms, array_map(
+            'strtoupper',
+            array_keys($checksumPriority))
         );
-        $checksumValidationInfo = $this->validateChecksum(
-            $checksumsToCheck,
-            $response
-        );
-        if ($checksumValidationInfo['status'] == "SUCCEEDED") {
+        $checksumValidationInfo = $this->validateChecksum($checksumsToCheck, $response);
+
+        if ($checksumValidationInfo['status'] === "SUCCEEDED") {
             $result['ChecksumValidated'] = $checksumValidationInfo['checksum'];
-        } elseif ($checksumValidationInfo['status'] == "FAILED") {
-            // Ignore failed validations on GetObject if it's a multipart get
-            // which returned a full multipart object
-            if ($command->getName() === "GetObject"
-                && !empty($checksumValidationInfo['checksumHeaderValue'])
-            ) {
-                $headerValue = $checksumValidationInfo['checksumHeaderValue'];
-                $lastDashPos = strrpos($headerValue, '-');
-                $endOfChecksum = substr($headerValue, $lastDashPos + 1);
-                if (is_numeric($endOfChecksum)
-                    && intval($endOfChecksum) > 1
-                    && intval($endOfChecksum) < 10000) {
-                    return $result;
-                }
+        } elseif ($checksumValidationInfo['status'] === "FAILED") {
+            if ($this->isMultipartGetObject($command, $checksumValidationInfo)) {
+                return $result;
             }
-
             throw new S3Exception(
                 "Calculated response checksum did not match the expected value",
                 $command
@@ -119,11 +111,10 @@ private function validateChecksum(
         $validationStatus = "SKIPPED";
         $checksumHeaderValue = null;
         if (!empty($checksumToValidate)) {
-            $checksumHeaderValue = $response->getHeader(
+            $checksumHeaderValue = $response->getHeaderLine(
                 'x-amz-checksum-' . $checksumToValidate
             );
-            if (isset($checksumHeaderValue)) {
-                $checksumHeaderValue = $checksumHeaderValue[0];
+            if (!empty($checksumHeaderValue)) {
                 $calculatedChecksumValue = $this->getEncodedValue(
                     $checksumToValidate,
                     $response->getBody()
@@ -160,4 +151,57 @@ private function chooseChecksumHeaderToValidate(
 
         return null;
     }
+
+    /**
+     * @param string $mode
+     * @param string $checksumModeEnabled
+     * @param array $responseAlgorithms
+     *
+     * @return bool
+     */
+    private function shouldSkipValidation(
+        string $mode,
+        string $checksumModeEnabled,
+        array $responseAlgorithms
+    ): bool
+    {
+        return empty($responseAlgorithms)
+            || ($mode === 'when_required' && $checksumModeEnabled !== 'enabled');
+    }
+
+    /**
+     * @return string[]
+     */
+    private function getChecksumPriority(): array
+    {
+        return extension_loaded('awscrt')
+            ? self::$supportedAlgorithms
+            : array_slice(self::$supportedAlgorithms, 1);
+    }
+
+    /**
+     * @param CommandInterface $command
+     * @param array $checksumValidationInfo
+     *
+     * @return bool
+     */
+    private function isMultipartGetObject(
+        CommandInterface $command,
+        array $checksumValidationInfo
+    ): bool
+    {
+        if ($command->getName() !== "GetObject"
+            || empty($checksumValidationInfo['checksumHeaderValue'])
+        ) {
+            return false;
+        }
+
+        $headerValue = $checksumValidationInfo['checksumHeaderValue'];
+        $lastDashPos = strrpos($headerValue, '-');
+        $endOfChecksum = substr($headerValue, $lastDashPos + 1);
+
+        return is_numeric($endOfChecksum)
+            && (int) $endOfChecksum > 1
+            && (int) $endOfChecksum < 10000;
+    }
 }
diff --git a/src/S3/S3Client.php b/src/S3/S3Client.php
index 77251df22e..b1ad9a695e 100644
--- a/src/S3/S3Client.php
+++ b/src/S3/S3Client.php
@@ -247,80 +247,109 @@ class S3Client extends AwsClient implements S3ClientInterface
     /** @var array */
     private static $mandatoryAttributes = ['Bucket', 'Key'];
 
+    /** @var array */
+    private static $checksumOptionEnum = [
+        'when_supported' => true,
+        'when_required' => true
+    ];
+
     public static function getArguments()
     {
         $args = parent::getArguments();
         $args['retries']['fn'] = [__CLASS__, '_applyRetryConfig'];
         $args['api_provider']['fn'] = [__CLASS__, '_applyApiProvider'];
 
-        return $args + [
-            'bucket_endpoint' => [
-                'type'    => 'config',
-                'valid'   => ['bool'],
-                'doc'     => 'Set to true to send requests to a hardcoded '
-                    . 'bucket endpoint rather than create an endpoint as a '
-                    . 'result of injecting the bucket into the URL. This '
-                    . 'option is useful for interacting with CNAME endpoints.',
-            ],
-            'use_arn_region' => [
-                'type'    => 'config',
-                'valid'   => [
-                    'bool',
-                    Configuration::class,
-                    CacheInterface::class,
-                    'callable'
+        return
+            [
+                'request_checksum_calculation' => [
+                    'type' => 'config',
+                    'valid' => ['string'],
+                    'doc' => 'Valid values are `when_supported` and `when_required`. Default is `when_supported`.'
+                        . ' `when_supported` results in checksum calculation when an operation has modeled checksum support.'
+                        . ' `when_required` results in checksum calculation when an operation has modeled checksum support and'
+                        . ' request checksums are modeled as required.',
+                    'fn' => [__CLASS__, '_apply_request_checksum_calculation'],
+                    'default' => [__CLASS__, '_default_request_checksum_calculation'],
                 ],
-                'doc'     => 'Set to true to allow passed in ARNs to override'
-                    . ' client region. Accepts...',
-                'fn' => [__CLASS__, '_apply_use_arn_region'],
-                'default' => [UseArnRegionConfigurationProvider::class, 'defaultProvider'],
-            ],
-            'use_accelerate_endpoint' => [
-                'type' => 'config',
-                'valid' => ['bool'],
-                'doc' => 'Set to true to send requests to an S3 Accelerate'
-                    . ' endpoint by default. Can be enabled or disabled on'
-                    . ' individual operations by setting'
-                    . ' \'@use_accelerate_endpoint\' to true or false. Note:'
-                    . ' you must enable S3 Accelerate on a bucket before it can'
-                    . ' be accessed via an Accelerate endpoint.',
-                'default' => false,
-            ],
-            'use_path_style_endpoint' => [
-                'type' => 'config',
-                'valid' => ['bool'],
-                'doc' => 'Set to true to send requests to an S3 path style'
-                    . ' endpoint by default.'
-                    . ' Can be enabled or disabled on individual operations by setting'
-                    . ' \'@use_path_style_endpoint\' to true or false.',
-                'default' => false,
-            ],
-            'disable_multiregion_access_points' => [
-                'type' => 'config',
-                'valid' => ['bool'],
-                'doc' => 'Set to true to disable the usage of'
-                    . ' multi region access points. These are enabled by default.'
-                    . ' Can be enabled or disabled on individual operations by setting'
-                    . ' \'@disable_multiregion_access_points\' to true or false.',
-                'default' => false,
-            ],
-            'disable_express_session_auth' => [
-                'type' => 'config',
-                'valid' => ['bool'],
-                'doc' => 'Set to true to disable the usage of'
-                    . ' s3 express session authentication. This is enabled by default.',
-                'default' => [__CLASS__, '_default_disable_express_session_auth'],
-            ],
-            's3_express_identity_provider' => [
-                'type'    => 'config',
-                'valid'   => [
-                    'bool',
-                    'callable'
+                'response_checksum_validation' => [
+                    'type' => 'config',
+                    'valid' => ['string'],
+                    'doc' => 'Valid values are `when_supported` and `when_required`. Default is `when_supported`.'
+                        . ' `when_supported` results in checksum validation when an operation has modeled checksum support.'
+                        . ' `when_required` results in checksum validation when an operation has modeled checksum support and'
+                        . ' `CheckSumMode` is set to `enabled`.',
+                    'fn' => [__CLASS__, '_apply_response_checksum_validation'],
+                    'default' => [__CLASS__, '_default_response_checksum_validation'],
+                ]
+            ]
+            + $args + [
+                'bucket_endpoint' => [
+                    'type'    => 'config',
+                    'valid'   => ['bool'],
+                    'doc'     => 'Set to true to send requests to a hardcoded '
+                        . 'bucket endpoint rather than create an endpoint as a '
+                        . 'result of injecting the bucket into the URL. This '
+                        . 'option is useful for interacting with CNAME endpoints.',
+                ],
+                'use_arn_region' => [
+                    'type'    => 'config',
+                    'valid'   => [
+                        'bool',
+                        Configuration::class,
+                        CacheInterface::class,
+                        'callable'
+                    ],
+                    'doc'     => 'Set to true to allow passed in ARNs to override'
+                        . ' client region. Accepts...',
+                    'fn' => [__CLASS__, '_apply_use_arn_region'],
+                    'default' => [UseArnRegionConfigurationProvider::class, 'defaultProvider'],
+                ],
+                'use_accelerate_endpoint' => [
+                    'type' => 'config',
+                    'valid' => ['bool'],
+                    'doc' => 'Set to true to send requests to an S3 Accelerate'
+                        . ' endpoint by default. Can be enabled or disabled on'
+                        . ' individual operations by setting'
+                        . ' \'@use_accelerate_endpoint\' to true or false. Note:'
+                        . ' you must enable S3 Accelerate on a bucket before it can'
+                        . ' be accessed via an Accelerate endpoint.',
+                    'default' => false,
                 ],
-                'doc'     => 'Specifies the provider used to generate identities to sign s3 express requests.  '
-                    . 'Set to `false` to disable s3 express auth, or a callable provider used to create s3 express '
-                    . 'identities or return null.',
-                'default' => [__CLASS__, '_default_s3_express_identity_provider'],
+                'use_path_style_endpoint' => [
+                    'type' => 'config',
+                    'valid' => ['bool'],
+                    'doc' => 'Set to true to send requests to an S3 path style'
+                        . ' endpoint by default.'
+                        . ' Can be enabled or disabled on individual operations by setting'
+                        . ' \'@use_path_style_endpoint\' to true or false.',
+                    'default' => false,
+                ],
+                'disable_multiregion_access_points' => [
+                    'type' => 'config',
+                    'valid' => ['bool'],
+                    'doc' => 'Set to true to disable the usage of'
+                        . ' multi region access points. These are enabled by default.'
+                        . ' Can be enabled or disabled on individual operations by setting'
+                        . ' \'@disable_multiregion_access_points\' to true or false.',
+                    'default' => false,
+                ],
+                'disable_express_session_auth' => [
+                    'type' => 'config',
+                    'valid' => ['bool'],
+                    'doc' => 'Set to true to disable the usage of'
+                        . ' s3 express session authentication. This is enabled by default.',
+                    'default' => [__CLASS__, '_default_disable_express_session_auth'],
+                ],
+                's3_express_identity_provider' => [
+                    'type'    => 'config',
+                    'valid'   => [
+                        'bool',
+                        'callable'
+                    ],
+                    'doc'     => 'Specifies the provider used to generate identities to sign s3 express requests.  '
+                        . 'Set to `false` to disable s3 express auth, or a callable provider used to create s3 express '
+                        . 'identities or return null.',
+                    'default' => [__CLASS__, '_default_s3_express_identity_provider'],
             ],
         ];
     }
@@ -392,7 +421,10 @@ public function __construct(array $args)
         parent::__construct($args);
         $stack = $this->getHandlerList();
         $stack->appendInit(SSECMiddleware::wrap($this->getEndpoint()->getScheme()), 's3.ssec');
-        $stack->appendBuild(ApplyChecksumMiddleware::wrap($this->getApi()), 's3.checksum');
+        $stack->appendBuild(
+            ApplyChecksumMiddleware::wrap($this->getApi(), $this->getConfig()),
+            's3.checksum'
+        );
         $stack->appendBuild(
             Middleware::contentType(['PutObject', 'UploadPart']),
             's3.content_type'
@@ -507,11 +539,106 @@ public static function _apply_use_arn_region($value, array &$args, HandlerList $
         }
     }
 
+    public static function _default_request_checksum_calculation(array $args): string
+    {
+        return ConfigurationResolver::resolve(
+            'request_checksum_calculation',
+            ApplyChecksumMiddleware::DEFAULT_CALCULATION_MODE,
+            'string',
+            $args
+        );
+    }
+
+    public static function _apply_request_checksum_calculation(
+        string $value,
+        array &$args
+    ): void
+    {
+        $value = strtolower($value);
+        if (array_key_exists($value, self::$checksumOptionEnum)) {
+            $args['request_checksum_calculation'] = $value;
+        } else {
+            $validValues = implode(' | ', array_keys(self::$checksumOptionEnum));
+            throw new \InvalidArgumentException(
+                'invalid value provided for `request_checksum_calculation`.'
+                . ' valid values are: ' . $validValues . '.'
+            );
+        }
+    }
+
+    public static function _default_response_checksum_validation(array $args): string
+    {
+        return ConfigurationResolver::resolve(
+            'response_checksum_validation',
+            ValidateResponseChecksumResultMutator::DEFAULT_VALIDATION_MODE,
+            'string',
+            $args
+        );
+    }
+
+    public static function _apply_response_checksum_validation(
+        $value,
+        array &$args
+    ): void
+    {
+        $value = strtolower($value);
+        if (array_key_exists($value, self::$checksumOptionEnum)) {
+            $args['request_checksum_calculation'] = $value;
+        } else {
+            $validValues = implode(' | ', array_keys(self::$checksumOptionEnum));
+            throw new \InvalidArgumentException(
+                'invalid value provided for `request_checksum_calculation`.'
+                . ' valid values are: ' . $validValues . '.'
+            );
+        }
+    }
+
+    public static function _default_disable_express_session_auth(array &$args)
+    {
+        return ConfigurationResolver::resolve(
+            's3_disable_express_session_auth',
+            false,
+            'bool',
+            $args
+        );
+    }
+
+    public static function _default_s3_express_identity_provider(array $args)
+    {
+        if ($args['config']['disable_express_session_auth']) {
+            return false;
+        }
+        return new S3ExpressIdentityProvider($args['region']);
+    }
+
     public function createPresignedRequest(CommandInterface $command, $expires, array $options = [])
     {
         $command = clone $command;
-        $command->getHandlerList()->remove('signer');
+        $list = $command->getHandlerList();
+        $list->remove('signer');
+
+        //Removes checksum calculation behavior by default
+        if (empty($command['ChecksumAlgorithm'])
+            && empty($command['AddContentMD5'])
+        ) {
+            $list->remove('s3.checksum');
+        }
+
         $request = \Aws\serialize($command);
+
+        //Applies ContentSHA256 parameter, if provided and not applied
+        // by middleware
+        $commandName = $command->getName();
+        if (!empty($command['ContentSHA256']
+            && isset(ApplyChecksumMiddleware::$sha256[$commandName])
+            && !$request->hasHeader('X-Amz-Content-Sha256')
+        )) {
+            $request = $request->withHeader(
+                'X-Amz-Content-Sha256',
+                $command['ContentSHA256']
+            );
+        }
+
         $signing_name = $command['@context']['signing_service']
             ?? $this->getSigningName($request->getUri()->getHost());
         $signature_version = $this->getSignatureVersionFromCommand($command);
@@ -763,23 +890,6 @@ private function getSigningName($host)
         return $this->getConfig('signing_name');
     }
 
-    public static function _default_disable_express_session_auth(array &$args) {
-        return ConfigurationResolver::resolve(
-            's3_disable_express_session_auth',
-            false,
-            'bool',
-            $args
-        );
-    }
-
-    public static function _default_s3_express_identity_provider(array $args)
-    {
-        if ($args['config']['disable_express_session_auth']) {
-            return false;
-        }
-        return new S3ExpressIdentityProvider($args['region']);
-    }
-
     /**
      * If EndpointProviderV2 is used, removes `Bucket` from request URIs.
      * This is now handled by the endpoint ruleset.
@@ -988,7 +1098,10 @@ public static function _applyApiProvider($value, array &$args, HandlerList $list
         );
         $s3Parser->addS3ResultMutator(
             'validate-response-checksum',
-            new ValidateResponseChecksumResultMutator($args['api'])
+            new ValidateResponseChecksumResultMutator(
+                $args['api'],
+                ['response_checksum_validation' => $args['response_checksum_validation']]
+            )
         );
         $args['parser'] = $s3Parser;
     }
diff --git a/src/Signature/S3SignatureV4.php b/src/Signature/S3SignatureV4.php
index 52a9f4e7dc..c99b281cfd 100644
--- a/src/Signature/S3SignatureV4.php
+++ b/src/Signature/S3SignatureV4.php
@@ -94,6 +94,7 @@ public function presign(
                 $this->getPresignedPayload($request)
             );
         }
+
         if (strpos($request->getUri()->getHost(), "accesspoint.s3-global")) {
             $request = $request->withHeader("x-amz-region-set", "*");
         }
diff --git a/tests/Integ/MultipartContext.php b/tests/Integ/MultipartContext.php
index e18fe00d41..3b547cc90e 100644
--- a/tests/Integ/MultipartContext.php
+++ b/tests/Integ/MultipartContext.php
@@ -2,18 +2,16 @@
 
 namespace Aws\Test\Integ;
 
+use Aws\CommandInterface;
 use Aws\Exception\MultipartUploadException;
 use Aws\Glacier\MultipartUploader as GlacierMultipartUploader;
 use Aws\ResultInterface;
 use Aws\S3\MultipartCopy;
 use Aws\S3\MultipartUploader as S3MultipartUploader;
 use Aws\S3\S3Client;
-use Behat\Behat\Tester\Exception\PendingException;
 use Aws\S3\BatchDelete;
 use Behat\Behat\Context\Context;
 use Behat\Behat\Context\SnippetAcceptingContext;
-use Behat\Gherkin\Node\PyStringNode;
-use Behat\Gherkin\Node\TableNode;
 use GuzzleHttp\Psr7;
 use GuzzleHttp\Psr7\NoSeekStream;
 use PHPUnit\Framework\Assert;
@@ -90,6 +88,43 @@ public function iUploadTheStreamToS3WithAConcurrencyFactorOf($concurrency)
         }
     }
 
+    /**
+     * @When /^I upload the stream to S3 with a checksum algorithm of "(crc32|sha256|sha1)"$/
+     */
+    public function iUploadTheStreamToS3WithAChecksumAlgorithmOf($checksumAlgorithm)
+    {
+        $client = self::getSdk()->createS3();
+        $uploader = new S3MultipartUploader($client, $this->stream, [
+            'bucket' => self::getResourceName(),
+            'key' => get_class($this->stream) . $checksumAlgorithm,
+            'before_initiate' => function (CommandInterface $command) use ($checksumAlgorithm) {
+                // $command is a CreateMultipartUpload operation
+                $command['ChecksumAlgorithm'] = $checksumAlgorithm;
+            },
+            'before_upload' => function (CommandInterface $command) use ($checksumAlgorithm) {
+                // $command is an UploadPart operation
+                $command['ChecksumAlgorithm'] = $checksumAlgorithm;
+            },
+            'before_complete' => function (CommandInterface $command) use ($checksumAlgorithm) {
+                // $command is a CompleteMultipartUpload operation
+                $command['ChecksumAlgorithm'] = $checksumAlgorithm;
+            },
+        ]);
+
+        try {
+            $this->result = $uploader->upload();
+        } catch (MultipartUploadException $e) {
+            $client->abortMultipartUpload($e->getState()->getId());
+            $message = "=====\n";
+            while ($e) {
+                $message .= $e->getMessage() . "\n";
+                $e = $e->getPrevious();
+            }
+            $message .= "=====\n";
+            Assert::fail($message);
+        }
+    }
+
     /**
      * @When /^I upload the stream to Glacier with a concurrency factor of "(\d+)"$/
      */
@@ -152,6 +187,12 @@ public function iCallMultipartCopyOnToANewKeyInTheSameBucket($filename)
      */
     public function theResultShouldContainA($key)
     {
+        if (strpos($key, "Checksum") === 0) {
+            $algorithm = substr($key, strlen("Checksum"));
+            $formattedAlgorithm = strtoupper($algorithm);
+            $key = "Checksum" . $formattedAlgorithm;
+        }
+
         Assert::assertArrayHasKey($key, $this->result);
     }
 
diff --git a/tests/S3/ApplyChecksumMiddlewareTest.php b/tests/S3/ApplyChecksumMiddlewareTest.php
index fcf62c13a3..257a2fa3c6 100644
--- a/tests/S3/ApplyChecksumMiddlewareTest.php
+++ b/tests/S3/ApplyChecksumMiddlewareTest.php
@@ -1,11 +1,10 @@
 <?php
 namespace Aws\Test\S3;
 
-use Aws\Api\ApiProvider;
-use Aws\Middleware;
+use Aws\S3\ApplyChecksumMiddleware;
 use Aws\Test\UsesServiceTrait;
-use Psr\Http\Message\RequestInterface;
-use PHPUnit\Framework\TestCase;
+use GuzzleHttp\Psr7\Request;
+use Yoast\PHPUnitPolyfills\TestCases\TestCase;
 
 /**
  * @covers Aws\S3\ApplyChecksumMiddleware
@@ -15,163 +14,154 @@ class ApplyChecksumMiddlewareTest extends TestCase
     use UsesServiceTrait;
 
     /**
-     * @dataProvider getContentMd5UseCases
+     * @dataProvider getFlexibleChecksumUseCases
      */
-    public function testAddsContentMd5AsAppropriate($operation, $args, $md5Added, $md5Value)
-    {
-        $s3 = $this->getTestClient(
-            's3',
-            ['api_provider' => ApiProvider::filesystem(__DIR__ . '/fixtures')]
-        );
-        $this->addMockResults($s3, [[]]);
-        $command = $s3->getCommand($operation, $args);
-        $command->getHandlerList()->appendBuild(
-            Middleware::tap(function ($cmd, RequestInterface $request) use ($md5Added, $md5Value) {
-                $this->assertSame($md5Added, $request->hasHeader('Content-MD5'));
-                $this->assertEquals($md5Value, $request->getHeaderLine('Content-MD5'));
-            })
+    public function testFlexibleChecksums(
+        $operation,
+        $config,
+        $commandArgs,
+        $body,
+        $headerAdded,
+        $headerValue
+    ){
+        if (isset($commandArgs['ChecksumAlgorithm'])
+            && $commandArgs['ChecksumAlgorithm'] === 'crc32c'
+            && !extension_loaded('awscrt')
+        ) {
+            $this->markTestSkipped("Cannot test crc32c without the CRT");
+        }
+
+        $client = $this->getTestClient('s3');
+        $nextHandler = function ($cmd, $request) use ($headerAdded, $headerValue, $commandArgs) {
+            $checksumName = $commandArgs['ChecksumAlgorithm'] ?? "crc32";
+            if ($headerAdded) {
+                $this->assertTrue( $request->hasHeader("x-amz-checksum-{$checksumName}"));
+            }
+            $this->assertEquals($headerValue, $request->getHeaderLine("x-amz-checksum-{$checksumName}"));
+        };
+        $service = $client->getApi();
+        $mw = new ApplyChecksumMiddleware($nextHandler, $service, $config);
+        $command = $client->getCommand($operation, $commandArgs);
+        $request = new Request(
+            $operation === 'getObject'
+                ? 'GET'
+                : 'PUT',
+            'https://foo.bar',
+            [],
+            $body
         );
-        $s3->execute($command);
+
+        $mw($command, $request);
     }
 
-    public function getContentMd5UseCases()
+    public function getFlexibleChecksumUseCases()
     {
         return [
-            // Test that explicitly proviced Content MD5 is passed through
+            // httpChecksum not modeled
             [
-                'PutBucketLogging',
+                'GetObject',
+                [],
                 [
                     'Bucket' => 'foo',
-                    'BucketLoggingStatus' => [
-                        'LoggingEnabled' => [
-                            'TargetBucket' => 'bar',
-                            'TargetPrefix' => 'baz'
-                        ]
-                    ],
-                    'ContentMD5' => 'custommd5'
+                    'Key' => 'bar',
+                    'ChecksumMode' => 'ENABLED'
                 ],
-                true,
-                'custommd5'
+                null,
+                false,
+                ''
             ],
-            // Test MD5 added for operations that require it
+            // default: when_supported. defaults to crc32
             [
-                'DeleteObjects',
-                ['Bucket' => 'foo', 'Delete' => ['Objects' => [['Key' => 'bar']]]],
+                'PutObject',
+                [],
+                [
+                    'Bucket' => 'foo',
+                    'Key' => 'bar',
+                    'Body' => 'abc'
+                ],
+                'abc',
                 true,
-                '/12roh/ATpPMcGD9Rj4ZlQ=='
+                'NSRBwg=='
             ],
-            // Test MD5 not added for operations that do not require it
+            // when_required when not required and no requested algorithm
             [
-                'GetObject',
-                ['Bucket' => 'foo', 'Key' => 'bar'],
+                'PutObject',
+                ['request_checksum_calculation' => 'when_required'],
+                [
+                    'Bucket' => 'foo',
+                    'Key' => 'bar',
+                    'Body' => 'abc'
+                ],
+                'abc',
                 false,
-                null,
+                ''
             ],
-        ];
-    }
-
-    public function testAddCrc32AsAppropriate()
-    {
-        $s3 = $this->getTestClient(
-            's3',
-            ['api_provider' => ApiProvider::filesystem(__DIR__ . '/fixtures')]
-        );
-        $this->addMockResults($s3, [[]]);
-        $command = $s3->getCommand('putBucketPolicy', [
-            'Bucket' => 'mybucket--use1-az1--x-s3',
-            'Policy' => 'policy'
-        ]);
-        $command->getHandlerList()->appendBuild(
-            Middleware::tap(function ($cmd, RequestInterface $request) {
-                $this->assertFalse($request->hasHeader('Content-MD5'));
-                $this->assertSame('8H0FFg==', $request->getHeaderLine('x-amz-checksum-crc32'));
-            })
-        );
-        $s3->execute($command);
-    }
-
-    /**
-     * @dataProvider getFlexibleChecksumUseCases
-     */
-    public function testAddsFlexibleChecksumAsAppropriate($operation, $args, $headerAdded, $headerValue)
-    {
-        if (isset($args['ChecksumAlgorithm'])
-            && $args['ChecksumAlgorithm'] === 'crc32c'
-            && !extension_loaded('awscrt')
-        ) {
-            $this->markTestSkipped("Cannot test crc32c without the CRT");
-        }
-        $s3 = $this->getTestClient(
-            's3',
-            ['api_provider' => ApiProvider::filesystem(__DIR__ . '/fixtures')]
-        );
-        $this->addMockResults($s3, [[]]);
-        $command = $s3->getCommand($operation, $args);
-        $command->getHandlerList()->appendBuild(
-            Middleware::tap(function ($cmd, RequestInterface $request) use ($headerAdded, $headerValue, $args) {
-                $checksumName = isset($args['ChecksumAlgorithm']) ? $args['ChecksumAlgorithm'] : "";
-                $this->assertSame($headerAdded, $request->hasHeader("x-amz-checksum-{$checksumName}"));
-                $this->assertEquals($headerValue, $request->getHeaderLine("x-amz-checksum-{$checksumName}"));
-            })
-        );
-        $s3->execute($command);
-    }
-
-    public function getFlexibleChecksumUseCases()
-    {
-        return [
-            // Test that explicitly proviced Content MD5 is passed through
+            // when_required when required and no requested algorithm
             [
-                'GetObject',
+                'PutObjectLockConfiguration',
+                ['request_checksum_calculation' => 'when_required'],
                 [
                     'Bucket' => 'foo',
                     'Key' => 'bar',
-                    'ChecksumMode' => 'ENABLED'
+                    'ObjectLockConfiguration' => 'blah'
                 ],
-                false,
-                ''
+                'blah',
+                true,
+                'zilhXA=='
             ],
+            // when_required when not required and requested algorithm
             [
                 'PutObject',
+                ['request_checksum_calculation' => 'when_required'],
                 [
                     'Bucket' => 'foo',
                     'Key' => 'bar',
+                    'Body' => 'blah',
                     'ChecksumAlgorithm' => 'crc32',
-                    'Body' => 'abc'
                 ],
+                'blah',
                 true,
-                'EZo2zw=='
+                'zilhXA=='
             ],
+            // when_supported and requested algorithm
             [
                 'PutObject',
+                [],
                 [
                     'Bucket' => 'foo',
                     'Key' => 'bar',
                     'ChecksumAlgorithm' => 'crc32c',
                     'Body' => 'abc'
                 ],
+                'abc',
                 true,
-                'oD04yw=='
+                'Nks/tw=='
             ],
+            // when_supported and requested algorithm
             [
                 'PutObject',
+                [],
                 [
                     'Bucket' => 'foo',
                     'Key' => 'bar',
                     'ChecksumAlgorithm' => 'sha256'
                 ],
+                '',
                 true,
-                'OmJVpLQxEjty3SMySBLGRfWtoBQ/ZXT2cT2Cjuly4XY='
+                '47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='
             ],
+            // when_supported and requested algorithm
             [
                 'PutObject',
+                [],
                 [
                     'Bucket' => 'foo',
                     'Key' => 'bar',
                     'ChecksumAlgorithm' => 'SHA1'
                 ],
+                '',
                 true,
-                'KckkIVRT7cC010EHaNNNuun5VBY='
+                '2jmj7l5rSw0yVb/vlWAYkK/YBwk='
             ],
         ];
     }
@@ -179,18 +169,19 @@ public function getFlexibleChecksumUseCases()
     /**
      * @dataProvider getContentSha256UseCases
      */
-    public function testAddsContentSHA256AsAppropriate($operation, $args, $hashAdded, $hashValue)
+    public function testAddsContentSHA256($operation, $args, $hashAdded, $hashValue)
     {
-        $s3 = $this->getTestClient('s3');
-        $this->addMockResults($s3, [[]]);
-        $command = $s3->getCommand($operation, $args);
-        $command->getHandlerList()->appendBuild(
-            Middleware::tap(function ($cmd, RequestInterface $request) use ($hashAdded, $hashValue) {
-                $this->assertSame($hashAdded, $request->hasHeader('x-amz-content-sha256'));
-                $this->assertEquals($hashValue, $request->getHeaderLine('x-amz-content-sha256'));
-            })
-        );
-        $s3->execute($command);
+        $client = $this->getTestClient('s3');
+        $nextHandler = function ($cmd, $request) use ($hashAdded, $hashValue) {
+            $this->assertSame($hashAdded, $request->hasHeader('x-amz-content-sha256'));
+            $this->assertEquals($hashValue, $request->getHeaderLine('x-amz-content-sha256'));
+        };
+        $service = $client->getApi();
+        $mw = new ApplyChecksumMiddleware($nextHandler, $service);
+        $command = $client->getCommand($operation, $args);
+        $request = new Request('PUT', 'foo');
+
+        $mw($command, $request);
     }
 
     public function getContentSha256UseCases()
@@ -221,4 +212,53 @@ public function getContentSha256UseCases()
             ],
         ];
     }
+
+    public function testAddContentMd5EmitsDeprecationWarning()
+    {
+        $this->expectDeprecation();
+        $this->expectDeprecationMessage('S3 no longer supports MD5 checksums.');
+        $client = $this->getTestClient('s3');
+        $nextHandler = function ($cmd, $request) {
+            $this->assertTrue($request->hasHeader('x-amz-checksum-crc32'));
+        };
+        $service = $client->getApi();
+        $mw = new ApplyChecksumMiddleware($nextHandler, $service);
+        $command = $client->getCommand('putObject', ['AddContentMD5' => true]);
+        $request = new Request('PUT', 'foo');
+
+        $mw($command, $request);
+    }
+
+    public function testInvalidChecksumThrows()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage(
+            'Unsupported algorithm supplied for input variable ChecksumAlgorithm'
+        );
+        $client = $this->getTestClient('s3');
+        $nextHandler = function ($cmd, $request) {
+            $this->assertTrue($request->hasHeader('x-amz-checksum-crc32'));
+        };
+        $service = $client->getApi();
+        $mw = new ApplyChecksumMiddleware($nextHandler, $service);
+        $command = $client->getCommand('putObject', ['ChecksumAlgorithm' => 'NotAnAlgorithm']);
+        $request = new Request('PUT', 'foo');
+
+        $mw($command, $request);
+    }
+
+    public function testDoesNotCalculateChecksumIfHeaderProvided()
+    {
+        $client = $this->getTestClient('s3');
+        $nextHandler = function ($cmd, $request) {
+            $this->assertTrue($request->hasHeader('x-amz-checksum-crc32c'));
+            $this->assertEquals('foo', $request->getHeaderLine('x-amz-checksum-crc32c'));
+        };
+        $service = $client->getApi();
+        $mw = new ApplyChecksumMiddleware($nextHandler, $service);
+        $command = $client->getCommand('putObject');
+        $request = new Request('PUT', 'foo', ['x-amz-checksum-crc32c' => 'foo']);
+
+        $mw($command, $request);
+    }
 }
diff --git a/tests/S3/MultipartUploaderTest.php b/tests/S3/MultipartUploaderTest.php
index b8a46e55dd..d5f750ff66 100644
--- a/tests/S3/MultipartUploaderTest.php
+++ b/tests/S3/MultipartUploaderTest.php
@@ -163,20 +163,9 @@ public function testS3MultipartUploadParams($stream, $size)
     {
         /** @var \Aws\S3\S3Client $client */
         $client = $this->getTestClient('s3');
-        $client->getHandlerList()->appendSign(
-            Middleware::tap(function ($cmd, $req) {
-                $name = $cmd->getName();
-                if ($name === 'UploadPart') {
-                    $this->assertTrue(
-                        $req->hasHeader('Content-MD5')
-                    );
-                }
-            })
-        );
         $uploadOptions = [
             'bucket'          => 'foo',
             'key'             => 'bar',
-            'add_content_md5' => true,
             'params'          => [
                 'RequestPayer'  => 'test',
                 'ContentLength' => $size
@@ -327,7 +316,6 @@ public function testUploaderAddsFlexibleChecksums($stream, $size)
         $uploadOptions = [
             'bucket'          => 'foo',
             'key'             => 'bar',
-            'add_content_md5' => true,
             'params'          => [
                 'RequestPayer'  => 'test',
                 'ChecksumAlgorithm' => 'Sha256'
@@ -363,4 +351,25 @@ public function testUploaderAddsFlexibleChecksums($stream, $size)
         $this->assertSame('xyz', $result['ChecksumSHA256']);
         $this->assertSame($url, $result['ObjectURL']);
     }
+
+    public function testAddContentMd5EmitsDeprecationNotice()
+    {
+        $this->expectDeprecation();
+        $this->expectDeprecationMessage('S3 no longer supports MD5 checksums.');
+        $data = str_repeat('.', 12 * self::MB);
+        $filename = sys_get_temp_dir() . '/' . self::FILENAME;
+        file_put_contents($filename, $data);
+        $source = Psr7\Utils::streamFor(fopen($filename, 'r'));
+        $client = $this->getTestClient('s3');
+        $options = ['bucket' => 'foo', 'key' => 'bar', 'add_content_md5' => true];
+        $this->addMockResults($client, [
+            new Result(['UploadId' => 'baz']),
+            new Result(['ETag' => 'A']),
+            new Result(['ETag' => 'B']),
+            new Result(['ETag' => 'C']),
+        ]);
+
+        $uploader = new MultipartUploader($client, $source, $options);
+        $result = $uploader->upload();
+    }
 }
diff --git a/tests/S3/ObjectUploaderTest.php b/tests/S3/ObjectUploaderTest.php
index b02b111faf..de6ae0d3cc 100644
--- a/tests/S3/ObjectUploaderTest.php
+++ b/tests/S3/ObjectUploaderTest.php
@@ -10,7 +10,7 @@
 use GuzzleHttp\Psr7\FnStream;
 use Psr\Http\Message\RequestInterface;
 use Psr\Http\Message\StreamInterface;
-use PHPUnit\Framework\TestCase;
+use Yoast\PHPUnitPolyfills\TestCases\TestCase;
 
 class ObjectUploaderTest extends TestCase
 {
@@ -250,7 +250,6 @@ public function testS3ObjectUploaderPutObjectParams()
         );
         $uploadOptions = [
             'params'          => ['RequestPayer' => 'test'],
-            'add_content_md5' => true,
             'before_upload'   => function($command) {
                 $this->assertSame('test', $command['RequestPayer']);
             },
@@ -283,20 +282,9 @@ public function testS3ObjectUploaderMultipartParams()
     {
         /** @var \Aws\S3\S3Client $client */
         $client = $this->getTestClient('s3');
-        $client->getHandlerList()->appendSign(
-            Middleware::tap(function ($cmd, $req) {
-                $name = $cmd->getName();
-                if ($name === 'UploadPart') {
-                    $this->assertTrue(
-                        $req->hasHeader('Content-MD5')
-                    );
-                }
-            })
-        );
         $uploadOptions = [
             'mup_threshold'   => self::MB * 4,
             'params'          => ['RequestPayer' => 'test'],
-            'add_content_md5' => true,
             'before_initiate' => function($command) {
                 $this->assertSame('test', $command['RequestPayer']);
             },
@@ -330,4 +318,63 @@ public function testS3ObjectUploaderMultipartParams()
 
         $this->assertSame($url, $result['ObjectURL']);
     }
+
+    /**
+     * @param $checksumAlgorithm
+     * @return void
+     *
+     * @dataProvider flexibleChecksumsProvider
+     */
+    public function testAddsFlexibleChecksums($checksumAlgorithm, $value)
+    {
+        if ($checksumAlgorithm === 'crc32c'
+            && !extension_loaded('awscrt')
+        ) {
+            $this->markTestSkipped();
+        }
+
+        $client = $this->getTestClient('S3');
+        $handlerList = $client->getHandlerList();
+        $handlerList->appendBuild(Middleware::tap(
+            function ($cmd, $req) use ($checksumAlgorithm, $value) {
+                $headerName = 'x-amz-checksum-' . $checksumAlgorithm;
+                $this->assertTrue($req->hasHeader($headerName));
+                $this->assertEquals($value, $req->getHeaderLine($headerName));
+            })
+        );
+        $this->addMockResults($client, [new Result()]);
+        $result = (new ObjectUploader(
+            $client,
+            'bucket',
+            'key',
+            $this->generateStream(1024 * 1024 * 1),
+            'private',
+            ['params' => ['ChecksumAlgorithm' => $checksumAlgorithm]]
+        ))->upload();
+    }
+
+    public function flexibleChecksumsProvider() {
+        return [
+            ['sha1', 'VfWih+7phcE4uG3HQZCHKfpUwFs='],
+            ['sha256', 'FT+vHyoAcJfTMSC77mlEpBy4vnZDwSIva8a8aewxaI8='],
+            ['crc32c', 'd8twAA=='],
+            ['crc32', '9p4rcQ==']
+        ];
+    }
+
+    public function testAddContentMd5EmitsDeprecationNotice()
+    {
+        $this->expectDeprecation();
+        $this->expectExceptionMessage('S3 no longer supports MD5 checksums.');
+        $client = $this->getTestClient('S3');
+        $this->addMockResults($client, [new Result()]);
+        $result = (new ObjectUploader(
+            $client,
+            'bucket',
+            'key',
+            $this->generateStream(1024 * 1024 * 1),
+            'private',
+            ['add_content_md5' => true]
+        ))->upload();
+    }
 }
diff --git a/tests/S3/Parser/ValidateResponseChecksumResultMutatorTest.php b/tests/S3/Parser/ValidateResponseChecksumResultMutatorTest.php
index e38597618a..2538241c33 100644
--- a/tests/S3/Parser/ValidateResponseChecksumResultMutatorTest.php
+++ b/tests/S3/Parser/ValidateResponseChecksumResultMutatorTest.php
@@ -25,25 +25,40 @@ class ValidateResponseChecksumResultMutatorTest extends TestCase
      * @dataProvider checksumCasesDataProvider
      * @param array $responseAlgorithms
      * @param array $checksumHeadersReturned
-     * @param string $expectedChecksum
-     *
+     * @param string|null $expectedChecksumAlgorithm
+     * @param array $config
+     * @param string $operation
+     * @param string|null $checksumMode
      * @return void
      */
-    public function testValidatesChoosesRightChecksum(
+    public function testChecksumValidation(
         array $responseAlgorithms,
         array $checksumHeadersReturned,
-        ?string $expectedChecksumAlgorithm
+        ?string $expectedChecksumAlgorithm,
+        array $config,
+        string $operation,
+        ?string $checksumMode
     ) {
+        if (!empty($responseAlgorithms)
+            && $responseAlgorithms[0] === 'CRC32C'
+            && !extension_loaded('awscrt')
+        ) {
+            $this->markTestSkipped();
+        }
+
         $s3Client = $this->getTestS3ClientWithResponseAlgorithms(
-            'GetObject',
+            $operation,
             $responseAlgorithms
         );
-        $mutator = new ValidateResponseChecksumResultMutator($s3Client->getApi());
+        $mutator = new ValidateResponseChecksumResultMutator(
+            $s3Client->getApi(),
+            $config
+        );
         $result = new Result();
         $command = new Command(
-            'GetObject',
+            $operation,
             [
-                'ChecksumMode' => 'enabled'
+                'ChecksumMode' => $checksumMode
             ],
             new HandlerList()
         );
@@ -56,7 +71,6 @@ public function testValidatesChoosesRightChecksum(
         }
 
         $result = $mutator($result, $command, $response);
-
         $this->assertEquals(
             $expectedChecksumAlgorithm,
             $result['ChecksumValidated']
@@ -72,37 +86,119 @@ public function testValidatesChoosesRightChecksum(
     public function checksumCasesDataProvider(): array
     {
         return [
+            //Default, when_supported, no checksum headers, skips validation
             [
                 ['CRC32', 'CRC32C'],
                 [],
+                null,
+                [],
+                'GetObject',
+                null
+            ],
+            //Default, when_supported, no modeled checksums, skips validation
+            [
+                [],
+                [],
+                null,
+                [],
+                'GetObject',
                 null
             ],
+            //Default, when_supported with modeled checksums
             [
                 ['SHA256', 'CRC32'],
                 [
                     ['sha256', 'E6TOUbfBBDPqSyozecOzDgB3K9CZKCI6d7PbKBAYvo0=']
                 ],
-                "SHA256"
+                "SHA256",
+                [],
+                'GetObject',
+                null
             ],
+            //Default, when_supported with modeled checksums
             [
                 ['CRC32', 'CRC32C'],
                 [
                     ["sha256", 'E6TOUbfBBDPqSyozecOzDgB3K9CZKCI6d7PbKBAYvo0='],
                     ['crc32', 'DIt2Ng==']
                 ],
-                "CRC32"
+                "CRC32",
+                [],
+                'GetObject',
+                null
             ],
+            //Default, when_supported with modeled checksums
             [
                 ['CRC32', 'CRC32C'],
                 [
                     ['crc64', '']
                 ],
+                null,
+                [],
+                'GetObject',
+                null
+            ],
+            //Default, when_supported, with modeled checksums, CRC32C
+            [
+                ['CRC32C', 'CRC32'],
+                [
+                    ["crc32c", 'k2udew=='],
+                ],
+                "CRC32C",
+                [],
+                'GetObject',
                 null
             ],
+            // when_required, with modeled checksums, with mode "enabled"
+            [
+                ['CRC32', 'CRC32C'],
+                [
+                    ["sha256", 'E6TOUbfBBDPqSyozecOzDgB3K9CZKCI6d7PbKBAYvo0='],
+                    ['crc32', 'DIt2Ng==']
+                ],
+                "CRC32",
+                ['response_checksum_validation' => 'when_required'],
+                'GetObject',
+                'enabled'
+            ],
+            // when_required, with modeled checksums, with mode "enabled"
+            [
+                ['SHA256', 'CRC32'],
+                [
+                    ['sha256', 'E6TOUbfBBDPqSyozecOzDgB3K9CZKCI6d7PbKBAYvo0=']
+                ],
+                "SHA256",
+                ['response_checksum_validation' => 'when_required'],
+                'GetObject',
+                'enabled'
+            ],
+            // when_required, with modeled checksums, with mode "enabled" CRC32C
+            [
+                ['CRC32C', 'CRC32'],
+                [
+                    ["crc32c", 'k2udew=='],
+                ],
+                "CRC32C",
+                ['response_checksum_validation' => 'when_required'],
+                'GetObject',
+                'enabled'
+            ],
+            // when_required, with modeled checksums, with mode unset, skips validation
+            [
+                ['CRC32'],
+                [
+                    ["crc32", ''],
+                ],
+                null,
+                ['response_checksum_validation' => 'when_required'],
+                'GetObject',
+                ''
+            ],
         ];
     }
 
-    public function testValidatesChecksumFailsOnBadValue() {
+    public function testValidatesChecksumFailsOnBadValue()
+    {
         $this->expectException(S3Exception::class);
         $this->expectExceptionMessage(
             'Calculated response checksum did not match the expected value'
@@ -124,7 +220,8 @@ public function testValidatesChecksumFailsOnBadValue() {
         $mutator($result, $command, $response);
     }
 
-    public function testValidatesChecksumSucceeds() {
+    public function testValidatesChecksumSucceeds()
+    {
         $mutator = $this->getValidateResponseChecksumMutator();
         $expectedValue = "E6TOUbfBBDPqSyozecOzDgB3K9CZKCI6d7PbKBAYvo0=";
         $expectedAlgorithm = "SHA256";
@@ -147,7 +244,8 @@ public function testValidatesChecksumSucceeds() {
 
     }
 
-    public function testValidatesChecksumSkipsValidation() {
+    public function testValidatesChecksumSkipsValidation()
+    {
         $mutator = $this->getValidateResponseChecksumMutator();
         $result = new Result();
         $command = new Command(
@@ -163,7 +261,8 @@ public function testValidatesChecksumSkipsValidation() {
         $this->assertEmpty($result['ChecksumValidated']);
     }
 
-    public function testSkipsGetObjectReturnsFullMultipart() {
+    public function testSkipsGetObjectReturnsFullMultipart()
+    {
         $expectedValue = "E6TOUbfBBDPqSyozecOzDgB3K9CZKCI6d7PbK-1034";
         $mutator = $this->getValidateResponseChecksumMutator();
         $result = new Result();
@@ -185,7 +284,8 @@ public function testSkipsGetObjectReturnsFullMultipart() {
         self::assertTrue(true);
     }
 
-    public function testValidatesSha256() {
+    public function testValidatesSha256()
+    {
         $expectedValue = "E6TOUbfBBDPqSyozecOzDgB3K9CZKCI6d7PbKBAYvo0=";
         $mutator = $this->getValidateResponseChecksumMutator();
         $result = new Result();
@@ -213,10 +313,11 @@ public function testValidatesSha256() {
      * @return ValidateResponseChecksumResultMutator
      */
     private function getValidateResponseChecksumMutator(
+        array $config = []
     ): ValidateResponseChecksumResultMutator
     {
         return new ValidateResponseChecksumResultMutator(
-            $this->getTestS3Client()->getApi()
+            $this->getTestS3Client()->getApi(), $config
         );
     }
 
diff --git a/tests/S3/S3ClientTest.php b/tests/S3/S3ClientTest.php
index bde077e577..5b816b78a0 100644
--- a/tests/S3/S3ClientTest.php
+++ b/tests/S3/S3ClientTest.php
@@ -1,6 +1,7 @@
 <?php
 namespace Aws\Test\S3;
 
+use Aws\Api\ApiProvider;
 use Aws\Api\DateTimeResult;
 use Aws\Command;
 use Aws\CommandInterface;
@@ -2302,39 +2303,14 @@ public function addMD5Provider() {
     /**
      * @dataProvider addMD5Provider
      */
-    public function testAutomaticallyComputesMD5($options, $operation)
+    public function testAddContentMd5EmitsDeprecationNotice($options, $operation)
     {
+        $this->expectDeprecation();
+        $this->expectExceptionMessage('S3 no longer supports MD5 checksums.');
         $s3 = $this->getTestClient('s3');
         $this->addMockResults($s3, [[]]);
         $options['AddContentMD5'] = true;
         $command = $s3->getCommand($operation, $options);
-        $command->getHandlerList()->appendSign(
-            Middleware::tap(function ($cmd, $req) {
-                $this->assertSame(
-                    'CY9rzUYh03PK3k6DJie09g==',
-                    $req->getHeader('Content-MD5')[0]
-                );
-            })
-        );
-        $s3->execute($command);
-    }
-
-    /**
-     * @dataProvider addMD5Provider
-     */
-    public function testDoesNotComputeMD5($options, $operation)
-    {
-        $s3 = $this->getTestClient('s3');
-        $this->addMockResults($s3, [[]]);
-        $options['AddContentMD5'] = false;
-        $command = $s3->getCommand($operation, $options);
-        $command->getHandlerList()->appendSign(
-            Middleware::tap(function ($cmd, $req) {
-                $this->assertFalse(
-                    $req->hasHeader('Content-MD5')
-                );
-            })
-        );
         $s3->execute($command);
     }
 
@@ -2531,8 +2507,8 @@ public function testS3RetriesOnNotParsableBody(array $retrySettings)
         $client->listBuckets();
         $this->assertEquals(0, $retries);
     }
-
-    /**
+  
+  /**
      * @param string $bucketName
      * @param bool $expected
      *
@@ -2563,4 +2539,361 @@ public function directoryBucketProvider(): array
             ['too-short--x-s3', false],                // invalid azid format, missing prefix
         ];
     }
+
+    public function testAddCrc32ForDirectoryBucketsAsAppropriate()
+    {
+        $s3 = $this->getTestClient('s3');
+        $this->addMockResults($s3, [[]]);
+        $command = $s3->getCommand('putBucketPolicy', [
+            'Bucket' => 'mybucket--use1-az1--x-s3',
+            'Policy' => 'policy'
+        ]);
+        $command->getHandlerList()->appendBuild(
+            Middleware::tap(function ($cmd, RequestInterface $request) {
+                $this->assertFalse($request->hasHeader('Content-MD5'));
+                $this->assertSame('8H0FFg==', $request->getHeaderLine('x-amz-checksum-crc32'));
+            })
+        );
+        $s3->execute($command);
+    }
+
+    /**
+     * @dataProvider getContentSha256UseCases
+     */
+    public function testAddsContentSHA256AsAppropriate($operation, $args, $hashAdded, $hashValue)
+    {
+        $s3 = $this->getTestClient('s3');
+        $this->addMockResults($s3, [[]]);
+        $command = $s3->getCommand($operation, $args);
+        $command->getHandlerList()->appendBuild(
+            Middleware::tap(function ($cmd, RequestInterface $request) use ($hashAdded, $hashValue) {
+                $this->assertSame($hashAdded, $request->hasHeader('x-amz-content-sha256'));
+                $this->assertEquals($hashValue, $request->getHeaderLine('x-amz-content-sha256'));
+            })
+        );
+        $s3->execute($command);
+    }
+
+    public function getContentSha256UseCases()
+    {
+        $hash = 'SHA256HASH';
+
+        return [
+            // Do nothing if ContentSHA256 was not provided.
+            [
+                'PutObject',
+                ['Bucket' => 'foo', 'Key' => 'bar', 'Body' => 'baz'],
+                false,
+                ''
+            ],
+            // Gets added for operations that allow it.
+            [
+                'PutObject',
+                ['Bucket' => 'foo', 'Key' => 'bar', 'Body' => 'baz', 'ContentSHA256' => $hash],
+                true,
+                $hash
+            ],
+            // Not added for operations that do not allow it.
+            [
+                'GetObject',
+                ['Bucket' => 'foo', 'Key' => 'bar', 'ContentSHA256' => $hash],
+                false,
+                '',
+            ],
+        ];
+    }
+
+    /**
+     * @dataProvider getFlexibleChecksumUseCases
+     */
+    public function testAddsFlexibleChecksumAsAppropriate($operation, $clientArgs, $operationArgs, $headerAdded, $headerValue)
+    {
+        if (isset($operationArgs['ChecksumAlgorithm'])
+            && $operationArgs['ChecksumAlgorithm'] === 'crc32c'
+            && !extension_loaded('awscrt')
+        ) {
+            $this->markTestSkipped("Cannot test crc32c without the CRT");
+        }
+        $s3 = $this->getTestClient('s3', $clientArgs);
+        $this->addMockResults($s3, [[]]);
+        $command = $s3->getCommand($operation, $operationArgs);
+        $command->getHandlerList()->appendBuild(
+            Middleware::tap(function ($cmd, RequestInterface $request) use ($headerAdded, $headerValue, $operationArgs) {
+                $checksumName = $operationArgs['ChecksumAlgorithm'] ?? "crc32";
+                if ($headerAdded) {
+                    $this->assertTrue($request->hasHeader("x-amz-checksum-{$checksumName}"));
+                }
+                $this->assertEquals($headerValue, $request->getHeaderLine("x-amz-checksum-{$checksumName}"));
+            })
+        );
+        $s3->execute($command);
+    }
+
+    public function getFlexibleChecksumUseCases()
+    {
+        return [
+            // httpChecksum not modeled
+            [
+                'GetObject',
+                [],
+                [
+                    'Bucket' => 'foo',
+                    'Key' => 'bar',
+                    'ChecksumMode' => 'ENABLED'
+                ],
+                false,
+                ''
+            ],
+            // default: when_supported. defaults to crc32
+            [
+                'PutObject',
+                [],
+                [
+                    'Bucket' => 'foo',
+                    'Key' => 'bar',
+                    'Body' => 'abc'
+                ],
+                true,
+                'NSRBwg=='
+            ],
+            // when_required when not required and no requested algorithm
+            [
+                'PutObject',
+                ['request_checksum_calculation' => 'when_required'],
+                [
+                    'Bucket' => 'foo',
+                    'Key' => 'bar',
+                    'Body' => 'abc'
+                ],
+                false,
+                ''
+            ],
+            // when_required when required and no requested algorithm
+            [
+                'PutObjectLockConfiguration',
+                ['request_checksum_calculation' => 'when_required'],
+                [
+                    'Bucket' => 'foo',
+                    'Key' => 'bar',
+                    'ObjectLockConfiguration' => []
+                ],
+                true,
+                'UHB63w=='
+            ],
+            // when_required when not required and requested algorithm
+            [
+                'PutObject',
+                ['request_checksum_calculation' => 'when_required'],
+                [
+                    'Bucket' => 'foo',
+                    'Key' => 'bar',
+                    'Body' => 'blah',
+                    'ChecksumAlgorithm' => 'crc32',
+                ],
+                true,
+                'zilhXA=='
+            ],
+            // when_supported and requested algorithm
+            [
+                'PutObject',
+                [],
+                [
+                    'Bucket' => 'foo',
+                    'Key' => 'bar',
+                    'ChecksumAlgorithm' => 'crc32c',
+                    'Body' => 'abc'
+                ],
+                true,
+                'Nks/tw=='
+            ],
+            // when_supported and requested algorithm
+            [
+                'PutObject',
+                [],
+                [
+                    'Bucket' => 'foo',
+                    'Key' => 'bar',
+                    'ChecksumAlgorithm' => 'sha256'
+                ],
+                true,
+                '47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='
+            ],
+            // when_supported and requested algorithm
+            [
+                'PutObject',
+                [],
+                [
+                    'Bucket' => 'foo',
+                    'Key' => 'bar',
+                    'ChecksumAlgorithm' => 'SHA1'
+                ],
+                true,
+                '2jmj7l5rSw0yVb/vlWAYkK/YBwk='
+            ]
+        ];
+    }
+
+    /**
+     * @param array $clientConfig
+     * @return void
+     *
+     * @dataProvider responseChecksumValidationProvider
+     */
+    public function testResponseChecksumValidation(
+        array $clientConfig,
+        ?string $checksumAlgorithm,
+        ?string $mode
+    ): void
+    {
+        if ($checksumAlgorithm === 'CRC32C'
+            && !extension_loaded('awscrt')
+        ) {
+            $this->markTestSkipped("Cannot test crc32c without the awscrt");
+        }
+
+        $handler = static function (RequestInterface $request) use ($checksumAlgorithm) {
+            return Promise\Create::promiseFor(new Response(
+                200,
+                ['x-amz-checksum-' . $checksumAlgorithm => 'AAAAAA==']
+            ));
+        };
+        $client = $this->getTestClient('s3', $clientConfig + ['http_handler' => $handler]);
+
+
+        $result = $client->getObject([
+            'Bucket' => 'bucket',
+            'Key' => 'key',
+            'ChecksumMode' => $mode
+        ]);
+
+        $this->assertEquals($checksumAlgorithm, $result['ChecksumValidated']);
+    }
+
+    public function responseChecksumValidationProvider(): array
+    {
+        return [
+            [
+                //default, when_supported, validates checksum for operation with modeled response checksums
+                [],
+                'CRC32',
+                null
+            ],
+            [
+                //default, when_supported, validates checksum for operation with modeled response checksums when
+                // CRT installed
+                [],
+                'CRC32C',
+                null
+            ],
+            [
+                // when_required, validates checksum for operation with modeled response checksums
+                // and mode is "enabled"
+                ['response_checksum_validation' => 'when_required'],
+                'CRC32',
+                'enabled'
+            ],
+            [
+                // when_required, validates checksum validation for operation with modeled response checksums
+                // and mode is "enabled" when CRT installed
+                ['response_checksum_validation' => 'when_required'],
+                'CRC32C',
+                'enabled'
+            ],
+            [
+                // when_required, skips checksum validation for operation with modeled response checksums
+                ['response_checksum_validation' => 'when_required'],
+                null,
+                null
+            ],
+        ];
+    }
+
+    /**
+     * @dataProvider checksumConfigProvider
+     * @return void
+     */
+    public function testChecksumConfigThrowsForInvalidInput(
+        string $option,
+        string $invalidOption
+    ): void
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage(
+            'valid values are: when_supported | when_required.'
+        );
+        $this->getTestClient(
+            's3',
+            [$option => 'foo']
+        );
+    }
+
+    public function checksumConfigProvider()
+    {
+        return [
+            ['request_checksum_calculation', 'foo'],
+            ['response_checksum_validation', 'foo']
+        ];
+    }
+
+    public function testCreatesPresignedRequestsWithoutChecksumByDefault()
+    {
+        /** @var S3Client $client */
+        $client = $this->getTestClient('S3', [
+            'region' => 'us-east-1',
+            'credentials' => ['key' => 'foo', 'secret' => 'bar']
+        ]);
+        $command = $client->getCommand('PutObject', ['Bucket' => 'foo', 'Key' => 'bar']);
+        $url = (string) $client->createPresignedRequest($command, 1342138769)->getUri();
+        $this->assertStringNotContainsString('x-amz-checksum-', $url);
+        $this->assertStringNotContainsString('x-amz-sdk-checksum-', $url);
+    }
+
+    public function testCreatesPresignedRequestsWithRequestedChecksumAlgorithm()
+    {
+        /** @var S3Client $client */
+        $client = $this->getTestClient('S3', [
+            'region' => 'us-east-1',
+            'credentials' => ['key' => 'foo', 'secret' => 'bar']
+        ]);
+        $command = $client->getCommand(
+            'PutObject',
+            ['Bucket' => 'foo', 'Key' => 'bar', 'ChecksumAlgorithm' => 'crc32']
+        );
+        $url = (string) $client->createPresignedRequest($command, 1342138769)->getUri();
+        $this->assertStringContainsString('x-amz-checksum-crc32=AAAAAA%3D%3D', $url);
+        $this->assertStringContainsString('x-amz-sdk-checksum-algorithm=crc32', $url);
+    }
+
+    public function testCreatesPresignedRequestsWithContentSha256Value()
+    {
+        /** @var S3Client $client */
+        $client = $this->getTestClient('S3', [
+            'region' => 'us-east-1',
+            'credentials' => ['key' => 'foo', 'secret' => 'bar']
+        ]);
+        $command = $client->getCommand(
+            'PutObject',
+            ['Bucket' => 'foo', 'Key' => 'bar', 'ContentSHA256' => 'foo']
+        );
+        $url = (string) $client->createPresignedRequest($command, 1342138769)->getUri();
+        $this->assertStringContainsString('X-Amz-Content-Sha256=foo', $url);
+        $this->assertStringNotContainsString('x-amz-sdk-checksum-algorithm', $url);
+        $this->assertStringNotContainsString('x-amz-checksum', $url);
+    }
+
+    public function testCreatesPresignedPutRequestsWithChecksumValue()
+    {
+        /** @var S3Client $client */
+        $client = $this->getTestClient('S3', [
+            'region' => 'us-east-1',
+            'credentials' => ['key' => 'foo', 'secret' => 'bar']
+        ]);
+        $command = $client->getCommand(
+            'PutObject',
+            ['Bucket' => 'foo', 'Key' => 'bar','ChecksumCRC32' => 'AAAAAA==']
+        );
+        $url = (string) $client->createPresignedRequest($command, 1342138769)->getUri();
+        $this->assertStringContainsString('x-amz-checksum-crc32=AAAAAA%3D%3D', $url);
+        $this->assertStringNotContainsString('x-amz-sdk-checksum-algorithm=crc32', $url);
+    }
 }
diff --git a/tests/S3/TransferTest.php b/tests/S3/TransferTest.php
index c6ac8e25d0..0299f00d9c 100644
--- a/tests/S3/TransferTest.php
+++ b/tests/S3/TransferTest.php
@@ -147,7 +147,6 @@ function (CommandInterface $cmd, RequestInterface $req) {
         $t = new Transfer($s3, $dir, 's3://foo/bar', [
             'mup_threshold' => 5248000,
             'debug' => $res,
-            'add_content_md5' => true
         ]);
 
         $t->transfer();
@@ -403,6 +402,112 @@ public function testCanDownloadFilesYieldedBySourceIterator()
         $downloader->transfer();
     }
 
+    public function testAddContentMd5EmitsDeprecationWarning()
+    {
+        $s3 = $this->getTestClient('s3');
+        $this->addMockResults($s3, []);
+
+        $this->expectDeprecation();
+        $this->expectDeprecationMessage('S3 no longer supports MD5 checksums.');
+        $s3->getHandlerList()->appendSign(Middleware::tap(
+            function (CommandInterface $cmd, RequestInterface $req) {
+                $this->assertTrue(isset($command['x-amz-checksum-crc32']));
+            }
+        ));
+
+        $dir = sys_get_temp_dir() . '/unittest';
+        `rm -rf $dir`;
+        mkdir($dir);
+        $filename = $dir . '/foo.txt';
+        $f = fopen($filename, 'w+');
+        fwrite($f, 'foo');
+        fclose($f);
+
+        $res = fopen('php://temp', 'r+');
+        $t = new Transfer($s3, $dir, 's3://foo/bar', [
+            'debug' => $res,
+            'add_content_md5' => true
+        ]);
+
+        $t->transfer();
+        rewind($res);
+        $output = stream_get_contents($res);
+        $this->assertStringContainsString("Transferring $filename -> s3://foo/bar/foo.txt", $output);
+        `rm -rf $dir`;
+    }
+
+    /**
+     * @param $checksumAlgorithm
+     * @param $value
+     * @return void
+     *
+     * @dataProvider flexibleChecksumsProvider
+     */
+    public function testAddsFlexibleChecksums($checksumAlgorithm)
+    {
+        if ($checksumAlgorithm === 'crc32c'
+            && !extension_loaded('awscrt')
+        ) {
+            $this->markTestSkipped();
+        }
+
+        $s3 = $this->getTestClient('s3');
+        $this->addMockResults($s3, [
+            new Result(['UploadId' => '123']),
+            new Result(['ETag' => 'a']),
+            new Result(['ETag' => 'b']),
+            new Result(['UploadId' => '123']),
+        ]);
+
+        $s3->getHandlerList()->appendSign(Middleware::tap(
+            function (CommandInterface $cmd, RequestInterface $req) use ($checksumAlgorithm) {
+                $name = $cmd->getName();
+                if ($name === 'UploadPart') {
+                    $headerName = 'x-amz-checksum-' . $checksumAlgorithm;
+                    $this->assertTrue($req->hasHeader($headerName));
+                }
+            }
+        ));
+
+        $dir = sys_get_temp_dir() . '/unittest';
+        `rm -rf $dir`;
+        mkdir($dir);
+        $filename = $dir . '/large.txt';
+        $f = fopen($filename, 'w+');
+        $line = str_repeat('.', 1024);
+        for ($i = 0; $i < 6000; $i++) {
+            fwrite($f, $line);
+        }
+        fclose($f);
+
+        $before = function ($cmd, $req = null) use ($checksumAlgorithm) {
+            if ($cmd->getName() === 'UploadPart') {
+                $cmd['ChecksumAlgorithm'] = $checksumAlgorithm;
+            }
+        };
+        $res = fopen('php://temp', 'r+');
+        $t = new Transfer($s3, $dir, 's3://foo/bar', [
+            'mup_threshold' => 5248000,
+            'debug' => $res,
+            'before' => $before
+        ]);
+
+        $t->transfer();
+        rewind($res);
+        $output = stream_get_contents($res);
+        $this->assertStringContainsString("Transferring $filename -> s3://foo/bar/large.txt (UploadPart) : Part=1", $output);
+        `rm -rf $dir`;
+    }
+
+    public function flexibleChecksumsProvider() {
+        return [
+            ['sha256'],
+            ['sha1'],
+            ['crc32c'],
+            ['crc32']
+        ];
+    }
+
     private function mockResult(callable $fn)
     {
         return function (callable $handler) use ($fn) {