How to verify AWS Signature Version 4 implementations

You can use the Python botocore package which is a dependency of the boto3 AWS client in order to verify if your implementation produces correct HMAC signatures for a given string-to-sign.

In order to do this, we’ll use a fixed AmzDate i.e. timestamp and fixed (but random) access keys. The string to sign is also some random-ish string. The only thing that matters is that none of the random strings are empty and all values are the same for the verification path using botocore as they are for your own implementation.

After that, compare the output from the botocore implementation with your own custom implementation. While you might want to check your implementation with different values, in practice it works (maybe except for rare corner cases) if it works correctly for one string.

Verifying the outpt

from botocore.auth import SigV4Auth
from collections import namedtuple

# Data structures for isolated testing
Credentials = namedtuple('Credentials', ['access_key', 'secret_key'])
Request = namedtuple('Request', ['context'])

amzDate = "20130524T000000Z" # Fixed date for testing
signer = SigV4Auth(Credentials(
    access_key="GBWZ45MPRGGMO2JILBXA",
    secret_key="346NO6UJCAMHLHX4SMFA"
), "s3", "global")
signature = signer.signature("ThisStringWillBeSigned", Request(
    context={"timestamp": amzDate}
))
print(signature)

With the values given in this script, the output is

3be60989db53028ca485b46a07df9287a1731df74a234ea247a99febb7c2eb31

Verifying intermediary results

If the global result matches, you’re already finished. There is typically no need to check the intermediary results and input strings.

The SigV4Auth.signature() function doesn’t provide any way of accessing the intermediary results. However, we can just copy its source code to obtain the  relevant intermediaries and print those as hex:

secret_key="346NO6UJCAMHLHX4SMFA"
datestamp = "20130524"
region_name = "global"
service_name = "s3"
string_to_sign = "ThisStringWillBeSigned"

sign_input =  (f"AWS4{secret_key}").encode(), datestamp
k_date = signer._sign(*sign_input)
k_region = signer._sign(k_date, region_name)
k_service = signer._sign(k_region, service_name)
k_signing = signer._sign(k_service, 'aws4_request')
sign_result = signer._sign(k_signing, string_to_sign, hex=True)

print("Sign input: ", sign_input)
print("k_date: ", k_date.hex(), "of length: ", len(k_date))
print("k_region: ", k_region.hex(), "of length: ", len(k_region))
print("k_service: ", k_service.hex(), "of length: ", len(k_service))
print("k_signing: ", k_signing.hex(), "of length: ", len(k_signing))
print("sign_result: ", sign_result)

This prints:

Sign input:  (b'AWS4346NO6UJCAMHLHX4SMFA', '20130524')
k_date:  a788ed61da3106091ac303738fe248c3d391e851858d9b048d3fddf0494cac61 of length:  32
k_region:  90331d205578b73aeaf4ef9082cbb704111d29364dcae4d4405ddfefc4e6a8b0 of length:  32
k_service:  a0b2fb2efe1977349c647d28e86d373aaa67ca9f452c15c7cfbdb9a4fabd685b of length:  32
k_signing:  e02df2af0ce8890816c931c8e72168921f5f481dfbcaf92a35324b65fc322865 of length:  32
sign_result:  3be60989db53028ca485b46a07df9287a1731df74a234ea247a99febb7c2eb31