4
4

More than 3 years have passed since last update.

AWS-SDKをbashで自作してみる

Last updated at Posted at 2020-05-04

概要

AWS-SDKをbashで作りました。
AWS-SDKやプログラミング環境の用意できないPCでも、AWSが扱えるようになります。

作るにあたって、以下の条件を守るようにしています。

  • プレーンなbash(今回はgitbash)を利用、標準のgitbashにないものは使わないこと
  • プログラミング言語は使わない。awk, sed, bashで解決すること
  • プロファイル情報はAWSと同じものを利用すること(~/.aws/credentials)

サービスごとに記事を開閉できるようにしていますので、必要なサービスだけ読んでいただければ。

構成図

draw_1.png

できあがったもの

SDKの全機能の動作確認はできないため、記事では以下の動作確認にとどめます。

  • DynamoDBのデータを操作
  • Lambdaを直接実行
  • SQSのデータを操作
  • STSから自身のプロファイル情報を取得

※なお、S3は違いが大きいため、今回のスクリプトでは対応しません。
 簡単な動作確認以上の試験はしていないので、利用は自己責任の範囲でお願いします。

利用の前に:プロファイルの用意

利用の際は、~/.aws/credentialsにプロファイル情報を作成しておいてください。
形式はテキスト、拡張子は不要です。

~/.aws/credentials
[default]
aws_access_key_id = AKIA**************************
aws_secret_access_key = *****************************

動作確認:DynamoDBのデータを操作

DynamoDBを操作する手順(クリックで開きます)
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"}}}

同じリクエストをAWS-SDK(boto3)で書いた場合

boto3
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"}}
)

引数を変えて、データの登録、スキャン操作もできることを確認

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.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を直接実行

Lambdaを操作する手順(クリックで開きます)
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\""}

同じリクエストをAWS-SDK(boto3)で書いた場合

boto3
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のデータを操作

SQSを操作する手順(クリックで開きます)
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>{&quot;id&quot;:&quot;NewMessage&quot;}</Body></Message></ReceiveMessageResult><ResponseMetadata><RequestId>9e05f85b-4a27-5d6f-97c4-af723bb68a70</RequestId></ResponseMetadata></ReceiveMessageResponse>

メッセージの送信をboto3で書いた場合

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から自身のプロファイル情報を取得

STSを操作する手順(クリックで開きます)
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>

同じ処理をboto3で書いた場合

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のファイル名で任意の場所に保存してください。
実行するコマンドは「動作確認」を参照してください。

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-sdk-bash.sh_署名情報の取得
# .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
}
aws-sdk-bash.sh_呼出側
# アクセスキー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}
aws-sdk-bash.sh_UTC日時の取得
# 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}
aws-sdk-bash.sh_一時ファイルの作成
# 一時ファイルを作成、一時ファイルは終了時に削除する
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}
aws-sdk-bash.sh_ヘッダを設定
# 送信する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では以下のように書きます。

HMAC-SHA256を取得
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}
aws-sdk-bash.sh_ハッシュ化
# 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で調べる方法

そのまま通信すると暗号化されるので、ちょっと小細工を入れます。

暗号化を切って、HTTP通信にする
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で読めるようになります。

capture_1.png

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を使います。

test.html
<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なら、右クリックでブラウザの「検証」を開くことで、ネットワークの通信データを確認できます。

lambda.png

ブラウザ固有のデータも含まれる、CORSが必要になる場合があるなどの違いはありますが、必要なデータはそろっています。
これを参考に、同じリクエストが飛ぶように設定すればOKです。

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4