Search code examples
python-3.xamazon-web-servicespython-requestsdigital-signatureamazon-ses

Getting Response 403 When signing an Amazon Request


I am trying to send an email through amazon SES without the SDK so I can send emails asynchronously in Python. I am using amazon's v4 signing method on their site here: https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html, but I'm not having any luck sending emails without the SDK. The output is:

RESPONSE++++++++++++++++++++++++++++++++++++
Response code: 403

<ErrorResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
  <Error>
    <Type>Sender</Type>
    <Code>SignatureDoesNotMatch</Code>
    <Message>The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.</Message>
  </Error>
  <RequestId>a19a5fa1-3228-11e9-b2bc-ddb6d8a1cb1c</RequestId>
</ErrorResponse>


Process finished with exit code 0

Here is the block of code generating that response:

import datetime
import hashlib
import hmac
import urllib.parse
import requests
method = 'GET'
service = 'ses'
host = 'email.us-east-1.amazonaws.com'
region = 'us-east-1'
endpoint = 'https://email.us-east-1.amazonaws.com/'


def sign(key, msg):
    return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest()


def getSignatureKey(key, dateStamp, regionName, serviceName):
    kDate = sign(('AWS4' + key).encode('utf-8'), dateStamp)
    kRegion = sign(kDate, regionName)
    kService = sign(kRegion, serviceName)
    kSigning = sign(kService, 'aws4_request')
    return kSigning


access_key = '<my access_key here>'
secret_key = '<my secret_key here>'
my_email = 'my email here'


t = datetime.datetime.utcnow()
amz_date = t.strftime('%Y%m%dT%H%M%SZ')
datestamp = t.strftime('%Y%m%d')
canonical_uri = '/'
canonical_headers = 'host:' + host + '\n'
signed_headers = 'host'
algorithm = 'AWS4-HMAC-SHA256'
credential_scope = datestamp + '/' + region + '/' + service + '/' + 'aws4_request'

canonical_querystring = '''Action=SendEmail
&Source=%s%40gmail.com
&Destination.ToAddresses.member.1=%s%40gmail.com
&Message.Subject.Data=This%20is%20the%20subject%20line.
&Message.Body.Text.Data=Hello.%20I%20hope%20you%20are%20having%20a%20good%20day''' % (my_email, my_email)

canonical_querystring += '&X-Amz-Algorithm=AWS4-HMAC-SHA256'
canonical_querystring += '&X-Amz-Credential=' + urllib.parse.quote_plus(access_key + '/' + credential_scope)
canonical_querystring += '&X-Amz-Date=' + amz_date
canonical_querystring += '&X-Amz-Expires=30'
canonical_querystring += '&X-Amz-SignedHeaders=' + signed_headers

payload_hash = hashlib.sha256(('').encode('utf-8')).hexdigest()
canonical_request = method + '\n' + canonical_uri + '\n' + canonical_querystring + '\n' + canonical_headers + '\n' + signed_headers + '\n' + payload_hash
string_to_sign = algorithm + '\n' +  amz_date + '\n' +  credential_scope + '\n' +  hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()
signing_key = getSignatureKey(secret_key, datestamp, region, service)
signature = hmac.new(signing_key, (string_to_sign).encode("utf-8"), hashlib.sha256).hexdigest()
canonical_querystring += '&X-Amz-Signature=' + signature
request_url = endpoint + "?" + canonical_querystring

print('\nBEGIN REQUEST++++++++++++++++++++++++++++++++++++')
print('Request URL = ' + request_url)
r = requests.get(request_url)

print('\nRESPONSE++++++++++++++++++++++++++++++++++++')
print('Response code: %d\n' % r.status_code)
print(r.text)

This is basically Amazon's code for generating a signature copied and pasted from their docs. Does anyone know what I'm doing wrong in signing my requests to amazon?

Edit* I changed the canonical_querystring to be alphabetical like so:

canonical_querystring = '''Action=SendEmail
&Destination.ToAddresses.member.1={}%40gmail.com
&Message.Body.Text.Data=Hello.%20I%20hope%20you%20are%20having%20a%20good%20day
&Message.Subject.Data=This%20is%20the%20subject%20line.
&Source={}%40gmail.com'''.format(my_email, my_email)

It's still giving me the same error though. Everything else in the query string is alphabetized, or "canonically ordered".


Solution

  • Your canonical query string is not canonical.

    You have Action... Source... Destination... Message but all of the parameters need to be lexically ordered.

    Step 3: Create the canonical query string.

    [...]

    The parameters must be sorted by name.

    This ordering is the reason why the value is called canonical. It's necessary because the relative positions of query string parameters aren't necessarily guaranteed. The parameters don't need to be sorted in the actual query string accompanying the request, but they do need to be sorted here, for signing.

    Since a given request can have only one possible valid signature, the parameters are sorted before signing to remove the ambiguity that might otherwise arise if a user agent or proxy rearranged the query parameters or ordered them arbitrarily when building the URL (as might be expected if, for example, the parameters are passed to the UA as an unordered hash/dictionary structure).