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

# 概要

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

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

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

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

## 構成図

![draw_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233180/a4f3b7fb-8efe-7e26-7bae-b2380a6d7443.png)

# できあがったもの

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

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

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

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

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

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

## 動作確認：DynamoDBのデータを操作

<details><summary>DynamoDBを操作する手順（クリックで開きます）</summary><div>

```bash: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）で書いた場合**

```python: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"}}
)
```

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

```bash: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;<br />host:@;<br />x-amz-date:@;<br />x-amz-target:DynamoDB_20120810.GetItem|@はスクリプト側で付与する<br />x-amz-targetで実施する処理を指定できる。<br />取得（GetItem）<br />追加（PutItem）<br />スキャン（Scan）…etc|
|POSTペイロード|{"TableName":"target_table", "Key": {"id" : "S" : "key"}}||

</div></details>

## 動作確認：Lambdaを直接実行

<details><summary>Lambdaを操作する手順（クリックで開きます）</summary><div>

```bash: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）で書いた場合**

```python: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|実施する操作<br />ラムダ名「sample_lambda」に「invoke」コマンドを発行する|
|URLクエリ||Lambdaでは使用しない|
|HTTPヘッダ|host:@;<br />x-amz-content-sha256:@;<br />x-amz-date:@;<br />x-amz-invocation-type:RequestResponse;<br />x-amz-user-agent:aws-sdk-js/2.668.0 callback|@のものはスクリプト側で付与する<br />応答あり同期処理のためRequestResponseを指定<br />UAはJavaScript版のUAを借用|
|POSTペイロード|{"Message":"Hello"}||

</div></details>

## 動作確認：SQSのデータを操作

<details><summary>SQSを操作する手順（クリックで開きます）</summary><div>

```bash: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で書いた場合**

```python
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:@;<br />x-amz-content-sha256:@;<br />x-amz-date:@;<br />x-amz-user-agent:aws-sdk-js/2.668.0 callback|@のものはスクリプト側で付与する<br />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はフォームデータ形式で送信される。<br />送信、受信などの操作の切り替えはActionで行う。|

</div></details>

## 動作確認：STSから自身のプロファイル情報を取得

<details><summary>STSを操作する手順（クリックで開きます）</summary><div>

```bash: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で書いた場合**

```python
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:@;<br />x-amz-content-sha256:@;<br />x-amz-date:@;<br />x-amz-user-agent:aws-sdk-js/2.668.0 callback|@のものはスクリプト側で付与する<br />UAはJavaScript版のUAを借用|
|POSTペイロード|Action=GetCallerIdentity&Version=2011-06-15|STSはフォームデータ形式で送信される。<br />操作の切り替えはActionで行う。|

</div></details>

## ソースコード

動かす場合は、aws-sdk-bash.shのファイル名で任意の場所に保存してください。
実行するコマンドは「動作確認」を参照してください。

```bash: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と同じことができます。

```math
\def\の{\unicode[serif]{x306E}}
\bbox[8px, border: 2px solid gray]{\rlap{\tt 1.\quad プロファイル情報\の取得}\hspace{80mm}}
```
```math
\triangledown
```
```math
\def\の{\unicode[serif]{x306E}}
\bbox[8px, border: 2px solid gray]{\rlap{\tt 2.\quad パラメータ\の取得と作成}\hspace{80mm}}
```
```math
\triangledown
```
```math
\bbox[8px, border: 2px solid gray]{\rlap{\tt 3.\quad パラメータをSHA256でハッシュ化}\hspace{80mm}}
```
```math
\triangledown
```
```math
\bbox[8px, border: 2px solid gray]{\rlap{\tt 4.\quad curlでHTTPリクエストを送信}\hspace{80mm}}
```

バージョン4の署名については、AWS公式の[「完全な 署名バージョン 4 署名プロセスの例 (Python)」](https://docs.aws.amazon.com/ja_jp/general/latest/gr/sigv4-signed-request-examples.html)を参考にしてください。

以降、それぞれの処理について解説します。

### 1. プロファイル情報の取得

~/.aws/credentialsからプロファイル情報を取得する処理です。

```math
\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とシークレットアクセスキーを取ります。

```bash: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
}
```

```bash: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で取得します。
時分秒を含むフォーマットと、時分秒を含まないフォーマットの文字列を作成します。

```math
\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}
```

```bash: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の変数で扱うこともできますが、ファイルで文字列を扱うほうが実装しやすくなります。
署名情報は、空行、末尾の改行、データの出現順序が厳密に決まっており、一つでもずれると署名が通らなくなります。

```math
\def\の{\unicode[serif]{x306E}}
\begin{array}{l|l}
\hline 
コマンド & 目的 \\
\hline 
{\tt mktemp} & /tmpに一時ファイルを作成します。\\
& ファイル名は重複なくつけられ、\\
& アクセス権限も適切に設定されます。\\
\hdashline 
{\tt trap} & 特定\のタイミングでコマンドを実行します。\\
& プロセス終了時に一時ファイルを削除します。\\
\hline 
\end{array}
```

```bash: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
```

**パラメータの置換**

リクエストの時にユーザーが意識しないパラメータがあります。
そういったパラメータは、@で値を省略することで、スクリプト側に設定させます。

```math
\begin{array}{ll}
ユーザーが設定するパラメータ & {\tt \text{x-amz-date:@}} \\
実際に送信されるパラメータ & {\tt \text{x-amz-date:20200504T145432Z}}\\
& \\
\end{array}
```

```math
\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}
```

```bash: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では以下のように書きます。

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

```math
\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}
```

```bash: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で調べる方法

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

```python:暗号化を切って、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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233180/dd85b411-0a5f-e437-21f3-4580e2e0f4c6.png)

Wiresharkを見ると、送信しているデータが以下の通りだとわかります。

```text:テキスト
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に「[サービス エンドポイントとクォータの表](https://docs.aws.amazon.com/ja_jp/general/latest/gr/aws-service-information.html)」があります。
　エンドポイントのプロトコルがHTTP or HTTPSになっているものは通信できます。

### ブラウザの検証機能で調べる方法

SSLが必要なサービスでペイロードを見る場合は、javascript版のSDKを使います。

```javascript: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233180/c848cb3e-84d0-ee99-7dab-8c60e1b67821.png)

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