AWS Signature v4の署名をJavaで作る「完全な署名バージョン4」for Java

AWSのSDKが使えないJava7を前提に書きました。

## AWS Sig V4とは
AWSのAPIを利用するときに、クライアント側からI AMユーザであることを示す為に送付する署名のことです。前はバージョン2とかいろいろあったみたいですが、今はV4がメインで使われているようです。
SigV4はAWS公式のルールにしたがって作り、HTTPリクエストのAuthorizationヘッダーの値に載せて送ります。それを受け取ったAWS側では、同じロジックに従って署名を作り、送られてきた署名と一致するかを確かめることで認証を行います。

API GatewayのI AMユーザを使ったアクセス制限を例に、今回は自前で実装してみます。
## SigV4の使い方
POSTであればリクエストヘッダーにAuthorization、X-Amz-Dateの二つを載せて送ります。GETであればクエリパラメータ文字列でもいけるようです。その2つの文字列を作るのにはルールがあり、特にAuthorizationのほうは面倒なルールに従って文字列を生成する必要があります。
本来であれば、SDKを使えば簡単にできるところですが、どんなことをしているのか中身が気になったので自前で書いてみました。

## 自前で署名するプログラム
https://docs.aws.amazon.com/ja_jp/general/latest/gr/signature-version-4.html
公式ドキュメントを参考にして署名をします。
手順は4つです。

1. タスク 1: 正規リクエストを作成する
2. タスク 2: 署名文字列を作成する
3. タスク 3: 署名を計算する
4. タスク 4: HTTP リクエストに署名を追加する

どの文字列を作るのにも改行の場所とか、暗号化する対象とか、色々気を使いながらルールに従って署名をします。簡単そうに見えていましたが、1文字でも違うと認証はじかれるので、結構ハマりました。

実際に書いてみたプログラムは以下です。
以下の関数`doShomei()`はパラメータは全部べた書きなので適宜パラメータ化したりなんなりしたほうが良いですね。


```java
package sample.XXXX;

import java.net.InetSocketAddress;
import java.net.Proxy;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.Test;

public class Shomei {
	private static final Log LOG = LogFactory.getLog(Shomei.class);

	public void doShomei() {
		/*
		 * タスク1. 署名バージョン4の正規リクエストを作成する.
		 *  https://docs.aws.amazon.com/ja_jp/general/latest/gr/sigv4-create-canonical-request.html
		 *  CanonicalRequest =
		 *  	HTTPRequestMethod + '\n' +
		 *  	CanonicalURI + '\n' +
		 *  	CanonicalQueryString + '\n' +
		 *  	CanonicalHeaders + '\n' +
		 *  	SignedHeaders + '\n' +
		 *  	HexEncode(Hash(RequestPayload))
		 */
		SimpleDateFormat xAmzDateFormatter = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
		xAmzDateFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
		String xAmzDate = xAmzDateFormatter.format(new Date()).trim();

		String host = "XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com";
		String httpRequestMethod = "POST";
		String canonicalUri = "/hoge/fuga";
		String signedHeaders = "content-type;host;x-amz-date";
		String canonicalQueryString = "";
		String contentType = "application/json";
		String canonicalHeaders = "content-type:" + contentType + "\nhost:" + host
				+ "\nx-amz-date:"
				+ xAmzDate;
		String requestPayload = "";
		String canonicalRequest =
				httpRequestMethod + '\n' +
						canonicalUri + '\n' +
						canonicalQueryString + '\n' +
						canonicalHeaders + '\n' + '\n' +
						signedHeaders + '\n' +
						DigestUtils.sha256Hex(requestPayload);

		LOG.debug(canonicalRequest);
		String hashedCanonicalRequest = DigestUtils.sha256Hex(canonicalRequest);

		/*
		 * タスク2. 署名バージョン4の署名文字列を作成する.
		 * https://docs.aws.amazon.com/ja_jp/general/latest/gr/sigv4-create-string-to-sign.html
		 * StringToSign =
		 *	Algorithm + \n +
		 *	RequestDateTime + \n +
		 * 	CredentialScope + \n +
		 * 	HashedCanonicalRequest
		 */
		String date = "20210314";
		String region = "ap-northeast-1";
		String service = "execute-api";
		String endStr = "aws4_request";
		String algorithm = "AWS4-HMAC-SHA256";
		String credentialScope = date + "/" + region + "/" + service + "/" + endStr;

		String stringToSign = algorithm + "\n"
				+ xAmzDate + "\n"
				+ credentialScope + "\n"
				+ hashedCanonicalRequest;

		/*
		 * タスク3. 署名バージョン4の署名を計算する.
		 * https://docs.aws.amazon.com/ja_jp/general/latest/gr/sigv4-calculate-signature.html
		 * 
		 * kSecret = your secret access key
		 * kDate = HMAC("AWS4" + kSecret, Date)
		 * kRegion = HMAC(kDate, Region)
		 * kService = HMAC(kRegion, Service)
		 * kSigning = HMAC(kService, "aws4_request")
		 */
		String accessKey = "API Gatewayに実行権限を持つIAMユーザーのアクセスキー";
		String secretKey = "API Gatewayに実行権限を持つIAMユーザーシークレットキー";
		try {
			/*
			 * Javaを使用して署名キーを取得
			 * https://docs.aws.amazon.com/ja_jp/general/latest/gr/signature-v4-examples.html#signature-v4-examples-java
			 */
			byte[] key = getSignatureKey(secretKey, date, region, service);
			String signature = String.valueOf(Hex.encodeHex(hmacSHA256(stringToSign, key)));
			LOG.debug(signature);

			/*
			 * タスク4. HTTPリクエストに署名を追加する
			 * https://docs.aws.amazon.com/ja_jp/general/latest/gr/sigv4-add-signature-to-request.html
			 * 
			 * Authorization: algorithm Credential=access key ID/credential scope, SignedHeaders=SignedHeaders, Signature=signature
			 */
			String authorization = algorithm + " Credential=" + accessKey + "/" + credentialScope
					+ ", SignedHeaders=" + signedHeaders + ", Signature=" + signature;
			LOG.debug(authorization);
			LOG.debug(xAmzDate);

		} catch (Exception e) {
			e.printStackTrace();
		}

	}

	public static byte[] hmacSHA256(String data, byte[] key) throws Exception {
		String algorithm = "HmacSHA256";
		Mac mac = Mac.getInstance(algorithm);
		mac.init(new SecretKeySpec(key, algorithm));
		return mac.doFinal(data.getBytes("UTF-8"));
	}

	public static byte[] getSignatureKey(String key, String dateStamp, String regionName, String serviceName)
			throws Exception {
		byte[] kSecret = ("AWS4" + key).getBytes("UTF-8");
		byte[] kDate = hmacSHA256(dateStamp, kSecret);
		byte[] kRegion = hmacSHA256(regionName, kDate);
		byte[] kService = hmacSHA256(serviceName, kRegion);
		byte[] kSigning = hmacSHA256("aws4_request", kService);
		return kSigning;
	}

}

```

### 結論
AWSのSDK使える人は使ったほうが早いと思いますし、公式もそれを推奨しています。メンテを考えるとSDK使ったほうが絶対によいです。ただ自前で書いても（ハマらなければ）大した時間かからないので、何らかの理由で自前でやる方はご参考にどうぞ。