[ PHP ] AWS 署名バージョン 4

SDKを使わないで自分で作ってみる

この記事は参考程度と思ってください。環境ごとに値が違うので。

AWS SDK for PHP バージョン 3 により、PHP 開発者は PHP コードでアマゾン ウェブ サービスを使用できます。

参考になるのは、「完全なバージョン 4 署名プロセスの例 (Python)」

Python を使用してバージョン 4 署名リクエストを作成する方法を説明します。

参考ドキュメントはこちら

署名バージョン 4 の正規リクエストを作成するステップについて説明します。

1.署名バージョン 4 の正規リクエストを作成する

CanonicalRequest =
    HTTPRequestMethod + '\n' +
    CanonicalURI + '\n' +
    CanonicalQueryString + '\n' +
    CanonicalHeaders + '\n' +
    SignedHeaders + '\n' +
    HexEncode(Hash(RequestPayload))

上記をPHPに置き換えると。

private function CanonicalRequest($canonicalRequest) {
    $str = $canonicalRequest['method'] . "\n"
         . $canonicalRequest['uri'] . "\n"
         . $canonicalRequest['query'] . "\n";
    if (count($canonicalRequest['headers']) > 0) {
        foreach ($canonicalRequest['headers'] as $item) {
            $str .= $item . "\n";
        }
    } else { $str .= "\n"; }
    $str .= "\n";
    $str .=$canonicalRequest['signedheaders'] . "\n"
         . $canonicalRequest['payload'];
    return hash('sha256', $str, true);
}

呼び出し側はこちら(payloadはPOSTなどのRAWデータが無ければこのまま)

$headers = [
    'content-type:application/json;charset=UTF-8',
    'host:' . { host },
    'x-amz-date:' . gmdate("Ymd\THis\Z")
];
$canonicalRequest = [
    'method' => 'GET',
    'uri' => '/test',
    'query' => '',
    'headers' => $headers,
    'signedheaders' => 'content-type;host;x-amz-date',
    'payload' => 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
];
$canonical_request = $this->CanonicalRequest($canonicalRequest);

ヘッダが増えれば、signedheadersも変わるのと、アルファベット順である必要があります。

payloadの求め方は、$payload = hash(‘sha256’, ”);

2.署名バージョン 4 の署名文字列を作成する

StringToSign =
    Algorithm + \n +
    RequestDateTime + \n +
    CredentialScope + \n +
    HashedCanonicalRequest

PHP変換後、

private function signature($param, $canonical_request, $signing_key) {
    $credential_scope = $param['datestamp'] . "/"
                      . { region } . "/"
                      . { service } . "/"
                      . 'aws4_request';
    $string_to_sign = { algorithm } . "\n"    // 'AWS4-HMAC-SHA256'
                    . $param['amzdate'] . "\n"
                    . $credential_scope . "\n"
                    . bin2hex($canonical_request);
    return hash_hmac('sha256', $string_to_sign, $signing_key, true);
}

呼び出し側

$gmtDate = gmdate("Ymd\THis\Z");
$param['amzdate'] = $gmtDate;
$param['datestamp'] = substr($gmtDate, 0, 8);

$signature = $this->signature(
    $param,
    $canonical_request,    // 1の正規リクエストで生成したデータ
    $signing_key);         // 3の署名データ

なぜか3の処理が入ってきてるけどドキュメント上はこちらが先だったり

3.AWS 署名バージョン 4 の署名を計算する

kSecret = your secret access key
kDate = HMAC("AWS4" + kSecret, Date)
kRegion = HMAC(kDate, Region)
kService = HMAC(kRegion, Service)
kSigning = HMAC(kService, "aws4_request")

PHP変換後は、

private function signatureKey($key, $dateStamp, $regionName, $serviceName) {
    $kDate = hash_hmac('sha256', $dateStamp, 'AWS4'.$key, true);
//echo 'kDate : '.bin2hex($kDate); // 中身を確認するときはこちらで
    $kRegion = hash_hmac('sha256', $regionName, $kDate, true);
    $kService = hash_hmac('sha256', $serviceName, $kRegion, true);
    $kSigning = hash_hmac('sha256', 'aws4_request', $kService, true);
    return $kSigning;
}

呼び出し側はこちら

$gmtDate = gmdate("Ymd\THis\Z");
$param['amzdate'] = $gmtDate;
$param['datestamp'] = substr($gmtDate, 0, 8);

$signing_key = $this->signatureKey(
    { secretkey },
    $datestamp, 
    { region },
    { service });

hash_hmacのバイナリで行う必要があります。デバッグにbin2hexを使うと確認できます。

secretkeyは一時認証などで取得できます。

4.署名情報をリクエストに追加する

Authorization: algorithm Credential=access key ID/credential scope, SignedHeaders=SignedHeaders, Signature=signature

いままでの処理はこの1行を作るためのもので、やっとゴールです。

private function createAuthorization($param, $canonicalRequest, $signature) {
    $credential = { accesskey } . "/"
                . $param['datestamp'] . "/"
                . { region } . "/"
                . { service } . "/"
                . 'aws4_request';
    $signedHeaders = $canonicalRequest['signedheaders'];
    $auth = { algorithm } . ' ' // 'AWS4-HMAC-SHA256'
          . 'Credential=' . $credential . ', '
          . 'SignedHeaders=' . $signedHeaders . ', '
          . 'Signature=' . bin2hex($signature);
    return $auth;
}

呼び出し元は

$gmtDate = gmdate("Ymd\THis\Z");
$param['amzdate'] = $gmtDate;
$param['datestamp'] = substr($gmtDate, 0, 8);

$canonicalRequest = [
    ...
    'signedheaders' => 'content-type;host;x-amz-date',
    ...
];

$auth = $this->createAuthorization(
    $param,
    $canonicalRequest,
    $signature);    // 3で生成したデータ
$authorization = 'Authorization:' . $auth;

おおまかにですが、こんな感じになります。一部は環境ごとに違ったり別処理が必要だったり

実際開発時や確認はPostmanを利用するほうが楽になります。

PHP-SDKが使えればよかったんですが、認証の一時発行が外部から動作しない?のか使えなくて全てPHPのCURLにて置き換えてます。

5.AWSの一時認証(おまけ)

private function signingAWS($cognito_id, $openid_token) {
    // urlは適当に
    $url = 'https://cognito-identity.ap-northeast-1.amazonaws.com/';
    $body = [
        'IdentityId' => $cognito_id,
        'Logins' => [
            'cognito-identity.amazonaws.com' => $openid_token,
        ]
    ];
    $headers = [
        'Content-Type: application/x-amz-json-1.1',
        'Origin: null',
        'X-Amz-Content-Sha256: ' . hash('sha256', json_encode($body)),
        'X-Amz-Target: AWSCognitoIdentityService.GetCredentialsForIdentity',
    ];

    $ret = $this->requestPost($url, $headers, $body);
    return $ret;
}

この方法であってるか判りません。

cognito_idとopenid_tokenはAWSのログイン時に取得できます。

値は実際の環境に合わせてください。

フォローする