├── .github └── PULL_REQUEST_TEMPLATE.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── IoT_Global_Device_Provisioning.jpg ├── LICENSE ├── README.md └── provisioning ├── cfn └── cfn-iot-global-device-provisioning.json ├── global-device ├── create-root-ca-bundle.sh ├── global-device.py └── requirements.txt └── lambda ├── lambda_function.py └── requirements.txt /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/aws-samples/aws-iot-global-device-provisioning/issues), or [recently closed](https://github.com/aws-samples/aws-iot-global-device-provisioning/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws-samples/aws-iot-global-device-provisioning/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/aws-samples/aws-iot-global-device-provisioning/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /IoT_Global_Device_Provisioning.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-global-device-provisioning/2acdc6b41e34b0b14dca9b92956691d594479554/IoT_Global_Device_Provisioning.jpg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Global IoT Device Provisioning 2 | 3 | This repository contains the sample implementation for AWS Global IoT Device Provisioning. The architecture and how the solution works has been published in the [AWS IoT Blog](https://aws.amazon.com/blogs/iot/provision-devices-globally-with-aws-iot/). 4 | 5 | This document will guide you to the process to setup the required AWS resources. A virtual device is provided which can be used to test the sample implementation. 6 | 7 | The setup has been tested on an EC2 Instance with Amazon Linux AMI [amzn-ami-hvm-2017.03.1.20170812-x86_64-gp2](https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html). Most of the resources will be deployed through an AWS CloudFormation stack. Some preparation is required before the stack can be launched. 8 | 9 | ## Architecture 10 | ![IoT_Global_Device_Provisioning.jpg](IoT_Global_Device_Provisioning.jpg) 11 | 12 | ## Create the environment 13 | 14 | 1. To lookup the geo location for the device's IP address [ipstack.com](https://ipstack.com/) is used in this example implementation. To use API from [ipstack.com](https://ipstack.com/) an API Access Key is required. To get your API Access Key follow the sign up steps at [ipstack.com](https://ipstack.com/). The Lambda function which determines the best region will get the API Access Key from an [environment variable](https://docs.aws.amazon.com/lambda/latest/dg/env_variables.html). 15 | 16 | 2. Launch an EC2 instance with Amazon Linux AMI [amzn-ami-hvm-2017.03.1.20170812-x86_64-gp2](https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html), ssh into the instance and clone the repository from github into your home directory. 17 | 18 | git clone https://github.com/aws-samples/aws-iot-global-device-provisioning.git 19 | 20 | 3. Create a rsa key pair in the directory `aws-iot-global-device-provisioning/provisioning` 21 | 22 | cd ~/aws-iot-global-device-provisioning/provisioning 23 | openssl genrsa -out global-provisioning.priv.key.pem 2048 24 | openssl rsa -in global-provisioning.priv.key.pem -outform PEM -pubout -out global-provisioning.pub.key.pem 25 | 26 | 4. Upgrade pip if the latest version 27 | 28 | sudo pip install --upgrade pip 29 | hash -r 30 | 31 | 5. Install required libraries for the Lambda function into the directory `lambda`. If you get warnings about requirements for cloud-init during the installation with pip you can safely ignore them 32 | 33 | cd lambda 34 | pip install -r requirements.txt -t . 35 | 36 | 6. Copy the public key into the directory where the Lambda is located 37 | 38 | cp ../global-provisioning.pub.key.pem . 39 | 40 | 7. Create an installation package for the Lambda function 41 | 42 | zip ../iot-global-provisioning.zip -r . 43 | 44 | 8. The Lambda function will be created by CloudFormation an the CloudFormation template expects the installation package to be present in a S3 bucket that must be located in the **same region** where the CloudFormation stack will be launched. By using the awscli to create a bucket and upload the Lambda installation package you would use the following commands: 45 | 46 | aws s3 mb s3:// 47 | aws s3 cp ../iot-global-provisioning.zip s3:/// 48 | You can also use the AWS Console to create a bucket and upload the installation package. 49 | 50 | 9. Copy the CloudFormation Template from `aws-iot-global-device-provisioning/provisioning/cfn/cfn-iot-global-device-provisioning.json` into the same S3 bucket. If the awscli is used the command looks similar to: 51 | 52 | cd .. 53 | aws s3 cp cfn/cfn-iot-global-device-provisioning.json s3:/// 54 | 55 | 10. Launch the CloudFormation template from the AWS CloudFormation console 56 | 57 | 1. Got to the AWS CloudFormation Console 58 | 2. Create Stack 59 | 3. Specify an Amazon S3 template URL: `http://s3-.amazonaws.com//cfn-iot-global-device-provisioning.json` 60 | 4. Next 61 | 5. Stack name: `IoTGlobalDeviceProvisioning` 62 | 6. IpStackApiKey: `` 63 | 7. S3BucketName: `` 64 | 8. Next 65 | 9. Next 66 | 10. Check `I acknowledge that AWS CloudFormation might create IAM resources with custom names.` 67 | 11. Create 68 | 12. It should take some minutes for the stack to be created. 69 | 70 | 11. The DynamoDB table `iot-global-provisioning` that was created by CloudFormation needs to be populated with device names that are allowed to be provisioned. The following command will create 10 devices named `mydevice1 ... mydevice10` in the DynamoDB table and put their provisioning state `prov_status` to `unprovisioned`: 71 | 72 | for i in 1 2 3 4 5 6 7 8 9 10; do aws dynamodb put-item --table-name iot-global-provisioning --item "{\"prov_status\": {\"S\": \"unprovisioned\"}, \"thing_name\":{\"S\": \"mydevice$i\"}}"; done 73 | 74 | 12. Scan the DynamoDB table to verify that the entries have been created: 75 | 76 | aws dynamodb scan --table-name iot-global-provisioning 77 | 78 | 13. Install python libraries required to run the example global-device 79 | 80 | cd ~/aws-iot-global-device-provisioning/provisioning/global-device 81 | sudo /usr/local/bin/pip install -r requirements.txt 82 | 83 | 14. Copy the private key you created earlier into the directory where the global device is located 84 | 85 | cp ../global-provisioning.priv.key.pem . 86 | 87 | 15. Get the CA certificates the could be used to sign the [AWS IoT server certificates](https://docs.aws.amazon.com/iot/latest/developerguide/managing-device-certs.html) 88 | 89 | ./create-root-ca-bundle.sh 90 | 91 | ## Provision a device 92 | In the previous section you have already configured a virtual device that can be used to demonstrate how the global device provisioning process works. This device is located in `~/aws-iot-global-device-provisioning/provisioning/global-device/global-device.py`. To start this example device you need to provide the API Gateway URL. The device will send provisioning requests to that URL. The API Gateway URL can be retrieved from the outputs section of the CloudFormation stack: 93 | 94 | * Go to the AWS CloudFormation console 95 | 96 | 1. Click `IoTGlobalDeviceProvisioning` 97 | 2. Expand `Outputs` 98 | 3. You will find the API Gateway URL as `IoTApiGWUrl` 99 | 100 | 101 | To demonstrate the provisioning process we will provision a device with the name `mydevice3`. The private key will be generated on the device and the corresponding CSR will be used in the provisioning process. After every major step the virtual device will wait for you to hit to better comprehend the provisioning process: 102 | 103 | Let's get started: 104 | 105 | ./global-device.py -t mydevice3 -a https://abcdefg.execute-api.AWS_REGION.amazonaws.com/test/device-provisioning -k 106 | 107 | => provisioning device with AWS IoT Core... 108 | thing-name: mydevice3 109 | use_own_priv_key: True 110 | == press to continue, to abort! 111 | 112 | => creating own private key... 113 | == press to continue, to abort! 114 | 115 | -----BEGIN PRIVATE KEY----- 116 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDUSOctOSUtmlVn 117 | iEorU3vKJtfMYECBdHZdfs72P0797Znw6jnUukP6sRsyiguJmTHDbp4nfvdADMmu 118 | 6mTcRTWFVKOVaJtcG5ay0yb5GT9O0u40DdKiYwkXnES/2t08lhD3cEbkxzsedav9 119 | vEyu9eJjh72aLvfSr2RBzGmo4FoSbVpAosN0axgwWWU5lAuXXW1Fex6wpFhxyPfC 120 | gEF1zkZzMvz9f6Y8RGDzmwpmF32pTq2b7i5Pu1OGC0GrCm9rH87gMjE2my9mjxJa 121 | bKFxwYxjZ9niQz5Y6aq0Ehes487VfFhmbnhQ48huiv0PdDRXc23u3/f92G4G6uSY 122 | LBapo8q5AgMBAAECggEBAKNTK7mZe8coNIkhTJ8k7drMI7+0VizDY8XvKGBAuQ+Y 123 | 3JWEP9YxMNgRpvEtUE8fNDA+TSPqBWSb8hfHcq4d+V2JjwoGn3EwMLOIzTVdfV2x 124 | 317hO6uAMqCdtC8/vnM8qfUVxxWBSzTWJ+tiEkWSHAmjh/a2KClKlAIjuS8a3XHK 125 | lZ0GByZ+I2ocZUqxSL3tuxubhBX1pDZehFcllygpVmzxE5QQeCtzWxMorgff+797 126 | JS799F82rBMgLg0qFtWLIOCbryw55V+ohAE0PzKW5Q+eHdRIqfn+X/bMnbZ0tA52 127 | aXWJdt3nSALMW9zzipkQ6wUp6EAVFO2HUeeiaMXyFOECgYEA7XOlcg1AKzliEZaV 128 | lStW+MMK6nm6BMbemKKK1NGGQYRADf2J70yCI+hS/Jy9IiqYPaB8XLtHWfihcuh3 129 | r3l3HgnAS/Vv0jvDqvSJTgxEkY6Qo52ojKIzHZqj68vBvpvLDyM3AI+x9K8OyVU/ 130 | jUb3K4tonmQOAG+Wo/wckrALRF0CgYEA5N39WPm31lmOHv3FPvktfj2elIfbTC8R 131 | 60F/qdfKpCQmXMrfdyCac4TuYqdBN59f3mEIVsxgqjphXQufXx/m3QfP74LH8fsM 132 | sp9Zkg3qxRUFH1PQGr8Wrp90OIQeGcEbnLssw1tLpmNqDY+UuYWWUzGIZuxZO6Yx 133 | mUB1BAyheg0CgYAV2QQqAEodMARz9dUBiqFP9jI07MpO0jV8+ceoWTbvJEn4f5GH 134 | cRSwVRn9oDZOxHiJgxCuP5ULFDNWrUkF3jk1jFQjKQwG3fTc7+8KPVq5wdJRG5p8 135 | hhgJ60aV1YOYFCGU3PqclJwdFVZY8/0K9LKdURBpMm+PXrUPlYzTelsvCQKBgQDh 136 | RSvId0ufHVkJYinS+TrxJj+/3RVaoH4XvMmm6HNaKwbjkQBx5lKAYBiwXAaSdDnN 137 | zl6B6PtAsuQAzJ7a57C6YKUoD+c0ZDI0Yyqr2yz5Pd5j3oBYwzvVN7gSpOBn4y6F 138 | j6rYwzTsGrBZlrkB/t5fFsM2425VixkIymwjRzdtxQKBgEDBknh7zEGgo69qIbWV 139 | YE1rVbVPKJVJsa7+Wx1lfjtxdbSM9Mgi/iuLhKrVhgw9bgAy7byI7/PQZ0f1KK6B 140 | j9Gifv0FysqFWq8f64ThP1ithLB1KcAWz7dtvv0q8sbBHpXx5Wr0UahuazEagZax 141 | gOXLODmPkMQa++2PY4O//Kw0 142 | -----END PRIVATE KEY----- 143 | 144 | -----BEGIN CERTIFICATE REQUEST----- 145 | MIICWTCCAUECAQAwFDESMBAGA1UEAwwJbXlkZXZpY2UzMIIBIjANBgkqhkiG9w0B 146 | AQEFAAOCAQ8AMIIBCgKCAQEA1EjnLTklLZpVZ4hKK1N7yibXzGBAgXR2XX7O9j9O 147 | /e2Z8Oo51LpD+rEbMooLiZkxw26eJ373QAzJrupk3EU1hVSjlWibXBuWstMm+Rk/ 148 | TtLuNA3SomMJF5xEv9rdPJYQ93BG5Mc7HnWr/bxMrvXiY4e9mi730q9kQcxpqOBa 149 | Em1aQKLDdGsYMFllOZQLl11tRXsesKRYccj3woBBdc5GczL8/X+mPERg85sKZhd9 150 | qU6tm+4uT7tThgtBqwpvax/O4DIxNpsvZo8SWmyhccGMY2fZ4kM+WOmqtBIXrOPO 151 | 1XxYZm54UOPIbor9D3Q0V3Nt7t/3/dhuBurkmCwWqaPKuQIDAQABoAAwDQYJKoZI 152 | hvcNAQELBQADggEBAIcA7O+Nw97yJlEmtN6bGoLHkRfsRrL+NNgHv4hHX+eS1EXJ 153 | O0H8W5CrblYXutmuxPa1+ugNMCB81y6on8T2RZQyiHagnBkLt5hBT+g240/QDZZU 154 | avrwM8Yo0HtEb8LyoP0mYV4O6jTFRgAAJXPJclNQOtgB+XG5XIf3SvGvL50kDMoY 155 | zBmszduL9SIKxALNTVOO+/WMMHl/CI3/bUbgmHr9DXynN8McrcNRn9EfkdKPNG4d 156 | eBf1TyU3c/k/89iWmECoycKschzBPE1c9hp526R/5RMbxx2JzGuFuZO9laYeUFbX 157 | KnCSFX+mJqdIcBj7GJI+dh0kh2RS9Dbbe3460n0= 158 | -----END CERTIFICATE REQUEST----- 159 | 160 | => request payload that will be send to the API Gateway... 161 | api gateway url: https://123v76ul4c.execute-api.eu-west-2.amazonaws.com/test/device-provisioning 162 | payload: {'thing-name': 'mydevice3', 'thing-name-sig': 'tBQisU1wlLyuolvPSqPT11GswHNUMMlHrAg3FpZq9ZGrI5/c5HxffwWRT1/R0PcTcUg/ewMwB8HX705GsNdz1udbjrtApEoFRaQrDYosej+1fL3dZQeqRzZLqoT7sIRX+AyM8/yaTNom5tmFNwmEdbY8xgR1NePIeURSlVmj5U4RGkQkQDBa/lcy4lSQFAmSd5NPcYLpcDwVr/TxXmGqJxOA/e/r5XobbEve4P2EuVFw7pIbNipljSRSip98yF4F9LzaTw2m/+eemtgsrPX++5/kr7+mST4QHDVK9yO6e9sUMpwyWB0lJ6I/XqwPfPu1acALrttb3gPHBw7gDu6pUA==', 'CSR': '-----BEGIN CERTIFICATE REQUEST-----\nMIICWTCCAUECAQAwFDESMBAGA1UEAwwJbXlkZXZpY2UzMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEA1EjnLTklLZpVZ4hKK1N7yibXzGBAgXR2XX7O9j9O\n/e2Z8Oo51LpD+rEbMooLiZkxw26eJ373QAzJrupk3EU1hVSjlWibXBuWstMm+Rk/\nTtLuNA3SomMJF5xEv9rdPJYQ93BG5Mc7HnWr/bxMrvXiY4e9mi730q9kQcxpqOBa\nEm1aQKLDdGsYMFllOZQLl11tRXsesKRYccj3woBBdc5GczL8/X+mPERg85sKZhd9\nqU6tm+4uT7tThgtBqwpvax/O4DIxNpsvZo8SWmyhccGMY2fZ4kM+WOmqtBIXrOPO\n1XxYZm54UOPIbor9D3Q0V3Nt7t/3/dhuBurkmCwWqaPKuQIDAQABoAAwDQYJKoZI\nhvcNAQELBQADggEBAIcA7O+Nw97yJlEmtN6bGoLHkRfsRrL+NNgHv4hHX+eS1EXJ\nO0H8W5CrblYXutmuxPa1+ugNMCB81y6on8T2RZQyiHagnBkLt5hBT+g240/QDZZU\navrwM8Yo0HtEb8LyoP0mYV4O6jTFRgAAJXPJclNQOtgB+XG5XIf3SvGvL50kDMoY\nzBmszduL9SIKxALNTVOO+/WMMHl/CI3/bUbgmHr9DXynN8McrcNRn9EfkdKPNG4d\neBf1TyU3c/k/89iWmECoycKschzBPE1c9hp526R/5RMbxx2JzGuFuZO9laYeUFbX\nKnCSFX+mJqdIcBj7GJI+dh0kh2RS9Dbbe3460n0=\n-----END CERTIFICATE REQUEST-----\n'} 163 | == press to continue, to abort! 164 | 165 | => sending request to API Gateway... 166 | <= headers: CaseInsensitiveDict({'x-amzn-requestid': '2548e86a-3e54-11e8-bf43-c750bb033a59', 'content-length': '1389', 'via': '1.1 008ae64ab7020a9aecc4c202669805d4.cloudfront.net (CloudFront)', 'x-cache': 'Miss from cloud front', 'x-amz-apigw-id': 'FOt0OGbzLPEFtQw=', 'x-amzn-trace-id': 'sampled=0;root=1-5acf5ce7-f0f1ad4f69bfe6aab1fe28de', 'connection': 'keep-alive', 'x-amz-cf-id': 'uOiG77A6Egn9R9voBea5pm40h2YquuVPEcThgegfJDS4oAUHumqg6A==', 'date': 'Thu, 12 Apr 2018 13:19:38 GMT', 'content-type': 'application/json'}) 167 | <= text: {"status": "success", "distance": 472.3831871963406, "endpointAddress": "anb9aqgu7xd6q.iot.us-east-2.amazonaws.com", "certificatePem": "-----BEGIN CERTIFICATE-----\nMIIDUDCCAjigAwIBAgIVAJSUm9DDV90x8oJQtManP3iE0KrzMA0GCSqGSIb3DQEB\nCwUAME0xSzBJBgNVBAsMQkFtYXpvbiBXZWIgU2VydmljZXMgTz1BbWF6b24uY29t\nIEluYy4gTD1TZWF0dGxlIFNUPVdhc2hpbmd0b24gQz1VUzAeFw0xODA0MTIxMzE3\nMzdaFw00OTEyMzEyMzU5NTlaMBQxEjAQBgNVBAMMCW15ZGV2aWNlMzCCASIwDQYJ\nKoZIhvcNAQEBBQADggEPADCCAQoCggEBANRI5y05JS2aVWeISitTe8om18xgQIF0\ndl1+zvY/Tv3tmfDqOdS6Q/qxGzKKC4mZMcNunid+90AMya7qZNxFNYVUo5Vom1wb\nlrLTJvkZP07S7jQN0qJjCRecRL/a3TyWEPdwRuTHOx51q/28TK714mOHvZou99Kv\nZEHMaajgWhJtWkCiw3RrGDBZZTmUC5ddbUV7HrCkWHHI98KAQXXORnMy/P1/pjxE\nYPObCmYXfalOrZvuLk+7U4YLQasKb2sfzuAyMTabL2aPElpsoXHBjGNn2eJDPljp\nqrQSF6zjztV8WGZueFDjyG6K/Q90NFdzbe7f9/3Ybgbq5JgsFqmjyrkCAwEAAaNg\nMF4wHwYDVR0jBBgwFoAU3w0vC3+7YSqpHx0zBwPHp0n/G0IwHQYDVR0OBBYEFCcZ\nmGaIQDStdWmF9wILb4BlyXpQMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeA\nMA0GCSqGSIb3DQEBCwUAA4IBAQCbRnd2O/YhKJfRdZdKC9H4tZGTXf9XEdQR9hyv\nl0giigGigh3cBOQleuAIFjUyjta/LgHDO5PFC8F8H9QClBlV4Ja8YxKu37Jd0h8A\nfj0AmRvpoqA3lRUpbi/O+lNnGCjkBEWE1g/QpbnTmGVjb6IiE9aNMYLTk7SO1u/I\n8XIPwfKgl5jDZ+xjvnOmckBrI8GX0AjiYPw9tMsqWZt7WA37So5UYHRnYuoP1xZ7\n+jQ8feReDowpUDcXHIHn58oZTo9XUwuARltpYFiyRVrTzSPPtWDp1WfQzwDbxYgb\nY9nEnXkio0vb8UwrEdczMNBHAvqiIHvaE21Ey4chOi+qLujh\n-----END CERTIFICATE-----\n", "region": "us-east-2"} 168 | == press to continue, to abort! 169 | 170 | => writing cert/key to file... 171 | => device is ready to communicate with AWS IoT... 172 | == press to continue, to abort! 173 | 174 | <= device connected to AWS IoT in region: us-east-2 175 | before you continue subscribe in the AWS IoT console to the topic: data/mydevice3/misc 176 | == press to continue, to abort! 177 | 178 | => start publishing... press to abort 179 | => publishing message: {'thing-name': 'mydevice3', 'global': 'device provisioning', 'date time': '2018-04-12T13:20:32'} 180 | => publishing message: {'thing-name': 'mydevice3', 'global': 'device provisioning', 'date time': '2018-04-12T13:20:34'} 181 | 182 | 183 | 184 | Lookup the provisioned device in the Dynamo Db table 185 | 186 | As part of the setup of the global device provisioning solution a DynamoDB table named `iot-global-provisioning` has been created and you have populated it with information about the devices which should be provisioned. By getting the entry for `mydevice3` from the DynamoDB table you should find the time when the device was provisioned as well as the region in which the device has been created and the provisioning status (prov_status) is now set to `provisioned`. 187 | 188 | Get item: 189 | 190 | aws dynamodb get-item --table-name iot-global-provisioning --key '{"thing_name": {"S": "mydevice3"}}' 191 | 192 | The output should look similar to: 193 | 194 | { 195 | "Item": { 196 | "aws_region": { 197 | "S": "us-east-2" 198 | }, 199 | "prov_datetime": { 200 | "S": "2018-05-15T10:47:12" 201 | }, 202 | "thing_name": { 203 | "S": "mydevice2" 204 | }, 205 | "prov_status": { 206 | "S": "provisioned" 207 | } 208 | } 209 | } 210 | 211 | If you start the device again with the command `./global-device.py -t mydevice3 -a https://abcdefg.execute-api.AWS_REGION.amazonaws.com/test/device-provisioning -k` it will notice that it has already been provisioned because of the existing device key and device certificate and will immediately start publishing messages towards AWS IoT Core. 212 | 213 | 214 | ### Modes to run the global device 215 | The global device can be run in three modes: 216 | 217 | 1. Send the thing name only in the provisioning request and receive private key, certificate and iot endpoint 218 | 219 | ./global-device.py -t mydevice1 -a 220 | 221 | 2. Create a private key and send thing name and CSR in the provisioning request 222 | 223 | ./global-device.py -t mydevice2 -a -k 224 | 225 | 3. Send a provisioning request with a wrong signature to demonstrate the failure of the signature verification process 226 | 227 | ./global-device.py -t mydevice3 -a -f 228 | 229 | 230 | ### Outlook/Improvements 231 | 232 | #### Best Region 233 | 234 | As this is an example implementation one can also think at various other scenarios how the best region for a device could be determined. You could define one AWS region per continent or a specific regions for particular countries e.g. if legal requirements exists. 235 | 236 | #### Unique Key Pair per Device 237 | 238 | In the sample implementation all devices share one private key to sign data in the provisioning request. The related public key is include in the Lambda installation package. An approach to use a unique key pair per device could be to deploy a unique private key on each device and to store the related public key in the DynamoDB table for device provisioning. 239 | 240 | #### Securing [ipstack.com](http://ipstack.com/) Api Access Key in Environment Variable 241 | 242 | If you want to secure sensitive information in environment variables like the [ipstack.com](http://ipstack.com/) API Access Key you can [encrypt your environment variables for lambda](https://docs.aws.amazon.com/lambda/latest/dg/env_variables.html#env_encrypt). 243 | 244 | ## License Summary 245 | 246 | This sample code is made available under a modified MIT license. See the LICENSE file. -------------------------------------------------------------------------------- /provisioning/cfn/cfn-iot-global-device-provisioning.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "AWSTemplateFormatVersion" : "2010-09-09", 4 | 5 | "Description" : "Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: MIT-0. AWS CloudFormation template for Global IoT Device Provisioning.", 6 | 7 | "Parameters" : { 8 | "S3BucketName": { 9 | "Description" : "S3 Bucket in the same region where you have put the zip for the Lambda.", 10 | "Type": "String", 11 | "ConstraintDescription" : "Must be an existing S3 Bucket in the same region.", 12 | "AllowedPattern" : ".+" 13 | }, 14 | "IpStackApiKey": { 15 | "Description" : "API Key to access the api from http://ipstack.com. If you don't have an API Key sign up at http://ipstack.com", 16 | "Type": "String", 17 | "ConstraintDescription" : "Must be an existing API Key for ipstack.com", 18 | "AllowedPattern" : ".+" 19 | } 20 | }, 21 | 22 | "Resources": { 23 | 24 | "LambdaGlobalIoTProvisioningRole": { 25 | "Type": "AWS::IAM::Role", 26 | "Properties": { 27 | "AssumeRolePolicyDocument": { 28 | "Statement": [ { 29 | "Effect": "Allow", 30 | "Principal": { 31 | "Service": [ "lambda.amazonaws.com" ] 32 | }, 33 | "Action": [ "sts:AssumeRole" ] 34 | } ] 35 | }, 36 | "ManagedPolicyArns": [ 37 | "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 38 | "arn:aws:iam::aws:policy/service-role/AWSIoTThingsRegistration" 39 | ], 40 | "Policies": [ { 41 | "PolicyName": {"Fn::Join": ["-", ["LambdaGlobalIoTProvisioningPolicy", {"Ref": "AWS::Region"} ]]}, 42 | "PolicyDocument": { 43 | "Version":"2012-10-17", 44 | "Statement":[ 45 | { 46 | "Effect":"Allow", 47 | "Action":[ 48 | "iot:DescribeEndpoint", 49 | "iot:CreateKeysAndCertificate", 50 | "iot:AttachPolicy", 51 | "dynamodb:GetItem", 52 | "dynamodb:UpdateItem" 53 | ], 54 | "Resource":"*" 55 | } 56 | ] 57 | } 58 | } 59 | ], 60 | "Path": "/", 61 | "RoleName": {"Fn::Join": ["-", ["LambdaGlobalIoTProvisioningRole", {"Ref": "AWS::Region"} ]]} 62 | } 63 | }, 64 | 65 | "GlobalProvisioningLambda": { 66 | "Type": "AWS::Lambda::Function", 67 | "Properties": { 68 | "Handler": "lambda_function.lambda_handler", 69 | "Role": { "Fn::GetAtt" : ["LambdaGlobalIoTProvisioningRole", "Arn"] }, 70 | "Code": { 71 | "S3Bucket": { "Ref": "S3BucketName"}, 72 | "S3Key": "iot-global-provisioning.zip" 73 | }, 74 | "Environment" : { 75 | "Variables": { "IPSTACK_API_KEY":{ "Ref": "IpStackApiKey"} } 76 | }, 77 | "Runtime": "python2.7", 78 | "MemorySize" : 128, 79 | "Timeout": "60", 80 | } 81 | }, 82 | 83 | "LambdaPermission": { 84 | "Type": "AWS::Lambda::Permission", 85 | "Properties": { 86 | "Action": "lambda:invokeFunction", 87 | "FunctionName": {"Fn::GetAtt": ["GlobalProvisioningLambda", "Arn"]}, 88 | "Principal": "apigateway.amazonaws.com", 89 | "SourceArn": {"Fn::Join": ["", 90 | ["arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", {"Ref": "IoTApi"}, "/*"] 91 | ]} 92 | } 93 | }, 94 | 95 | "IoTApi": { 96 | "Type": "AWS::ApiGateway::RestApi", 97 | "Properties": { 98 | "Body": { 99 | "swagger" : "2.0", 100 | "info" : { 101 | "version" : "2018-03-16T15:33:58Z", 102 | "title" : "IoT API" 103 | }, 104 | "basePath" : "/test", 105 | "schemes" : [ "https" ], 106 | "paths" : { 107 | "/device-provisioning" : { 108 | "post" : { 109 | "consumes" : [ "application/json" ], 110 | "produces" : [ "application/json" ], 111 | "responses" : { 112 | "200" : { 113 | "description" : "200 response", 114 | "schema" : { 115 | "$ref" : "#/definitions/Empty" 116 | } 117 | } 118 | }, 119 | "x-amazon-apigateway-integration" : { 120 | "uri": {"Fn::Join": ["", 121 | ["arn:aws:apigateway:", {"Ref": "AWS::Region"}, ":lambda:path/2015-03-31/functions/", {"Fn::GetAtt": ["GlobalProvisioningLambda", "Arn"]}, "/invocations"] 122 | ]}, 123 | "responses" : { 124 | "default" : { 125 | "statusCode" : "200" 126 | } 127 | }, 128 | "passthroughBehavior" : "when_no_templates", 129 | "httpMethod" : "POST", 130 | "requestTemplates" : { 131 | "application/json" : "## See http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html\n## This template will pass through all parameters including path, querystring, header, stage variables, and context through to the integration endpoint via the body/payload\n#set($allParams = $input.params())\n{\n\"body-json\" : $input.json('$'),\n\"params\" : {\n#foreach($type in $allParams.keySet())\n #set($params = $allParams.get($type))\n\"$type\" : {\n #foreach($paramName in $params.keySet())\n \"$paramName\" : \"$util.escapeJavaScript($params.get($paramName))\"\n #if($foreach.hasNext),#end\n #end\n}\n #if($foreach.hasNext),#end\n#end\n},\n\"stage-variables\" : {\n#foreach($key in $stageVariables.keySet())\n\"$key\" : \"$util.escapeJavaScript($stageVariables.get($key))\"\n #if($foreach.hasNext),#end\n#end\n},\n\"context\" : {\n \"account-id\" : \"$context.identity.accountId\",\n \"api-id\" : \"$context.apiId\",\n \"api-key\" : \"$context.identity.apiKey\",\n \"authorizer-principal-id\" : \"$context.authorizer.principalId\",\n \"caller\" : \"$context.identity.caller\",\n \"cognito-authentication-provider\" : \"$context.identity.cognitoAuthenticationProvider\",\n \"cognito-authentication-type\" : \"$context.identity.cognitoAuthenticationType\",\n \"cognito-identity-id\" : \"$context.identity.cognitoIdentityId\",\n \"cognito-identity-pool-id\" : \"$context.identity.cognitoIdentityPoolId\",\n \"http-method\" : \"$context.httpMethod\",\n \"stage\" : \"$context.stage\",\n \"source-ip\" : \"$context.identity.sourceIp\",\n \"user\" : \"$context.identity.user\",\n \"user-agent\" : \"$context.identity.userAgent\",\n \"user-arn\" : \"$context.identity.userArn\",\n \"request-id\" : \"$context.requestId\",\n \"resource-id\" : \"$context.resourceId\",\n \"resource-path\" : \"$context.resourcePath\"\n }\n}\n" 132 | }, 133 | "contentHandling" : "CONVERT_TO_TEXT", 134 | "type" : "aws" 135 | } 136 | } 137 | } 138 | }, 139 | "definitions" : { 140 | "Empty" : { 141 | "type" : "object", 142 | "title" : "Empty Schema" 143 | } 144 | } 145 | } 146 | } 147 | }, 148 | 149 | "IoTApiCWRole": { 150 | "Type": "AWS::IAM::Role", 151 | "Properties": { 152 | "AssumeRolePolicyDocument": { 153 | "Version": "2012-10-17", 154 | "Statement": [{ 155 | "Effect": "Allow", 156 | "Principal": { "Service": [ "apigateway.amazonaws.com" ] }, 157 | "Action": "sts:AssumeRole" 158 | }] 159 | }, 160 | "Path": "/", 161 | "ManagedPolicyArns": ["arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"] 162 | } 163 | }, 164 | 165 | "IoTApiAccount": { 166 | "Type": "AWS::ApiGateway::Account", 167 | "Properties": { 168 | "CloudWatchRoleArn": {"Fn::GetAtt": ["IoTApiCWRole", "Arn"] } 169 | } 170 | }, 171 | 172 | "IoTApiStage": { 173 | "DependsOn": ["IoTApiAccount"], 174 | "Type": "AWS::ApiGateway::Stage", 175 | "Properties": { 176 | "DeploymentId": {"Ref": "IoTApiDeployment"}, 177 | "MethodSettings": [{ 178 | "DataTraceEnabled": true, 179 | "HttpMethod": "*", 180 | "LoggingLevel": "INFO", 181 | "ResourcePath": "/*" 182 | }], 183 | "RestApiId": {"Ref": "IoTApi"}, 184 | "StageName": "test" 185 | } 186 | }, 187 | 188 | "IoTApiDeployment": { 189 | "Type": "AWS::ApiGateway::Deployment", 190 | "Properties": { 191 | "RestApiId": {"Ref": "IoTApi"} 192 | } 193 | }, 194 | 195 | "IoTProvisioningTable": { 196 | "Type" : "AWS::DynamoDB::Table", 197 | "Properties" : { 198 | "AttributeDefinitions" : [ { 199 | "AttributeName" : "thing_name", 200 | "AttributeType" : "S" 201 | } 202 | ], 203 | "KeySchema" : [ { 204 | "AttributeName" : "thing_name", 205 | "KeyType" : "HASH" 206 | } 207 | ], 208 | "ProvisionedThroughput" : { 209 | "ReadCapacityUnits" : "5", 210 | "WriteCapacityUnits" : "5" 211 | }, 212 | "TableName" : "iot-global-provisioning", 213 | } 214 | } 215 | 216 | }, 217 | 218 | "Outputs": { 219 | "IoTApiGWUrl": { 220 | "Description": "Amazon API Gateway URL for global iot device provisioning.", 221 | "Value": {"Fn::Join": ["", 222 | ["https://", {"Ref": "IoTApi"}, ".execute-api.", {"Ref": "AWS::Region"}, ".amazonaws.com/test/device-provisioning"] 223 | ]} 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /provisioning/global-device/create-root-ca-bundle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | # software and associated documentation files (the "Software"), to deal in the Software 7 | # without restriction, including without limitation the rights to use, copy, modify, 8 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | # permit persons to whom the Software is furnished to do so. 10 | # 11 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | # create-root-ca-bundle.sh 19 | # creates a file containing all the CAs that could be 20 | # used to sign an AWS IoT server certificate 21 | # See also: https://docs.aws.amazon.com/iot/latest/developerguide/managing-device-certs.html 22 | 23 | ROOT_CA_FILE=root.ca.bundle.pem 24 | 25 | cp /dev/null $ROOT_CA_FILE 26 | 27 | for url in 'https://www.amazontrust.com/repository/AmazonRootCA1.pem' \ 28 | 'https://www.amazontrust.com/repository/AmazonRootCA2.pem' \ 29 | 'https://www.amazontrust.com/repository/AmazonRootCA3.pem' \ 30 | 'https://www.amazontrust.com/repository/AmazonRootCA4.pem' \ 31 | 'https://www.symantec.com/content/en/us/enterprise/verisign/roots/VeriSign-Class%203-Public-Primary-Certification-Authority-G5.pem' 32 | do 33 | echo $url 34 | wget -O - $url >> $ROOT_CA_FILE 35 | done 36 | -------------------------------------------------------------------------------- /provisioning/global-device/global-device.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # coding: utf-8 4 | 5 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 8 | # software and associated documentation files (the "Software"), to deal in the Software 9 | # without restriction, including without limitation the rights to use, copy, modify, 10 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 11 | # permit persons to whom the Software is furnished to do so. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 14 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 15 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 16 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 17 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 18 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | 21 | # Global IoT Device Provisioning 22 | # 23 | # An example IoT Device for a global device provisioning approach. The device sends it's thing name to an API gateway. If the device has the permssion to be provisioned it get's as a result the region, iot endpoint, private key and certificate. 24 | # 25 | # With this information the device is able to connect to the endpoint where it has been provisioned and may publish data. 26 | 27 | import argparse 28 | import base64 29 | import json 30 | import os 31 | import requests 32 | import sys 33 | import time 34 | import uuid 35 | from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTClient 36 | from OpenSSL import crypto 37 | from OpenSSL.crypto import X509 38 | from time import gmtime, strftime 39 | 40 | # globals 41 | 42 | # signing key file 43 | priv_key_file = 'global-provisioning.priv.key.pem' 44 | #pub_key_file = 'global-provisioning.pub.key.pem' 45 | #rootCAPath = "root.ca.pem" 46 | rootCAPath = 'root.ca.bundle.pem' 47 | 48 | 49 | # 50 | # parse command line args 51 | # 52 | parser = argparse.ArgumentParser(description='Sample device for global provisiong with AWS IoT Core') 53 | parser.add_argument("-t", "--thing-name", action="store", required=True, dest="thing_name", help="thing name for your device") 54 | parser.add_argument("-a", "--api-gw", action="store", required=True, dest="api_gw", help="API Gateway URL for device provisioning") 55 | parser.add_argument("-k", "--own-key", action="store_true", dest="use_own_priv_key", default=False, 56 | help="Use own private key for the device") 57 | parser.add_argument("-c", "--continue", action="store_true", dest="continue_provisioning", default=False, 58 | help="continue the provisioning process without interaction") 59 | parser.add_argument("-f", "--fake-device", action="store_true", dest="fake_device", default=False, 60 | help="use a fake device name to demonstrate that verifying the sig fails") 61 | 62 | args = parser.parse_args() 63 | thing_name = args.thing_name 64 | api_gw = args.api_gw 65 | use_own_priv_key = args.use_own_priv_key 66 | continue_provisioning = args.continue_provisioning 67 | fake_device = args.fake_device 68 | 69 | endpoint = None 70 | 71 | def cont(): 72 | if continue_provisioning: 73 | return 74 | raw_input("== press to continue, to abort!\n") 75 | 76 | # key/cert/csr file name for the thing 77 | key_file = thing_name + '.device.key.pem' 78 | csr_file = thing_name + '.device.csr.pem' 79 | cert_file = thing_name + '.device.cert.pem' 80 | endpoint_file = thing_name + '.endpoint' 81 | 82 | 83 | if os.path.isfile(key_file) and os.path.isfile(cert_file): 84 | print("=> device already provisioned") 85 | f = open(endpoint_file, 'r') 86 | line = f.readline().rstrip('\n') 87 | f.close() 88 | endpoint_region = line.split('::') 89 | endpoint = endpoint_region[0] 90 | region = endpoint_region[1] 91 | print(" endpoint: {}, region: {}".format(endpoint, region)) 92 | cont() 93 | else: 94 | print("=> provisioning device with AWS IoT Core...") 95 | print(" thing-name: {}".format(thing_name)) 96 | print(" use_own_priv_key: {}".format(use_own_priv_key)) 97 | cont() 98 | 99 | # ### Create Signature for Thing Name 100 | # For a valid provisioning request the device must send it's name as well as the sig for the thing name. The signature is created with a private key. 101 | f = open(priv_key_file, 'r') 102 | priv_key_pem = f.read() 103 | f.close() 104 | priv_key = crypto.load_privatekey(crypto.FILETYPE_PEM, priv_key_pem) 105 | sig = crypto.sign(priv_key, thing_name, 'sha256') 106 | sig = base64.b64encode(sig) 107 | 108 | 109 | # ### Create a Provisioning Request with own private key and CSR 110 | # If you want to create the private on your own you can send a CSR together with the provisioning request and let AWS IoT issue the certificate. 111 | 112 | if use_own_priv_key: 113 | print("=> creating own private key...") 114 | cont() 115 | device_priv_key = crypto.PKey() 116 | device_priv_key.generate_key(crypto.TYPE_RSA, 2048) 117 | print(crypto.dump_privatekey(crypto.FILETYPE_PEM, device_priv_key)) 118 | 119 | file_k = open(key_file,"w") 120 | file_k.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, device_priv_key)) 121 | file_k.close() 122 | 123 | csr = crypto.X509Req() 124 | subj = csr.get_subject() 125 | setattr(subj, "CN", thing_name) 126 | csr.set_pubkey(device_priv_key) 127 | csr.sign(device_priv_key, 'sha256') 128 | print(crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)) 129 | 130 | file_cs= open(csr_file,"w") 131 | file_cs.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)) 132 | file_cs.close() 133 | else: 134 | print("=> using private key from AWS IoT") 135 | 136 | 137 | if fake_device: 138 | print("=> faking device name") 139 | thing_name = str(uuid.uuid4()) 140 | 141 | payload = {'thing-name': thing_name, 'thing-name-sig': sig} 142 | if use_own_priv_key: 143 | payload = {'thing-name': thing_name, 'thing-name-sig': sig, 'CSR': crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)} 144 | 145 | print("=> request payload that will be send to the API Gateway...") 146 | print(" api gateway url: {}".format(api_gw)) 147 | print(" payload: {}".format(payload)) 148 | cont() 149 | 150 | # ### Send Provisioning Request 151 | # Send the provisioning request to an API Gateway. The API Gateway will call a Lambda which provisions the device. 152 | print("=> sending request to API Gateway...") 153 | r = requests.post(api_gw, data=json.dumps(payload)) 154 | print("<= headers: {}".format(r.headers)) 155 | print("<= text: {}".format(r.text)) 156 | status = r.json()["status"] 157 | if status == "error": 158 | print("<= error: device not provisioned") 159 | sys.exit() 160 | cont() 161 | 162 | 163 | # ### Return Values 164 | # Testing some return values from the provisioning request. 165 | 166 | region = r.json()["region"] 167 | endpoint = r.json()["endpointAddress"] 168 | print("<= region: {}".format(region)) 169 | print("<= endpoint: {}".format(endpoint)) 170 | print("<= certificatePem:\n{}".format(r.json()["certificatePem"])) 171 | 172 | if not use_own_priv_key: 173 | print("<= PrivateKey\n{}".format(r.json()["PrivateKey"])) 174 | 175 | 176 | # ### Store Key and Certificate 177 | # Write key and certificate to file. 178 | 179 | print("=> writing cert/key to file...") 180 | file_c = open(cert_file,"w") 181 | file_c.write(r.json()["certificatePem"]) 182 | file_c.close() 183 | 184 | if not use_own_priv_key: 185 | file_k = open(key_file,"w") 186 | file_k.write(r.json()["PrivateKey"]) 187 | file_k.close() 188 | 189 | file_e = open(endpoint_file,"w") 190 | file_e.write(endpoint + '::' + region) 191 | file_e.close() 192 | 193 | 194 | print("=> device is ready to communicate with AWS IoT...") 195 | cont() 196 | 197 | host = endpoint 198 | certificatePath = cert_file 199 | privateKeyPath = key_file 200 | clientId = thing_name 201 | topic = "data/" + thing_name + "/misc" 202 | 203 | 204 | myAWSIoTMQTTClient = None 205 | myAWSIoTMQTTClient = AWSIoTMQTTClient(clientId) 206 | myAWSIoTMQTTClient.configureEndpoint(host, 8883) 207 | myAWSIoTMQTTClient.configureCredentials(rootCAPath, privateKeyPath, certificatePath) 208 | 209 | # AWSIoTMQTTClient connection configuration 210 | myAWSIoTMQTTClient.configureAutoReconnectBackoffTime(1, 32, 20) 211 | myAWSIoTMQTTClient.configureOfflinePublishQueueing(-1) # Infinite offline Publish queueing 212 | myAWSIoTMQTTClient.configureDrainingFrequency(2) # Draining: 2 Hz 213 | myAWSIoTMQTTClient.configureConnectDisconnectTimeout(10) # 10 sec 214 | myAWSIoTMQTTClient.configureMQTTOperationTimeout(5) # 5 sec 215 | 216 | # Connect and subscribe to AWS IoT 217 | if myAWSIoTMQTTClient.connect(): 218 | print("<= device connected to AWS IoT in region: {}".format(region)) 219 | print(" before you continue subscribe in the AWS IoT console to the topic: {}".format(topic)) 220 | cont() 221 | else: 222 | print("error: device could not connect to AWS IoT in region: {}".format(region)) 223 | print(" check logs, thing, certificate, key, policy") 224 | sys.exit() 225 | 226 | 227 | 228 | # ### Publish a message 229 | # **Before you publish a message to the AWS IoT Console and subcribe to "data/#"**. The device publishes a message. If everthing went well during the registration process the message should be seen in the AWS IoT Console. 230 | print("=> start publishing... press to abort") 231 | 232 | while True: 233 | message = {"thing-name": thing_name, "global": "device provisioning", 234 | "datetime": time.strftime("%Y-%m-%dT%H:%M:%S", gmtime())} 235 | print ("=> publishing message: {}".format(message)) 236 | myAWSIoTMQTTClient.publish(topic, json.dumps(message, indent=4), 0) 237 | time.sleep(2) 238 | 239 | 240 | # This message should not arrive because the policy in this example allows the device only to publish to "data/${iot: 241 | # ClientId}/#' 242 | 243 | # In[ ]: 244 | 245 | 246 | #myAWSIoTMQTTClient.publish('other/topic', json.dumps(message, indent=4), 0) 247 | -------------------------------------------------------------------------------- /provisioning/global-device/requirements.txt: -------------------------------------------------------------------------------- 1 | AWSIoTPythonSDK 2 | pyOpenSSL 3 | -------------------------------------------------------------------------------- /provisioning/lambda/lambda_function.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | # software and associated documentation files (the "Software"), to deal in the Software 5 | # without restriction, including without limitation the rights to use, copy, modify, 6 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | 17 | import base64 18 | import boto3 19 | import json 20 | import logging 21 | import os 22 | import re 23 | import requests 24 | import string 25 | import sys 26 | import time 27 | from OpenSSL import crypto 28 | from OpenSSL.crypto import X509 29 | from time import gmtime, strftime 30 | from geopy.distance import great_circle 31 | 32 | # globals 33 | ipstack_api_url = 'http://api.ipstack.com/' 34 | ipstack_api_key = os.environ['IPSTACK_API_KEY'] 35 | 36 | iot_policy_name = 'GlobalDevicePolicy' 37 | dynamodb_table_name = 'iot-global-provisioning' 38 | pub_key_file = 'global-provisioning.pub.key.pem' 39 | 40 | # Configure logging 41 | logger = logging.getLogger() 42 | 43 | for h in logger.handlers: 44 | logger.removeHandler(h) 45 | h = logging.StreamHandler(sys.stdout) 46 | 47 | FORMAT = "[%(asctime)s - %(levelname)s - %(filename)s:%(lineno)s - %(funcName)s - %(message)s" 48 | h.setFormatter(logging.Formatter(FORMAT)) 49 | 50 | logger.addHandler(h) 51 | logger.setLevel(logging.INFO) 52 | 53 | class RequestIdAdapter(logging.LoggerAdapter): 54 | def process(self, msg, kwargs): 55 | return '%s]: %s' % (self.extra['request_id'], msg), kwargs 56 | 57 | regions = [ 58 | {"name": "ap-northeast-1", "lat": "35.9", "lon": "140.0"}, 59 | {"name": "eu-west-1", "lat": "53.5", "lon": "-6.1"}, 60 | {"name": "ap-southeast-2", "lat": "-33.7", "lon": "151.4"}, 61 | {"name": "us-east-2", "lat": "40.4", "lon": "-82.5"}, 62 | {"name": "eu-central-1", "lat": "50.3", "lon": "8.9"}, 63 | {"name": "us-east-1", "lat": "32.4", "lon": "-98.0"}, 64 | {"name": "ap-northeast-2", "lat": "37.8", "lon": "127.2"}, 65 | {"name": "ap-southeast-1", "lat": "1.5", "lon": "104.1"}, 66 | {"name": "ap-south-1", "lat": "19.1", "lon": "73.0"}, 67 | {"name": "us-west-2", "lat": "44.2", "lon": "-120.5"}, 68 | {"name": "eu-west-2", "lat": "51.7", "lon": "0.1"} 69 | ] 70 | 71 | default_region = "eu-west-2" 72 | 73 | def get_ip_location(ip): 74 | request_url = ipstack_api_url + '/' + ip + '?access_key=' + ipstack_api_key 75 | logger.debug("request_url: {}".format(request_url)) 76 | r = requests.get(request_url) 77 | j = json.loads(r.text) 78 | logger.debug("j: {}".format(j)) 79 | return j 80 | 81 | 82 | def find_best_region(lat, lon): 83 | min_distance = 40000 84 | closest_region = None 85 | 86 | for r in regions: 87 | logger.debug("r: {}".format(r)) 88 | elat = float(r["lat"]) 89 | elon = float(r["lon"]) 90 | logger.debug("elat: {}, elon: {}".format(elat, elon)) 91 | distance = great_circle((lat, lon), (elat, elon)).km 92 | logger.debug("distance: {}".format(distance)) 93 | if distance <= min_distance: 94 | min_distance = distance 95 | closest_region = r["name"] 96 | logger.debug("min_distance: {}".format(min_distance)) 97 | 98 | logger.info("closest_region: {}, distance: {}".format(closest_region, min_distance)) 99 | 100 | return {"region": closest_region, "distance": min_distance} 101 | 102 | 103 | def get_account_id(): 104 | client = boto3.client('sts') 105 | response = client.get_caller_identity() 106 | logger.info("response: {}".format(response)) 107 | return response['Account'] 108 | 109 | 110 | def create_iot_policy_if_missing(c_iot, region): 111 | try: 112 | response = c_iot.get_policy(policyName = iot_policy_name) 113 | logger.info("policy exists already: response: {}".format(response)) 114 | except Exception as e: 115 | if re.match('.*ResourceNotFoundException.*', str(e)): 116 | logger.info("creating iot policy {}".format(iot_policy_name)) 117 | account_id = get_account_id() 118 | arn_connect = 'arn:aws:iot:' + region + ':' + account_id + ':client/${iot:ClientId}' 119 | arn_publish = 'arn:aws:iot:' + region + ':' + account_id + ':topic/data/${iot:ClientId}/*' 120 | logger.info("arn_connect: {}".format(arn_connect)) 121 | logger.info("arn_publish: {}".format(arn_publish)) 122 | 123 | policy_document = '''{ 124 | "Version": "2012-10-17", 125 | "Statement": [{ 126 | "Effect": "Allow", 127 | "Action": ["iot:Connect"], 128 | "Resource": [ "''' + arn_connect + '''" ] 129 | }, 130 | { 131 | "Effect": "Allow", 132 | "Action": ["iot:Publish"], 133 | "Resource": [ "''' + arn_publish + '''" ] 134 | }] 135 | }''' 136 | 137 | response = c_iot.create_policy( 138 | policyName = iot_policy_name, 139 | policyDocument = policy_document 140 | ) 141 | logger.info("response: {}".format(response)) 142 | else: 143 | logger.error("unknown error: {}".format(e)) 144 | 145 | 146 | def provision_device(thing_name, region, CSR): 147 | answer = {} 148 | logger.info("thing_name: {}, region {}".format(thing_name, region)) 149 | c_iot = boto3.client('iot', region_name = region) 150 | 151 | # endpoint 152 | response = c_iot.describe_endpoint(endpointType='iot:Data-ATS') 153 | logger.info("response: {}".format(response)) 154 | answer['endpointAddress'] = response['endpointAddress'] 155 | 156 | # create policy if missing 157 | create_iot_policy_if_missing(c_iot, region) 158 | 159 | # create thing 160 | response = c_iot.create_thing(thingName = thing_name) 161 | logger.info("response: {}".format(response)) 162 | 163 | if CSR: 164 | logger.info("CSR received: create_certificate_from_csr") 165 | # create cert from csr 166 | response = c_iot.create_certificate_from_csr( 167 | certificateSigningRequest = CSR, 168 | setAsActive = True 169 | ) 170 | logger.debug("response: {}".format(response)) 171 | certificate_arn = response['certificateArn'] 172 | certificate_id = response['certificateId'] 173 | logger.info("certificate_arn: {}, certificate_id: {}".format(certificate_arn, certificate_id)) 174 | answer['certificatePem'] = response['certificatePem'] 175 | else: 176 | logger.info("no CSR received: create_keys_and_certificate") 177 | # create key/cert 178 | response = c_iot.create_keys_and_certificate(setAsActive = True) 179 | logger.debug("response: {}".format(response)) 180 | certificate_arn = response['certificateArn'] 181 | certificate_id = response['certificateId'] 182 | logger.info("certificate_arn: {}, certificate_id: {}".format(certificate_arn, certificate_id)) 183 | answer['certificatePem'] = response['certificatePem'] 184 | answer['PrivateKey'] = response['keyPair']['PrivateKey'] 185 | 186 | # attach policy to certificate 187 | response = c_iot.attach_policy( 188 | policyName = iot_policy_name, 189 | target = certificate_arn 190 | ) 191 | logger.info("response: {}".format(response)) 192 | 193 | response = c_iot.attach_thing_principal( 194 | thingName = thing_name, 195 | principal = certificate_arn 196 | ) 197 | logger.info("response: {}".format(response)) 198 | 199 | return answer 200 | 201 | 202 | def device_marked_for_provisioning(thing_name): 203 | c_dynamo = boto3.client('dynamodb') 204 | key = {"thing_name": {"S": thing_name}} 205 | logger.info("key {}".format(key)) 206 | 207 | response = c_dynamo.get_item(TableName = dynamodb_table_name, Key = key) 208 | logger.info("response: {}".format(response)) 209 | 210 | if 'Item' in response: 211 | if 'prov_status' in response['Item']: 212 | status = response['Item']['prov_status']['S'] 213 | logger.info("status: {}".format(status)) 214 | if status == "unprovisioned": 215 | return True 216 | else: 217 | logger.warn("no status in result") 218 | return False 219 | else: 220 | logger.error("thing {} not found in DynamoDB".format(thing_name)) 221 | 222 | return False 223 | 224 | 225 | def update_device_provisioning_status(thing_name, region): 226 | c_dynamo = boto3.client('dynamodb') 227 | datetime = time.strftime("%Y-%m-%dT%H:%M:%S", gmtime()) 228 | 229 | key = {"thing_name": {"S": thing_name}} 230 | logger.info("key {}".format(key)) 231 | update_expression = "SET prov_status = :s, prov_datetime = :d, aws_region = :r" 232 | expression_attribute_values = {":s": {"S": "provisioned"}, ":d": {"S": datetime}, ":r": {"S": region}} 233 | 234 | response = c_dynamo.update_item( 235 | TableName = dynamodb_table_name, 236 | Key = key, 237 | UpdateExpression = update_expression, 238 | ExpressionAttributeValues = expression_attribute_values 239 | ) 240 | logger.info("response: {}".format(response)) 241 | 242 | 243 | def sig_verified(message, sig): 244 | f = open(pub_key_file, 'r') 245 | pub_key_pem = f.read() 246 | f.close() 247 | 248 | pub_key = crypto.load_publickey(crypto.FILETYPE_PEM, pub_key_pem) 249 | sig = base64.b64decode(sig) 250 | pub_key_x509 = X509() 251 | pub_key_x509.set_pubkey(pub_key) 252 | 253 | try: 254 | crypto.verify(pub_key_x509, sig, message, 'sha256') 255 | logger.info("signature verified for message {}".format(message)) 256 | return True 257 | except Exception as e: 258 | logger.error("verifying signature failed for message {}: {}".format(message, e)) 259 | 260 | 261 | def lambda_handler(event, context): 262 | global logger 263 | logger = RequestIdAdapter(logger, {'request_id': context.aws_request_id}) 264 | 265 | logger.info("event: {}".format(event)) 266 | 267 | thing_name = None 268 | thing_name_sig = None 269 | CSR = None 270 | answer = {} 271 | 272 | if 'body-json' in event: 273 | if 'thing-name' in event['body-json']: 274 | thing_name = event['body-json']['thing-name'] 275 | 276 | if 'thing-name-sig' in event['body-json']: 277 | thing_name_sig = event['body-json']['thing-name-sig'] 278 | 279 | if 'CSR' in event['body-json']: 280 | CSR = event['body-json']['CSR'] 281 | else: 282 | logger.error("invalid request: key body-json not found in event") 283 | return {"status": "error", "message": "invalid request"} 284 | 285 | 286 | logger.info("thing_name: {}".format(thing_name)) 287 | logger.info("thing_name_sig: {}".format(thing_name_sig)) 288 | logger.info("CSR: {}".format(CSR)) 289 | 290 | if thing_name == None: 291 | logger.error("no thing-name in request") 292 | return {"status": "error", "message": "no thing name"} 293 | 294 | if thing_name_sig == None: 295 | logger.error("no thing-name-sig in request") 296 | return {"status": "error", "message": "no sig"} 297 | 298 | if not sig_verified(thing_name, thing_name_sig): 299 | logger.error("signature could not be verified") 300 | return {"status": "error", "message": "wrong sig"} 301 | 302 | if not device_marked_for_provisioning(thing_name): 303 | logger.error("device is not marked for provisioning") 304 | return {"status": "error", "message": "you not"} 305 | 306 | if 'params' in event and 'header' in event['params'] and 'X-Forwarded-For' in event['params']['header']: 307 | device_addrs = str(event['params']['header']['X-Forwarded-For']).translate(None, string.whitespace).split(',') 308 | logger.info(device_addrs) 309 | else: 310 | logger.warn("can not find X-Forwarded-For") 311 | return {"status": "error", "message": "no location"} 312 | 313 | location = get_ip_location(device_addrs[0]) 314 | if location['latitude'] == None or location['longitude'] == None: 315 | logger.warn("no latitude or longitude for IP {}, using default region {}".format(device_addrs[0], default_region)) 316 | answer = provision_device(thing_name, default_region, CSR) 317 | answer['region'] = default_region 318 | answer['message'] = "no latitude or longitude for IP {}, using default region {}".format(device_addrs[0], default_region) 319 | answer['status'] = 'success' 320 | else: 321 | lat = float(location['latitude']) 322 | lon = float(location['longitude']) 323 | logger.info("lat: {}, lon: {}".format(lat, lon)) 324 | best_region = find_best_region(lat, lon) 325 | answer = provision_device(thing_name, best_region['region'], CSR) 326 | answer['region'] = best_region['region'] 327 | answer['distance'] = best_region['distance'] 328 | answer['status'] = 'success' 329 | 330 | update_device_provisioning_status(thing_name, best_region['region']) 331 | 332 | return answer 333 | -------------------------------------------------------------------------------- /provisioning/lambda/requirements.txt: -------------------------------------------------------------------------------- 1 | pyOpenSSL 2 | geopy 3 | requests 4 | --------------------------------------------------------------------------------