Skip to content

Commit

Permalink
Add PKCS12 connect sample (#581)
Browse files Browse the repository at this point in the history
* Add PKCS12 sample

* Fix CI yaml

* Need to compile sample

* Fix error due to struct initialization

* Is the sample stalling or getting the credentials?

* Adjust PKCS12 file paths

* Run the sample directly to, hopefully, see sample logs

* Try PKCS12 key path a different way

* Revert back to PKCS12 CI code from JS

* Pipe the output from running the sample to hopefully see why it is stalling in CI but not locally nor in other SDKs

* Try adding a timeout to see the output?

* Try turning on logs

* Print the output on timeout

* Use a pipe to get the output?

* Output to CRT logs and try to print that file

* Try a relative path for the log file?

* Does it even hit the executable? Try passing --help to see if that works

* Try running directly with logging

* Pass the endpoint directly to see if that resolves the endpoint address

* Try a hard-coded PKCS12 password to see if it makes a difference

* Try relative paths?

* Try again

* Revert back to how Python does it again

* Modified wrong file path

* Try getting the PKCS12 key from S3

* use a tmp pkcs12 key file path

* test with sudo python3

* update permission

* test with logs

* update security permision

* update key file path

* test with sudo

* test github action

* test github security

* fix yml

* fix yaml

* fix import pkcs12 key path

* try access identity from keychain

* test identity

* test different keychain

* try create local pkcs12 file

* fix yaml

* remove comments

* update pkcs12 passworkd

* test with identity file

* kick ci

* clean up the secrets and sample

* kick ci

* fix file path

* clean up commands

* improve ci.ym;

* remove unnecssary file

---------

Co-authored-by: Zhihui Xia <zhvxia@amazon.com>
  • Loading branch information
TwistedTwigleg and xiazhvera authored Aug 8, 2023
1 parent e065cff commit 11855fc
Show file tree
Hide file tree
Showing 11 changed files with 303 additions and 4 deletions.
1 change: 1 addition & 0 deletions .builder/actions/build_samples.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def run(self, env):
'samples/mqtt/basic_connect',
'samples/mqtt/custom_authorizer_connect',
'samples/mqtt/pkcs11_connect',
'samples/mqtt/pkcs12_connect',
'samples/mqtt/websocket_connect',
'samples/mqtt/windows_cert_connect',
'samples/mqtt/x509_credentials_provider_connect',
Expand Down
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ jobs:
runs-on: macos-latest
permissions:
id-token: write # This is required for requesting the JWT
security-events: write # This is required for pkcs12 sample to sign the key
steps:
- name: Build ${{ env.PACKAGE_NAME }} + consumers
run: |
Expand All @@ -330,6 +331,23 @@ jobs:
- name: run MQTT3 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: |
aws s3 cp s3://iot-sdk-ci-bucket-us-east1/pkcs12_identity.p12 ./pkcs12_identity.p12
pkcs12_identity_name=$(aws secretsmanager get-secret-value --region us-east-1 --secret-id "ci/macos/pkcs12_identity" --query "SecretString" | cut -f2 -d":" | cut -f2 -d\")
pkcs12_identity_password=$(aws secretsmanager get-secret-value --region us-east-1 --secret-id "ci/macos/pkcs12_identity_password" --query "SecretString" | cut -f2 -d":" | cut -f2 -d\")
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
iot_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 ./iot_pkcs12_key.p12 -name PubSub_Thing_Alias -password pass:$iot_pkcs12_password
security create-keychain -p test_password build.keychain
security set-keychain-settings -lut 21600 build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p test_password build.keychain
security import pkcs12_identity.p12 -A -k build.keychain -f pkcs12 -P $pkcs12_identity_password -T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S 'apple-tool:,apple:' -k test_password build.keychain
/usr/bin/codesign --force -s $pkcs12_identity_name ./aws-iot-device-sdk-cpp-v2/build/samples/mqtt/pkcs12_connect/pkcs12-connect -v
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)
uses: aws-actions/configure-aws-credentials@v1
with:
Expand Down
24 changes: 24 additions & 0 deletions .github/workflows/ci_run_pkcs12_connect_cfg.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"language": "CPP",
"sample_file": "./aws-iot-device-sdk-cpp-v2/build/samples/mqtt/pkcs12_connect/pkcs12-connect",
"sample_region": "us-east-1",
"sample_main_class": "",
"arguments": [
{
"name": "--endpoint",
"secret": "ci/endpoint"
},
{
"name": "--pkcs12_file",
"data": "./iot_pkcs12_key.p12"
},
{
"name": "--pkcs12_password",
"secret": "ci/PubSub/key_pkcs12_password"
},
{
"name": "--verbosity",
"data": "Trace"
}
]
}
1 change: 1 addition & 0 deletions samples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ add_subdirectory(jobs/describe_job_execution)
add_subdirectory(mqtt/basic_connect)
add_subdirectory(mqtt/custom_authorizer_connect)
add_subdirectory(mqtt/pkcs11_connect)
add_subdirectory(mqtt/pkcs12_connect)
add_subdirectory(mqtt/websocket_connect)
add_subdirectory(mqtt/windows_cert_connect)
add_subdirectory(mqtt/x509_credentials_provider_connect)
Expand Down
3 changes: 3 additions & 0 deletions samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* [Mqtt5 Shared Subscription](./mqtt5/mqtt5_shared_subscription/README.md)
* [Websocket Connect](./mqtt/websocket_connect/README.md)
* [PKCS#11 Connect](./mqtt/pkcs11_connect/README.md)
* [PKCS#12 Connect](./mqtt/pkcs12_connect/README.md)
* [x509 Credentials Provider Connect](./mqtt/x509_credentials_provider_connect/README.md)
* [Windows Certificate MQTT Connect](./mqtt/windows_cert_connect/README.md)
* [Custom Authorizer Connect](./mqtt/custom_authorizer_connect/README.md)
Expand Down Expand Up @@ -68,6 +69,8 @@ cmake -DCMAKE_PREFIX_PATH="<absolute path sdk-cpp-workspace dir>" -DCMAKE_BUILD_
cmake --build . --config "<Release|RelWithDebInfo|Debug>"
```

Note that building all the samples at once is currently only available in the V2 C++ IoT SDK at this time.

### Sample Build Notes

* `-DCMAKE_PREFIX_PATH` needs to be set to the path aws-iot-device-sdk-cpp-v2 installed at. Since [Installation](../README.md#Installation) takes `sdk-cpp-workspace` as an example, this file uses that example too.
Expand Down
25 changes: 25 additions & 0 deletions samples/mqtt/pkcs12_connect/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
cmake_minimum_required(VERSION 3.1)
# note: cxx-17 requires cmake 3.8, cxx-20 requires cmake 3.12
project(pkcs12-connect CXX)

file(GLOB SRC_FILES
"*.cpp"
"../../utils/CommandLineUtils.cpp"
"../../utils/CommandLineUtils.h"
)

add_executable(${PROJECT_NAME} ${SRC_FILES})

set_target_properties(${PROJECT_NAME} PROPERTIES
CXX_STANDARD 14)

#set warnings
if (MSVC)
target_compile_options(${PROJECT_NAME} PRIVATE /W4 /WX /wd4068)
else ()
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wno-long-long -pedantic -Werror)
endif ()

find_package(aws-crt-cpp REQUIRED)

target_link_libraries(${PROJECT_NAME} AWS::aws-crt-cpp)
64 changes: 64 additions & 0 deletions samples/mqtt/pkcs12_connect/README.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](../basic_connect/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

This sample can be run using the following command:

```sh
./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
./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
./pkcs12-connect --endpoint <endpoint> --pkcs12_file <path to PKCS12 file> --pkcs12_password <password for PKCS12 file>
```
135 changes: 135 additions & 0 deletions samples/mqtt/pkcs12_connect/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
#include <aws/crt/Api.h>
#include <aws/crt/UUID.h>
#include <aws/crt/io/Pkcs11.h>

#include "../../utils/CommandLineUtils.h"

using namespace Aws::Crt;

int main(int argc, char *argv[])
{

/************************ Setup ****************************/

// Do the global initialization for the API.
ApiHandle apiHandle;

/**
* 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.
*/
Utils::cmdData cmdData = Utils::parseSampleInputPKCS12Connect(argc, argv, &apiHandle);

// Create the MQTT builder and populate it with data from cmdData.
Aws::Iot::MqttClient client;
struct Aws::Iot::Pkcs12Options options;
options.pkcs12_file = cmdData.input_pkcs12File;
options.pkcs12_password = cmdData.input_pkcs12Password;
Aws::Iot::MqttClientConnectionConfigBuilder clientConfigBuilder(options);
if (!clientConfigBuilder)
{
fprintf(
stderr,
"MqttClientConnectionConfigBuilder failed: %s\n",
Aws::Crt::ErrorDebugString(Aws::Crt::LastError()));
exit(-1);
}
if (cmdData.input_ca != "")
{
clientConfigBuilder.WithCertificateAuthority(cmdData.input_ca.c_str());
}
clientConfigBuilder.WithEndpoint(cmdData.input_endpoint);

// Create the MQTT connection from the MQTT builder
auto clientConfig = clientConfigBuilder.Build();
if (!clientConfig)
{
fprintf(
stderr,
"Client Configuration initialization failed with error %s\n",
Aws::Crt::ErrorDebugString(clientConfig.LastError()));
exit(-1);
}
auto connection = client.NewConnection(clientConfig);
if (!*connection)
{
fprintf(
stderr,
"MQTT Connection Creation failed with error %s\n",
Aws::Crt::ErrorDebugString(connection->LastError()));
exit(-1);
}

/**
* In a real world application you probably don't want to enforce synchronous behavior
* but this is a sample console application, so we'll just do that with a condition variable.
*/
std::promise<bool> connectionCompletedPromise;
std::promise<void> connectionClosedPromise;

// Invoked when a MQTT connect has completed or failed
auto onConnectionCompleted =
[&](Aws::Crt::Mqtt::MqttConnection &, int errorCode, Aws::Crt::Mqtt::ReturnCode returnCode, bool) {
if (errorCode)
{
fprintf(stdout, "Connection failed with error %s\n", Aws::Crt::ErrorDebugString(errorCode));
connectionCompletedPromise.set_value(false);
}
else
{
fprintf(stdout, "Connection completed with return code %d\n", returnCode);
connectionCompletedPromise.set_value(true);
}
};

// Invoked when a MQTT connection was interrupted/lost
auto onInterrupted = [&](Aws::Crt::Mqtt::MqttConnection &, int error) {
fprintf(stdout, "Connection interrupted with error %s\n", Aws::Crt::ErrorDebugString(error));
};

// Invoked when a MQTT connection was interrupted/lost, but then reconnected successfully
auto onResumed = [&](Aws::Crt::Mqtt::MqttConnection &, Aws::Crt::Mqtt::ReturnCode, bool) {
fprintf(stdout, "Connection resumed\n");
};

// Invoked when a disconnect message has completed.
auto onDisconnect = [&](Aws::Crt::Mqtt::MqttConnection &) {
fprintf(stdout, "Disconnect completed\n");
connectionClosedPromise.set_value();
};

// Assign callbacks
connection->OnConnectionCompleted = std::move(onConnectionCompleted);
connection->OnDisconnect = std::move(onDisconnect);
connection->OnConnectionInterrupted = std::move(onInterrupted);
connection->OnConnectionResumed = std::move(onResumed);

/************************ Run the sample ****************************/

// Connect
fprintf(stdout, "Connecting...\n");
if (!connection->Connect(cmdData.input_clientId.c_str(), false /*cleanSession*/, 1000 /*keepAliveTimeSecs*/))
{
fprintf(stderr, "MQTT Connection failed with error %s\n", Aws::Crt::ErrorDebugString(connection->LastError()));
exit(-1);
}

// wait for the OnConnectionCompleted callback to fire, which sets connectionCompletedPromise...
if (connectionCompletedPromise.get_future().get() == false)
{
fprintf(stderr, "Connection failed\n");
exit(-1);
}

// Disconnect
if (connection->Disconnect())
{
connectionClosedPromise.get_future().wait();
}
return 0;
}
21 changes: 21 additions & 0 deletions samples/utils/CommandLineUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ namespace Utils
static const char *m_cmd_proxy_password = "proxy_password";
static const char *m_cmd_shadow_property = "shadow_property";
static const char *m_cmd_region = "region";
static const char *m_cmd_pkcs12_file = "pkcs12_file";
static const char *m_cmd_pkcs12_password = "pkcs12_password";
static const char *m_cmd_print_discover_resp_only = "print_discover_resp_only";

CommandLineUtils::CommandLineUtils()
Expand Down Expand Up @@ -960,4 +962,23 @@ namespace Utils
return returnData;
}

cmdData parseSampleInputPKCS12Connect(int argc, char *argv[], Aws::Crt::ApiHandle *api_handle)
{
CommandLineUtils cmdUtils = CommandLineUtils();
cmdUtils.RegisterProgramName("pkcs12-connect");
cmdUtils.AddCommonMQTTCommands();
cmdUtils.RegisterCommand(m_cmd_pkcs12_file, "<path>", "Path to the PKCS#12 file.");
cmdUtils.RegisterCommand(m_cmd_pkcs12_password, "<str>", "Password for the PKCS#12 file.");
cmdUtils.RegisterCommand(m_cmd_client_id, "<str>", "Client id to use (optional, default='test-*')");
s_addLoggingSendArgumentsStartLogging(argc, argv, api_handle, &cmdUtils);

cmdData returnData = cmdData();
s_parseCommonMQTTCommands(&cmdUtils, &returnData);
returnData.input_clientId =
cmdUtils.GetCommandOrDefault(m_cmd_client_id, Aws::Crt::String("test-") + Aws::Crt::UUID().ToString());
returnData.input_pkcs12File = cmdUtils.GetCommandRequired(m_cmd_pkcs12_file);
returnData.input_pkcs12Password = cmdUtils.GetCommandRequired(m_cmd_pkcs12_password);
return returnData;
}

} // namespace Utils
4 changes: 4 additions & 0 deletions samples/utils/CommandLineUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,9 @@ namespace Utils
Aws::Crt::String input_proxyPassword;
// Shadow
Aws::Crt::String input_shadowProperty;
// PKCS12
Aws::Crt::String input_pkcs12File;
Aws::Crt::String input_pkcs12Password;
// Greengrass Discovery
bool input_PrintDiscoverRespOnly;
};
Expand All @@ -300,5 +303,6 @@ namespace Utils
cmdData parseSampleInputSecureTunnel(int argc, char *argv[], Aws::Crt::ApiHandle *api_handle);
cmdData parseSampleInputSecureTunnelNotification(int argc, char *argv[], Aws::Crt::ApiHandle *api_handle);
cmdData parseSampleInputShadow(int argc, char *argv[], Aws::Crt::ApiHandle *api_handle);
cmdData parseSampleInputPKCS12Connect(int argc, char *argv[], Aws::Crt::ApiHandle *api_handle);

} // namespace Utils
11 changes: 7 additions & 4 deletions utils/run_sample_ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ def launch_sample():

exit_code = 0

print("Launching sample...")
print("Launching sample...", flush=True)

# Java
if (config_json['language'] == "Java"):
Expand All @@ -277,9 +277,12 @@ def launch_sample():

# C++
elif (config_json['language'] == "CPP"):
sample_return = subprocess.run(
args=config_json_arguments_list, executable=config_json['sample_file'])
exit_code = sample_return.returncode
try:
sample_return = subprocess.run(
args=config_json_arguments_list, executable=config_json['sample_file'], timeout=600, stderr=subprocess.STDOUT, stdout=subprocess.PIPE)
exit_code = sample_return.returncode
except subprocess.TimeoutExpired as timeOut:
sys.exit(-1)

elif (config_json['language'] == "Python"):
config_json_arguments_list.append("--is_ci")
Expand Down

0 comments on commit 11855fc

Please sign in to comment.