diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a26123cb..9b14cc2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,6 +109,13 @@ jobs: - name: run PubSub sample run: | python3 ${{ env.CI_UTILS_FOLDER }}/run_sample_ci.py --file ${{ env.CI_SAMPLES_CFG_FOLDER }}/ci_run_pubsub_cfg.json + - name: run PKCS12 sample + run: | + 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 + 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 + 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\") + openssl pkcs12 -export -in /tmp/certificate.pem -inkey /tmp/privatekey.pem -out ./pkcs12-key.p12 -name PubSub_Thing_Alias -password pass:$pkcs12_password + python3 ${{ env.CI_UTILS_FOLDER }}/run_sample_ci.py --file ${{ env.CI_SAMPLES_CFG_FOLDER }}/ci_run_pkcs12_connect_cfg.json - name: configure AWS credentials (MQTT5 samples) uses: aws-actions/configure-aws-credentials@v1 with: diff --git a/.github/workflows/ci_run_pkcs12_connect_cfg.json b/.github/workflows/ci_run_pkcs12_connect_cfg.json new file mode 100644 index 00000000..3b633187 --- /dev/null +++ b/.github/workflows/ci_run_pkcs12_connect_cfg.json @@ -0,0 +1,20 @@ +{ +"language": "Python", +"sample_file": "./aws-iot-device-sdk-python-v2/samples/pkcs12_connect.py", +"sample_region": "us-east-1", +"sample_main_class": "", +"arguments": [ + { + "name": "--endpoint", + "secret": "ci/endpoint" + }, + { + "name": "--pkcs12_file", + "data": "./pkcs12-key.p12" + }, + { + "name": "--pkcs12_password", + "secret": "ci/PubSub/key_pkcs12_password" + } +] +} diff --git a/awsiot/mqtt5_client_builder.py b/awsiot/mqtt5_client_builder.py index 23aa7098..f8aa9fba 100644 --- a/awsiot/mqtt5_client_builder.py +++ b/awsiot/mqtt5_client_builder.py @@ -442,6 +442,31 @@ def mtls_with_pkcs11(*, cert_file_contents=cert_bytes) return _builder(tls_ctx_options, **kwargs) +def mtls_with_pkcs12(*, + pkcs12_filepath: str, + pkcs12_password: str, + **kwargs) -> awscrt.mqtt.Connection: + """ + This builder creates an :class:`awscrt.mqtt.Connection`, configured for an mTLS MQTT connection to AWS IoT, + using a PKCS#12 certificate. + + NOTE: MacOS only + + This function takes all :mod:`common arguments` + described at the top of this doc, as well as... + + Args: + pkcs12_filepath: Path to the PKCS12 file to use + + pkcs12_password: The password for the PKCS12 file. + """ + _check_required_kwargs(**kwargs) + + tls_ctx_options = awscrt.io.TlsContextOptions.create_client_with_mtls_pkcs12( + pkcs12_filepath=pkcs12_filepath, + pkcs12_password=pkcs12_password) + return _builder(tls_ctx_options, **kwargs) + def mtls_with_windows_cert_store_path(*, cert_store_path: str, diff --git a/awsiot/mqtt_connection_builder.py b/awsiot/mqtt_connection_builder.py index 50c95aa9..785cd774 100644 --- a/awsiot/mqtt_connection_builder.py +++ b/awsiot/mqtt_connection_builder.py @@ -325,6 +325,31 @@ def mtls_with_pkcs11(*, return _builder(tls_ctx_options, **kwargs) +def mtls_with_pkcs12(*, + pkcs12_filepath: str, + pkcs12_password: str, + **kwargs) -> awscrt.mqtt.Connection: + """ + This builder creates an :class:`awscrt.mqtt.Connection`, configured for an mTLS MQTT connection to AWS IoT, + using a PKCS#12 certificate. + + NOTE: MacOS only + + This function takes all :mod:`common arguments` + described at the top of this doc, as well as... + + Args: + pkcs12_filepath: Path to the PKCS12 file to use + + pkcs12_password: The password for the PKCS12 file. + """ + _check_required_kwargs(**kwargs) + + tls_ctx_options = awscrt.io.TlsContextOptions.create_client_with_mtls_pkcs12( + pkcs12_filepath=pkcs12_filepath, + pkcs12_password=pkcs12_password) + return _builder(tls_ctx_options, **kwargs) + def mtls_with_windows_cert_store_path(*, cert_store_path: str, diff --git a/documents/MQTT5_Userguide.md b/documents/MQTT5_Userguide.md index 48dc8f51..e325a912 100644 --- a/documents/MQTT5_Userguide.md +++ b/documents/MQTT5_Userguide.md @@ -14,6 +14,7 @@ * [MQTT over Websockets with Sigv4 authentication](#mqtt-over-websockets-with-sigv4-authentication) * [Direct MQTT with Custom Authentication](#direct-mqtt-with-custom-authentication) * [Direct MQTT with PKCS11 Method](#direct-mqtt-with-pkcs11-method) + * [Direct MQTT with PKCS12 Method](#direct-mqtt-with-pkcs12-method) * [MQTT over Websockets with Cognito authentication](#mqtt-over-websockets-with-cognito-authentication) * [HTTP Proxy](#http-proxy) * [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 **Note**: Currently, TLS integration with PKCS#11 is only available on Unix devices. +#### **Direct MQTT with PKCS12 Method** + +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: + +```python + # other builder configurations can be added using **kwargs in the builder + + client = mqtt5_client_builder.mtls_with_pkcs12( + pkcs12_filepath = ", + pkcs12_password = " + endpoint = "") +``` + +**Note**: Currently, TLS integration with PKCS#12 is only available on MacOS devices. + #### **MQTT over Websockets with Cognito authentication** 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 --git a/samples/README.md b/samples/README.md index 95ac9a5c..690fe29e 100644 --- a/samples/README.md +++ b/samples/README.md @@ -7,6 +7,7 @@ * [Websocket Connect](./websocket_connect.md) * [MQTT5 PKCS#11 Connect](./mqtt5_pkcs11_connect.md) * [PKCS#11 Connect](./pkcs11_connect.md) +* [PKCS#12 Connect](./pkcs12_connect.md) * [Windows Certificate Connect](./windows_cert_connect/README.md) * [MQTT5 Custom Authorizer Connect](./mqtt5_custom_authorizer_connect.md) * [Custom Authorizer Connect](./custom_authorizer_connect.md) diff --git a/samples/pkcs12_connect.md b/samples/pkcs12_connect.md new file mode 100644 index 00000000..5478a2fd --- /dev/null +++ b/samples/pkcs12_connect.md @@ -0,0 +1,64 @@ +# PKCS12 Connect + +[**Return to main sample list**](../README.md) + +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. + +**WARNING: MacOS only**. Currently, TLS integration with PKCS12 is only available on MacOS devices. + +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-*"
+      ]
+    }
+  ]
+}
+
+ +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. + +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 PKCS12 connect use the following command: + +```sh +python3 pkcs12_connect --endpoint --pkcs12_file --pkcs12_password +``` + +You can also pass a Certificate Authority file (CA) if your certificate and key combination requires it: + +```sh +python3 pkcs12_connect --endpoint --pkcs12_file --pkcs12_password --ca_file +``` + +### How to setup and run + +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: + +```sh +openssl pkcs12 -export -in -inkey -out -name -password pass: +``` + +Once converted, you can then run the PKCS12 connect sample with the following: + +```sh +python3 pkcs12_connect --endpoint --pkcs12_file --pkcs12_password +``` diff --git a/samples/pkcs12_connect.py b/samples/pkcs12_connect.py new file mode 100644 index 00000000..ad284f73 --- /dev/null +++ b/samples/pkcs12_connect.py @@ -0,0 +1,63 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0. + +from awscrt import http, io +from awsiot import mqtt_connection_builder +from utils.command_line_utils import CommandLineUtils + +# This sample shows how to create a MQTT connection using a certificate file and key file. +# This sample is intended to be used as a reference for making MQTT connections. + +# 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__': + + io.init_logging(log_level=io.LogLevel.Trace, file_name="stderr") + + # cmdData is the arguments/input from the command line placed into a single struct for + # use in this sample. This handles all of the command line parsing, validating, etc. + # See the Utils/CommandLineUtils for more information. + cmdData = CommandLineUtils.parse_sample_input_pkcs12_connect() + + # Create the proxy options if the data is present in cmdData + proxy_options = None + if cmdData.input_proxy_host is not None and cmdData.input_proxy_port != 0: + proxy_options = http.HttpProxyOptions( + host_name=cmdData.input_proxy_host, + port=cmdData.input_proxy_port) + + # Create a MQTT connection from the command line data + mqtt_connection = mqtt_connection_builder.mtls_with_pkcs12( + endpoint=cmdData.input_endpoint, + port=cmdData.input_port, + pkcs12_filepath=cmdData.input_pkcs12_file, + pkcs12_password=cmdData.input_pkcs12_password, + on_connection_interrupted=on_connection_interrupted, + on_connection_resumed=on_connection_resumed, + client_id=cmdData.input_clientId, + clean_session=False, + keep_alive_secs=30, + http_proxy_options=proxy_options) + + if not cmdData.input_is_ci: + print(f"Connecting to {cmdData.input_endpoint} with client ID '{cmdData.input_clientId}'...") + 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!") diff --git a/samples/utils/command_line_utils.py b/samples/utils/command_line_utils.py index b365dbc3..1e097a07 100644 --- a/samples/utils/command_line_utils.py +++ b/samples/utils/command_line_utils.py @@ -284,6 +284,9 @@ class CmdData: input_job_time : int # Shadow input_shadow_property : str + # PKCS12 + input_pkcs12_file : str + input_pkcs12_password : str def __init__(self) -> None: pass @@ -794,6 +797,38 @@ def parse_sample_input_x509_connect(): cmdData.input_is_ci = cmdUtils.get_command(CommandLineUtils.m_cmd_is_ci, None) != None return cmdData + def parse_sample_input_pkcs12_connect(): + # Parse arguments + cmdUtils = CommandLineUtils("PKCS12 Connect - Make a MQTT connection.") + cmdUtils.add_common_mqtt_commands() + cmdUtils.add_common_proxy_commands() + cmdUtils.add_common_logging_commands() + cmdUtils.register_command(CommandLineUtils.m_cmd_pkcs12_file, "", + "Path to the PKCS12 file to use.", True, str) + cmdUtils.register_command(CommandLineUtils.m_cmd_pkcs12_password, "", + "The password for the PKCS12 file.", False, str) + cmdUtils.register_command(CommandLineUtils.m_cmd_port, "", + "Connection port for direct connection. " + + "AWS IoT supports 443 and 8883 (optional, default=8883).", + False, int) + cmdUtils.register_command(CommandLineUtils.m_cmd_client_id, "", + "Client ID to use for MQTT connection (optional, default='test-*').", + default="test-" + str(uuid4())) + # Needs to be called so the command utils parse the commands + cmdUtils.get_args() + + cmdData = CommandLineUtils.CmdData() + cmdData.input_endpoint = cmdUtils.get_command_required(CommandLineUtils.m_cmd_endpoint) + cmdData.input_port = int(cmdUtils.get_command(CommandLineUtils.m_cmd_port, 8883)) + cmdData.input_pkcs12_file = cmdUtils.get_command_required(CommandLineUtils.m_cmd_pkcs12_file) + cmdData.input_pkcs12_password = cmdUtils.get_command_required(CommandLineUtils.m_cmd_pkcs12_password) + cmdData.input_ca = cmdUtils.get_command(CommandLineUtils.m_cmd_ca_file, None) + cmdData.input_clientId = cmdUtils.get_command(CommandLineUtils.m_cmd_client_id, "test-" + str(uuid4())) + cmdData.input_proxy_host = cmdUtils.get_command(CommandLineUtils.m_cmd_proxy_host) + cmdData.input_proxy_port = int(cmdUtils.get_command(CommandLineUtils.m_cmd_proxy_port)) + cmdData.input_is_ci = cmdUtils.get_command(CommandLineUtils.m_cmd_is_ci, None) != None + return cmdData + # Constants for commonly used/needed commands m_cmd_endpoint = "endpoint" @@ -840,3 +875,5 @@ def parse_sample_input_x509_connect(): m_cmd_count = "count" m_cmd_group_identifier = "group_identifier" m_cmd_shadow_property = "shadow_property" + m_cmd_pkcs12_file = "pkcs12_file" + m_cmd_pkcs12_password = "pkcs12_password"