Azure StorageでのRest APIの署名のつくりかた（with パケットキャプチャ）

本記事は、Azure StorageでのRest APIの署名作成についての調査結果をまとめたものになります。

# 前提

* ストレージアカウント等は、本記事中では以下の識別子を使用しています。

| 項目 | 本記事での識別子 |  
|:----|:----|
| ストレージアカウント | mystorageaccount |
| コンテナ名          | mycontainer |
| PUT/GET/DELETEするBlob名 | sample.txt |

* オペレーションごとの、リクエストヘッダおよび`StringToSign`に必要なヘッダは以下の通りです。
（〇：必須　-：不要）

| Request Header | Put Blob |  Get Blob | Detele Blob | List Blobs | 
|:---------------|:--------:|:---------:|:----------:|:-----------:|
| Content-Length | 〇 | - | - | - |
| x-ms-date (or Date)| 〇 | 〇 | 〇 | 〇 |
| x-ms-version  | 〇 | 〇 | 〇 | 〇 |
| x-ms-blob-type | 〇 | - | - | - |

* リクエストヘッダには`x-ms-date`と`Date`のうち少なくとも一方が存在する必要があります。リクエストヘッダにいずれか一方のみが存在する場合、`StringToSign`にはリクエストヘッダに存在する方のみを記述し、存在しない方は記述しません。リクエストヘッダに両方が同時に存在する場合、`StringToSign`には`x-ms-date`のみ記述します。

| リクエストヘッダ | StringToSignへの記述 | 
|:---------------|:--------|
| x-ms-dateのみ存在 |x-ms-dateのみ記述する |
| Dateのみ存在| Dateのみ記述する |
| 上2つが同時に存在  | x-ms-dateのみ記述する |

* 本記事に出てくる「アクセスキー」は、Azureポータルの以下画面の「キー」で確認できます。
（[ホーム] > [ストレージアカウント]でストレージアカウントを選択 > [アクセスキー]）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51871/d43c9c91-fe2d-2b70-835c-1a2531b1eb8c.png)

* アクセスキーは、Base64エンコードされた文字列です。本記事では、例示のために`VGhpcyBpcyBzYW1wbGUgb2YgQXp1cmUgU3RvcmFnZSBBY2Nlc3MgS2V5IHN0cmluZyBCYXNlNjQgRW5jb2RlZA==`というキーを使用していますが、これは実在しないキーです。くれぐれも本物のアクセスキーをソースコードにハードコーディングしないようにご注意下さい。

* 本記事のサンプルコードはC#で記述していますが、C#特有の文法は使用していないため、C#未経験者でも内容が直観的に理解できるコードになっています。念のため説明しておきますと、`string s = "abc";`はString型（文字列型）の変数`s`を宣言し、その値を文字列`"abc"`で初期化するという意味です。また、`byte[] stringToSign_bytes;`は、バイト配列型の変数`stringToSign_bytes`を宣言するという意味です。

# 署名のつくりかた

おおまかな流れとしては、以下となります。

1. `StringToSign`を作成する。
2. アクセスキー（`AccessKey`）をキーとして、`StringToSign`のHMAC-SHA256ハッシュ値を算出する。
3. 2で算出したHMAC-SHA256ハッシュ値をBase64エンコードした文字列を、リクエストヘッダの`Authoraization`に設定する。

AWS S3のSignature V4の手順と比べると、Azure Storaogeの手順はかなりシンプルで助かります。

以下では、Put/Get/List/Deleteの各オペレーションでの署名の作成手順について解説します。

## Put Blob

`StringToSign`を作成します。

* `StringToSign`の末尾（`CanonicalResource`の末尾）に改行文字はつけません。
* HTTPヘッダと`StringToSign`において、リクエストヘッダ `Content-Length`, `x-ms-blob-type`、`x-ms-date`または`Date`、`x-ms-version`が必須です。
* `CanonicalHeaders`のキー名は小文字です。
* `CanonicalHeaders`の並び順は、キー名小文字での辞書順（昇順）です。

```csharp:StringToSign
string StringToSign =
    // GeneralHeaders
    "PUT\n" +  // Verb
    "\n" +     // Content-Encoding
    "\n" +     // Content-Language
    "4\n" +    // Content-Length (Required)
    "\n" +     // Content-MD5
    "\n" +     // Content-Type
    "\n" +     // Date
    "\n" +     // If-Modified-Since
    "\n" +     // If-Match
    "\n" +     // If-Non-Match
    "\n" +     // If-Unmodified-Since
    "\n" +     // Range
    // CanonicalHeaders
    "x-ms-blob-type:BlockBlob\n" +
    "x-ms-date:Sun, 08 Mar 2020 03:39:02 GMT\n" +
    "x-ms-version:2017-07-29\n" +
    // CanonicalResource
    "/mystorageaccount/mycontainer/sample.txt";
```

作成した`StringToSign`と、アクセスキーをインプットとしてして、HMAC-SHA256のハッシュ値を算出します。
算出したハッシュ値（バイト配列）を、Base64エンコードして文字列にします。

（以下のコード例では、例示のためアクセスキーをソースコードにハードコーディングしていますが、これは実在しないキーです。くれぐれも本物のアクセスキーをソースコードに記述しないよう、ご注意下さい）

```csharp:Signature
    string AccessKey = "VGhpcyBpcyBzYW1wbGUgb2YgQXp1cmUgU3RvcmFnZSBBY2Nlc3MgS2V5IHN0cmluZyBCYXNlNjQgRW5jb2RlZA==";
    byte[] accessKey_bytes = Base64.DecodeFromBase64String(AccessKey);

    byte[] stringToSign_bytes = UTF8.GetBytes(StringToSign);
    byte[] signature_bytes = HMAC_SHA256.ComputeHash(stringToSign_bytes, accessKey_bytes);
    string signature_base64 = Base64.EncodeToBase64String(signature_bytes);

    string Signature = signature_base64;
    // ex) Signature is "QmFzZTY044OH44Kz44O844OJ44GX44G+44GX44GfPw=="
```

完成した`SharedKey`を、HTTP Requestの`Authorization`ヘッダにセットします。

```csharp:SharedKey
    string SharedKey = "SharedKey mystorageaccount:" + Signature;
    // ex) SharedKey is "SharedKey mystorageaccount:QmFzZTY044OH44Kz44O844OJ44GX44G+44GX44GfPw=="
    httpRequestHeaders.Authorization = new AuthorizationHeaderValue(SharedKey);
```

最終的に、Authorizationヘッダは以下のようになります。

```http:Authorizationヘッダ（例）
Authorization: SharedKey mystorageaccount:QmFzZTY044OH44Kz44O844OJ44GX44G+44GX44GfPw==
```

## Get Blob

* HTTPヘッダと`StringToSign`において、リクエストヘッダ `x-ms-date`（または`Date`）、`x-ms-version`が必須です。

```csharp:StringToSign
string StringToSign =
    // GeneralHeaders
    "GET\n" +  // Verb
    "\n" +     // Content-Encoding
    "\n" +     // Content-Language
    "\n" +     // Content-Length
    "\n" +     // Content-MD5
    "\n" +     // Content-Type
    "\n" +     // Date
    "\n" +     // If-Modified-Since
    "\n" +     // If-Match
    "\n" +     // If-Non-Match
    "\n" +     // If-Unmodified-Since
    "\n" +     // Range
    // CanonicalHeaders
    "x-ms-date:Sun, 08 Mar 2020 03:39:02 GMT\n" +
    "x-ms-version:2017-07-29\n" +
    // CanonicalResource
    "/mystorageaccount/mycontainer/sample.txt";
```

以降の手順はPut Blobと同じです。

## List Blobs

* `CanoncalResource`末尾にクエリー文字列がつきます。
* `StringToSign`の末尾（`restype:container`の末尾）に改行文字はつきません。
* HTTPヘッダと`StringToSign`において、リクエストヘッダ `x-ms-date`または`Date`、`x-ms-version`が必須です。
* `CanonicalResource`に、クエリー文字列（`comp=list&restype=container`）を`key:value`の形式でリスト化した文字列が必須です。
* `CanonicalResource`のクエリーの名前（`comp`、`restype`）は小文字です。
* `CanonicalResource`のクエリーの並び順は、キー名小文字での辞書順（昇順）です。


```csharp:StringToSign
string StringToSign =
    // GeneralHeaders
    "GET\n" +  // Verb
    "\n" +     // Content-Encoding
    "\n" +     // Content-Language
    "\n" +     // Content-Length
    "\n" +     // Content-MD5
    "\n" +     // Content-Type
    "\n" +     // Date
    "\n" +     // If-Modified-Since
    "\n" +     // If-Match
    "\n" +     // If-Non-Match
    "\n" +     // If-Unmodified-Since
    "\n" +     // Range
    // CanonicalHeaders
    "x-ms-date:Sun, 08 Mar 2020 03:39:02 GMT\n" +
    "x-ms-version:2017-07-29\n" +
    // CanonicalResource
    "/mystorageaccount/mycontainer\n" +
    "comp:list\n" +
    "restype:container";
```

以降の手順はPut Blobと同様です。

## Delete Blob

* HTTPヘッダと`StringToSign`において、リクエストヘッダ `x-ms-date`（または`Date`）、`x-ms-version`が必須です。

```csharp:StringtoSign
string StringToSign =
    "DELETE\n" +  // Verb
    // GeneralHeaders
    "\n" +     // Content-Encoding
    "\n" +     // Content-Language
    "\n" +     // Content-Length
    "\n" +     // Content-MD5
    "\n" +     // Content-Type
    "\n" +     // Date
    "\n" +     // If-Modified-Since
    "\n" +     // If-Match
    "\n" +     // If-Non-Match
    "\n" +     // If-Unmodified-Since
    "\n" +     // Range
    // CanonicalHeaders
    "x-ms-date:Sun, 08 Mar 2020 03:39:02 GMT\n" +
    "x-ms-version:2017-07-29\n" +
    // CanonicalResource
    "/mystorageaccount/mycontainer/sample.txt";
```

以降の手順はPut Blobと同じです。

# 【参考】実際のパケットキャプチャ

参考情報として、各操作でのパケットキャプチャを乗せておきます。

## Put Blob

```http:Request
PUT /mycontainer/sample.txt HTTP/1.1
Authorization: SharedKey mystorageaccount:QmFzZTY044OH44Kz44O844OJ44GX44G+44GX44GfPw==
x-ms-date: Sun, 08 Mar 2020 04:46:28 GMT
x-ms-version: 2017-07-29
x-ms-blob-type: BlockBlob
Host: mystorageaccount.blob.core.windows.net
Content-Length: 4
Expect: 100-continue
Connection: Keep-Alive

hoge
```

HTTPレスポンスの`Content-MD5`には、リクエストのメッセージボディ（`hoge`）のMD5ハッシュ値をBase64エンコードした文字列（`6nA+eqHv2gBk6qUH2eirfg==`）がセットされます。

```http:Response
HTTP/1.1 201 Created
Content-Length: 0
Content-MD5: 6nA+eqHv2gBk6qUH2eirfg==
Last-Modified: Sun, 08 Mar 2020 04:46:27 GMT
ETag: "0x8D7C31BA9E7D293"
Server: Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0
x-ms-request-id: d738ae8f-101e-0058-4404-f51de0000000
x-ms-version: 2017-07-29
x-ms-request-server-encrypted: true
Date: Sun, 08 Mar 2020 04:46:26 GMT
```

## Get Blob

```http:Request
GET /mycontainer/sample.txt HTTP/1.1
x-ms-date: Sun, 08 Mar 2020 04:46:28 GMT
x-ms-version: 2017-07-29
Authorization: SharedKey mystorageaccount:QmFzZTY044OH44Kz44O844OJ44GX44G+44GX44GfPw==
Host: mystorageaccount.blob.core.windows.net
```

HTTPレスポンスの`Content-MD5`には、レスポンスのメッセージボディ（`hoge`）のMD5ハッシュ値をBase 64エンコードした文字列（`6nA+eqHv2gBk6qUH2eirfg==`）がセットされます。

```http:Response
HTTP/1.1 200 OK
Content-Length: 4
Content-Type: application/octet-stream
Content-MD5: 6nA+eqHv2gBk6qUH2eirfg==
Last-Modified: Sun, 08 Mar 2020 04:46:27 GMT
Accept-Ranges: bytes
ETag: "0x8D7C31BA9E7D293"
Server: Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0
x-ms-request-id: d738aeb5-101e-0058-6304-f51de0000000
x-ms-version: 2017-07-29
x-ms-lease-status: unlocked
x-ms-lease-state: available
x-ms-blob-type: BlockBlob
x-ms-server-encrypted: true
Date: Sun, 08 Mar 2020 04:46:27 GMT

hoge
```

## List Blobs

```http:Request
GET /mycontainer?restype=container&comp=list HTTP/1.1
x-ms-date: Sun, 08 Mar 2020 04:46:28 GMT
x-ms-version: 2017-07-29
Authorization: SharedKey mystorageaccount:QmFzZTY044OH44Kz44O844OJ44GX44G+44GX44GfPw==
Host: mystorageaccount.blob.core.windows.net
```

```http:Response(ResponseHeader)
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Type: application/xml
Server: Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0
x-ms-request-id: d738aee2-101e-0058-0b04-f51de0000000
x-ms-version: 2017-07-29
Date: Sun, 08 Mar 2020 04:46:27 GMT
```

```xml:Response(MessageBody)
<?xml version="1.0" encoding="utf-8"?>
<EnumerationResults ServiceEndpoint="http://mystorageaccount.blob.core.windows.net/" ContainerName="mycontainer">
  <Blobs>
    <Blob>
      <Name>sample.txt</Name>
      <Properties>
        <Last-Modified>Sun, 08 Mar 2020 04:46:27 GMT</Last-Modified>
        <Etag>0x8D7C31BA9E7D293</Etag>
        <Content-Length>4</Content-Length>
        <Content-Type>application/octet-stream</Content-Type>
        <Content-Encoding />
        <Content-Language />
        <Content-MD5>6nA+eqHv2gBk6qUH2eirfg==</Content-MD5>
        <Cache-Control />
        <Content-Disposition />
        <BlobType>BlockBlob</BlobType>
        <AccessTier>Hot</AccessTier>
        <AccessTierInferred>true</AccessTierInferred>
        <LeaseStatus>unlocked</LeaseStatus>
        <LeaseState>available</LeaseState>
        <ServerEncrypted>true</ServerEncrypted>
      </Properties>
    </Blob>
  </Blobs>
  <NextMarker />
</EnumerationResults>
```
## Delete Blob
```http:Request
DELETE /mycontainer/sample.txt HTTP/1.1
x-ms-date: Sun, 08 Mar 2020 04:46:28 GMT
x-ms-version: 2017-07-29
Authorization: SharedKey mystorageaccount:QmFzZTY044OH44Kz44O844OJ44GX44G+44GX44GfPw==
Host: mystorageaccount.blob.core.windows.net
Content-Length: 0
```

```http:Response
HTTP/1.1 202 Accepted
Content-Length: 0
Server: Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0
x-ms-request-id: d738af03-101e-0058-2904-f51de0000000
x-ms-version: 2017-07-29
x-ms-delete-type-permanent: true
Date: Sun, 08 Mar 2020 04:46:27 GMT
```


# 参考
[Blob service REST API (公式)](https://docs.microsoft.com/en-us/rest/api/storageservices/blob-service-rest-api)
[Microsoft Azure～REST API 操作やってみた～](https://u-618.org/microsoft-azure-rest-api-blob/)　…　 PHPでの実装例です。とても分かりやすくて、何度も参考にさせていただきました。