Amazon Pay の API クライアントを書く際、Amazon のAPIサーバに送信するリクエストに、RSA-SHA256, RSA PSS パディングを使って署名を作り、リクエストに含めて送信する必要があります。
Java や Node はクライアントライブラリがあったので、それを使って簡単に署名できたのですが、弊社 TORICO ではサーバサイドは主に Python を使っており、既存のクライアントライブラリは無かったため、Node のライブラリを参考に署名コードを書きました。
Amazon Pay のAPI
今回は、Amazon Pay の Checkout v2 API を使う必要がありました。
https://developer.amazon.com/ja/docs/amazon-pay/intro.html
リクエストヘッダに含める署名については、
この CV2 (Checkout V2) の 署名リクエストのページに解説があります。
このページを見ていくと、「署名を計算します」の箇所に
署名を計算するには、ステップ2で作成した署名する文字列に秘密鍵を使用して署名します。 SHA256ハッシュとソルト長20のRSASSA-PSSアルゴリズムを使用します。結果をBase64エンコードして、この手順を完了します。 RSASSA-PSSを使用して計算されたすべての署名は、入力が同じであっても一意であることに注意してください。
とありますので、この「 SHA256ハッシュとソルト長20のRSASSA-PSSアルゴリズム」を Pythonでコーディングする必要があります。
ところで、この最後の「 入力が同じであっても一意であることに注意してください」は、「〜一意でない」の間違いじゃないですかね。英語版ページは調べてませんが。
Amazon Pay Scratchpad
https://pay-api.amazon.jp/tools/scratchpad/index.html
検証リクエストを発行できるサイトが用意されていますので、署名ロジックの確認に使うと良さそうです。
私は、今回の開発中は存在を知らなかったので、使っていません。
Node.js の場合
node.js の場合は、クライアントライブラリは
@amazonpay/amazon-pay-api-sdk-nodejs があり、これを使うと署名を含めたAPIリクエストが一発で行えます。
ヘッダの署名を行っているコードは
https://github.com/amzn/amazon-pay-api-sdk-nodejs/blob/master/src/clientHelper.js#L142 このあたりで、
「 SHA256ハッシュとソルト長20のRSASSA-PSSアルゴリズム」のコードは
https://github.com/amzn/amazon-pay-api-sdk-nodejs/blob/master/src/clientHelper.js#L84 ここです。
Python の場合
上記 node.js 相当のコードを書くわけですが、hmac ライブラリには相当のコードはありません。
RSA や パディングの基礎的なロジックは cryptography に入っており、実際の使い方は PyJWT がいい感じになっているので、PyJWT を参考にします。
https://github.com/jpadilla/pyjwt/blob/master/jwt/algorithms.py#L232
PSS パディングはここです。
この PSS の第一引数の _mgf ってなんだ、と思いましたが、同モジュール中にあるMGF1(RSAAlgorithm.SHA256())
を入れたら動きました。
署名部分のコード
署名部分のコードはこのようになります。
import base64
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_private_key
private_key = b'''-----BEGIN PRIVATE KEY-----
MIIEvQIB....
....N/Qn4=
-----END PRIVATE KEY-----'''
string_to_sign = 'AMZN-PAY-RSASSA-PSS\nxxxxxxxxxxxxxxxxxxxxxxxx'
key = load_pem_private_key(private_key, password=None)
signature = key.sign(
string_to_sign.encode('utf-8'),
padding.PSS(padding.MGF1(hashes.SHA256()), 20),
hashes.SHA256())
signature = base64.b64encode(signature).decode('utf-8')
後になって気づきましたが、PyJWT に RSAPSSAlgorithm という、今回の用途にぴったりなクラスがあったので、これを使うともうちょっとシンプルなコードになるかもしれません。
署名元の文字列の生成も含めたコード
checkoutSessions を行うコードはこのような感じです。
node.js のコードを参考にした箇所がいくつかあり、それらは実際には使われない、不要なコードになってます。
import base64
import datetime
from hashlib import sha256
import requests
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_private_key
private_key = b'''-----BEGIN PRIVATE KEY-----
MIIEv....
....N/Qn4=
-----END PRIVATE KEY-----'''
config = {
'publicKeyId': 'SANDBOX-AEXXXXXXXXXXXX',
'privateKey': private_key,
'region': 'jp',
'sandbox': True,
}
constants = {
'SDK_VERSION': '2.1.4', 'API_VERSION': 'v2', 'RETRIES': 3,
'API_ENDPOINTS': {'na': 'pay-api.amazon.com', 'eu': 'pay-api.amazon.eu', 'jp': 'pay-api.amazon.jp'},
'REGION_MAP': {'na': 'na', 'us': 'na', 'de': 'eu', 'uk': 'eu', 'eu': 'eu', 'jp': 'jp'},
'AMAZON_SIGNATURE_ALGORITHM': 'AMZN-PAY-RSASSA-PSS',
}
checkoutSessionId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
options = {
'method': "GET",
'urlFragment': f"/v2/checkoutSessions/{checkoutSessionId}",
'headers': {},
'payload': ''
}
pay_date = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
headers = {
'x-amz-pay-region': config['region'],
'x-amz-pay-host': 'pay-api.amazon.jp',
'x-amz-pay-date': pay_date,
'content-type': 'application/json',
'accept': 'application/json',
'user-agent': 'amazon-pay-api-sdk-nodejs/2.1.4 (JS/14.15.1; darwin)',
}
lowercase_sorted_header_keys = list(sorted(headers.keys(), key=lambda x: x.lower()))
signed_headers = ';'.join(lowercase_sorted_header_keys)
canonical_request = [
options['method'],
options['urlFragment'],
'', # GETパラメータだが一旦無し
] + [
f'{h}:{headers[h]}' for h in lowercase_sorted_header_keys
] + [
'', # 空行入れる
signed_headers,
sha256(options['payload'].encode('utf-8')).hexdigest()
]
canonical_request_bytes = ('\n'.join(canonical_request)).encode('utf-8')
string_to_sign = constants['AMAZON_SIGNATURE_ALGORITHM'] + '\n' + sha256(canonical_request_bytes).hexdigest()
key = load_pem_private_key(config['privateKey'], password=None)
signature = key.sign(
string_to_sign.encode('utf-8'),
padding.PSS(padding.MGF1(hashes.SHA256()), 20),
hashes.SHA256())
signature = base64.b64encode(signature).decode('utf-8')
headers['authorization'] = \
f"{constants['AMAZON_SIGNATURE_ALGORITHM']} " \
f"PublicKeyId={config['publicKeyId']}, " \
f"SignedHeaders={signed_headers}, " \
f"Signature={signature}"
response = requests.get(
f"https://pay-api.amazon.jp{options['urlFragment']}",
headers=headers
)
print(response)
print(response.json())