AWS 署名 version4 の C 言語実装を読んでみた

以前、紹介した aws_dynamo に C 言語による aws 署名 version4 の実装が含まれているため、ソースコードリーディングしてみました。

DynamoDB を aws_dynamo から触ってみた

公式ドキュメントにあるとおり aws 署名 version4 は 3 つの手順からなります。

  1. 正規リクエストの作成
  2. 署名文字列の作成
  3. 署名の作成

正規リクエストの作成

タスク 1: 署名バージョン 4 の正規リクエストを作成する は以下の関数で実装されています。

公式ドキュメントにある擬似コードをそれぞれ対応する箇所にコメントとして入れています。

C 言語ではハッシュの計算には OpenSSH ライブラリを使用することが多いです。

char *aws_sigv4_create_hashed_canonical_request(const char *http_request_method,
					 const char *canonical_uri,
					 const char *canonical_query_string,
					 const char *canonical_headers,
					 const char *signed_headers,
					 const char *request_payload)
{
	SHA256_CTX ctx;
	unsigned char hash[SHA256_DIGEST_LENGTH];
	int i;
	unsigned char hex_hash[SHA256_DIGEST_LENGTH * 2 + 1];
	char *canonical_request;

	/*
	    HexHash = HexEncode(Hash(RequestPayload))
	*/
	SHA256_Init(&ctx);
	SHA256_Update(&ctx, request_payload, strlen(request_payload));
	SHA256_Final(hash, &ctx);

	/*
	    1 byte ずつ地道に 16 進文字表現を生成する
	*/
	for (i = 0; i < SHA256_DIGEST_LENGTH; i++) {
		sprintf(hex_hash + i * 2, "%.2x", hash[i]);
	}

	/*
	    CanonicalRequest =
                HTTPRequestMethod + '\n' +
                CanonicalURI + '\n' +
                CanonicalQueryString + '\n' +
                CanonicalHeaders + '\n' +
                SignedHeaders + '\n' +
                HexHash
	*/
	if (asprintf(&canonical_request,
	     "%s\n%s\n%s\n%s\n%s\n%s",
	     http_request_method,
	     canonical_uri,
	     canonical_query_string,
	     canonical_headers,
	     signed_headers,
	     hex_hash) == -1) {
		Warnx("aws_dynamo_create_signature: failed to create message");
		return NULL;
	}

	/*
	    HashedCanonicalRequest = HexEncode(Hash(CanonicalRequest))
	*/
	SHA256_Init(&ctx);
	SHA256_Update(&ctx, canonical_request, strlen(canonical_request));
	SHA256_Final(hash, &ctx);

	free(canonical_request);

	for (i = 0; i < SHA256_DIGEST_LENGTH; i++) {
		sprintf(hex_hash + i * 2, "%.2x", hash[i]);
	}

	return strdup(hex_hash);
}

署名文字列の作成

タスク 2: 署名バージョン 4 の署名文字列を作成する は以下の関数で実装されています。

char *aws_sigv4_create_string_to_sign(char *iso8601_basic_date,
				      char *yyyy_mm_dd,
				      const char *region,
				      const char *service,
				      const char *hashed_canonical_request)
{
	char *string_to_sign;

	/*
	    StringToSign  =
                Algorithm + '\n' +
                RequestDate + '\n' +
                CredentialScope + '\n' +
                HashedCanonicalRequest
	*/
	if (asprintf(&string_to_sign,
	     "AWS4-HMAC-SHA256\n%s\n%s/%s/%s/aws4_request\n%s",
	     iso8601_basic_date,
	     yyyy_mm_dd, region, service,
	     hashed_canonical_request) == -1) {
		Warnx("aws_dynamo_create_signature: failed to create string to sign.");
		return NULL;
	}

	return string_to_sign;
}

署名の作成

タスク 3: AWS 署名バージョン 4 を計算する は以下の関数で実装されています。

int aws_sigv4_derive_signing_key(
	const char *aws_secret_access_key,
	const char *yyyy_mm_dd,
	const char *region,
	const char *service,
	unsigned char **key /* OUT */,
	int *key_len /* OUT */)
{
	unsigned char md[EVP_MAX_MD_SIZE];
	unsigned int md_len;
	unsigned char date_key[128];
	int n;
	HMAC_CTX hmac_ctx;

	HMAC_CTX_init(&hmac_ctx);

	n = snprintf(date_key, sizeof(date_key), "AWS4%s",
		aws_secret_access_key);

	if (n < 0 || n > sizeof(date_key)) {
		Warnx("aws_sigv4_derive_signing_key: did not create date key.");
		return -1;
	}

	/*
	    kDate = HMAC("AWS4" + kSecret, Date)
	*/
	HMAC_Init_ex(&hmac_ctx, date_key, strlen(date_key), EVP_sha256(), NULL);
	HMAC_Update(&hmac_ctx, yyyy_mm_dd, strlen(yyyy_mm_dd));
	HMAC_Final(&hmac_ctx, md, &md_len);
	
	/*
	    kRegion = HMAC(kDate, Region)
	*/
	HMAC_Init_ex(&hmac_ctx, md, md_len, EVP_sha256(), NULL);
	HMAC_Update(&hmac_ctx, region, strlen(region));
	HMAC_Final(&hmac_ctx, md, &md_len);
	
	/*
	    kService = HMAC(kRegion, Service)
	*/
	HMAC_Init_ex(&hmac_ctx, md, md_len, EVP_sha256(), NULL);
	HMAC_Update(&hmac_ctx, service, strlen(service));
	HMAC_Final(&hmac_ctx, md, &md_len);
	
	/*
	    kSigning = HMAC(kService, "aws4_request")
	*/
	HMAC_Init_ex(&hmac_ctx, md, md_len, EVP_sha256(), NULL);
	HMAC_Update(&hmac_ctx, "aws4_request", strlen("aws4_request"));
	HMAC_Final(&hmac_ctx, md, &md_len);
	
	HMAC_CTX_cleanup(&hmac_ctx);

	*key = malloc(md_len);
	if (*key == NULL) {
		Warnx("aws_sigv4_derive_signing_key: key alloc failed.");
		return -1;
	}
	memcpy(*key, md, md_len);
	*key_len = md_len;
	return 0;
}

char *aws_sigv4_create_signature(
	const char *aws_secret_access_key,
	const char *yyyy_mm_dd,
	const char *region,
	const char *service,
	const unsigned char *message)
{
	HMAC_CTX hmac_ctx;
	unsigned char md[EVP_MAX_MD_SIZE];
	unsigned int md_len;
	char *signature;
	int message_len = strlen(message);
	unsigned char *key = NULL;
	int key_len = 0;
	int i;

	HMAC_CTX_init(&hmac_ctx);

	aws_sigv4_derive_signing_key(aws_secret_access_key,
		yyyy_mm_dd, region, service, &key, &key_len);

	/*
	    signature = HexEncode(HMAC(derived-signing-key, string-to-sign))
	*/
	HMAC_Init_ex(&hmac_ctx, key, key_len, EVP_sha256(), NULL);
	HMAC_Update(&hmac_ctx, message, message_len);
	HMAC_Final(&hmac_ctx, md, &md_len);
	free(key);
	signature = malloc(md_len * 2 + 1);

	if (signature == NULL) {
		Warnx("aws_sigv4_create_signature: failed to allocate sig");
		HMAC_CTX_cleanup(&hmac_ctx);
		return NULL;
	}

	for (i = 0; i < md_len; i++) {
		sprintf(signature + i * 2, "%.2x", md[i]);
	}

	HMAC_CTX_cleanup(&hmac_ctx);

	return signature;
}

まとめ

C 言語の場合、特に文字列操作周りでコード量が多くなりがちにはなりますが、aws にネイティブコードで触ることができるのは特徴だと思いますので、興味のある方は触ってみてはいかがでしょうか。