September 04 2018
Amazon Web Services Amazon Web Services has been
online for more than a decade, and now supports a dizzying array of services
backed by a relatively easy-to-use REST API. Unfortunately, while the ecosystem
of tools and libraries has expanded exponentially, working with these services
tends to require a deep scaffolding of dependencies. Lately I've been playing
with OpenWRT on my home network and wanted to make some
AWS REST API requests. The device has a fairly generous 128MB of storage, but
even so, pulling in the official awscli
Python package with its dependencies
weighs in at over 30MB, not including Python itself.
Introducing aws4sign, a zero-dependency, MIT-licensed, single-file Python2 library and CLI tool that computes the AWS v4 signature.
There are two ways to use this.
The aws4sign.py
file itself contains a simple __main__
that makes it easy
to drive signature generation from anywhere that can invoke an executable.
The following bash snippet calls the AWS Route53 hostedzone
API using curl
:
# AWS keys
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
# Inputs to the signature algorithm; must be immutable
#
# The $now parameter in particular is interesting. The AWS signature algorithm
# includes the current time, which we pass to aws4sign.py using the -t option.
now=$(date '+%s')
url=https://route53.amazonaws.com/2013-04-01/hostedzone
# Compute all headers
auth_header=$(python2.7 ./aws4sign.py -t $now -p authorization $url | cut -f2)
date_header=$(python2.7 ./aws4sign.py -t $now -p x-amz-date $url | cut -f2)
content_header=$(python2.7 ./aws4sign.py -t $now -p x-amz-content-sha256 $url | cut -f2)
curl -s \
-H "authorization: $auth_header" \
-H "x-amz-date: $date_header" \
-H "x-amz-content-sha256: $content_header" \
$url
The only thing of note here is that we end up calling aws4sign.py
once for
each header that we need to pass to curl
, selecting the header to display
using the -p
option. If we omitted this option, all headers would be emitted
(one per line), but parsing these is a bit more involved than desirable for
such a simple example. Instead, we just emit a single header each time and use
cut
to grab its value. Note, however, that because the AWS signature
algorithm uses a timestamp, we need to ensure that aws4sign.py
has a constant
notion of time across invocations. We do this by computing the current time
up-front and passing it using the -t
option.
Copy and paste the single 100-line
aws4_signature_parts()
function into your code. Or integrate it into a module of your own. Whatever.
No dependencies. No mucking with PIP. No incompatible licenses.
The following code invokes the Route53 hostedzone
API using urllib2
:
def aws4_signature_parts(...):
...
def aws_route53(aws_key, aws_key_secret, path, data=None):
url = 'https://route53.amazonaws.com/2013-04-01/{}'.format(path)
_, _, headers = aws4_signature_parts(
aws_key,
aws_key_secret,
'GET' if data is None else 'POST',
url,
data='' if data is None else data)
return urllib2.urlopen(urllib2.Request(url, headers=headers, data=data))
print aws_route53(aws_key, aws_secret, 'hostedzone').read()
The first two return values from aws4_signature_parts()
should probably be
ignored by most users -- they are mostly in place to provide visibility into
the signing process for validation and testing purposes.
That's it! Happy signing.