This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
# AWS Signature V4 | |
- https://docs.aws.amazon.com/ja_jp/general/latest/gr/signature-version-4.html | |
## How to use (Deno) | |
```shell | |
export AWS_ACCESS_KEY_ID="(Your AWS Access Key ID)" | |
export AWS_SECRET_ACCESS_KEY="(Your AWS Secret Access Key)" | |
export AWS_SESSION_TOKEN="(Your AWS Session Token)" # Optional | |
$ deno run \ | |
--allow-net=sts.ap-northeast-1.amazonaws.com \ | |
--allow-env=AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_SESSION_TOKEN \ | |
"https://gist.githubusercontent.com/mryhryki/58a1ad77a5e3f3ff14c23324c7b346af/raw/991d7f0a8f31592a720bbb724af797bab8eb7afe/aws_signature_v4.ts" | |
``` | |
*/ | |
// ---------- Environment Variables ---------- | |
const getOptionalEnv = (key: string): string | null | undefined => Deno.env.get(key); | |
const getRequiredEnv = (key: string): string => { | |
const envValue = getOptionalEnv(key); | |
if (envValue != null && envValue.trim() !== "") return envValue; | |
throw new Error(`${key} is not declared`); | |
}; | |
// ---------- Utilities ---------- | |
const textToBin = (text: string): Uint8Array => | |
new TextEncoder().encode(text); | |
const binToHexText = (buf: Uint8Array): string => | |
[...buf].map((b): string => b.toString(16).padStart(2, "0")).join(""); | |
const digestSha256 = async (data: Uint8Array): Promise<Uint8Array> => | |
new Uint8Array(await crypto.subtle.digest("SHA-256", data)); | |
// https://stackoverflow.com/a/56416039 | |
const hmacSha256 = async (key: Uint8Array, message: Uint8Array): Promise<Uint8Array> => { | |
const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" }, true, ["sign"]); | |
const signedData = await crypto.subtle.sign("HMAC", cryptoKey, message); | |
return new Uint8Array(signedData); | |
}; | |
// ---------- AWS Signature V4 ---------- | |
interface AwsParams { | |
awsRegion: string; | |
awsService: string; | |
awsAccessKeyId: string; | |
awsSecretAccessKey: string; | |
awsSessionToken?: string | null; | |
} | |
const signRequest = async (request: Request, params: AwsParams): Promise<Request> => { | |
const dateTimeText = new Date().toISOString().replace(/\.[0-9]{3}/, "").replace(/[-:]/g, ""); | |
const dateText = dateTimeText.substring(0, 8); | |
const url = new URL(request.url); | |
const { awsRegion, awsService, awsAccessKeyId, awsSessionToken, awsSecretAccessKey } = params; | |
// Task1: https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html | |
const signedHeaders = new Headers({ Host: url.host, "X-Amz-Date": dateTimeText }); | |
if (awsSessionToken != null) signedHeaders.set("X-Amz-Security-Token", awsSessionToken); | |
const signedHeadersText: string = Array.from(signedHeaders.entries()) | |
.map(([key]) => key.toLowerCase()) | |
.sort() | |
.join(";"); | |
const canonicalQueryString = Array.from(url.searchParams.entries()) | |
.map(([key, val]) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`) | |
.sort() | |
.join("&"); | |
const canonicalHeaders: string = Array.from(signedHeaders.entries()) | |
.map(([key, val]) => `${key.toLowerCase().trim().replace(/ +/g, " ")}:${val.trim().replace(/ +/g, " ")}\n`) | |
.sort() | |
.join(""); | |
const hashedPayload: string = binToHexText(await digestSha256(new Uint8Array(await request.clone().arrayBuffer()))); | |
const canonicalRequest: string = [ | |
request.method, | |
url.pathname, | |
canonicalQueryString, | |
canonicalHeaders, | |
signedHeadersText, | |
hashedPayload, | |
].join("\n"); | |
const hashedCanonicalRequest: string = binToHexText(await digestSha256(textToBin(canonicalRequest))); | |
// Task2: https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html | |
const stringToSign: string = [ | |
"AWS4-HMAC-SHA256", | |
dateTimeText, | |
`${dateText}/${awsRegion}/${awsService}/aws4_request`, | |
hashedCanonicalRequest, | |
].join("\n"); | |
// Task3: https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html | |
const kDate = await hmacSha256(textToBin(`AWS4${awsSecretAccessKey}`), textToBin(dateText)); | |
const kRegion = await hmacSha256(kDate, textToBin(awsRegion)); | |
const kService = await hmacSha256(kRegion, textToBin(awsService)); | |
const kSigning = await hmacSha256(kService, textToBin("aws4_request")); | |
const signature = binToHexText(await hmacSha256(kSigning, textToBin(stringToSign))); | |
// Task4: https://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html | |
const signedRequest = request.clone(); | |
signedHeaders.forEach((value, name) => signedRequest.headers.set(name, value)); | |
const credential = [ awsAccessKeyId, dateText, awsRegion, awsService, "aws4_request"].join("/"); | |
const authorization = `AWS4-HMAC-SHA256 Credential=${credential}, SignedHeaders=${signedHeadersText}, Signature=${signature}`; | |
signedRequest.headers.set("Authorization", authorization); | |
return signedRequest; | |
}; | |
// ---------- Request Example ---------- | |
// GetCallerIdentity - AWS Security Token Service | |
// https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html | |
const awsRegion = "ap-northeast-1"; | |
const awsService = "sts"; | |
const signedRequest = await signRequest( | |
new Request(`https://${awsService}.${awsRegion}.amazonaws.com/`, { | |
method: "POST", | |
headers: { | |
"Content-Type": "application/x-www-form-urlencoded", | |
"Accept-Encoding": "identity", | |
"Accept": "application/json", | |
}, | |
body: "Action=GetCallerIdentity&Version=2011-06-15", | |
}), | |
{ | |
awsRegion, | |
awsService, | |
awsAccessKeyId: getRequiredEnv("AWS_ACCESS_KEY_ID"), | |
awsSecretAccessKey: getRequiredEnv("AWS_SECRET_ACCESS_KEY"), | |
awsSessionToken: getOptionalEnv("AWS_SESSION_TOKEN"), | |
}, | |
); | |
const response = await fetch(signedRequest); | |
console.log("Status:", response.status); | |
console.log("Body:", JSON.stringify(await response.json(), null, 2)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment