【Ruby】AWS署名バージョン4のAuthorization作成手順


##はじめに
AWS署名バージョン2が2019年6月24日に廃止となり、
以降 [バージョン4のみサポート](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/UsingAWSSDK.html#UsingAWSSDK-sig2-deprecation) すると発表があったのはご存知かと。

しかしAWSドキュメントを見てもバージョン2と比べどうも情報が少ないなと感じたり、
手順も見づらいなと思いながらも、試しに実装をしてみました。

今後対応が必要になる方のお役に立てればと、作成手順をここに残します。

※本記事はAuthorizationヘッダーでの認証方法です。
　その他のやり方として、クエリストリングでの方法もあります。
※今回はPUTを例として記載しています。
※署名やハッシュ文字列はテスト用で出力されたもののため、実際の利用はできません。


##手順
###タスク 1：署名バージョン 4 の正規リクエストを作成する
https://docs.aws.amazon.com/ja_jp/general/latest/gr/sigv4-create-canonical-request.html
まずは、例となっている正規リクエストの擬似コードを御覧ください。

```rb
CanonicalRequest =
  HTTPRequestMethod + '\n' +
  CanonicalURI + '\n' +
  CanonicalQueryString + '\n' +
  CanonicalHeaders + '\n' +
  SignedHeaders + '\n' +
  HexEncode(Hash(RequestPayload))
```
＊ `HTTPRequestMethod`
　→ メソッド。GETやPUT、POSTでOK。
<br>

＊ `CanonicalURI`
　→ URI。http\://example.amazonaws.com 等の接続先を指定。
<br>

＊ `CanonicalQueryString`
　→ クエリストリング。あれば[通常の表記](http://e-words.jp/w/%E3%82%AF%E3%82%A8%E3%83%AA%E6%96%87%E5%AD%97%E5%88%97.html)、なければ空文字列でOK。
<br>

＊ `CanonicalHeaders`
　→ ヘッダー情報。"Host"ヘッダーが必須。
　条件　１．キーは小文字
　　　　２．キーと値はコロンで区切られている
　　　　３．キーはソート(ASC)されている
　　　　４．ヘッダー間は改行で区切られている

■実装例

```rb
@headers['Host'] = "example.amazonaws.com"
@headers['X-Amz-Content-Sha256'] = "xyz" #16進数ダイジェスト（xyzは仮

def canonical_headers
  list = []
  @headers.each_pair do |k,v|
    k = k.downcase
    list << [k,v]
  end

  list.sort.map do |k,v|
    "#{k}:#{v}"
  end.join("\n")
end

# 結果
"host:example.amazonaws.com
x-amz-content-sha256:xyz"
```
<br>

＊ `SignedHeaders`
　→ ヘッダーリスト。キーのみ。
　条件　１．キーは小文字
　　　　２．キーはソート(ASC)されている　
　　　　３．キー感はセミコロンで区切られている

■実装例

```rb
@headers['Host'] = "example.amazonaws.com"
@headers['X-Amz-Content-Sha256'] = "xyz" #16進数ダイジェスト

def signed_headers
  @headers.keys.inject([]) do |list, k|
    k = k.downcase
    list << k
  end.sort.join(';')
end

# 結果
"host;x-amz-content-sha256"
```
<br>

＊ `HexEncode(Hash(RequestPayload))`
　→ PUT対象の16進数ダイジェスト（SHA256）。
　※ ファイルのPUTの場合はreadが必要です

■実装例

```rb
def hexdigest(value)
  digest = OpenSSL::Digest::SHA256.new
  if value.respond_to?(:read)
    digest.update(value.read)
  else
    digest.update(value)
  end
end
```

<br>
これらの各手順の全てを改行にて結合すると

■実装例

```rb
def canonical_request
  [
    "PUT",
    "http://example.amazonaws.com",
    "",
    canonical_headers + "\n",
    signed_headers,
    hexdigest("xyz"),
  ].join("\n")
end
```

■結果

```text
PUT
http://example.amazonaws.com

host:example.amazonaws.com
x-amz-content-sha256:xyz

host;x-amz-content-sha256
3608bca1e44ea6c4d268eb6db02260269892c0b42b86bbf1e77a6fa16c3c9282
```
となります。

仕上げに、これらの正規リクエストを16進数ダイジェストに変換します。

```rb

# 実行
hexdigest(canonical_request)
# 結果
a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2
```
<br>

###タスク 2：署名バージョン 4 の署名文字列を作成する
https://docs.aws.amazon.com/ja_jp/general/latest/gr/sigv4-create-string-to-sign.html
まずは、例となっている署名文字列の構造を御覧ください。

```rb
StringToSign =
    Algorithm + \n +
    RequestDateTime + \n +
    CredentialScope + \n +
    HashedCanonicalRequest
```
＊ `Algorithm `
　→ 使用するハッシュアルゴリズム。SHA256の場合は"AWS4-HMAC-SHA256"。
<br>

＊ `RequestDateTime `
　→ 日時。YYYYMMDD'T'HHMMSS'Z'形式である必要がある。
<br>

＊ `CredentialScope `
　→ 認証情報スコープ。日付、対象とするリージョン、リクエストしているサービス、
　　 および小文字の終了文字列 ("aws4_request") を含む文字列を作る。

```text
20190301/us-east-1/s3/aws4_request
```
<br>

＊ `HashedCanonicalRequest `
　→ 正規リクエストのハッシュ。タスク1の仕上げで作成したハッシュ（下記結果）を入れる。

```rb

# 実行
hexdigest(canonical_request)
# 結果
a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2
```
<br>
これらの各文字列の全てを改行にて結合すると

■実装例

```rb

def string_to_sign
  [
    "AWS4-HMAC-SHA256",
    "20190301T123600Z",
    "20190301/us-east-1/s3/aws4_request",
    hexdigest(canonical_request),
  ].join("\n")
end
```

■結果

```text
AWS4-HMAC-SHA256
20190301T123600Z
20190301/us-east-1/s3/aws4_request
a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2
```
となります。
<br>

###タスク 3：AWS 署名バージョン 4 の署名を計算する
https://docs.aws.amazon.com/ja_jp/general/latest/gr/sigv4-calculate-signature.html
まずは、例となっている署名キー取得の疑似コードを御覧ください。

※各パラメータはバイナリ形式である必要があります。

```rb
kSecret = your secret access key
kDate = HMAC("AWS4" + kSecret, Date)
kRegion = HMAC(kDate, Region)
kService = HMAC(kRegion, Service)
kSigning = HMAC(kService, "aws4_request")
```
＊ `HMAC("AWS4" + kSecret, Date) `
　→ kSecret = シークレットキー、Date = 日付（YYYYMMDD形式）。
<br>

＊ `HMAC(kDate, Region) `
　→ Region = 対象とするリージョン
<br>

＊ `HMAC(kRegion, Service) `
　→ Service = リクエストしているサービス
<br>

＊ `HMAC(kService, "aws4_request") `
　→ aws4_request = 固定文字列
<br>

※バイナリ形式は下記のような手段で変換を行う

```rb
def hmac(key, value)
  OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), key, value)
end
```
<br>
仕上げに、署名(signature)の計算を行います。
※こちらはバイナリ値16進数表現

■実装例

```rb
def signature
  kSecret = your secret access key
  kDate = hmac("AWS4" + kSecret, Date)
  kRegion = hmac(kDate, Region)
  kService = hmac(kRegion, Service)
  kSigning = HMAC(kService, "aws4_request")

  OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), kSigning, string_to_sign)
end
```

■結果

```text

5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7
```
となります。
<br>

###タスク ４：HTTP リクエストに署名を追加する
https://docs.aws.amazon.com/ja_jp/general/latest/gr/sigv4-add-signature-to-request.html
まずは、例となっているAuthorizationヘッダーの疑似コードを御覧ください。

```text
Authorization: algorithm Credential=access key ID/credential scope, SignedHeaders=SignedHeaders, Signature=signature
```
＊ `algorithm `
　→ 使用するハッシュアルゴリズム。SHA256の場合は"AWS4-HMAC-SHA256"。
<br>

＊ `access key ID/credential scope `
　→ access key ID = アクセスキー。
　→ credential scope = 認証情報スコープ。日付、対象とするリージョン、リクエストしているサービス、
　　 および小文字の終了文字列 ("aws4_request") を含む文字列を作る。
<br>

＊ `SignedHeaders `
　→ ヘッダーリスト。キーのみ。　※タスク１にて作成（signed_headerメソッド）
<br>

＊ `signature `
　→ 署名。　※タスク3にて作成（signatureメソッド）

```text
# 最終的なヘッダー例
Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7
```
<br>

##ソースコード
<details><summary>ソースの全容</summary><div>

```rb
@heds = {}
@heds['Host'] = "example.amazonaws.com"
@heds['X-Amz-Content-Sha256'] = @bodydigest
@bodydigest = hexdigest(File.open("test.text").read) #16進数ダイジェスト
@dt = "20190301T123600Z"
@region = "s-east-1"
@service = "s3"
@scope = [@dt[0,8], @region, @service, 'aws4_request'].join("/")
@secretkey = "aaaaaa"
@accesskey = "bbbbbb"

# Authorizationヘッダーの生成
def _get_authorization(access_key_id, secret_key)
  [
    "AWS4-HMAC-SHA256 Credential=#{@accesskey}/#{@scope}",
    "SignedHeaders=#{signed_headers}",
    "Signature=#{signature}",
  ].join(',')
end

#正規リクエストの取得
def canonical_request
  [
    "PUT",
    "http://example.amazonaws.com",
    "",
    canonical_headers + "\n",
    signed_headers,
    @bodydigest,
  ].join("\n")
end

# 署名文字列の取得
def string_to_sign
  [
    "AWS4-HMAC-SHA256",
    @dt,
    @scope,
    hexdigest(canonical_request),
  ].join("\n")
end

# 署名の生成
def signature
  kSecret = @secretkey
  kDate = hmac("AWS4" + kSecret, @dt[0.8])
  kRegion = hmac(kDate, @region)
  kService = hmac(kRegion, @service)
  kSigning = hmac(kService, "aws4_request")

  OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), kSigning, string_to_sign)
end

# リクエストヘッダー
def canonical_headers
  list = []
  @heds.each_pair do |k,v|
    k = k.downcase
    list << [k,v]
  end

  list.sort.map do |k,v|
    "#{k}:#{v}"
  end.join("\n")
end

# ヘッダーキーリスト
def signed_headers
  @heds.keys.inject([]) do |list, k|
    k = k.downcase
    list << k
  end.sort.join(';')
end

# 16進数ダイジェスト（SHA256）
def hexdigest(value)
  digest = OpenSSL::Digest::SHA256.new
  if value.respond_to?(:read)
    digest.update(value.read)
  else
    digest.update(value)
  end
end

# hmac変換
def hmac(key, value)
  OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), key, value)
end
```
</div></details>
##さいごに
よくよく読み解いてみると難しくない気がしなくもないのですが、
v2のドキュメントに比べ内容が薄く分かりづらくなっているような気がしなくもないですね・・・
（私だけでしょうか？）

これらがお困りの方のお役に立てたらこれ幸いです。