gasからAWSのV4署名を作成して、APIでSESからメールを送信する

Google SpreadSheetのgasからAWSのV4署名を作成して、APIでSESから別ドメインのメールを送信する。
少しニッチなニーズ（死語）ですがドキュメントオーナーのgmailアドレスに依存しない形になるので、可用性は高まります。
セキュリティ、メールに関する知識が足りないので、足りない部分はコメント頂けると幸いです。

https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/send-email-api.html
（参考：公式 Amazon SES API を使用して E メールを送信する）

>直接 HTTPS リクエストを作成する - これは、リクエストの認証と署名を手動で処理し、リクエストを手動で作成する必要があるため、最も高度な方法です。リクエストの実行方法の詳細については、「Amazon SES API の使用」を参照してください。

最も高度な方法……
本稿では、gas内でAWSのV4署名を作成して、その署名を使用して、APIを叩いて、メールを送信するまで、です。GETのみ、POSTもできるようですが、検証までの時間が足りませんでした。

## 1．V4 署名の作成
https://docs.aws.amazon.com/ja_jp/general/latest/gr/sigv4-signed-request-examples.html
（AWS公式：完全な署名バージョン 4 署名プロセスの例 (Python)）
公式にPythonのサンプルコードがあるので、そのままgasに置換して作成します。

以下、実装です。


```
/**
 * AWSのV4署名を作成する 
 * 
 * @param method GET/POST
 * @param host 使用するAPIのエンドポイントのHOST
 * @param region 使用するAPIのリージョン
 * @param service 使用するAPIのサービス EC2とかmailとか
 * @param dateStamp 現在日付 new Date(), "UTC", "yyyyMMdd"
 * @param amzdate aws形式の日付 new Date(), "UTC", "yyyyMMdd'T'HHmmss'Z'"
 * @param requestParameters リクエストパラメータ Action=CreateUser&UserName=NewUser&Version=2010-05-08
 * @param signedHeaders host;x-amz-date 固定　サンプルがそうだったから
 * @param secretAccessKey アクセスする秘密鍵
 */
function getSignature(method, host, region, service, dateStamp, amzdate, requestParameters, signedHeaders, secretAccessKey) {

// ************* 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)
　 let canonicalUri = '/';

// 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.
  let canonicalQuerystring = requestParameters;

// 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. Note that there is a trailing \n.
  let canonicalHeaders = 'host:' + host + '\n' + 'x-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.
// let signedHeaders = 'host;x-amz-date';

// Step 6: Create payload hash (hash of the request body content). For GET
// requests, the payload is an empty string ("").
  let payloadHash = getSHA256('');

// Step 7: Combine elements to create canonical request
  let canonicalRequest = method + '\n' + canonicalUri + '\n' + canonicalQuerystring + '\n' + canonicalHeaders + '\n' + signedHeaders + '\n' + payloadHash;

// ************* TASK 2: CREATE THE STRING TO SIGN*************
// Match the algorithm to the hashing algorithm you use, either SHA-1 or
// SHA-256 (recommended)

  let algorithm = 'AWS4-HMAC-SHA256'
  let credentialScope = dateStamp + '/' + region + '/' + service + '/' + 'aws4_request';
  let stringToSign = algorithm + '\n' +  amzdate + '\n' +  credentialScope + '\n' + getSHA256(canonicalRequest);

  let signaturekey = getSignatureKey(secretAccessKey, dateStamp, region, service);
  let signature = getHmacSha256Signature(signaturekey, stringToSign, 'HEX');

  return signature
}

```

## 2．署名キーの作成

署名を作成するために、署名キーを作成する必要があります。
前段の署名を作成するソースコードでは、下記関数があたります。
getSignatureKey

https://docs.aws.amazon.com/ja_jp/general/latest/gr/signature-v4-examples.html
（AWS公式：署名バージョン 4 の署名キーを取得する方法の例）
署名キーもサンプルがあるので、そのままgasにします。
HmacSHA256を作成するために、最初は、gasのutilities.computeHmacSha256Signatureを使用したところ取得できる値がサンプルと違うため、確認したところ、バイナリ形式で鍵を送らないといけないみたいでした。

https://creators-note.chatwork.com/entry/2017/12/20/163128
（参考：Google Apps ScriptでChatWorkのWebhook署名を検証する方法）
Chatworkの中の人が、バイナリ形式で送る方法書いてくれていたので、参考にしました。

>コードを見て気づいた方もいらっしゃるかもしれませんが、今回GASで標準で提供されるUtilities.computeHmacSha256Signature)ではなく、jsSHA - SHA Hashes in JavaScriptを直接埋め込んでいます。

>これはUtilities.computeHmacSignatureがバイナリ形式の入力に対応しておらず、署名の検証に使用することができないためです。

同じくコードを貼り付け、HEXの形から、鍵を作れるようになりました。

https://stackoverflow.com/questions/41232615/how-to-get-hex-value-from-computehmacsha256signature-method-of-google-apps-scrip
（参考）
stackoverflowでも、私と同じようにサンプルと同じようにならない、間違っているのではないか、と書いている人がいました。
ちゃんと公式に書いているんですけどね

> これらはバイナリデータの 16 進エンコード表現ですが、キー自体と中間値はバイナリ形式となることに注意してください。

サンプルの値の鍵がページにあるので、途中経過の計算があっているかは確認できます。

```
kSecret  = '41575334774a616c725855746e46454d492f4b374d44454e472b62507852666943594558414d504c454b4559'
kDate    = '969fbb94feb542b71ede6f87fe4d5fa29c789342b0f407474670f0c2489e0a0d'
kRegion  = '69daa0209cd9c5ff5c8ced464a696fd4252e981430b10e3d3fd8e2f197d7a70c'
kService = 'f72cfd46f26bc4643f06a11eabb6c0ba18780c19a8da0c31ace671265e3c87fa'
kSigning = 'f4780e2d9f65fa895f9c67b32ce1baf0b0d8a43505a000a1a9e090d414db404d'
```

getでも、空のpayloadをダイジェスト値にして送らないといけないため、ダイジェスト値の作成は以下を参考にしました。
https://qiita.com/SogoK/items/cc0d514ffe74009e5fd5

以下、実装です。


```
function getSignatureKey(key, dateStamp, region, service) {
  let keyDateStamp = getHmacSha256Signature('AWS4' + key, dateStamp, 'TEXT');
  let keyRegion = getHmacSha256Signature(keyDateStamp, region, 'HEX');
  let keyService = getHmacSha256Signature(keyRegion, service, 'HEX');
  let keySinging = getHmacSha256Signature(keyService, 'aws4_request', 'HEX');
  return keySinging;
}
function getHmacSha256Signature(key, value, keyFormat = 'TEXT')
{
  let shaObj = new jsSHA("SHA-256", "TEXT");
  shaObj.setHMACKey(key, keyFormat);
  shaObj.update(value);
  let signature = shaObj.getHMAC("HEX");
  Logger.log(`key: ${key} value:${value} signature:${signature}`)
  return signature 
}

//https://qiita.com/SogoK/items/cc0d514ffe74009e5fd5
function getSHA256(input) {
  var rawHash = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, input, Utilities.Charset.UTF_8);
  var txtHash = '';
  for (i = 0; i < rawHash.length; i++) {
    var hashVal = rawHash[i];
    if (hashVal < 0) {
      hashVal += 256;
    }
    if (hashVal.toString(16).length == 1) {
      txtHash += '0';
    }
    txtHash += hashVal.toString(16);
  }
  return txtHash;
}

//https://creators-note.chatwork.com/entry/2017/12/20/163128
'use strict';(function(I){function w(c,a,d){var l=0,b=[],g=0,f,n,k,e,h,q,y,p,m=!1,t=[],r=[],u,z=!1;d=d||{};f=d.encoding||"UTF8";u=d.numRounds||1;if(u!==parseInt(u,10)||1>u)throw Error("numRounds must a integer >= 1");if(0===c.lastIndexOf("SHA-",0))if(q=function(b,a){return A(b,a,c)},y=function(b,a,l,f){var g,e;if("SHA-224"===c||"SHA-256"===c)g=(a+65>>>9<<4)+15,e=16;else throw Error("Unexpected error in SHA-2 implementation");for(;b.length<=g;)b.push(0);b[a>>>5]|=128<<24-a%32;a=a+l;b[g]=a&4294967295;
b[g-1]=a/4294967296|0;l=b.length;for(a=0;a<l;a+=e)f=A(b.slice(a,a+e),f,c);if("SHA-224"===c)b=[f[0],f[1],f[2],f[3],f[4],f[5],f[6]];else if("SHA-256"===c)b=f;else throw Error("Unexpected error in SHA-2 implementation");return b},p=function(b){return b.slice()},"SHA-224"===c)h=512,e=224;else if("SHA-256"===c)h=512,e=256;else throw Error("Chosen SHA variant is not supported");else throw Error("Chosen SHA variant is not supported");k=B(a,f);n=x(c);this.setHMACKey=function(b,a,g){var e;if(!0===m)throw Error("HMAC key already set");
if(!0===z)throw Error("Cannot set HMAC key after calling update");f=(g||{}).encoding||"UTF8";a=B(a,f)(b);b=a.binLen;a=a.value;e=h>>>3;g=e/4-1;if(e<b/8){for(a=y(a,b,0,x(c));a.length<=g;)a.push(0);a[g]&=4294967040}else if(e>b/8){for(;a.length<=g;)a.push(0);a[g]&=4294967040}for(b=0;b<=g;b+=1)t[b]=a[b]^909522486,r[b]=a[b]^1549556828;n=q(t,n);l=h;m=!0};this.update=function(a){var c,f,e,d=0,p=h>>>5;c=k(a,b,g);a=c.binLen;f=c.value;c=a>>>5;for(e=0;e<c;e+=p)d+h<=a&&(n=q(f.slice(e,e+p),n),d+=h);l+=d;b=f.slice(d>>>
5);g=a%h;z=!0};this.getHash=function(a,f){var d,h,k,q;if(!0===m)throw Error("Cannot call getHash after setting HMAC key");k=C(f);switch(a){case "HEX":d=function(a){return D(a,e,k)};break;case "B64":d=function(a){return E(a,e,k)};break;case "BYTES":d=function(a){return F(a,e)};break;case "ARRAYBUFFER":try{h=new ArrayBuffer(0)}catch(v){throw Error("ARRAYBUFFER not supported by this environment");}d=function(a){return G(a,e)};break;default:throw Error("format must be HEX, B64, BYTES, or ARRAYBUFFER");
}q=y(b.slice(),g,l,p(n));for(h=1;h<u;h+=1)q=y(q,e,0,x(c));return d(q)};this.getHMAC=function(a,f){var d,k,t,u;if(!1===m)throw Error("Cannot call getHMAC without first setting HMAC key");t=C(f);switch(a){case "HEX":d=function(a){return D(a,e,t)};break;case "B64":d=function(a){return E(a,e,t)};break;case "BYTES":d=function(a){return F(a,e)};break;case "ARRAYBUFFER":try{d=new ArrayBuffer(0)}catch(v){throw Error("ARRAYBUFFER not supported by this environment");}d=function(a){return G(a,e)};break;default:throw Error("outputFormat must be HEX, B64, BYTES, or ARRAYBUFFER");
}k=y(b.slice(),g,l,p(n));u=q(r,x(c));u=y(k,e,h,u);return d(u)}}function m(){}function D(c,a,d){var l="";a/=8;var b,g;for(b=0;b<a;b+=1)g=c[b>>>2]>>>8*(3+b%4*-1),l+="0123456789abcdef".charAt(g>>>4&15)+"0123456789abcdef".charAt(g&15);return d.outputUpper?l.toUpperCase():l}function E(c,a,d){var l="",b=a/8,g,f,n;for(g=0;g<b;g+=3)for(f=g+1<b?c[g+1>>>2]:0,n=g+2<b?c[g+2>>>2]:0,n=(c[g>>>2]>>>8*(3+g%4*-1)&255)<<16|(f>>>8*(3+(g+1)%4*-1)&255)<<8|n>>>8*(3+(g+2)%4*-1)&255,f=0;4>f;f+=1)8*g+6*f<=a?l+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(n>>>
6*(3-f)&63):l+=d.b64Pad;return l}function F(c,a){var d="",l=a/8,b,g;for(b=0;b<l;b+=1)g=c[b>>>2]>>>8*(3+b%4*-1)&255,d+=String.fromCharCode(g);return d}function G(c,a){var d=a/8,l,b=new ArrayBuffer(d),g;g=new Uint8Array(b);for(l=0;l<d;l+=1)g[l]=c[l>>>2]>>>8*(3+l%4*-1)&255;return b}function C(c){var a={outputUpper:!1,b64Pad:"=",shakeLen:-1};c=c||{};a.outputUpper=c.outputUpper||!1;!0===c.hasOwnProperty("b64Pad")&&(a.b64Pad=c.b64Pad);if("boolean"!==typeof a.outputUpper)throw Error("Invalid outputUpper formatting option");
if("string"!==typeof a.b64Pad)throw Error("Invalid b64Pad formatting option");return a}function B(c,a){var d;switch(a){case "UTF8":case "UTF16BE":case "UTF16LE":break;default:throw Error("encoding must be UTF8, UTF16BE, or UTF16LE");}switch(c){case "HEX":d=function(a,b,c){var f=a.length,d,k,e,h,q;if(0!==f%2)throw Error("String of HEX type must be in byte increments");b=b||[0];c=c||0;q=c>>>3;for(d=0;d<f;d+=2){k=parseInt(a.substr(d,2),16);if(isNaN(k))throw Error("String of HEX type contains invalid characters");
h=(d>>>1)+q;for(e=h>>>2;b.length<=e;)b.push(0);b[e]|=k<<8*(3+h%4*-1)}return{value:b,binLen:4*f+c}};break;case "TEXT":d=function(c,b,d){var f,n,k=0,e,h,q,m,p,r;b=b||[0];d=d||0;q=d>>>3;if("UTF8"===a)for(r=3,e=0;e<c.length;e+=1)for(f=c.charCodeAt(e),n=[],128>f?n.push(f):2048>f?(n.push(192|f>>>6),n.push(128|f&63)):55296>f||57344<=f?n.push(224|f>>>12,128|f>>>6&63,128|f&63):(e+=1,f=65536+((f&1023)<<10|c.charCodeAt(e)&1023),n.push(240|f>>>18,128|f>>>12&63,128|f>>>6&63,128|f&63)),h=0;h<n.length;h+=1){p=k+
q;for(m=p>>>2;b.length<=m;)b.push(0);b[m]|=n[h]<<8*(r+p%4*-1);k+=1}else if("UTF16BE"===a||"UTF16LE"===a)for(r=2,n="UTF16LE"===a&&!0||"UTF16LE"!==a&&!1,e=0;e<c.length;e+=1){f=c.charCodeAt(e);!0===n&&(h=f&255,f=h<<8|f>>>8);p=k+q;for(m=p>>>2;b.length<=m;)b.push(0);b[m]|=f<<8*(r+p%4*-1);k+=2}return{value:b,binLen:8*k+d}};break;case "B64":d=function(a,b,c){var f=0,d,k,e,h,q,m,p;if(-1===a.search(/^[a-zA-Z0-9=+\/]+$/))throw Error("Invalid character in base-64 string");k=a.indexOf("=");a=a.replace(/\=/g,
"");if(-1!==k&&k<a.length)throw Error("Invalid '=' found in base-64 string");b=b||[0];c=c||0;m=c>>>3;for(k=0;k<a.length;k+=4){q=a.substr(k,4);for(e=h=0;e<q.length;e+=1)d="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".indexOf(q[e]),h|=d<<18-6*e;for(e=0;e<q.length-1;e+=1){p=f+m;for(d=p>>>2;b.length<=d;)b.push(0);b[d]|=(h>>>16-8*e&255)<<8*(3+p%4*-1);f+=1}}return{value:b,binLen:8*f+c}};break;case "BYTES":d=function(a,b,c){var d,n,k,e,h;b=b||[0];c=c||0;k=c>>>3;for(n=0;n<a.length;n+=
1)d=a.charCodeAt(n),h=n+k,e=h>>>2,b.length<=e&&b.push(0),b[e]|=d<<8*(3+h%4*-1);return{value:b,binLen:8*a.length+c}};break;case "ARRAYBUFFER":try{d=new ArrayBuffer(0)}catch(l){throw Error("ARRAYBUFFER not supported by this environment");}d=function(a,b,c){var d,n,k,e,h;b=b||[0];c=c||0;n=c>>>3;h=new Uint8Array(a);for(d=0;d<a.byteLength;d+=1)e=d+n,k=e>>>2,b.length<=k&&b.push(0),b[k]|=h[d]<<8*(3+e%4*-1);return{value:b,binLen:8*a.byteLength+c}};break;default:throw Error("format must be HEX, TEXT, B64, BYTES, or ARRAYBUFFER");
}return d}function r(c,a){return c>>>a|c<<32-a}function J(c,a,d){return c&a^~c&d}function K(c,a,d){return c&a^c&d^a&d}function L(c){return r(c,2)^r(c,13)^r(c,22)}function M(c){return r(c,6)^r(c,11)^r(c,25)}function N(c){return r(c,7)^r(c,18)^c>>>3}function O(c){return r(c,17)^r(c,19)^c>>>10}function P(c,a){var d=(c&65535)+(a&65535);return((c>>>16)+(a>>>16)+(d>>>16)&65535)<<16|d&65535}function Q(c,a,d,l){var b=(c&65535)+(a&65535)+(d&65535)+(l&65535);return((c>>>16)+(a>>>16)+(d>>>16)+(l>>>16)+(b>>>
16)&65535)<<16|b&65535}function R(c,a,d,l,b){var g=(c&65535)+(a&65535)+(d&65535)+(l&65535)+(b&65535);return((c>>>16)+(a>>>16)+(d>>>16)+(l>>>16)+(b>>>16)+(g>>>16)&65535)<<16|g&65535}function x(c){var a=[],d;if(0===c.lastIndexOf("SHA-",0))switch(a=[3238371032,914150663,812702999,4144912697,4290775857,1750603025,1694076839,3204075428],d=[1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225],c){case "SHA-224":break;case "SHA-256":a=d;break;case "SHA-384":a=[new m,new m,
new m,new m,new m,new m,new m,new m];break;case "SHA-512":a=[new m,new m,new m,new m,new m,new m,new m,new m];break;default:throw Error("Unknown SHA variant");}else throw Error("No SHA variants supported");return a}function A(c,a,d){var l,b,g,f,n,k,e,h,m,r,p,w,t,x,u,z,A,B,C,D,E,F,v=[],G;if("SHA-224"===d||"SHA-256"===d)r=64,w=1,F=Number,t=P,x=Q,u=R,z=N,A=O,B=L,C=M,E=K,D=J,G=H;else throw Error("Unexpected error in SHA-2 implementation");d=a[0];l=a[1];b=a[2];g=a[3];f=a[4];n=a[5];k=a[6];e=a[7];for(p=
0;p<r;p+=1)16>p?(m=p*w,h=c.length<=m?0:c[m],m=c.length<=m+1?0:c[m+1],v[p]=new F(h,m)):v[p]=x(A(v[p-2]),v[p-7],z(v[p-15]),v[p-16]),h=u(e,C(f),D(f,n,k),G[p],v[p]),m=t(B(d),E(d,l,b)),e=k,k=n,n=f,f=t(g,h),g=b,b=l,l=d,d=t(h,m);a[0]=t(d,a[0]);a[1]=t(l,a[1]);a[2]=t(b,a[2]);a[3]=t(g,a[3]);a[4]=t(f,a[4]);a[5]=t(n,a[5]);a[6]=t(k,a[6]);a[7]=t(e,a[7]);return a}var H;H=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,
2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,
2756734187,3204031479,3329325298];"function"===typeof define&&define.amd?define(function(){return w}):"undefined"!==typeof exports?("undefined"!==typeof module&&module.exports&&(module.exports=w),exports=w):I.jsSHA=w})(this);
```


## 3．APIのリクエスト

作成した署名を使用してリクエストを送信します。
サンプルでは、リクエストのヘッダーにHostを付与しろと書いてありますが、gasのUrlFetchApp.fetchでは付与すると使用できないと、エラーになりました。付与しなくても、リクエストは成功するようです。
リクエストのパラメータは、ASCIIコード順に並べないと、署名に失敗します。

https://pfs.nifcloud.com/api/signature_v4.htm
（参考：ニフクラAPI（シグネチャーバージョン 4 生成方法））

>パラメーター名でASCIIコード順にソートします。

最初サンプル通り、
https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/using-ses-api-examples.html
（参考：公式 Amazon SES API の GET と POST の例）

```
https://email.us-west-2.amazonaws.com/
?Action=SendEmail
&Source=user%40example.com
&Destination.ToAddresses.member.1=allan%40example.com
&Message.Subject.Data=This%20is%20the%20subject%20line.
&Message.Body.Text.Data=Hello.%20I%20hope%20you%20are%20having%20a%20good%20day.
```
としていたら署名に失敗しました。パラメータがASCII順に並んでいないためです。

```
https://email.us-west-2.amazonaws.com/
?Action=SendEmail
&Destination.ToAddresses.member.1=allan%40example.com
&Message.Body.Text.Data=Hello.%20I%20hope%20you%20are%20having%20a%20good%20day.
&Message.Subject.Data=This%20is%20the%20subject%20line.
&Source=user%40example.com
```
Destination→Message.Body→Message.Subject→Source ASCII順にして、署名を作成する必要がある

以下、実装です。

```

// AWS SESにpostする
function postMail(fromAddress = 'no-reply@sample.com', toAddress = 'rei-ta@sample.jp',title = 'test', body = 'body', bccAddress = '') {

  const accesskeyId = 'AKIsssssssZWH';
  const secretAccessKey = '6oddddddddddddddddZLiEKGM+KO';

  const method = 'GET';
  const host = 'email.ap-northeast-1.amazonaws.com';
  const region = 'ap-northeast-1';
  const service = 'ses';

  const encodeToAddress =  encodeURIComponent(toAddress);
  const base64Body=  encodeURIComponent(Utilities.base64Encode(`From:${fromAddress}
To: ${toAddress}
Bcc: ${bccAddress}
Subject: ${title}

${body}`,Utilities.Charset.UTF_8));
  let request_parameters = `Action=SendRawEmail&RawMessage.Data=${base64Body}`;

  requestAwsApi(method, host, region, service, accesskeyId, secretAccessKey, request_parameters);

} 

function requestAwsApi(method, host, region, service, accesskeyId, secretAccessKey, requestParameters) {

  let dateStamp = Utilities.formatDate(new Date(), "UTC", "yyyyMMdd");
  let amzdate = Utilities.formatDate(new Date(), "UTC", "yyyyMMdd'T'HHmmss'Z'");
  let signedHeaders = 'host;x-amz-date';
  let signature = getSignature(method, host, region, service, dateStamp, amzdate, requestParameters, signedHeaders, secretAccessKey);

  var headers = {
    'Content-Type': 'application/x-www-form-urlencoded',
    'X-Amz-Date': amzdate,
    'Authorization': `AWS4-HMAC-SHA256 Credential=${accesskeyId}/${dateStamp}/${region}/${service}/aws4_request, SignedHeaders=${signedHeaders}, Signature=${signature}`
  }
  Logger.log(headers)

  var options = {
    'method' : method,
    'headers' : headers,
    'muteHttpExceptions' : true,
  }
 try {
    var url = 'https://' + host + '?' + requestParameters;
    console.log(`url: ${url}`);

    var response = UrlFetchApp.fetch(url, options);
    console.log(response.getContentText());

  } catch(e) {
    // 例外エラー処理
    Logger.log('Error:')
    Logger.log(e)
  }
  return response;

}
```


ちょっと煩雑になっているので足りない部分があると思いますが、最低限伝えたいのはgasで、AWSのV4署名が作成できて、APIのリクエストできるか、ですが、答えは「頑張ればできる」です。
（ググっても、できるかどうかが、出てこなかったので不安になりながら作業していました。）

やるかどうかは別として、gasからAPI経由でec2のシャットダウンとかもできるはずです。