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.