Skip to content

Commit 11855fc

Browse files
Add PKCS12 connect sample (#581)
* 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 <[email protected]>
1 parent e065cff commit 11855fc

File tree

11 files changed

+303
-4
lines changed

11 files changed

+303
-4
lines changed

.builder/actions/build_samples.py

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def run(self, env):
2020
'samples/mqtt/basic_connect',
2121
'samples/mqtt/custom_authorizer_connect',
2222
'samples/mqtt/pkcs11_connect',
23+
'samples/mqtt/pkcs12_connect',
2324
'samples/mqtt/websocket_connect',
2425
'samples/mqtt/windows_cert_connect',
2526
'samples/mqtt/x509_credentials_provider_connect',

.github/workflows/ci.yml

+18
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ jobs:
313313
runs-on: macos-latest
314314
permissions:
315315
id-token: write # This is required for requesting the JWT
316+
security-events: write # This is required for pkcs12 sample to sign the key
316317
steps:
317318
- name: Build ${{ env.PACKAGE_NAME }} + consumers
318319
run: |
@@ -330,6 +331,23 @@ jobs:
330331
- name: run MQTT3 PubSub sample
331332
run: |
332333
python3 ${{ env.CI_UTILS_FOLDER }}/run_sample_ci.py --file ${{ env.CI_SAMPLES_CFG_FOLDER }}/ci_run_pubsub_cfg.json
334+
- name: run PKCS12 sample
335+
run: |
336+
aws s3 cp s3://iot-sdk-ci-bucket-us-east1/pkcs12_identity.p12 ./pkcs12_identity.p12
337+
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\")
338+
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\")
339+
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
340+
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
341+
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\")
342+
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
343+
security create-keychain -p test_password build.keychain
344+
security set-keychain-settings -lut 21600 build.keychain
345+
security default-keychain -s build.keychain
346+
security unlock-keychain -p test_password build.keychain
347+
security import pkcs12_identity.p12 -A -k build.keychain -f pkcs12 -P $pkcs12_identity_password -T /usr/bin/codesign -T /usr/bin/security
348+
security set-key-partition-list -S 'apple-tool:,apple:' -k test_password build.keychain
349+
/usr/bin/codesign --force -s $pkcs12_identity_name ./aws-iot-device-sdk-cpp-v2/build/samples/mqtt/pkcs12_connect/pkcs12-connect -v
350+
python3 ${{ env.CI_UTILS_FOLDER }}/run_sample_ci.py --file ${{ env.CI_SAMPLES_CFG_FOLDER }}/ci_run_pkcs12_connect_cfg.json
333351
- name: configure AWS credentials (MQTT5)
334352
uses: aws-actions/configure-aws-credentials@v1
335353
with:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"language": "CPP",
3+
"sample_file": "./aws-iot-device-sdk-cpp-v2/build/samples/mqtt/pkcs12_connect/pkcs12-connect",
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": "./iot_pkcs12_key.p12"
14+
},
15+
{
16+
"name": "--pkcs12_password",
17+
"secret": "ci/PubSub/key_pkcs12_password"
18+
},
19+
{
20+
"name": "--verbosity",
21+
"data": "Trace"
22+
}
23+
]
24+
}

samples/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ add_subdirectory(jobs/describe_job_execution)
1010
add_subdirectory(mqtt/basic_connect)
1111
add_subdirectory(mqtt/custom_authorizer_connect)
1212
add_subdirectory(mqtt/pkcs11_connect)
13+
add_subdirectory(mqtt/pkcs12_connect)
1314
add_subdirectory(mqtt/websocket_connect)
1415
add_subdirectory(mqtt/windows_cert_connect)
1516
add_subdirectory(mqtt/x509_credentials_provider_connect)

samples/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* [Mqtt5 Shared Subscription](./mqtt5/mqtt5_shared_subscription/README.md)
77
* [Websocket Connect](./mqtt/websocket_connect/README.md)
88
* [PKCS#11 Connect](./mqtt/pkcs11_connect/README.md)
9+
* [PKCS#12 Connect](./mqtt/pkcs12_connect/README.md)
910
* [x509 Credentials Provider Connect](./mqtt/x509_credentials_provider_connect/README.md)
1011
* [Windows Certificate MQTT Connect](./mqtt/windows_cert_connect/README.md)
1112
* [Custom Authorizer Connect](./mqtt/custom_authorizer_connect/README.md)
@@ -68,6 +69,8 @@ cmake -DCMAKE_PREFIX_PATH="<absolute path sdk-cpp-workspace dir>" -DCMAKE_BUILD_
6869
cmake --build . --config "<Release|RelWithDebInfo|Debug>"
6970
```
7071

72+
Note that building all the samples at once is currently only available in the V2 C++ IoT SDK at this time.
73+
7174
### Sample Build Notes
7275

7376
* `-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.
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
cmake_minimum_required(VERSION 3.1)
2+
# note: cxx-17 requires cmake 3.8, cxx-20 requires cmake 3.12
3+
project(pkcs12-connect CXX)
4+
5+
file(GLOB SRC_FILES
6+
"*.cpp"
7+
"../../utils/CommandLineUtils.cpp"
8+
"../../utils/CommandLineUtils.h"
9+
)
10+
11+
add_executable(${PROJECT_NAME} ${SRC_FILES})
12+
13+
set_target_properties(${PROJECT_NAME} PROPERTIES
14+
CXX_STANDARD 14)
15+
16+
#set warnings
17+
if (MSVC)
18+
target_compile_options(${PROJECT_NAME} PRIVATE /W4 /WX /wd4068)
19+
else ()
20+
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wno-long-long -pedantic -Werror)
21+
endif ()
22+
23+
find_package(aws-crt-cpp REQUIRED)
24+
25+
target_link_libraries(${PROJECT_NAME} AWS::aws-crt-cpp)

samples/mqtt/pkcs12_connect/README.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](../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.
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+
This sample can be run using the following command:
41+
42+
```sh
43+
./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+
./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+
./pkcs12-connect --endpoint <endpoint> --pkcs12_file <path to PKCS12 file> --pkcs12_password <password for PKCS12 file>
64+
```

samples/mqtt/pkcs12_connect/main.cpp

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/**
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0.
4+
*/
5+
#include <aws/crt/Api.h>
6+
#include <aws/crt/UUID.h>
7+
#include <aws/crt/io/Pkcs11.h>
8+
9+
#include "../../utils/CommandLineUtils.h"
10+
11+
using namespace Aws::Crt;
12+
13+
int main(int argc, char *argv[])
14+
{
15+
16+
/************************ Setup ****************************/
17+
18+
// Do the global initialization for the API.
19+
ApiHandle apiHandle;
20+
21+
/**
22+
* cmdData is the arguments/input from the command line placed into a single struct for
23+
* use in this sample. This handles all of the command line parsing, validating, etc.
24+
* See the Utils/CommandLineUtils for more information.
25+
*/
26+
Utils::cmdData cmdData = Utils::parseSampleInputPKCS12Connect(argc, argv, &apiHandle);
27+
28+
// Create the MQTT builder and populate it with data from cmdData.
29+
Aws::Iot::MqttClient client;
30+
struct Aws::Iot::Pkcs12Options options;
31+
options.pkcs12_file = cmdData.input_pkcs12File;
32+
options.pkcs12_password = cmdData.input_pkcs12Password;
33+
Aws::Iot::MqttClientConnectionConfigBuilder clientConfigBuilder(options);
34+
if (!clientConfigBuilder)
35+
{
36+
fprintf(
37+
stderr,
38+
"MqttClientConnectionConfigBuilder failed: %s\n",
39+
Aws::Crt::ErrorDebugString(Aws::Crt::LastError()));
40+
exit(-1);
41+
}
42+
if (cmdData.input_ca != "")
43+
{
44+
clientConfigBuilder.WithCertificateAuthority(cmdData.input_ca.c_str());
45+
}
46+
clientConfigBuilder.WithEndpoint(cmdData.input_endpoint);
47+
48+
// Create the MQTT connection from the MQTT builder
49+
auto clientConfig = clientConfigBuilder.Build();
50+
if (!clientConfig)
51+
{
52+
fprintf(
53+
stderr,
54+
"Client Configuration initialization failed with error %s\n",
55+
Aws::Crt::ErrorDebugString(clientConfig.LastError()));
56+
exit(-1);
57+
}
58+
auto connection = client.NewConnection(clientConfig);
59+
if (!*connection)
60+
{
61+
fprintf(
62+
stderr,
63+
"MQTT Connection Creation failed with error %s\n",
64+
Aws::Crt::ErrorDebugString(connection->LastError()));
65+
exit(-1);
66+
}
67+
68+
/**
69+
* In a real world application you probably don't want to enforce synchronous behavior
70+
* but this is a sample console application, so we'll just do that with a condition variable.
71+
*/
72+
std::promise<bool> connectionCompletedPromise;
73+
std::promise<void> connectionClosedPromise;
74+
75+
// Invoked when a MQTT connect has completed or failed
76+
auto onConnectionCompleted =
77+
[&](Aws::Crt::Mqtt::MqttConnection &, int errorCode, Aws::Crt::Mqtt::ReturnCode returnCode, bool) {
78+
if (errorCode)
79+
{
80+
fprintf(stdout, "Connection failed with error %s\n", Aws::Crt::ErrorDebugString(errorCode));
81+
connectionCompletedPromise.set_value(false);
82+
}
83+
else
84+
{
85+
fprintf(stdout, "Connection completed with return code %d\n", returnCode);
86+
connectionCompletedPromise.set_value(true);
87+
}
88+
};
89+
90+
// Invoked when a MQTT connection was interrupted/lost
91+
auto onInterrupted = [&](Aws::Crt::Mqtt::MqttConnection &, int error) {
92+
fprintf(stdout, "Connection interrupted with error %s\n", Aws::Crt::ErrorDebugString(error));
93+
};
94+
95+
// Invoked when a MQTT connection was interrupted/lost, but then reconnected successfully
96+
auto onResumed = [&](Aws::Crt::Mqtt::MqttConnection &, Aws::Crt::Mqtt::ReturnCode, bool) {
97+
fprintf(stdout, "Connection resumed\n");
98+
};
99+
100+
// Invoked when a disconnect message has completed.
101+
auto onDisconnect = [&](Aws::Crt::Mqtt::MqttConnection &) {
102+
fprintf(stdout, "Disconnect completed\n");
103+
connectionClosedPromise.set_value();
104+
};
105+
106+
// Assign callbacks
107+
connection->OnConnectionCompleted = std::move(onConnectionCompleted);
108+
connection->OnDisconnect = std::move(onDisconnect);
109+
connection->OnConnectionInterrupted = std::move(onInterrupted);
110+
connection->OnConnectionResumed = std::move(onResumed);
111+
112+
/************************ Run the sample ****************************/
113+
114+
// Connect
115+
fprintf(stdout, "Connecting...\n");
116+
if (!connection->Connect(cmdData.input_clientId.c_str(), false /*cleanSession*/, 1000 /*keepAliveTimeSecs*/))
117+
{
118+
fprintf(stderr, "MQTT Connection failed with error %s\n", Aws::Crt::ErrorDebugString(connection->LastError()));
119+
exit(-1);
120+
}
121+
122+
// wait for the OnConnectionCompleted callback to fire, which sets connectionCompletedPromise...
123+
if (connectionCompletedPromise.get_future().get() == false)
124+
{
125+
fprintf(stderr, "Connection failed\n");
126+
exit(-1);
127+
}
128+
129+
// Disconnect
130+
if (connection->Disconnect())
131+
{
132+
connectionClosedPromise.get_future().wait();
133+
}
134+
return 0;
135+
}

samples/utils/CommandLineUtils.cpp

+21
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ namespace Utils
6666
static const char *m_cmd_proxy_password = "proxy_password";
6767
static const char *m_cmd_shadow_property = "shadow_property";
6868
static const char *m_cmd_region = "region";
69+
static const char *m_cmd_pkcs12_file = "pkcs12_file";
70+
static const char *m_cmd_pkcs12_password = "pkcs12_password";
6971
static const char *m_cmd_print_discover_resp_only = "print_discover_resp_only";
7072

7173
CommandLineUtils::CommandLineUtils()
@@ -960,4 +962,23 @@ namespace Utils
960962
return returnData;
961963
}
962964

965+
cmdData parseSampleInputPKCS12Connect(int argc, char *argv[], Aws::Crt::ApiHandle *api_handle)
966+
{
967+
CommandLineUtils cmdUtils = CommandLineUtils();
968+
cmdUtils.RegisterProgramName("pkcs12-connect");
969+
cmdUtils.AddCommonMQTTCommands();
970+
cmdUtils.RegisterCommand(m_cmd_pkcs12_file, "<path>", "Path to the PKCS#12 file.");
971+
cmdUtils.RegisterCommand(m_cmd_pkcs12_password, "<str>", "Password for the PKCS#12 file.");
972+
cmdUtils.RegisterCommand(m_cmd_client_id, "<str>", "Client id to use (optional, default='test-*')");
973+
s_addLoggingSendArgumentsStartLogging(argc, argv, api_handle, &cmdUtils);
974+
975+
cmdData returnData = cmdData();
976+
s_parseCommonMQTTCommands(&cmdUtils, &returnData);
977+
returnData.input_clientId =
978+
cmdUtils.GetCommandOrDefault(m_cmd_client_id, Aws::Crt::String("test-") + Aws::Crt::UUID().ToString());
979+
returnData.input_pkcs12File = cmdUtils.GetCommandRequired(m_cmd_pkcs12_file);
980+
returnData.input_pkcs12Password = cmdUtils.GetCommandRequired(m_cmd_pkcs12_password);
981+
return returnData;
982+
}
983+
963984
} // namespace Utils

samples/utils/CommandLineUtils.h

+4
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,9 @@ namespace Utils
274274
Aws::Crt::String input_proxyPassword;
275275
// Shadow
276276
Aws::Crt::String input_shadowProperty;
277+
// PKCS12
278+
Aws::Crt::String input_pkcs12File;
279+
Aws::Crt::String input_pkcs12Password;
277280
// Greengrass Discovery
278281
bool input_PrintDiscoverRespOnly;
279282
};
@@ -300,5 +303,6 @@ namespace Utils
300303
cmdData parseSampleInputSecureTunnel(int argc, char *argv[], Aws::Crt::ApiHandle *api_handle);
301304
cmdData parseSampleInputSecureTunnelNotification(int argc, char *argv[], Aws::Crt::ApiHandle *api_handle);
302305
cmdData parseSampleInputShadow(int argc, char *argv[], Aws::Crt::ApiHandle *api_handle);
306+
cmdData parseSampleInputPKCS12Connect(int argc, char *argv[], Aws::Crt::ApiHandle *api_handle);
303307

304308
} // namespace Utils

utils/run_sample_ci.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ def launch_sample():
252252

253253
exit_code = 0
254254

255-
print("Launching sample...")
255+
print("Launching sample...", flush=True)
256256

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

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

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

0 commit comments

Comments
 (0)