Skip to content

Commit a7e5d59

Browse files
TwistedTwiglegbretambrose
authored andcommitted
Add X509 sample (#418)
1 parent 997320e commit a7e5d59

File tree

7 files changed

+263
-1
lines changed

7 files changed

+263
-1
lines changed

.github/workflows/ci.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ env:
2424
CI_IOT_CONTAINERS_ROLE: ${{ secrets.AWS_CI_IOT_CONTAINERS }}
2525
CI_PUBSUB_ROLE: ${{ secrets.AWS_CI_PUBSUB_ROLE }}
2626
CI_COGNITO_ROLE: ${{ secrets.AWS_CI_COGNITO_ROLE }}
27+
CI_X509_ROLE: ${{ secrets.AWS_CI_X509_ROLE }}
2728
CI_CUSTOM_AUTHORIZER_ROLE: ${{ secrets.AWS_CI_CUSTOM_AUTHORIZER_ROLE }}
2829
CI_SHADOW_ROLE: ${{ secrets.AWS_CI_SHADOW_ROLE }}
2930
CI_JOBS_ROLE: ${{ secrets.AWS_CI_JOBS_ROLE }}
@@ -253,6 +254,14 @@ jobs:
253254
- name: run Cognito Connect sample
254255
run: |
255256
python3 ${{ env.CI_UTILS_FOLDER }}/run_sample_ci.py --file ${{ env.CI_SAMPLES_CFG_FOLDER }}/ci_run_cognito_connect_cfg.json
257+
- name: configure AWS credentials (X509)
258+
uses: aws-actions/configure-aws-credentials@v1
259+
with:
260+
role-to-assume: ${{ env.CI_X509_ROLE }}
261+
aws-region: ${{ env.AWS_DEFAULT_REGION }}
262+
- name: run X509 sample
263+
run: |
264+
python3 ${{ env.CI_UTILS_FOLDER }}/run_sample_ci.py --file ${{ env.CI_SAMPLES_CFG_FOLDER }}/ci_run_x509_connect_cfg.json
256265
- name: configure AWS credentials (MQTT5 samples)
257266
uses: aws-actions/configure-aws-credentials@v1
258267
with:
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"language": "Python",
3+
"sample_file": "./aws-iot-device-sdk-python-v2/samples/x509_connect.py",
4+
"sample_region": "us-east-1",
5+
"sample_main_class": "",
6+
"arguments": [
7+
{
8+
"name": "--endpoint",
9+
"secret": "ci/endpoint"
10+
},
11+
{
12+
"name": "--x509_cert",
13+
"secret": "ci/PubSub/cert",
14+
"filename": "tmp_certificate.pem"
15+
},
16+
{
17+
"name": "--x509_key",
18+
"secret": "ci/PubSub/key",
19+
"filename": "tmp_key.pem"
20+
},
21+
{
22+
"name": "--x509_endpoint",
23+
"secret": "ci/X509/endpoint_credentials"
24+
},
25+
{
26+
"name": "--x509_role_alias",
27+
"secret": "ci/X509/alias"
28+
},
29+
{
30+
"name": "--signing_region",
31+
"data": "us-east-1"
32+
},
33+
{
34+
"name": "--x509_thing_name",
35+
"data": "CI_PubSub_Thing"
36+
}
37+
]
38+
}

samples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* [MQTT5 Custom Authorizer Connect](./mqtt5_custom_authorizer_connect.md)
1111
* [Custom Authorizer Connect](./custom_authorizer_connect.md)
1212
* [Cognito Connect](./cognito_connect.md)
13+
* [X509 Connect](./x509_connect.md)
1314
* [Shadow](./shadow.md)
1415
* [Jobs](./jobs.md)
1516
* [Fleet Provisioning](./fleetprovisioning.md)

samples/utils/command_line_utils.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,39 @@ def add_common_custom_authorizer_commands(self):
143143
"<str>",
144144
"The password to send when connecting through a custom authorizer (optional)")
145145

146+
def add_common_x509_commands(self):
147+
self.register_command(
148+
self.m_cmd_x509_endpoint,
149+
"<str>",
150+
"The credentials endpoint to fetch x509 credentials from",
151+
)
152+
self.register_command(
153+
self.m_cmd_x509_thing_name,
154+
"<str>",
155+
"Thing name to fetch x509 credentials on behalf of"
156+
)
157+
self.register_command(
158+
self.m_cmd_x509_role_alias,
159+
"<str>",
160+
"Role alias to use with the x509 credentials provider"
161+
)
162+
self.register_command(
163+
self.m_cmd_x509_key,
164+
"<path>",
165+
"Path to the IoT thing private key used in fetching x509 credentials"
166+
)
167+
self.register_command(
168+
self.m_cmd_x509_cert,
169+
"<path>",
170+
"Path to the IoT thing certificate used in fetching x509 credentials"
171+
)
172+
173+
self.register_command(
174+
self.m_cmd_x509_ca,
175+
"<path>",
176+
"Path to the root certificate used in fetching x509 credentials"
177+
)
178+
146179
"""
147180
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.
148181
"""
@@ -400,3 +433,9 @@ def build_mqtt5_client(self,
400433
m_cmd_custom_auth_authorizer_signature = "custom_auth_authorizer_signature"
401434
m_cmd_custom_auth_password = "custom_auth_password"
402435
m_cmd_cognito_identity = "cognito_identity"
436+
m_cmd_x509_endpoint = "x509_endpoint"
437+
m_cmd_x509_thing_name = "x509_thing_name"
438+
m_cmd_x509_role_alias = "x509_role_alias"
439+
m_cmd_x509_cert = "x509_cert"
440+
m_cmd_x509_key = "x509_key"
441+
m_cmd_x509_ca = "x509_ca_file"

samples/x509_connect.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# x509 Credentials Provider Connect
2+
3+
[**Return to main sample list**](./README.md)
4+
5+
This sample is similar to the [Basic Connect](./basic_connect.md), but the connection uses a X.509 certificate
6+
to source the AWS credentials when connecting.
7+
8+
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.
9+
10+
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.
11+
12+
<details>
13+
<summary>(see sample policy)</summary>
14+
<pre>
15+
{
16+
"Version": "2012-10-17",
17+
"Statement": [
18+
{
19+
"Effect": "Allow",
20+
"Action": [
21+
"iot:Connect"
22+
],
23+
"Resource": [
24+
"arn:aws:iot:<b>region</b>:<b>account</b>:client/test-*"
25+
]
26+
},
27+
{
28+
"Effect":"Allow",
29+
"Action":"iot:AssumeRoleWithCertificate",
30+
"Resource":"arn:aws:iot:<b>region</b>:<b>account</b>:rolealias/<b>role-alias</b>"
31+
}
32+
]
33+
}
34+
</pre>
35+
36+
Replace with the following with the data from your AWS account:
37+
* `<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`.
38+
* `<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.
39+
* `<role-alias>`: The X509 role alias you created and wish to connect using.
40+
41+
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.
42+
43+
</details>
44+
45+
## How to run
46+
47+
To run the x509 Credentials Provider Connect sample use the following command:
48+
49+
``` sh
50+
# For Windows: replace 'python3' with 'python' and '/' with '\'
51+
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>
52+
```
53+
54+
You can also pass a Certificate Authority file (CA) if your X509 certificate and key combination requires it:
55+
56+
``` sh
57+
# For Windows: replace 'python3' with 'python' and '/' with '\'
58+
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+
```

samples/x509_connect.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0.
3+
4+
from awscrt import io, http, auth
5+
from uuid import uuid4
6+
from awsiot import mqtt_connection_builder
7+
8+
# This sample shows how to create a MQTT connection using X509 files to connect.
9+
# This sample is intended to be used as a reference for making MQTT connections via X509.
10+
11+
# Parse arguments
12+
import utils.command_line_utils as command_line_utils
13+
cmdUtils = command_line_utils.CommandLineUtils("X509 Connect - Make a MQTT connection using X509.")
14+
cmdUtils.add_common_mqtt_commands()
15+
cmdUtils.add_common_proxy_commands()
16+
cmdUtils.add_common_logging_commands()
17+
cmdUtils.add_common_x509_commands()
18+
cmdUtils.register_command("signing_region", "<str>",
19+
"The signing region used for the websocket signer",
20+
True, str)
21+
cmdUtils.register_command("client_id", "<str>",
22+
"Client ID to use for MQTT connection (optional, default='test-*').",
23+
default="test-" + str(uuid4()))
24+
cmdUtils.register_command("is_ci", "<str>", "If present the sample will run in CI mode (optional, default='None')")
25+
# Needs to be called so the command utils parse the commands
26+
cmdUtils.get_args()
27+
is_ci = cmdUtils.get_command("is_ci", None) is not None
28+
29+
# Callback when connection is accidentally lost.
30+
def on_connection_interrupted(connection, error, **kwargs):
31+
print(f"Connection interrupted. error: {error}")
32+
33+
# Callback when an interrupted connection is re-established.
34+
def on_connection_resumed(connection, return_code, session_present, **kwargs):
35+
print(f"Connection resumed. return_code: {return_code} session_present: {session_present}")
36+
37+
38+
if __name__ == '__main__':
39+
40+
############################################################
41+
# Pull data from the command line
42+
############################################################
43+
input_endpoint = cmdUtils.get_command_required("endpoint")
44+
input_signing_region = cmdUtils.get_command_required("signing_region")
45+
input_ca_file = cmdUtils.get_command("ca_file")
46+
input_client_id = cmdUtils.get_command_required("client_id")
47+
48+
input_proxy_host = cmdUtils.get_command("proxy_host")
49+
input_proxy_port = cmdUtils.get_command("proxy_port")
50+
51+
input_x509_endpoint = cmdUtils.get_command_required("x509_endpoint")
52+
input_x509_thing_name = cmdUtils.get_command_required("x509_thing_name")
53+
input_x509_role_alias = cmdUtils.get_command_required("x509_role_alias")
54+
input_x509_cert = cmdUtils.get_command_required("x509_cert")
55+
input_x509_key = cmdUtils.get_command_required("x509_key")
56+
input_x509_ca_file = cmdUtils.get_command("x509_ca_file")
57+
58+
############################################################
59+
# Set up and create the MQTT connection
60+
############################################################
61+
62+
# Set up the config needed to make a MQTT connection
63+
64+
proxy_options = None
65+
if input_proxy_host is not None and input_proxy_port is not None:
66+
proxy_options = http.HttpProxyOptions(
67+
host_name=input_proxy_host,
68+
port=input_proxy_port)
69+
70+
x509_tls_options = io.TlsContextOptions.create_client_with_mtls_from_path(input_x509_cert, input_x509_key)
71+
x509_tls_options.ca_dirpath = input_x509_ca_file
72+
x509_tls_context = io.ClientTlsContext(x509_tls_options)
73+
74+
x509_provider = auth.AwsCredentialsProvider.new_x509(
75+
endpoint=input_x509_endpoint,
76+
thing_name=input_x509_thing_name,
77+
role_alias=input_x509_role_alias,
78+
tls_ctx=x509_tls_context,
79+
http_proxy_options=proxy_options
80+
)
81+
82+
# Create the MQTT connection from the configuration
83+
84+
mqtt_connection = mqtt_connection_builder.websockets_with_default_aws_signing(
85+
endpoint=input_endpoint,
86+
region=input_signing_region,
87+
credentials_provider=x509_provider,
88+
http_proxy_options=proxy_options,
89+
ca_filepath=input_ca_file,
90+
on_connection_interrupted=on_connection_interrupted,
91+
on_connection_resumed=on_connection_resumed,
92+
client_id=input_client_id,
93+
clean_session=False,
94+
keep_alive_secs=30)
95+
96+
############################################################
97+
# Use the MQTT connection to connect and disconnect
98+
############################################################
99+
100+
if not is_ci:
101+
print (f"Connecting to {input_endpoint} with client ID '{input_client_id}'...")
102+
else:
103+
print("Connecting to endpoint with client ID")
104+
105+
# Connect
106+
connect_future = mqtt_connection.connect()
107+
108+
# Future.result() waits until a result is available
109+
connect_future.result()
110+
print("Connected!")
111+
112+
# Disconnect
113+
print("Disconnecting...")
114+
disconnect_future = mqtt_connection.disconnect()
115+
disconnect_future.result()
116+
print("Disconnected!")

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def _load_version():
4040
"Operating System :: OS Independent",
4141
],
4242
install_requires=[
43-
'awscrt==0.16.10',
43+
'awscrt==0.16.13',
4444
],
4545
python_requires='>=3.7',
4646
)

0 commit comments

Comments
 (0)