├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── certs └── root.ca.pem ├── config.ini ├── dotnet-core ├── .gitignore ├── AWS.IoT.FleetProvisioning.sln ├── AWS.IoT.FleetProvisioning │ ├── AWS.IoT.FleetProvisioning.csproj │ ├── Certificates │ │ ├── CertificateLoader.cs │ │ └── ICertificateLoader.cs │ ├── Configuration │ │ ├── ISettings.cs │ │ └── Settings.cs │ ├── ConsoleApplication.cs │ ├── Extensions │ │ └── JsonExtensions.cs │ ├── IoTClient │ │ ├── IPermanentClient.cs │ │ ├── IProvisioningClient.cs │ │ ├── PermanentClient.cs │ │ └── ProvisioningClient.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Provisioning │ │ ├── DeviceProvisioningHandler.cs │ │ └── IDeviceProvisioningHandler.cs │ └── appsettings.json └── README.md ├── main.py ├── provisioning_handler.py ├── requirements.txt └── utils ├── __init__.py └── config_loader.py /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, or recently closed, 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' 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](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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 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 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to 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 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Device Fleet Provisioning with AWS IoTCore 2 | 3 | *Updates*: 4 | - Now with the ability to respond to cert rotation requests. When the device has been informed it needs to rotate certificates, simply set an additional (optional) attribute isRotation = True. This update is used in conjunction with a cert_rotation policy specified below. This solution relies on setting a cert_issuance date in the registry when the certificate is registered. This is handled by the provisioning template. Once the device is notified, it can process the rotation through setting the flag below. 5 | 6 | ``` 7 | provisioner.get_official_certs(callback, isRotation=True) 8 | ``` 9 | ------------------------------------------- 10 | 11 | It can often be difficult to manage the secure provisioning of myriad IoT devices in the field. This process can often involve invasive workflow measures, qualified personnel, secure handling of sensitive information, and management of dispensed credentials. Through IoT Core, AWS Fleet Provisioning provides a service oriented, api approach to managing credentials. To learn more about these rich capabilities, read here: https://docs.aws.amazon.com/iot/latest/developerguide/iot-provision.html 12 | 13 | To aid in the adoption and utilization of the functionality mentioned above, this repo provides a reference client to illustrate how device(s) might interact with the provided services to deliver the desired experience. Specifically, the client demonstrates how a common "bootstrap" certificate (placed on n devices) can, upon a first-run experience: 14 | 15 | 1. Connect to IoTCore with stringent bootstrap credentials 16 | 1. Obtain a unique private key and "production" certificate 17 | 1. Present proof of ownership of the production credentials 18 | 1. Prompt the execution of a provisioning template (custom provisioning logic) 19 | 1. Rotate the certificates (decommission bootstrap, promote new cert) 20 | 1. Test the rights of the newly acquired certificate. 21 | 22 | 23 | ## Dependencies of the solution 24 | * Intended to be compatible with AWS Greengrass ... this solution depends on a python library (asyncio) which is __only available w/ python 3.7 and above.__ Please ensure your solution has at least this version. 25 | 26 | * A .NET Core port of the reference client application is available within the [dotnet-core](/dotnet-core) folder - this does not currently support certificate rotation feature available on the Python version. 27 | 28 | * With any connection to IoT Core, you will require the addition of a root CA. We have included a root ca in the repo for convenience but we can't guarantee it will remain current. You can download/replace the contents from the latest contents here: https://www.amazontrust.com/repository/AmazonRootCA1.pem 29 | 30 | * It is recommended to use the general sample provisioning template below if you want the provisioning template to create a thing in IoT Core, Activate the cert, etc. Specifically, ensure the THING node attributes are included in YOUR template if you don't use it verbatim. 31 | 32 | In order to run the client solution seamlessly you must configure dependencies in 2 dimensions: 33 | AWS Console / Edge Device 34 | 35 | ### On the AWS Console: 36 | #### Create a common bootstrap certificate. 37 | 1. Go to the *IoT Core* Service, and in the menu on the left select *Secure* and finally, *Certificates*. 38 | 1. Select *Create* to create your common bootstrap certificates. 39 | 1. Choose *One Click Certificate Creation* (This will create your bootstrap cert to be placed on all devices) 40 | 1. Download and store certificates. 41 | 1. ! Don't forget to download a root.ca.pem and select the button to *ACTIVATE* your certificate on the same screen. 42 | 43 | #### Create Provisioning Template / Attach Policies 44 | 1. In console, select *Onboard* and then *Fleet Provisioning Templates* and finally, *Create*. 45 | 1. Name your provisioning template (e.g. - birthing_template). Remember this name! 46 | 1. Create or associate a basic IoT Role with this template. (at least - AWSIoTThingsRegistration) 47 | 1. Select "Use the AWS IoT registry ..." to ensure the sample code works appropriately as it creates things here. 48 | 1. Select Next 49 | 1. Create or select the policy that you wish fully provisioned devices to have. (see sample open policy below) 50 | 1. Select Next 51 | 1. Enter a Thing name prefix (e.g. MyDevices_) and optionally type, groups or attributes for fully provisioned devices. 52 | 1. Select Create Template 53 | 1. Select the bootstrap certificate you created above and click the *Attach Policy* button. 54 | 1. Ignore the section on Create IAM role to Provision devices, and select *Enable template*. 55 | 1. Now select *close* to return to the console. 56 | 57 | ### On the Edge device 58 | 59 | #### Basic python hygiene 60 | 1. Clone the aws-iot-fleet-provisioning repo to your edge device. 61 | 1. Consider running the solution in a python virtual environment. 62 | 1. Install python dependencies: ```pip3 install -r requirements.txt``` (requirements.txt located in solution root) 63 | 64 | #### Solution setup 65 | 1. Take your downloaded bootstrap credentials (including root.ca.pem) and securely store them on your device. 66 | 1. Find config.ini within the solution and configure the below parameters: 67 | ```python 68 | SECURE_CERT_PATH = PATH/TO/YOUR/CERTS 69 | ROOT_CERT = root.ca.pem 70 | CLAIM_CERT = xxxxxxxxxx-certificate.pem.crt 71 | SECURE_KEY = xxxxxxxxxx-private.pem.key 72 | IOT_ENDPOINT = xxxxxxxxxx-ats.iot.us-east-1.amazonaws.com 73 | PRODUCTION_TEMPLATE = my_template (e.g. - birthing_template) 74 | CERT_ROTATION_TEMPLATE = my_certrotation_template 75 | ``` 76 | 77 | #### Run solution (may need to use *sudo* if storing certificates in a protected dir) 78 | 1. > python3 main.py 79 | 80 | ``` 81 | ##### CONNECTING WITH PROVISIONING CLAIM CERT ##### 82 | ##### SUCCESS. SAVING KEYS TO DEVICE! ##### 83 | ##### CREATING THING ACTIVATING CERT ##### 84 | ##### CERT ACTIVATED AND THING birth_1234567-abcde-fghij-klmno-1234567abc-TLS350 CREATED ##### 85 | ##### CONNECTING WITH OFFICIAL CERT ##### 86 | ##### ACTIVATED AND TESTED CREDENTIALS (xxxxxxxxxx-private.pem.key, xxxxxxxxxx-certificate.pem.crt). ##### 87 | ##### FILES SAVED TO PATH/TO/YOUR/CERTS ##### 88 | ``` 89 | If the solution runs without error, you should notice the new certificates saved in the same directory as the bootstrap certs. You will also notice the creation of THINGS in the IoT Registry that are activated. As this solution is only meant to demo the solution, each subsequent run will use the original bootstrap cert to request new credentials, and therefore also create another thing. Thing names are created based on a hardcoded GUID-Like string (name however you'd like), alternatively, a randomly generated serial number is also shown (commented out) in the code. 90 | 91 | ### See below for examples of necessary artifacts as part of this solution: 92 | 93 | #### Sample "birth_policy" applied to a bootstrap certificate with permissions limited only to provisioning api's. 94 | Note: If using the fleet provisioning feature in the console, this policy will be applied to the certificate automatically. 95 | Also, if you intend to copy/paste the below policy note the arn's and change the region/account number as appropriate. 96 | ```json 97 | { 98 | "Version": "2012-10-17", 99 | "Statement": [ 100 | { 101 | "Effect": "Allow", 102 | "Action": [ 103 | "iot:Connect" 104 | ], 105 | "Resource": "" 106 | }, 107 | { 108 | "Effect": "Allow", 109 | "Action": [ 110 | "iot:Publish", 111 | "iot:Receive" 112 | ], 113 | "Resource": [ 114 | "arn:aws:iot:us-east-1:XXXXXXXXXXXX:topic/$aws/certificates/create/*", 115 | "arn:aws:iot:us-east-1:XXXXXXXXXXXX:topic/$aws/provisioning-templates/birthing_template/provision/*" 116 | ] 117 | }, 118 | { 119 | "Effect": "Allow", 120 | "Action": "iot:Subscribe", 121 | "Resource": [ 122 | "arn:aws:iot:us-east-1:XXXXXXXXXXXX:topicfilter/$aws/certificates/create/*", 123 | "arn:aws:iot:us-east-1:XXXXXXXXXXXX:topicfilter/$aws/provisioning-templates/birthing_template/provision/*" 124 | ] 125 | } 126 | ] 127 | } 128 | ``` 129 | 130 | 131 | 132 | #### Sample Policy for fully provisioned devices - aptly named 'full_citizen_role' 133 | ``` json 134 | { 135 | "Version": "2012-10-17", 136 | "Statement": [ 137 | { 138 | "Effect": "Allow", 139 | "Action": [ 140 | "iot:Publish", 141 | "iot:Subscribe", 142 | "iot:Connect", 143 | "iot:Receive" 144 | ], 145 | "Resource": [ 146 | "" 147 | ] 148 | }, 149 | { 150 | "Effect": "Allow", 151 | "Action": [ 152 | "iot:GetThingShadow", 153 | "iot:UpdateThingShadow", 154 | "iot:DeleteThingShadow" 155 | ], 156 | "Resource": [ 157 | "" 158 | ] 159 | }, 160 | { 161 | "Effect": "Allow", 162 | "Action": [ 163 | "greengrass:*" 164 | ], 165 | "Resource": [ 166 | "" 167 | ] 168 | } 169 | ] 170 | } 171 | ``` 172 | 173 | #### Sample provisioning hook where you validate the request before activating a certificate 174 | ``` 175 | import json 176 | from datetime import date 177 | 178 | provision_response = { 179 | 'allowProvisioning': False, 180 | "parameterOverrides": {"CertDate": date.today().strftime("%m/%d/%y")} 181 | } 182 | 183 | 184 | def handler(event, context): 185 | 186 | ######################## 187 | ## Stringent validation against internal API's/DB etc to validate the request before proceeding 188 | ## 189 | ## if event['parameters']['SerialNumber'] = "approved by company CSO": 190 | ## provision_response["allowProvisioning"] = True 191 | ##################### 192 | 193 | 194 | return provision_response 195 | ``` 196 | 197 | #### Sample provisioning template JSON 198 | ``` json 199 | { 200 | "Parameters": { 201 | "CertDate": { 202 | "Type": "String" 203 | }, 204 | "deviceId": { 205 | "Type": "String" 206 | }, 207 | "AWS::IoT::Certificate::Id": { 208 | "Type": "String" 209 | } 210 | }, 211 | "Resources": { 212 | "certificate": { 213 | "Properties": { 214 | "CertificateId": { 215 | "Ref": "AWS::IoT::Certificate::Id" 216 | }, 217 | "Status": "Active" 218 | }, 219 | "Type": "AWS::IoT::Certificate" 220 | }, 221 | "policy": { 222 | "Properties": { 223 | "PolicyName": "fleetprov_prod_template" 224 | }, 225 | "Type": "AWS::IoT::Policy" 226 | }, 227 | "thing": { 228 | "OverrideSettings": { 229 | "AttributePayload": "MERGE", 230 | "ThingGroups": "DO_NOTHING", 231 | "ThingTypeName": "REPLACE" 232 | }, 233 | "Properties": { 234 | "AttributePayload": { 235 | "cert_issuance": { 236 | "Ref": "CertDate" 237 | } 238 | }, 239 | "ThingGroups": [], 240 | "ThingName": { 241 | "Ref": "deviceId" 242 | } 243 | }, 244 | "Type": "AWS::IoT::Thing" 245 | } 246 | }, 247 | "DeviceConfiguration": { 248 | } 249 | } 250 | 251 | 252 | ``` 253 | 254 | #### Sample Cert Rotation Provisioning Template. Used to activate a new AWS IoT Certificate, and update the cert_issuance attribute in the registry. 255 | ``` 256 | { 257 | "Parameters": { 258 | "SerialNumber": { 259 | "Type": "String" 260 | }, 261 | "CertDate": { 262 | "Type": "String" 263 | }, 264 | "AWS::IoT::Certificate::Id": { 265 | "Type": "String" 266 | } 267 | }, 268 | "Resources": { 269 | "certificate": { 270 | "Properties": { 271 | "CertificateId": { 272 | "Ref": "AWS::IoT::Certificate::Id" 273 | }, 274 | "Status": "Active" 275 | }, 276 | "Type": "AWS::IoT::Certificate" 277 | }, 278 | "policy": { 279 | "Properties": { 280 | "PolicyName": "fleetprov_prod_template" 281 | }, 282 | "Type": "AWS::IoT::Policy" 283 | }, 284 | "thing": { 285 | "OverrideSettings": { 286 | "AttributePayload": "REPLACE", 287 | "ThingGroups": "REPLACE", 288 | "ThingTypeName": "REPLACE" 289 | }, 290 | "Properties": { 291 | "AttributePayload": { 292 | "cert_issuance": { 293 | "Ref": "CertDate" 294 | } 295 | }, 296 | "ThingGroups": [], 297 | "ThingName": { 298 | "Ref": "SerialNumber" 299 | } 300 | }, 301 | "Type": "AWS::IoT::Thing" 302 | } 303 | } 304 | } 305 | ``` 306 | 307 | #### Sample AWS Lambda function used as a provisioning hook for cert rotation requests. 308 | ``` 309 | import json 310 | import boto3 311 | from datetime import date, timedelta 312 | 313 | client = boto3.client('iot') 314 | endpoint = boto3.client('iot-data') 315 | 316 | #used to validate device actually needs a new cert 317 | CERT_ROTATION_DAYS = 360 318 | 319 | #validation check date for registry query 320 | target_date = date.today()-timedelta(days=CERT_ROTATION_DAYS) 321 | target_date = target_date.strftime("%Y%m%d") 322 | 323 | #Set up payload with new cert issuance date 324 | provision_response = {'allowProvisioning': False, "parameterOverrides": { 325 | "CertDate": date.today().strftime("%Y%m%d")}} 326 | 327 | 328 | def handler(event, context): 329 | 330 | # Future log Cloudwatch logs 331 | print("Received event: " + json.dumps(event, indent=2)) 332 | 333 | thing_name = event['parameters']['SerialNumber'] 334 | response = client.describe_thing( 335 | thingName=thing_name) 336 | 337 | try: 338 | #Cross reference ID of requester with entry in registry to ensure device needs a rotation. 339 | if int(response['attributes']['cert_issuance']) < int(target_date): 340 | provision_response["allowProvisioning"] = True 341 | except: 342 | provision_response["allowProvisioning"] = False 343 | 344 | return provision_response 345 | ``` 346 | 347 | #### Sample Lambda used by Cloudwatch as a monitoring agent to notify devices when they're due for a cert rotation 348 | ``` 349 | import json 350 | import boto3 351 | from datetime import date, timedelta 352 | 353 | client = boto3.client('iot') 354 | endpoint = boto3.client('iot-data') 355 | 356 | #Set Cert Rotation Interval 357 | CERT_ROTATION_DAYS = 360 358 | 359 | #Check for certificate expiry due in next 2 weeks. 360 | target_date = date.today()-timedelta(days=CERT_ROTATION_DAYS) 361 | 362 | #Convert to numeric format 363 | target_date = target_date.strftime("%Y%m%d") 364 | 365 | 366 | def lambda_handler(event, context): 367 | 368 | response = client.search_index( 369 | queryString='attributes.cert_issuance<{}'.format(target_date), 370 | maxResults=100) 371 | 372 | for thing in response['things']: 373 | endpoint.publish( 374 | topic='cmd/{}'.format(thing['thingName']), 375 | payload='{"msg":"rotate_cert"}' 376 | ) 377 | 378 | return { 379 | 'things': response['things'] 380 | } 381 | ``` 382 | 383 | 384 | 385 | 386 | ## License 387 | 388 | This library is licensed under the MIT-0 License. See the LICENSE file. 389 | 390 | -------------------------------------------------------------------------------- /certs/root.ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF 3 | ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 4 | b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL 5 | MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv 6 | b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj 7 | ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM 8 | 9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw 9 | IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 10 | VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L 11 | 93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm 12 | jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC 13 | AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA 14 | A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI 15 | U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs 16 | N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv 17 | o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU 18 | 5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy 19 | rqXRfboQnoZsG4q5WTP468SQvvG5 20 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [SETTINGS] 2 | # Set the path to the location containing your certificates (root, private, claim certificate) 3 | SECURE_CERT_PATH = /PATH/TO/CERTS 4 | 5 | # Specify the names for the root cert, provisioning claim cert, and the private key. 6 | ROOT_CERT = root.ca.pem 7 | CLAIM_CERT = bootstrap-certificate.pem.crt 8 | SECURE_KEY = bootstrap-private.pem.key 9 | 10 | # Set the name of your IoT Endpoint 11 | IOT_ENDPOINT = xxxxxxxxxxxxxx-ats.iot.{REGION}.amazonaws.com 12 | 13 | # Include the name for the provisioning template that was created in IoT Core 14 | PRODUCTION_TEMPLATE = production_template 15 | CERT_ROTATION_TEMPLATE = cert_rotation -------------------------------------------------------------------------------- /dotnet-core/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudio 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudio 4 | 5 | ### VisualStudio ### 6 | ## Ignore Visual Studio temporary files, build results, and 7 | ## files generated by popular Visual Studio add-ons. 8 | ## 9 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 10 | 11 | # User-specific files 12 | *.rsuser 13 | *.suo 14 | *.user 15 | *.userosscache 16 | *.sln.docstates 17 | 18 | # User-specific files (MonoDevelop/Xamarin Studio) 19 | *.userprefs 20 | 21 | # Mono auto generated files 22 | mono_crash.* 23 | 24 | # Build results 25 | [Dd]ebug/ 26 | [Dd]ebugPublic/ 27 | [Rr]elease/ 28 | [Rr]eleases/ 29 | x64/ 30 | x86/ 31 | [Aa][Rr][Mm]/ 32 | [Aa][Rr][Mm]64/ 33 | bld/ 34 | [Bb]in/ 35 | [Oo]bj/ 36 | [Ll]og/ 37 | [Ll]ogs/ 38 | 39 | # Visual Studio 2015/2017 cache/options directory 40 | .vs/ 41 | # Uncomment if you have tasks that create the project's static files in wwwroot 42 | #wwwroot/ 43 | 44 | # Visual Studio 2017 auto generated files 45 | Generated\ Files/ 46 | 47 | # MSTest test Results 48 | [Tt]est[Rr]esult*/ 49 | [Bb]uild[Ll]og.* 50 | 51 | # NUnit 52 | *.VisualState.xml 53 | TestResult.xml 54 | nunit-*.xml 55 | 56 | # Build Results of an ATL Project 57 | [Dd]ebugPS/ 58 | [Rr]eleasePS/ 59 | dlldata.c 60 | 61 | # Benchmark Results 62 | BenchmarkDotNet.Artifacts/ 63 | 64 | # .NET Core 65 | project.lock.json 66 | project.fragment.lock.json 67 | artifacts/ 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*[.json, .xml, .info] 147 | 148 | # Visual Studio code coverage results 149 | *.coverage 150 | *.coveragexml 151 | 152 | # NCrunch 153 | _NCrunch_* 154 | .*crunch*.local.xml 155 | nCrunchTemp_* 156 | 157 | # MightyMoose 158 | *.mm.* 159 | AutoTest.Net/ 160 | 161 | # Web workbench (sass) 162 | .sass-cache/ 163 | 164 | # Installshield output folder 165 | [Ee]xpress/ 166 | 167 | # DocProject is a documentation generator add-in 168 | DocProject/buildhelp/ 169 | DocProject/Help/*.HxT 170 | DocProject/Help/*.HxC 171 | DocProject/Help/*.hhc 172 | DocProject/Help/*.hhk 173 | DocProject/Help/*.hhp 174 | DocProject/Help/Html2 175 | DocProject/Help/html 176 | 177 | # Click-Once directory 178 | publish/ 179 | 180 | # Publish Web Output 181 | *.[Pp]ublish.xml 182 | *.azurePubxml 183 | # Note: Comment the next line if you want to checkin your web deploy settings, 184 | # but database connection strings (with potential passwords) will be unencrypted 185 | *.pubxml 186 | *.publishproj 187 | 188 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 189 | # checkin your Azure Web App publish settings, but sensitive information contained 190 | # in these scripts will be unencrypted 191 | PublishScripts/ 192 | 193 | # NuGet Packages 194 | *.nupkg 195 | # NuGet Symbol Packages 196 | *.snupkg 197 | # The packages folder can be ignored because of Package Restore 198 | **/[Pp]ackages/* 199 | # except build/, which is used as an MSBuild target. 200 | !**/[Pp]ackages/build/ 201 | # Uncomment if necessary however generally it will be regenerated when needed 202 | #!**/[Pp]ackages/repositories.config 203 | # NuGet v3's project.json files produces more ignorable files 204 | *.nuget.props 205 | *.nuget.targets 206 | 207 | # Microsoft Azure Build Output 208 | csx/ 209 | *.build.csdef 210 | 211 | # Microsoft Azure Emulator 212 | ecf/ 213 | rcf/ 214 | 215 | # Windows Store app package directories and files 216 | AppPackages/ 217 | BundleArtifacts/ 218 | Package.StoreAssociation.xml 219 | _pkginfo.txt 220 | *.appx 221 | *.appxbundle 222 | *.appxupload 223 | 224 | # Visual Studio cache files 225 | # files ending in .cache can be ignored 226 | *.[Cc]ache 227 | # but keep track of directories ending in .cache 228 | !?*.[Cc]ache/ 229 | 230 | # Others 231 | ClientBin/ 232 | ~$* 233 | *~ 234 | *.dbmdl 235 | *.dbproj.schemaview 236 | *.jfm 237 | *.pfx 238 | *.publishsettings 239 | orleans.codegen.cs 240 | 241 | # Including strong name files can present a security risk 242 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 243 | #*.snk 244 | 245 | # Since there are multiple workflows, uncomment next line to ignore bower_components 246 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 247 | #bower_components/ 248 | 249 | # RIA/Silverlight projects 250 | Generated_Code/ 251 | 252 | # Backup & report files from converting an old project file 253 | # to a newer Visual Studio version. Backup files are not needed, 254 | # because we have git ;-) 255 | _UpgradeReport_Files/ 256 | Backup*/ 257 | UpgradeLog*.XML 258 | UpgradeLog*.htm 259 | ServiceFabricBackup/ 260 | *.rptproj.bak 261 | 262 | # SQL Server files 263 | *.mdf 264 | *.ldf 265 | *.ndf 266 | 267 | # Business Intelligence projects 268 | *.rdl.data 269 | *.bim.layout 270 | *.bim_*.settings 271 | *.rptproj.rsuser 272 | *- [Bb]ackup.rdl 273 | *- [Bb]ackup ([0-9]).rdl 274 | *- [Bb]ackup ([0-9][0-9]).rdl 275 | 276 | # Microsoft Fakes 277 | FakesAssemblies/ 278 | 279 | # GhostDoc plugin setting file 280 | *.GhostDoc.xml 281 | 282 | # Node.js Tools for Visual Studio 283 | .ntvs_analysis.dat 284 | node_modules/ 285 | 286 | # Visual Studio 6 build log 287 | *.plg 288 | 289 | # Visual Studio 6 workspace options file 290 | *.opt 291 | 292 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 293 | *.vbw 294 | 295 | # Visual Studio LightSwitch build output 296 | **/*.HTMLClient/GeneratedArtifacts 297 | **/*.DesktopClient/GeneratedArtifacts 298 | **/*.DesktopClient/ModelManifest.xml 299 | **/*.Server/GeneratedArtifacts 300 | **/*.Server/ModelManifest.xml 301 | _Pvt_Extensions 302 | 303 | # Paket dependency manager 304 | .paket/paket.exe 305 | paket-files/ 306 | 307 | # FAKE - F# Make 308 | .fake/ 309 | 310 | # CodeRush personal settings 311 | .cr/personal 312 | 313 | # Python Tools for Visual Studio (PTVS) 314 | __pycache__/ 315 | *.pyc 316 | 317 | # Cake - Uncomment if you are using it 318 | # tools/** 319 | # !tools/packages.config 320 | 321 | # Tabs Studio 322 | *.tss 323 | 324 | # Telerik's JustMock configuration file 325 | *.jmconfig 326 | 327 | # BizTalk build output 328 | *.btp.cs 329 | *.btm.cs 330 | *.odx.cs 331 | *.xsd.cs 332 | 333 | # OpenCover UI analysis results 334 | OpenCover/ 335 | 336 | # Azure Stream Analytics local run output 337 | ASALocalRun/ 338 | 339 | # MSBuild Binary and Structured Log 340 | *.binlog 341 | 342 | # NVidia Nsight GPU debugger configuration file 343 | *.nvuser 344 | 345 | # MFractors (Xamarin productivity tool) working folder 346 | .mfractor/ 347 | 348 | # Local History for Visual Studio 349 | .localhistory/ 350 | 351 | # BeatPulse healthcheck temp database 352 | healthchecksdb 353 | 354 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 355 | MigrationBackup/ 356 | 357 | # Ionide (cross platform F# VS Code tools) working folder 358 | .ionide/ 359 | 360 | # End of https://www.toptal.com/developers/gitignore/api/visualstudio 361 | 362 | 363 | ## Custom 364 | **/Certs/ -------------------------------------------------------------------------------- /dotnet-core/AWS.IoT.FleetProvisioning.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.IoT.FleetProvisioning", "AWS.IoT.FleetProvisioning\AWS.IoT.FleetProvisioning.csproj", "{49A5E221-2443-4D6C-A59B-F9902973EB97}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | Release|Any CPU = Release|Any CPU 9 | EndGlobalSection 10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 11 | {49A5E221-2443-4D6C-A59B-F9902973EB97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 12 | {49A5E221-2443-4D6C-A59B-F9902973EB97}.Debug|Any CPU.Build.0 = Debug|Any CPU 13 | {49A5E221-2443-4D6C-A59B-F9902973EB97}.Release|Any CPU.ActiveCfg = Release|Any CPU 14 | {49A5E221-2443-4D6C-A59B-F9902973EB97}.Release|Any CPU.Build.0 = Release|Any CPU 15 | EndGlobalSection 16 | EndGlobal 17 | -------------------------------------------------------------------------------- /dotnet-core/AWS.IoT.FleetProvisioning/AWS.IoT.FleetProvisioning.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | Always 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /dotnet-core/AWS.IoT.FleetProvisioning/Certificates/CertificateLoader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Runtime.InteropServices; 4 | using System.Security.Cryptography; 5 | using System.Security.Cryptography.X509Certificates; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace AWS.IoT.FleetProvisioning.Certificates 9 | { 10 | public class CertificateLoader : ICertificateLoader 11 | { 12 | private readonly ILogger _logger; 13 | 14 | public CertificateLoader(ILogger logger) 15 | { 16 | _logger = logger; 17 | } 18 | 19 | public X509Certificate2 LoadX509Certificate(string directory, string certificate, string privateKey) 20 | { 21 | _logger.LogDebug($"Within {nameof(LoadX509Certificate)} method."); 22 | 23 | _logger.LogTrace($"{nameof(directory)}: {directory}"); 24 | _logger.LogTrace($"{nameof(certificate)}: {certificate}"); 25 | _logger.LogTrace($"{nameof(privateKey)}: {privateKey}"); 26 | 27 | var certificatePath = Path.Combine(directory, certificate); 28 | var privateKeyPath = Path.Combine(directory, privateKey); 29 | 30 | // thanks to: 31 | // https://github.com/dotnet/runtime/issues/19581#issuecomment-581147166 32 | using var publicKey = new X509Certificate2(certificatePath); 33 | 34 | var privateKeyText = File.ReadAllText(privateKeyPath); 35 | var privateKeyBlocks = privateKeyText.Split("-", StringSplitOptions.RemoveEmptyEntries); 36 | var privateKeyBytes = Convert.FromBase64String(privateKeyBlocks[1]); 37 | using var rsa = RSA.Create(); 38 | 39 | if (privateKeyBlocks[0] == "BEGIN PRIVATE KEY") 40 | { 41 | rsa.ImportPkcs8PrivateKey(privateKeyBytes, out _); 42 | } 43 | else if (privateKeyBlocks[0] == "BEGIN RSA PRIVATE KEY") 44 | { 45 | rsa.ImportRSAPrivateKey(privateKeyBytes, out _); 46 | } 47 | 48 | var certificateWithKey = publicKey.CopyWithPrivateKey(rsa); 49 | 50 | // Need to export and create new Certificate otherwise certificate will be used without secrets. 51 | // Mqtt connection will not be established and fail with Exception - System.ComponentModel.Win32Exception (0x8009030E): No credentials are available in the security package 52 | var certificateBytes = certificateWithKey.Export(X509ContentType.Pfx); 53 | var result = new X509Certificate2(certificateBytes); 54 | return result; 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /dotnet-core/AWS.IoT.FleetProvisioning/Certificates/ICertificateLoader.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography.X509Certificates; 2 | 3 | namespace AWS.IoT.FleetProvisioning.Certificates 4 | { 5 | public interface ICertificateLoader 6 | { 7 | X509Certificate2 LoadX509Certificate(string directory, string certificate, string privateKey); 8 | } 9 | } -------------------------------------------------------------------------------- /dotnet-core/AWS.IoT.FleetProvisioning/Configuration/ISettings.cs: -------------------------------------------------------------------------------- 1 | namespace AWS.IoT.FleetProvisioning.Configuration 2 | { 3 | public interface ISettings 4 | { 5 | string SecureCertificatePath { get; set; } 6 | string RootCertificate { get; set; } 7 | string ClaimCertificate { get; set; } 8 | string ClaimCertificateKey { get; set; } 9 | string IotEndpoint { get; set; } 10 | string ProvisioningTemplate { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /dotnet-core/AWS.IoT.FleetProvisioning/Configuration/Settings.cs: -------------------------------------------------------------------------------- 1 | namespace AWS.IoT.FleetProvisioning.Configuration 2 | { 3 | public class Settings : ISettings 4 | { 5 | public string SecureCertificatePath { get; set; } 6 | public string RootCertificate { get; set; } 7 | public string ClaimCertificate { get; set; } 8 | public string ClaimCertificateKey { get; set; } 9 | public string IotEndpoint { get; set; } 10 | public string ProvisioningTemplate { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /dotnet-core/AWS.IoT.FleetProvisioning/ConsoleApplication.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using AWS.IoT.FleetProvisioning.Extensions; 4 | using AWS.IoT.FleetProvisioning.Provisioning; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace AWS.IoT.FleetProvisioning 8 | { 9 | public class ConsoleApplication 10 | { 11 | private readonly IDeviceProvisioningHandler _handler; 12 | private readonly ILogger _logger; 13 | 14 | public ConsoleApplication(ILogger logger, IDeviceProvisioningHandler handler) 15 | { 16 | _logger = logger; 17 | _handler = handler; 18 | } 19 | 20 | public async Task GetPermanentCertificatesAsync() 21 | { 22 | _logger.LogDebug($"Within {nameof(GetPermanentCertificatesAsync)} method."); 23 | await _handler.BeginProvisioningFlowAsync(Callback); 24 | } 25 | 26 | private void Callback(object message) 27 | { 28 | _logger.LogDebug($"Within {nameof(Callback)} method."); 29 | _logger.LogInformation($"Message: {message.ToJson()}"); 30 | Console.WriteLine(message.ToJson()); 31 | Console.WriteLine($"##### PROVISIONED THING NAMED '{_handler.ThingName}' SUCCESSFULLY #####"); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /dotnet-core/AWS.IoT.FleetProvisioning/Extensions/JsonExtensions.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Serialization; 3 | 4 | namespace AWS.IoT.FleetProvisioning.Extensions 5 | { 6 | public static class JsonExtensions 7 | { 8 | public static string ToJson(this object obj, bool disableIndent = false, JsonSerializerSettings settings = null) 9 | { 10 | var formatting = disableIndent ? Formatting.None : Formatting.Indented; 11 | 12 | settings ??= JsonSerializerHelper.GetSettings(); 13 | 14 | return JsonConvert.SerializeObject(obj, formatting, settings); 15 | } 16 | 17 | public static T FromJson(this string json, JsonSerializerSettings settings = null) where T : new() 18 | { 19 | settings ??= JsonSerializerHelper.GetSettings(); 20 | 21 | return string.IsNullOrEmpty(json) 22 | ? (T) (object) null 23 | : JsonConvert.DeserializeObject(json, settings); 24 | } 25 | } 26 | 27 | public static class JsonSerializerHelper 28 | { 29 | public static JsonSerializerSettings GetSettings() 30 | { 31 | return new JsonSerializerSettings 32 | { 33 | ContractResolver = new CamelCasePropertyNamesContractResolver() 34 | }; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /dotnet-core/AWS.IoT.FleetProvisioning/IoTClient/IPermanentClient.cs: -------------------------------------------------------------------------------- 1 | namespace AWS.IoT.FleetProvisioning.IoTClient 2 | { 3 | public interface IPermanentClient : IProvisioningClient 4 | { 5 | void UpdateClientCredentials(string permanentCertificate, string permanentCertificateKey); 6 | } 7 | } -------------------------------------------------------------------------------- /dotnet-core/AWS.IoT.FleetProvisioning/IoTClient/IProvisioningClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AWS.IoT.FleetProvisioning.IoTClient 4 | { 5 | public interface IProvisioningClient 6 | { 7 | void OnMessage(Action callback); 8 | void Connect(string clientId); 9 | void Subscribe(string topic, int qos, Action callback); 10 | void Publish(string topic, object payload, int qos); 11 | } 12 | } -------------------------------------------------------------------------------- /dotnet-core/AWS.IoT.FleetProvisioning/IoTClient/PermanentClient.cs: -------------------------------------------------------------------------------- 1 | using AWS.IoT.FleetProvisioning.Certificates; 2 | using AWS.IoT.FleetProvisioning.Configuration; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace AWS.IoT.FleetProvisioning.IoTClient 6 | { 7 | public class PermanentClient : ProvisioningClient, IPermanentClient 8 | { 9 | private readonly ILogger _logger; 10 | 11 | public PermanentClient(ILogger logger, ISettings settings, 12 | ICertificateLoader certificateLoader) : base(logger, settings, certificateLoader) 13 | { 14 | _logger = logger; 15 | } 16 | 17 | public void UpdateClientCredentials(string permanentCertificate, string permanentCertificateKey) 18 | { 19 | _logger.LogDebug($"Withing {nameof(UpdateClientCredentials)} method."); 20 | 21 | _logger.LogTrace($"{nameof(permanentCertificate)}: {permanentCertificate}"); 22 | _logger.LogTrace($"{nameof(permanentCertificateKey)}: {permanentCertificateKey}"); 23 | 24 | UpdateClient(permanentCertificate, permanentCertificateKey); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /dotnet-core/AWS.IoT.FleetProvisioning/IoTClient/ProvisioningClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Security.Cryptography.X509Certificates; 5 | using System.Text; 6 | using AWS.IoT.FleetProvisioning.Certificates; 7 | using AWS.IoT.FleetProvisioning.Configuration; 8 | using AWS.IoT.FleetProvisioning.Extensions; 9 | using uPLibrary.Networking.M2Mqtt; 10 | using uPLibrary.Networking.M2Mqtt.Messages; 11 | using Microsoft.Extensions.Logging; 12 | using Newtonsoft.Json; 13 | 14 | namespace AWS.IoT.FleetProvisioning.IoTClient 15 | { 16 | public class ProvisioningClient : IProvisioningClient 17 | { 18 | private readonly ICertificateLoader _certificateLoader; 19 | private readonly ILogger _logger; 20 | private readonly ISettings _settings; 21 | private Action _messageCallback; 22 | private readonly Dictionary> _subscribeCallbackDictionary = new Dictionary>(); 23 | 24 | public ProvisioningClient(ILogger logger, ISettings settings, 25 | ICertificateLoader certificateLoader) 26 | { 27 | _logger = logger; 28 | _settings = settings; 29 | _certificateLoader = certificateLoader; 30 | 31 | InitializeClient(settings.IotEndpoint, settings.SecureCertificatePath, settings.RootCertificate, 32 | settings.ClaimCertificate, settings.ClaimCertificateKey); 33 | } 34 | 35 | private MqttClient MqttClient { get; set; } 36 | 37 | /// 38 | /// Callback that gets called when the client receives a new message. The callback registration should happen before 39 | /// calling connect/connectAsync. This callback, if present, will always be triggered regardless of whether there is any 40 | /// message callback registered upon subscribe API call. It is for the purpose to aggregating the processing of received 41 | /// messages in one function. 42 | /// 43 | /// 44 | public void OnMessage(Action callback) 45 | { 46 | _logger.LogDebug($"Within {nameof(OnMessage)} method."); 47 | _messageCallback = callback; 48 | } 49 | 50 | /// 51 | /// Method used to connect to connect to AWS IoTCore Service. Endpoint collected from config. 52 | /// 53 | /// A unique identifier used for the MQTT connection 54 | public void Connect(string clientId) 55 | { 56 | _logger.LogDebug($"Within {nameof(Connect)} method."); 57 | 58 | _logger.LogTrace($"{nameof(clientId)}: {clientId}"); 59 | 60 | MqttClient.Connect(clientId.ToString()); 61 | 62 | _logger.LogDebug($"Connected to AWS IoT with client id: {clientId}"); 63 | } 64 | 65 | /// 66 | /// Subscribe to the desired topic and register a callback. 67 | /// 68 | /// Topic name or filter to subscribe to. 69 | /// Quality of Service. Could be 0 or 1. 70 | /// 71 | /// Function to be called when a new message for the subscribed topic comes in. 72 | /// 73 | public void Subscribe(string topic, int qos, Action callback) 74 | { 75 | _logger.LogDebug($"Within {nameof(Subscribe)} method."); 76 | 77 | _logger.LogTrace($"{nameof(topic)}: {topic}"); 78 | _logger.LogTrace($"{nameof(qos)}: {qos}"); 79 | 80 | _subscribeCallbackDictionary[topic] = callback; 81 | 82 | MqttClient.Subscribe(new[] {topic}, new[] {(byte) qos}); 83 | } 84 | 85 | /// 86 | /// Publish a new message to the desired topic with QoS. 87 | /// 88 | /// Topic name to publish to. 89 | /// Payload to publish. 90 | /// Quality of Service. Could be 0 or 1. 91 | public void Publish(string topic, object payload, int qos) 92 | { 93 | _logger.LogDebug($"Within {nameof(Publish)} method."); 94 | 95 | _logger.LogTrace($"{nameof(topic)}: {topic}"); 96 | _logger.LogTrace($"{nameof(qos)}: {qos}"); 97 | 98 | var message = payload.ToJson(true, new JsonSerializerSettings()); 99 | 100 | _logger.LogTrace($"{nameof(message)}: {message}"); 101 | 102 | MqttClient.Publish(topic, Encoding.UTF8.GetBytes(message), (byte) qos, false); 103 | } 104 | 105 | /// 106 | /// Method used by the derived class `PermanentClient` to apply the permanent certificate. 107 | /// 108 | /// Name of the permanent certificate file (*.pem.crt) 109 | /// Name of the private key file (*.pem.key) 110 | protected void UpdateClient(string certificate, string certificateKey) 111 | { 112 | MqttClient.MqttMsgPublished -= ClientOnMqttMsgPublished; 113 | MqttClient.MqttMsgSubscribed -= ClientOnMqttMsgSubscribed; 114 | MqttClient.MqttMsgUnsubscribed -= ClientOnMqttMsgUnsubscribed; 115 | MqttClient.MqttMsgPublishReceived -= ClientOnMqttMsgPublishReceived; 116 | 117 | InitializeClient(_settings.IotEndpoint, _settings.SecureCertificatePath, _settings.RootCertificate, 118 | certificate, certificateKey); 119 | } 120 | 121 | private void InitializeClient(string iotEndpoint, string certificatePath, string rootCertificate, 122 | string certificate, string certificateKey) 123 | { 124 | var caCert = X509Certificate.CreateFromCertFile(Path.Join(certificatePath, rootCertificate)); 125 | var clientCert = _certificateLoader.LoadX509Certificate(certificatePath, certificate, certificateKey); 126 | 127 | MqttClient = new MqttClient(iotEndpoint, 8883, true, caCert, clientCert, MqttSslProtocols.TLSv1_2); 128 | 129 | MqttClient.MqttMsgPublished += ClientOnMqttMsgPublished; 130 | MqttClient.MqttMsgSubscribed += ClientOnMqttMsgSubscribed; 131 | MqttClient.MqttMsgUnsubscribed += ClientOnMqttMsgUnsubscribed; 132 | MqttClient.MqttMsgPublishReceived += ClientOnMqttMsgPublishReceived; 133 | } 134 | 135 | private void ClientOnMqttMsgPublished(object sender, MqttMsgPublishedEventArgs e) 136 | { 137 | _logger.LogTrace($"OnMqttMsgPublished triggered with sender: '{sender.ToJson()}', e: '{e.ToJson()}'"); 138 | } 139 | 140 | private void ClientOnMqttMsgSubscribed(object sender, MqttMsgSubscribedEventArgs e) 141 | { 142 | _logger.LogTrace($"OnMqttMsgSubscribed triggered with sender: '{sender.ToJson()}', e: '{e.ToJson()}'"); 143 | } 144 | 145 | private void ClientOnMqttMsgUnsubscribed(object sender, MqttMsgUnsubscribedEventArgs e) 146 | { 147 | _logger.LogTrace($"OnMqttMsgUnsubscribed triggered with sender: '{sender.ToJson()}', e: '{e.ToJson()}'"); 148 | } 149 | 150 | private void ClientOnMqttMsgPublishReceived(object sender, MqttMsgPublishEventArgs e) 151 | { 152 | _logger.LogTrace($"OnMqttMsgPublishReceived on topic: '{e.Topic}' with QoS level '{e.QosLevel}'"); 153 | 154 | var message = Encoding.UTF8.GetString(e.Message); 155 | _logger.LogTrace($"{nameof(message)}: {message}"); 156 | 157 | _messageCallback?.Invoke(message); 158 | 159 | if (_subscribeCallbackDictionary.ContainsKey(e.Topic)) 160 | { 161 | _subscribeCallbackDictionary[e.Topic].Invoke(message); 162 | } 163 | } 164 | } 165 | } -------------------------------------------------------------------------------- /dotnet-core/AWS.IoT.FleetProvisioning/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using AWS.IoT.FleetProvisioning.Certificates; 5 | using AWS.IoT.FleetProvisioning.Configuration; 6 | using AWS.IoT.FleetProvisioning.IoTClient; 7 | using AWS.IoT.FleetProvisioning.Provisioning; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Hosting; 11 | using Microsoft.Extensions.Logging; 12 | 13 | namespace AWS.IoT.FleetProvisioning 14 | { 15 | public class Program 16 | { 17 | public static async Task Main(string[] args) 18 | { 19 | try 20 | { 21 | var serviceProvider = CreateHostBuilder(args).Build().Services; 22 | 23 | await serviceProvider 24 | .GetService() 25 | .GetPermanentCertificatesAsync(); 26 | } 27 | catch (DirectoryNotFoundException) 28 | { 29 | Console.WriteLine("### Bootstrap cert non-existent. Official cert may already be in place. ###"); 30 | } 31 | catch (Exception e) 32 | { 33 | Console.WriteLine(e); 34 | Console.ReadKey(true); 35 | } 36 | finally 37 | { 38 | Console.WriteLine(); 39 | Console.WriteLine("Program completed... Press Ctrl+C to exit."); 40 | } 41 | } 42 | 43 | private static IHostBuilder CreateHostBuilder(string[] args) 44 | { 45 | ISettings settings = null; 46 | 47 | return Host.CreateDefaultBuilder(args) 48 | .ConfigureLogging(loggingBuilder => 49 | { 50 | loggingBuilder.ClearProviders(); 51 | loggingBuilder.SetMinimumLevel(LogLevel.None); 52 | loggingBuilder.AddConsole(); 53 | }) 54 | .ConfigureAppConfiguration(builder => 55 | { 56 | var configurationRoot = builder.Build(); 57 | var configurationSection = configurationRoot.GetSection(nameof(Settings)); 58 | settings = configurationSection.Get(); 59 | }) 60 | .ConfigureServices(services => 61 | { 62 | // Add settings to our DI container for later uses 63 | services.AddSingleton(settings); 64 | 65 | services.AddTransient(); 66 | services.AddTransient(); 67 | services.AddTransient(); 68 | services.AddTransient(); 69 | 70 | // IMPORTANT! Register our application entry point 71 | services.AddTransient(); 72 | }); 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /dotnet-core/AWS.IoT.FleetProvisioning/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "AWS.IoT.FleetProvisioning": { 5 | "commandName": "Project", 6 | "environmentVariables": { 7 | "DOTNET_ENVIRONMENT": "Development" 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /dotnet-core/AWS.IoT.FleetProvisioning/Provisioning/DeviceProvisioningHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using AWS.IoT.FleetProvisioning.Configuration; 6 | using AWS.IoT.FleetProvisioning.Extensions; 7 | using AWS.IoT.FleetProvisioning.IoTClient; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace AWS.IoT.FleetProvisioning.Provisioning 11 | { 12 | public class DeviceProvisioningHandler : IDeviceProvisioningHandler 13 | { 14 | private const int LoopDelay = 500; 15 | private readonly Guid _clientId; 16 | 17 | private readonly ILogger _logger; 18 | private readonly IPermanentClient _permanentClient; 19 | private readonly IProvisioningClient _provisioningClient; 20 | private readonly ISettings _settings; 21 | 22 | private bool _callbackReturned; 23 | private object _messagePayload; 24 | private string _permanentCertificate; 25 | private string _permanentCertificateKey; 26 | 27 | public DeviceProvisioningHandler(ILogger logger, ISettings settings, 28 | IProvisioningClient provisioningClient, IPermanentClient permanentClient) 29 | { 30 | _logger = logger; 31 | _settings = settings; 32 | _provisioningClient = provisioningClient; 33 | _permanentClient = permanentClient; 34 | 35 | _clientId = Guid.NewGuid(); 36 | } 37 | 38 | public string ThingName { get; set; } 39 | 40 | /// 41 | /// Initiates an async loop/call to kick off the provisioning flow 42 | /// 43 | /// 44 | /// 45 | public async Task BeginProvisioningFlowAsync(Action callback) 46 | { 47 | _logger.LogDebug($"Within {nameof(BeginProvisioningFlowAsync)} method."); 48 | 49 | // Set OnMessageCallback on the provisioning client 50 | _provisioningClient.OnMessage(MessageCallback); 51 | 52 | // Connect to IoT Core with provision claim credentials 53 | _logger.LogInformation("##### CONNECTING WITH PROVISIONING CLAIM CERT #####"); 54 | Console.WriteLine("##### CONNECTING WITH PROVISIONING CLAIM CERT #####"); 55 | _provisioningClient.Connect(_clientId.ToString()); 56 | 57 | // Monitors topics for errors 58 | EnableErrorMonitor(); 59 | 60 | // Make a publish call to topic to get official certificates 61 | _provisioningClient.Publish("$aws/certificates/create/json", new { }, 0); 62 | 63 | while (!_callbackReturned) 64 | { 65 | _logger.LogDebug($"Adding a delay of {LoopDelay} milliseconds."); 66 | await Task.Delay(LoopDelay); 67 | } 68 | 69 | callback?.Invoke(_messagePayload); 70 | } 71 | 72 | /// 73 | /// Subscribe to pertinent IoTCore topics that would emit errors 74 | /// 75 | private void EnableErrorMonitor() 76 | { 77 | _logger.LogDebug($"Within {nameof(EnableErrorMonitor)} method."); 78 | 79 | _provisioningClient.Subscribe( 80 | $"$aws/provisioning-templates/{_settings.ProvisioningTemplate}/provision/json/rejected", 1, 81 | BasicCallback); 82 | _provisioningClient.Subscribe("$aws/certificates/create/json/rejected", 1, BasicCallback); 83 | } 84 | 85 | /// 86 | /// Callback Message handler responsible for workflow routing of msg responses from provisioning services. 87 | /// 88 | /// The response message payload. 89 | private void MessageCallback(string message) 90 | { 91 | _logger.LogDebug($"Within {nameof(MessageCallback)} method."); 92 | 93 | var data = message.FromJson(); 94 | 95 | // A response has been received from the service that contains certificate data. 96 | if (data.certificateId != null) 97 | { 98 | _logger.LogInformation("##### SUCCESS. SAVING KEYS TO DEVICE! #####"); 99 | Console.WriteLine("##### SUCCESS. SAVING KEYS TO DEVICE! #####"); 100 | AssembleCertificates(data); 101 | } 102 | else if (data.deviceConfiguration != null) 103 | { 104 | ThingName = (string) data.thingName; 105 | _logger.LogInformation($"##### CERT ACTIVATED AND THING {ThingName} CREATED #####"); 106 | Console.WriteLine($"##### CERT ACTIVATED AND THING {ThingName} CREATED #####"); 107 | 108 | 109 | ValidateCertificate(); 110 | } 111 | } 112 | 113 | /// 114 | /// Method takes the payload and constructs/saves the certificate and private key. Method uses existing AWS IoT Core naming 115 | /// convention. 116 | /// 117 | /// Certifiable certificate/key data. 118 | private void AssembleCertificates(dynamic data) 119 | { 120 | _logger.LogDebug($"Within {nameof(AssembleCertificates)} method."); 121 | 122 | var certificateId = data.certificateId; 123 | var prefix = ((string) certificateId).Substring(0, 10); 124 | 125 | _permanentCertificate = $"{prefix}-certificate.pem.crt"; 126 | File.WriteAllText(Path.Combine(_settings.SecureCertificatePath, _permanentCertificate), 127 | (string) data.certificatePem); 128 | 129 | _permanentCertificateKey = $"{prefix}-private.pem.key"; 130 | File.WriteAllText(Path.Combine(_settings.SecureCertificatePath, _permanentCertificateKey), 131 | (string) data.privateKey); 132 | 133 | RegisterThing((string) data.certificateOwnershipToken); 134 | } 135 | 136 | /// 137 | /// Calls the fleet provisioning service responsible for acting upon instructions within device templates. 138 | /// 139 | /// The token response from certificate creation to prove ownership/immediate possession of the certs. 140 | private void RegisterThing(string token) 141 | { 142 | _logger.LogDebug($"Within {nameof(RegisterThing)} method."); 143 | 144 | _logger.LogInformation("##### CREATING THING ACTIVATING CERT #####"); 145 | Console.WriteLine("##### CREATING THING ACTIVATING CERT #####"); 146 | 147 | var registerTemplate = new 148 | { 149 | certificateOwnershipToken = token, 150 | parameters = new 151 | { 152 | SerialNumber = _clientId.ToString() 153 | } 154 | }; 155 | 156 | // Register thing / activate certificate 157 | _provisioningClient.Publish($"$aws/provisioning-templates/{_settings.ProvisioningTemplate}/provision/json", 158 | registerTemplate, 0); 159 | } 160 | 161 | /// 162 | /// Responsible for (re)connecting to IoTCore with the newly provisioned/activated certificate - (first class citizen cert) 163 | /// 164 | private void ValidateCertificate() 165 | { 166 | _logger.LogDebug($"Within {nameof(ValidateCertificate)} method."); 167 | _permanentClient.UpdateClientCredentials(_permanentCertificate, _permanentCertificateKey); 168 | 169 | _logger.LogInformation("##### CONNECTING WITH OFFICIAL CERT #####"); 170 | Console.WriteLine("##### CONNECTING WITH OFFICIAL CERT #####"); 171 | 172 | _permanentClient.Connect(ThingName); 173 | 174 | NewCertificatePublishAndSubscribe(); 175 | } 176 | 177 | /// 178 | /// Method testing a call to the 'openworld' topic (which was specified in the policy for the new certificate) 179 | /// 180 | private void NewCertificatePublishAndSubscribe() 181 | { 182 | _permanentClient.Subscribe($"{ThingName}/openworld", 1, BasicCallback); 183 | 184 | Thread.Sleep(500); 185 | 186 | var payload = new 187 | { 188 | ServiceResponse = "##### RESPONSE FROM A PREVIOUSLY FORBIDDEN TOPIC #####" 189 | }; 190 | _permanentClient.Publish($"{ThingName}/openworld", payload, 0); 191 | } 192 | 193 | /// 194 | /// Method responding to the openworld publish attempt. Demonstrating a successful pub/sub with new certificate. 195 | /// 196 | /// 197 | private void BasicCallback(string message) 198 | { 199 | _logger.LogDebug($"Within {nameof(BasicCallback)} method."); 200 | _logger.LogDebug(message); 201 | 202 | _messagePayload = message; 203 | _callbackReturned = true; 204 | } 205 | } 206 | } -------------------------------------------------------------------------------- /dotnet-core/AWS.IoT.FleetProvisioning/Provisioning/IDeviceProvisioningHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace AWS.IoT.FleetProvisioning.Provisioning 5 | { 6 | public interface IDeviceProvisioningHandler 7 | { 8 | /// 9 | /// Initiates an async loop/call to kick off the provisioning flow 10 | /// 11 | /// 12 | /// 13 | Task BeginProvisioningFlowAsync(Action callback); 14 | 15 | /// 16 | /// Name of the provisioned thing 17 | /// 18 | string ThingName { get; set; } 19 | } 20 | } -------------------------------------------------------------------------------- /dotnet-core/AWS.IoT.FleetProvisioning/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Error", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | }, 9 | "settings": { 10 | // absolute path to the folder containing your certificates 11 | "secureCertificatePath": "/PATH/TO/CERTS", 12 | // names for root certificate, provisioning claim certificate, and private key. 13 | "rootCertificate": "root.ca.pem", 14 | "claimCertificate": "bootstrap-certificate.pem.crt", 15 | "claimCertificateKey": "bootstrap-private.pem.key", 16 | // IoT Data:ATS Endpoint (run `aws iot describe-endpoint --endpoint-type iot:Data-ATS` to get this value) 17 | "iotEndpoint": "xxxxxxxxxxxxxx-ats.iot.{REGION}.amazonaws.com", 18 | // name for the provisioning template that was created in IoT Core 19 | "provisioningTemplate": "Provisioning-Template" 20 | } 21 | } -------------------------------------------------------------------------------- /dotnet-core/README.md: -------------------------------------------------------------------------------- 1 | ## Fleet Provisioning from a .NET Core Client 2 | 3 | 1. Follow the instructions in the [root-level README](../README.md) to configure the dependencies on the AWS Console. 4 | 2. Refer [this section](../README.md#see-below-for-examples-of-necessary-artifacts-as-part-of-this-solution) for the samples on IoT policies and provisioning templates. 5 | 3. Clone the repository to the edge device. 6 | 4. Run `dotnet restore dotnet-core/AWS.IoT.FleetProvisioning/AWS.IoT.FleetProvisioning.csproj` to install the NuGet dependencies. 7 | 5. Place the downloaded bootstrap credentials on your device (at the `dotnet-core/AWS.IoT.FleetProvisioning/Certs/` folder) beside the `root.ca.pem` file. 8 | 6. Update `dotnet-core/AWS.IoT.FleetProvisioning/appsettings.json` to configure the below parameters: 9 | ```json 10 | "settings": { 11 | // absolute path to the folder containing your certificates 12 | "secureCertificatePath": "/PATH/TO/CERTS", 13 | // names for root certificate, provisioning claim certificate, and private key. 14 | "rootCertificate": "root.ca.pem", 15 | "claimCertificate": "bootstrap-certificate.pem.crt", 16 | "claimCertificateKey": "bootstrap-private.pem.key", 17 | // IoT Data:ATS Endpoint (run `aws iot describe-endpoint --endpoint-type iot:Data-ATS` to get this value) 18 | "iotEndpoint": "xxxxxxxxxxxxxx-ats.iot.{REGION}.amazonaws.com", 19 | // name for the provisioning template that was created in IoT Core 20 | "provisioningTemplate": "Provisioning-Template" 21 | } 22 | ``` 23 | 7. Change into the project directory: `cd dotnet-core/AWS.IoT.FleetProvisioning` 24 | 8. Run the solution: `dotnet run` 25 | 26 | If the solution runs without errors, you will notice that the new certificates are saved in `secureCertificatePath` directory. You will also notice that new "Thing" has been created and activated in [the IoT Registry](https://console.aws.amazon.com/iot/home#/thinghub). As this is only meant to be a demo, each subsequent run will use the original bootstrap cert to request new credentials, and therefore will create new "Things". Thing names are based on dynamically generated "serial numbers" (which are just new `Guid`s) as can be seen in the code. -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | # ----------------------------------------- 5 | # Consuming sample, demonstrating how a device process would leverage the provisioning class. 6 | # The handler makes use of the asycio library and therefore requires Python 3.7. 7 | # 8 | # Prereq's: 9 | # 1) A provisioning claim certificate has been cut from AWSIoT. 10 | # 2) A restrictive "birth" policy has been associated with the certificate. 11 | # 3) A provisioning template was created to manage the activities to be performed during new certificate activation. 12 | # 4) The claim certificate was placed securely on the device fleet and shipped to the field. (along with root/ca and private key) 13 | # 14 | # Execution: 15 | # 1) The paths to the certificates, and names of IoTCore endpoint and provisioning template are set in config.ini (this project) 16 | # 2) A device boots up and encounters it's "first run" experience and executes the process (main) below. 17 | # 3) The process instatiates a handler that uses the bootstrap certificate to connect to IoTCore. 18 | # 4) The connection only enables calls to the Foundry provisioning services, where a new certificate is requested. 19 | # 5) The certificate is assembled from the response payload, and a foundry service call is made to activate the certificate. 20 | # 6) The provisioning template executes the instructions provided and the process rotates to the new certificate. 21 | # 7) Using the new certificate, a pub/sub call is demonstrated on a previously forbidden topic to test the new certificate. 22 | # 8) New certificates are saved locally, and can be stored/consumed as the application deems necessary. 23 | # 24 | # 25 | # Initial version - Raleigh Murch, AWS 26 | # email: murchral@amazon.com 27 | # ------------------------------------------------------------------------------ 28 | 29 | from provisioning_handler import ProvisioningHandler 30 | from utils.config_loader import Config 31 | from pyfiglet import Figlet 32 | 33 | 34 | #Set Config path 35 | CONFIG_PATH = 'config.ini' 36 | 37 | config = Config(CONFIG_PATH) 38 | config_parameters = config.get_section('SETTINGS') 39 | secure_cert_path = config_parameters['SECURE_CERT_PATH'] 40 | bootstrap_cert = config_parameters['CLAIM_CERT'] 41 | 42 | # Demo Theater 43 | f = Figlet(font='slant') 44 | print(f.renderText(' F l e e t')) 45 | print(f.renderText('Provisioning')) 46 | print(f.renderText('----------')) 47 | 48 | # Provided callback for provisioning method feedback. 49 | def callback(payload): 50 | print(payload) 51 | 52 | # Used to kick off the provisioning lifecycle, exchanging the bootstrap cert for a 53 | # production certificate after being validated by a provisioning hook lambda. 54 | # 55 | # isRotation = True is used to rotate from one production certificate to a new production certificate. 56 | # Certificates signed by AWS IoT Root CA expire on 12/31/2049. Security best practices 57 | # urge frequent rotation of x.509 certificates and this method (used in conjunction with 58 | # a cloud cert management pattern) attempts to make cert exchange easy. 59 | def run_provisioning(isRotation): 60 | 61 | provisioner = ProvisioningHandler(CONFIG_PATH) 62 | 63 | if isRotation: 64 | provisioner.get_official_certs(callback, isRotation=True) 65 | else: 66 | #Check for availability of bootstrap cert 67 | try: 68 | with open("{}/{}".format(secure_cert_path, bootstrap_cert)) as f: 69 | # Call super-method to perform aquisition/activation 70 | # of certs, creation of thing, etc. Returns general 71 | # purpose callback at this point. 72 | # Instantiate provisioning handler, pass in path to config 73 | provisioner.get_official_certs(callback) 74 | 75 | except IOError: 76 | print("### Bootstrap cert non-existent. Official cert may already be in place.") 77 | 78 | if __name__ == "__main__": 79 | run_provisioning(isRotation=False) 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /provisioning_handler.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | # ------------------------------------------------------------------------------ 5 | # Demonstrates how to call/orchestrate AWS fleet provisioning services 6 | # with a provided bootstrap certificate (aka - provisioning claim cert). 7 | # 8 | # Initial version - Raleigh Murch, AWS 9 | # email: murchral@amazon.com 10 | # ------------------------------------------------------------------------------ 11 | 12 | from awscrt import io, mqtt, auth, http 13 | from awsiot import mqtt_connection_builder 14 | from utils.config_loader import Config 15 | import time 16 | import logging 17 | import json 18 | import os 19 | import asyncio 20 | import glob 21 | 22 | 23 | class ProvisioningHandler: 24 | 25 | def __init__(self, file_path): 26 | """Initializes the provisioning handler 27 | 28 | Arguments: 29 | file_path {string} -- path to your configuration file 30 | """ 31 | #Logging 32 | logging.basicConfig(level=logging.ERROR) 33 | self.logger = logging.getLogger(__name__) 34 | 35 | #Load configuration settings from config.ini 36 | config = Config(file_path) 37 | self.config_parameters = config.get_section('SETTINGS') 38 | self.secure_cert_path = self.config_parameters['SECURE_CERT_PATH'] 39 | self.iot_endpoint = self.config_parameters['IOT_ENDPOINT'] 40 | self.template_name = self.config_parameters['PRODUCTION_TEMPLATE'] 41 | self.rotation_template = self.config_parameters['CERT_ROTATION_TEMPLATE'] 42 | self.claim_cert = self.config_parameters['CLAIM_CERT'] 43 | self.secure_key = self.config_parameters['SECURE_KEY'] 44 | self.root_cert = self.config_parameters['ROOT_CERT'] 45 | 46 | # Sample Provisioning Template requests a serial number as a 47 | # seed to generate Thing names in IoTCore. Simulating here. 48 | #self.unique_id = str(int(round(time.time() * 1000))) 49 | self.unique_id = "1234567-abcde-fghij-klmno-1234567abc-TLS350" 50 | 51 | # ------------------------------------------------------------------------------ 52 | # -- PROVISIONING HOOKS EXAMPLE -- 53 | # Provisioning Hooks are a powerful feature for fleet provisioning. Most of the 54 | # heavy lifting is performed within the cloud lambda. However, you can send 55 | # device attributes to be validated by the lambda. An example is shown in the line 56 | # below (.hasValidAccount could be checked in the cloud against a database). 57 | # Alternatively, a serial number, geo-location, or any attribute could be sent. 58 | # 59 | # -- Note: This attribute is passed up as part of the register_thing method and 60 | # will be validated in your lambda's event data. 61 | # ------------------------------------------------------------------------------ 62 | 63 | self.primary_MQTTClient = None 64 | self.test_MQTTClient = None 65 | self.callback_returned = False 66 | self.message_payload = {} 67 | self.isRotation = False 68 | 69 | 70 | def core_connect(self): 71 | """ Method used to connect to AWS IoTCore Service. Endpoint collected from config. 72 | 73 | """ 74 | if self.isRotation: 75 | self.logger.info('##### CONNECTING WITH EXISTING CERT #####') 76 | print('##### CONNECTING WITH EXISTING CERT #####') 77 | self.get_current_certs() 78 | else: 79 | self.logger.info('##### CONNECTING WITH PROVISIONING CLAIM CERT #####') 80 | print('##### CONNECTING WITH PROVISIONING CLAIM CERT #####') 81 | 82 | event_loop_group = io.EventLoopGroup(1) 83 | host_resolver = io.DefaultHostResolver(event_loop_group) 84 | client_bootstrap = io.ClientBootstrap(event_loop_group, host_resolver) 85 | 86 | self.primary_MQTTClient = mqtt_connection_builder.mtls_from_path( 87 | endpoint=self.iot_endpoint, 88 | cert_filepath="{}/{}".format(self.secure_cert_path, self.claim_cert), 89 | pri_key_filepath="{}/{}".format(self.secure_cert_path, self.secure_key), 90 | client_bootstrap=client_bootstrap, 91 | ca_filepath="{}/{}".format(self.secure_cert_path, self.root_cert), 92 | on_connection_interrupted=self.on_connection_interrupted, 93 | on_connection_resumed=self.on_connection_resumed, 94 | client_id=self.unique_id, 95 | clean_session=False, 96 | keep_alive_secs=6) 97 | 98 | print("Connecting to {} with client ID '{}'...".format(self.iot_endpoint, self.unique_id)) 99 | connect_future = self.primary_MQTTClient.connect() 100 | # Future.result() waits until a result is available 101 | connect_future.result() 102 | print("Connected!") 103 | 104 | 105 | def on_connection_interrupted(self, connection, error, **kwargs): 106 | print('connection interrupted with error {}'.format(error)) 107 | 108 | 109 | def on_connection_resumed(self, connection, return_code, session_present, **kwargs): 110 | print('connection resumed with return code {}, session present {}'.format(return_code, session_present)) 111 | 112 | 113 | def get_current_certs(self): 114 | non_bootstrap_certs = glob.glob('{}/[!boot]*.crt'.format(self.secure_cert_path)) 115 | non_bootstrap_key = glob.glob('{}/[!boot]*.key'.format(self.secure_cert_path)) 116 | 117 | #Get the current cert 118 | if len(non_bootstrap_certs) > 0: 119 | self.claim_cert = os.path.basename(non_bootstrap_certs[0]) 120 | 121 | #Get the current key 122 | if len(non_bootstrap_key) > 0: 123 | self.secure_key = os.path.basename(non_bootstrap_key[0]) 124 | 125 | 126 | def enable_error_monitor(self): 127 | """ Subscribe to pertinent IoTCore topics that would emit errors 128 | """ 129 | 130 | template_reject_topic = "$aws/provisioning-templates/{}/provision/json/rejected".format(self.template_name) 131 | certificate_reject_topic = "$aws/certificates/create/json/rejected" 132 | 133 | template_accepted_topic = "$aws/provisioning-templates/{}/provision/json/accepted".format(self.template_name) 134 | certificate_accepted_topic = "$aws/certificates/create/json/accepted" 135 | 136 | subscribe_topics = [template_reject_topic, certificate_reject_topic, template_accepted_topic, certificate_accepted_topic] 137 | 138 | for mqtt_topic in subscribe_topics: 139 | print("Subscribing to topic '{}'...".format(mqtt_topic)) 140 | mqtt_topic_subscribe_future, _ = self.primary_MQTTClient.subscribe( 141 | topic=mqtt_topic, 142 | qos=mqtt.QoS.AT_LEAST_ONCE, 143 | callback=self.basic_callback) 144 | 145 | # Wait for subscription to succeed 146 | mqtt_topic_subscribe_result = mqtt_topic_subscribe_future.result() 147 | print("Subscribed with {}".format(str(mqtt_topic_subscribe_result['qos']))) 148 | 149 | 150 | def get_official_certs(self, callback, isRotation=False): 151 | """ Initiates an async loop/call to kick off the provisioning flow. 152 | 153 | Triggers: 154 | on_message_callback() providing the certificate payload 155 | """ 156 | if isRotation: 157 | self.template_name = self.rotation_template 158 | self.isRotation = True 159 | 160 | return asyncio.run(self.orchestrate_provisioning_flow(callback)) 161 | 162 | async def orchestrate_provisioning_flow(self,callback): 163 | # Connect to core with provision claim creds 164 | self.core_connect() 165 | 166 | # Monitor topics for errors 167 | self.enable_error_monitor() 168 | 169 | # Make a publish call to topic to get official certs 170 | #self.primary_MQTTClient.publish("$aws/certificates/create/json", "{}", 0) 171 | 172 | self.primary_MQTTClient.publish( 173 | topic="$aws/certificates/create/json", 174 | payload="{}", 175 | qos=mqtt.QoS.AT_LEAST_ONCE) 176 | time.sleep(1) 177 | 178 | # Wait the function return until all callbacks have returned 179 | # Returned denoted when callback flag is set in this class. 180 | while not self.callback_returned: 181 | await asyncio.sleep(0) 182 | 183 | return callback(self.message_payload) 184 | 185 | 186 | 187 | def on_message_callback(self, payload): 188 | """ Callback Message handler responsible for workflow routing of msg responses from provisioning services. 189 | 190 | Arguments: 191 | payload {bytes} -- The response message payload. 192 | """ 193 | json_data = json.loads(payload) 194 | 195 | # A response has been recieved from the service that contains certificate data. 196 | if 'certificateId' in json_data: 197 | self.logger.info('##### SUCCESS. SAVING KEYS TO DEVICE! #####') 198 | print('##### SUCCESS. SAVING KEYS TO DEVICE! #####') 199 | self.assemble_certificates(json_data) 200 | 201 | # A response contains acknowledgement that the provisioning template has been acted upon. 202 | elif 'deviceConfiguration' in json_data: 203 | if self.isRotation: 204 | self.logger.info('##### ACTIVATION COMPLETE #####') 205 | print('##### ACTIVATION COMPLETE #####') 206 | else: 207 | self.logger.info('##### CERT ACTIVATED AND THING {} CREATED #####'.format(json_data['thingName'])) 208 | print('##### CERT ACTIVATED AND THING {} CREATED #####'.format(json_data['thingName'])) 209 | 210 | self.validate_certs() 211 | elif 'service_response' in json_data: 212 | self.logger.info(json_data) 213 | print('##### SUCCESSFULLY USED PROD CERTIFICATES #####') 214 | else: 215 | self.logger.info(json_data) 216 | 217 | def assemble_certificates(self, payload): 218 | """ Method takes the payload and constructs/saves the certificate and private key. Method uses 219 | existing AWS IoT Core naming convention. 220 | 221 | Arguments: 222 | payload {string} -- Certifiable certificate/key data. 223 | 224 | Returns: 225 | ownership_token {string} -- proof of ownership from certificate issuance activity. 226 | """ 227 | ### Cert ID 228 | cert_id = payload['certificateId'] 229 | self.new_key_root = cert_id[0:10] 230 | 231 | self.new_cert_name = '{}-certificate.pem.crt'.format(self.new_key_root) 232 | ### Create certificate 233 | f = open('{}/{}'.format(self.secure_cert_path, self.new_cert_name), 'w+') 234 | f.write(payload['certificatePem']) 235 | f.close() 236 | 237 | 238 | ### Create private key 239 | self.new_key_name = '{}-private.pem.key'.format(self.new_key_root) 240 | f = open('{}/{}'.format(self.secure_cert_path, self.new_key_name), 'w+') 241 | f.write(payload['privateKey']) 242 | f.close() 243 | 244 | ### Extract/return Ownership token 245 | self.ownership_token = payload['certificateOwnershipToken'] 246 | 247 | # Register newly aquired cert 248 | self.register_thing(self.unique_id, self.ownership_token) 249 | 250 | 251 | 252 | def register_thing(self, serial, token): 253 | """Calls the fleet provisioning service responsible for acting upon instructions within device templates. 254 | 255 | Arguments: 256 | serial {string} -- unique identifer for the thing. Specified as a property in provisioning template. 257 | token {string} -- The token response from certificate creation to prove ownership/immediate possession of the certs. 258 | 259 | Triggers: 260 | on_message_callback() - providing acknowledgement that the provisioning template was processed. 261 | """ 262 | if self.isRotation: 263 | self.logger.info('##### VALIDATING EXPIRY & ACTIVATING CERT #####') 264 | print('##### VALIDATING EXPIRY & ACTIVATING CERT #####') 265 | else: 266 | self.logger.info('##### CREATING THING ACTIVATING CERT #####') 267 | print('##### CREATING THING ACTIVATING CERT #####') 268 | 269 | register_template = {"certificateOwnershipToken": token, "parameters": {"SerialNumber": serial}} 270 | 271 | #Register thing / activate certificate 272 | self.primary_MQTTClient.publish( 273 | topic="$aws/provisioning-templates/{}/provision/json".format(self.template_name), 274 | payload=json.dumps(register_template), 275 | qos=mqtt.QoS.AT_LEAST_ONCE) 276 | time.sleep(2) 277 | 278 | def validate_certs(self): 279 | """Responsible for (re)connecting to IoTCore with the newly provisioned/activated certificate - (first class citizen cert) 280 | """ 281 | self.logger.info('##### CONNECTING WITH OFFICIAL CERT #####') 282 | print('##### CONNECTING WITH OFFICIAL CERT #####') 283 | self.cert_validation_test() 284 | self.new_cert_pub_sub() 285 | print("##### ACTIVATED AND TESTED CREDENTIALS ({}, {}). #####".format(self.new_key_name, self.new_cert_name)) 286 | print("##### FILES SAVED TO {} #####".format(self.secure_cert_path)) 287 | 288 | def cert_validation_test(self): 289 | event_loop_group = io.EventLoopGroup(1) 290 | host_resolver = io.DefaultHostResolver(event_loop_group) 291 | client_bootstrap = io.ClientBootstrap(event_loop_group, host_resolver) 292 | 293 | self.test_MQTTClient = mqtt_connection_builder.mtls_from_path( 294 | endpoint=self.iot_endpoint, 295 | cert_filepath="{}/{}".format(self.secure_cert_path, self.new_cert_name), 296 | pri_key_filepath="{}/{}".format(self.secure_cert_path, self.new_key_name), 297 | client_bootstrap=client_bootstrap, 298 | ca_filepath="{}/{}".format(self.secure_cert_path, self.root_cert), 299 | client_id=self.unique_id + "-Prod", 300 | clean_session=False, 301 | keep_alive_secs=6) 302 | 303 | print("Connecting with Prod certs to {} with client ID '{}'...".format(self.iot_endpoint, self.unique_id + "-Prod")) 304 | connect_future = self.test_MQTTClient.connect() 305 | # Future.result() waits until a result is available 306 | connect_future.result() 307 | print("Connected with Prod certs!") 308 | 309 | def basic_callback(self, topic, payload, **kwargs): 310 | print("Received message from topic '{}': {}".format(topic, payload)) 311 | self.message_payload = payload 312 | self.on_message_callback(payload) 313 | 314 | if topic == "openworld": 315 | # Finish the run successfully 316 | print("Successfully provisioned") 317 | self.callback_returned = True 318 | elif (topic == "$aws/provisioning-templates/{}/provision/json/rejected".format(self.template_name) or 319 | topic == "$aws/certificates/create/json/rejected"): 320 | print("Failed provisioning") 321 | self.callback_returned = True 322 | 323 | def new_cert_pub_sub(self): 324 | """Method testing a call to the 'openworld' topic (which was specified in the policy for the new certificate) 325 | """ 326 | 327 | new_cert_topic = "openworld" 328 | print("Subscribing to topic '{}'...".format(new_cert_topic)) 329 | mqtt_topic_subscribe_future, _ = self.test_MQTTClient.subscribe( 330 | topic=new_cert_topic, 331 | qos=mqtt.QoS.AT_LEAST_ONCE, 332 | callback=self.basic_callback) 333 | 334 | # Wait for subscription to succeed 335 | mqtt_topic_subscribe_result = mqtt_topic_subscribe_future.result() 336 | print("Subscribed with {}".format(str(mqtt_topic_subscribe_result['qos']))) 337 | 338 | self.test_MQTTClient.publish( 339 | topic="openworld", 340 | payload=json.dumps({"service_response": "##### RESPONSE FROM PREVIOUSLY FORBIDDEN TOPIC #####"}), 341 | qos=mqtt.QoS.AT_LEAST_ONCE) 342 | 343 | 344 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asyncio==3.4.3 2 | pyfiglet==0.8.post1 3 | awsiotsdk==1.7.0 4 | 5 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-fleet-provisioning/0759985e1c2ddeee5a2f713f2017566f0783512e/utils/__init__.py -------------------------------------------------------------------------------- /utils/config_loader.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | # ----------------------------------------- 5 | 6 | from configparser import ConfigParser 7 | 8 | 9 | class Config: 10 | def __init__(self, config_file_path): 11 | self.cf = ConfigParser() 12 | self.cf.optionxform = str 13 | self.config_file_path = config_file_path 14 | self.cf.read(self.config_file_path) 15 | 16 | def get_section(self, section): 17 | return dict(self.cf.items(section)) 18 | --------------------------------------------------------------------------------