Skip to content

Commit 8712da8

Browse files
authored
fix: make v4 signing formatting consistent w/ spec (#56)
* Rename 'canonicalize' to show V2 only. * Refactor / simplify header whitespace normalization. * Sign user-supplied payload hash.
1 parent b9c0bca commit 8712da8

File tree

2 files changed

+20
-11
lines changed

2 files changed

+20
-11
lines changed

google/cloud/storage/_signing.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import collections
1919
import datetime
2020
import hashlib
21-
import re
2221
import json
2322

2423
import six
@@ -31,8 +30,6 @@
3130

3231

3332
NOW = datetime.datetime.utcnow # To be replaced by tests.
34-
MULTIPLE_SPACES_RE = r"\s+"
35-
MULTIPLE_SPACES = re.compile(MULTIPLE_SPACES_RE)
3633

3734
SERVICE_ACCOUNT_URL = (
3835
"https://googleapis.dev/python/google-api-core/latest/"
@@ -192,7 +189,7 @@ def get_canonical_headers(headers):
192189
normalized = collections.defaultdict(list)
193190
for key, val in headers:
194191
key = key.lower().strip()
195-
val = MULTIPLE_SPACES.sub(" ", val.strip())
192+
val = " ".join(val.split())
196193
normalized[key].append(val)
197194

198195
ordered_headers = sorted((key, ",".join(val)) for key, val in normalized.items())
@@ -206,8 +203,8 @@ def get_canonical_headers(headers):
206203
)
207204

208205

209-
def canonicalize(method, resource, query_parameters, headers):
210-
"""Canonicalize method, resource
206+
def canonicalize_v2(method, resource, query_parameters, headers):
207+
"""Canonicalize method, resource per the V2 spec.
211208
212209
:type method: str
213210
:param method: The HTTP verb that will be used when requesting the URL.
@@ -301,6 +298,7 @@ def generate_signed_url_v2(
301298
:type resource: str
302299
:param resource: A pointer to a specific resource
303300
(typically, ``/bucket-name/path/to/blob.txt``).
301+
Caller should have already URL-encoded the value.
304302
305303
:type expiration: Union[Integer, datetime.datetime, datetime.timedelta]
306304
:param expiration: Point in time when the signed URL should expire.
@@ -368,7 +366,7 @@ def generate_signed_url_v2(
368366
"""
369367
expiration_stamp = get_expiration_seconds_v2(expiration)
370368

371-
canonical = canonicalize(method, resource, query_parameters, headers)
369+
canonical = canonicalize_v2(method, resource, query_parameters, headers)
372370

373371
# Generate the string to sign.
374372
elements_to_sign = [
@@ -462,6 +460,7 @@ def generate_signed_url_v4(
462460
:type resource: str
463461
:param resource: A pointer to a specific resource
464462
(typically, ``/bucket-name/path/to/blob.txt``).
463+
Caller should have already URL-encoded the value.
465464
466465
:type expiration: Union[Integer, datetime.datetime, datetime.timedelta]
467466
:param expiration: Point in time when the signed URL should expire.
@@ -589,13 +588,20 @@ def generate_signed_url_v4(
589588
ordered_query_parameters = sorted(query_parameters.items())
590589
canonical_query_string = six.moves.urllib.parse.urlencode(ordered_query_parameters)
591590

591+
lowercased_headers = dict(ordered_headers)
592+
593+
if "x-goog-content-sha256" in lowercased_headers:
594+
payload = lowercased_headers["x-goog-content-sha256"]
595+
else:
596+
payload = "UNSIGNED-PAYLOAD"
597+
592598
canonical_elements = [
593599
method,
594600
resource,
595601
canonical_query_string,
596602
canonical_header_string,
597603
signed_headers,
598-
"UNSIGNED-PAYLOAD",
604+
payload,
599605
]
600606
canonical_request = "\n".join(canonical_elements)
601607

tests/unit/test__signing.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -309,12 +309,12 @@ def test_w_embedded_ws(self):
309309
self.assertEqual(ordered, expected_ordered)
310310

311311

312-
class Test_canonicalize(unittest.TestCase):
312+
class Test_canonicalize_v2(unittest.TestCase):
313313
@staticmethod
314314
def _call_fut(*args, **kwargs):
315-
from google.cloud.storage._signing import canonicalize
315+
from google.cloud.storage._signing import canonicalize_v2
316316

317-
return canonicalize(*args, **kwargs)
317+
return canonicalize_v2(*args, **kwargs)
318318

319319
def test_wo_headers_or_query_parameters(self):
320320
method = "GET"
@@ -650,6 +650,9 @@ def test_w_custom_host_header(self):
650650
def test_w_custom_headers(self):
651651
self._generate_helper(headers={"x-goog-foo": "bar"})
652652

653+
def test_w_custom_payload_hash_goog(self):
654+
self._generate_helper(headers={"x-goog-content-sha256": "DEADBEEF"})
655+
653656
def test_w_custom_query_parameters_w_string_value(self):
654657
self._generate_helper(query_parameters={"bar": "/"})
655658

0 commit comments

Comments
 (0)