Direct Upload returns AWS error "SignatureDoesNotMatch"

I’ve tried to sanitize any potentially secure info with “XXX” in the below logs…

I first make a request to get an upload location, which seems to go well:

Request Path:  /ingest/stage/upload
Request Head:
{"Content-Type":["application\/json"],"Accept":["application\/json"],"x-api-key":["XXX"],"User-Agent":["GuzzleHttp\/7"],"Host":["api.shotstack.io"]}
Request Body:

Response Head:
{"Date":["Wed, 22 Feb 2023 17:12:30 GMT"],"Content-Type":["application\/json"],"Content-Length":["1414"],"Connection":["keep-alive"],"x-amzn-RequestId":["ab12af6b-b822-4765-b1b8-ff4b9fa2d038"],"access-control-allow-origin":["*"],"x-amz-apigw-id":["XXX"],"X-Amzn-Trace-Id":["Root=1-63f64cfd-4a2f4fb221e89bcd679629bd;Sampled=0"]}
Response Body:
{"data":{"type":"upload","id":"zzyav7kc-1crg-cy34-3wgv-3yo3nw1fet3d","attributes":{"id":"zzyav7kc-1crg-cy34-3wgv-3yo3nw1fet3d","url":"https://shotstack-ingest-api-stage-sources.s3.ap-southeast-2.amazonaws.com/7q5g79h2g8/zzyav7kc-1crg-cy34-3wgv-3yo3nw1fet3d/source?AWSAccessKeyId=XXX&Expires=1677089550&Signature=XXX&x-amz-acl=public-read&x-amz-security-token=XXX","expires":"2023-02-22T18:12:30.185Z"}}}

I then try to upload my test file (which is a 1 second long mp3 file that was generated by GCP Text-To-Speech API):

Request Path:  /7q5g79h2g8/zzyav7kc-1crg-cy34-3wgv-3yo3nw1fet3d/source?AWSAccessKeyId=XXX&Expires=1677089550&Signature=XXX&x-amz-acl=public-read&x-amz-security-token=XXX
Request Head:
{"Content-Type":["audio\/mpeg"],"Content-Length":["4032"],"x-api-key":["XXX"],"User-Agent":["GuzzleHttp\/7"],"Host":["shotstack-ingest-api-stage-sources.s3.ap-southeast-2.amazonaws.com"]}
Request Body:
XXX
Response Head:
{"x-amz-request-id":["XXX"],"x-amz-id-2":["XXX"],"Content-Type":["application\/xml"],"Transfer-Encoding":["chunked"],"Date":["Wed, 22 Feb 2023 17:12:31 GMT"],"Server":["AmazonS3"],"Connection":["close"]}
Response Body:
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message><AWSAccessKeyId>XXX</AWSAccessKeyId><StringToSign>
XXX

Which responds with a generic AWS error message. I presume shotstack’s ingest api is a light wrapper for AWS api calls, but since I called shotstack’s api, I have no idea what I’m supposed to do to resolve an AWS error message. :confused:

Is it something wrong with the staging end-point, or do I have a malformed request?

Things I’ve tied so far:

  • Verifying the POST data.attibute.url is identical to the PUT end-point
    – They are.
  • Removing the extra headers from the PUT request
    – Same result.
  • Using the “v1” end-point instead of “stage”
    – Received forbidden response

Try removing the Content-Type header. That will affect the signature and we determine the content type when the file arrives. We don’t know the content type in advance so do not include it in the signature.

This worked Lucas. Thank you.

To anyone else using PHP and Guzzle. Guzzle REALLY wants to automatically add the Content-Type header to your put requests. Their documentation says you can prevent it by sending [ … ‘headers’ => null … ] in your request, but when PUTting a file, it’ll still automatically add them anyways.

To get around that behavior, you have to add a middleware handler to forcefully remove the Content-Type header from the request, which will look something like this:

// setup middleware to forcefully remove Content-Type header
$stack = \GuzzleHttp\HandlerStack::create();
$stack->setHandler(new \GuzzleHttp\Handler\CurlHandler());
$stack->push(\GuzzleHttp\Middleware::mapRequest(function (\Psr\Http\Message\RequestInterface $request) {
    return $request->withoutHeader('Content-Type');
}));
        
//create client
$connection = new \GuzzleHttp\Client([ 'handler' => $stack ]);

Good to hear and thank you for sharing your PHP solution.