signer.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. # -*- coding: utf-8 -*-
  2. # MinIO Python Library for Amazon S3 Compatible Cloud Storage,
  3. # (C) 2015-2020 MinIO, Inc.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. """
  17. minio.signer
  18. ~~~~~~~~~~~~~~~
  19. This module implements all helpers for AWS Signature version '4' support.
  20. :copyright: (c) 2015 by MinIO, Inc.
  21. :license: Apache 2.0, see LICENSE for more details.
  22. """
  23. from __future__ import absolute_import, annotations
  24. import hashlib
  25. import hmac
  26. import re
  27. from collections import OrderedDict
  28. from datetime import datetime
  29. from typing import Mapping, cast
  30. from urllib.parse import SplitResult
  31. from . import time
  32. from .credentials import Credentials
  33. from .helpers import DictType, queryencode, sha256_hash
  34. SIGN_V4_ALGORITHM = 'AWS4-HMAC-SHA256'
  35. _MULTI_SPACE_REGEX = re.compile(r"( +)")
  36. def _hmac_hash(
  37. key: bytes,
  38. data: bytes,
  39. hexdigest: bool = False,
  40. ) -> bytes | str:
  41. """Return HMacSHA256 digest of given key and data."""
  42. hasher = hmac.new(key, data, hashlib.sha256)
  43. return hasher.hexdigest() if hexdigest else hasher.digest()
  44. def _get_scope(date: datetime, region: str, service_name: str) -> str:
  45. """Get scope string."""
  46. return f"{time.to_signer_date(date)}/{region}/{service_name}/aws4_request"
  47. def _get_canonical_headers(
  48. headers: Mapping[str, str | list[str] | tuple[str]],
  49. ) -> tuple[str, str]:
  50. """Get canonical headers."""
  51. ordered_headers = {}
  52. for key, values in headers.items():
  53. key = key.lower()
  54. if key not in (
  55. "authorization",
  56. "user-agent",
  57. ):
  58. values = values if isinstance(values, (list, tuple)) else [values]
  59. ordered_headers[key] = ",".join([
  60. _MULTI_SPACE_REGEX.sub(" ", value).strip() for value in values
  61. ])
  62. ordered_headers = OrderedDict(sorted(ordered_headers.items()))
  63. signed_headers = ";".join(ordered_headers.keys())
  64. canonical_headers = "\n".join(
  65. [f"{key}:{value}" for key, value in ordered_headers.items()],
  66. )
  67. return canonical_headers, signed_headers
  68. def _get_canonical_query_string(query: str) -> str:
  69. """Get canonical query string."""
  70. query = query or ""
  71. return "&".join(
  72. [
  73. "=".join(pair) for pair in sorted(
  74. [params.split("=") for params in query.split("&")],
  75. )
  76. ],
  77. )
  78. def _get_canonical_request_hash(
  79. method: str,
  80. url: SplitResult,
  81. headers: Mapping[str, str | list[str] | tuple[str]],
  82. content_sha256: str,
  83. ) -> tuple[str, str]:
  84. """Get canonical request hash."""
  85. canonical_headers, signed_headers = _get_canonical_headers(headers)
  86. canonical_query_string = _get_canonical_query_string(url.query)
  87. # CanonicalRequest =
  88. # HTTPRequestMethod + '\n' +
  89. # CanonicalURI + '\n' +
  90. # CanonicalQueryString + '\n' +
  91. # CanonicalHeaders + '\n\n' +
  92. # SignedHeaders + '\n' +
  93. # HexEncode(Hash(RequestPayload))
  94. canonical_request = (
  95. f"{method}\n"
  96. f"{url.path or '/'}\n"
  97. f"{canonical_query_string}\n"
  98. f"{canonical_headers}\n\n"
  99. f"{signed_headers}\n"
  100. f"{content_sha256}"
  101. )
  102. return sha256_hash(canonical_request), signed_headers
  103. def _get_string_to_sign(
  104. date: datetime,
  105. scope: str,
  106. canonical_request_hash: str,
  107. ) -> str:
  108. """Get string-to-sign."""
  109. return (
  110. f"AWS4-HMAC-SHA256\n{time.to_amz_date(date)}\n{scope}\n"
  111. f"{canonical_request_hash}"
  112. )
  113. def _get_signing_key(
  114. secret_key: str,
  115. date: datetime,
  116. region: str,
  117. service_name: str,
  118. ) -> bytes:
  119. """Get signing key."""
  120. date_key = cast(
  121. bytes,
  122. _hmac_hash(
  123. ("AWS4" + secret_key).encode(),
  124. time.to_signer_date(date).encode(),
  125. ),
  126. )
  127. date_region_key = cast(bytes, _hmac_hash(date_key, region.encode()))
  128. date_region_service_key = cast(
  129. bytes,
  130. _hmac_hash(date_region_key, service_name.encode()),
  131. )
  132. return cast(
  133. bytes,
  134. _hmac_hash(date_region_service_key, b"aws4_request"),
  135. )
  136. def _get_signature(signing_key: bytes, string_to_sign: str) -> str:
  137. """Get signature."""
  138. return cast(
  139. str,
  140. _hmac_hash(signing_key, string_to_sign.encode(), hexdigest=True),
  141. )
  142. def _get_authorization(
  143. access_key: str,
  144. scope: str,
  145. signed_headers: str,
  146. signature: str,
  147. ) -> str:
  148. """Get authorization."""
  149. return (
  150. f"AWS4-HMAC-SHA256 Credential={access_key}/{scope}, "
  151. f"SignedHeaders={signed_headers}, Signature={signature}"
  152. )
  153. def _sign_v4(
  154. *,
  155. service_name: str,
  156. method: str,
  157. url: SplitResult,
  158. region: str,
  159. headers: DictType,
  160. credentials: Credentials,
  161. content_sha256: str,
  162. date: datetime,
  163. ) -> DictType:
  164. """Do signature V4 of given request for given service name."""
  165. scope = _get_scope(date, region, service_name)
  166. canonical_request_hash, signed_headers = _get_canonical_request_hash(
  167. method, url, headers, content_sha256,
  168. )
  169. string_to_sign = _get_string_to_sign(date, scope, canonical_request_hash)
  170. signing_key = _get_signing_key(
  171. credentials.secret_key, date, region, service_name,
  172. )
  173. signature = _get_signature(signing_key, string_to_sign)
  174. authorization = _get_authorization(
  175. credentials.access_key, scope, signed_headers, signature,
  176. )
  177. headers["Authorization"] = authorization
  178. return headers
  179. def sign_v4_s3(
  180. *,
  181. method: str,
  182. url: SplitResult,
  183. region: str,
  184. headers: DictType,
  185. credentials: Credentials,
  186. content_sha256: str,
  187. date: datetime,
  188. ) -> DictType:
  189. """Do signature V4 of given request for S3 service."""
  190. return _sign_v4(
  191. service_name="s3",
  192. method=method,
  193. url=url,
  194. region=region,
  195. headers=headers,
  196. credentials=credentials,
  197. content_sha256=content_sha256,
  198. date=date,
  199. )
  200. def sign_v4_sts(
  201. *,
  202. method: str,
  203. url: SplitResult,
  204. region: str,
  205. headers: DictType,
  206. credentials: Credentials,
  207. content_sha256: str,
  208. date: datetime,
  209. ) -> DictType:
  210. """Do signature V4 of given request for STS service."""
  211. return _sign_v4(
  212. service_name="sts",
  213. method=method,
  214. url=url,
  215. region=region,
  216. headers=headers,
  217. credentials=credentials,
  218. content_sha256=content_sha256,
  219. date=date,
  220. )
  221. def _get_presign_canonical_request_hash( # pylint: disable=invalid-name
  222. *,
  223. method: str,
  224. url: SplitResult,
  225. access_key: str,
  226. scope: str,
  227. date: datetime,
  228. expires: int,
  229. ) -> tuple[str, SplitResult]:
  230. """Get canonical request hash for presign request."""
  231. x_amz_credential = queryencode(access_key + "/" + scope)
  232. canonical_headers, signed_headers = "host:" + url.netloc, "host"
  233. query = url.query+"&" if url.query else ""
  234. query += (
  235. f"X-Amz-Algorithm=AWS4-HMAC-SHA256"
  236. f"&X-Amz-Credential={x_amz_credential}"
  237. f"&X-Amz-Date={time.to_amz_date(date)}"
  238. f"&X-Amz-Expires={expires}"
  239. f"&X-Amz-SignedHeaders={signed_headers}"
  240. )
  241. parts = list(url)
  242. parts[3] = query
  243. url = SplitResult(*parts)
  244. canonical_query_string = _get_canonical_query_string(query)
  245. # CanonicalRequest =
  246. # HTTPRequestMethod + '\n' +
  247. # CanonicalURI + '\n' +
  248. # CanonicalQueryString + '\n' +
  249. # CanonicalHeaders + '\n\n' +
  250. # SignedHeaders + '\n' +
  251. # HexEncode(Hash(RequestPayload))
  252. canonical_request = (
  253. f"{method}\n"
  254. f"{url.path or '/'}\n"
  255. f"{canonical_query_string}\n"
  256. f"{canonical_headers}\n\n"
  257. f"{signed_headers}\n"
  258. f"UNSIGNED-PAYLOAD"
  259. )
  260. return sha256_hash(canonical_request), url
  261. def presign_v4(
  262. *,
  263. method: str,
  264. url: SplitResult,
  265. region: str,
  266. credentials: Credentials,
  267. date: datetime,
  268. expires: int,
  269. ) -> SplitResult:
  270. """Do signature V4 of given presign request."""
  271. scope = _get_scope(date, region, "s3")
  272. canonical_request_hash, url = _get_presign_canonical_request_hash(
  273. method=method,
  274. url=url,
  275. access_key=credentials.access_key,
  276. scope=scope,
  277. date=date,
  278. expires=expires,
  279. )
  280. string_to_sign = _get_string_to_sign(date, scope, canonical_request_hash)
  281. signing_key = _get_signing_key(credentials.secret_key, date, region, "s3")
  282. signature = _get_signature(signing_key, string_to_sign)
  283. parts = list(url)
  284. parts[3] = url.query + "&X-Amz-Signature=" + queryencode(signature)
  285. url = SplitResult(*parts)
  286. return url
  287. def get_credential_string(access_key: str, date: datetime, region: str) -> str:
  288. """Get credential string of given access key, date and region."""
  289. return f"{access_key}/{time.to_signer_date(date)}/{region}/s3/aws4_request"
  290. def post_presign_v4(
  291. data: str,
  292. secret_key: str,
  293. date: datetime,
  294. region: str,
  295. ) -> str:
  296. """Do signature V4 of given presign POST form-data."""
  297. return _get_signature(
  298. _get_signing_key(secret_key, date, region, "s3"),
  299. data,
  300. )