【AWS IoT】Authorizing Direct CallsをJavaで実装する【Java】

# はじめに
AWS IoTには、Thing個別のクライアント証明書を用いてTemporary Security Credentialsを発行し、S3などのAWSリソースにアクセスできる仕組みがあります。
デバイス側の実装としてJavaの例がなかったので試してみました。
\# そもそもIoTデバイスで稼働するアプリをJavaで実装する例が多くないのではと思いますが…

Authorizing Direct Calls基本的な仕組みは下記記事にまとめてあります。
[【AWS IoT】Thing用の個別のクライアント証明書を使ってS3/DynamoDBにアクセスする](https://qiita.com/tmiki/items/84fac41c0cd5adc856ab)

本稿で検証に利用したJavaのコードは下記に置いてあります。
https://github.com/tmiki/aws-iot-authorizing-direct-calls-sample

# 検証した環境
- OS: Windows 10 Pro
- Java: java version "10.0.1" 2018-04-17
- Eclipse: Photon Release (4.8.0)
- OpenSSL: OpenSSL 1.1.1a  20 Nov 2018
- 利用したライブラリ:
    - Apache Commons IO: 2.6
    - Apache HC: 4.5.9
    - AWS SDK for Java: 1.11.618
    - org.json: 20190722
- 各種KeyStoreファイル／パスワード
    - AWSのRootCA局証明書を格納するKeyStore（JKS形式）: AWSRootCaStore.bin
    - 上記KeyStoreのパスワード: password_amazon_root_ca
    - クライアント証明書・秘密鍵を格納するKeyStore（PKCS12形式）: ClientCertKey.p12
    - 上記KeyStoreのパスワード: password_client_cert_key_store
- AWS IoT:
    - IoT Endpoint: abcdefgh123456.credentials.iot.ap-northeast-1.amazonaws.com
    - Thing名: InvestigateAuthorizingDirectCallsThing


# 作業概要
- 各種KeyStoreファイルを作成する
- Javaのソースコードを修正する
- 実行する

# 検証内容
## 各種KeyStoreファイルを作成する
### RootCA局証明書用のKeyStore作成
まず、下記記事をもとに、AWSのRootCA局証明書、クライアント証明書・秘密鍵を準備します。

[【AWS IoT】Thing用の個別のクライアント証明書を使ってS3/DynamoDBにアクセスする](https://qiita.com/tmiki/items/84fac41c0cd5adc856ab)

下記URLからRootCA局証明書をダウンロードし、KeyStoreを作成します。
https://www.amazontrust.com/repository/AmazonRootCA1.pem

作成するKeyStoreファイル名は、「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作成
次に、クライアント証明書用のKeyStoreファイルを作成します。
keytoolコマンドには証明書・秘密鍵を直接インポートする機能がないようなので、いったんOpenSSLを用いてPKCS12形式で作成します。
exportするためのパスワードを聞かれますので、適当に入力します。

```
$ 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
```

先ほど作成したファイルをもとに、クライアント証明書・秘密鍵用のKeyStoreファイルを作成します。
なお、検証が完全にできてはいませんが、JavaはJKS形式だけではなく、PKCS12形式もKeyStoreとして扱えるようです。
もしかしたらこの作業は不要かもしれません。

```
$ 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サンプルコードの準備
まず、下記Gitリポジトリを適当にCloneしてください。
https://github.com/tmiki/aws-iot-authorizing-direct-calls-sample

### KeyStoreファイルの格納
サンプルコードの直下のディレクトリに下記のKeyStoreファイルを格納します。
クライアント証明書・秘密鍵用のKeyStoreとしては、client_cert_key_store.binとClientCertKey.p12は実質同等のものです。Javaはどちらの形式でも扱えるようなので、どちらも対応できるように実装しておきました。
本稿ではClientCertKey.p12を用いて検証します。

- AWSRootCaStore.bin
- client_cert_key_store.bin
- ClientCertKey.p12

### 定数の変更
メインとなるInvestigateAwsIotADCクラスの先頭に定数が定義してありますので、環境に合わせて変更してください。

```
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****";

```

## 実行する
すみません、Eclipseから「InvestigateAwsIotADC」を実行してください。
GradleでJarファイルをビルドしようと思ったのですが、依存するライブラリを含めることができず…
確認次第、ここは更新します。

サンプルコードは、以下の処理を実行します。

- HTTPクライアントオブジェクトを作成し、
- Authorizing Direct CallsのEndpointにHTTPリクエストを発行し、Temporary Security Credentials(AccessKeyId/SecretAccessKey/SessionToken)を取得する
- 取得したTemporary Security CredentialsをもとにAWS S3 APIを発行し、S3上のファイルを取得する
- 取得したファイルの内容をコンソールに出力する

実行に成功すると、コンソールに下記のように出力されます。
WARNINGが出力されることがありますが、ここでは無視します。

```:実行結果
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.
```

# 解説
## HTTPクライアントの生成
まず、Apache HCのSSLContextBuilderを利用してSSLContextオブジェクトを作成しますが、この際にKeyStoreを指定します。
loadTrustMaterial()メソッドで、RootCA局証明書を格納したKeyStoreを指定します。
loadKeyMaterial()メソッドで、クライアント証明書・秘密鍵を指定したKeyStoreを指定します。このとき、KeyStore全体のパスワードと、当該秘密鍵に関するパスワードの2つの指定が必要ですが、PKCS12形式を利用する場合はそのような概念はないため、どちらも同じパスワードを指定するようです。
\# このあたりの仕様がまだ調べ切れておらず、不明確です。

生成したSSLContextオブジェクトを指定してSSLConnectionSocketFactoryを生成します。
CloseableHttpClientオブジェクトを生成する際には、このSSLConnectionSocketFactoryオブジェクトを指定して、buildします。

```java: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();

	}

```

## HTTPリクエストの実行
Apache HCの標準的なやり方でHTTPリクエストを発行します。
この際、「x-amzn-iot-thingname」ヘッダにThing名を指定する必要があります。

HTTPリクエストが成功すると、Temporary Security CredentialsがJSON形式で返却されます。

```java: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;
	}
```

## HTTPレスポンスBodyのパースとBasicSessionCredentialsオブジェクトの生成
返却されたHTTPレスポンスBodyをJSONとしてパースして、AccessKeyId/SecretAccessKey/SessionTokenの値をそれぞれ取り出します。
これを、BasicSessionCredentialsに詰めて返却します。

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

```java: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);
	}

```

## S3上のファイルの取得
BasicSessionCredentialsを利用してS3上のファイルを取得し、その内容をコンソールに出力します。

```java: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;
	}
```

# おわりに
Javaでクライアント証明書を扱うのは想像以上に大変でした。Apache HCがうまいことやってくれているところもありますが、それでもだいぶ煩雑です。

なお、JavaのKeyStoreの仕様をいまいち正確に把握しきれていないのですが、JKS形式だけでなくPKCS12形式も扱えるようです。
これはこれで別途調べてみたいところです。