From 53feb938922a669b7b0df76e9221d33e79f28397 Mon Sep 17 00:00:00 2001 From: Tim Kellogg Date: Mon, 19 Oct 2015 08:54:11 -0700 Subject: [PATCH 1/4] Added aws iot scaffold-certs command --- awscli/customizations/iot/__init__.py | 212 ++++++++++++++++++++ awscli/handlers.py | 2 + tests/functional/iot/test_generate_certs.py | 8 + 3 files changed, 222 insertions(+) create mode 100644 awscli/customizations/iot/__init__.py create mode 100644 tests/functional/iot/test_generate_certs.py diff --git a/awscli/customizations/iot/__init__.py b/awscli/customizations/iot/__init__.py new file mode 100644 index 000000000000..3ead18af2f87 --- /dev/null +++ b/awscli/customizations/iot/__init__.py @@ -0,0 +1,212 @@ +import os, platform, stat +from awscli.customizations.commands import BasicCommand + +ROOTCA = """\ +-----BEGIN CERTIFICATE----- +MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW +ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW +ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln +biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp +U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y +aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1 +nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex +t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz +SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG +BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+ +rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/ +NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E +BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH +BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy +aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv +MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE +p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y +5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK +WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ +4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N +hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq +-----END CERTIFICATE----- +""" + +DEFAULT_POLICY = """\ +{ + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Action":["iot:*"], + "Resource": ["*"] + }] +} +""" + +def register_customizations(cli): + cli.register('building-command-table.iot', register_commands) + + +def register_commands(command_table, session, **kwargs): + command_table['scaffold-certs'] = GenerateCertsCommand(session) + +class GenerateCertsCommand(BasicCommand): + NAME = 'scaffold-certs' + DESCRIPTION = ( + 'Generate certificates and encryption keys as well as two scripts ' + 'that act as thin wrappers around mosquitto_pub and mosquitto_sub ' + 'to quickly get started with MQTT. This command generates several ' + 'files on your computer, so specify --base-dir to make them go ' + 'somewhere other than right here.') + ARG_TABLE = [ + {'name': 'base-dir', 'help_text': ( + 'The directory on your computer where to generate all these files. ' + 'The default is current working directory'), + 'action': 'store', 'required': False, 'cli_type_name': 'string', }, + {'name': 'policy-name', + 'help_text': ( + 'The name of the policy that enables the new device to publish and ' + 'subscribe over MQTT. Default value is "PubSubToAnyTopic"'), + 'action': 'store', 'required': False}, + ] + + def __init__(self, session): + super(GenerateCertsCommand, self).__init__(session) + + def _run_main(self, parsed_args, parsed_globals, **kwargs): + # We need region for writing publish.sh & subscribe.sh files later + if parsed_globals.region: + self._region = parsed_globals.region + else: + profile = parsed_globals.profile or 'default' + if profile in self._session.full_config['profiles']: + profile_config = self._session.full_config['profiles'][profile] + if 'region' in profile_config: + self._region = profile_config['region'] + else: + raise ArgumentError("Specified profile '{0}' not found".format(profile)) + + if not self._region: + self._region = 'us-east-1' + + self._set_client(parsed_globals) + self._process_args(parsed_args) + self._generate_certs(parsed_args) + + self._print_instructions() + + # This is to make the functional test pass. Maybe we can write a better test? + return 0 + + def _set_client(self, parsed_globals): + + # This is called from _run_main and is used to ensure that we have + # a service/endpoint object to work with. + self.client = self._session.create_client('iot', + region_name = self._region, + endpoint_url = parsed_globals.endpoint_url, + verify = parsed_globals.verify_ssl) + + def _process_args(self, parsed_args): + self._base_dir = os.path.abspath(parsed_args.base_dir or '.') + self._policy_name = parsed_args.policy_name or 'PubSubToAnyTopic' + self._policy_document = DEFAULT_POLICY + + def _generate_certs(self, parsed_args): + + # Ensure that the destination exists + if not os.path.exists(self._base_dir): + os.makedirs(self._base_dir) + + certs = self.client.create_keys_and_certificate(setAsActive = True) + + self._write_file('certificate.pem', certs['certificatePem']) + self._write_file('certificate-arn.txt', certs['certificateArn']) + self._write_file('certificate-id.txt', certs['certificateId']) + self._write_file('public.pem', certs['keyPair']['PublicKey']) + self._write_file('private.pem', certs['keyPair']['PrivateKey']) + + self.client.create_policy(policyName = self._policy_name, + policyDocument = self._policy_document) + + self.client.attach_principal_policy(principal = certs['certificateArn'], + policyName = self._policy_name) + + self._write_file('rootCA.pem', ROOTCA) + self._write_publish() + self._write_subscribe() + + def _write_file(self, fname, value): + path = os.path.join(self._base_dir, fname) + handle = open(path, 'w') + handle.write(value) + handle.close() + + def _write_publish(self): + self._write_script_file('publish', 'mosquitto_pub') + + def _write_subscribe(self): + self._write_script_file('subscribe', 'mosquitto_sub') + + def _write_script_file(self, name, cmd): + if platform.uname()[0] == 'Windows': + ext = '.bat' + all_args = "%*" + prelude = "" + else: + ext = '.sh' + all_args = '"$@"' + prelude = "#!/bin/sh\n\n" + + # We use this in _print_instructions + self._ext = ext + + params = ( + " --cafile \"{0}{2}rootCA.pem\" " + "--cert \"{0}{2}cert.pem\" " + "--key \"{0}{2}private.pem\" " + "-h data.iot.{1}.amazonaws.com " + "-p 8883 ").format(self._base_dir, self._region, os.sep) + + fname = name + ext + content = prelude + cmd + params + all_args + + self._write_file(fname, content) + + st = os.stat(fname) + os.chmod(fname, st.st_mode | stat.S_IEXEC) + + def _print_instructions(self): + msg = ( + 'Welcome! You are now ready to publish and subscribe to MQTT topics. ' + 'We have written several files to "{0}":\n' + '\n' + ' * certificate.pem - the X.509 certificate that represents the ' + 'virtual device that was just registered.\n' + ' * certificate-arn.txt - contains just the ARN of the certificate, ' + 'so you don\'t forget.\n' + ' * certificate-id.txt - contains just the ID of the certificate, ' + 'so you don\'t forget.\n' + ' * public.pem - the public key.\n' + ' * private.pem - the private key.\n' + ' * publish{1} - a script to help you publish to MQTT topics.\n' + ' * subscribe{1} - a script to help you subscribe to MQTT topics.\n' + '\n' + 'The publish{1} and subscribe{1} scripts are just thin wrappers ' + 'around mosquitto_pub and mosquitto_sub, respectively. Mosquitto is ' + 'a popular MQTT command-line client. You can absolutely use these ' + 'scripts as a guide to use any other MQTT client that you want.\n' + '\n' + 'Now you\'re ready to get started. Try running this command:\n' + '\n' + ' publish{1} -t some/topic -m "AWS IoT is fun!" -d\n' + '\n' + 'At any point, use the --help option on publish{1} or subscribe{1} ' + 'to get a full listing of options that mosquitto_pub and/or mosquitto_sub ' + 'supports.') + + msg = msg.format(self._base_dir, self._ext) + for line in msg.splitlines(): + print line + + diff --git a/awscli/handlers.py b/awscli/handlers.py index 9f44d00b3532..d910f639bcc2 100644 --- a/awscli/handlers.py +++ b/awscli/handlers.py @@ -66,6 +66,7 @@ from awscli.customizations.route53 import register_create_hosted_zone_doc_fix from awscli.customizations.codecommit import initialize as codecommit_init from awscli.customizations.iot_data import register_custom_endpoint_note +from awscli.customizations import iot def awscli_initialize(event_handlers): @@ -133,3 +134,4 @@ def awscli_initialize(event_handlers): register_modify_put_configuration_recorder(event_handlers) codecommit_init(event_handlers) register_custom_endpoint_note(event_handlers) + iot.register_customizations(event_handlers) diff --git a/tests/functional/iot/test_generate_certs.py b/tests/functional/iot/test_generate_certs.py new file mode 100644 index 000000000000..32737091c5a5 --- /dev/null +++ b/tests/functional/iot/test_generate_certs.py @@ -0,0 +1,8 @@ +from awscli.testutils import BaseAWSCommandParamsTest + +class TestGenerateCerts(BaseAWSCommandParamsTest): + prefix = 'iot generate-certs ' + + def test_cmd_args(self): + cmdline = self.prefix + '--policy-name Pub2DaSub --base-dir /tmp/foosball' + self.assert_params_for_cmd(cmdline) From b59fe4223a5812cbdd703b40c92a57987ab8c8c3 Mon Sep 17 00:00:00 2001 From: Tim Kellogg Date: Mon, 19 Oct 2015 09:52:02 -0700 Subject: [PATCH 2/4] Fixed bug in instructions ('!' is special) --- awscli/customizations/iot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awscli/customizations/iot/__init__.py b/awscli/customizations/iot/__init__.py index 3ead18af2f87..f3bc4c36230f 100644 --- a/awscli/customizations/iot/__init__.py +++ b/awscli/customizations/iot/__init__.py @@ -199,7 +199,7 @@ def _print_instructions(self): '\n' 'Now you\'re ready to get started. Try running this command:\n' '\n' - ' publish{1} -t some/topic -m "AWS IoT is fun!" -d\n' + ' publish{1} -t some/topic -m "AWS IoT is fun" -d\n' '\n' 'At any point, use the --help option on publish{1} or subscribe{1} ' 'to get a full listing of options that mosquitto_pub and/or mosquitto_sub ' From b554f34df6cfe401cb9a96e2fc132c3000d82112 Mon Sep 17 00:00:00 2001 From: Tim Kellogg Date: Tue, 27 Oct 2015 14:11:52 -0700 Subject: [PATCH 3/4] Added Mosquitto attribution & license headers --- awscli/customizations/iot/__init__.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/awscli/customizations/iot/__init__.py b/awscli/customizations/iot/__init__.py index f3bc4c36230f..5d5f1c4eddf7 100644 --- a/awscli/customizations/iot/__init__.py +++ b/awscli/customizations/iot/__init__.py @@ -1,3 +1,15 @@ +# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. import os, platform, stat from awscli.customizations.commands import BasicCommand @@ -193,9 +205,10 @@ def _print_instructions(self): ' * subscribe{1} - a script to help you subscribe to MQTT topics.\n' '\n' 'The publish{1} and subscribe{1} scripts are just thin wrappers ' - 'around mosquitto_pub and mosquitto_sub, respectively. Mosquitto is ' - 'a popular MQTT command-line client. You can absolutely use these ' - 'scripts as a guide to use any other MQTT client that you want.\n' + 'around mosquitto_pub and mosquitto_sub, respectively. Eclipse ' + 'Mosquitto (http://www.eclipse.org/mosquitto/) is a popular MQTT ' + 'command-line client. You can absolutely use these scripts as a ' + 'guide to use any other MQTT client that you want.\n' '\n' 'Now you\'re ready to get started. Try running this command:\n' '\n' From 0f2237d8f0114f2d44dd5a2b59ba5de7cc1341c3 Mon Sep 17 00:00:00 2001 From: Tim Kellogg Date: Tue, 27 Oct 2015 16:22:43 -0700 Subject: [PATCH 4/4] Download the rootCA.pem from VeriSign --- awscli/customizations/iot/__init__.py | 40 +++++++-------------------- 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/awscli/customizations/iot/__init__.py b/awscli/customizations/iot/__init__.py index 5d5f1c4eddf7..89ad43075dc2 100644 --- a/awscli/customizations/iot/__init__.py +++ b/awscli/customizations/iot/__init__.py @@ -10,39 +10,19 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. + +""" +This module adds the `aws iot scaffold-certs` subcommand that glues a +few API calls together to quickly get a working command line MQTT client +for an AWS account & region. +""" + import os, platform, stat from awscli.customizations.commands import BasicCommand -ROOTCA = """\ ------BEGIN CERTIFICATE----- -MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB -yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL -ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp -U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW -ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 -aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL -MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW -ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln -biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp -U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y -aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1 -nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex -t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz -SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG -BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+ -rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/ -NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E -BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH -BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy -aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv -MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE -p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y -5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK -WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ -4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N -hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq ------END CERTIFICATE----- -""" +# The VeriSign rootCA.pem file can't be packaged here due to distribution +# restrictions. Therefore, we download it on the user's behalf. +ROOTCA = "$(curl http://www.symantec.com/content/en/us/enterprise/verisign/roots/VeriSign-Class%203-Public-Primary-Certification-Authority-G5.pem)" DEFAULT_POLICY = """\ {