[Python] Boto3以外でV4署名リクエストを行う

AWSのAPIへのリクエストではV4署名が必要です。Boto3でなく自分で直接HTTPリクエストを作る場合は署名も自分で行う必要があります。そのような時に使える方法をご紹介します。
2019.07.12

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

AWSのAPIへのリクエストではV4署名による認証が必要です。 通常はAWS SDK(Pythonの場合はBoto3)が署名を行ってくれますので、ユーザは署名について意識する必要はありません。 しかし、AWSと同じ認証方式のAPIに対するHTTPリクエストを自分で生成する場合は、署名も自分で行う必要があります。 例えば以下のようなケースです。

  • IAM認証しているElasticsearch Serviceへリクエストするとき
  • API Gatewayで作ったAPIにIAM認証をかけているとき

本記事では、このような場合に使える方法をいくつかご紹介します。

署名用パッケージを使う

簡単かつ確実なのは、署名用に作られたパッケージを用いることです。基本的にはこの方法が良いと思います。2つご紹介します。

requests-aws4auth

https://github.com/sam-washington/requests-aws4auth

Elasticsearch Serviceの公式ドキュメントでも紹介されているパッケージです。 以下のように利用します。

    url = 'https://s3.{}.amazonaws.com/'.format(os.environ['AWS_REGION'])
    auth = requests_aws4auth.AWS4Auth(
        os.environ['AWS_ACCESS_KEY_ID'],
        os.environ['AWS_SECRET_ACCESS_KEY'],
        os.environ['AWS_REGION'],
        's3',
        session_token=os.environ['AWS_SESSION_TOKEN'],
    )
    response = requests.get(url, auth=auth)

aws-requests-auth

https://github.com/DavidMuller/aws-requests-auth

以下で紹介されているものです。

以下のように利用します。

    host = 's3.{}.amazonaws.com'.format(os.environ['AWS_REGION'])
    url = 'https://s3.{}.amazonaws.com/'.format(os.environ['AWS_REGION'])
    auth = aws_requests_auth.aws_auth.AWSRequestsAuth(
        aws_access_key=os.environ['AWS_ACCESS_KEY_ID'],
        aws_secret_access_key=os.environ['AWS_SECRET_ACCESS_KEY'],
        aws_token=os.environ['AWS_SESSION_TOKEN'],
        aws_host=host,
        aws_region=os.environ['AWS_REGION'],
        aws_service='s3')
    response = requests.get(url, auth=auth)

サービス専用のクライアントを使う

Elasticsearch Serviceの場合、専用のクライアント elasticsearch-py が用意されていますので、これを使うこともできます。 使い方はElasticsearch Serviceの公式ドキュメントに記載されています。 自分では試していませんが、以下のように使うようです(公式ドキュメントからの引用です)。

from elasticsearch import Elasticsearch, RequestsHttpConnection
from requests_aws4auth import AWS4Auth
import boto3

host = '' # For example, my-test-domain.us-east-1.es.amazonaws.com
region = '' # e.g. us-west-1

service = 'es'
credentials = boto3.Session().get_credentials()
awsauth = AWS4Auth(credentials.access_key, credentials.secret_key, region, service)

es = Elasticsearch(
    hosts = [{'host': host, 'port': 443}],
    http_auth = awsauth,
    use_ssl = True,
    verify_certs = True,
    connection_class = RequestsHttpConnection
)

document = {
    "title": "Moneyball",
    "director": "Bennett Miller",
    "year": "2011"
}

es.index(index="movies", doc_type="_doc", id="5", body=document)

print(es.get(index="movies", doc_type="_doc", id="5"))

botocoreを利用する

下記の記事で行われている方法です。 Boto3が行う署名処理は内部実装であるbotocoreに含まれていますので、それを直接利用することでも署名を行うことができます。

[小ネタ] botocoreのAWS APIリクエストの署名プロセスのみを利用する

ただしこの記事は2年前のもので現在ではAPIが変わっています。 具体的にはBotocoreHTTPSessionをURLLib3Sessionに変える必要がありました。

    url = 'https://s3.{}.amazonaws.com/'.format(os.environ['AWS_REGION'])
    credentials = botocore.credentials.Credentials(
        os.environ['AWS_ACCESS_KEY_ID'],
        os.environ['AWS_SECRET_ACCESS_KEY'],
        os.environ['AWS_SESSION_TOKEN'])
    request = botocore.awsrequest.AWSRequest(method='GET', url=url)
    botocore.auth.S3SigV4Auth(
        credentials, 's3', os.environ['AWS_REGION']).add_auth(request)
    response = botocore.httpsession.URLLib3Session().send(request.prepare())

このように、botocoreはユーザが直接利用することを基本的に想定しておらず、APIが予告なく変わる可能性があります。 ですので、もし利用する場合はバージョンアップすると動作しなくなるなどの可能性を理解した上でご利用ください。

参考: https://github.com/boto/botocore/issues/1544

自力でやる

どうしても他のパッケージを使うことができない事情がある、あるいは個人の趣味として自分で実装してみたい、などの場合には、 自力で署名を生成することになります。 署名の手順はなかなか複雑ですが、まずは下記のドキュメントから見ていくと良いと思います。

署名バージョン 4 を使用して AWS リクエストに署名する

基本的には以下の流れで署名を生成します。

  1. 正規リクエスト文字列を作成し、そのハッシュ値を求める
  2. そのハッシュ値を含めた署名文字列を作る
  3. 署名文字列とシークレットキーなどから署名を生成する
  4. 署名とアクセスキーを含めたAuthorizationヘッダをつけてリクエストする

実装する際は下記のドキュメント(現在英語のみ)もご参照ください。S3へのリクエストではx-amz-content-sha256ヘッダが必要だったりします。

Signature Calculations for the Authorization Header: Transferring Payload in a Single Chunk (AWS Signature Version 4)

まとめ

Boto3以外でV4署名リクエストを行う方法をご紹介しました。基本的には自分で実装せずにパッケージを利用することをお勧めします。 あまり利用するケースは多くないかもしれませんが、参考になれば幸いです。なお、自分が試したコードは下記に置いてあります。

https://gist.github.com/ktsmy/ff46f3c008232fad29a5a65f5b831fb2