2009/06/23

Amazon Web サービスの署名認証 - with Ruby 1.9.1

Amazon Product Advertising APIの認証の件
Amazon Product Advertising API (認証対応)

を読んで自分のID で試してみました。OS X のRuby 1.8.6 でちゃんと動きました。うれしい。
OS X 10.5.7 のOpenSSL のバージョンは古いので


dig = HMAC::SHA256.digest(secret_key, msg)

で試しました。

でも ruby 1.9.1 で動かすと


ruby-openid-2.1.6/lib/hmac/hmac.rb:94:in `ensure in digest': undefined method `reset_key' for nil:NilClass (NoMethodError)

となります。

あちこちみたあげく livedoor.com 経由で
amazon-auth-proxy
をみつけたのでこちらも手元で試してみました(cgi ぬきで)。module が拡張されていたのでもしや?と思いましたがやっぱり Ruby 1.8.6 では動くけどRuby 1.9.1 だと同じようなエラーがでて動きません。

エラーが指し示すライブラリ ruby-openid/lib/hmac/hmac.rb の中身をみて考えてみました。
reset_key メソッドはちゃんと存在しているし現にRuby 1.8.6 では動くので、一体なにがnil なのか?と少し書き足してエラーを再現してみたところ...


def set_key(key)
...
  for i in 0 .. key.size - 1
  key_xor_ipad[i] ^= key[i]
...

このメソッドにある ^= の部分でエラーになっていることがわかりました。Ruby 1.8.6 で動かすと nil じゃないけど、Ruby 1.9.1 だとnil になる。これはなぜだろう?あちこちに p をつけて実行してみて気づいたのは Ruby 1.9.1 の string[0]と Ruby 1.8 の string[0] の解釈が違うらしいということでした。

プログラミング言語 Ruby
p.59 によると
Ruby 1.8 の string[0]は、ASCII
Ruby 1.9 の string[0]は 文字列
とありました。

key[i]は、key に納められている文字列の1文字目の ASCII がかえってくる前提で書かれているけど、Ruby 1.9.1 で動かすと文字列そのもの("m"とか)がかえってきてしまう。これが nil になる原因なんだとわかりました。

...ということでいろいろ試しつつRuby 1.9.1 で動くものを作ってみました。とりあえず Amazon の認証にはクリアできましたが記述については自信がありません。

# 今回の Amazon からの宿題は Ruby 1.9.1 のString に関する知識を強制的に深める効果がありました。不幸中の幸いというかなんというか感謝。


# coding: utf-8

# Library:
# http://rubyforge.org/projects/ruby-openid/
# Original Page:
# http://github.com/tdtds/amazon-auth-proxy/blob/035bd4de90c4223994046fb489d7ccded566a879/amazon-auth-proxy.cgi
# http://diaspar.jp/node/239

# 使用したRubyのバージョン
# ruby 1.9.1p129 (2009-05-12 revision 23412) [i386-darwin9.7.0]

# Ruby 1.9.1 のgem は使ってない為ライブラリの絶対パスを push している。
# gem19 unpack ruby-openid-2.1.6.gem で解凍後のパス
oid = '/path/to/ruby-openid-2.1.6/lib'
$LOAD_PATH.push(oid)

require 'uri'
require 'time'
require 'openid'
require 'rexml/document'

module HMAC
 # Original
 # => http://github.com/tdtds/amazon-auth-proxy/blob/035bd4de90c4223994046fb489d7ccded566a879/amazon-auth-proxy.cgi
  # Ruby 1.9.1 用に改造してみる。
  IPAD = "\x36" * 64
  OPAD = "\x5c" * 64
  module_function
  def sha256( key, message )
    ipad, opad = [],[]
    # バイトごとにArrayに追加
  IPAD.each_byte{|x| ipad << x }
    OPAD.each_byte{|x| opad << x}
    ikey = ipad
    okey = opad
    akey = []
  # うけとったkey もArrayへ
    key.each_byte{|x| akey << x}
    key.size.times{|i|
      ikey[i] = akey[i] ^ ipad[i]
      okey[i] = akey[i] ^ opad[i]
    }
    # コードポイントから文字列へ
    ik = ikey.pack("C*")
    ok = okey.pack("C*")
    value = Digest::SHA256.digest( ik + message )
    value = Digest::SHA256.digest( ok + value )
  end

end

module Aws
  class Amazon

    def initialize(host, uri)
      @host = host
      @uri = uri
    end

    def escape(string)
   # Original
      # => ruby19/1.9.1/cgi/uri.rb CGI::escape(string)
      string.gsub(/([^ a-zA-Z0-9_.-]+)/) do
        '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
      end.tr(' ', '+')
    end
    
    def local_utc
      t = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
      escape(t)
    end
    
    def make_req(isbn, access_key, secret_key)
   # original はこちら => http://diaspar.jp/node/239
      req = 
      [
        "Service=AWSECommerceService",
        "Version=2009-03-31",
        "AWSAccessKeyId=#{access_key}",
        "Operation=ItemLookup",
        "SearchIndex=Books",
        "ResponseGroup=Medium",
        "IdType=ISBN",
        "ItemId=#{isbn}",
        "Timestamp=#{local_utc}"
       ].sort.join('&')
      msg = ["GET", @host, @uri, req].join("\n")
      # 拡張モジュール1.9.1用改造版呼び出し
      hash = HMAC::sha256(secret_key, msg)
      mh = [hash].pack("m").chomp 
      sig = escape(mh)
      return "#{@uri}?#{req}&Signature=#{sig}"
    end

  end
end

#-------------------#

accesskey = 'xxxxxxxxxxxxxxxxxxx'
secretkey = 'xxxxxxxxxxxxxxxxxxxx' 
host = 'webservices.amazon.co.jp'
path = '/onca/xml'

#isbn = "9784274066429"
#isbn = "9784839927844"
#isbn = "9784894712850"
isbn = "9784797336610"
req = Aws::Amazon.new(host, path).make_req(isbn, accesskey, secretkey)

#exit

# 署名認証リクエスト開始
begin
  Net::HTTP.start(host, 80) { |http|
    resp = http.get(req).body
  # ドキュメントをUTF-8 へ強制変換
    ustr = resp.force_encoding("UTF-8")
    doc = REXML::Document.new(ustr)
    doc.elements.each('ItemLookupResponse/Items/Item/ItemAttributes') { |item|
      puts '%s -> %s' % [item.text('EAN'), item.text('Title')]
    }
  }
end

結果


% ruby19 for-19.rb 
9784797336610 -> たのしいRuby 第2版 Rubyではじめる気軽なプログラミング

+++ 試したライブラリ+++
ruby-openid-2.1.6.gem
ruby-hmac-0.3.2.gem

+++ 参考にしたところ+++
Amazon Product Advertising APIの認証の件
Amazon Product Advertising API (認証対応)
github.com/tdtds

[を] アマゾンAPIを使うのに2009年8月15日から認証が必要になるらしい
KeN's GNU/Linux Diary | Amazon アソシエイト Web サービスの名称変更および署名認証についてのお知らせ

+++ Amazon +++
Product Advertising API 開発者向けガイド リクエストの署名認証について>(参考訳
http://developer.amazonwebservices.com/connect/ann.jspa?annID=442

+++ 追記 +++
Amazon Web Services Developer Community : OSSからの認証利用

私も Amazon Web サービスが元に戻ってくれたらいいのにな、と思いました。

2 件のコメント:

ただただし さんのコメント...

ご連絡いただいたのに返事もできずにすみません! しばらく忙しいのでちょっと放置しちゃいますけど、あとで見て取り込みたいと思います。

midore さんのコメント...

コメントありがとうございます。m(_)m
たださんがつかいやすいよう取り込んで(あるいは修正)くだされば幸いです。私の方では amazon AWS 用のデスクトップアプリケーションをgithub に公開しています。デスクトップアプリケーションなのでこのまま上記のモジュールの部分をつかわせていただこうかと思っています。こちらも何か問題があるかないかよくわかっていませんので時間がかかるかもしれません。