Skip to content

Instantly share code, notes, and snippets.

@Varriount
Last active January 9, 2020 23:06
Show Gist options
  • Save Varriount/076f406c475b5369301a2fcd7b4527af to your computer and use it in GitHub Desktop.
Save Varriount/076f406c475b5369301a2fcd7b4527af to your computer and use it in GitHub Desktop.
AWS Request Signing for Nim. Requires the `nimcrypto` package.
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
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()
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