Skip to content

PKCS12 connect sample #445

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 4 commits into from
Apr 24, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
20 changes: 20 additions & 0 deletions .github/workflows/ci_run_pkcs12_connect_cfg.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
25 changes: 25 additions & 0 deletions awsiot/mqtt5_client_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<awsiot.mqtt_connection_builder>`
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,
Expand Down
25 changes: 25 additions & 0 deletions awsiot/mqtt_connection_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<awsiot.mqtt_connection_builder>`
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,
Expand Down
16 changes: 16 additions & 0 deletions documents/MQTT5_Userguide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 file path>,
pkcs12_password = "<PKCS12 password>
endpoint = "<account-specific endpoint>")
```

**Note**: Currently, TLS integration with PKCS#11 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.
Expand Down
1 change: 1 addition & 0 deletions samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
64 changes: 64 additions & 0 deletions samples/pkcs12_connect.md
Original file line number Diff line number Diff line change
@@ -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.

<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-*"
]
}
]
}
</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.

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 PKCS12 connect use the following command:

```sh
python3 pkcs12_connect --endpoint <endpoint> --pkcs12_file <path to PKCS12 file> --pkcs12_password <password for PKCS12 file>
```

You can also pass a Certificate Authority file (CA) if your certificate and key combination requires it:

```sh
python3 pkcs12_connect --endpoint <endpoint> --pkcs12_file <path to PKCS12 file> --pkcs12_password <password for PKCS12 file> --ca_file <path to 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 <my-certificate.pem.crt> -inkey <my-private-key.pem.key> -out <my-pkcs12-key.pem.key> -name <alias here> -password pass:<password here>
```

Once converted, you can then run the PKCS12 connect sample with the following:

```sh
python3 pkcs12_connect --endpoint <endpoint> --pkcs12_file <my-pkcs12-key.pem.key> --pkcs12_password <password here>
```
63 changes: 63 additions & 0 deletions samples/pkcs12_connect.py
Original file line number Diff line number Diff line change
@@ -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!")
37 changes: 37 additions & 0 deletions samples/utils/command_line_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,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
Expand Down Expand Up @@ -780,6 +783,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>",
"Path to the PKCS12 file to use.", True, str)
cmdUtils.register_command(CommandLineUtils.m_cmd_pkcs12_password, "<str>",
"The password for the PKCS12 file.", False, str)
cmdUtils.register_command(CommandLineUtils.m_cmd_port, "<int>",
"Connection port for direct connection. " +
"AWS IoT supports 443 and 8883 (optional, default=8883).",
False, int)
cmdUtils.register_command(CommandLineUtils.m_cmd_client_id, "<str>",
"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"
Expand Down Expand Up @@ -824,3 +859,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"