niiyan's blog

niiyanの個人ブログ。

Amazon アソシエイト Web サービス改め Product Advertising API の電子署名について調べてみました

Amazon アソシエイト Web サービスが Product Advertising API に名称変更されたのにともない、リクエストごとに電子署名を含めなければならなくなるようです。これは 2009 年 5 月 11 日から同 8 月 15 日まで段階的に導入され、その後、認証されないリクエストについては処理されなくなるとのことです。

私はこの件について最初にメールで知ったのですが、Amazon アソシエイト・プログラム(アフィリエイト) 公式ブログ: Amazon アソシエイト Web サービスの名称変更および署名認証についてのお知らせにも同様の記事が掲載されています。詳しくはそちらを参照してください。

なんだか非常にややこしそうな話で、すでに半ば心が折れそうな状態ですが、ちょっとがんばって調べてみました。まだ書きかけの部分もありますが、あとで調べなおしたことがあれば随時追記していきます。

なお以下、REST にしぼって話を進めます。SOAP については、Authenticating SOAP Requests (Product Advertising API) を参照してください。

認証用のパラメータ

Authentication Parameters (Product Advertising API) より:

Signature
Timestamp
  • Signature パラメータを含める場合は必須。それ以外の場合はオプション。
  • デフォルト値なし。
  • dateTime オブジェクトでなければならない。
  • ISO 8601 で定義されているフォーマットの固定長サブセット。
  • Universal Time (GMT) で表される。
  • YYYY-MM-DDThh:mm:ssZ

The mapping so defined is one-to-one, except that '+00:00', '-00:00', and 'Z' all represent the same zero-length duration timezone, UTC; 'Z' is its canonical representation.

http://www.w3.org/TR/xmlschema-2/#dateTime
    • GMTUTC は微妙に違う*1が、ここでは UTC と同義と考えていいのだろうか。

認証までの手順

Basic Authentication Process (Product Advertising API) より:

Amazon Web サービス(AWS)のアカウントを取得済みで、Access Key ID および Secret Access Key もすでに知っているものという前提で。

あなた(私)側
  1. AWS へのリクエストを作成
  2. Secret Access Key を使って HMAC-SHA 署名を作成
  3. リクエストと署名を AWS に送信
AWS
  1. Access Key ID を使って Secret Access Key を検索
  2. リクエストされたデータと Secret Access Key から HMAC-SHA 署名を生成
  3. 2 つの署名を比較し、マッチしたらそのリクエストは認証される。比較に失敗した場合は、リクエストは破棄され、AWS はエラーを返す。

アクセスキー ID およびシークレットアクセスキーの確認方法

REST リクエストの例

Example REST Requests (Product Advertising API) より:

  1. タイムスタンプの追加
    • 上の例の末尾に 「&Timestamp=〜」という形で追加。
    • ここでは、「2009-01-01T12:00:00Z」というタイムスタンプを使用。
  2. カンマ、コロンを URL エンコード
  3. パラメータ/値のペアに分割し、アンパサンド(&)を削除
  4. 上のペアをバイト値によってソート
    • アルファベット順ではない。小文字のパラメータは大文字のパラメータより後になる。
    • 「初めてのPython 第2版」の p.554 によると、sort メソッドでは「通常、大文字は小文字よりも前に置かれます」とあるので、普通に sort() で大丈夫かも。
    • sort( )について少し詳しく - ひきメモによると、文字列は文字コード順とある。
  5. アンパサンドで再結合
  6. 再結合した文字列の上に「GET」「webservices.amazon.com」「/onca/xml」の 3 行を追加:
  7. 署名用の文字列の完成
  8. Secret Access Key と上の文字列を使って、SHA256*2 ハッシュアルゴリズムRFC 2104 互換の HMAC*3 を算出。
  9. プラス(+)とイコール(=)を URL エンコード
  10. URL エンコードされた署名をリクエストに追加(「&Signature=〜」)

Python電子署名を作るには

Python でソートするには

sort か sorted を使う

参考:

Python で URL エンコードするには

urllib.urlencode か urllib.quote(または urllib.quote_plus)?

カンマとコロンだけでいいのなら、正規表現の置換でもいいのだろうか?

Python でタイムスタンプを生成するには

time または datetime を使う?

参考: 5.1 datetime -- 基本的な日付型および時間型

Python で SHA256 を求めるには

PythonでMD5・SHA1を求める - Fioの素敵な日々によると、Python で SHA256 を求めるには hashlib を使うといいらしいです。

よく調べてみると、鍵付きハッシュでは hmac モジュールを使うらしいことがわかりました。そして、new() の digestmod に hashlib.sha256 コンストラクタを指定すると。

参考: 10.2 hmac -- メッセージ認証のための鍵付きハッシュ化10.1 hashlib -- セキュアハッシュおよびメッセージダイジェスト

PythonBase64 エンコードするには

base64 モジュールを使って、Base64エンコードできるようです。7.11 base64 -- RFC 3548: Base16, Base32, Base64 テータの符号化を参照。

ほかに binascii モジュールでも Base64 エンコードは可能なようです。ただし、最後に改行文字が含まれるため、今回の場合はそれを取り除く必要があると思われます(rstrip()?)。

参考: PythonでBase64・Hexを求める - Fioの素敵な日々7.13 binascii -- バイナリデータと ASCIIデータとの間での変換

Google App Engine で利用できるライブラリか否か

Google App Engine Python Library Support - Google App Engine - Google Code を見る。

base64 モジュールはサポートされていない模様。要確認。無理なら、binascii を使用する方向で考える。

【2009-05-22 0:40 追記】実際に試してみましたが、Google App Engine 上でも base64 モジュールは使えるようです。【追記終わり】

実際にやってみた

Example REST Requests を Python

Example REST Requests (Product Advertising API) を、実際に Python でやってみました。Access Key ID、Secret Access Key およびタイムスタンプは、Example REST Requests の中で使われているもの(ダミー)をそのまま使っています。

[を] アマゾンAPIを使うのに2009年8月15日から認証が必要になるらしいAmazon Product Advertising APIの認証の件 - zorioの日記を参考にさせていただきました。

# -*- coding: utf-8 -*-

"""
AWS Product Advertising API の電子署名を生成するテスト
"""

# モジュールのインポート
import urllib # URL エンコード
import hashlib, hmac # HMAC-SHA256 の算出
import base64 # Base64 エンコード(base64 の場合)
#~ import binascii # Base64 エンコード(binascii の場合)

# Secret Access Key
secret_access_key = "1234567890" # ダミー

# リクエスト(リスト)
reqs = [
"Service=AWSECommerceService",
"AWSAccessKeyId=00000000000000000000",
"Operation=ItemLookup",
"ItemId=0679722769",
"ResponseGroup=ItemAttributes%2COffers%2CImages%2CReviews",
"Version=2009-01-06",
"Timestamp=2009-01-01T12%3A00%3A00Z"
]

# sort でソート
reqs.sort()

# & で再結合
req_rejoined = "&".join(reqs)

# 追加の 3 行と結合して、署名用の文字列の完成
message = """GET
webservices.amazon.com
/onca/xml
""" + req_rejoined

# Secret Access Key を使って HMAC-SHA256 を算出
hmac_digest = hmac.new(secret_access_key, message, hashlib.sha256).digest()

# Base64 エンコード
base64_encoded = base64.b64encode(hmac_digest) # base64 の場合
#~ base64_encoded = binascii.b2a_base64(hmac_digest).rstrip() # binascii の場合。改行文字を取り除く

# URL エンコード
result = urllib.quote(base64_encoded)

# 結果
print result #=> Nace%2BU3Az4OhN7tISqgs1vdLBHBEijWcBeCqL5xN9xg%3D

ためしに自分の Access Key ID と Secret Access Key を使ってリクエストしてみたところ、エラーが出ることなく商品情報を取得することができました。

ジェイムズ・ジョイスの「ユリシーズ」だったのですね!

リクエストが辞書の場合

上の Example REST Requests の例を元にしたものは、URL エンコード済みの「パラメータ=値」のリストを対象にしていました。今度は URL エンコード前の状態からはじめて、リクエストの部分をリストではなく辞書(ディクショナリ)にしてやってみました。コメントアウトしていますが、タイムスタンプの生成部分も追加しています。

無題メモランダム: amazon Product Advertising APIの署名認証をPythonでやってみるを参考にさせていただきました。

# -*- coding: utf-8 -*-

"""
AWS Product Advertising API の電子署名を生成するテスト(その 2)
"""

# モジュールのインポート
#~ from datetime import datetime # タイムスタンプの生成
import urllib # URL エンコード
import hashlib, hmac # HMAC-SHA256 の算出
import base64 # Base64 エンコード(base64 モジュール)
#~ import binascii # Base64 エンコード(binascii モジュール)

# 初期設定
aws_domain = "webservices.amazon.com" # 日本なら "webservices.amazon.co.jp"
access_key_id = "00000000000000000000" # ダミー
secret_access_key = "1234567890" # ダミー

# タイムスタンプの生成
timestamp = "2009-01-01T12:00:00Z" # テスト用
#~ utc_time = datetime.utcnow() # UTC 現在日時
#~ timestamp = utc_time.strftime("%Y-%m-%dT%H:%M:%SZ") # 整形

# リクエスト(辞書)
reqs = {
"Service": "AWSECommerceService",
"AWSAccessKeyId": access_key_id,
"Operation": "ItemLookup",
"ItemId": "0679722769",
"ResponseGroup": "ItemAttributes,Offers,Images,Reviews",
"Version": "2009-01-06",
"Timestamp": timestamp
}

# sorted でソート
reqs_sorted = sorted(reqs.items()) # items() でキーと値をペアにしたタプルのリストを対象に

#~ # URL エンコード
#~ reqs_urlencoded = urllib.urlencode(reqs_sorted) # パラメータと値のペアが & で結合された文字列を返す

# URL エンコード(2009-05-28 修正)
req_list = [] # リストの初期化
for req in reqs_sorted:
    # key=value のペアに整形
    pair = "%s=%s" % (req[0], urllib.quote(req[1])) # quote() で URL エンコード
    # リストに追加
    req_list.append(pair)
# & で結合された文字列を生成
reqs_urlencoded = "&".join(req_list)

# 追加の 3 行を改行(\n)を使って結合、署名用の文字列の完成
message = "\n".join(["GET", aws_domain, "/onca/xml", reqs_urlencoded])

# Secret Access Key を使って HMAC-SHA256 を算出
hmac_digest = hmac.new(secret_access_key, message, hashlib.sha256).digest()

# Base64 エンコード
base64_encoded = base64.b64encode(hmac_digest) # base64 の場合
#~ base64_encoded = binascii.b2a_base64(hmac_digest).rstrip() # binascii の場合。改行文字を取り除く

# URL エンコード(2 回目)
signature = urllib.quote(base64_encoded) # quote_plus()?

# 正規化されたリクエスト
formatted_request = "http://" + aws_domain + "/onca/xml?" + reqs_urlencoded + "&Signature=" + signature

# 結果表示
print signature #=> Nace%2BU3Az4OhN7tISqgs1vdLBHBEijWcBeCqL5xN9xg%3D
print formatted_request #=> http://webservices.amazon.com/onca/xml?AWSAccessKeyId=00000000000000000000&ItemId=0679722769&Operation=ItemLookup&ResponseGroup=ItemAttributes%2COffers%2CImages%2CReviews&Service=AWSECommerceService&Timestamp=2009-01-01T12%3A00%3A00Z&Version=2009-01-06&Signature=Nace%2BU3Az4OhN7tISqgs1vdLBHBEijWcBeCqL5xN9xg%3D

感想など

あとで書く

追記

2009-05-10 22:34 追記
2009-05-10 22:49 追記
  • 「認証までの手順」を更新。
2009-05-12 0:08 追記
  • 以下を更新:
    • 「REST リクエストの例」を追加。
    • Python でソートするには」を更新。「Python で URL エンコードするには」、「Python でタイムスタンプを生成するには」を追加。
    • 「関連リンク」に 3 件追加。
2009-05-12 2:15 追記
  • 「REST リクエストの例」を更新。
  • Python で SHA256 を求めるには」を更新(hmac モジュールについて)。
2009-05-13 0:06 追記
  • 「実際にやってみた」を追加。
2009-05-13 0:54 追記
2009-05-14 1:08 追記
  • 「実際にやってみた」を更新。
  • 「関連リンク」を更新。
2009-05-22 0:41 追記
2009-05-28 0:55 追記