1 /** 2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 * SPDX-License-Identifier: Apache-2.0. 4 */ 5 6 package com.example.customkeyops; 7 8 import software.amazon.awssdk.crt.CRT; 9 import software.amazon.awssdk.crt.CrtResource; 10 import software.amazon.awssdk.crt.CrtRuntimeException; 11 import software.amazon.awssdk.crt.io.*; 12 import software.amazon.awssdk.crt.mqtt.*; 13 14 import java.io.BufferedReader; 15 import java.io.ByteArrayOutputStream; 16 import java.io.FileReader; 17 import java.nio.charset.StandardCharsets; 18 import java.security.KeyFactory; 19 import java.security.PrivateKey; 20 import java.security.Signature; 21 import java.security.interfaces.RSAPrivateKey; 22 import java.security.spec.PKCS8EncodedKeySpec; 23 import java.util.Base64; 24 import java.util.UUID; 25 import java.util.concurrent.CompletableFuture; 26 import java.util.concurrent.CountDownLatch; 27 import java.util.concurrent.ExecutionException; 28 29 /** 30 * A sample for testing the custom private key operations. See the Java V2 SDK sample for a more in-depth 31 * sample with additional options that can be configured via the terminal. This is for testing primarily. 32 */ 33 public class CustomKeyOps { 34 static String clientId = "test-" + UUID.randomUUID().toString(); 35 static String caFilePath; 36 static String certPath; 37 static String keyPath; 38 static String endpoint; 39 static boolean showHelp = false; 40 static int port = 8883; 41 printUsage()42 static void printUsage() { 43 System.out.println( 44 "Usage:\n"+ 45 " --help This message\n"+ 46 " -e|--endpoint AWS IoT service endpoint hostname\n"+ 47 " -p|--port Port to connect to on the endpoint\n"+ 48 " -r|--ca_file Path to the root certificate\n"+ 49 " -c|--cert Path to the IoT thing certificate\n"+ 50 " -k|--key Path to the IoT thing private key in PKCS8 format\n" 51 ); 52 } 53 parseCommandLine(String[] args)54 static void parseCommandLine(String[] args) { 55 for (int idx = 0; idx < args.length; ++idx) { 56 switch (args[idx]) { 57 case "--help": 58 showHelp = true; 59 break; 60 case "-e": 61 case "--endpoint": 62 if (idx + 1 < args.length) { 63 endpoint = args[++idx]; 64 } 65 break; 66 case "-p": 67 case "--port": 68 if (idx + 1 < args.length) { 69 port = Integer.parseInt(args[++idx]); 70 } 71 break; 72 case "-r": 73 case "--ca_file": 74 if (idx + 1 < args.length) { 75 caFilePath = args[++idx]; 76 } 77 break; 78 case "-c": 79 case "--cert": 80 if (idx + 1 < args.length) { 81 certPath = args[++idx]; 82 } 83 break; 84 case "-k": 85 case "--key": 86 if (idx + 1 < args.length) { 87 keyPath = args[++idx]; 88 } 89 break; 90 default: 91 System.out.println("Unrecognized argument: " + args[idx]); 92 } 93 } 94 } 95 96 static class MyKeyOperationHandler implements TlsKeyOperationHandler { 97 RSAPrivateKey key; 98 MyKeyOperationHandler(String keyPath)99 MyKeyOperationHandler(String keyPath) { 100 key = loadPrivateKey(keyPath); 101 } 102 performOperation(TlsKeyOperation operation)103 public void performOperation(TlsKeyOperation operation) { 104 try { 105 System.out.println("MyKeyOperationHandler.performOperation" + operation.getType().name()); 106 107 if (operation.getType() != TlsKeyOperation.Type.SIGN) { 108 throw new RuntimeException("Simple sample only handles SIGN operations"); 109 } 110 111 if (operation.getSignatureAlgorithm() != TlsSignatureAlgorithm.RSA) { 112 throw new RuntimeException("Simple sample only handles RSA keys"); 113 } 114 115 if (operation.getDigestAlgorithm() != TlsHashAlgorithm.SHA256) { 116 throw new RuntimeException("Simple sample only handles SHA256 digests"); 117 } 118 119 // A SIGN operation's inputData is the 32bytes of the SHA-256 digest. 120 // Before doing the RSA signature, we need to construct a PKCS1 v1.5 DigestInfo. 121 // See https://datatracker.ietf.org/doc/html/rfc3447#section-9.2 122 byte[] digest = operation.getInput(); 123 124 // These are the appropriate bytes for the SHA-256 AlgorithmIdentifier: 125 // https://tools.ietf.org/html/rfc3447#page-43 126 byte[] sha256DigestAlgorithm = { 0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, (byte)0x86, 0x48, 0x01, 127 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20 }; 128 129 ByteArrayOutputStream digestInfoStream = new ByteArrayOutputStream(); 130 digestInfoStream.write(sha256DigestAlgorithm); 131 digestInfoStream.write(digest); 132 byte[] digestInfo = digestInfoStream.toByteArray(); 133 134 // Sign the DigestInfo 135 Signature rsaSign = Signature.getInstance("NONEwithRSA"); 136 rsaSign.initSign(key); 137 rsaSign.update(digestInfo); 138 byte[] signatureBytes = rsaSign.sign(); 139 140 operation.complete(signatureBytes); 141 142 } catch (Exception ex) { 143 System.out.println("Error during key operation:" + ex); 144 operation.completeExceptionally(ex); 145 } 146 } 147 loadPrivateKey(String filepath)148 RSAPrivateKey loadPrivateKey(String filepath) { 149 /* Adapted from: https://stackoverflow.com/a/27621696 150 * You probably need to convert your private key file from PKCS#1 151 * to PKCS#8 to get it working with this sample: 152 * 153 * $ openssl pkcs8 -topk8 -in my-private.pem.key -out my-private-pk8.pem.key -nocrypt 154 * 155 * IoT Core vends keys as PKCS#1 by default, 156 * but Java only seems to have this PKCS8EncodedKeySpec class */ 157 try { 158 /* Read the BASE64-encoded contents of the private key file */ 159 StringBuilder pemBase64 = new StringBuilder(); 160 try (BufferedReader reader = new BufferedReader(new FileReader(filepath))) { 161 String line; 162 while ((line = reader.readLine()) != null) { 163 // Strip off PEM header and footer 164 if (line.startsWith("---")) { 165 if (line.contains("RSA")) { 166 throw new RuntimeException("private key must be converted from PKCS#1 to PKCS#8"); 167 } 168 continue; 169 } 170 pemBase64.append(line); 171 } 172 } 173 174 String pemBase64String = pemBase64.toString(); 175 byte[] der = Base64.getDecoder().decode(pemBase64String); 176 177 /* Create PrivateKey instance */ 178 PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(der); 179 KeyFactory keyFactory = KeyFactory.getInstance("RSA"); 180 PrivateKey privateKey = keyFactory.generatePrivate(keySpec); 181 return (RSAPrivateKey)privateKey; 182 183 } catch (Exception ex) { 184 throw new RuntimeException(ex); 185 } 186 } 187 } 188 main(String[] args)189 public static void main(String[] args) { 190 191 parseCommandLine(args); 192 if (showHelp || endpoint == null || certPath == null || keyPath == null) { 193 printUsage(); 194 return; 195 } 196 197 MqttClientConnectionEvents callbacks = new MqttClientConnectionEvents() { 198 @Override 199 public void onConnectionInterrupted(int errorCode) { 200 if (errorCode != 0) { 201 System.out.println("Connection interrupted: " + errorCode + ": " + CRT.awsErrorString(errorCode)); 202 } 203 } 204 205 @Override 206 public void onConnectionResumed(boolean sessionPresent) { 207 System.out.println("Connection resumed: " + (sessionPresent ? "existing session" : "clean session")); 208 } 209 }; 210 211 MyKeyOperationHandler myKeyOperationHandler = new MyKeyOperationHandler(keyPath); 212 TlsContextCustomKeyOperationOptions keyOperationOptions = new TlsContextCustomKeyOperationOptions(myKeyOperationHandler) 213 .withCertificateFilePath(certPath); 214 215 try { 216 217 // ======================================================================================================== 218 // Connection building part of sample 219 EventLoopGroup eventLoopGroup = new EventLoopGroup(1); 220 HostResolver resolver = new HostResolver(eventLoopGroup); 221 ClientBootstrap clientBootstrap = new ClientBootstrap(eventLoopGroup, resolver); 222 223 // Build a connection similar to how it is built in SDK 224 TlsContextOptions tlsOptions = TlsContextOptions.createWithMtlsCustomKeyOperations(keyOperationOptions); 225 MqttConnectionConfig config = new MqttConnectionConfig(); 226 // Set all the settings 227 tlsOptions.overrideDefaultTrustStoreFromPath(null, caFilePath); 228 config.setEndpoint(endpoint); 229 config.setPort(port); 230 config.setClientId(clientId); 231 config.setCleanSession(true); 232 config.setProtocolOperationTimeoutMs(60000); 233 config.setConnectionCallbacks(callbacks); 234 // Build the client and connection 235 ClientTlsContext clientTlsContext = new ClientTlsContext(tlsOptions); 236 MqttClient client = new MqttClient(clientBootstrap, clientTlsContext); 237 config.setMqttClient(client); 238 config.setUsername("?SDK=JavaV2&Version=1.0.0-SNAPSHOT"); 239 MqttClientConnection connection = new MqttClientConnection(config); 240 241 // ======================================================================================================== 242 // Connection/Disconnection part of sample: 243 244 CompletableFuture<Boolean> connected = connection.connect(); 245 try { 246 boolean sessionPresent = connected.get(); 247 System.out.println("Connected to " + (!sessionPresent ? "new" : "existing") + " session!"); 248 } catch (Exception ex) { 249 // Comment this out to test MyKeyOperationHandler exception routes. 250 throw new RuntimeException("Exception occurred during connect", ex); 251 } 252 253 CompletableFuture<Void> disconnected = connection.disconnect(); 254 disconnected.get(); 255 256 // ======================================================================================================== 257 // Close all the stuff 258 connection.close(); 259 client.close(); 260 config.close(); 261 clientTlsContext.close(); 262 tlsOptions.close(); 263 clientBootstrap.close(); 264 resolver.close(); 265 eventLoopGroup.close(); 266 267 } catch (CrtRuntimeException | InterruptedException | ExecutionException ex) { 268 throw new RuntimeException("CustomKeyOpsPubSub execution failure", ex); 269 } 270 271 CrtResource.waitForNoResources(); 272 System.out.println("Complete!"); 273 } 274 } 275