Amazon Products API Request Signing


Anyone using the Amazon Product Advertising API (previously known as the Amazon Associates Web Service) should be aware that on August 15th Amazon will start requiring all incoming requests to be signed in accordance with their API documentation. Subscribers to the service should have received an email from Amazon, but if not let this serve as a PSA as well as a tutorial. So far we have implemented solutions for a handful of clients using a variety of languages, and I wanted to share our results with the hope that it will save some headaches.

In this tutorial, we will cover signing requests using Python, PHP, and Visual Basic. If you look at the Product Advertising API documentation (or any of Amazon’s APIs really), you will see the details for signing REST requests. The process is straightforward, and more or less easy to implement.

  • Add an ISO 8601 timestamp (in GMT) as the Timestamp parameters
  • Sort all of the URL params
  • Construct a string to sign
  • Compute an HMAC signature using the SHA256 hash algorithm
  • Base64 encode the resulting signature and set it as the Signature parameter

General Disclaimer

The following code snippets are not meant to be ingredients in copy-pasta, they are meant to illustrate the procedure for signing Amazon API requests and show users the appropriate functions/libraries/resources for doing so. None of the following code will run as-is.

Since the Python implementation is by far the easiest and most readable, it will be useful to demonstrate what is going on.

Python using built-in libraries: hmac, hashlib, and base64

import base64,hashlib,hmac,time
from urllib import urlencode
 
AWS_ACCESS_KEY_ID = "Your Access Key ID"
AWS_SECRET_ACCESS_KEY = "Your Secret Key"
 
base_url = "http://ecs.amazonaws.com/onca/xml"
url_params = {'Operation':"ItemSearch",'Service':"AWSECommerceService",
 'AWSAccessKeyId':AWS_ACCESS_KEY_ID,'AssociateTag':"yourtag-10",
 'Version':"2006-09-11",'Availability':"Available",'Condition':"All",
 'ItemPage':"1",'ResponseGroup':"Images,ItemAttributes,EditorialReview",
 'Keywords':"Amazon"}
 
# Add a ISO 8601 compliant timestamp (in GMT)
url_params['Timestamp'] = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime())
 
# Sort the URL parameters by key
keys = url_params.keys()
keys.sort()
# Get the values in the same order of the sorted keys
values = map(url_params.get, keys)
 
# Reconstruct the URL paramters and encode them
url_string = urlencode( zip(keys,values) )
url_string = url_string.replace('+'," ") 
url_string = url_string.replace(':',":") 
 
#Construct the string to sign
string_to_sign = """GET
ecs.amazonaws.com
/onca/xml
%s""" % url_string
 
# Sign the request
signature = hmac.new(
    key=AWS_SECRET_ACCESS_KEY,
    msg=string_to_sign,
    digestmod=hashlib.sha256).digest()
 
# Base64 encode the signature
signature = base64.encodestring( signature )
 
# Make the signature URL safe
signature = signature.replace('+','+')
signature = signature.replace('=','=')
url_string += "&Signature;=%s" % signature
print "%s?%s" % (base_url,url_string)

A few quick notes before moving on to the other examples. The timestamp here follows the ISO 8601 standard. This standard does not require milliseconds or the terminating Z, but I have seen examples with and without each of these. I believe all of the following timestamps would be valid:

  • 2009-07-25T07:31Z
  • 2009-07-25T07:31:00Z
  • 2009-07-25T07:31:00.00000Z
  • 2009-07-25T07:31
  • 2009-07-25T07:31:00
  • 2009-07-25T07:31:00.00000

That said, the time format in the above example is known to work (we use it in production code, and I believe it’s the same time format used by Boto). The other issue I’d like to address is constructing the “string to sign”. This is a string which describes the request in a standardized way. The general format is:

VERB
sub.domain.com
/api/path
param1=value1&param2=value2&...

The VERB is one of GET, PUT, DELETE, or POST (the HTTP REST verbs). Since the Products API is read-only, we will only be working with the GET verb. The base of the “string to sign” in the above example will be consistent for all Product Advertising API calls.
GET
ecs.amazonaws.com
/onca/xml


Python Resources


PHP using built-in functions: hash_hmac, base64_encode

The PHP implementation is also pretty straightforward. I have seen some implementations that rely on additional packages install with Pear/Pecl, but I do not see the reasoning behind going outside of the standard library of functions.

$AWS_ACCESS_KEY_ID = "Your Access Key ID";
$AWS_SECRET_ACCESS_KEY = "Your Secret Key";
 
$base_url = "http://ecs.amazonaws.com/onca/xml";
$url_params = array('Operation'=>"ItemSearch",'Service'=>"AWSECommerceService",
 'AWSAccessKeyId'=>$AWS_ACCESS_KEY_ID,'AssociateTag'=>"yourtag-10",
 'Version'=>"2006-09-11",'Availability'=>"Available",'Condition'=>"All",
 'ItemPage'=>"1",'ResponseGroup'=>"Images,ItemAttributes,EditorialReview",
 'Keywords'=>"Amazon");
 
// Add the Timestamp
$url_params['Timestamp'] = gmdate("Y-m-d\TH:i:s.\\0\\0\\0\\Z", time());
 
// Sort the URL parameters
$url_parts = array();
foreach(array_keys($url_params) as $key)
    $url_parts[] = $key."=".$url_params[$key];
sort($url_parts);
 
// Construct the string to sign
$string_to_sign = "GET\necs.amazonaws.com\n/onca/xml\n".implode("&",$url_parts);
$string_to_sign = str_replace('+','%20',$string_to_sign);
$string_to_sign = str_replace(':','%3A',$string_to_sign);
$string_to_sign = str_replace(';',urlencode(';'),$string_to_sign);
 
// Sign the request
$signature = hash_hmac("sha256",$string_to_sign,$AWS_SECRET_ACCESS_KEY,TRUE);
 
// Base64 encode the signature and make it URL safe
$signature = base64_encode(signature);
$signature = str_replace('+','%2B',$signature);
$signature = str_replace('=','%3D',$signature);
 
$url_string = implode("&",$url_parts);
$url = $base_url.$url_string."&Signature=".$signature;
print $url;

PHP Resources


Visual Basic 6 *sigh*

This was one a beast. I’m not a VB expert, and never hope to be, but I can make my way around well enough. Like most VB developers, I spent hours scouring forums, reading expert sex change, and banging my head on my desk. The resulting code is far to verbose to post here, but I will provide an archive of the files I came up with along with a brief description.

Starting with the simplest, I made a Class Module called TimeStamp (here is the CLS file). This was the fastest/easiest way I could figure out how to get an ISO 8601 timestamp. I imagine it could be done more elegantly, but that wasn’t my goal.

Next are two files I found and imported with minimal modification: Base64 and CSHA256 (BAS file and CLS file, respectively). The former is a Base64 encode/decoder and the latter is a pure VB6 implementation of the SHA-256 hashing algorithm. Without these two files, none of this is remotely possible - mad props (and condolences) to the person who wrote CSHA256.

The CSHA256 file does require one modification. In the ConvertToWordArray function change AscB to Asc

lByte = AscB(Mid(sMessage, lByteCount + 1, 1))
lByte = Asc(Mid(sMessage, lByteCount + 1, 1))

The final file glues it all together: HMAC_SHA256 (dunno why I named it that). Here is the BAS file. The only function you need to worry about in here is signEncode. Here is how it is used:

Dim URL As String
Dim AWSAccessKeyId As String
Dim AWSSecretAccessKey As String
Dim ts As TimeStamp
Set AWSAccessKeyId = "Your AWS Access Key Id"
Set AWSSecretAccessKey = "Your AWS Secret Access Key"
Set ts = New TimeStamp
URL = "http://ecs.amazonaws.com/onca/xml?Service=AWSEcommerceService" &_ 
    "&Timestamp=" & ts.getIsoTimestamp() & "&Operation=ItemSearch&" & _
    "&Service=AWSECommerceService&AWSAccessKeyId=" & AWSAccessKeyId & _
    "&AssociateTag=yourtag-10&Version=2006-09-11&Availability=Available" & _
    "&Condition=All&ItemPage=1&ResponseGroup=Images,ItemAttributes,EditorialReview" & _
    "&Keywords=Amazon"
URL = signEncode(URL)
MsgBox URL

The signEncode function does pretty much everything. It first separates the base URL from the request parameters, then sorts the parameters and calculates the signature. I had to fiddle around with it for a while before I found an issue with the Base64 encoding. I can’t take any credit for any of this really since I just gathered all the pieces and put them together. However, I hope this compilation of files and explanation will be useful.


Visual Basic Resources


Other Amazon Resources


There you have it. Amazon Product Advertising API request signing in three languages: Python, PHP, and Visual Basic 6. Enjoy!

-David

david on 07/25/2009 | 9 Comments
cailinanne
on 08/12/2009

By the way . . . if you truly are running the above Python sample code in production, it’s only working because of the accidental “Signature;=” instead of “Signature=”.  This typo makes Amazon ignore the signature completely, which currently works because it’s only August 12th!

cailinanne
on 08/12/2009

Sorry - apparently my first comment did not post correctly.  The above Python code does not work for 3 reasons.
#1.  Instead of “Signature;=” you need “Signature=”
#2.  I get a trailing newline on the signature, and had to add a signature = signature.strip()
#3.  The signature must be urlencoded.  It seems like you maybe have faulty version of this in the line signature = signature.replace(’=’,’=’) which as far as I can tell does nothing.  The PHP code for that block looks correct.

cailinanne
on 08/12/2009

One last nitpick on the Python code . . . you need

url_string = url_string.replace(’+’,” “)
url_string = url_string.replace(’:’,”:”)

instead of what you have.

cailinanne
on 08/12/2009

Sigh, my point got lost in the HTML interpretation of the comment.  You need to replace + with PercentTwoZero and the colon with PercentThreeA   .  (Can’t figure out how to escape that properly in this comment box, so I’ve written out the characters instead.)

Alex
on 08/20/2009

The PHP code also doesn’t quite work as is.

The line:
$url = $base_url.$url_string.”&Signature;=”.$signature;
Should be:
$url = $base_url.”?”.$url_string.”&Signature;=”.$signature;  // Need to seperate the URL from the query string

And the line:
$signature = base64_encode(signature);
Should be:
$signature = base64_encode($signature);

In addition, I’m still not getting the correct signature from this code. Amazon is refusing it. It could be me, but there’s probably something else wrong there.

Thomas Keller
on 08/21/2009

In the VB code there are two mistakes:
1) the service parameter is twice
2) even if I delete the one and change the double ?? to ? in the given example there comes an error:  <?xml version=“1.0” encoding=“UTF-8” ?>
  AWS.InvalidServiceParameter
  <Message>The Service parameter is invalid. Please modify the Service parameter and retry.</Message>

Thomas Keller
on 08/21/2009

I tried the code with the modification mentioned in the post before with the example given by amazon in http://associates-amazon.s3.amazonaws.com/signed-requests/helper/index.html.
The answer is:
  SignatureDoesNotMatch
  <Message>The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.

david
on 08/21/2009

cailinanne: Yea, this CMS does weird things to code snippets. I believe that ‘;’ was added automatically.
Alex/Thomas: I kindly refer you to the general disclaimer at the top. We’re not serving copy-pasta here - just the general idea of how to accomplish this.

Thomas Keller
on 08/21/2009

I forgot to say that the same request gets a different signature by signed-Request-Helper (i used always the same timestamp) and that that signature works :-(

New Comment


Commenting is not available in this weblog entry.

Categories

Calendar

July 2010
S
M
T
W
T
F
S
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Copyright ©2009. Loot whatever you like as long you're not a Rogue.