Skip to content

Add X509 sample #418

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Mar 17, 2023
Merged
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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:
Expand Down
38 changes: 38 additions & 0 deletions .github/workflows/ci_run_x509_connect_cfg.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
1 change: 1 addition & 0 deletions samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
69 changes: 69 additions & 0 deletions samples/utils/command_line_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,39 @@ def add_common_custom_authorizer_commands(self):
"<str>",
"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,
"<str>",
"The credentials endpoint to fetch x509 credentials from",
)
self.register_command(
self.m_cmd_x509_thing_name,
"<str>",
"Thing name to fetch x509 credentials on behalf of"
)
self.register_command(
self.m_cmd_x509_role_alias,
"<str>",
"Role alias to use with the x509 credentials provider"
)
self.register_command(
self.m_cmd_x509_key,
"<path>",
"Path to the IoT thing private key used in fetching x509 credentials"
)
self.register_command(
self.m_cmd_x509_cert,
"<path>",
"Path to the IoT thing certificate used in fetching x509 credentials"
)

self.register_command(
self.m_cmd_x509_ca,
"<path>",
"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.
"""
Expand Down Expand Up @@ -211,6 +244,36 @@ def build_websocket_mqtt_connection(self, on_connection_interrupted, on_connecti
keep_alive_secs=30)
return mqtt_connection

def build_websocket_x509_mqtt_connection(self, on_connection_interrupted, on_connection_resumed):
proxy_options = self.get_proxy_options_for_mqtt_connection()

x509_tls_options = io.TlsContextOptions.create_client_with_mtls_from_path(
self.get_command_required(self.m_cmd_x509_cert),
self.get_command_required(self.m_cmd_x509_key))
if (self.get_command(self.m_cmd_x509_ca) != None):
x509_tls_options.ca_dirpath = self.get_command(self.m_cmd_x509_ca)
x509_tls_context = io.ClientTlsContext(x509_tls_options)

x509_provider = auth.AwsCredentialsProvider.new_x509(
endpoint=self.get_command_required(self.m_cmd_x509_endpoint),
thing_name=self.get_command_required(self.m_cmd_x509_thing_name),
role_alias=self.get_command_required(self.m_cmd_x509_role_alias),
tls_ctx=x509_tls_context,
http_proxy_options=proxy_options
)
mqtt_connection = mqtt_connection_builder.websockets_with_default_aws_signing(
endpoint=self.get_command_required(self.m_cmd_endpoint),
region=self.get_command_required(self.m_cmd_signing_region),
credentials_provider=x509_provider,
http_proxy_options=proxy_options,
ca_filepath=self.get_command(self.m_cmd_ca_file),
on_connection_interrupted=on_connection_interrupted,
on_connection_resumed=on_connection_resumed,
client_id=self.get_command_required("client_id"),
clean_session=False,
keep_alive_secs=30)
return mqtt_connection

def build_cognito_mqtt_connection(self, on_connection_interrupted, on_connection_resumed):
proxy_options = self.get_proxy_options_for_mqtt_connection()

Expand Down Expand Up @@ -400,3 +463,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"
59 changes: 59 additions & 0 deletions samples/x509_connect.md
Original file line number Diff line number Diff line change
@@ -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.

<details>
<summary>(see sample policy)</summary>
<pre>
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"iot:Connect"
],
"Resource": [
"arn:aws:iot:<b>region</b>:<b>account</b>:client/test-*"
]
},
{
"Effect":"Allow",
"Action":"iot:AssumeRoleWithCertificate",
"Resource":"arn:aws:iot:<b>region</b>:<b>account</b>:rolealias/<b>role-alias</b>"
}
]
}
</pre>

Replace with the following with the data from your AWS account:
* `<region>`: 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`.
* `<account>`: 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.
* `<role-alias>`: 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 <client ID here>` to send the client ID your policy supports.

</details>

## 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 <endpoint> --signing_region <region> --x509_cert <path to x509 cert> --x509_endpoint <x509 credentials endpoint> --x509_key <path to x509 key> --x509_role_alias <alias> -x509_thing_name <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 <endpoint> --signing_region <region> --x509_cert <path to x509 cert> --x509_endpoint <x509 credentials endpoint> --x509_key <path to x509 key> --x509_role_alias <alias> -x509_thing_name <thing name> --x509_ca_file <path to x509 CA file>
```
59 changes: 59 additions & 0 deletions samples/x509_connect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0.

from awscrt import io
from uuid import uuid4

# 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("Basic Connect - Make a MQTT connection.")
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", "<str>",
"The signing region used for the websocket signer",
True, str)
cmdUtils.register_command("client_id", "<str>",
"Client ID to use for MQTT connection (optional, default='test-*').",
default="test-" + str(uuid4()))
cmdUtils.register_command("is_ci", "<str>", "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("Connection interrupted. error: {}".format(error))

# Callback when an interrupted connection is re-established.
def on_connection_resumed(connection, return_code, session_present, **kwargs):
print("Connection resumed. return_code: {} session_present: {}".format(return_code, session_present))


if __name__ == '__main__':
# Create a connection using X509 authentication to connect
# Note: The data for the connection is gotten from cmdUtils.
# (see build_websocket_x509_mqtt_connection for implementation)
mqtt_connection = cmdUtils.build_websocket_x509_mqtt_connection(on_connection_interrupted, on_connection_resumed)

if not is_ci:
print("Connecting to {} with client ID '{}'...".format(
cmdUtils.get_command(cmdUtils.m_cmd_endpoint), cmdUtils.get_command("client_id")))
else:
print("Connecting to endpoint with client ID")

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!")
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
)