概要
AWS-SDKをbashで作りました。
AWS-SDKやプログラミング環境の用意できないPCでも、AWSが扱えるようになります。
作るにあたって、以下の条件を守るようにしています。
- プレーンなbash(今回はgitbash)を利用、標準のgitbashにないものは使わないこと
- プログラミング言語は使わない。awk, sed, bashで解決すること
- プロファイル情報はAWSと同じものを利用すること(~/.aws/credentials)
サービスごとに記事を開閉できるようにしていますので、必要なサービスだけ読んでいただければ。
構成図
できあがったもの
SDKの全機能の動作確認はできないため、記事では以下の動作確認にとどめます。
- DynamoDBのデータを操作
- Lambdaを直接実行
- SQSのデータを操作
- STSから自身のプロファイル情報を取得
※なお、S3は違いが大きいため、今回のスクリプトでは対応しません。
簡単な動作確認以上の試験はしていないので、利用は自己責任の範囲でお願いします。
利用の前に:プロファイルの用意
利用の際は、~/.aws/credentialsにプロファイル情報を作成しておいてください。
形式はテキスト、拡張子は不要です。
[default]
aws_access_key_id = AKIA**************************
aws_secret_access_key = *****************************
動作確認:DynamoDBのデータを操作
同じリクエストをAWS-SDK(boto3)で書いた場合 引数を変えて、データの登録、スキャン操作もできることを確認 リクエストパラメータの詳細DynamoDBを操作する手順(クリックで開きます)
./aws-sdk-bash.sh "default" "ap-northeast-1" "dynamodb" "/" "" \
"content-type:application/x-amz-json-1.0;host:@;x-amz-date:@;x-amz-target:DynamoDB_20120810.GetItem" \
"{\"TableName\": \"target_table\", \"Key\": {\"id\": {\"S\": \"key\"}}}"
{"Item":{"entity":{"S":"string_data"},"id":{"S":"key"}}}
from boto3 import Session
Session(
profile_name = "default",
region_name = "ap-northeast-1"
).client(
service_name = "dynamodb"
).get_item(
TableName = "target_table",
Key = {"id" : {"S" : "key"}}
)
# データを入れる
./aws-sdk-bash.sh "default" "ap-northeast-1" "dynamodb" "/" "" \
"content-type:application/x-amz-json-1.0;host:@;x-amz-date:@;x-amz-target:DynamoDB_20120810.PutItem" \
"{\"TableName\": \"target_table\", \"Item\": {\"id\": {\"S\": \"newData\"}, \"entity\":{\"S\":\"new_string_data\"}}}"
{}
# データを入れた後、Scanで件数が増えていることを確認する
./aws-sdk-bash.sh "default" "ap-northeast-1" "dynamodb" "/" "" \
"content-type:application/x-amz-json-1.0;host:@;x-amz-date:@;x-amz-target:DynamoDB_20120810.Scan" \
"{\"TableName\": \"target_table\"}"
{"Count":2,"Items":[{"entity":{"S":"new_string_data"},"id":{"S":"newData"}},{"entity":{"S":"string_data"},"id":{"S":"key"}}],"ScannedCount":2}
パラメータ名
上記リクエスト例の内容
備考
プロファイル名
default
認証名
リージョン
ap-northeast-1
リージョン情報
サービス名
dynamodb
サービス名。DynamoDB
URIパス
/
エンドポイントのパス、DynamoDBではルートパス
URLクエリ
DynamoDBでは使用しない
HTTPヘッダ
content-type:application/x-amz-json-1.0;
host:@;
x-amz-date:@;
x-amz-target:DynamoDB_20120810.GetItem@はスクリプト側で付与する
x-amz-targetで実施する処理を指定できる。
取得(GetItem)
追加(PutItem)
スキャン(Scan)…etc
POSTペイロード
{"TableName":"target_table", "Key": {"id" : "S" : "key"}}
動作確認:Lambdaを直接実行
同じリクエストをAWS-SDK(boto3)で書いた場合 リクエストパラメータLambdaを操作する手順(クリックで開きます)
./aws-sdk-bash.sh "default" "ap-northeast-1" "lambda" "/2015-03-31/functions/sample_lambda/invocations" "" \
"host:@;x-amz-content-sha256:@;x-amz-date:@;x-amz-invocation-type:RequestResponse;x-amz-user-agent:aws-sdk-js/2.668.0 callback" \
"{\"Message\":\"Hello\"}"
{"statusCode": 200, "body": "\"Hello from Lambda! I am SAMPLE\""}
from boto3 import Session
import json
Session(
profile_name = "default",
region_name = "ap-northeast-1"
).client(
service_name = "lambda"
).invoke(
FunctionName = "sample_lambda",
InvocationType = "RequestResponse",
Payload = json.dumps({"Message" : "Hello"})
)
パラメータ名
上記リクエスト例の内容
備考
プロファイル
default
認証名
リージョン
ap-northeast-1
リージョン情報
サービス名
lambda
サービス名。Lambda
URIパス
/2015-03-31/functions/sample_lambda/invocations
実施する操作
ラムダ名「sample_lambda」に「invoke」コマンドを発行する
URLクエリ
Lambdaでは使用しない
HTTPヘッダ
host:@;
x-amz-content-sha256:@;
x-amz-date:@;
x-amz-invocation-type:RequestResponse;
x-amz-user-agent:aws-sdk-js/2.668.0 callback@のものはスクリプト側で付与する
応答あり同期処理のためRequestResponseを指定
UAはJavaScript版のUAを借用
POSTペイロード
{"Message":"Hello"}
動作確認:SQSのデータを操作
メッセージの送信をboto3で書いた場合 リクエストパラメータSQSを操作する手順(クリックで開きます)
# メッセージを送信
./aws-sdk-bash.sh "default" "ap-northeast-1" "sqs" "/" "" \
"host:@;x-amz-content-sha256:@;x-amz-date:@;x-amz-user-agent:aws-sdk-js/2.668.0 callback" \
"Action=SendMessage&MessageBody=%7B%22id%22%3A%22NewMessage%22%7D&QueueUrl=https%3A%2F%2Fsqs.ap-northeast-1.amazonaws.com%2F***********************%2Fsqs-send-request-test-0424&Version=2012-11-05"
<?xml version="1.0"?><SendMessageResponse xmlns="http://queue.amazonaws.com/doc/2012-11-05/"><SendMessageResult><MessageId>013a849d-f344-4d90-a6be-e37302c6b029</MessageId><MD5OfMessageBody>abc832604cb20908715bca6f197c8945</MD5OfMessageBody></SendMessageResult><ResponseMetadata><RequestId>01e07779-a593-5162-b698-2050d59e1524</RequestId></ResponseMetadata></SendMessageResponse>
# 送信したメッセージを受信
./aws-sdk-bash.sh "default" "ap-northeast-1" "sqs" "/" "" \
"host:@;x-amz-content-sha256:@;x-amz-date:@;x-amz-user-agent:aws-sdk-js/2.668.0 callback" \
"Action=ReceiveMessage&QueueUrl=https%3A%2F%2Fsqs.ap-northeast-1.amazonaws.com%2F*****************%2Fsqs-send-request-test-0424&Version=2012-11-05"
<?xml version="1.0"?><ReceiveMessageResponse xmlns="http://queue.amazonaws.com/doc/2012-11-05/"><ReceiveMessageResult><Message><MessageId>6335c486-d1b6-4478-84c4-40b92f5d655b</MessageId><ReceiptHandle>***********</ReceiptHandle><MD5OfBody>abc832604cb20908715bca6f197c8945</MD5OfBody><Body>{"id":"NewMessage"}</Body></Message></ReceiveMessageResult><ResponseMetadata><RequestId>9e05f85b-4a27-5d6f-97c4-af723bb68a70</RequestId></ResponseMetadata></ReceiveMessageResponse>
from boto3 import Session
import json
Session(
profile_name = "default",
region_name = "ap-northeast-1"
).client(
service_name = "sqs"
).send_message(
QueueUrl = "https://sqs.ap-northeast-1.amazonaws.com/**************/sqs-send-request-test-0424",
MessageBody = json.dumps({"id": "NewMessage"})
)
パラメータ名
上記リクエスト例の内容
備考
プロファイル
default
認証名
リージョン
ap-northeast-1
リージョン情報
サービス名
sqs
サービス名。SQS
URIパス
/
SQSでは使用しない
URLクエリ
SQSでは使用しない
HTTPヘッダ
host:@;
x-amz-content-sha256:@;
x-amz-date:@;
x-amz-user-agent:aws-sdk-js/2.668.0 callback@のものはスクリプト側で付与する
UAはJavaScript版のUAを借用
POSTペイロード
Action=SendMessage&MessageBody=%7B%22id%22%3A%22NewMessage%22%7D&QueueUrl=https%3A%2F%2Fsqs.ap-northeast-1.amazonaws.com%2F***********************%2Fsqs-send-request-test-0424&Version=2012-11-05
SQSはフォームデータ形式で送信される。
送信、受信などの操作の切り替えはActionで行う。
動作確認:STSから自身のプロファイル情報を取得
同じ処理をboto3で書いた場合 リクエストパラメータSTSを操作する手順(クリックで開きます)
# 自身の情報を取得する
./aws-sdk-bash.sh "default" "ap-northeast-1" "sts" "/" "" \
"host:@;x-amz-content-sha256:@;x-amz-date:@;x-amz-user-agent:aws-sdk-js/2.668.0 callback" \
"Action=GetCallerIdentity&Version=2011-06-15"
<GetCallerIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<GetCallerIdentityResult>
<Arn>arn:aws:iam::*****************:user/**************</Arn>
<UserId>*****************</UserId>
<Account>*****************</Account>
</GetCallerIdentityResult>
<ResponseMetadata>
<RequestId>1703851c-5b55-49b5-8bcf-130ab4dbb6f2</RequestId>
</ResponseMetadata>
</GetCallerIdentityResponse>
from boto3 import Session
Session(
profile_name = "default",
region_name = "ap-northeast-1"
).client(
service_name = "sts"
).get_caller_identity()
パラメータ名
上記リクエスト例の内容
備考
プロファイル
default
認証名
リージョン
ap-northeast-1
リージョン情報
サービス名
sts
サービス名。STS
URIパス
/
SQSでは使用しない
URLクエリ
SQSでは使用しない
HTTPヘッダ
host:@;
x-amz-content-sha256:@;
x-amz-date:@;
x-amz-user-agent:aws-sdk-js/2.668.0 callback@のものはスクリプト側で付与する
UAはJavaScript版のUAを借用
POSTペイロード
Action=GetCallerIdentity&Version=2011-06-15
STSはフォームデータ形式で送信される。
操作の切り替えはActionで行う。
ソースコード
動かす場合は、aws-sdk-bash.shのファイル名で任意の場所に保存してください。
実行するコマンドは「動作確認」を参照してください。
#!/bin/bash
# プロファイルのパスは固定
CREDENTIALS_FILE=~/.aws/credentials
# アルゴリズムは署名v4、hmacのSHA256
ALGORITHM='AWS4-HMAC-SHA256'
# スクリプトの引数
_INPUT_PROFILE_NAME=$1
_INPUT_REGION=$2
_INPUT_SERVICE=$3
_INPUT_CANONICAL_URI=$4
_INPUT_CANONICAL_QUERY_STRING=$5
_INPUT_OPTIONAL_HEADERS=$6
_INPUT_PAYLOAD=$7
#エンドポイントの作成
METHOD=POST
PROTOCOL=https
HOST_NAME=${_INPUT_SERVICE}.${_INPUT_REGION}.amazonaws.com
# .awsファイルからプロファイル情報を取得する
# 入力 プロファイルのパス、プロファイル名、プロファイルのキー名、ファイルの読込行数
get_credentials () {
_CREDENTIALS_FILE=$1; _PROFILE_NAME=$2; _KEY_NAME=$3; _READ_LENGTH=$4;
# PROFILE_NAMEの行番号を取得する
PROFILE_IDX=`nl $_CREDENTIALS_FILE | grep $_PROFILE_NAME | head -n 1 | awk '{print $1}'`
PROFILE_IDX_END=`expr $PROFILE_IDX + $_READ_LENGTH`
# アクセスキーIDを取得する
RESULT=`cat $CREDENTIALS_FILE | sed -n "${PROFILE_IDX},${PROFILE_IDX_END}p" | grep "=" | grep ${_KEY_NAME} | \
tr -d " " | sed "s/=/ /g" | awk '{print $2}' | \
head -n 1`
echo -n $RESULT
}
# SHA256でハッシュを作成する
# 入力 メッセージ:ファイル、キー:なし
# 出力 ハッシュ:hex形式
create_digest_from_file () {
cat $1 | openssl dgst -sha256 | grep stdin | awk '{print $2}'
}
# HMAC-SHA256でハッシュを作成する
# 入力 メッセージ:テキスト、キー:テキスト
# 出力 ハッシュ:hex形式
sign_from_string () {
echo -n $2 | openssl dgst -sha256 -hmac $1 | grep stdin | awk '{print $2}'
}
# HMAC-SHA256でハッシュを作成する
# 入力 メッセージ:テキスト、キー:hex形式
# 出力 ハッシュ:hex形式
sign_from_string_with_hex_key () {
echo -n $2 | openssl dgst -sha256 -mac hmac -macopt hexkey:$1 | grep stdin | awk '{print $2}'
}
# HMAC-SHA256でハッシュを作成する
# 入力 メッセージ:ファイル、キー:hex形式
# 出力 ハッシュ:hex形式
sign_from_file_with_hex_key () {
cat $2 | openssl dgst -sha256 -mac hmac -macopt hexkey:$1 | grep stdin | awk '{print $2}'
}
# 署名v4の基本情報(アクセスキーID、送信日時、リージョン、サービス名)をHMAC-SHA256でハッシュ化する
get_signature_key () {
TEMP_DATE=`sign_from_string AWS4$1 $2`
TEMP_REGION=`sign_from_string_with_hex_key $TEMP_DATE $3`
TEMP_SERVICE=`sign_from_string_with_hex_key $TEMP_REGION $4`
sign_from_string_with_hex_key $TEMP_SERVICE 'aws4_request'
}
# アクセスキーIDを取得する
ACCESS_KEY_ID=`get_credentials $CREDENTIALS_FILE ${_INPUT_PROFILE_NAME} aws_access_key_id 2`
# シークレットアクセスキーを取得する
SECRET_ACCESS_KEY=`get_credentials $CREDENTIALS_FILE ${_INPUT_PROFILE_NAME} aws_secret_access_key 2`
# UTC日時を取得する(フォーマット例:2020/12/31T12:34:50のとき AMZ_DATE:20201231T123450Z DATE_STAMP:20201231)
UTC_DATE=`date -Iseconds -u | sed "s/+/ /g" | awk '{print $1 "Z"}'`
AMZ_DATE=`echo -n $UTC_DATE | sed "s/-//g" | sed "s/://g"`
DATE_STAMP=`echo -n $UTC_DATE | sed "s/-//g" | sed "s/T/ /g" | awk '{print $1}'`
# 一時ファイルを作成、一時ファイルは終了時に削除する
TEMP_HEADERS=`mktemp`
TEMP_CANONICAL_REQUEST=`mktemp`
TEMP_STRING_TO_SIGN=`mktemp`
TEMP_PAYLOAD=`mktemp`
trap "rm -f $TEMP_HEADERS; rm -f $TEMP_CANONICAL_REQUEST; rm -f $TEMP_STRING_TO_SIGN; rm -f $TEMP_PAYLOAD" EXIT
# ペイロードを一時ファイルに書き写す
echo -n ${_INPUT_PAYLOAD} > $TEMP_PAYLOAD
# 送信するBODYデータからSHA256のハッシュ(キーなし)を作成する
PAYLOAD_HASH=`create_digest_from_file $TEMP_PAYLOAD`
# 送信ヘッダを設定する
echo -n "${_INPUT_OPTIONAL_HEADERS}" | sed "s#x-amz-content-sha256:@#x-amz-content-sha256:${PAYLOAD_HASH}#" | sed "s#host:@#host:${HOST_NAME}#" | sed "s#x-amz-date:@#x-amz-date:${AMZ_DATE}#" | xargs -d ";" -r -I @ echo @ >> $TEMP_HEADERS
SIGNED_HEADERS=`echo -n "${_INPUT_OPTIONAL_HEADERS}" | xargs -d ";" -r -I @ echo ";@" | sed 's/:.*//'`
SIGNED_HEADERS=`echo -n $SIGNED_HEADERS | sed 's/ //g' | sed 's/^;//'`
# 正規リクエストを作成する
# リクエストの情報を標準化する
# 参考情報:タスク 1: 署名バージョン 4 の正規リクエストを作成する
# https://docs.aws.amazon.com/ja_jp/general/latest/gr/sigv4-create-canonical-request.html
echo $METHOD > $TEMP_CANONICAL_REQUEST
echo $_INPUT_CANONICAL_URI >> $TEMP_CANONICAL_REQUEST
echo $_INPUT_CANONICAL_QUERY_STRING >> $TEMP_CANONICAL_REQUEST
cat $TEMP_HEADERS >> $TEMP_CANONICAL_REQUEST
echo "" >> $TEMP_CANONICAL_REQUEST
echo $SIGNED_HEADERS >> $TEMP_CANONICAL_REQUEST
echo -n $PAYLOAD_HASH >> $TEMP_CANONICAL_REQUEST
# 署名文字列を作成する
# 参考情報:タスク 2: 署名バージョン 4 の署名文字列を作成する
# https://docs.aws.amazon.com/ja_jp/general/latest/gr/sigv4-create-string-to-sign.html
CANONICAL_REQUEST_HASH=`create_digest_from_file $TEMP_CANONICAL_REQUEST`
CREDENTIAL_SCOPE=${DATE_STAMP}/${_INPUT_REGION}/${_INPUT_SERVICE}/"aws4_request"
# 署名のアルゴリズム、署名の有効範囲を設定する
echo ${ALGORITHM} > $TEMP_STRING_TO_SIGN
echo ${AMZ_DATE} >> $TEMP_STRING_TO_SIGN
echo ${CREDENTIAL_SCOPE} >> $TEMP_STRING_TO_SIGN
echo -n ${CANONICAL_REQUEST_HASH} >> $TEMP_STRING_TO_SIGN
# 署名のため、Signatureを計算する
# 参考情報:タスク 3: AWS署名バージョン 4 の署名を計算する
# https://docs.aws.amazon.com/ja_jp/general/latest/gr/sigv4-calculate-signature.html
SIGNING_KEY=`get_signature_key ${SECRET_ACCESS_KEY} ${DATE_STAMP} ${_INPUT_REGION} ${_INPUT_SERVICE}`
SIGNATURE=`sign_from_file_with_hex_key ${SIGNING_KEY} ${TEMP_STRING_TO_SIGN}`
# 署名をHTTPリクエストのヘッダに設定する
# 参考情報:タスク 4: HTTP リクエストに署名を追加する
# https://docs.aws.amazon.com/ja_jp/general/latest/gr/sigv4-add-signature-to-request.html
AUTHORIZATION_HEADER="${ALGORITHM} Credential=${ACCESS_KEY_ID}/${CREDENTIAL_SCOPE}, SignedHeaders=${SIGNED_HEADERS}, Signature=${SIGNATURE}"
# HTTPリクエストを送信する。
QUERY_STRING=${_INPUT_CANONICAL_QUERY_STRING}
if [ ! $QUERY_STRING = "" ]; then
QUERY_STRING="?${QUERY_STRING}"
fi
curl -s -X POST ${PROTOCOL}://${HOST_NAME}${_INPUT_CANONICAL_URI}${QUERY_STRING} -d @$TEMP_PAYLOAD -H @$TEMP_HEADERS -H "Authorization: ${AUTHORIZATION_HEADER}"
ソースコードの解説
処理の流れは以下の通りです。
様々な言語のために様々なSDKがありますが、この4つさえ実装すればAWS-SDKと同じことができます。
\def\の{\unicode[serif]{x306E}}
\bbox[8px, border: 2px solid gray]{\rlap{\tt 1.\quad プロファイル情報\の取得}\hspace{80mm}}
\triangledown
\def\の{\unicode[serif]{x306E}}
\bbox[8px, border: 2px solid gray]{\rlap{\tt 2.\quad パラメータ\の取得と作成}\hspace{80mm}}
\triangledown
\bbox[8px, border: 2px solid gray]{\rlap{\tt 3.\quad パラメータをSHA256でハッシュ化}\hspace{80mm}}
\triangledown
\bbox[8px, border: 2px solid gray]{\rlap{\tt 4.\quad curlでHTTPリクエストを送信}\hspace{80mm}}
バージョン4の署名については、AWS公式の「完全な 署名バージョン 4 署名プロセスの例 (Python)」を参考にしてください。
以降、それぞれの処理について解説します。
1. プロファイル情報の取得
~/.aws/credentialsからプロファイル情報を取得する処理です。
\def\の{\unicode[serif]{x306E}}
\begin{array}{l|l}
\hline
コマンド & 目的 \\
\hline
{\tt nl} & 行番号 + ファイル\の内容を表示します。\\
& 行番号\の出るcatです。\\
\hdashline
{\tt sed\;\text{-n}} & 指定行\のテキストを取得します。\\
\hline
\end{array}
「nl + grep プロファイル名」で、取得するプロファイル名の行数を取得します。
「sed -n」でプロファイル名の後ろの行が取れるため、そこからアクセスキーIDとシークレットアクセスキーを取ります。
# .awsファイルからプロファイル情報を取得する
# 入力 プロファイルのパス、プロファイル名、プロファイルのキー名、プロファイルの読込行数
get_credentials () {
_CREDENTIALS_FILE=$1; _PROFILE_NAME=$2; _KEY_NAME=$3; _READ_LENGTH=$4;
# PROFILE_NAMEの行番号を取得する
PROFILE_IDX=`nl $_CREDENTIALS_FILE | grep $_PROFILE_NAME | head -n 1 | awk '{print $1}'`
PROFILE_IDX_END=`expr $PROFILE_IDX + $_READ_LENGTH`
# アクセスキーIDを取得する
RESULT=`cat $CREDENTIALS_FILE | sed -n "${PROFILE_IDX},${PROFILE_IDX_END}p" | grep "=" | grep ${_KEY_NAME} | \
tr -d " " | sed "s/=/ /g" | awk '{print $2}' | \
head -n 1`
echo -n $RESULT
}
# アクセスキーIDを取得する
ACCESS_KEY_ID=`get_credentials $CREDENTIALS_FILE ${_INPUT_PROFILE_NAME} aws_access_key_id 2`
# シークレットアクセスキーを取得する
SECRET_ACCESS_KEY=`get_credentials $CREDENTIALS_FILE ${_INPUT_PROFILE_NAME} aws_secret_access_key 2`
2.パラメータの取得と作成
日時の取得
日時はUTCで取得します。
時分秒を含むフォーマットと、時分秒を含まないフォーマットの文字列を作成します。
\def\の{\unicode[serif]{x306E}}
\begin{array}{l|l}
\hline
値 & フォーマット \\
\hline
{\tt \text{AMZ_DATE}} & {\tt YYYYMMDDTHHMMSS}\のフォーマットで指定します。\\
& 後ろには{\tt UTC}を指す{\tt Z}をつけます。\\
& コロン、ハイフン\のような記号は入れません。\\
\hdashline
{\tt \text{DATE_STAMP}} & {\tt YYYYMMDD}\のフォーマットで指定します。\\
& 日付だけを指定します。\\
& コロン、ハイフン\のような記号は入れません。\\
\hline
\end{array}
# UTC日時を取得する(フォーマット例:2020/12/31T12:34:50のとき AMZ_DATE:20201231T123450Z DATE_STAMP:20201231)
UTC_DATE=`date -Iseconds -u | sed "s/+/ /g" | awk '{print $1 "Z"}'`
AMZ_DATE=`echo -n $UTC_DATE | sed "s/-//g" | sed "s/://g"`
DATE_STAMP=`echo -n $UTC_DATE | sed "s/-//g" | sed "s/T/ /g" | awk '{print $1}'`
テキストを扱う
文字列は一時ファイルに書き出して扱います。
一時ファイルはプロセスが終了したタイミングで削除されます。
bashの変数で扱うこともできますが、ファイルで文字列を扱うほうが実装しやすくなります。
署名情報は、空行、末尾の改行、データの出現順序が厳密に決まっており、一つでもずれると署名が通らなくなります。
\def\の{\unicode[serif]{x306E}}
\begin{array}{l|l}
\hline
コマンド & 目的 \\
\hline
{\tt mktemp} & /tmpに一時ファイルを作成します。\\
& ファイル名は重複なくつけられ、\\
& アクセス権限も適切に設定されます。\\
\hdashline
{\tt trap} & 特定\のタイミングでコマンドを実行します。\\
& プロセス終了時に一時ファイルを削除します。\\
\hline
\end{array}
# 一時ファイルを作成、一時ファイルは終了時に削除する
TEMP_HEADERS=`mktemp`
TEMP_CANONICAL_REQUEST=`mktemp`
TEMP_STRING_TO_SIGN=`mktemp`
TEMP_PAYLOAD=`mktemp`
trap "rm -f $TEMP_HEADERS; rm -f $TEMP_CANONICAL_REQUEST; rm -f $TEMP_STRING_TO_SIGN; rm -f $TEMP_PAYLOAD" EXIT
パラメータの置換
リクエストの時にユーザーが意識しないパラメータがあります。
そういったパラメータは、@で値を省略することで、スクリプト側に設定させます。
\begin{array}{ll}
ユーザーが設定するパラメータ & {\tt \text{x-amz-date:@}} \\
実際に送信されるパラメータ & {\tt \text{x-amz-date:20200504T145432Z}}\\
& \\
\end{array}
\def\の{\unicode[serif]{x306E}}
\begin{array}{l|l}
\hline
ヘッダ\のキー & 設定される値 \\
\hline
{\tt \text{x-amz-content-sha256}} & ペイロード\のハッシュ値\\
\hdashline
{\tt host} & {\tt AWSエンドポイント\のURL}\\
\hdashline
{\tt \text{x-amz-date}} & リクエストした日時\\
\hline
\end{array}
# 送信するBODYデータからSHA256のハッシュ(キーなし)を作成する
PAYLOAD_HASH=`create_digest_from_file $TEMP_PAYLOAD`
# 送信ヘッダを設定する
echo -n "${_INPUT_OPTIONAL_HEADERS}" | sed "s#x-amz-content-sha256:@#x-amz-content-sha256:${PAYLOAD_HASH}#" | sed "s#host:@#host:${HOST_NAME}#" | sed "s#x-amz-date:@#x-amz-date:${AMZ_DATE}#" | xargs -d ";" -r -I @ echo @ >> $TEMP_HEADERS
SIGNED_HEADERS=`echo -n "${_INPUT_OPTIONAL_HEADERS}" | xargs -d ";" -r -I @ echo ";@" | sed 's/:.*//'`
SIGNED_HEADERS=`echo -n $SIGNED_HEADERS | sed 's/ //g' | sed 's/^;//'`
3.ハッシュ化
HMAC-SHA256の取得は、Pythonでは以下のように書きます。
def sign(key, msg):
return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
def getSignatureKey(key, dateStamp, regionName, serviceName):
kDate = sign(("AWS4" + key).encode("utf-8"), dateStamp)
kRegion = sign(kDate, regionName)
kService = sign(kRegion, serviceName)
kSigning = sign(kService, "aws4_request")
return kSigning
同じことをbashで実現するには、opensslを使います。
\def\の{\unicode[serif]{x306E}}
\begin{array}{l|l}
\hline
コマンド & 目的 \\
\hline
{\tt openssl\;dgst\;\text{-}sha256} & テキストを{\tt SHA256}でハッシュ化します。\\
& 結果は16進数になります。\\
\hdashline
{\tt openssl\;dgst\;\text{-}sha256} & テキストを{\tt SHA256}でハッシュ化します。\\
\qquad{\tt \text{-}hmac\;(key)} & プレーンテキスト\のキーを指定します。\\
& 結果は16進数になります。\\
\hdashline
{\tt openssl\;dgst\;\text{-}sha256} & テキストを{\tt SHA256}でハッシュ化します。\\
\qquad{\tt \text{-}mac\;hmac} & 16進数\のキーを指定します。\\
\qquad{\tt \text{-}macopt\;hexkey:(key)}& 結果は16進数になります。\\
\hline
\end{array}
# SHA256でハッシュを作成する
# 入力 メッセージ:ファイル、キー:なし
# 出力 ハッシュ:hex形式
create_digest_from_file () {
cat $1 | openssl dgst -sha256 | grep stdin | awk '{print $2}'
}
# HMAC-SHA256でハッシュを作成する
# 入力 メッセージ:テキスト、キー:テキスト
# 出力 ハッシュ:hex形式
sign_from_string () {
echo -n $2 | openssl dgst -sha256 -hmac $1 | grep stdin | awk '{print $2}'
}
# HMAC-SHA256でハッシュを作成する
# 入力 メッセージ:テキスト、キー:hex形式
# 出力 ハッシュ:hex形式
sign_from_string_with_hex_key () {
echo -n $2 | openssl dgst -sha256 -mac hmac -macopt hexkey:$1 | grep stdin | awk '{print $2}'
}
# HMAC-SHA256でハッシュを作成する
# 入力 メッセージ:ファイル、キー:hex形式
# 出力 ハッシュ:hex形式
sign_from_file_with_hex_key () {
cat $2 | openssl dgst -sha256 -mac hmac -macopt hexkey:$1 | grep stdin | awk '{print $2}'
}
# 署名v4の基本情報(アクセスキーID、送信日時、リージョン、サービス名)をHMAC-SHA256でハッシュ化する
# PythonのgetSignatureKeyと同じ処理
get_signature_key () {
TEMP_DATE=`sign_from_string AWS4$1 $2`
TEMP_REGION=`sign_from_string_with_hex_key $TEMP_DATE $3`
TEMP_SERVICE=`sign_from_string_with_hex_key $TEMP_REGION $4`
sign_from_string_with_hex_key $TEMP_SERVICE 'aws4_request'
}
AWS-SDKのリクエストの調べ方
AWS-SDKが投げるペイロードとヘッダの形式は、サービスによってバラバラです。
公式にドキュメントはありませんので、自分で調べるしかありません。
サービス | メソッドの指定方法 | ペイロード |
---|---|---|
Lambda | URLパス | JSON |
DynamoDB | ヘッダ | JSON |
SQS | ペイロード | Form形式 |
STS | ペイロード | Form形式 |
boto3+Wiresharkで調べる方法
そのまま通信すると暗号化されるので、ちょっと小細工を入れます。
import boto3
client = boto3.client("dynamodb", use_ssl = False)
print(client.get_item(TableName = "target_table", Key = {"id" : {"S":"key"}}))
use_sslをFalseにすると80ポートへの送信になります。
平文で通信するので、Wiresharkで読めるようになります。
Wiresharkを見ると、送信しているデータが以下の通りだとわかります。
POST / HTTP/1.1
Host: dynamodb.ap-northeast-1.amazonaws.com
Accept-Encoding: identity
X-Amz-Target: DynamoDB_20120810.GetItem
Content-Type: application/x-amz-json-1.0
User-Agent: Boto3/1.12.43 Python/3.8.2 Windows/10 Botocore/1.15.43
X-Amz-Date: 20200501T213154Z
Authorization: AWS4-HMAC-SHA256 Credential=AKIA**********/20200501/ap-northeast-1/dynamodb/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-amz-target, Signature=***********************************************************
Content-Length: 58
{"TableName": "target_table", "Key": {"id": {"S": "key"}}}
同じリクエストが飛ぶようにデータを設定すればOKです。
なお、一部のサービスではuse_sslが使えません。たとえばLambdaやMQTTがそうです。
HTTPSにしか対応しないサービスでポート80につなごうとすると、レスポンスが返らずにタイムアウトします。
※SSL無しで通信できるかどうかは、AWSに「サービス エンドポイントとクォータの表」があります。
エンドポイントのプロトコルがHTTP or HTTPSになっているものは通信できます。
ブラウザの検証機能で調べる方法
SSLが必要なサービスでペイロードを見る場合は、javascript版のSDKを使います。
<script src="https://sdk.amazonaws.com/js/aws-sdk-2.668.0.min.js"></script>
<script type="text/javascript">
AWS.config.update({
accessKeyId : 'AKIA******************',
secretAccessKey : '**********************************'
});
AWS.config.region = 'ap-northeast-1';
let lambda = new AWS.Lambda();
let params = {
FunctionName : 'sample_lambda',
InvocationType : 'RequestResponse',
Payload : JSON.stringify({
"Message" : "Hello"
})
};
lambda.invoke(params, (err, data) => console.log(JSON.parse(data.Payload)));
</script>
HTMLファイルに確認したい処理を書いた後、ブラウザで開きます。
Chromeなら、右クリックでブラウザの「検証」を開くことで、ネットワークの通信データを確認できます。
ブラウザ固有のデータも含まれる、CORSが必要になる場合があるなどの違いはありますが、必要なデータはそろっています。
これを参考に、同じリクエストが飛ぶように設定すればOKです。