diff --git a/google/cloud/storage/_signing.py b/google/cloud/storage/_signing.py
index e7c8e33..0da075d 100644
--- a/google/cloud/storage/_signing.py
+++ b/google/cloud/storage/_signing.py
@@ -18,7 +18,6 @@ import binascii
 import collections
 import datetime
 import hashlib
-import re
 import json
 
 import six
@@ -31,8 +30,6 @@ from google.cloud import _helpers
 
 
 NOW = datetime.datetime.utcnow  # To be replaced by tests.
-MULTIPLE_SPACES_RE = r"\s+"
-MULTIPLE_SPACES = re.compile(MULTIPLE_SPACES_RE)
 
 SERVICE_ACCOUNT_URL = (
     "https://googleapis.dev/python/google-api-core/latest/"
@@ -192,7 +189,7 @@ def get_canonical_headers(headers):
     normalized = collections.defaultdict(list)
     for key, val in headers:
         key = key.lower().strip()
-        val = MULTIPLE_SPACES.sub(" ", val.strip())
+        val = " ".join(val.split())
         normalized[key].append(val)
 
     ordered_headers = sorted((key, ",".join(val)) for key, val in normalized.items())
@@ -206,8 +203,8 @@ _Canonical = collections.namedtuple(
 )
 
 
-def canonicalize(method, resource, query_parameters, headers):
-    """Canonicalize method, resource
+def canonicalize_v2(method, resource, query_parameters, headers):
+    """Canonicalize method, resource per the V2 spec.
 
     :type method: str
     :param method: The HTTP verb that will be used when requesting the URL.
@@ -301,6 +298,7 @@ def generate_signed_url_v2(
     :type resource: str
     :param resource: A pointer to a specific resource
                      (typically, ``/bucket-name/path/to/blob.txt``).
+                     Caller should have already URL-encoded the value.
 
     :type expiration: Union[Integer, datetime.datetime, datetime.timedelta]
     :param expiration: Point in time when the signed URL should expire.
@@ -368,7 +366,7 @@ def generate_signed_url_v2(
     """
     expiration_stamp = get_expiration_seconds_v2(expiration)
 
-    canonical = canonicalize(method, resource, query_parameters, headers)
+    canonical = canonicalize_v2(method, resource, query_parameters, headers)
 
     # Generate the string to sign.
     elements_to_sign = [
@@ -462,6 +460,7 @@ def generate_signed_url_v4(
     :type resource: str
     :param resource: A pointer to a specific resource
                      (typically, ``/bucket-name/path/to/blob.txt``).
+                     Caller should have already URL-encoded the value.
 
     :type expiration: Union[Integer, datetime.datetime, datetime.timedelta]
     :param expiration: Point in time when the signed URL should expire.
@@ -589,13 +588,20 @@ def generate_signed_url_v4(
     ordered_query_parameters = sorted(query_parameters.items())
     canonical_query_string = six.moves.urllib.parse.urlencode(ordered_query_parameters)
 
+    lowercased_headers = dict(ordered_headers)
+
+    if "x-goog-content-sha256" in lowercased_headers:
+        payload = lowercased_headers["x-goog-content-sha256"]
+    else:
+        payload = "UNSIGNED-PAYLOAD"
+
     canonical_elements = [
         method,
         resource,
         canonical_query_string,
         canonical_header_string,
         signed_headers,
-        "UNSIGNED-PAYLOAD",
+        payload,
     ]
     canonical_request = "\n".join(canonical_elements)
 

[Storage] V4 Signature: Formatting Inconsistencies
Tracking issue for work defined in companion document.

[New Client Request - GCS SigV4 Formatting Inconsistencies](https://docs.google.com/document/d/11OpYYj2FMFsWe-slIKK-JQC97CUh4CYOWeraR0wuImo).

diff --git a/tests/unit/test__signing.py b/tests/unit/test__signing.py
index ebd7f9c..c7231b5 100644
--- a/tests/unit/test__signing.py
+++ b/tests/unit/test__signing.py
@@ -309,12 +309,12 @@ class Test_get_canonical_headers(unittest.TestCase):
         self.assertEqual(ordered, expected_ordered)
 
 
-class Test_canonicalize(unittest.TestCase):
+class Test_canonicalize_v2(unittest.TestCase):
     @staticmethod
     def _call_fut(*args, **kwargs):
-        from google.cloud.storage._signing import canonicalize
+        from google.cloud.storage._signing import canonicalize_v2
 
-        return canonicalize(*args, **kwargs)
+        return canonicalize_v2(*args, **kwargs)
 
     def test_wo_headers_or_query_parameters(self):
         method = "GET"
@@ -650,6 +650,9 @@ class Test_generate_signed_url_v4(unittest.TestCase):
     def test_w_custom_headers(self):
         self._generate_helper(headers={"x-goog-foo": "bar"})
 
+    def test_w_custom_payload_hash_goog(self):
+        self._generate_helper(headers={"x-goog-content-sha256": "DEADBEEF"})
+
     def test_w_custom_query_parameters_w_string_value(self):
         self._generate_helper(query_parameters={"bar": "/"})
 

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-cov pytest-xdist mock
apt-get update apt-get install -y gcc
3.9
requirements/base.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
cachetools==4.2.4
certifi==2025.1.31
charset-normalizer==3.4.1
coverage==7.8.0
exceptiongroup==1.2.2
execnet==2.1.1
google-api-core==2.10.2
google-auth==1.35.0
google-cloud-core==1.7.3
-e git+https://github.com/googleapis/python-storage.git@b9c0bca1a5f1c1d2f2c637f67f39c2edf71c7705#egg=google_cloud_storage
google-resumable-media==0.5.1
googleapis-common-protos==1.69.2
idna==3.10
iniconfig==2.1.0
mock==5.2.0
packaging==24.2
pluggy==1.5.0
protobuf==4.25.6
pyasn1==0.6.1
pyasn1_modules==0.4.2
pytest==8.3.5
pytest-cov==6.0.0
pytest-xdist==3.6.1
requests==2.32.3
rsa==4.9
six==1.17.0
tomli==2.2.1
urllib3==2.3.0

name: python-storage
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - cachetools==4.2.4
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - coverage==7.8.0
      - exceptiongroup==1.2.2
      - execnet==2.1.1
      - google-api-core==2.10.2
      - google-auth==1.35.0
      - google-cloud-core==1.7.3
      - google-resumable-media==0.5.1
      - googleapis-common-protos==1.69.2
      - idna==3.10
      - iniconfig==2.1.0
      - mock==5.2.0
      - packaging==24.2
      - pluggy==1.5.0
      - protobuf==4.25.6
      - pyasn1==0.6.1
      - pyasn1-modules==0.4.2
      - pytest==8.3.5
      - pytest-cov==6.0.0
      - pytest-xdist==3.6.1
      - requests==2.32.3
      - rsa==4.9
      - six==1.17.0
      - tomli==2.2.1
      - urllib3==2.3.0
prefix: /opt/conda/envs/python-storage

tests/unit/test__signing.py::Test_canonicalize_v2::test_w_headers_and_resumable tests/unit/test__signing.py::Test_canonicalize_v2::test_w_query_paramters tests/unit/test__signing.py::Test_canonicalize_v2::test_wo_headers_or_query_parameters
tests/unit/test__signing.py::Test_get_expiration_seconds_v2::test_w_expiration_int tests/unit/test__signing.py::Test_get_expiration_seconds_v2::test_w_expiration_naive_datetime tests/unit/test__signing.py::Test_get_expiration_seconds_v2::test_w_expiration_none tests/unit/test__signing.py::Test_get_expiration_seconds_v2::test_w_expiration_other_zone_datetime tests/unit/test__signing.py::Test_get_expiration_seconds_v2::test_w_expiration_timedelta_days tests/unit/test__signing.py::Test_get_expiration_seconds_v2::test_w_expiration_timedelta_seconds tests/unit/test__signing.py::Test_get_expiration_seconds_v2::test_w_expiration_utc_datetime tests/unit/test__signing.py::Test_get_expiration_seconds_v2::test_w_invalid_expiration_type tests/unit/test__signing.py::Test_get_expiration_seconds_v4::test_w_expiration_int tests/unit/test__signing.py::Test_get_expiration_seconds_v4::test_w_expiration_int_gt_seven_days tests/unit/test__signing.py::Test_get_expiration_seconds_v4::test_w_expiration_naive_datetime tests/unit/test__signing.py::Test_get_expiration_seconds_v4::test_w_expiration_none tests/unit/test__signing.py::Test_get_expiration_seconds_v4::test_w_expiration_other_zone_datetime tests/unit/test__signing.py::Test_get_expiration_seconds_v4::test_w_expiration_timedelta tests/unit/test__signing.py::Test_get_expiration_seconds_v4::test_w_expiration_utc_datetime tests/unit/test__signing.py::Test_get_expiration_seconds_v4::test_w_invalid_expiration_type tests/unit/test__signing.py::Test_get_signed_query_params_v2::test_it tests/unit/test__signing.py::Test_get_canonical_headers::test_w_dict tests/unit/test__signing.py::Test_get_canonical_headers::test_w_embedded_ws tests/unit/test__signing.py::Test_get_canonical_headers::test_w_list_and_multiples tests/unit/test__signing.py::Test_get_canonical_headers::test_w_none tests/unit/test__signing.py::Test_generate_signed_url_v2::test_w_custom_headers_dict tests/unit/test__signing.py::Test_generate_signed_url_v2::test_w_custom_headers_list tests/unit/test__signing.py::Test_generate_signed_url_v2::test_w_custom_query_parameters_w_none_value tests/unit/test__signing.py::Test_generate_signed_url_v2::test_w_custom_query_parameters_w_string_value tests/unit/test__signing.py::Test_generate_signed_url_v2::test_w_endpoint tests/unit/test__signing.py::Test_generate_signed_url_v2::test_w_expiration_int tests/unit/test__signing.py::Test_generate_signed_url_v2::test_w_generation tests/unit/test__signing.py::Test_generate_signed_url_v2::test_w_method tests/unit/test__signing.py::Test_generate_signed_url_v2::test_w_method_resumable tests/unit/test__signing.py::Test_generate_signed_url_v2::test_w_response_disposition tests/unit/test__signing.py::Test_generate_signed_url_v2::test_w_response_type tests/unit/test__signing.py::Test_generate_signed_url_v2::test_with_access_token tests/unit/test__signing.py::Test_generate_signed_url_v2::test_with_google_credentials tests/unit/test__signing.py::Test_generate_signed_url_v4::test_w_api_access_endpoint tests/unit/test__signing.py::Test_generate_signed_url_v4::test_w_content_md5 tests/unit/test__signing.py::Test_generate_signed_url_v4::test_w_content_type tests/unit/test__signing.py::Test_generate_signed_url_v4::test_w_custom_headers tests/unit/test__signing.py::Test_generate_signed_url_v4::test_w_custom_host_header tests/unit/test__signing.py::Test_generate_signed_url_v4::test_w_custom_payload_hash_goog tests/unit/test__signing.py::Test_generate_signed_url_v4::test_w_custom_query_parameters_w_none_value tests/unit/test__signing.py::Test_generate_signed_url_v4::test_w_custom_query_parameters_w_string_value tests/unit/test__signing.py::Test_generate_signed_url_v4::test_w_defaults tests/unit/test__signing.py::Test_generate_signed_url_v4::test_w_expiration_too_long tests/unit/test__signing.py::Test_generate_signed_url_v4::test_w_generation tests/unit/test__signing.py::Test_generate_signed_url_v4::test_w_method tests/unit/test__signing.py::Test_generate_signed_url_v4::test_w_method_resumable tests/unit/test__signing.py::Test_generate_signed_url_v4::test_w_response_disposition tests/unit/test__signing.py::Test_generate_signed_url_v4::test_w_response_type tests/unit/test__signing.py::Test_generate_signed_url_v4::test_with_access_token tests/unit/test__signing.py::Test_sign_message::test_sign_bytes tests/unit/test__signing.py::Test_sign_message::test_sign_bytes_failure tests/unit/test__signing.py::test_conformance_bucket[test_data0] tests/unit/test__signing.py::test_conformance_blob[test_data0] tests/unit/test__signing.py::test_conformance_blob[test_data1] tests/unit/test__signing.py::test_conformance_blob[test_data2] tests/unit/test__signing.py::test_conformance_blob[test_data3] tests/unit/test__signing.py::test_conformance_blob[test_data4] tests/unit/test__signing.py::test_conformance_blob[test_data5] tests/unit/test__signing.py::test_conformance_blob[test_data6] tests/unit/test__signing.py::test_conformance_blob[test_data7] tests/unit/test__signing.py::test_conformance_blob[test_data8]
google/cloud/storage/_signing.py
google/cloud/storage/_signing.py