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
--Create various KeyStore files --Modify Java source code
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
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
First, please clone the following Git repository appropriately. https://github.com/tmiki/aws-iot-authorizing-direct-calls-sample
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.
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****";
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.
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();
}
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 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);
}
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;
}
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