Created
November 2, 2017 04:29
-
-
Save Varriount/5f99f862393a186665cd454ae6137ff4 to your computer and use it in GitHub Desktop.
Optimized sigv4.nim
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 SignatureV4 Authorization Library | |
Implements functions to handle the AWS Signature v4 request signing | |
http://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html | |
]# | |
import os, times | |
import strutils except toLower | |
import sequtils, algorithm, tables, nimSHA2 | |
import securehash, hmac, base64, re, unicode | |
from uri import parseUri | |
type | |
AwsCredentials* = tuple | |
id: string | |
secret: string | |
AwsScope* = object | |
date*: string | |
region*: string | |
service*: string | |
# Our AWS4 constants, not quite sure how to handle these, so they act as defaults | |
# TODO - Support more just SHA256 hashing for sigv4 | |
const | |
alg = "AWS4-HMAC-SHA256" | |
term = "aws4_request" | |
# Some convenience operators, for fun and aesthetics | |
proc `$`(s: AwsScope): string = | |
return s.date[0..7] & "/" & s.region & "/" & s.service | |
proc `!$`(s: string): string = | |
return toLowerASCII(hex(computeSHA256(s))) | |
proc `!$`(k, s: string): string = | |
return toLowerASCII(hex(hmac_sha256(k, s))) | |
proc `?$`(k, s: string): string = | |
return $hmac_sha256(k, s) | |
# Copied from cgi library and modified to fit the AWS-approved uri_encode | |
# http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html | |
proc uri_encode(s: string, notEncode: set[char]): string = | |
result = newStringOfCap(s.len + s.len shr 2) # assume 12% non-alnum-chars | |
for i in 0..(s.len - 1): | |
case s[i] | |
of 'a'..'z', 'A'..'Z', '0'..'9', '-', '.', '_', '~': | |
add(result, s[i]) | |
of notEncode: | |
add(result, s[i]) | |
else: | |
add(result, '%') | |
add(result, toHex(ord(s[i]), 2).toUpperASCII) | |
# trim leading and trailing, as well as collapse multiple into single | |
proc condense_whitespace(x: string): string= | |
return strip(x).replace(re"\s+", " ") | |
# don't encode the slashes in the path | |
proc create_canonical_path(path: string): string= | |
return uri_encode(path, {'/'}) | |
# create the canonical querystring string | |
# TODO - Test sigv4 with query string parameters to sign | |
proc create_canonical_qs(query: string): string = | |
result = "" | |
if query.len < 1: | |
return result | |
var queryParts = query.split("&") | |
sort(queryParts, cmp[string]) | |
for part in queryParts: | |
result.add(uri_encode(part, {'='})) | |
# create the canonical and signed headers strings | |
proc create_canonical_and_signed_headers(headers: TableRef): (string, string) = | |
# First create an ordered list of the header names | |
var headerNames = newSeq[string](len(headers)) | |
for index, headerName in keys(headers): | |
shallowCopy(headerNames[index], headerName) | |
sort(headerNames, cmp[string]) | |
# Next, create the canonical headers string and the signed headers string | |
var | |
canonicalHeaders = "" | |
signedHeaders = "" | |
for name in headerNames: | |
let loweredName = toLower(name) | |
signedHeaders.add(loweredName) | |
canonicalHeaders.add(loweredName) | |
canonicalHeaders.add(':') | |
let values = headers[name] | |
for value in values: | |
canonicalHeaders.add(condenseWhitespace(value)) | |
canonicalHeaders.add("\c\l") | |
signedHeaders.add(';') | |
# create the canonical request string | |
# http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html | |
proc create_canonical_request*(headers: var TableRef, action: string, url: string, payload: string="", unsignedPayload: bool=true, contentSha: bool=true): (string, string)= | |
let | |
uri = parseUri(url) | |
cpath = create_canonical_path(uri.path) | |
cquery = create_canonical_qs(uri.query) | |
var hashload = "UNSIGNED-PAYLOAD" | |
if payload.len>0 or not unsignedPayload: | |
# !$a => toLowerASCII(hex(computeSHA256(a))) | |
hashload = !$payload | |
# add the host header for signing, will remove later so we don't have 2 | |
headers["Host"] = @[uri.hostname] | |
# sometimes we don't want/need this, like for the AWS test suite | |
if contentSha: | |
headers["X-Amz-Content-Sha256"] = @[hashload] | |
let (chead, signed) = create_canonical_and_signed_headers(headers) | |
return (signed, ("$1\n$2\n$3\n$4\n$5\n$6" % [action, cpath, cquery, chead, signed, hashload])) | |
# create a signing key with a lot of hashing of the credential scope | |
proc create_signing_key*(secret: string, scope: AwsScope, termination: string=term): string = | |
# (a ?$ b) => $hmac_sha256(a, b) | |
return ("AWS4" & secret) ?$ scope.date[0..7] ?$ scope.region ?$ scope.service ?$ termination | |
# ? cleaner than $hmac_sha256($hmac_sha256($hmac_sha256($hmac_sha256("AWS4" & secret, date[0..7]),region),service),termination) ? | |
# add AWS headers, including Authorization, to the header table, return our signing key (good for 7 days with scope) | |
proc create_aws_authorization*(id: string, | |
key: string, | |
request: (string, string, string), | |
headers: var TableRef, | |
scope: AwsScope, | |
opts: (string, string)=(alg, term)): string= | |
# add our AWS date header | |
# TODO - Check for existing Date or X-Amz-Date header and use that instead | |
# mostly useful for testing I think | |
# check for correct format or let them fail on their own? | |
headers["X-Amz-Date"] = @[scope.date] | |
# create signed headers and canonical request string | |
# http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html | |
let (signed_head, canonical_request) = create_canonical_request(headers, request[0], request[1], request[2]) | |
# delete host header since it's added by the the httpclient.request later and having 2 Host headers is Forbidden | |
headers.del("Host") | |
# create string to sign | |
# http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html | |
let to_sign = "$1\n$2\n$3/$4\n$5" % [opts[0], scope.date, $scope, opts[1], !$canonical_request] | |
# create signing key and export for caching | |
# sign the string with our key | |
# http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html | |
let sig = key !$ to_sign | |
# create AWS authorization header to add to request | |
# http://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html | |
return ("$1 Credential=$2/$3/$4, SignedHeaders=$5, Signature=$6" % [opts[0],id,$scope, opts[1],signed_head, sig]) | |
# add AWS headers, including Authorization, to the header table, return our signing key (good for 7 days withi scope) | |
proc create_aws_authorization*(creds: AwsCredentials, | |
request: (string, string, string), | |
headers: var TableRef, | |
scope: AwsScope, | |
opts: (string, string)=(alg, term)): string= | |
result = create_signing_key(creds[1],scope, opts[1]) | |
# add AWS authorization header | |
# http://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html | |
headers["Authorization"] = @[create_aws_authorization(creds[0],result, request, headers, scope, opts)] | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment