[Home] [Feed] [Twitter] [GitHub]

Simple AWS Request Signing

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.

Invoke as an executable

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.

Integrated with Python code

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.