├── 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 | --------------------------------------------------------------------------------