Last active
January 9, 2020 23:06
-
-
Save Varriount/076f406c475b5369301a2fcd7b4527af to your computer and use it in GitHub Desktop.
AWS Request Signing for Nim. Requires the `nimcrypto` package.
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
import strutils | |
import tables | |
import algorithm | |
import httpcore | |
import unicode | |
import strformat | |
import times | |
import nimcrypto | |
import httpclient | |
import utils | |
const | |
decodedChars = {'a'..'z', 'A'..'Z', '0'..'9', '-', '.', '_', '~'} | |
lineFeed = '\l' | |
signingAlgorithm = "AWS4-HMAC-SHA256" | |
# ## Debug Procedures & Templates ## # | |
template updates*(a, b: untyped) = | |
when defined(awsDebug): | |
when b is array or b is seq: | |
for element in b: | |
write stdout, element | |
else: | |
write stdout, b | |
update(a, b) | |
template echoDebugSection(name, body: untyped) = | |
when defined(awsDebug): | |
echo "++++ ", name, " Start ++++" | |
body | |
when defined(awsDebug): | |
echo "\n++++ ", name, " End ++++" | |
echo "" | |
template echoDebugValue(name, value) = | |
when defined(awsDebug): | |
echo "Calculated ", name, ":" | |
echo " ", value | |
echo "" | |
# StringSegment Implementation | |
type | |
Sha256Digest = MDigest[sha256.bits] | |
StringSegment = tuple[start, mid, stop: int] | |
proc sortStringSegments(segments: var seq[StringSegment], source: string) = | |
## Sorts a list of string segments using their corresponding source string. | |
## | |
## Each segment is sorted using lexicographic comparison of it's first | |
## segment (characters between segment.start and segment.stop) | |
segments.sort do (first, second: StringSegment) -> int: | |
result = cmp( | |
toOpenArray(source, first.start, first.stop), | |
toOpenArray(source, second.start, second.stop) | |
) | |
# ## Canonical URI Procs ## # | |
## The canonical URI is the URI-encoded version of the absolute path component | |
## of the URI, which is everything in the URI from the HTTP host to the | |
## question mark character ("?") that begins the query string parameters | |
## (if any) | |
proc updateWithUriEncoded*[T]( | |
result : var T, | |
decodedChars: set[char], | |
data : openarray[char], | |
lowercase = true) = | |
var hexContainer = ['\0', '\0'] | |
for index, character in data: | |
if character in decodedChars + {'/'}: | |
updates(result, character) | |
else: | |
hexCharInto( | |
hexContainer, | |
ord(character), | |
lowercase = lowercase | |
) | |
updates(result, '%') | |
updates(result, hexContainer) | |
proc updateWithCanonicalUri(result: var sha256, uri: openarray[char]) = | |
## Update the given sha256 object with the canonical version of the given URI. | |
updateWithUriEncoded( | |
result = result, | |
decodedChars = decodedChars + {'/'}, | |
data = uri, | |
lowercase = false | |
) | |
# ## Canonical Query String Procs ## # | |
## To construct the canonical query string, complete the following steps: | |
## - Sort the parameter names by character code point in ascending order. | |
## For example, a parameter name that begins with the uppercase letter F | |
## precedes a parameter name that begins with a lowercase letter b. | |
## - URI-encode each parameter name and value according to the following | |
## rules: | |
## - Do not URI-encode any of the unreserved characters that RFC 3986 | |
## defines: A-Z, a-z, 0-9, hyphen ( - ), underscore ( _ ), period ( . ), | |
## and tilde ( ~ ). | |
## - Percent-encode all other characters with %XY, where X and Y are | |
## hexadecimal characters (0-9 and uppercase A-F). For example, the space | |
## character must be encoded as %20 (not using '+', as some encoding | |
## schemes do) and extended UTF-8 characters must be in the form | |
## %XY%ZA%BC. | |
## - Build the canonical query string by starting with the first parameter | |
## name in the sorted list. | |
## - For each parameter, append the URI-encoded parameter name, followed by | |
## the equals sign character (=), followed by the URI-encoded parameter | |
## value. Use an empty string for parameters that have no value. | |
## - Append the ampersand character (&) after each parameter value, except | |
## for the last value in the list. | |
proc findQueryStringSegments(result: var seq[StringSegment], qs: openarray[char]) = | |
## Add the query parameter segments found in a query string to a given | |
## sequence. | |
## | |
## This allows the query canonicalization code to do sorting without copying | |
## the query string into seperate segments - only the segment markers must be | |
## moved. | |
template addSegment(segmentStart, equalsIndex, segmentEnd) = | |
# Attempt to protect against invalid query strings by performing | |
# character position checks. | |
let valid = ( | |
segmentStart < segmentEnd and | |
segmentStart < equalsIndex and | |
equalsIndex < segmentEnd | |
) | |
if valid: | |
add(result, (segmentStart, equalsIndex, segmentEnd)) | |
var | |
lastAmpersand = -1 | |
lastEquals = 0 | |
for index, character in qs: | |
case character: | |
of '&': | |
addSegment(lastAmpersand+1, lastEquals, index-1) | |
lastAmpersand = index | |
of '=': | |
lastEquals = index | |
else: | |
discard | |
addSegment(lastAmpersand+1, lastEquals, high(qs)) | |
proc updateWithCanonicalQueryString(result: var sha256, qs: string) = | |
## Update the given sha256 object with the canonicalized version of the given | |
## query string. | |
if len(qs) == 0: | |
return | |
var segments = newSeq[StringSegment]() | |
# Find and order all the query segments | |
findQueryStringSegments(segments, qs) | |
sortStringSegments(segments, qs) | |
# Add uri-encoded versions of the query segments to the sha256 object. | |
for index, segment in segments: | |
for segmentIndex in segment.start .. segment.stop: | |
updates(result, qs[segmentIndex]) | |
# updateWithUriEncoded( | |
# result = result, | |
# decodedChars = decodedChars, | |
# data = toOpenArray(qs, segment.start, segment.mid - 1) | |
# ) | |
# updates(result, '=') | |
# updateWithUriEncoded( | |
# result = result, | |
# decodedChars = decodedChars, | |
# data = toOpenArray(qs, segment.mid+1, segment.stop) | |
# ) | |
if index != high(segments): | |
updates(result, '&') | |
# ## Header Data Procs ## # | |
## The header canonicalization procedures both need similar transformations | |
## performed on header data: | |
## - Both the header list and the signed header name list need lowered | |
## header names. | |
## - The header list needs values with leading and trailing whitespace | |
## removed, and multiple runs of whitespace condensed. | |
## | |
## To optimize the above process, the below procedures are used to turn an | |
## HttpHeaders object into a HeaderData object, which has had these | |
## transformations performed on it. | |
type HeaderData = tuple[ | |
data : string, | |
segments: seq[StringSegment] | |
] | |
proc addLoweredString(result: var string, name: string) = | |
var oldLen = shiftLen(result, len(name)) | |
for index, character in name: | |
result[oldLen] = toLowerAscii(character) | |
inc oldLen | |
proc addCondensedString(result: var string, source: string) = | |
var | |
startPos = 0 | |
endPos = 0 | |
# Find the start index | |
for index in countUp(0, high(source)): | |
let character = source[index] | |
if character != ' ': | |
startPos = index | |
break | |
# Find the stop index | |
for index in countDown(high(source), 0): | |
let character = source[index] | |
if character != ' ': | |
endPos = index | |
break | |
# Handle cases where the string is entirely whitespace | |
if startPos >= endPos: | |
return | |
# Condense spaces | |
var seenSpace = false | |
template addSpace = | |
if seenSpace: | |
seenSpace = false | |
add(result, ' ') | |
for index in startPos..endPos: | |
let character = source[index] | |
case character | |
of ' ': | |
seenSpace = true | |
else: | |
addSpace() | |
add(result, character) | |
addSpace() | |
proc createHeaderData(headers: HttpHeaders): HeaderData = | |
result.data = "" | |
result.segments = @[] | |
# Add the canonical headers | |
for key, values in headers.table: | |
var segment: StringSegment | |
segment.start = len(result.data) | |
# Add the lowered version of the header | |
addLoweredString(result.data, key) | |
add(result.data, ':') | |
segment.mid = high(result.data) | |
# Add a trimmed, comma-separated string | |
for value in values: | |
addCondensedString(result.data, value) | |
add(result.data, ',') | |
# Add a newline | |
setLen(result.data, high(result.data)) | |
segment.stop = high(result.data) | |
add(result.data, '\l') | |
# Add the segment | |
add(result.segments, segment) | |
# Order the segments | |
sortStringSegments(result.segments, result.data) | |
# ## Canonical Header Procs ## # | |
## The canonical headers consist of a list of all the HTTP headers that you are | |
## including with the signed request. | |
## | |
## To create the canonical headers list, convert all header names to lowercase | |
## and remove leading spaces and trailing spaces. Convert sequential spaces in | |
## the header value to a single space. | |
## | |
## Build the canonical headers list by sorting the (lowercase) headers by | |
## character code and then iterating through the header names. | |
## Construct each header according to the following rules: | |
## - Append the lowercase header name followed by a colon. | |
## - Append a comma-separated list of values for that header. | |
## Do not sort the values in headers that have multiple values. | |
## - Append a new line ('\n'). | |
## | |
## In the canonical form, the following changes were made: | |
## - The header names were converted to lowercase characters. | |
## - The headers were sorted by character code. | |
## - Leading and trailing spaces were removed from the header values. | |
## - Sequential spaces in a b c were converted to a single space for the | |
## header values. | |
proc updateWithCanonicalHeaders(result: var sha256, headerData: HeaderData) = | |
for segment in headerData.segments: | |
updates( | |
result, | |
toOpenArray(headerData.data, segment.start, segment.stop) | |
) | |
updates(result, lineFeed) | |
# ## Signed Header Procs ## # | |
## This signed header list is the list of headers that you included in the | |
## canonical headers. By adding this list of headers, you tell AWS which | |
## headers in the request are part of the signing process and which ones AWS | |
## can ignore (for example, any additional headers added by a proxy) for | |
## purposes of validating the request. | |
## | |
## To create the signed headers list, convert all header names to lowercase, | |
## sort them by character code, and use a semicolon to separate the header | |
## names. | |
## | |
## For each header name except the last, append a semicolon (';') to the header | |
## name to separate it from the following header name. | |
proc updateWithHeaderList(result: var sha256, headerData: HeaderData) = | |
for index, segment in headerData.segments: | |
updates( | |
result, | |
toOpenArray(headerData.data, segment.start, segment.mid-1) | |
) | |
if index != high(headerData.segments): | |
updates(result, ';') | |
proc addHeaderList(result: var string, headerData: HeaderData) = | |
for index, segment in headerData.segments: | |
add( | |
result, | |
toOpenArray(headerData.data, segment.start, segment.mid-1) | |
) | |
if index != high(headerData.segments): | |
add(result, ';') | |
# ## Hash Payload procs ## # | |
## Use a hash (digest) function like SHA256 to create a hashed value from the | |
## payload in the body of the HTTP or HTTPS request. | |
## | |
## The hashed payload must be represented as a lowercase hexadecimal string. | |
## If the payload is empty, use an empty string as the input to the hash | |
## function. | |
proc updateWithPayloadHash(result: var sha256, payload: openarray[char]) = | |
let payloadDigest = digest(sha256, payload) | |
for dataByte in payloadDigest.data: | |
updates( | |
result, | |
hexArray(dataByte, lowercase = true) | |
) | |
# ## Primary Signing Procs ## # | |
proc createCredentialScope(region, service, date: openarray[char]): string = | |
result = fmt("{date}/{region}/{service}/aws4_request") | |
proc createCanonicalRequestHash*( | |
httpMethod : openarray[char], | |
uri : openarray[char], | |
queryString: string, | |
payload : openarray[char], | |
headerData : HeaderData): Sha256Digest = | |
var shaContext: sha256 | |
init(shaContext) | |
echoDebugSection("Canonical Request") do: | |
updates(shaContext, httpMethod) | |
updates(shaContext, lineFeed) | |
updateWithCanonicalUri(shaContext, uri) | |
updates(shaContext, lineFeed) | |
updateWithCanonicalQueryString(shaContext, queryString) | |
updates(shaContext, lineFeed) | |
updateWithCanonicalHeaders(shaContext, headerData) | |
updates(shaContext, lineFeed) | |
updateWithHeaderList(shaContext, headerData) | |
updates(shaContext, lineFeed) | |
updateWithPayloadHash(shaContext, payload) | |
result = finish(shaContext) | |
echoDebugValue("Canonical Request Hash", toLowerAscii($result)) | |
proc addSignature( | |
result : var string, | |
requestDateTimeString: openarray[char], | |
credentialScope : openarray[char], | |
canonicalRequestHash : Sha256Digest, | |
signingKey : Sha256Digest) = | |
## To create the string to sign, concatenate the algorithm, date and time, | |
## credential scope, and digest of the canonical request, as shown in the | |
## following pseudocode: | |
## StringToSign = | |
## Algorithm + \n + | |
## RequestDateTime + \n + | |
## CredentialScope + \n + | |
## HashedCanonicalRequest | |
var signingHmac: HMAC[sha256] | |
init(signingHmac, signingKey.data) | |
echoDebugSection("String to Sign") do: | |
updates(signingHmac, signingAlgorithm) | |
updates(signingHmac, lineFeed) | |
updates(signingHmac, requestDateTimeString) | |
updates(signingHmac, lineFeed) | |
updates(signingHmac, credentialScope) | |
updates(signingHmac, lineFeed) | |
for dataByte in canonicalRequestHash.data: | |
updates( | |
signingHmac, | |
hexArray(dataByte, lowercase = true) | |
) | |
let digest = finish(signingHmac) | |
for d in digest.data: | |
result &= hexArray(d, lowercase=true) | |
echoDebugValue("Signature", toLowerAscii($digest)) | |
proc createSigningKey( | |
secretKey : openarray[char], | |
requestDateString: openarray[char], | |
region : openarray[char], | |
service : openarray[char]): Sha256Digest = | |
## To create the signing key, use the secret access key to create a series of | |
## hash-based message authentication codes (HMACs). This is shown in the | |
## following pseudocode, where HMAC(key, data) represents an HMAC-SHA256 | |
## function that returns output in binary format. The result of each hash | |
## function becomes input for the next one. | |
## | |
## Pseudocode for deriving a signing key: | |
## kSecret = your secret access key | |
## kDate = HMAC("AWS4" + kSecret, Date) | |
## kRegion = HMAC(kDate, Region) | |
## kService = HMAC(kRegion, Service) | |
## kSigning = HMAC(kService, "aws4_request") | |
let | |
dateKey = hmac(sha256, "AWS4" & secretKey, requestDateString) | |
regionKey = hmac(sha256, dateKey.data, region) | |
serviceKey = hmac(sha256, regionKey.data, service) | |
result = hmac(sha256, serviceKey.data, "aws4_request") | |
proc addAwsAuthSignature( | |
result : var string, | |
httpMethod : openarray[char], | |
uri : openarray[char], | |
queryString : string, | |
payload : openarray[char], | |
headerData : HeaderData, | |
region : openarray[char], | |
service : openarray[char], | |
secretKey : openarray[char], | |
requestDateString : openarray[char], | |
requestDateTimeString: openarray[char], | |
credentialScope : openarray[char]) = | |
when defined(awsDebug): | |
echo "Creating AWS Auth Signature:" | |
echo fmt" result = {repr(result)}" | |
echo fmt" httpMethod = {repr($httpMethod)}" | |
echo fmt" uri = {repr($uri)}" | |
echo fmt" queryString = {repr(queryString)}" | |
echo fmt" payload = {repr($payload)}" | |
echo fmt" headerData = {headerData}" | |
echo fmt" region = {repr($region)}" | |
echo fmt" service = {repr($service)}" | |
echo fmt" secretKey = {repr($secretKey)}" | |
echo fmt" requestDateString = {repr($requestDateString)}" | |
echo fmt" requestDateTimeString = {repr($requestDateTimeString)}" | |
echo fmt" credentialScope = {repr($credentialScope)}" | |
echo "" | |
let | |
canonicalRequestHash = createCanonicalRequestHash( | |
httpMethod = httpMethod, | |
uri = uri, | |
queryString = queryString, | |
payload = payload, | |
headerData = headerData | |
) | |
signingKey = createSigningKey( | |
secretKey = secretKey, | |
requestDateString = requestDateString, | |
region = region, | |
service = service | |
) | |
addSignature( | |
result = result, | |
requestDateTimeString = requestDateTimeString, | |
credentialScope = credentialScope, | |
canonicalRequestHash = canonicalRequestHash, | |
signingKey = signingKey | |
) | |
proc addAwsQueryAuth*( | |
queryString : var string, | |
httpMethod : openarray[char], | |
uri : openarray[char], | |
payload : openarray[char], | |
headers : HttpHeaders, | |
region : openarray[char], | |
service : openarray[char], | |
secretKey : openarray[char], | |
accessKey : openarray[char], | |
requestDateTime: DateTime, | |
expires : int) = | |
let | |
requestDateTimeString = format(requestDateTime, "yyyyMMdd'T'HHmmss'Z'") | |
requestDateString = requestDateTimeString[0..7] | |
var | |
headerData = createHeaderData(headers) | |
credentialScope = createCredentialScope(region, service, requestDateString) | |
template addParam(name, value) = | |
add(queryString, '&') | |
add(queryString, name) | |
add(queryString, '=') | |
add(queryString, value) | |
addParam("X-Amz-Algorithm", signingAlgorithm) | |
addParam("X-Amz-Date", requestDateTimeString) | |
addParam("X-Amz-Expires", expires) | |
add(queryString, "&X-Amz-Credential=") | |
add(queryString, accessKey) | |
add(queryString, "/") | |
add(queryString, credentialScope) | |
add(queryString, "&X-Amz-SignedHeaders=") | |
addHeaderList(queryString, headerData) | |
var signatureValue = "" | |
addAwsAuthSignature( | |
result = signatureValue, | |
httpMethod = httpMethod, | |
uri = uri, | |
queryString = queryString, | |
payload = payload, | |
headerData = headerData, | |
region = region, | |
service = service, | |
secretKey = secretKey, | |
requestDateString = requestDateString, | |
requestDateTimeString = requestDateTimeString, | |
credentialScope = credentialScope | |
) | |
addParam("X-Amz-Signature", signatureValue) | |
proc addAwsHeaderAuth*( | |
headers : var HttpHeaders, | |
httpMethod : openarray[char], | |
uri : openarray[char], | |
queryString : string, | |
payload : openarray[char], | |
region : openarray[char], | |
service : openarray[char], | |
secretKey : openarray[char], | |
accessKey : openarray[char], | |
requestDateTime: DateTime) = | |
## Creates the authorization string required by requests to the AWS API. | |
## The produced string must be used in the "Authorization" header. | |
## If you need to use pre-signed authorization query parameters, use | |
## `awsQueryAuth` instead. | |
## | |
## NOTE: | |
## This procedure makes the following assumptions: | |
## - The queryString parameter does *not* start with an '?'. | |
## - The URI has been formatted for use by the desired service. | |
## This usually means normalization. | |
let | |
requestDateTimeString = format(requestDateTime, "yyyyMMdd'T'HHmmss'Z'") | |
requestDateString = requestDateTimeString[0..7] | |
headers["X-Amz-Date"] = requestDateTimeString | |
let | |
headerData = createHeaderData(headers) | |
credentialScope = createCredentialScope(region, service, requestDateString) | |
var headerValue = fmt( | |
"AWS4-HMAC-SHA256 " & | |
"Credential={accessKey}/{credentialScope}, " & | |
"SignedHeaders=" | |
) | |
addHeaderList(headerValue, headerData) | |
add(headerValue, ", Signature=") | |
addAwsAuthSignature( | |
result = headerValue, | |
httpMethod = httpMethod, | |
uri = uri, | |
queryString = queryString, | |
payload = payload, | |
headerData = headerData, | |
region = region, | |
service = service, | |
secretKey = secretKey, | |
requestDateString = requestDateString, | |
requestDateTimeString = requestDateTimeString, | |
credentialScope = credentialScope | |
) | |
headers["Authorization"] = headerValue |
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
import httputils | |
import sequtils | |
import times | |
from httpcore import newHttpHeaders, add, `[]`, del | |
import tables | |
import strutils | |
import aws | |
proc main = | |
let | |
accessKey = readLine(stdin) | |
secretKey = readLine(stdin) | |
region = readLine(stdin) | |
service = readLine(stdin) | |
let | |
requestBody = toSeq(readAll(stdin).replace("\c", "").replace("\l", "\c\l")) | |
request = parseRequest(requestBody) | |
var headerTable = newHttpHeaders() | |
for name, value in request.headers: | |
add(headerTable, name, value) | |
var payload = requestBody[request.size() .. high(requestBody)] | |
var | |
uriParts = split(request.uri, '?', 1) | |
uri = uriParts[0] | |
queryString = | |
if len(uriParts) == 2: | |
uriParts[1] | |
else: | |
"" | |
var requestDateTime = parse("20110909T233600Z", "yyyyMMdd'T'HHmmss'Z'") | |
addAwsHeaderAuth( | |
headers = headerTable, | |
httpMethod = $request.meth, | |
uri = uri, | |
queryString = queryString, | |
payload = payload, | |
region = region, | |
service = service, | |
secretKey = secretKey, | |
accessKey = accessKey, | |
requestDateTime = requestDateTime | |
) | |
echo strip(headerTable["Authorization", 0]) | |
main() |
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
from strutils import toHex | |
import nimcrypto | |
# ## Misc Procs ## # | |
proc hexCharInto*(result: var openarray[char], c: int|byte, lowercase: bool) = | |
assert len(result) >= 2 | |
const | |
upperHexChars = "0123456789ABCDEF" | |
lowerHexChars = "0123456789abcdef" | |
if lowercase: | |
# echo "lowercase" | |
result[0] = lowerHexChars[c shr 4] | |
result[1] = lowerHexChars[c and 0xF] | |
else: | |
# echo "uppercase" | |
result[0] = upperHexChars[c shr 4] | |
result[1] = upperHexChars[c and 0xF] | |
proc addHexCharTo*(result: var string, c: int|byte, lowercase: bool) = | |
let newLen = len(result) + 2 | |
setLen(result, newLen) | |
hexCharInto( | |
result = toOpenArray(result, newLen - 2, newLen - 1), | |
c = c, | |
lowercase = lowercase | |
) | |
proc hexArray*(c: int|byte, lowercase: bool): array[2, char] = | |
hexCharInto(result, c, lowercase) | |
# ## Cryptographic Procedures ## # | |
proc `$`*(digest: MDigest, lowercase: bool): string = | |
result = "" | |
for d in digest.data: | |
addHexCharTo(result, d, lowercase) | |
proc update*[T](ctx: var T; data: char) = | |
update(ctx, cast[ptr byte](unsafeAddr data), 1) | |
# ## String Procedures ## # | |
template shiftLen*(container, amount): untyped = | |
let oldLen = len(container) | |
setLen(container, len(container) + amount) | |
oldLen | |
proc add*(s: var string, a: openarray[char]) = | |
let oldLen = shiftLen(s, len(a)) | |
for index, character in a: | |
s[oldLen + index] = character | |
proc `&`*(x: string, y: openarray[char]): string = | |
result = "" | |
result.add(x) | |
result.add(y) | |
proc `$`*(s: openarray[char]): string = | |
result = newString(len(s)) | |
for index, character in s: | |
result[index] = character | |
proc c_memcmp(a, b: ptr char, size: csize): cint {. | |
importc: "memcmp", header: "<string.h>", noSideEffect | |
.} | |
proc `cmp`*(x, y: openarray[char]): int = | |
let minlen = min(x.len, y.len) | |
result = int(c_memcmp(unsafeAddr x[0], unsafeAddr y[0], minlen.csize)) | |
if result == 0: | |
result = x.len - y.len |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment