• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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