Skip to content

Commit 33f67d7

Browse files
PKCS12 connect sample (#445)
1 parent fde3572 commit 33f67d7

9 files changed

+258
-0
lines changed

Diff for: .github/workflows/ci.yml

+7
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ jobs:
109109
- name: run PubSub sample
110110
run: |
111111
python3 ${{ env.CI_UTILS_FOLDER }}/run_sample_ci.py --file ${{ env.CI_SAMPLES_CFG_FOLDER }}/ci_run_pubsub_cfg.json
112+
- name: run PKCS12 sample
113+
run: |
114+
cert=$(aws secretsmanager get-secret-value --region us-east-1 --secret-id "ci/PubSub/cert" --query "SecretString" | cut -f2 -d":" | cut -f2 -d\") && echo -e "$cert" > /tmp/certificate.pem
115+
key=$(aws secretsmanager get-secret-value --region us-east-1 --secret-id "ci/PubSub/key" --query "SecretString" | cut -f2 -d":" | cut -f2 -d\") && echo -e "$key" > /tmp/privatekey.pem
116+
pkcs12_password=$(aws secretsmanager get-secret-value --region us-east-1 --secret-id "ci/PubSub/key_pkcs12_password" --query "SecretString" | cut -f2 -d":" | cut -f2 -d\")
117+
openssl pkcs12 -export -in /tmp/certificate.pem -inkey /tmp/privatekey.pem -out ./pkcs12-key.p12 -name PubSub_Thing_Alias -password pass:$pkcs12_password
118+
python3 ${{ env.CI_UTILS_FOLDER }}/run_sample_ci.py --file ${{ env.CI_SAMPLES_CFG_FOLDER }}/ci_run_pkcs12_connect_cfg.json
112119
- name: configure AWS credentials (MQTT5 samples)
113120
uses: aws-actions/configure-aws-credentials@v1
114121
with:

Diff for: .github/workflows/ci_run_pkcs12_connect_cfg.json

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"language": "Python",
3+
"sample_file": "./aws-iot-device-sdk-python-v2/samples/pkcs12_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": "--pkcs12_file",
13+
"data": "./pkcs12-key.p12"
14+
},
15+
{
16+
"name": "--pkcs12_password",
17+
"secret": "ci/PubSub/key_pkcs12_password"
18+
}
19+
]
20+
}

Diff for: awsiot/mqtt5_client_builder.py

+25
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,31 @@ def mtls_with_pkcs11(*,
442442
cert_file_contents=cert_bytes)
443443
return _builder(tls_ctx_options, **kwargs)
444444

445+
def mtls_with_pkcs12(*,
446+
pkcs12_filepath: str,
447+
pkcs12_password: str,
448+
**kwargs) -> awscrt.mqtt.Connection:
449+
"""
450+
This builder creates an :class:`awscrt.mqtt.Connection`, configured for an mTLS MQTT connection to AWS IoT,
451+
using a PKCS#12 certificate.
452+
453+
NOTE: MacOS only
454+
455+
This function takes all :mod:`common arguments<awsiot.mqtt_connection_builder>`
456+
described at the top of this doc, as well as...
457+
458+
Args:
459+
pkcs12_filepath: Path to the PKCS12 file to use
460+
461+
pkcs12_password: The password for the PKCS12 file.
462+
"""
463+
_check_required_kwargs(**kwargs)
464+
465+
tls_ctx_options = awscrt.io.TlsContextOptions.create_client_with_mtls_pkcs12(
466+
pkcs12_filepath=pkcs12_filepath,
467+
pkcs12_password=pkcs12_password)
468+
return _builder(tls_ctx_options, **kwargs)
469+
445470

446471
def mtls_with_windows_cert_store_path(*,
447472
cert_store_path: str,

Diff for: awsiot/mqtt_connection_builder.py

+25
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,31 @@ def mtls_with_pkcs11(*,
325325

326326
return _builder(tls_ctx_options, **kwargs)
327327

328+
def mtls_with_pkcs12(*,
329+
pkcs12_filepath: str,
330+
pkcs12_password: str,
331+
**kwargs) -> awscrt.mqtt.Connection:
332+
"""
333+
This builder creates an :class:`awscrt.mqtt.Connection`, configured for an mTLS MQTT connection to AWS IoT,
334+
using a PKCS#12 certificate.
335+
336+
NOTE: MacOS only
337+
338+
This function takes all :mod:`common arguments<awsiot.mqtt_connection_builder>`
339+
described at the top of this doc, as well as...
340+
341+
Args:
342+
pkcs12_filepath: Path to the PKCS12 file to use
343+
344+
pkcs12_password: The password for the PKCS12 file.
345+
"""
346+
_check_required_kwargs(**kwargs)
347+
348+
tls_ctx_options = awscrt.io.TlsContextOptions.create_client_with_mtls_pkcs12(
349+
pkcs12_filepath=pkcs12_filepath,
350+
pkcs12_password=pkcs12_password)
351+
return _builder(tls_ctx_options, **kwargs)
352+
328353

329354
def mtls_with_windows_cert_store_path(*,
330355
cert_store_path: str,

Diff for: documents/MQTT5_Userguide.md

+16
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* [MQTT over Websockets with Sigv4 authentication](#mqtt-over-websockets-with-sigv4-authentication)
1515
* [Direct MQTT with Custom Authentication](#direct-mqtt-with-custom-authentication)
1616
* [Direct MQTT with PKCS11 Method](#direct-mqtt-with-pkcs11-method)
17+
* [Direct MQTT with PKCS12 Method](#direct-mqtt-with-pkcs12-method)
1718
* [MQTT over Websockets with Cognito authentication](#mqtt-over-websockets-with-cognito-authentication)
1819
* [HTTP Proxy](#http-proxy)
1920
* [Client Lifecycle Management](#client-lifecycle-management)
@@ -171,6 +172,21 @@ A MQTT5 direct connection can be made using a PKCS11 device rather than using a
171172

172173
**Note**: Currently, TLS integration with PKCS#11 is only available on Unix devices.
173174

175+
#### **Direct MQTT with PKCS12 Method**
176+
177+
A MQTT5 direct connection can be made using a PKCS12 file rather than using a PEM encoded private key. To create a MQTT5 builder configured for this connection, see the following code:
178+
179+
```python
180+
# other builder configurations can be added using **kwargs in the builder
181+
182+
client = mqtt5_client_builder.mtls_with_pkcs12(
183+
pkcs12_filepath = "<PKCS12 file path>,
184+
pkcs12_password = "<PKCS12 password>
185+
endpoint = "<account-specific endpoint>")
186+
```
187+
188+
**Note**: Currently, TLS integration with PKCS#12 is only available on MacOS devices.
189+
174190
#### **MQTT over Websockets with Cognito authentication**
175191

176192
A MQTT5 websocket connection can be made using Cognito to authenticate rather than the AWS credentials located on the device or via key and certificate. Instead, Cognito can authenticate the connection using a valid Cognito identity ID. This requires a valid Cognito identity ID, which can be retrieved from a Cognito identity pool. A Cognito identity pool can be created from the AWS console.

Diff for: samples/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* [Websocket Connect](./websocket_connect.md)
88
* [MQTT5 PKCS#11 Connect](./mqtt5_pkcs11_connect.md)
99
* [PKCS#11 Connect](./pkcs11_connect.md)
10+
* [PKCS#12 Connect](./pkcs12_connect.md)
1011
* [Windows Certificate Connect](./windows_cert_connect/README.md)
1112
* [MQTT5 Custom Authorizer Connect](./mqtt5_custom_authorizer_connect.md)
1213
* [Custom Authorizer Connect](./custom_authorizer_connect.md)

Diff for: samples/pkcs12_connect.md

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# PKCS12 Connect
2+
3+
[**Return to main sample list**](../README.md)
4+
5+
This sample is similar to the [Basic Connect](../BasicConnect/README.md) sample, in that it connects via Mutual TLS (mTLS) using a certificate and key file. However, unlike the Basic Connect where the certificate and private key file are stored on disk, this sample uses a PKCS#12 file instead.
6+
7+
**WARNING: MacOS only**. Currently, TLS integration with PKCS12 is only available on MacOS devices.
8+
9+
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.
10+
11+
<details>
12+
<summary>(see sample policy)</summary>
13+
<pre>
14+
{
15+
"Version": "2012-10-17",
16+
"Statement": [
17+
{
18+
"Effect": "Allow",
19+
"Action": [
20+
"iot:Connect"
21+
],
22+
"Resource": [
23+
"arn:aws:iot:<b>region</b>:<b>account</b>:client/test-*"
24+
]
25+
}
26+
]
27+
}
28+
</pre>
29+
30+
Replace with the following with the data from your AWS account:
31+
* `<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`.
32+
* `<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.
33+
34+
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.
35+
36+
</details>
37+
38+
## How to run
39+
40+
To run the PKCS12 connect use the following command:
41+
42+
```sh
43+
python3 pkcs12_connect --endpoint <endpoint> --pkcs12_file <path to PKCS12 file> --pkcs12_password <password for PKCS12 file>
44+
```
45+
46+
You can also pass a Certificate Authority file (CA) if your certificate and key combination requires it:
47+
48+
```sh
49+
python3 pkcs12_connect --endpoint <endpoint> --pkcs12_file <path to PKCS12 file> --pkcs12_password <password for PKCS12 file> --ca_file <path to CA file>
50+
```
51+
52+
### How to setup and run
53+
54+
To use the certificate and key files provided by AWS IoT Core, you will need to convert them into PKCS#12 format and then import them into your Java keystore. You can convert the certificate and key file to PKCS12 using the following command:
55+
56+
```sh
57+
openssl pkcs12 -export -in <my-certificate.pem.crt> -inkey <my-private-key.pem.key> -out <my-pkcs12-key.pem.key> -name <alias here> -password pass:<password here>
58+
```
59+
60+
Once converted, you can then run the PKCS12 connect sample with the following:
61+
62+
```sh
63+
python3 pkcs12_connect --endpoint <endpoint> --pkcs12_file <my-pkcs12-key.pem.key> --pkcs12_password <password here>
64+
```

Diff for: samples/pkcs12_connect.py

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0.
3+
4+
from awscrt import http, io
5+
from awsiot import mqtt_connection_builder
6+
from utils.command_line_utils import CommandLineUtils
7+
8+
# This sample shows how to create a MQTT connection using a certificate file and key file.
9+
# This sample is intended to be used as a reference for making MQTT connections.
10+
11+
# Callback when connection is accidentally lost.
12+
def on_connection_interrupted(connection, error, **kwargs):
13+
print("Connection interrupted. error: {}".format(error))
14+
15+
# Callback when an interrupted connection is re-established.
16+
def on_connection_resumed(connection, return_code, session_present, **kwargs):
17+
print("Connection resumed. return_code: {} session_present: {}".format(return_code, session_present))
18+
19+
20+
if __name__ == '__main__':
21+
22+
io.init_logging(log_level=io.LogLevel.Trace, file_name="stderr")
23+
24+
# cmdData is the arguments/input from the command line placed into a single struct for
25+
# use in this sample. This handles all of the command line parsing, validating, etc.
26+
# See the Utils/CommandLineUtils for more information.
27+
cmdData = CommandLineUtils.parse_sample_input_pkcs12_connect()
28+
29+
# Create the proxy options if the data is present in cmdData
30+
proxy_options = None
31+
if cmdData.input_proxy_host is not None and cmdData.input_proxy_port != 0:
32+
proxy_options = http.HttpProxyOptions(
33+
host_name=cmdData.input_proxy_host,
34+
port=cmdData.input_proxy_port)
35+
36+
# Create a MQTT connection from the command line data
37+
mqtt_connection = mqtt_connection_builder.mtls_with_pkcs12(
38+
endpoint=cmdData.input_endpoint,
39+
port=cmdData.input_port,
40+
pkcs12_filepath=cmdData.input_pkcs12_file,
41+
pkcs12_password=cmdData.input_pkcs12_password,
42+
on_connection_interrupted=on_connection_interrupted,
43+
on_connection_resumed=on_connection_resumed,
44+
client_id=cmdData.input_clientId,
45+
clean_session=False,
46+
keep_alive_secs=30,
47+
http_proxy_options=proxy_options)
48+
49+
if not cmdData.input_is_ci:
50+
print(f"Connecting to {cmdData.input_endpoint} with client ID '{cmdData.input_clientId}'...")
51+
else:
52+
print("Connecting to endpoint with client ID")
53+
54+
connect_future = mqtt_connection.connect()
55+
# Future.result() waits until a result is available
56+
connect_future.result()
57+
print("Connected!")
58+
59+
# Disconnect
60+
print("Disconnecting...")
61+
disconnect_future = mqtt_connection.disconnect()
62+
disconnect_future.result()
63+
print("Disconnected!")

Diff for: samples/utils/command_line_utils.py

+37
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,9 @@ class CmdData:
284284
input_job_time : int
285285
# Shadow
286286
input_shadow_property : str
287+
# PKCS12
288+
input_pkcs12_file : str
289+
input_pkcs12_password : str
287290

288291
def __init__(self) -> None:
289292
pass
@@ -794,6 +797,38 @@ def parse_sample_input_x509_connect():
794797
cmdData.input_is_ci = cmdUtils.get_command(CommandLineUtils.m_cmd_is_ci, None) != None
795798
return cmdData
796799

800+
def parse_sample_input_pkcs12_connect():
801+
# Parse arguments
802+
cmdUtils = CommandLineUtils("PKCS12 Connect - Make a MQTT connection.")
803+
cmdUtils.add_common_mqtt_commands()
804+
cmdUtils.add_common_proxy_commands()
805+
cmdUtils.add_common_logging_commands()
806+
cmdUtils.register_command(CommandLineUtils.m_cmd_pkcs12_file, "<path>",
807+
"Path to the PKCS12 file to use.", True, str)
808+
cmdUtils.register_command(CommandLineUtils.m_cmd_pkcs12_password, "<str>",
809+
"The password for the PKCS12 file.", False, str)
810+
cmdUtils.register_command(CommandLineUtils.m_cmd_port, "<int>",
811+
"Connection port for direct connection. " +
812+
"AWS IoT supports 443 and 8883 (optional, default=8883).",
813+
False, int)
814+
cmdUtils.register_command(CommandLineUtils.m_cmd_client_id, "<str>",
815+
"Client ID to use for MQTT connection (optional, default='test-*').",
816+
default="test-" + str(uuid4()))
817+
# Needs to be called so the command utils parse the commands
818+
cmdUtils.get_args()
819+
820+
cmdData = CommandLineUtils.CmdData()
821+
cmdData.input_endpoint = cmdUtils.get_command_required(CommandLineUtils.m_cmd_endpoint)
822+
cmdData.input_port = int(cmdUtils.get_command(CommandLineUtils.m_cmd_port, 8883))
823+
cmdData.input_pkcs12_file = cmdUtils.get_command_required(CommandLineUtils.m_cmd_pkcs12_file)
824+
cmdData.input_pkcs12_password = cmdUtils.get_command_required(CommandLineUtils.m_cmd_pkcs12_password)
825+
cmdData.input_ca = cmdUtils.get_command(CommandLineUtils.m_cmd_ca_file, None)
826+
cmdData.input_clientId = cmdUtils.get_command(CommandLineUtils.m_cmd_client_id, "test-" + str(uuid4()))
827+
cmdData.input_proxy_host = cmdUtils.get_command(CommandLineUtils.m_cmd_proxy_host)
828+
cmdData.input_proxy_port = int(cmdUtils.get_command(CommandLineUtils.m_cmd_proxy_port))
829+
cmdData.input_is_ci = cmdUtils.get_command(CommandLineUtils.m_cmd_is_ci, None) != None
830+
return cmdData
831+
797832

798833
# Constants for commonly used/needed commands
799834
m_cmd_endpoint = "endpoint"
@@ -840,3 +875,5 @@ def parse_sample_input_x509_connect():
840875
m_cmd_count = "count"
841876
m_cmd_group_identifier = "group_identifier"
842877
m_cmd_shadow_property = "shadow_property"
878+
m_cmd_pkcs12_file = "pkcs12_file"
879+
m_cmd_pkcs12_password = "pkcs12_password"

0 commit comments

Comments
 (0)