1# Copyright: (c) 2018, Aaron Haaf <aabonh@gmail.com> 2# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 3 4from __future__ import (absolute_import, division, print_function) 5__metaclass__ = type 6 7import datetime 8import hashlib 9import hmac 10import operator 11 12try: 13 from boto3 import session 14except ImportError: 15 pass 16 17from ansible.module_utils.six.moves.urllib.parse import urlencode 18from ansible.module_utils.urls import open_url 19 20from .ec2 import HAS_BOTO3 21from .ec2 import get_aws_connection_info 22 23 24def hexdigest(s): 25 """ 26 Returns the sha256 hexdigest of a string after encoding. 27 """ 28 29 return hashlib.sha256(s.encode("utf-8")).hexdigest() 30 31 32def format_querystring(params=None): 33 """ 34 Returns properly url-encoded query string from the provided params dict. 35 36 It's specially sorted for cannonical requests 37 """ 38 39 if not params: 40 return "" 41 42 # Query string values must be URL-encoded (space=%20). The parameters must be sorted by name. 43 return urlencode(sorted(params.items(), operator.itemgetter(0))) 44 45 46# Key derivation functions. See: 47# http://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html#signature-v4-examples-python 48def sign(key, msg): 49 ''' 50 Return digest for key applied to msg 51 ''' 52 53 return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest() 54 55 56def get_signature_key(key, dateStamp, regionName, serviceName): 57 ''' 58 Returns signature key for AWS resource 59 ''' 60 61 kDate = sign(("AWS4" + key).encode("utf-8"), dateStamp) 62 kRegion = sign(kDate, regionName) 63 kService = sign(kRegion, serviceName) 64 kSigning = sign(kService, "aws4_request") 65 return kSigning 66 67 68def get_aws_credentials_object(module): 69 ''' 70 Returns aws_access_key_id, aws_secret_access_key, session_token for a module. 71 ''' 72 73 if not HAS_BOTO3: 74 module.fail_json("get_aws_credentials_object requires boto3") 75 76 dummy, dummy, boto_params = get_aws_connection_info(module, boto3=True) 77 s = session.Session(**boto_params) 78 79 return s.get_credentials() 80 81 82# Reference: https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html 83def signed_request( 84 module=None, 85 method="GET", service=None, host=None, uri=None, 86 query=None, body="", headers=None, 87 session_in_header=True, session_in_query=False 88): 89 """Generate a SigV4 request to an AWS resource for a module 90 91 This is used if you wish to authenticate with AWS credentials to a secure endpoint like an elastisearch domain. 92 93 Returns :class:`HTTPResponse` object. 94 95 Example: 96 result = signed_request( 97 module=this, 98 service="es", 99 host="search-recipes1-xxxxxxxxx.us-west-2.es.amazonaws.com", 100 ) 101 102 :kwarg host: endpoint to talk to 103 :kwarg service: AWS id of service (like `ec2` or `es`) 104 :kwarg module: An AnsibleAWSModule to gather connection info from 105 106 :kwarg body: (optional) Payload to send 107 :kwarg method: (optional) HTTP verb to use 108 :kwarg query: (optional) dict of query params to handle 109 :kwarg uri: (optional) Resource path without query parameters 110 111 :kwarg session_in_header: (optional) Add the session token to the headers 112 :kwarg session_in_query: (optional) Add the session token to the query parameters 113 114 :returns: HTTPResponse 115 """ 116 117 if not HAS_BOTO3: 118 module.fail_json("A sigv4 signed_request requires boto3") 119 120 # "Constants" 121 122 t = datetime.datetime.utcnow() 123 amz_date = t.strftime("%Y%m%dT%H%M%SZ") 124 datestamp = t.strftime("%Y%m%d") # Date w/o time, used in credential scope 125 algorithm = "AWS4-HMAC-SHA256" 126 127 # AWS stuff 128 129 region, dummy, dummy = get_aws_connection_info(module, boto3=True) 130 credentials = get_aws_credentials_object(module) 131 access_key = credentials.access_key 132 secret_key = credentials.secret_key 133 session_token = credentials.token 134 135 if not access_key: 136 module.fail_json(msg="aws_access_key_id is missing") 137 if not secret_key: 138 module.fail_json(msg="aws_secret_access_key is missing") 139 140 credential_scope = "/".join([datestamp, region, service, "aws4_request"]) 141 142 # Argument Defaults 143 144 uri = uri or "/" 145 query_string = format_querystring(query) if query else "" 146 147 headers = headers or dict() 148 query = query or dict() 149 150 headers.update({ 151 "host": host, 152 "x-amz-date": amz_date, 153 }) 154 155 # Handle adding of session_token if present 156 if session_token: 157 if session_in_header: 158 headers["X-Amz-Security-Token"] = session_token 159 if session_in_query: 160 query["X-Amz-Security-Token"] = session_token 161 162 if method == "GET": 163 body = "" 164 165 # Derived data 166 167 body_hash = hexdigest(body) 168 signed_headers = ";".join(sorted(headers.keys())) 169 170 # Setup Cannonical request to generate auth token 171 172 cannonical_headers = "\n".join([ 173 key.lower().strip() + ":" + value for key, value in headers.items() 174 ]) + "\n" # Note additional trailing newline 175 176 cannonical_request = "\n".join([ 177 method, 178 uri, 179 query_string, 180 cannonical_headers, 181 signed_headers, 182 body_hash, 183 ]) 184 185 string_to_sign = "\n".join([algorithm, amz_date, credential_scope, hexdigest(cannonical_request)]) 186 187 # Sign the Cannonical request 188 189 signing_key = get_signature_key(secret_key, datestamp, region, service) 190 signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest() 191 192 # Make auth header with that info 193 194 authorization_header = "{0} Credential={1}/{2}, SignedHeaders={3}, Signature={4}".format( 195 algorithm, access_key, credential_scope, signed_headers, signature 196 ) 197 198 # PERFORM THE REQUEST! 199 200 url = "https://" + host + uri 201 202 if query_string != "": 203 url = url + "?" + query_string 204 205 final_headers = { 206 "x-amz-date": amz_date, 207 "Authorization": authorization_header, 208 } 209 210 final_headers.update(headers) 211 212 return open_url(url, method=method, data=body, headers=final_headers) 213