[AWS IoT] Implementing Authorizing Direct Calls in Java [Java]

Introduction

AWS IoT has a mechanism to issue Temporary Security Credentials using Thing's individual client certificates and access AWS resources such as S3. There was no Java example as a device-side implementation, so I tried it. \ # In the first place, I don't think there are many examples of implementing apps that run on IoT devices in Java ...

Authorizing Direct Calls The basic mechanism is summarized in the following article. [AWS IoT] Access S3 / DynamoDB using a separate client certificate for Thing

The Java code used for verification in this article is located below. https://github.com/tmiki/aws-iot-authorizing-direct-calls-sample

Verified environment

Work outline

--Create various KeyStore files --Modify Java source code

Verification content

Create various KeyStore files

Create KeyStore for Root CA certificate

First, prepare the AWS Root CA certificate, client certificate, and private key based on the following article.

[AWS IoT] Access S3 / DynamoDB using a separate client certificate for Thing

Download the Root CA station certificate from the URL below and create a KeyStore. https://www.amazontrust.com/repository/AmazonRootCA1.pem

The name of the KeyStore file to be created is "AWSRootCaStore.bin".

$ keytool -import -trustcacerts -alias AmazonRootCA1 -file ./AmazonRootCA1.pem -keystore AWSRootCaStore.bin -storepass password_amazon_root_ca
Owner: CN=Amazon Root CA 1, O=Amazon, C=US
Issuer: CN=Amazon Root CA 1, O=Amazon, C=US
Serial number: 66c9fcf99bf8c0a39e2f0788a43e696365bca
Valid from: Tue May 26 09:00:00 JST 2015 until: Sun Jan 17 09:00:00 JST 2038
Certificate fingerprints:
      SHA1: 8D:A7:F9:65:EC:5E:FC:37:91:0F:1C:6E:59:FD:C1:CC:6A:6E:DE:16
      SHA256: 8E:CD:E6:88:4F:3D:87:B1:12:5B:A3:1A:C3:FC:B1:3D:70:16:DE:7F:57:CC:90:4F:E1:CB:97:C6:AE:98:19:6E
Signature algorithm name: SHA256withRSA
Subject Public Key Algorithm: 2048-bit RSA key
Version: 3

Extensions:

#1: ObjectId: 2.5.29.19 Criticality=true
BasicConstraints:[
CA:true
PathLen:2147483647
]

#2: ObjectId: 2.5.29.15 Criticality=true
KeyUsage [
DigitalSignature
Key_CertSign
Crl_Sign
]

#3: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 84 18 CC 85 34 EC BC 0C   94 94 2E 08 59 9C C7 B2  ....4.......Y...
0010: 10 4E 0A 08                                        .N..
]
]

Trust this certificate? [no]:  yes
Certificate was added to keystore

$ ls -l AWSRootCaStore.bin
-rw-r--r-- 1 mta 197121 1146 Aug 25 12:09 AWSRootCaStore.bin

KeyStore creation for client certificate / private key

Next, create a KeyStore file for the client certificate. The keytool command does not seem to have a function to directly import the certificate / private key, so create it in PKCS12 format using OpenSSL once. You will be asked for the password to export, so enter it appropriately.

$ openssl pkcs12 -export -in c231554173-certificate.pem.crt -inkey c231554173-private.pem.key -out ClientCertKey.p12
Enter Export Password:
Verifying - Enter Export Password:
     
$ ls -l ClientCertKey.p12
-rw-r--r-- 1 vagrant vagrant 2461 Aug 25 15:54 ClientCertKey.p12

Create a KeyStore file for the client certificate / private key based on the file created earlier. Although it has not been completely verified, Java seems to be able to handle not only JKS format but also PKCS12 format as KeyStore. Perhaps this work is unnecessary.

$ keytool -importkeystore \
  -srckeystore ClientCertKey.p12 -srcstoretype pkcs12 \
  -destkeystore client_cert_key_store.bin -deststorepass "password_client_cert_key_store"
Importing keystore ClientCertKey.p12 to client_cert_key_store.bin...
Enter source keystore password:  previously_input_password
Entry for alias 1 successfully imported.
Import command completed:  1 entries successfully imported, 0 entries failed or cancelled

Java sample code preparation

First, please clone the following Git repository appropriately. https://github.com/tmiki/aws-iot-authorizing-direct-calls-sample

Store KeyStore files

Store the following KeyStore file in the directory directly under the sample code. Client_cert_key_store.bin and ClientCertKey.p12 are practically equivalent as KeyStore for client certificate / private key. Java seems to be able to handle either format, so I implemented it so that it can handle both. In this article, we will use ClientCertKey.p12 for verification.

Change constant

Constants are defined at the beginning of the main InvestigateAwsIotADC class, so change them according to your environment.

public class InvestigateAwsIotADC {
	// Constant values of the Amazon Root CA.
	static String ROOT_CA_FILE_PATH = "AWSRootCaStore.bin";
	static String ROOT_CA_FILE_PASSWORD = "password_amazon_root_ca";

	// Constant values of a client certificate.
	static String CLIENT_CERT_KEY_STORE_PKCS12_FILE_PATH = "ClientCertKey.p12";
	static String CLIENT_CERT_KEY_STORE_PKCS12_PASSWORD = "password_client_cert_key_store";

	// Replace the domain name with yours.
	static String AWS_IOT_ROLE_ALIAS_ENDPOINT = "https://****YourEndpointDomain****/role-aliases/****YourRoleAlias****/credentials";
	static String AWS_IOT_THING_NAME = "****YourThingName***";
	static String AWS_IOT_ADC_HEADER_NAME = "x-amzn-iot-thingname";


	// Replace the AWS resources configurations with yours.
	static Regions AWS_REGION = Regions.AP_NORTHEAST_1;
	static String AWS_S3_BUCKET_NAME = "****YourBucketName****";
	static String AWS_S3_FILE_NAME = "****YourFile****";

Execute

Excuse me, please run "Investigate AwsIot ADC" from Eclipse. I wanted to build a Jar file in Gradle, but I couldn't include the dependent libraries ... We will update here as soon as it is confirmed.

The sample code does the following:

--Create an HTTP client object and --Issue an HTTP request to the Endpoint of Authorizing Direct Calls and get Temporary Security Credentials (AccessKeyId / SecretAccessKey / SessionToken) --Issue AWS S3 API based on the acquired Temporary Security Credentials and acquire the file on S3 --Output the contents of the acquired file to the console

If the execution is successful, the following will be output to the console. WARNING may be output, but ignore it here.

Execution result


Started.
Http request: GET https://abcdefgh123456.credentials.iot.ap-northeast-1.amazonaws.com/role-aliases/InvestigateIotRoleAlias/credentials HTTP/1.1
Http Status Code: 200
Http Response Header: content-type: application/json
Http Response Header: content-length: 1189
Http Response Header: date: Sun, 25 Aug 2019 15:02:34 GMT
Http Response Header: x-amzn-RequestId: 0d33da6d-58d0-f450-aa66-f8278cf6834e
Http Response Body: {"credentials":{"accessKeyId":"ASIA................","secretAccessKey":"NPf6................................","expiration":"2019-08-25T16:02:34Z"}}
WARNING: Your profile name includes a 'profile ' prefix. This is considered part of the profile name in the Java SDK, so you will need to include this prefix in your profile name when you reference this profile from your Java code. [Mon Aug 26 00:02:35 JST 2019]
WARNING: Your profile name includes a 'profile ' prefix. This is considered part of the profile name in the Java SDK, so you will need to include this prefix in your profile name when you reference this profile from your Java code. [Mon Aug 26 00:02:35 JST 2019]
The content of the S3 file: retrieved


Finished.

Commentary

HTTP client generation

First, create an SSLContext object using Apache HC's SSLContextBuilder, but specify the KeyStore at this time. In the loadTrustMaterial () method, specify the KeyStore that stores the RootCA station certificate. In the loadKeyMaterial () method, specify the KeyStore that specifies the client certificate / private key. At this time, it is necessary to specify the password for the entire KeyStore and the password for the private key, but since there is no such concept when using the PKCS12 format, it seems that both specify the same password. \ # The specifications around here have not been investigated yet and are unclear.

Create an SSLConnectionSocketFactory by specifying the generated SSLContext object. When creating the CloseableHttpClient object, specify this SSLConnectionSocketFactory object and build it.

createHttpClient()


	public static CloseableHttpClient createHttpClient() {
		// The "custom()" method returns "SSLContextBuilder".
		// Its parameters are a little complicated.
		//
		// https://hc.apache.org/httpcomponents-core-ga/httpcore/apidocs/org/apache/http/ssl/SSLContexts.html#custom()
		// https://hc.apache.org/httpcomponents-core-ga/httpcore/apidocs/org/apache/http/ssl/SSLContextBuilder.html
		SSLContext sslcontext = null;
		try {
			sslcontext = SSLContexts.custom()//
				.loadKeyMaterial(new File(CLIENT_CERT_KEY_STORE_PKCS12_FILE_PATH), CLIENT_CERT_KEY_STORE_PKCS12_PASSWORD.toCharArray(), CLIENT_CERT_KEY_STORE_PKCS12_PASSWORD.toCharArray())
				.loadTrustMaterial(new File(ROOT_CA_FILE_PATH), ROOT_CA_FILE_PASSWORD.toCharArray())
				.build();

		} catch (Exception e) {
			System.out.println("An Exception has been thrown while creating an SSLContext.");
			e.printStackTrace();
		}


		// https://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/conn/ssl/SSLConnectionSocketFactory.html
		SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext);

		// https://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/client/HttpClients.html
		return HttpClients.custom().setSSLSocketFactory(sslsf).build();

	}

Executing HTTP request

Issue HTTP requests in the standard Apache HC way. At this time, you need to specify the Thing name in the "x-amzn-iot-thingname" header.

If the HTTP request is successful, Temporary Security Credentials will be returned in JSON format.

invokeHttpRequest()


	public static String invokeHttpRequest(CloseableHttpClient httpclient) {

		// Add a header specify an AWS IoT thing name described in the AWS IOT document below.
		// https://docs.aws.amazon.com/iot/latest/developerguide/authorizing-direct-aws.html

		HttpUriRequest request = RequestBuilder.get() //
				.setUri(AWS_IOT_ROLE_ALIAS_ENDPOINT)
				.setHeader(AWS_IOT_ADC_HEADER_NAME, AWS_IOT_THING_NAME)
				.build();


		System.out.println("Http request: "+ request.getRequestLine());

		try {
			CloseableHttpResponse response = httpclient.execute(request);

			// Prints an HTTP Status Code, HTTP Headers and an HTTP Response Body.
			System.out.println("Http Status Code: " + response.getStatusLine().getStatusCode());
			List<Header> headers = Arrays.asList(response.getAllHeaders());
			headers.forEach((header) -> {System.out.println("Http Response Header: " + header);});
			String response_body = IOUtils.toString(response.getEntity().getContent(), "UTF-8");
			System.out.println("Http Response Body: " + response_body);

			response.close();

			return (response_body);

		} catch (IOException e) {
			System.out.println("An Exception has been thrown while invoking an HTTP request.");
			e.printStackTrace();
		}

		return null;
	}

Parse HTTP Response Body and generate BasicSessionCredentials object

Parse the returned HTTP response Body as JSON and retrieve the values of AccessKeyId / SecretAccessKey / SessionToken respectively. This is packed in BasicSessionCredentials and returned.

BasicSessionCredentials (AWS SDK for Java - 1.11.618) https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/BasicSessionCredentials.html

parseCredentialsFromHttpResponse()


	public static BasicSessionCredentials parseCredentialsFromHttpResponse(String http_response) {
		JSONObject response_json = new JSONObject(http_response);

		String access_key_id = response_json.getJSONObject("credentials").getString("accessKeyId");
		String secret_access_key = response_json.getJSONObject("credentials").getString("secretAccessKey");
		String session_token = response_json.getJSONObject("credentials").getString("sessionToken");

		return new BasicSessionCredentials(access_key_id, secret_access_key, session_token);
	}

Get files on S3

Use BasicSessionCredentials to get the file on S3 and output its contents to the console.

getS3Object()


	public static String getS3Object(BasicSessionCredentials temporary_credentials) {
		// See also the following document.
		// https://docs.aws.amazon.com/AmazonS3/latest/dev/AuthUsingTempSessionTokenJava.html
		AmazonS3 s3Client = AmazonS3ClientBuilder.standard() //
				.withCredentials(new AWSStaticCredentialsProvider(temporary_credentials))
				.withRegion(AWS_REGION)
				.build();

		S3Object s3object = s3Client.getObject(AWS_S3_BUCKET_NAME, AWS_S3_FILE_NAME);


		try {
			String s3object_content = IOUtils.toString(s3object.getObjectContent(), "UTF-8");
			return s3object_content;
		} catch (IOException e) {
			System.out.println("An Exception has been thrown while retreiving an S3 object.");
			e.printStackTrace();
		}

		return null;
	}

in conclusion

Working with client certificates in Java was harder than I expected. Apache HC does a good job in some places, but it's still quite complicated.

I haven't fully grasped the Java KeyStore specifications, but it seems that it can handle not only JKS format but also PKCS12 format. This is where I would like to investigate separately.

Recommended Posts

[AWS IoT] Implementing Authorizing Direct Calls in Java [Java]
Try implementing Android Hilt in Java
Try implementing GraphQL server in Java
Deleting AWS S3 Objects in Java
Renamed folders in AWS S3 (Java)
Try running AWS X-Ray in Java
Programming with direct sum types in Java (Neta)
Changes in Java 11
Rock-paper-scissors in Java
Pi in Java
FizzBuzz in Java
It's late! Try implementing Android Notification in Java (Beginner)