diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8de9f99e..7c07625c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,7 @@ env: CI_IOT_CONTAINERS_ROLE: ${{ secrets.AWS_CI_IOT_CONTAINERS }} CI_PUBSUB_ROLE: ${{ secrets.AWS_CI_PUBSUB_ROLE }} CI_COGNITO_ROLE: ${{ secrets.AWS_CI_COGNITO_ROLE }} + CI_X509_ROLE: ${{ secrets.AWS_CI_X509_ROLE }} CI_CUSTOM_AUTHORIZER_ROLE: ${{ secrets.AWS_CI_CUSTOM_AUTHORIZER_ROLE }} CI_SHADOW_ROLE: ${{ secrets.AWS_CI_SHADOW_ROLE }} CI_JOBS_ROLE: ${{ secrets.AWS_CI_JOBS_ROLE }} @@ -253,6 +254,14 @@ jobs: - name: run Cognito Connect sample run: | python3 ${{ env.CI_UTILS_FOLDER }}/run_sample_ci.py --file ${{ env.CI_SAMPLES_CFG_FOLDER }}/ci_run_cognito_connect_cfg.json + - name: configure AWS credentials (X509) + uses: aws-actions/configure-aws-credentials@v1 + with: + role-to-assume: ${{ env.CI_X509_ROLE }} + aws-region: ${{ env.AWS_DEFAULT_REGION }} + - name: run X509 sample + run: | + python3 ${{ env.CI_UTILS_FOLDER }}/run_sample_ci.py --file ${{ env.CI_SAMPLES_CFG_FOLDER }}/ci_run_x509_connect_cfg.json - name: configure AWS credentials (MQTT5 samples) uses: aws-actions/configure-aws-credentials@v1 with: diff --git a/.github/workflows/ci_run_x509_connect_cfg.json b/.github/workflows/ci_run_x509_connect_cfg.json new file mode 100644 index 00000000..ac64d384 --- /dev/null +++ b/.github/workflows/ci_run_x509_connect_cfg.json @@ -0,0 +1,38 @@ +{ + "language": "Python", + "sample_file": "./aws-iot-device-sdk-python-v2/samples/x509_connect.py", + "sample_region": "us-east-1", + "sample_main_class": "", + "arguments": [ + { + "name": "--endpoint", + "secret": "ci/endpoint" + }, + { + "name": "--x509_cert", + "secret": "ci/PubSub/cert", + "filename": "tmp_certificate.pem" + }, + { + "name": "--x509_key", + "secret": "ci/PubSub/key", + "filename": "tmp_key.pem" + }, + { + "name": "--x509_endpoint", + "secret": "ci/X509/endpoint_credentials" + }, + { + "name": "--x509_role_alias", + "secret": "ci/X509/alias" + }, + { + "name": "--signing_region", + "data": "us-east-1" + }, + { + "name": "--x509_thing_name", + "data": "CI_PubSub_Thing" + } + ] +} diff --git a/samples/README.md b/samples/README.md index 4af8b24b..0ce36f7a 100644 --- a/samples/README.md +++ b/samples/README.md @@ -10,6 +10,7 @@ * [MQTT5 Custom Authorizer Connect](./mqtt5_custom_authorizer_connect.md) * [Custom Authorizer Connect](./custom_authorizer_connect.md) * [Cognito Connect](./cognito_connect.md) +* [X509 Connect](./x509_connect.md) * [Shadow](./shadow.md) * [Jobs](./jobs.md) * [Fleet Provisioning](./fleetprovisioning.md) diff --git a/samples/utils/command_line_utils.py b/samples/utils/command_line_utils.py index b622bdd8..46218237 100644 --- a/samples/utils/command_line_utils.py +++ b/samples/utils/command_line_utils.py @@ -143,6 +143,39 @@ def add_common_custom_authorizer_commands(self): "", "The password to send when connecting through a custom authorizer (optional)") + def add_common_x509_commands(self): + self.register_command( + self.m_cmd_x509_endpoint, + "", + "The credentials endpoint to fetch x509 credentials from", + ) + self.register_command( + self.m_cmd_x509_thing_name, + "", + "Thing name to fetch x509 credentials on behalf of" + ) + self.register_command( + self.m_cmd_x509_role_alias, + "", + "Role alias to use with the x509 credentials provider" + ) + self.register_command( + self.m_cmd_x509_key, + "", + "Path to the IoT thing private key used in fetching x509 credentials" + ) + self.register_command( + self.m_cmd_x509_cert, + "", + "Path to the IoT thing certificate used in fetching x509 credentials" + ) + + self.register_command( + self.m_cmd_x509_ca, + "", + "Path to the root certificate used in fetching x509 credentials" + ) + """ Returns the command if it exists and has been passed to the console, otherwise it will print the help for the sample and exit the application. """ @@ -400,3 +433,9 @@ def build_mqtt5_client(self, m_cmd_custom_auth_authorizer_signature = "custom_auth_authorizer_signature" m_cmd_custom_auth_password = "custom_auth_password" m_cmd_cognito_identity = "cognito_identity" + m_cmd_x509_endpoint = "x509_endpoint" + m_cmd_x509_thing_name = "x509_thing_name" + m_cmd_x509_role_alias = "x509_role_alias" + m_cmd_x509_cert = "x509_cert" + m_cmd_x509_key = "x509_key" + m_cmd_x509_ca = "x509_ca_file" diff --git a/samples/x509_connect.md b/samples/x509_connect.md new file mode 100644 index 00000000..c1a0a5c2 --- /dev/null +++ b/samples/x509_connect.md @@ -0,0 +1,59 @@ +# x509 Credentials Provider Connect + +[**Return to main sample list**](./README.md) + +This sample is similar to the [Basic Connect](./basic_connect.md), but the connection uses a X.509 certificate +to source the AWS credentials when connecting. + +See the [Authorizing direct calls to AWS services using AWS IoT Core credential provider](https://docs.aws.amazon.com/iot/latest/developerguide/authorizing-direct-aws.html) page for instructions on how to setup the IAM roles, the trust policy for the IAM roles, how to setup the IoT Core Role alias, and how to get the credential provider endpoint for your AWS account. + +Your IoT Core Thing's [Policy](https://docs.aws.amazon.com/iot/latest/developerguide/iot-policies.html) must provide privileges for this sample to connect. Below is a sample policy that can be used on your IoT Core Thing that will allow this sample to run as intended. + +
+(see sample policy) +
+{
+  "Version": "2012-10-17",
+  "Statement": [
+    {
+      "Effect": "Allow",
+      "Action": [
+        "iot:Connect"
+      ],
+      "Resource": [
+        "arn:aws:iot:region:account:client/test-*"
+      ]
+    },
+    {
+      "Effect":"Allow",
+      "Action":"iot:AssumeRoleWithCertificate",
+      "Resource":"arn:aws:iot:region:account:rolealias/role-alias"
+    }
+  ]
+}
+
+ +Replace with the following with the data from your AWS account: +* ``: The AWS IoT Core region where you created your AWS IoT Core thing you wish to use with this sample. For example `us-east-1`. +* ``: Your AWS IoT Core account ID. This is the set of numbers in the top right next to your AWS account name when using the AWS IoT Core website. +* ``: The X509 role alias you created and wish to connect using. + +Note that in a real application, you may want to avoid the use of wildcards in your ClientID or use them selectively. Please follow best practices when working with AWS on production applications using the SDK. Also, for the purposes of this sample, please make sure your policy allows a client ID of `test-*` to connect or use `--client_id ` to send the client ID your policy supports. + +
+ +## How to run + +To run the x509 Credentials Provider Connect sample use the following command: + +``` sh +# For Windows: replace 'python3' with 'python' and '/' with '\' +python3 x509_connect.py --endpoint --signing_region --x509_cert --x509_endpoint --x509_key --x509_role_alias -x509_thing_name +``` + +You can also pass a Certificate Authority file (CA) if your X509 certificate and key combination requires it: + +``` sh +# For Windows: replace 'python3' with 'python' and '/' with '\' +python3 x509_connect.py --endpoint --signing_region --x509_cert --x509_endpoint --x509_key --x509_role_alias -x509_thing_name --x509_ca_file +``` diff --git a/samples/x509_connect.py b/samples/x509_connect.py new file mode 100644 index 00000000..7bea2bfd --- /dev/null +++ b/samples/x509_connect.py @@ -0,0 +1,116 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0. + +from awscrt import io, http, auth +from uuid import uuid4 +from awsiot import mqtt_connection_builder + +# This sample shows how to create a MQTT connection using X509 files to connect. +# This sample is intended to be used as a reference for making MQTT connections via X509. + +# Parse arguments +import utils.command_line_utils as command_line_utils +cmdUtils = command_line_utils.CommandLineUtils("X509 Connect - Make a MQTT connection using X509.") +cmdUtils.add_common_mqtt_commands() +cmdUtils.add_common_proxy_commands() +cmdUtils.add_common_logging_commands() +cmdUtils.add_common_x509_commands() +cmdUtils.register_command("signing_region", "", + "The signing region used for the websocket signer", + True, str) +cmdUtils.register_command("client_id", "", + "Client ID to use for MQTT connection (optional, default='test-*').", + default="test-" + str(uuid4())) +cmdUtils.register_command("is_ci", "", "If present the sample will run in CI mode (optional, default='None')") +# Needs to be called so the command utils parse the commands +cmdUtils.get_args() +is_ci = cmdUtils.get_command("is_ci", None) is not None + +# Callback when connection is accidentally lost. +def on_connection_interrupted(connection, error, **kwargs): + print(f"Connection interrupted. error: {error}") + +# Callback when an interrupted connection is re-established. +def on_connection_resumed(connection, return_code, session_present, **kwargs): + print(f"Connection resumed. return_code: {return_code} session_present: {session_present}") + + +if __name__ == '__main__': + + ############################################################ + # Pull data from the command line + ############################################################ + input_endpoint = cmdUtils.get_command_required("endpoint") + input_signing_region = cmdUtils.get_command_required("signing_region") + input_ca_file = cmdUtils.get_command("ca_file") + input_client_id = cmdUtils.get_command_required("client_id") + + input_proxy_host = cmdUtils.get_command("proxy_host") + input_proxy_port = cmdUtils.get_command("proxy_port") + + input_x509_endpoint = cmdUtils.get_command_required("x509_endpoint") + input_x509_thing_name = cmdUtils.get_command_required("x509_thing_name") + input_x509_role_alias = cmdUtils.get_command_required("x509_role_alias") + input_x509_cert = cmdUtils.get_command_required("x509_cert") + input_x509_key = cmdUtils.get_command_required("x509_key") + input_x509_ca_file = cmdUtils.get_command("x509_ca_file") + + ############################################################ + # Set up and create the MQTT connection + ############################################################ + + # Set up the config needed to make a MQTT connection + + proxy_options = None + if input_proxy_host is not None and input_proxy_port is not None: + proxy_options = http.HttpProxyOptions( + host_name=input_proxy_host, + port=input_proxy_port) + + x509_tls_options = io.TlsContextOptions.create_client_with_mtls_from_path(input_x509_cert, input_x509_key) + x509_tls_options.ca_dirpath = input_x509_ca_file + x509_tls_context = io.ClientTlsContext(x509_tls_options) + + x509_provider = auth.AwsCredentialsProvider.new_x509( + endpoint=input_x509_endpoint, + thing_name=input_x509_thing_name, + role_alias=input_x509_role_alias, + tls_ctx=x509_tls_context, + http_proxy_options=proxy_options + ) + + # Create the MQTT connection from the configuration + + mqtt_connection = mqtt_connection_builder.websockets_with_default_aws_signing( + endpoint=input_endpoint, + region=input_signing_region, + credentials_provider=x509_provider, + http_proxy_options=proxy_options, + ca_filepath=input_ca_file, + on_connection_interrupted=on_connection_interrupted, + on_connection_resumed=on_connection_resumed, + client_id=input_client_id, + clean_session=False, + keep_alive_secs=30) + + ############################################################ + # Use the MQTT connection to connect and disconnect + ############################################################ + + if not is_ci: + print (f"Connecting to {input_endpoint} with client ID '{input_client_id}'...") + else: + print("Connecting to endpoint with client ID") + + # Connect + connect_future = mqtt_connection.connect() + + # Future.result() waits until a result is available + connect_future.result() + print("Connected!") + + # Disconnect + print("Disconnecting...") + disconnect_future = mqtt_connection.disconnect() + disconnect_future.result() + print("Disconnected!") diff --git a/setup.py b/setup.py index 78279ecb..8e6cfd06 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ def _load_version(): "Operating System :: OS Independent", ], install_requires=[ - 'awscrt==0.16.10', + 'awscrt==0.16.13', ], python_requires='>=3.7', )