AWS IOT GetThingShadow API 请求发送响应 "Signature expired"

AWS IOT GetThingShadow API request sends response "Signature expired"

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);
    });
});

在 运行 此代码中,我得到的典型响应是

{ message: 'Signature expired: 20201017T000000Z is now earlier than 20201017T073249Z (20201017T073749Z - 5 min.)', traceId: 'b8f04573-2afd-d26a-5f2a-a13dd2dade3' }

我不知道出了什么问题或如何消除此错误。

这里使用ISO格式,结构为YYYYMMDDTHHMMSSZ

Signature expired: YYYYMMDDT000000Z is now earlier than YYYYMMDDT073249Z (YYYYMMDDT073749Z - 5 min.) Why is HHMMSS always zero in the reported message?

我想做的是通过向引用此 (AWS_IOT_GetThingShadow API)

的 API 发送请求来获取“事物”影子文档

但是,为了验证我的请求,我必须做很多其他的事情,这些在此处有说明 Signing AWS requests。我已经简单地执行了提到的 4 个任务/步骤来签署请求。

他们在 python 中提供了示例脚本 (sigv4-signed-request-examples),我按照它来编写代码。

我已经坚持了很长一段时间了。如果有人对此有任何想法,请帮助。

编辑:通过在 STRING_TO_SIGN 和 HEADERS 中使用 X_AMZ_DATE 解决了上述问题。我错误地使用了 DATE。我已经相应地更新了上面的代码。 我得到的新错误是

{ message: 'Credential should be scoped to correct service. ', traceId: 'e711927a-11f4-ae75-c4fe-8cdc5a120c0d' }

我不确定凭据有什么问题。我已经正确设置了 REGION。我将 SERVICE 用作 iot,这对于请求影子 API.

也应该是正确的

编辑:事实证明 iot 是错误的。更改了 SERVICE = "iotdata",现在我可以成功请求影子数据了。找到解决方案 。奇怪的是我在 AWS 文档中找不到它。另一个错误是 CANONICAL_URI = URI 中域之后和查询字符串之前的路径 所以在我的例子中它将是 CANONICAL_URI = /things/${THING_NAME}/shadow

我正在发布我的代码的正确最终版本,以防有人遇到类似问题。 我的原始代码中有三处错误。

  • X_AMZ_DATE (YYYYMMDDTHHMMSSZ) 没有在 HEADERS 和 STRING_TO_SIGN 中使用它。因此,出现 Signature expired 错误。
  • SERVICE 我以为是 iot 但它是 iotdataCredential should be scoped to correct service 错误已解决。
  • CANONICAL_URI 应该只包含域之后和查询参数之前的部分。例如。如果请求 URI 是 https://foo.bar.baz.com/foo1/foo2/foo3?bar1=baz1&bar2=baz2 那么 CANONICAL_URI = "/foo1/foo2/foo3"

const https = require('https');
const crypto = require('crypto');
const utf8 = require('utf8');
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";
const CANONICAL_URI = `/things/${THING_NAME}/shadow`;
const CANONICAL_QUERY_STRING = "";
const HOST = `${ENDPOINT}`;
const DATE = getDate();
const TIME = getTime();
const X_AMZ_DATE = `${DATE}T${TIME}Z`;

// 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");
// console.log(`Payload: ${PAYLOAD_HEX_HASH_ENCODED}`);
// 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 = "iotdata";
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
                };


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);
    });
});