George MacKerron: code blog

GIS, software development, and other snippets

Signing Amazon Product Advertising API calls in Ruby

I have a simple site that generates covers for CDs I burn from iTunes purchases and so on (it pre-dates widespread use of JS libraries, and is in much need of prettifying). The site uses Amazon Product Advertising API calls to search and retrieve album cover art and track listings. Since earlier this month, such API calls have to be cryptographically signed.

This is somewhat annoying — the site’s original design has it communicating independently with Amazon (using Amazon’s XSLT API feature to transform their XML data into JSON), and that’s no longer possible with the use of a private key. But it’s not unfixable. The site now sends its API call first to my server, which returns a signed version, and then forwards the signed call on to Amazon.

I found most of what I needed for this on Chris Roos’ blog, but his version still wasn’t quite working for me (the two problems I recall are that Ruby’s CGI.escape doesn’t quite follow Amazon’s requirements, and that times need converting to GMT).

Anyway, in case you’re looking to do the same, here’s what I ended up with:

#!/usr/bin/env ruby
 
# Note: You need hmac.rb and hmac-sha2.rb from http://deisui.org/~ueno/ruby/hmac.html 
# somewhere in your require paths. ruby-hmac is currently broken under Ruby 1.9.
 
%w(rubygems cgi time hmac-sha2 base64).each { |lib| require lib }
 
ACCESS_IDENTIFIER = 'YOUR_PUBLIC_ID'
SECRET_IDENTIFIER = 'YOUR_PRIVATE_ID'
 
def aws_escape(s)
  s.gsub(/[^A-Za-z0-9_.~-]/) { |c| '%' + c[0].to_s(16).upcase }  
  # for 1.9, you'd replace [0] with .ord -- but ruby-hmac seems broken under 1.9
end
 
cgi = CGI.new
params = cgi.params.dup
 
amazon_endpoint = params.delete('amazon_endpoint')
amazon_path = params.delete('amazon_path')
js_callback = params.delete('js_callback')
 
signing_params = {
  'AWSAccessKeyId' => ACCESS_IDENTIFIER,
  'Timestamp' => Time.now.gmtime.iso8601
}
 
params.merge!(signing_params)
 
canonical_querystring = params.sort.collect do |key, value| 
  [aws_escape(key.to_s), aws_escape(value.to_s)].join('=') 
end.join('&')
 
string_to_sign = "GET\n#{amazon_endpoint}\n#{amazon_path}\n#{canonical_querystring}"
 
hmac = HMAC::SHA256.new(SECRET_IDENTIFIER)
hmac.update(string_to_sign)
signature = Base64.encode64(hmac.digest).chomp
 
params['Signature'] = signature
querystring = params.sort.collect do |key, value| 
  [aws_escape(key.to_s), aws_escape(value.to_s)].join('=') 
end.join('&')
 
signed_url = "http://#{amazon_endpoint}#{amazon_path}?#{querystring}"
 
cgi.out('type' => 'text/javascript') { "#{js_callback}('#{signed_url}');" }

You can test this locally by feeding key/value parameters to CGI, followed by Ctrl-D. These, for example:

amazon_endpoint=ecs.amazonaws.com
amazon_path=/onca/xml
js_callback=do_stuff
Service=AWSECommerceService
Version=2009-03-31
Operation=ItemSearch
SearchIndex=Books
Keywords=george+monbiot

Share

Written by George

August 22nd, 2009 at 12:04 pm