20

I’m trying to calculate a signature to make Amazon Marketplace API calls, but I keep getting the following error:

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.

I’ve wrapped the signature creation process into a class:

<?php
namespace App\Marketplace\Amazon;

class Signature
{
    protected $signedString;

    public function __construct($url, array $parameters, $secretAccessKey)
    {
        $stringToSign = $this->calculateStringToSign($url, $parameters);

        $this->signedString = $this->sign($stringToSign, $secretAccessKey);
    }

    protected function calculateStringToSign($url, array $parameters)
    {
        $url = parse_url($url);

        $string = "POST\n";
        $string .= $url['host'] . "\n";
        $string .= $url['path'] . "\n";
        $string .= $this->getParametersAsString($parameters);

        return $string;
    }

    protected function sign($data, $secretAccessKey)
    {
        return base64_encode(hash_hmac('sha256', $data, $secretAccessKey, true));
    }

    protected function getParametersAsString(array $parameters)
    {
        uksort($parameters, 'strcmp');

        $queryParameters = [];

        foreach ($parameters as $key => $value) {
            $queryParameters[$key] = $this->urlEncode($value);
        }

        return http_build_query($queryParameters);
    }

    protected function urlEncode($value)
    {
        return str_replace('%7E', '~', rawurlencode($value));
    }

    public function __toString()
    {
        return $this->signedString;
    }
}

But I can’t for the life of me see where I’m going wrong. I’ve followed the guide in the API, and looked at the Java example as well as the antiquated Marketplace PHP SDK*.

EDIT: And here is how I’m using the Signature class:

$version = '2011-07-01';

$url = 'https://mws.amazonservices.com/Sellers/'.$version;

$timestamp = gmdate('c', time());

$parameters = [
    'AWSAccessKeyId' => $command->accessKeyId,
    'Action' => 'GetAuthToken',
    'SellerId' => $command->sellerId,
    'SignatureMethod' => 'HmacSHA256',
    'SignatureVersion' => 2,
    'Timestamp' => $timestamp,
    'Version' => $version,
];

$signature = new Signature($url, $parameters, $command->secretAccessKey);

$parameters['Signature'] = strval($signature);

try {
    $response = $this->client->post($url, [
        'headers' => [
            'User-Agent' => 'my-app-name',
        ],
        'body' => $parameters,
    ]);

    dd($response->getBody());
} catch (\Exception $e) {
    dd(strval($e->getResponse()));
}

As an aside: I know the Marketplace credentials are correct as I’ve logged in to the account and retrieved the access key, secret, and seller IDs.

* I’m not using the SDK as it doesn’t support the API call I need: SubmitFeed.

Martin Bean
  • 38,379
  • 25
  • 128
  • 201

2 Answers2

8

I’m not sure what I’ve changed, but my signature generation is working now. Below is the contents of the class:

<?php
namespace App\Marketplace\Amazon;

class Signature
{
    /**
     * The signed string.
     *
     * @var string
     */
    protected $signedString;

    /**
     * Create a new signature instance.
     *
     * @param  string  $url
     * @param  array   $data
     * @param  string  $secretAccessKey
     */
    public function __construct($url, array $parameters, $secretAccessKey)
    {
        $stringToSign = $this->calculateStringToSign($url, $parameters);

        $this->signedString = $this->sign($stringToSign, $secretAccessKey);
    }

    /**
     * Calculate the string to sign.
     *
     * @param  string  $url
     * @param  array   $parameters
     * @return string
     */
    protected function calculateStringToSign($url, array $parameters)
    {
        $url = parse_url($url);

        $string = "POST\n";
        $string .= $url['host']."\n";
        $string .= $url['path']."\n";
        $string .= $this->getParametersAsString($parameters);

        return $string;
    }

    /**
     * Computes RFC 2104-compliant HMAC signature.
     *
     * @param  string  $data
     * @param  string  $secretAccessKey
     * @return string
     */
    protected function sign($data, $secretAccessKey)
    {
        return base64_encode(hash_hmac('sha256', $data, $secretAccessKey, true));
    }

    /**
     * Convert paremeters to URL-encoded query string.
     *
     * @param  array  $parameters
     * @return string
     */
    protected function getParametersAsString(array $parameters)
    {
        uksort($parameters, 'strcmp');

        $queryParameters = [];

        foreach ($parameters as $key => $value) {
            $key = rawurlencode($key);
            $value = rawurlencode($value);

            $queryParameters[] = sprintf('%s=%s', $key, $value);
        }

        return implode('&', $queryParameters);
    }

    /**
     * The string representation of this signature.
     *
     * @return string
     */
    public function __toString()
    {
        return $this->signedString;
    }

}
Martin Bean
  • 38,379
  • 25
  • 128
  • 201
2

Try this function after calling your sign function:

  function amazonEncode($text)
  {
    $encodedText = "";
    $j = strlen($text);
    for($i=0;$i<$j;$i++)
    {
      $c = substr($text,$i,1);
      if (!preg_match("/[A-Za-z0-9\-_.~]/",$c))
      {
        $encodedText .= sprintf("%%%02X",ord($c));
      }
      else
      {
        $encodedText .= $c;
      }
    }
    return $encodedText;
  }

Reference

After you've created the canonical string as described in Format the Query Request, you calculate the signature by creating a hash-based message authentication code (HMAC) using either the HMAC-SHA1 or HMAC-SHA256 protocols. The HMAC-SHA256 protocol is preferred.

The resulting signature must be base-64 encoded and then URI encoded.

Arun Poudel
  • 627
  • 6
  • 21
  • I’m not sure what the body of your `amazonEncode()` method is doing, but I seem to be doing what the documentation says in my `sign()` method, but using the `base64_encode` and `hash_hmac` functions. Can you explain the difference? Also, the documentation you provided is for AWS, not MWS. – Martin Bean Apr 18 '15 at 22:54
  • It's similar, the signature generation for both of them. You will need to URI encode the generated base64, which is almost what my amazonEncode does. – Arun Poudel Apr 18 '15 at 22:58
  • I’ve wrapped the result of my `sign()` method in your `amazonEncode()` function, but I’m still getting `SignatureDoesNotMatch` error. – Martin Bean Apr 18 '15 at 23:08