| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354 |
- # -*- coding: utf-8 -*-
- # MinIO Python Library for Amazon S3 Compatible Cloud Storage,
- # (C) 2015-2020 MinIO, Inc.
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- """
- minio.signer
- ~~~~~~~~~~~~~~~
- This module implements all helpers for AWS Signature version '4' support.
- :copyright: (c) 2015 by MinIO, Inc.
- :license: Apache 2.0, see LICENSE for more details.
- """
- from __future__ import absolute_import, annotations
- import hashlib
- import hmac
- import re
- from collections import OrderedDict
- from datetime import datetime
- from typing import Mapping, cast
- from urllib.parse import SplitResult
- from . import time
- from .credentials import Credentials
- from .helpers import DictType, queryencode, sha256_hash
- SIGN_V4_ALGORITHM = 'AWS4-HMAC-SHA256'
- _MULTI_SPACE_REGEX = re.compile(r"( +)")
- def _hmac_hash(
- key: bytes,
- data: bytes,
- hexdigest: bool = False,
- ) -> bytes | str:
- """Return HMacSHA256 digest of given key and data."""
- hasher = hmac.new(key, data, hashlib.sha256)
- return hasher.hexdigest() if hexdigest else hasher.digest()
- def _get_scope(date: datetime, region: str, service_name: str) -> str:
- """Get scope string."""
- return f"{time.to_signer_date(date)}/{region}/{service_name}/aws4_request"
- def _get_canonical_headers(
- headers: Mapping[str, str | list[str] | tuple[str]],
- ) -> tuple[str, str]:
- """Get canonical headers."""
- ordered_headers = {}
- for key, values in headers.items():
- key = key.lower()
- if key not in (
- "authorization",
- "user-agent",
- ):
- values = values if isinstance(values, (list, tuple)) else [values]
- ordered_headers[key] = ",".join([
- _MULTI_SPACE_REGEX.sub(" ", value).strip() for value in values
- ])
- ordered_headers = OrderedDict(sorted(ordered_headers.items()))
- signed_headers = ";".join(ordered_headers.keys())
- canonical_headers = "\n".join(
- [f"{key}:{value}" for key, value in ordered_headers.items()],
- )
- return canonical_headers, signed_headers
- def _get_canonical_query_string(query: str) -> str:
- """Get canonical query string."""
- query = query or ""
- return "&".join(
- [
- "=".join(pair) for pair in sorted(
- [params.split("=") for params in query.split("&")],
- )
- ],
- )
- def _get_canonical_request_hash(
- method: str,
- url: SplitResult,
- headers: Mapping[str, str | list[str] | tuple[str]],
- content_sha256: str,
- ) -> tuple[str, str]:
- """Get canonical request hash."""
- canonical_headers, signed_headers = _get_canonical_headers(headers)
- canonical_query_string = _get_canonical_query_string(url.query)
- # CanonicalRequest =
- # HTTPRequestMethod + '\n' +
- # CanonicalURI + '\n' +
- # CanonicalQueryString + '\n' +
- # CanonicalHeaders + '\n\n' +
- # SignedHeaders + '\n' +
- # HexEncode(Hash(RequestPayload))
- canonical_request = (
- f"{method}\n"
- f"{url.path or '/'}\n"
- f"{canonical_query_string}\n"
- f"{canonical_headers}\n\n"
- f"{signed_headers}\n"
- f"{content_sha256}"
- )
- return sha256_hash(canonical_request), signed_headers
- def _get_string_to_sign(
- date: datetime,
- scope: str,
- canonical_request_hash: str,
- ) -> str:
- """Get string-to-sign."""
- return (
- f"AWS4-HMAC-SHA256\n{time.to_amz_date(date)}\n{scope}\n"
- f"{canonical_request_hash}"
- )
- def _get_signing_key(
- secret_key: str,
- date: datetime,
- region: str,
- service_name: str,
- ) -> bytes:
- """Get signing key."""
- date_key = cast(
- bytes,
- _hmac_hash(
- ("AWS4" + secret_key).encode(),
- time.to_signer_date(date).encode(),
- ),
- )
- date_region_key = cast(bytes, _hmac_hash(date_key, region.encode()))
- date_region_service_key = cast(
- bytes,
- _hmac_hash(date_region_key, service_name.encode()),
- )
- return cast(
- bytes,
- _hmac_hash(date_region_service_key, b"aws4_request"),
- )
- def _get_signature(signing_key: bytes, string_to_sign: str) -> str:
- """Get signature."""
- return cast(
- str,
- _hmac_hash(signing_key, string_to_sign.encode(), hexdigest=True),
- )
- def _get_authorization(
- access_key: str,
- scope: str,
- signed_headers: str,
- signature: str,
- ) -> str:
- """Get authorization."""
- return (
- f"AWS4-HMAC-SHA256 Credential={access_key}/{scope}, "
- f"SignedHeaders={signed_headers}, Signature={signature}"
- )
- def _sign_v4(
- *,
- service_name: str,
- method: str,
- url: SplitResult,
- region: str,
- headers: DictType,
- credentials: Credentials,
- content_sha256: str,
- date: datetime,
- ) -> DictType:
- """Do signature V4 of given request for given service name."""
- scope = _get_scope(date, region, service_name)
- canonical_request_hash, signed_headers = _get_canonical_request_hash(
- method, url, headers, content_sha256,
- )
- string_to_sign = _get_string_to_sign(date, scope, canonical_request_hash)
- signing_key = _get_signing_key(
- credentials.secret_key, date, region, service_name,
- )
- signature = _get_signature(signing_key, string_to_sign)
- authorization = _get_authorization(
- credentials.access_key, scope, signed_headers, signature,
- )
- headers["Authorization"] = authorization
- return headers
- def sign_v4_s3(
- *,
- method: str,
- url: SplitResult,
- region: str,
- headers: DictType,
- credentials: Credentials,
- content_sha256: str,
- date: datetime,
- ) -> DictType:
- """Do signature V4 of given request for S3 service."""
- return _sign_v4(
- service_name="s3",
- method=method,
- url=url,
- region=region,
- headers=headers,
- credentials=credentials,
- content_sha256=content_sha256,
- date=date,
- )
- def sign_v4_sts(
- *,
- method: str,
- url: SplitResult,
- region: str,
- headers: DictType,
- credentials: Credentials,
- content_sha256: str,
- date: datetime,
- ) -> DictType:
- """Do signature V4 of given request for STS service."""
- return _sign_v4(
- service_name="sts",
- method=method,
- url=url,
- region=region,
- headers=headers,
- credentials=credentials,
- content_sha256=content_sha256,
- date=date,
- )
- def _get_presign_canonical_request_hash( # pylint: disable=invalid-name
- *,
- method: str,
- url: SplitResult,
- access_key: str,
- scope: str,
- date: datetime,
- expires: int,
- ) -> tuple[str, SplitResult]:
- """Get canonical request hash for presign request."""
- x_amz_credential = queryencode(access_key + "/" + scope)
- canonical_headers, signed_headers = "host:" + url.netloc, "host"
- query = url.query+"&" if url.query else ""
- query += (
- f"X-Amz-Algorithm=AWS4-HMAC-SHA256"
- f"&X-Amz-Credential={x_amz_credential}"
- f"&X-Amz-Date={time.to_amz_date(date)}"
- f"&X-Amz-Expires={expires}"
- f"&X-Amz-SignedHeaders={signed_headers}"
- )
- parts = list(url)
- parts[3] = query
- url = SplitResult(*parts)
- canonical_query_string = _get_canonical_query_string(query)
- # CanonicalRequest =
- # HTTPRequestMethod + '\n' +
- # CanonicalURI + '\n' +
- # CanonicalQueryString + '\n' +
- # CanonicalHeaders + '\n\n' +
- # SignedHeaders + '\n' +
- # HexEncode(Hash(RequestPayload))
- canonical_request = (
- f"{method}\n"
- f"{url.path or '/'}\n"
- f"{canonical_query_string}\n"
- f"{canonical_headers}\n\n"
- f"{signed_headers}\n"
- f"UNSIGNED-PAYLOAD"
- )
- return sha256_hash(canonical_request), url
- def presign_v4(
- *,
- method: str,
- url: SplitResult,
- region: str,
- credentials: Credentials,
- date: datetime,
- expires: int,
- ) -> SplitResult:
- """Do signature V4 of given presign request."""
- scope = _get_scope(date, region, "s3")
- canonical_request_hash, url = _get_presign_canonical_request_hash(
- method=method,
- url=url,
- access_key=credentials.access_key,
- scope=scope,
- date=date,
- expires=expires,
- )
- string_to_sign = _get_string_to_sign(date, scope, canonical_request_hash)
- signing_key = _get_signing_key(credentials.secret_key, date, region, "s3")
- signature = _get_signature(signing_key, string_to_sign)
- parts = list(url)
- parts[3] = url.query + "&X-Amz-Signature=" + queryencode(signature)
- url = SplitResult(*parts)
- return url
- def get_credential_string(access_key: str, date: datetime, region: str) -> str:
- """Get credential string of given access key, date and region."""
- return f"{access_key}/{time.to_signer_date(date)}/{region}/s3/aws4_request"
- def post_presign_v4(
- data: str,
- secret_key: str,
- date: datetime,
- region: str,
- ) -> str:
- """Do signature V4 of given presign POST form-data."""
- return _get_signature(
- _get_signing_key(secret_key, date, region, "s3"),
- data,
- )
|