diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d134f859..7a92abc28 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: - '!main' env: - BUILDER_VERSION: v0.9.11 + BUILDER_VERSION: v0.9.14 BUILDER_SOURCE: releases BUILDER_HOST: https://d19elf31gohf1l.cloudfront.net PACKAGE_NAME: aws-iot-device-sdk-java-v2 diff --git a/pom.xml b/pom.xml index faa659c87..bef673035 100644 --- a/pom.xml +++ b/pom.xml @@ -14,6 +14,7 @@ samples/PubSubStress samples/RawPubSub samples/Pkcs11PubSub + samples/WindowsCertPubSub samples/Shadow samples/Identity diff --git a/samples/Pkcs11PubSub/src/main/java/pkcs11pubsub/Pkcs11PubSub.java b/samples/Pkcs11PubSub/src/main/java/pkcs11pubsub/Pkcs11PubSub.java index ddde08cd3..8a9e62515 100644 --- a/samples/Pkcs11PubSub/src/main/java/pkcs11pubsub/Pkcs11PubSub.java +++ b/samples/Pkcs11PubSub/src/main/java/pkcs11pubsub/Pkcs11PubSub.java @@ -131,16 +131,18 @@ public void onConnectionResumed(boolean sessionPresent) { pkcs11Options.withPrivateKeyObjectLabel(pkcs11KeyLabel); } - try ( - AwsIotMqttConnectionBuilder builder = AwsIotMqttConnectionBuilder - .newMtlsPkcs11Builder(pkcs11Options)) { + try (AwsIotMqttConnectionBuilder builder = AwsIotMqttConnectionBuilder + .newMtlsPkcs11Builder(pkcs11Options)) { if (rootCaPath != null) { builder.withCertificateAuthorityFromPath(null, rootCaPath); } - builder.withConnectionEventCallbacks(callbacks).withClientId(clientId) - .withEndpoint(endpoint).withPort((short) port).withCleanSession(true) + builder.withConnectionEventCallbacks(callbacks) + .withClientId(clientId) + .withEndpoint(endpoint) + .withPort((short) port) + .withCleanSession(true) .withProtocolOperationTimeoutMs(60000); try (MqttClientConnection connection = builder.build()) { diff --git a/samples/README.md b/samples/README.md index e185d3d5b..ee14b38ea 100644 --- a/samples/README.md +++ b/samples/README.md @@ -2,6 +2,7 @@ * [BasicPubSub](#basicpubsub) * [Pkcs11PubSub](#pkcs11pubsub) +* [WindowsCertPubSub](#windowscertpubsub) * [Shadow](#shadow) * [Jobs](#jobs) * [fleet provisioning](#fleet-provisioning) @@ -91,6 +92,70 @@ To run this sample using [SoftHSM2](https://www.opendnssec.org/softhsm/) as the mvn compile exec:java -pl samples/Pkcs11PubSub -Dexec.mainClass=pkcs11pubsub.Pkcs11PubSub -Dexec.args='--endpoint --cert --rootca --pkcs11Lib --pin --tokenLabel --keyLabel ' ``` +## WindowsCertPubSub + +WARNING: Windows only + +This sample shows connecting to IoT Core using mutual TLS, +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/WindowsCertPubSub` + +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: + + ```sh + mvn compile exec:java -pl samples/WindowsCertPubSub "-Dexec.mainClass=windowscertpubsub.WindowsCertPubSub" "-Dexec.args=--endpoint xxxx-ats.iot.xxxx.amazonaws.com --cert CurrentUser\MY\A11F8A9B5DF5B98BA3508FBCA575D09570E0D2C6 --rootca AmazonRootCA1.pem" + ``` + + ## Shadow This sample uses the AWS IoT diff --git a/samples/WindowsCertPubSub/pom.xml b/samples/WindowsCertPubSub/pom.xml new file mode 100644 index 000000000..6c8cd82c7 --- /dev/null +++ b/samples/WindowsCertPubSub/pom.xml @@ -0,0 +1,54 @@ + + 4.0.0 + software.amazon.awssdk.iotdevicesdk + WindowsCertPubSub + jar + 1.0-SNAPSHOT + ${project.groupId}:${project.artifactId} + Sample for connecting to AWS IoT Core with certificate in a Windows certificate store + https://github.com/awslabs/aws-iot-device-sdk-java-v2 + + 1.8 + 1.8 + UTF-8 + + + + software.amazon.awssdk.iotdevicesdk + aws-iot-device-sdk + 1.0.0-SNAPSHOT + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.4.0 + + main + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.2.0 + + + add-source + generate-sources + + add-source + + + + ../Utils/CommandLineUtils + + + + + + + + diff --git a/samples/WindowsCertPubSub/src/main/java/windowscertpubsub/WindowsCertPubSub.java b/samples/WindowsCertPubSub/src/main/java/windowscertpubsub/WindowsCertPubSub.java new file mode 100644 index 000000000..544fb76f3 --- /dev/null +++ b/samples/WindowsCertPubSub/src/main/java/windowscertpubsub/WindowsCertPubSub.java @@ -0,0 +1,152 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package windowscertpubsub; + +import software.amazon.awssdk.crt.*; +import software.amazon.awssdk.crt.io.*; +import software.amazon.awssdk.crt.mqtt.*; +import software.amazon.awssdk.iot.AwsIotMqttConnectionBuilder; + +import java.nio.charset.StandardCharsets; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; + +import utils.commandlineutils.CommandLineUtils; + +public class WindowsCertPubSub { + + // When run normally, we want to exit nicely even if something goes wrong + // When run from CI, we want to let an exception escape which in turn causes the + // exec:java task to return a non-zero exit code + static String ciPropValue = System.getProperty("aws.crt.ci"); + static boolean isCI = ciPropValue != null && Boolean.valueOf(ciPropValue); + + static String clientId = "test-" + UUID.randomUUID().toString(); + static String rootCaPath; + static String windowsCertStorePath; + static String endpoint; + static String topic = "test/topic"; + static String message = "Hello World!"; + static int messagesToPublish = 10; + static int port = 8883; + + static CommandLineUtils cmdUtils; + + /* + * When called during a CI run, throw an exception that will escape and fail the + * exec:java task When called otherwise, print what went wrong (if anything) and + * just continue (return from main) + */ + static void onApplicationFailure(Throwable cause) { + if (isCI) { + throw new RuntimeException("execution failure", cause); + } else if (cause != null) { + System.out.println("Exception encountered: " + cause.toString()); + } + } + + public static void main(String[] args) { + + cmdUtils = new CommandLineUtils(); + cmdUtils.registerProgramName("WindowsCertPubSub"); + cmdUtils.addCommonMQTTCommands(); + cmdUtils.removeCommand("cert"); + cmdUtils.removeCommand("key"); + cmdUtils.registerCommand("cert", "", "Path to certificate in Windows cert store. " + + "e.g. \"CurrentUser\\MY\\6ac133ac58f0a88b83e9c794eba156a98da39b4c\""); + cmdUtils.registerCommand("client_id", "", "Client id to use (optional, default='test-*')."); + cmdUtils.registerCommand("port", "", "Port to connect to on the endpoint (optional, default='8883')."); + cmdUtils.registerCommand("topic", "", "Topic to subscribe/publish to (optional, default='test/topic')."); + cmdUtils.registerCommand("message", "", "Message to publish (optional, default='Hello World')."); + cmdUtils.registerCommand("count", "", "Number of messages to publish (optional, default='10')."); + cmdUtils.registerCommand("help", "", "Prints this message"); + cmdUtils.sendArguments(args); + + if (cmdUtils.hasCommand("help")) { + cmdUtils.printHelp(); + System.exit(1); + } + + endpoint = cmdUtils.getCommandRequired("endpoint", ""); + windowsCertStorePath = cmdUtils.getCommandRequired("cert", ""); + rootCaPath = cmdUtils.getCommandOrDefault("root_ca", rootCaPath); + clientId = cmdUtils.getCommandOrDefault("client_id", clientId); + port = Integer.parseInt(cmdUtils.getCommandOrDefault("port", String.valueOf(port))); + topic = cmdUtils.getCommandOrDefault("topic", topic); + message = cmdUtils.getCommandOrDefault("message", message); + messagesToPublish = Integer.parseInt(cmdUtils.getCommandOrDefault("count", String.valueOf(messagesToPublish))); + + MqttClientConnectionEvents callbacks = new MqttClientConnectionEvents() { + @Override + public void onConnectionInterrupted(int errorCode) { + if (errorCode != 0) { + System.out.println("Connection interrupted: " + errorCode + ": " + CRT.awsErrorString(errorCode)); + } + } + + @Override + public void onConnectionResumed(boolean sessionPresent) { + System.out.println("Connection resumed: " + (sessionPresent ? "existing session" : "clean session")); + } + }; + + try (AwsIotMqttConnectionBuilder builder = + AwsIotMqttConnectionBuilder.newMtlsWindowsCertStorePathBuilder(windowsCertStorePath)) { + + if (rootCaPath != null) { + builder.withCertificateAuthorityFromPath(null, rootCaPath); + } + + builder.withConnectionEventCallbacks(callbacks) + .withClientId(clientId) + .withEndpoint(endpoint) + .withPort((short) port) + .withCleanSession(true) + .withProtocolOperationTimeoutMs(60000); + + try (MqttClientConnection connection = builder.build()) { + + CompletableFuture connected = connection.connect(); + try { + boolean sessionPresent = connected.get(); + System.out.println("Connected to " + (!sessionPresent ? "new" : "existing") + " session!"); + } catch (Exception ex) { + throw new RuntimeException("Exception occurred during connect", ex); + } + + CountDownLatch countDownLatch = new CountDownLatch(messagesToPublish); + + CompletableFuture subscribed = connection.subscribe(topic, QualityOfService.AT_LEAST_ONCE, (message) -> { + String payload = new String(message.getPayload(), StandardCharsets.UTF_8); + System.out.println("MESSAGE: " + payload); + countDownLatch.countDown(); + }); + + subscribed.get(); + + int count = 0; + while (count++ < messagesToPublish) { + CompletableFuture published = connection.publish(new MqttMessage(topic, message.getBytes(), QualityOfService.AT_LEAST_ONCE, false)); + published.get(); + Thread.sleep(1000); + } + + countDownLatch.await(); + + CompletableFuture disconnected = connection.disconnect(); + disconnected.get(); + } + } catch (CrtRuntimeException | InterruptedException | ExecutionException ex) { + onApplicationFailure(ex); + } + + CrtResource.waitForNoResources(); + + System.out.println("Complete!"); + } +} diff --git a/sdk/pom.xml b/sdk/pom.xml index 20a712a6c..d7c3ea031 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -42,7 +42,7 @@ software.amazon.awssdk.crt aws-crt - 0.15.23 + 0.16.0 org.slf4j @@ -169,7 +169,7 @@ software.amazon.awssdk.eventstream* - pubsub:greengrass:identity:jobs:pubsubstress:rawpubsub:pkcs11pubsub:shadow + pubsub:greengrass:identity:jobs:pubsubstress:rawpubsub:pkcs11pubsub:windowscertpubsub:shadow
AWS IoT Device SDK Java V2 API Reference
Copyright © 2021. All rights reserved. diff --git a/sdk/src/main/java/software/amazon/awssdk/iot/AwsIotMqttConnectionBuilder.java b/sdk/src/main/java/software/amazon/awssdk/iot/AwsIotMqttConnectionBuilder.java index 4dd63a2f9..ee2a659af 100644 --- a/sdk/src/main/java/software/amazon/awssdk/iot/AwsIotMqttConnectionBuilder.java +++ b/sdk/src/main/java/software/amazon/awssdk/iot/AwsIotMqttConnectionBuilder.java @@ -129,6 +129,8 @@ public static AwsIotMqttConnectionBuilder newMtlsBuilder(byte[] certificate, byt /** * Create a new builder with mTLS, using a PKCS#11 library for private key operations. * + * NOTE: Unix only + * * @param pkcs11Options PKCS#11 options * @return {@link AwsIotMqttConnectionBuilder} */ @@ -138,6 +140,24 @@ public static AwsIotMqttConnectionBuilder newMtlsPkcs11Builder(TlsContextPkcs11O } } + /** + * Create a new builder with mTLS, using a certificate in a Windows certificate store. + * + * NOTE: Windows only + * + * @param certificatePath Path to certificate in a Windows certificate store. + * The path must use backslashes and end with the + * certificate's thumbprint. Example: + * {@code CurrentUser\MY\A11F8A9B5DF5B98BA3508FBCA575D09570E0D2C6} + * @return {@link AwsIotMqttConnectionBuilder} + */ + public static AwsIotMqttConnectionBuilder newMtlsWindowsCertStorePathBuilder(String certificatePath) { + try (TlsContextOptions tlsContextOptions = TlsContextOptions + .createWithMtlsWindowsCertStorePath(certificatePath)) { + return new AwsIotMqttConnectionBuilder(tlsContextOptions); + } + } + /** * Create a new builder with no default Tls options *