Skip to content

Instantly share code, notes, and snippets.

@mstoykov
Last active February 26, 2022 06:33
Show Gist options
  • Save mstoykov/38cc1293daa9080b11e26053589a6865 to your computer and use it in GitHub Desktop.
Save mstoykov/38cc1293daa9080b11e26053589a6865 to your computer and use it in GitHub Desktop.
k6 compatible awsv4 library
/* eslint-__ENV node */
/* eslint no-use-before-define: [0, "nofunc"] */
"use strict";
// sources of inspiration:
// https://web-identity-federation-playground.s3.amazonaws.com/js/sigv4.js
// http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
var crypto = require("k6/crypto");
function createCanonicalRequest(
method,
pathname,
query,
headers,
payload,
doubleEscape
) {
return [
method.toUpperCase(),
createCanonicalURI(
doubleEscape
? pathname
.split(/\//g)
.map(v => encodeURIComponent(v))
.join("/")
: pathname
),
createCanonicalQueryString(query),
createCanonicalHeaders(headers),
createSignedHeaders(headers),
createCanonicalPayload(payload)
].join("\n");
};
exports.createCanonicalRequest = createCanonicalRequest;
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("");
};
exports.createCanonicalHeaders = createCanonicalHeaders;
function createSignedHeaders(headers) {
return Object.keys(headers)
.sort()
.map(function(name) {
return name.toLowerCase().trim();
})
.join(";");
};
exports.createSignedHeaders = createSignedHeaders;
function createCredentialScope(time, region, service) {
return [toDate(time), region, service, "aws4_request"].join("/");
};
exports.createCredentialScope = createCredentialScope;
function createStringToSign(time, region, service, request) {
return [
"AWS4-HMAC-SHA256",
toTime(time),
createCredentialScope(time, region, service),
hash(request, "hex")
].join("\n");
};
exports.createStringToSign = createStringToSign;
function createAuthorizationHeader(
key,
scope,
signedHeaders,
signature
) {
return [
"AWS4-HMAC-SHA256 Credential=" + key + "/" + scope,
"SignedHeaders=" + signedHeaders,
"Signature=" + signature
].join(", ");
};
exports.createAuthorizationHeader = createAuthorizationHeader;
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");
};
exports.createSignature = createSignature;
function createPresignedS3URL(name, options) {
options = options || {};
options.method = options.method || "GET";
options.bucket = options.bucket || __ENV.AWS_S3_BUCKET;
options.signSessionToken = true;
options.doubleEscape = false;
return createPresignedURL(
options.method,
options.bucket + ".s3.amazonaws.com",
"/" + name,
"s3",
"UNSIGNED-PAYLOAD",
options
);
};
exports.createPresignedS3URL = createPresignedS3URL;
function createPresignedURL(
method,
host,
path,
service,
payload,
options
) {
options = options || {};
options.key = options.key || __ENV.AWS_ACCESS_KEY_ID;
options.secret = options.secret || __ENV.AWS_SECRET_ACCESS_KEY;
options.sessionToken = options.sessionToken || __ENV.AWS_SESSION_TOKEN;
options.protocol = options.protocol || "https";
options.timestamp = options.timestamp || Date.now();
options.region = options.region || __ENV.AWS_REGION || "us-east-1";
options.expires = options.expires || 86400; // 24 hours
options.headers = options.headers || {};
options.signSessionToken = options.signSessionToken || false;
options.doubleEscape =
options.doubleEscape !== undefined ? options.doubleEscape : true;
// host is required
options.headers.Host = host;
var query = options.query ? queryParse(options.query) : {};
query["X-Amz-Algorithm"] = "AWS4-HMAC-SHA256";
query["X-Amz-Credential"] =
options.key +
"/" +
createCredentialScope(options.timestamp, options.region, service);
query["X-Amz-Date"] = toTime(options.timestamp);
query["X-Amz-Expires"] = options.expires;
query["X-Amz-SignedHeaders"] = createSignedHeaders(options.headers);
// when a session token must be "signed" into the canonical request
// (needed for some services, such as s3)
if (options.sessionToken && options.signSessionToken) {
query["X-Amz-Security-Token"] = options.sessionToken;
}
var canonicalRequest = createCanonicalRequest(
method,
path,
query,
options.headers,
payload,
options.doubleEscape
);
var stringToSign = createStringToSign(
options.timestamp,
options.region,
service,
canonicalRequest
);
var signature = createSignature(
options.secret,
options.timestamp,
options.region,
service,
stringToSign
);
query["X-Amz-Signature"] = signature;
// when a session token must NOT be "signed" into the canonical request
// (needed for some services, such as IoT)
if (options.sessionToken && !options.signSessionToken) {
query["X-Amz-Security-Token"] = options.sessionToken;
} else {
delete query["X-Amz-Security-Token"];
}
return (
options.protocol + "://" + host + path + "?" + createCanonicalQueryString(query)
);
};
exports.createPresignedURL = createPresignedURL;
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);
}
@mstoykov
Copy link
Author

This aws demo does the following:
Create s3 bucket .. (in Europe) in setup
Each iteration creates a somewhat randomly named object
Tries to read it publicly and checks it fails
Changes the ACL to be public
Tries to read it publicly and checks it succeeds
In teardown the whole bucket is emptied and than deleted

For it to work there will need to be environmental variables AWS_ACCESS_KEY_ID and
AWS_SECRET_ACCESS_KEY in order to authenticate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment