├── LICENSE
├── README.md
├── hmac_ltd.py
├── awsiot_sign_test.py
└── awsiot_sign.py
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Tom Manning
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # aws-signature-iot-python
2 |
3 | aws signature generator creates headers for the REST API for AWS-IOT shadows. It runs on micropython and uses micropython libraries.
4 |
5 | Usage:
6 | * GET: micropython awsiot_sign_test.py -a yourAccessKey -k yourSecretKey -e yourEndPointId -s shadowName
7 | * POST: micropython awsiot_sign_test.py -a yourAccessKey -k yourSecretKey -e yourEndPointId -s shadowName -m POST -b "{\"state\": {\"reported\": {\"status\":\"test of REST POST\"}}}"
8 |
9 | Note: escaped double-quotes have to be used in the body argument; single quotes are not valid JSON.
10 |
11 | awsiot-sign.py is intended for an embedded environment (like the ESP) so that REST can be used to get and update AWS-IOT shadows. This an alternative to using MQTT + TLS or a webhook. It does require secure storage of the AWS secret key.
12 |
13 | awsiot_sign_test.py provides a command line interface for the awsiot-signing function.
14 |
15 | A simplified/limited hmac module which uses a subset of the hash lib is provided (the one in micropython-lib didn't handle binary keys).
16 |
17 | Also the micropython urequest library (as of this commit) had to be modified in order have the GET work: get the content-length from the header and do a socket.read of that content length.
18 |
19 | The code has been tested on Mac OS X, ESP8266 and ESP32 using micropython 1.8.7 and 1.9.
20 |
--------------------------------------------------------------------------------
/hmac_ltd.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | Title: A "keyed-hash message authentication code" implementation in pure python.
4 |
5 | edited from: https://github.com/brschdr/python-hmac
6 | only supports sha256; doesn't support hexdigest since uhashlib doesn't
7 |
8 | License: This code is in Public Domain or MIT License, choose suitable one for you.
9 |
10 | Description: This HMAC implementation is in accordance with RFC 2104 specification.
11 | User supplied "key" and "message" must be a Python Byte Object.
12 | """
13 |
14 | try:
15 | import uhashlib as _hashlib
16 | except ImportError:
17 | import hashlib as _hashlib
18 |
19 | class HMAC:
20 |
21 | def __init__(self,key,message,hash_h):
22 |
23 | """ key and message must be byte object """
24 |
25 | self.i_key_pad = bytearray()
26 | self.o_key_pad = bytearray()
27 | self.key = key
28 | self.message = message
29 | self.blocksize = 64
30 | self.hash_h = hash_h
31 | self.init_flag = False
32 |
33 |
34 | def init_pads(self):
35 | """ creating inner padding and outer padding """
36 |
37 | for i in range(self.blocksize):
38 | self.i_key_pad.append(0x36 ^ self.key[i])
39 | self.o_key_pad.append(0x5c ^ self.key[i])
40 |
41 |
42 | def init_key(self):
43 | """ key regeneration """
44 |
45 | if len(self.key) > self.blocksize:
46 | self.key = bytearray(_hashlib.md5(key).digest())
47 | elif len(self.key) < self.blocksize:
48 | i = len(self.key)
49 | while i < self.blocksize:
50 | self.key += b"\x00"
51 | i += 1
52 |
53 |
54 | def digest(self):
55 | """ returns a digest, byte object. """
56 |
57 | if self.init_flag == False:
58 | self.init_key()
59 | self.init_pads()
60 | self.init_flag = True
61 |
62 | return self.hash_h(bytes(self.o_key_pad)+self.hash_h(bytes(self.i_key_pad)+self.message).digest()).digest()
63 |
64 | def new(key, msg = None, digestmod = None):
65 | return HMAC(key, msg, digestmod)
66 |
--------------------------------------------------------------------------------
/awsiot_sign_test.py:
--------------------------------------------------------------------------------
1 | #!/usr/local/bin/micropython
2 | # AWS Version 4 signing tester: makes requests to AWS-IOT based on command line args
3 |
4 |
5 | import sys, os
6 | import argparse
7 | from time import gmtime
8 | #import utime
9 | import urequests
10 |
11 | import awsiot_sign
12 |
13 |
14 | def main(argv=None):
15 | if argv is None:
16 | argv = sys.argv
17 |
18 | parser = argparse.ArgumentParser(description='AWS IOT REST IO')
19 | parser.add_argument('-k','--secret_key', required=True)
20 | parser.add_argument('-a','--access_key', required=True)
21 | parser.add_argument('-e','--endpt_prefix', required=True)
22 | parser.add_argument('-s','--shadow_id', required=True)
23 | parser.add_argument('-m','--method', help='method: GET/POST', choices=['GET', 'POST'], default='GET')
24 | parser.add_argument('-r','--region', help='AWS region; eg us-west-1', default='us-east-1')
25 | parser.add_argument('-b','--body', help='shadow state, in json format', default='')
26 | parser.add_argument('-d','--do_request', help='set to False to disable sending request to AWS: True/False', type=bool, default=True)
27 | args = parser.parse_args()
28 |
29 | # Create a date for headers and the credential string
30 | # the following doesn't work in micropython & utime doesn't support gmtime()
31 | # amzdate = time.strftime('%Y%m%dT%H%M%SZ', time.gmtime())
32 | # so using gmtime and then doing a format
33 | t = gmtime()
34 | # Date w/o time, used in credential scope
35 | datestamp = '{0}{1:02d}{2:02d}'.format(t[0],t[1],t[2])
36 | #datestamp = utime.strftime('%Y%m%d')
37 |
38 | DUMMY_TOD_FOR_AMZDATE = False
39 | if (DUMMY_TOD_FOR_AMZDATE == True):
40 | time_now_utc = "T12:34:56Z"
41 | else:
42 | time_now_utc = 'T{0:02d}{1:02d}{2:02d}Z'.format(t[3],t[4],t[5])
43 | amzdate = datestamp + time_now_utc
44 |
45 | request_dict = awsiot_sign.request_gen(args.endpt_prefix, args.shadow_id, args.access_key, args.secret_key, amzdate, method=args.method, region=args.region, body=args.body)
46 |
47 | endpoint = 'https://' + request_dict["host"] + request_dict["uri"]
48 |
49 | if (args.do_request == True):
50 | print('\nBEGIN REQUEST++++++++++++++++++++++++++++++++++++')
51 | print('Request URL = ' + endpoint)
52 | if (args.method == 'GET'):
53 | r = urequests.get(endpoint, headers=request_dict["headers"])
54 | else:
55 | r = urequests.post(endpoint, headers=request_dict["headers"], data=args.body)
56 |
57 | print('\nRESPONSE++++++++++++++++++++++++++++++++++++')
58 | print('Response code: %d\n' % r.status_code)
59 | #print(r.reason)
60 | #print(r.headers)
61 | print(r.json())
62 |
63 | if __name__ == "__main__":
64 | sys.exit(main())
65 |
--------------------------------------------------------------------------------
/awsiot_sign.py:
--------------------------------------------------------------------------------
1 | """AWS Version 4 signing Python module.
2 |
3 | Implements the signing algorithm as described here:
4 | http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
5 |
6 | However:
7 | - specific to the AWS-IOT service; service is hardcoded: 'iotdata'
8 | - specific to micropython; uses micropython modules
9 | """
10 |
11 | try:
12 | from uhashlib import sha256 as _sha256
13 | except ImportError:
14 | print("Warning: not using uhashlib")
15 | from hashlib import sha256 as _sha256
16 |
17 | import hmac_ltd as _hmac
18 | import ubinascii as _ubinascii
19 |
20 |
21 | def request_gen(endpt_prefix, shadow_id, access_key, secret_key, date_time_stamp, method='GET', region='us-east-1', body=''):
22 | service = 'iotdata'
23 | request_type = 'aws4_request'
24 | algorithm = 'AWS4-HMAC-SHA256'
25 |
26 | date_stamp = date_time_stamp[:8]
27 |
28 | return_dict = {}
29 | return_dict["host"] = endpt_prefix + '.' + 'iot' + '.' + region + '.' + 'amazonaws.com'
30 | return_dict["uri"] = '/things/' + shadow_id + '/shadow'
31 |
32 | # make the signing key from date, region, service and request_type
33 | # in micropython, the key has to be a byte array
34 | key = bytearray()
35 | key.extend(('AWS4' + secret_key).encode())
36 | kDate = _hmac.new(key, date_stamp, _sha256).digest()
37 | #print("request_gen: kDate: {}".format(kDate))
38 | # kDate, kRegion, kService & kSigning are binary byte arrays
39 | kRegion = _hmac.new(kDate, region, _sha256).digest()
40 | #print("request_gen: kRegion: {}".format(kRegion))
41 | kService = _hmac.new(kRegion, service, _sha256).digest()
42 | signing_key = _hmac.new(kService, request_type, _sha256).digest()
43 |
44 | # make the string to sign
45 | canonical_querystring = '' #no request params for shadows
46 | canonical_headers = 'host:' + return_dict["host"] + '\n' + 'x-amz-date:' + date_time_stamp + '\n'
47 | signed_headers = 'host;x-amz-date'
48 | payload_hash = _ubinascii.hexlify(_sha256(body).digest()).decode("utf-8")
49 |
50 | canonical_request = method + '\n' + return_dict["uri"] + '\n' + canonical_querystring + '\n' + canonical_headers + '\n' + signed_headers + '\n' + payload_hash
51 | #print('\n === canonical_request: \n' + canonical_request + '\n =========== end of canonical_request')
52 |
53 | credential_scope = date_stamp + '/' + region + '/' + service + '/' + request_type
54 | string_to_sign = algorithm + '\n' + date_time_stamp + '\n' + credential_scope + '\n' + _ubinascii.hexlify(_sha256(canonical_request).digest()).decode("utf-8")
55 | #print('\n === string_to_sign: \n' + string_to_sign + '\n =========== end of string_to_sign')
56 | #print('signing_key: ' + str(_ubinascii.hexlify(signing_key)))
57 |
58 | # generate the signature:
59 | signature = _ubinascii.hexlify(_hmac.new(signing_key, string_to_sign, _sha256).digest()).decode("utf-8")
60 |
61 | authorization_header = algorithm + ' ' + 'Credential=' + access_key + '/' + credential_scope + ', ' + 'SignedHeaders=' + signed_headers + ', ' + 'Signature=' + signature
62 |
63 | return_dict["headers"] = {'x-amz-date':date_time_stamp, 'Authorization':authorization_header}
64 | return return_dict
65 |
--------------------------------------------------------------------------------