API ConnectからAmazon SQSのAPIを公開してみた

#はじめに
[Amazon Simple Queue Service (SQS)](https://aws.amazon.com/jp/sqs/) は、完全マネージド型のメッセージキューイングサービスです。
このキューに対して、メッセージを送信等を実施するための[API](https://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-working-with-apis.html)が提供されています。
このAPIのリクエストには、[HMAC-SHAによる認証プロセス](https://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-api-request-authentication.html)が必要となります。

この記事では、API Connectでのアセンブリー処理（GatewayScript開発）でHMAC-SHA認証プロセスを実装して、APIを公開する方法を紹介しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/a2ee63ca-e7a3-84fa-4a39-889022cefe0c.png)


#事前準備

**1. Amazon SQSでのキュー作成**

Amazon SQSでキューを作成します。
キューの名前は、「test-queue」としています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/9dd96221-53e0-5e56-a267-066a5f31d84f.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/386b8591-f4b3-6a9f-f54d-c410c0e99fe2.png)


**2. Amazon SQS用のアクセスキー発行**

AWSにAPI接続するためのアクセスキーを発行します。
コンソールの「マイセキュリティ資格情報」から作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/03417213-1c00-97d8-bc28-56276cebb0e9.png)




**3. IBM Cloudでのインスタンス登録**

今回は、[IBM CloudのAPI Connectサービス](https://cloud.ibm.com/catalog/services/api-connect)のライトプランで検証環境を用意して検証します。サービス登録できたら、API Connectサービスにアクセスします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/29b29f41-3422-d945-2b0c-246cbc11e9f3.png)

# API Connectでのアセンブリー実装

新規APIを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/d3c4cd14-40d3-a8d4-1155-cac0a272842c.png)

API名は「test-api」とします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/63f830f5-7eff-9406-3aa9-50faceb6a22a.png)

パスは「/test」を登録します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/e247d8e4-cb59-2eed-4fb7-ad287d611906.png)


アセンブルでは、HMAC-SHA認証プロセスを実施するための「GatewayScriptポリシー」と、Amazon SQSへリクエストするための「Invokeポリシー」を設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/b21c79f0-d350-7eca-5db8-0d9ef2aad85c.png)


GatewayScriptでの処理内容については、以下のガイドを参考にしています。
PythonでのHMAC-SHAのサンプルコードがありますので、こちらをGatewayScript（JavaScript）で実装します。

[Examples of the complete Signature Version 4 signing process (Python) ]
(https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html)

GatewayScriptの内容は以下になります。ここでは、テストのため、キューへ送信するメッセージは、「This is a test」と固定しています。


```javascript:サンプルコード
var crypto = require("crypto");

// ************* REQUEST VALUES *************
var method = "GET";
var service = "sqs";
var host = "sqs.us-east-2.amazonaws.com";
var region = "us-east-2";
var endpoint = "https://sqs.us-east-2.amazonaws.com/15141112****/test-queue";
var request_parameters = "Action=SendMessage&MessageBody=This is a test&Version=2012-11-05";

var access_key = "AKIASGQGOPA27WSP****";
var secret_key = "SKpkJ1GfxZHJhuYtTkbSy7LtE3Wg8ILCOQZU****";

function getSignatureKey(key, dateStamp, regionName, serviceName) {
  var tempkey = Buffer.from("AWS4" + key, 'utf8');
  var kDate = crypto.createHmac("sha256", tempkey).update(dateStamp).digest("");
  var kRegion = crypto.createHmac("sha256", kDate).update(regionName).digest("");
  var kService = crypto.createHmac("sha256", kRegion).update(serviceName).digest("");
  var kSigning = crypto.createHmac("sha256", kService).update("aws4_request").digest("");
  return kSigning
}


//# Create a date for headers and the credential string
var now = new Date();
var yyyy = now.getUTCFullYear();
var MM = ('0' + (now.getUTCMonth() + 1)).slice(-2);
var dd = ('0' + now.getUTCDate()).slice(-2);
var hh = ('0' + now.getUTCHours()).slice(-2);
var mm = ('0' + now.getUTCMinutes()).slice(-2);
var ss = ('0' + now.getUTCSeconds()).slice(-2);

var amzdate = yyyy + MM + dd + 'T' + hh + mm + ss + 'Z';
var datestamp = yyyy + MM + dd;


//# ************* TASK 1: CREATE A CANONICAL REQUEST *************
//# http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
//# Step 1 is to define the verb (GET, POST, etc.)--already done.

//# Step 2: Create canonical URI--the part of the URI from domain to query
//# string (use '/' if no path)
var canonical_uri = "/15141112****/test-queue";

//# Step 3: Create the canonical query string. In this example (a GET request),
//# request parameters are in the query string. Query string values must
//# be URL-encoded (space=%20). The parameters must be sorted by name.
//# For this example, the query string is pre-formatted in the request_parameters variable.
var canonical_querystring = encodeURI(request_parameters) ;

//# Step 4: Create the canonical headers and signed headers. Header names
//# must be trimmed and lowercase, and sorted in code point order from
//# low to high. 
var canonical_headers = `host:${host}\nx-amz-date:${amzdate}\n` ;

//# Step 5: Create the list of signed headers. This lists the headers
//# in the canonical_headers list, delimited with ";" and in alpha order.
//# Note: The request can include any headers; canonical_headers and
//# signed_headers lists those that you want to be included in the
//# hash of the request. "Host" and "x-amz-date" are always required.
var signed_headers = "host;x-amz-date";

//# Step 6: Create payload hash (hash of the request body content). For GET
//# requests, the payload is an empty string ("").
var payload_hash = crypto.createHash("sha256").update("").digest("hex");

//# Step 7: Combine elements to create canonical request
var canonical_request =`${method}\n${canonical_uri}\n${canonical_querystring}\n${canonical_headers}\n${signed_headers}\n${payload_hash}`;

//# ************* TASK 2: CREATE THE STRING TO SIGN*************
//# Match the algorithm to the hashing algorithm you use, either SHA-1 or
//# SHA-256 (recommended)
var algorithm = "AWS4-HMAC-SHA256";
var credential_scope = datestamp + "/" + region + "/" + service + "/" + "aws4_request";
var string_to_sign =`${algorithm}\n${amzdate}\n${credential_scope}\n${crypto.createHash("sha256").update(canonical_request).digest("hex")}`;

//# ************* TASK 3: CALCULATE THE SIGNATURE *************
//# Create the signing key using the function defined above.
var signing_key = getSignatureKey(secret_key, datestamp, region, service);

//# Sign the string_to_sign using the signing_key
var signature = crypto.createHmac("sha256", signing_key).update(string_to_sign).digest("hex");


//# ************* TASK 4: ADD SIGNING INFORMATION TO THE REQUEST *************
//# The signing information can be either in a query string value or in
//# a header named Authorization. This code shows how to use a header.
//# Create authorization header and add to request headers
var authorization_header =  algorithm +  " " + "Credential=" + access_key + "/" + credential_scope + ", " +  "SignedHeaders=" + signed_headers + ", " + "Signature=" + signature; 

var request_url = endpoint + "?" + canonical_querystring;

var headers = {
  "X-Amz-Date": amzdate,
  "Authorization": authorization_header
};

// ************* SET VALUE FOR INVOKE POLICY *************
apim.setvariable('message.headers',headers, 'set');
apim.setvariable('target-url',request_url, 'set');
```

Invokeポリシーでは、GatewayScriptで設定した環境変数の「target-url」を使用してAmazon SQSにリクエストしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/6336e18b-e0bf-9bd9-fc3d-b8cdbe668061.png)


この内容でAPIを公開します。


# API呼び出しテスト

CURLからAPI ConnectのAPIを呼び出してみます。
結果は、200 OKとなり、送信が正常環境していることを確認できます。メッセージのIDは、「dc675b0a-58f1-4612-8941-5fbca8dcc4a2」となっています。

```bash
$  curl -i --request GET \
>    --url https://api.eu.apiconnect.ibmcloud.com/***/sb/test-api/test \
>    --header 'accept: application/json'
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   312    0   312    0     0    312      0 --:--:--  0:00:01 --:--:--   193
HTTP/1.1 200 OK
Date: Fri, 28 May 2021 02:40:06 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
X-Backside-Transport: OK OK
X-Global-Transaction-ID: 33bfcbfe60b058060014de2a
x-amzn-RequestId: f1acf8e5-880e-5084-9521-a1cfd248af14
Access-Control-Expose-Headers: APIm-Debug-Trans-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-Global-Transaction-ID
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET
CF-Cache-Status: DYNAMIC
cf-request-id: 0a5270ec7c00005f5e88020000000001
Expect-CT: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
Server: cloudflare
CF-RAY: 65641dc0cf9d5f5e-NRT

{"SendMessageResponse":{"ResponseMetadata":{"RequestId":"f1acf8e5-880e-5084-9521-a1cfd248af14"},"SendMessageResult":{"MD5OfMessageAttributes":null,"MD5OfMessageBody":"ce114e4501d2f4e2dcea3e17b546f339","MD5OfMessageSystemAttributes":null,"MessageId":"dc675b0a-58f1-4612-8941-5fbca8dcc4a2","SequenceNumber":null}}}

```

Amazon SQSのキューを確認してみると、上記のメッセージが送信されていることを確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/bc62cf26-630e-778c-b910-4082a1792724.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403443/a4ae9b7e-74bc-464c-2f6f-a5cac0fd109f.png)

# GatewayScript開発におけるTips

アセンブリー開発において、以下のような対応がありましたので、検証メモを残しておきます。

**1. HMAC-SHA署名が合致しない場合**

GatewayScriptのHMAC-SHA認証プロセスの開発において、不備があると、署名が合致しないというエラーとなってしまいます。
このエラーでは、署名に使用する途中計算のデータ（GatewaySriptでは「canonial_request」と、「string_to_sign」）がこうなるはずという情報が送られています。この情報をもとに認証プロセスロジックを見直して不備を修正しました。

```bash
BODY: <?xml version="1.0"?><ErrorResponse xmlns="http://queue.amazonaws.com/doc/2012-11-05/"><Error><Type>Sender</Type><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.

The Canonical String for this request should have been
'GET
/151411128373/test-queue
Action=SendMessage&amp;MessageBody=1&amp;Version=2012-11-05
host:sqs.us-east-2.amazonaws.com
x-amz-date:20210528T041000Z

host;x-amz-date
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'

The String-to-Sign should have been
'AWS4-HMAC-SHA256
20210528T041000Z
20210528/us-east-2/sqs/aws4_request
9b0fd7f44c8a5158e80108f29b0fa3edec9b282d04952fc931af437625b89ba5'
</Message><Detail/></Error><RequestId>116991fe-619b-5a82-8095-c8d40eb7bc75</RequestId></ErrorResponse>
```


**2. GatewayScriptポリシーにおける改行文字（\n）の対応**

コード内では、「canonical_request」等で特定の変数に複数行のデータを設定する内容になっています。
しかし、Pythonでのサンプルコードのように（`var canonical_request = method + "\n" + ・・・`）と設定してしまうと、以下のエラーとなってしまいます。

```JSON
{
  "httpCode": "500",
  "httpMessage": "Internal Server Error",
  "moreInformation": "Invalid or unexpected token"
}
```

今回は、改行を含む変数設定は（``var canonical_request = `${method}\n・・・` ``）という形で回避しています。



**3. cryptoモジュールのcreateHmacメソッドのキー情報について**

コード内で、getSignatureKey関数でcrypto.createHmacにキー情報を渡して処理していますが、そのまま渡してしまうと以下のようなエラーとなります。

```bash
{
  "httpCode": "500",
  "httpMessage": "Internal Server Error",
  "moreInformation": "*Named shared secret key 'SKpkJ1GfxZHJhuYtTkbSy7LtE3Wg8ILCOQZU****' not found*"
}
```

このエラーについては、以下のサイトで問合せされていて、キー情報は、UTF-8ストリングのバッファーである必要があります。

[AES Encryption Decryption in API Connect Gateway Script JSON DatapowerAES Encryption Decryption in API Connect Gateway Script JSON Datapower](https://stackoverflow.com/questions/52596435/aes-encryption-decryption-in-api-connect-gateway-script-json-datapower)