const https = require('https');
const crypto = require('crypto');
const utf8 = require('utf8');
const awsIoT = require('aws-iot-device-sdk');
const {AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY} = require('../../config');
const endpointFile = require('../../endpoint.json');
function sign(key, msg){
// since we are deriving key, refer getSignatureKey function
// we use binary format hash => digest() and not digest('hex')
return crypto.createHmac("sha256", key).update(utf8.encode(msg)).digest();
}
function getSignatureKey(key, date, region, service){
// deriving the key as follows:
// HMAC(HMAC(HMAC(HMAC("AWS4" + kSecret,"20150830"),"us-east-1"),"iam"),"aws4_request")
let kDate = sign(utf8.encode(`AWS4${key}`), date);
let kRegion = sign(kDate, region);
let kService = sign(kRegion, service);
let kSigning = sign(kService, 'aws4_request');
return kSigning;
}
function getTime(){
let timeObj = new Date().toISOString();
let arr = timeObj.split(':');
let len = arr[0].length;
let hh = arr[0].slice(len-2,len);
let mm = arr[1];
let ss = arr[2].slice(0,2);
return `${hh}${mm}${ss}`;
}
function getDate(){
let timeObj = new Date().toISOString();
let arr = timeObj.split('-');
let year = arr[0];
let month = arr[1];
let day = arr[2].slice(0,2);
return `${year}${month}${day}`;
}
const ENDPOINT = endpointFile.endpointAddress;
const THING_NAME = __dirname.split('/').pop();
const URI = `https://${ENDPOINT}/things/${THING_NAME}/shadow`;
console.log(URI);
// access key = access key ID + secret access key
// SigV4
// Signing Key derived from credential scope
// Step1
// Create a canonical request (CR)
// CanonicalRequest =
// HTTPRequestMethod + '\n' +
// CanonicalURI + '\n' +
// CanonicalQueryString + '\n' +
// CanonicalHeaders + '\n' +
// SignedHeaders + '\n' +
// HexEncode(Hash(RequestPayload))
const CONTENT_TYPE = "application/json; charset=utf-8";
const HTTP_REQUEST_METHOD = "GET";
// remove the protocol part from URI and query parameters (none in this case)
const CANONICAL_URI = URI.slice(URI.indexOf('/')+2, URI.length);
// console.log(`CANONICAL_URI: ${CANONICAL_URI}`);
const CANONICAL_QUERY_STRING = "";
const HOST = `${ENDPOINT}`;
const DATE = getDate();
const TIME = getTime();
const X_AMZ_DATE = `${DATE}T${TIME}Z`;
console.log(`X_AMZ_DATE: ${X_AMZ_DATE}`);
// note the trailing \n is present
const CANONICAL_HEADER = `content-type:${CONTENT_TYPE}\n`+
`host:${HOST}\n`+
`x-amz-date:${X_AMZ_DATE}\n`;
const SIGNED_HEADER = "content-type;host;x-amz-date";
// payload is the contents of request body
const PAYLOAD = "";
const PAYLOAD_HEX_HASH_ENCODED = crypto.createHash("sha256").update(utf8.encode(PAYLOAD)).digest("hex");
// string for signing CR_STRING = canonical request + metadata
const CANONICAL_REQUEST = `${HTTP_REQUEST_METHOD}\n`+
`${CANONICAL_URI}\n`+
`${CANONICAL_QUERY_STRING}\n`+
`${CANONICAL_HEADER}\n`+
`${SIGNED_HEADER}\n`+
`${PAYLOAD_HEX_HASH_ENCODED}`;
// Step2
// signing key STR_TO_SIGN
const HASH_ALGO = "AWS4-HMAC-SHA256";
const REGION = "us-east-2";
const SERVICE = "iot";
const CREDENTIAL_SCOPE = `${DATE}/`+
`${REGION}/`+
`${SERVICE}/`+
`aws4_request`;
const STRING_TO_SIGN = `${HASH_ALGO}\n`+
`${X_AMZ_DATE}\n`+
`${CREDENTIAL_SCOPE}\n`+
crypto.createHash("sha256")
.update(CANONICAL_REQUEST)
.digest("hex");
// Step3
const SECRET_KEY = AWS_SECRET_ACCESS_KEY;
const SIGNING_KEY = getSignatureKey(SECRET_KEY, DATE, REGION, SERVICE);
const SIGNATURE = crypto.createHmac("sha256", SIGNING_KEY).update(utf8.encode(STRING_TO_SIGN)).digest("hex");
// Step4
// Add SIGNATURE to HTTP request in a header or as a query string parameter
const ACCESS_KEY_ID = AWS_ACCESS_KEY_ID;
const AUTHORIZATION_HEADER = `${HASH_ALGO}`+
` Credential=`+
`${ACCESS_KEY_ID}`+
`/`+
`${CREDENTIAL_SCOPE}`+
`, SignedHeaders=`+
`${SIGNED_HEADER}`+
`, Signature=`+
`${SIGNATURE}`;
const HEADERS = {
'host':HOST,
'content-type':CONTENT_TYPE,
'Authorization':AUTHORIZATION_HEADER,
'x-amz-date':X_AMZ_DATE
};
const OPTIONS = {
hostname: HOST,
path: `/things/${THING_NAME}/shadow`,
headers: HEADERS
};
// send request
https.get(OPTIONS, res=>{
res.setEncoding("utf-8");
let body = "";
res.on("data", data=>{
body += data;
});
res.on("end", ()=>{
body = JSON.parse(body);
console.log(body);
});
});
On running this code the typical response I'm getting is
{ message: 'Signature expired: 20201017T000000Z is now earlier than 20201017T073249Z (20201017T073749Z - 5 min.)', traceId: 'b8f04573-2afd-d26a-5f2a-a13dd2dade3' }
I don't know what is going wrong or what to do to remove this error.
The ISO format is used here with this structure YYYYMMDDTHHMMSSZ
Signature expired: YYYYMMDDT000000Z is now earlier than YYYYMMDDT073249Z (YYYYMMDDT073749Z - 5 min.) Why is HHMMSS always zero in the reported message?
What I'm trying to do is get "thing" shadow document by sending a request to the API referring to this (AWS_IOT_GetThingShadow API)
However, for authenticating my request I have to do a lot of other stuff which is stated here Signing AWS requests. I have simply performed the 4 tasks / steps mentioned in order to sign the request.
They have provided an example script (sigv4-signed-request-examples) in python which I followed to write my code.
I have been stuck on this for quite a while now. If anyone has any idea about this please help.
EDIT: The above problem was solved by using X_AMZ_DATE in STRING_TO_SIGN and HEADERS. I was wrongly using DATE. I have updated the above code accordingly. New error I am gettiing is
{ message: 'Credential should be scoped to correct service. ', traceId: 'e711927a-11f4-ae75-c4fe-8cdc5a120c0d' }
I am not sure what is wrong with the credentials. I have set the REGION correctly. I am using SERVICE as iot which should be correct as well for requesting shadow API.
EDIT: It turns out iot is wrong. Changed SERVICE = "iotdata" and now I can successfully request shadow data. Solution found here. It is strange that I couldn't find it anywhere in the AWS docs. Another thing wrong was CANONICAL_URI = path in URI after domain and before query strings
So in my case it will be CANONICAL_URI = /things/${THING_NAME}/shadow