From 581a92e25760d12e87c71b53121794f93a874e05 Mon Sep 17 00:00:00 2001 From: Michael Graeb Date: Thu, 24 Mar 2022 16:14:32 -0700 Subject: [PATCH] Support mutual TLS using a certificate from a Windows cert store (#408) Add the ability to use a client certificate located in a Windows certificate store. Previously, the client certificate and private key had to be passed by filepath or file contents. With this change, certificates and keys stored on TPM devices can be used. Add new `windows_cert_pub_sub` sample to show this in action. --- .builder/actions/build_samples.py | 1 + crt/aws-crt-cpp | 2 +- samples/README.md | 69 ++++- .../mqtt/windows_cert_pub_sub/CMakeLists.txt | 25 ++ samples/mqtt/windows_cert_pub_sub/main.cpp | 265 ++++++++++++++++++ 5 files changed, 358 insertions(+), 4 deletions(-) create mode 100644 samples/mqtt/windows_cert_pub_sub/CMakeLists.txt create mode 100644 samples/mqtt/windows_cert_pub_sub/main.cpp diff --git a/.builder/actions/build_samples.py b/.builder/actions/build_samples.py index b0e0e70ae..13d546970 100644 --- a/.builder/actions/build_samples.py +++ b/.builder/actions/build_samples.py @@ -20,6 +20,7 @@ def run(self, env): 'samples/mqtt/basic_pub_sub', 'samples/mqtt/pkcs11_pub_sub', 'samples/mqtt/raw_pub_sub', + 'samples/mqtt/windows_cert_pub_sub', 'samples/shadow/shadow_sync', 'samples/greengrass/basic_discovery', 'samples/identity/fleet_provisioning', diff --git a/crt/aws-crt-cpp b/crt/aws-crt-cpp index 320e2a27d..f8146663a 160000 --- a/crt/aws-crt-cpp +++ b/crt/aws-crt-cpp @@ -1 +1 @@ -Subproject commit 320e2a27df6b1c9a57f04b6e65962cf56bd686d1 +Subproject commit f8146663a02b43415a33be0dff426192cae428cf diff --git a/samples/README.md b/samples/README.md index ebde88f98..2e367b649 100644 --- a/samples/README.md +++ b/samples/README.md @@ -2,6 +2,7 @@ * [Basic MQTT Pub-Sub](#basic-mqtt-pub-sub) * [PKCS#11 MQTT Pub-Sub](#pkcs11-mqtt-pub-sub) +* [Windows Certificate MQTT Pub-Sub](#windows-certificate-mqtt-pub-sub) * [Raw MQTT Pub-Sub](#raw-mqtt-pub-sub) * [Fleet provisioning](#fleet-provisioning) * [Shadow](#shadow) @@ -121,7 +122,7 @@ but the private key for mutual TLS is stored on a PKCS#11 compatible smart card WARNING: Unix only. Currently, TLS integration with PKCS#11 is only available on Unix devices. -source: `samples/mqtt/pkcs11_pub_sub/main/cpp` +source: `samples/mqtt/pkcs11_pub_sub/main.cpp` To run this sample using [SoftHSM2](https://www.opendnssec.org/softhsm/) as the PKCS#11 device: @@ -144,9 +145,9 @@ To run this sample using [SoftHSM2](https://www.opendnssec.org/softhsm/) as the If this spits out an error message, create a config file: * Default location: `~/.config/softhsm2/softhsm2.conf` - * This file must specify token dir, default value is: + * This file must specify a valid token directory: ``` - directories.tokendir = /usr/local/var/lib/softhsm/tokens/ + directories.tokendir = /path/for/my/softhsm/tokens/ ``` 4) Create token and import private key. @@ -167,6 +168,68 @@ To run this sample using [SoftHSM2](https://www.opendnssec.org/softhsm/) as the ./pkcs11-pub-sub --endpoint --ca_file --cert --pkcs11_lib --pin --token_label --key_label ``` +## Windows Certificate MQTT Pub-Sub + +WARNING: Windows only + +This sample is similar to the [Basic Pub-Sub](#basic-mqtt-pub-sub), +but your certificate and private key are in a +[Windows certificate store](https://docs.microsoft.com/en-us/windows-hardware/drivers/install/certificate-stores), +rather than simply being files on disk. + +To run this sample you need the path to your certificate in the store, +which will look something like: +"CurrentUser\MY\A11F8A9B5DF5B98BA3508FBCA575D09570E0D2C6" +(where "CurrentUser\MY" is the store and "A11F8A9B5DF5B98BA3508FBCA575D09570E0D2C6" is the certificate's thumbprint) + +If your certificate and private key are in a +[TPM](https://docs.microsoft.com/en-us/windows/security/information-protection/tpm/trusted-platform-module-overview), +you would use them by passing their certificate store path. + +source: `samples/mqtt/windows_cert_pub_sub/main.cpp` + +To run this sample with a basic certificate from AWS IoT Core: + +1) Create an IoT Thing with a certificate and key if you haven't already. + +2) Combine the certificate and private key into a single .pfx file. + + You will be prompted for a password while creating this file. Remember it for the next step. + + If you have OpenSSL installed: + ```powershell + openssl pkcs12 -in certificate.pem.crt -inkey private.pem.key -out certificate.pfx + ``` + + Otherwise use [CertUtil](https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/certutil). + ```powershell + certutil -mergePFX certificate.pem.crt,private.pem.key certificate.pfx + ``` + +3) Add the .pfx file to a Windows certificate store using PowerShell's + [Import-PfxCertificate](https://docs.microsoft.com/en-us/powershell/module/pki/import-pfxcertificate) + + In this example we're adding it to "CurrentUser\MY" + + ```powershell + $mypwd = Get-Credential -UserName 'Enter password below' -Message 'Enter password below' + Import-PfxCertificate -FilePath certificate.pfx -CertStoreLocation Cert:\CurrentUser\MY -Password $mypwd.Password + ``` + + Note the certificate thumbprint that is printed out: + ``` + Thumbprint Subject + ---------- ------- + A11F8A9B5DF5B98BA3508FBCA575D09570E0D2C6 CN=AWS IoT Certificate + ``` + + So this certificate's path would be: "CurrentUser\MY\A11F8A9B5DF5B98BA3508FBCA575D09570E0D2C6" + +4) Now you can run the sample: + + ``` + .\windows-cert-pub-sub.exe --endpoint xxxx-ats.iot.xxxx.amazonaws.com --ca_file AmazonRootCA.pem --cert CurrentUser\My\A11F8A9B5DF5B98BA3508FBCA575D09570E0D2C6 + ``` ## Raw MQTT Pub-Sub diff --git a/samples/mqtt/windows_cert_pub_sub/CMakeLists.txt b/samples/mqtt/windows_cert_pub_sub/CMakeLists.txt new file mode 100644 index 000000000..6358611cd --- /dev/null +++ b/samples/mqtt/windows_cert_pub_sub/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(windows-cert-pub-sub 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} PRIVATE AWS::aws-crt-cpp) diff --git a/samples/mqtt/windows_cert_pub_sub/main.cpp b/samples/mqtt/windows_cert_pub_sub/main.cpp new file mode 100644 index 000000000..429f89da8 --- /dev/null +++ b/samples/mqtt/windows_cert_pub_sub/main.cpp @@ -0,0 +1,265 @@ +/** + * 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 the Lib ****************************/ + /* + * Do the global initialization for the API. + */ + ApiHandle apiHandle; + + apiHandle.InitializeLogging(LogLevel::Error, stderr); + + /*********************** Parse Arguments ***************************/ + Utils::CommandLineUtils cmdUtils = Utils::CommandLineUtils(); + cmdUtils.RegisterProgramName("windows-cert-pub-sub"); + cmdUtils.AddCommonMQTTCommands(); + cmdUtils.AddCommonTopicMessageCommands(); + cmdUtils.RemoveCommand("cert"); + cmdUtils.RemoveCommand("key"); + cmdUtils.RegisterCommand( + "cert", + "", + "Your client certificate in the Windows certificate store. e.g. " + "'CurrentUser\\MY\\6ac133ac58f0a88b83e9c794eba156a98da39b4c'"); + cmdUtils.RegisterCommand("client_id", "", "Client id to use (optional, default='test-*')."); + cmdUtils.RegisterCommand("help", "", "Prints this message"); + const char **const_argv = (const char **)argv; + cmdUtils.SendArguments(const_argv, const_argv + argc); + + if (cmdUtils.HasCommand("help")) + { + cmdUtils.PrintHelp(); + exit(-1); + } + String endpoint = cmdUtils.GetCommandRequired("endpoint"); + String windowsCertStorePath = cmdUtils.GetCommandRequired("cert"); + String topic = cmdUtils.GetCommandOrDefault("topic", "test/topic"); + String messagePayload = cmdUtils.GetCommandOrDefault("message", "Hello world!"); + int messageCount = std::stoi(cmdUtils.GetCommandOrDefault("count", "10").c_str()); + String caFile = cmdUtils.GetCommandOrDefault("ca_file", ""); + String clientId = cmdUtils.GetCommandOrDefault("client_id", String("test-") + Aws::Crt::UUID().ToString()); + + /********************** Now Setup an Mqtt Client ******************/ + if (apiHandle.GetOrCreateStaticDefaultClientBootstrap()->LastError() != AWS_ERROR_SUCCESS) + { + fprintf( + stderr, + "ClientBootstrap failed with error %s\n", + ErrorDebugString(apiHandle.GetOrCreateStaticDefaultClientBootstrap()->LastError())); + exit(-1); + } + + Aws::Iot::MqttClientConnectionConfigBuilder builder(windowsCertStorePath.c_str()); + if (!builder) + { + fprintf(stderr, "MqttClientConnectionConfigBuilder failed: %s\n", ErrorDebugString(Aws::Crt::LastError())); + exit(-1); + } + + /* + * Note that that remainder of this code is identical to the other "MQTT pub sub" samples. + */ + + if (!caFile.empty()) + { + builder.WithCertificateAuthority(caFile.c_str()); + } + + builder.WithEndpoint(endpoint); + + auto clientConfig = builder.Build(); + if (!clientConfig) + { + fprintf( + stderr, + "Client Configuration initialization failed with error %s\n", + ErrorDebugString(clientConfig.LastError())); + exit(-1); + } + + Aws::Iot::MqttClient mqttClient; + if (!mqttClient) + { + fprintf(stderr, "MQTT Client Creation failed with error %s\n", ErrorDebugString(mqttClient.LastError())); + exit(-1); + } + + /* + * Now create a connection object. Note: This type is move only + * and its underlying memory is managed by the client. + */ + auto connection = mqttClient.NewConnection(clientConfig); + + if (!connection) + { + fprintf(stderr, "MQTT Connection Creation failed with error %s\n", ErrorDebugString(mqttClient.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; + + /* + * This will execute when an mqtt connect has completed or failed. + */ + auto onConnectionCompleted = [&](Mqtt::MqttConnection &, int errorCode, Mqtt::ReturnCode returnCode, bool) { + if (errorCode) + { + fprintf(stdout, "Connection failed with error %s\n", ErrorDebugString(errorCode)); + connectionCompletedPromise.set_value(false); + } + else + { + if (returnCode != AWS_MQTT_CONNECT_ACCEPTED) + { + fprintf(stdout, "Connection failed with mqtt return code %d\n", (int)returnCode); + connectionCompletedPromise.set_value(false); + } + else + { + fprintf(stdout, "Connection completed successfully."); + connectionCompletedPromise.set_value(true); + } + } + }; + + auto onInterrupted = [&](Mqtt::MqttConnection &, int error) { + fprintf(stdout, "Connection interrupted with error %s\n", ErrorDebugString(error)); + }; + + auto onResumed = [&](Mqtt::MqttConnection &, Mqtt::ReturnCode, bool) { fprintf(stdout, "Connection resumed\n"); }; + + /* + * Invoked when a disconnect message has completed. + */ + auto onDisconnect = [&](Mqtt::MqttConnection &) { + { + fprintf(stdout, "Disconnect completed\n"); + connectionClosedPromise.set_value(); + } + }; + + connection->OnConnectionCompleted = std::move(onConnectionCompleted); + connection->OnDisconnect = std::move(onDisconnect); + connection->OnConnectionInterrupted = std::move(onInterrupted); + connection->OnConnectionResumed = std::move(onResumed); + + /* + * Actually perform the connect dance. + */ + fprintf(stdout, "Connecting...\n"); + if (!connection->Connect(clientId.c_str(), false /*cleanSession*/, 1000 /*keepAliveTimeSecs*/)) + { + fprintf(stderr, "Failed to kick off MQTT async Connect(): %s\n", 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); + } + + std::mutex receiveMutex; + std::condition_variable receiveSignal; + int receivedCount = 0; + + /* + * This is invoked upon the receipt of a Publish on a subscribed topic. + */ + auto onMessage = [&](Mqtt::MqttConnection &, + const String &topic, + const ByteBuf &byteBuf, + bool /*dup*/, + Mqtt::QOS /*qos*/, + bool /*retain*/) { + { + std::lock_guard lock(receiveMutex); + ++receivedCount; + fprintf(stdout, "Publish #%d received on topic %s\n", receivedCount, topic.c_str()); + fprintf(stdout, "Message: "); + fwrite(byteBuf.buffer, 1, byteBuf.len, stdout); + fprintf(stdout, "\n"); + } + + receiveSignal.notify_all(); + }; + + /* + * Subscribe for incoming publish messages on topic. + */ + std::promise subscribeFinishedPromise; + auto onSubAck = [&](Mqtt::MqttConnection &, uint16_t packetId, const String &topic, Mqtt::QOS QoS, int errorCode) { + if (errorCode) + { + fprintf(stderr, "Subscribe failed with error %s\n", aws_error_debug_str(errorCode)); + exit(-1); + } + else + { + if (!packetId || QoS == AWS_MQTT_QOS_FAILURE) + { + fprintf(stderr, "Subscribe rejected by the broker."); + exit(-1); + } + else + { + fprintf(stdout, "Subscribe on topic %s on packetId %d Succeeded\n", topic.c_str(), packetId); + } + } + subscribeFinishedPromise.set_value(); + }; + + connection->Subscribe(topic.c_str(), AWS_MQTT_QOS_AT_LEAST_ONCE, onMessage, onSubAck); + subscribeFinishedPromise.get_future().wait(); + + int publishedCount = 0; + while (publishedCount < messageCount) + { + ByteBuf payload = ByteBufFromArray((const uint8_t *)messagePayload.data(), messagePayload.length()); + + auto onPublishComplete = [](Mqtt::MqttConnection &, uint16_t, int) {}; + connection->Publish(topic.c_str(), AWS_MQTT_QOS_AT_LEAST_ONCE, false, payload, onPublishComplete); + ++publishedCount; + + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + } + + { + std::unique_lock receivedLock(receiveMutex); + receiveSignal.wait(receivedLock, [&] { return receivedCount >= messageCount; }); + } + + /* + * Unsubscribe from the topic. + */ + std::promise unsubscribeFinishedPromise; + connection->Unsubscribe( + topic.c_str(), [&](Mqtt::MqttConnection &, uint16_t, int) { unsubscribeFinishedPromise.set_value(); }); + unsubscribeFinishedPromise.get_future().wait(); + + /* Disconnect */ + if (connection->Disconnect()) + { + connectionClosedPromise.get_future().wait(); + } + + return 0; +}