Skip to content

Custom TLS key operation support #298

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Aug 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ mvn clean install
mkdir sdk-workspace
cd sdk-workspace
# Clone the CRT repository
# (Use the latest version of the CRT here instead of "v0.17.1")
git clone --branch v0.17.1 --recurse-submodules https://github.com/awslabs/aws-crt-java.git
# (Use the latest version of the CRT here instead of "v0.18.0")
git clone --branch v0.18.0 --recurse-submodules https://github.com/awslabs/aws-crt-java.git
cd aws-crt-java
# Compile and install the CRT
mvn install -Dmaven.test.skip=true
Expand All @@ -102,8 +102,8 @@ NOTE: The shadow sample does not currently complete on android due to its depend
mkdir sdk-workspace
cd sdk-workspace
# Clone the CRT repository
# (Use the latest version of the CRT here instead of "v0.17.1")
git clone --branch v0.17.1 --recurse-submodules https://github.com/awslabs/aws-crt-java.git
# (Use the latest version of the CRT here instead of "v0.18.0")
git clone --branch v0.18.0 --recurse-submodules https://github.com/awslabs/aws-crt-java.git
# Compile and install the CRT for Android
cd aws-crt-java/android
./gradlew connectedCheck # optional, will run the unit tests on any connected devices/emulators
Expand All @@ -127,11 +127,11 @@ repositories {
}

dependencies {
implementation 'software.amazon.awssdk.crt:android:0.17.1'
implementation 'software.amazon.awssdk.crt:android:0.18.0'
}
```

Replace `0.16.4` in `software.amazon.awssdk.crt:android:0.16.4` with the latest version of the CRT.
Replace `0.18.0` in `software.amazon.awssdk.crt:android:0.18.0` with the latest version of the CRT.
Look up the latest CRT version here: https://github.com/awslabs/aws-crt-java/releases

#### Caution
Expand Down Expand Up @@ -170,4 +170,3 @@ We need your help in making this SDK great. Please participate in the community
## License

This library is licensed under the Apache 2.0 License.

1 change: 1 addition & 0 deletions android/app/src/main/assets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ Files required to run samples:
ca-certificates.crt - Taken from any recent Linux /etc/ssl
certificate.pem - IoT Thing Certificate
privatekey.pem - IoT Thing Private Key
privatekey_p8.pem - IoT Thing Private Key in PKCS#8 format
endpoint.txt - IoT ATS Endpoint
AmazonRootCA1.pem - Available from https://www.amazontrust.com/repository/AmazonRootCA1.pem

Expand Down
2 changes: 1 addition & 1 deletion android/iotdevicesdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ repositories {
}

dependencies {
api 'software.amazon.awssdk.crt:aws-crt-android:0.17.1'
api 'software.amazon.awssdk.crt:aws-crt-android:0.18.0'
implementation 'org.slf4j:slf4j-api:1.7.30'
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
Expand Down
16 changes: 16 additions & 0 deletions codebuild/samples/customkeyops-linux.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash

set -e

env

pushd $CODEBUILD_SRC_DIR/samples/CustomKeyOpsPubSub

ENDPOINT=$(aws secretsmanager get-secret-value --secret-id "unit-test/endpoint" --query "SecretString" | cut -f2 -d":" | sed -e 's/[\\\"\}]//g')

mvn compile

echo "Custom Key Ops test"
mvn exec:java -Dexec.mainClass="customkeyopspubsub.CustomKeyOpsPubSub" -Daws.crt.ci="True" -Dexec.arguments="--endpoint,$ENDPOINT,--key,/tmp/privatekey_p8.pem,--cert,/tmp/certificate.pem"

popd
1 change: 1 addition & 0 deletions codebuild/samples/linux-smoke-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ phases:
- $CODEBUILD_SRC_DIR/codebuild/samples/pubsub-linux.sh
- $CODEBUILD_SRC_DIR/codebuild/samples/connect-linux.sh
- $CODEBUILD_SRC_DIR/codebuild/samples/connect-auth-linux.sh
- $CODEBUILD_SRC_DIR/codebuild/samples/customkeyops-linux.sh
post_build:
commands:
- echo Build completed on `date`
Expand Down
1 change: 1 addition & 0 deletions codebuild/samples/setup-linux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ mvn install -DskipTests=true

cert=$(aws secretsmanager get-secret-value --secret-id "unit-test/certificate" --query "SecretString" | cut -f2 -d":" | cut -f2 -d\") && echo -e "$cert" > /tmp/certificate.pem
key=$(aws secretsmanager get-secret-value --secret-id "unit-test/privatekey" --query "SecretString" | cut -f2 -d":" | cut -f2 -d\") && echo -e "$key" > /tmp/privatekey.pem
key_p8=$(aws secretsmanager get-secret-value --secret-id "unit-test/privatekey-p8" --query "SecretString" | cut -f2 -d":" | cut -f2 -d\") && echo -e "$key_p8" > /tmp/privatekey_p8.pem

1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<module>samples/Greengrass</module>
<module>samples/Jobs</module>
<module>samples/PubSubStress</module>
<module>samples/CustomKeyOpsPubSub</module>
<module>samples/WindowsCertConnect</module>
<module>samples/Shadow</module>
<module>samples/Identity</module>
Expand Down
54 changes: 54 additions & 0 deletions samples/CustomKeyOpsPubSub/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>software.amazon.awssdk.iotdevicesdk</groupId>
<artifactId>CustomKeyOpsPubSub</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>${project.groupId}:${project.artifactId}</name>
<description>Java bindings for the AWS IoT Core Service</description>
<url>https://github.com/awslabs/aws-iot-device-sdk-java-v2</url>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>software.amazon.awssdk.iotdevicesdk</groupId>
<artifactId>aws-iot-device-sdk</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.4.0</version>
<configuration>
<mainclass>main</mainclass>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>../Utils/CommandLineUtils</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/

package customkeyopspubsub;

import software.amazon.awssdk.crt.CRT;
import software.amazon.awssdk.crt.CrtResource;
import software.amazon.awssdk.crt.CrtRuntimeException;
import software.amazon.awssdk.crt.io.*;
import software.amazon.awssdk.crt.mqtt.*;
import software.amazon.awssdk.iot.AwsIotMqttConnectionBuilder;

import software.amazon.awssdk.crt.Log;
import software.amazon.awssdk.crt.Log.LogLevel;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.FileReader;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
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 CustomKeyOpsPubSub {

// 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 CommandLineUtils cmdUtils;

static String topic = "test/topic";
static String message = "Hello World!";
static int messagesToPublish = 10;
static String certPath;
static String keyPath;

/*
* 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("CustomKeyOpsPubSub execution failure", cause);
} else if (cause != null) {
System.out.println("Exception encountered: " + cause.toString());
}
}

static class MyKeyOperationHandler implements TlsKeyOperationHandler {
RSAPrivateKey key;

MyKeyOperationHandler(String keyPath) {
key = loadPrivateKey(keyPath);
}

public void performOperation(TlsKeyOperation operation) {
try {
System.out.println("MyKeyOperationHandler.performOperation" + operation.getType().name());

if (operation.getType() != TlsKeyOperation.Type.SIGN) {
throw new RuntimeException("Simple sample only handles SIGN operations");
}

if (operation.getSignatureAlgorithm() != TlsSignatureAlgorithm.RSA) {
throw new RuntimeException("Simple sample only handles RSA keys");
}

if (operation.getDigestAlgorithm() != TlsHashAlgorithm.SHA256) {
throw new RuntimeException("Simple sample only handles SHA256 digests");
}

// A SIGN operation's inputData is the 32bytes of the SHA-256 digest.
// Before doing the RSA signature, we need to construct a PKCS1 v1.5 DigestInfo.
// See https://datatracker.ietf.org/doc/html/rfc3447#section-9.2
byte[] digest = operation.getInput();

// These are the appropriate bytes for the SHA-256 AlgorithmIdentifier:
// https://tools.ietf.org/html/rfc3447#page-43
byte[] sha256DigestAlgorithm = { 0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, (byte)0x86, 0x48, 0x01,
0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20 };

ByteArrayOutputStream digestInfoStream = new ByteArrayOutputStream();
digestInfoStream.write(sha256DigestAlgorithm);
digestInfoStream.write(digest);
byte[] digestInfo = digestInfoStream.toByteArray();

// Sign the DigestInfo
Signature rsaSign = Signature.getInstance("NONEwithRSA");
rsaSign.initSign(key);
rsaSign.update(digestInfo);
byte[] signatureBytes = rsaSign.sign();

operation.complete(signatureBytes);

} catch (Exception ex) {
System.out.println("Error during key operation:" + ex);
operation.completeExceptionally(ex);
}
}

RSAPrivateKey loadPrivateKey(String filepath) {
/* Adapted from: https://stackoverflow.com/a/27621696
* You probably need to convert your private key file from PKCS#1
* to PKCS#8 to get it working with this sample:
*
* $ openssl pkcs8 -topk8 -in my-private.pem.key -out my-private-pk8.pem.key -nocrypt
*
* IoT Core vends keys as PKCS#1 by default,
* but Java only seems to have this PKCS8EncodedKeySpec class */
try {
/* Read the BASE64-encoded contents of the private key file */
StringBuilder pemBase64 = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new FileReader(filepath))) {
String line;
while ((line = reader.readLine()) != null) {
// Strip off PEM header and footer
if (line.startsWith("---")) {
if (line.contains("RSA")) {
throw new RuntimeException("private key must be converted from PKCS#1 to PKCS#8");
}
continue;
}
pemBase64.append(line);
}
}

String pemBase64String = pemBase64.toString();
byte[] der = Base64.getDecoder().decode(pemBase64String);

/* Create PrivateKey instance */
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(der);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
return (RSAPrivateKey)privateKey;

} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}

public static void main(String[] args) {

cmdUtils = new CommandLineUtils();
cmdUtils.registerProgramName("CustomKeyOpsPubSub");
cmdUtils.addCommonMQTTCommands();
cmdUtils.addCommonTopicMessageCommands();
cmdUtils.registerCommand("key", "<path>", "Path to your PKCS#8 key in PEM format.");
cmdUtils.registerCommand("cert", "<path>", "Path to your client certificate in PEM format.");
cmdUtils.registerCommand("client_id", "<int>", "Client id to use (optional, default='test-*').");
cmdUtils.registerCommand("port", "<int>", "Port to connect to on the endpoint (optional, default='8883').");
cmdUtils.registerCommand("count", "<int>", "Number of messages to publish (optional, default='10').");
cmdUtils.sendArguments(args);

keyPath = cmdUtils.getCommandRequired("key", "");
certPath = cmdUtils.getCommandRequired("cert", "");

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"));
}
};

MyKeyOperationHandler myKeyOperationHandler = new MyKeyOperationHandler(keyPath);
TlsContextCustomKeyOperationOptions keyOperationOptions = new TlsContextCustomKeyOperationOptions(myKeyOperationHandler)
.withCertificateFilePath(certPath);

try {
MqttClientConnection connection = cmdUtils.buildCustomKeyOperationConnection(callbacks, keyOperationOptions);
if (connection == null)
{
onApplicationFailure(new RuntimeException("MQTT connection creation failed!"));
}

CompletableFuture<Boolean> 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<Integer> 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<Integer> published = connection.publish(new MqttMessage(topic, message.getBytes(), QualityOfService.AT_LEAST_ONCE, false));
published.get();
Thread.sleep(1000);
}

countDownLatch.await();

CompletableFuture<Void> disconnected = connection.disconnect();
disconnected.get();

connection.close();

} catch (CrtRuntimeException | InterruptedException | ExecutionException ex) {
onApplicationFailure(ex);
}

CrtResource.waitForNoResources();
System.out.println("Complete!");
}
}
Loading