はじめに
今回は、昔にElixirでAWSを使うためのSignatureを生成してくれるメソッドの製作経験を語ろうと思います。
基本的にAWSを利用するのはexawsというライブラリーを利用すればいいですが、
何せ初心者なので、あの時公式のドキュメントとPythonコードだけ読んで直接作ってしまいました・・・
無駄足になりましたが、AWS Signatureの概念について理解が深めたので、よしとしましょう!
AWS Signature V4とは
基本概念
Amazon Web ServicesのAPIを利用するために、もちろん利用者(自分)の情報をリクエストに入れて、Amazonに知らせる必要がある。
それは、自分のアクセスキーだけを提供しても実現できるが、それだけだと、もしリクエスト内容が途中で誰か悪意のある者に変えられたら(MethodをPUTからDELETEへ置換したり)大惨事になりかねないので、リクエスト内容が途中で変えられたことはないと保証してくれるのがSignature。
実際の仕組み
では、Signatureはどうやってリクエスト内容を保証するんだろう?
答えは簡単、リクエストの内容を全部、自分とAmazonしか分からないキーでエンコードして、Signatureを生成、それをリクエストを入れて送信。
Amazon側はリクエストを受信した後、全く同じのプロセスを経てSignatureを算出、それとリクエスト中のSignatureと同様であれば、そのリクエストは信頼できるということ。
もし誰かが途中でリクエスト内容を変えたら、Signatureは完全に合わなくなるので、処理されることはない。Signatureを偽造しようとしても、キーが分からなければ、それもできるはずがない。リクエストの安全性が保証されるわけだ。
Elixirで実現
概念を知ったら、基本的にはAmazonの手順を従って作れば難しいことはない。
Pythonコードを見ながら作るのは一番早いかもしれない。
直接コメントで説明するのでコードを読んだらわかると思う。
defmodule TestProject.AWSRequest do
# 例として AWS Rekognitionを利用
# 以下の二つは基本固定している、他は使いたいAPIによって変更する
@content_type "application/x-amz-json-1.1"
@algorithm "AWS4-HMAC-SHA256"
@method "POST"
@host "rekognition.ap-northeast-1.amazonaws.com"
@region "ap-northeast-1"
@service "rekognition"
@amz_target "RekognitionService.CompareFaces"
@doc """
Return a `map` contains headers.
`access_key`: Your AWS Access key
`secret_key`: Your AWS secret key
`request_body`: Must be string (for encoding).
"""
def gen_headers(access_key, secret_key, request_body) do
# URIとQuery、特にないので "/" と "" にする
canonical_uri = "/"
canonical_querystring = ""
# 時間を準備
time_now = DateTime.utc_now |> DateTime.to_naive
amz_date = time_now |> format_to_amz_date
day = time_now |> format_to_MMYYDD
# Headerを準備
canonical_headers = "content-type:" <> @content_type <> "\n"
<> "host:" <> @host <> "\n"
<> "x-amz-date:" <> amz_date <> "\n"
<> "x-amz-target:" <> @amz_target <> "\n"
# Headerの内容説明
signed_headers = "content-type;host;x-amz-date;x-amz-target"
# RequestのBodyをエンコード
hashed_payload = hash_sha256(request_body)
# ここまでこれば準備完了
# これからSignature生成に入る
# 基本的には全部の情報を串刺しにして、キーを使ってSignatureを生成する
# その前は、キー自体も生成する必要がある(secret keyなどを使って)
# まずは情報を串刺しにして、SignされるStringを生成する
canonical_request = @method <> "\n"
<> canonical_uri <> "\n"
<> canonical_querystring <> "\n"
<> canonical_headers <> "\n"
<> signed_headers <> "\n"
<> hashed_payload
hashed_canonical_request = hash_sha256(canonical_request)
scope = "#{day}/#{@region}/#{@service}/aws4_request"
string_to_sign = "AWS4-HMAC-SHA256\n#{amz_date}\n#{scope}\n#{hashed_canonical_request}"
# Signをするキーの生成とSignatureの生成
signature = secret_key |> build_signing_key(day) |> build_signature(string_to_sign)
# 完成!
%{
"Host" => @host,
"X-Amz-Target" => @amz_target,
"X-Amz-Date"=> amz_date,
"Content-Type" => @content_type,
"Authorization" => "#{@algorithm} Credential=#{access_key}/#{scope}, SignedHeaders=#{signed_headers}, Signature=#{signature}"
}
end
# 時間系
defp format_to_amz_date(time) do
formatted_time = time
|> NaiveDateTime.to_iso8601
|> String.split(".")
|> List.first
|> String.replace("-", "")
|> String.replace(":", "")
formatted_time <> "Z"
end
defp format_to_MMYYDD(date) do
date
|> NaiveDateTime.to_date
|> Date.to_iso8601
|> String.replace("-", "")
end
# エンコード用
defp hmac_sha256(key, data) do
:crypto.hmac(:sha256, key, data)
end
# エンコード用
defp hash_sha256(data) do
:crypto.hash(:sha256, data)
|> bytes_to_string
end
# サインするためのキーを生成
defp build_signing_key(secret_key, day) do
hmac_sha256("AWS4#{secret_key}", day)
|> hmac_sha256(@region)
|> hmac_sha256(@service)
|> hmac_sha256("aws4_request")
end
# サインをする
defp build_signature(signing_key, string_to_sign) do
hmac_sha256(signing_key, string_to_sign)
|> bytes_to_string
end
# エンコード用
defp bytes_to_string(bytes) do
Base.encode16(bytes, case: :lower)
end
end