diff --git a/.builder/actions/build_samples.py b/.builder/actions/build_samples.py index 838087e2e..78cd1032a 100644 --- a/.builder/actions/build_samples.py +++ b/.builder/actions/build_samples.py @@ -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', diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6214088e5..550fc2fb0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: | @@ -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: 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 000000000..15167cce6 --- /dev/null +++ b/.github/workflows/ci_run_pkcs12_connect_cfg.json @@ -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" + } + ] +} diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt index b4a751f64..494efb564 100644 --- a/samples/CMakeLists.txt +++ b/samples/CMakeLists.txt @@ -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) diff --git a/samples/README.md b/samples/README.md index dba3e90c0..3ca1ed425 100644 --- a/samples/README.md +++ b/samples/README.md @@ -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) @@ -68,6 +69,8 @@ cmake -DCMAKE_PREFIX_PATH="" -DCMAKE_BUILD_ cmake --build . --config "" ``` +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. diff --git a/samples/mqtt/pkcs12_connect/CMakeLists.txt b/samples/mqtt/pkcs12_connect/CMakeLists.txt new file mode 100644 index 000000000..b257f8b37 --- /dev/null +++ b/samples/mqtt/pkcs12_connect/CMakeLists.txt @@ -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) diff --git a/samples/mqtt/pkcs12_connect/README.md b/samples/mqtt/pkcs12_connect/README.md new file mode 100644 index 000000000..c98dd13d2 --- /dev/null +++ b/samples/mqtt/pkcs12_connect/README.md @@ -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. + +
+(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 + +This sample can be run using the following command: + +```sh +./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 +./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 +./pkcs12-connect --endpoint --pkcs12_file --pkcs12_password +``` diff --git a/samples/mqtt/pkcs12_connect/main.cpp b/samples/mqtt/pkcs12_connect/main.cpp new file mode 100644 index 000000000..3ab739276 --- /dev/null +++ b/samples/mqtt/pkcs12_connect/main.cpp @@ -0,0 +1,135 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +#include +#include +#include + +#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 connectionCompletedPromise; + std::promise 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; +} diff --git a/samples/utils/CommandLineUtils.cpp b/samples/utils/CommandLineUtils.cpp index c6ca7a403..40e08b79a 100644 --- a/samples/utils/CommandLineUtils.cpp +++ b/samples/utils/CommandLineUtils.cpp @@ -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() @@ -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 to the PKCS#12 file."); + cmdUtils.RegisterCommand(m_cmd_pkcs12_password, "", "Password for the PKCS#12 file."); + cmdUtils.RegisterCommand(m_cmd_client_id, "", "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 diff --git a/samples/utils/CommandLineUtils.h b/samples/utils/CommandLineUtils.h index 9c48b6d70..16c7b2db0 100644 --- a/samples/utils/CommandLineUtils.h +++ b/samples/utils/CommandLineUtils.h @@ -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; }; @@ -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 diff --git a/utils/run_sample_ci.py b/utils/run_sample_ci.py index 512d6fe27..6990bd119 100644 --- a/utils/run_sample_ci.py +++ b/utils/run_sample_ci.py @@ -252,7 +252,7 @@ def launch_sample(): exit_code = 0 - print("Launching sample...") + print("Launching sample...", flush=True) # Java if (config_json['language'] == "Java"): @@ -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")