Skip to content

Instantly share code, notes, and snippets.

@huyphan
Created January 5, 2023 10:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save huyphan/5cb684e901f93fb22b2ff5ddadc7f841 to your computer and use it in GitHub Desktop.
Save huyphan/5cb684e901f93fb22b2ff5ddadc7f841 to your computer and use it in GitHub Desktop.
Helper library to allow Grafana K6 to sign SigV4 requests to API Gateway
/** Modified version of https://github.com/s0rc3r3r01/awsv4sign/blob/master/awsv4sign.js to make it work with API Gateway */
/* eslint-__ENV node */
/* eslint no-use-before-define: [0, "nofunc"] */
"use strict";
var crypto = require("k6/crypto");
//v4Sign takes a standard http request object and returns the request signed with AWS V4 Signature
function v4Sign(req, awsCredentials, options) {
options = options || {};
options.timestamp = options.timestamp || Date.now();
options.region = options.region || awsCredentials.region || "us-west-2";
options.key = awsCredentials.accessKeyId;
options.secret = awsCredentials.secretAccessKey;
options.sessionToken = awsCredentials.sessionToken;
options.doubleEscape = false;
//Appending required X-Amz-Date header
req.params = req.params || {};
req.params.headers = req.params.headers || {};
req.params.headers["host"] = req.hostname;
req.params.headers["x-amz-date"] = toTime(options.timestamp);
req.params.headers["x-amz-security-token"] = options.sessionToken;
req.params.headers['x-amz-content-sha256'] = hash(req.payload || '', 'hex')
var signedHeaders = createSignedHeaders(req.params.headers);
var canonicalRequest = createCanonicalRequest(
req.method,
req.path,
req.query,
req.params.headers,
signedHeaders,
req.payload,
options.doubleEscape
);
var scope = createCredentialScope(
options.timestamp,
options.region,
req.service
);
var stringToSign = createStringToSign(
options.timestamp,
scope,
canonicalRequest
);
var signature = createSignature(
options.secret,
options.timestamp,
options.region,
req.service,
stringToSign
);
req.params.headers["Authorization"] = createAuthorizationHeader(
options.key,
scope,
signedHeaders,
signature
);
return req;
}
exports.v4Sign = v4Sign;
function createCanonicalRequest(
method,
pathname,
query,
headers,
signedHeaders,
payload,
doubleEscape
) {
return [
method.toUpperCase(),
createCanonicalURI(
doubleEscape
? pathname
.split(/\//g)
.map((v) => encodeURIComponent(v))
.join("/")
: pathname
),
createCanonicalQueryString(query),
createCanonicalHeaders(headers),
signedHeaders,
createCanonicalPayload(payload),
].join("\n");
}
function createCanonicalURI(uri) {
var url = uri;
if (uri[uri.length - 1] == "/" && url[url.length - 1] != "/") {
url += "/";
}
return url;
}
function queryParse(qs) {
if (typeof qs !== "string" || qs.length === 0) {
return {};
}
var result = {};
var split = qs.split("&");
for (let i = 0; i < split.length; i++) {
let parts = split[i].split("=");
if (parts.length === 2) {
result[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]);
} else {
result[decodeURIComponent(split[i])] = "";
}
}
return result;
}
function createCanonicalPayload(payload) {
if (payload == "UNSIGNED-PAYLOAD") {
return payload;
}
return hash(payload || "", "hex");
}
function createCanonicalQueryString(params) {
if (!params) {
return "";
}
if (typeof params == "string") {
params = queryParse(params);
}
return Object.keys(params)
.sort()
.map(function (key) {
var values = Array.isArray(params[key]) ? params[key] : [params[key]];
return values
.sort()
.map(function (val) {
return encodeURIComponent(key) + "=" + encodeURIComponent(val);
})
.join("&");
})
.join("&");
}
createCanonicalQueryString = createCanonicalQueryString;
function createCanonicalHeaders(headers) {
return Object.keys(headers)
.sort()
.map(function (name) {
var values = Array.isArray(headers[name])
? headers[name]
: [headers[name]];
return (
name.toLowerCase().trim() +
":" +
values
.map(function (v) {
return v.replace(/\s+/g, " ").replace(/^\s+|\s+$/g, "");
})
.join(",") +
"\n"
);
})
.join("");
}
function createSignedHeaders(headers) {
return Object.keys(headers)
.sort()
.map(function (name) {
return name.toLowerCase().trim();
})
.join(";");
}
function createCredentialScope(time, region, service) {
return [toDate(time), region, service, "aws4_request"].join("/");
}
exports.createCredentialScope = createCredentialScope;
function createStringToSign(time, scope, request) {
return ["AWS4-HMAC-SHA256", toTime(time), scope, hash(request, "hex")].join(
"\n"
);
}
function createAuthorizationHeader(key, scope, signedHeaders, signature) {
return [
"AWS4-HMAC-SHA256 Credential=" + key + "/" + scope,
"SignedHeaders=" + signedHeaders,
"Signature=" + signature,
].join(", ");
}
function createSignature(secret, time, region, service, stringToSign) {
var h1 = hmac("AWS4" + secret, toDate(time), "binary"); // date-key
var h2 = hmac(h1, region, "binary"); // region-key
var h3 = hmac(h2, service, "binary"); // service-key
var h4 = hmac(h3, "aws4_request", "binary"); // signing-key
return hmac(h4, stringToSign, "hex");
}
function toTime(time) {
return new Date(time).toISOString().replace(/[:\-]|\.\d{3}/g, "");
}
function toDate(time) {
return toTime(time).substring(0, 8);
}
function hmac(key, data, encoding) {
return crypto.hmac("sha256", key, data, encoding);
}
function hash(string, encoding) {
return crypto.sha256(string, encoding);
}
import { check } from 'k6';
import http from 'k6/http';
import awsv4sign from "./awsv4sign.js";
var crypto = require("k6/crypto");
const CREDENTIALS = {
region: '<your-region>',
accessKeyId: __ENV.AWS_ACCESS_KEY_ID,
secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY,
sessionToken: __ENV.AWS_SESSION_TOKEN,
};
export default function() {
const request = {
method: 'POST',
hostname: '<your-api-gateway>.execute-api.<your-region>.amazonaws.com',
path: '/prod/path/to/your/resource',
service: 'execute-api',
payload: JSON.stringify({
"key": "value"
}),
}
const wrappedReq = awsv4sign.v4Sign(request, CREDENTIALS);
const res = http.post(
'https://<your-api-gateway>.execute-api.<your-region>.amazonaws.com/prod/path/to/your/resource',
wrappedReq.payload,
wrappedReq.params
);
check(res, {
'is status 200': (r) => r.status === 200,
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment