Aws-sdk-php: SignatureDoesNotMatch with presigned URL

Created on 21 Feb 2014  路  30Comments  路  Source: aws/aws-sdk-php

I have the following code

$client = S3Client::factory(array('region' => 'eu-west-1','key' => 'xxx','secret' => 'xxx',));

$command = $client->getCommand('PutObject', array(
    'Bucket' => 'myBucket',
    'Key' => 'testing/signed_'.time(),
    'ContentType' => 'image/jpeg',
    'Body' => 'dump' //it's mandatory, it's not needed here
));
$signedUrl = $command->createPresignedUrl('+5 minutes');
$signedUrl .= '&Content-Type=image%2Fjpeg';
echo("\n\nThe URL is: ". $signedUrl . "\n");
echo("Now run from console for upload:\ncurl -v -H \"Content-Type: image/jpeg\" -T /tmp/temp.jpg '" . $signedUrl . "'");

When trying to use curl command I'm getting and error _SignatureDoesNotMatch_ with message _The request signature we calculated does not match the signature you provided. Check your key and signing method._

The similar code in aws-sdk for Javascript is working, so I thing there is an issue in aws-sdk-php.

Please help me. It's urgent :+1:

Most helpful comment

Just a note that GET, POST, PUT, DELETE, and other HTTP verbs all require a different signature. If you try to use a PUT pre-signed URL to do a POST, it will (and should) fail.

All 30 comments

I see that you are concatenating some text the the $signedUrl variable. This is going to change the actual signature and cause the error because signed URLs have to be in a very specific format. Does removing the concatenation fix the issue?

Thank you for quick response.

I tried to make URL the same like the one generated by aws-sdk so I add Content-Type parameter. It doesn't work in both cases.

I debugged the issue a bit and I see that a ContentMD5 is being added for the body that is specified in the command. This is not the desired behavior for your use case of creating a pre-signed PUT URL, so you'll need to disable the automatic addition of the Content-MD5 header by setting the ContentMD5 option to false.

You also don't need to include '&Content-Type=image%2Fjpeg' in the URL.

$command = $client->getCommand('PutObject', array(
    'Bucket'      => 'myBucket',
    'Key'         => 'testing/signed_'.time(),
    'ContentType' => 'image/jpeg',
    'Body'        => '',
    'ContentMD5'  => false
));

Adding this parameter tells the SDK to not generate a Content-MD5 header for the command. Does this resolve the issue?

Hi.

This resolve the issue. That you for your help.

I had a similar problem; presigned Put request worked even without ContentMD5 => false until two days ago but then it suddenly stopped working. Before finding this solution (which also works) I solved it by making a RequestInterface with $client->put().
Any explanation why this behavior occured only recently and not before?!

I too am testing this functionality using this code

$client = S3Client::factory(array('region' => 'us-east-1','key' => 'xxx','secret' => 'xxx',));
$url = $client->getCommand('PutObject', array(
            'Bucket' => $this->getBucket(),
            'Key' => $uploadKey,
            'ContentType' => 'image/jpeg',
            'Body'        => '',
            'ContentMD5'  => false
        ))->createPresignedUrl('+5 minutes');

Which generates a URL string for me. However, I have tried sending both a POST and a PUT request to this result URL attaching an image file as binary post body data, and I get:

<?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> ...

Am I calculating the URL incorrectly or am I sending the file improperly?

Please note: You no longer need to set ContentMD5 to false.

@fousheezy Can you share the command you're using to send the request to the pre-signed URL? Is it through PHP, cURL, something else?

@LukasRos Is this still an issue for you?

Using the above method for generating the URL.

I am using POSTman (chrome plugin) to make RESTful calls to test if something works. I have tried a POST and a PUT to the resulting presigned URL, attaching the image as binary body data. Ought I use some other way?

Do you know what the HTTP request looks like when it's sent over the wire from POSTman? Is it adding the appropriate content-type?

Chrome Inspected Request sent over network:
_Method_
PUT
_Headers_

Accept:*/*
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en-US,en;q=0.8
Cache-Control:no-cache
Connection:keep-alive
Content-Length:24964
Content-Type:image/jpeg
Host:droparoo-dev-john.s3.amazonaws.com
Origin:chrome-extension://fhbjgbiflinjbdggehcddcbncdddomop
Postman-Token:8fe25ca0-a0be-b3d1-4602-46e5734a48ce
User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36

_Query Params_

AWSAccessKeyId:XXX
Expires:1406834171
Signature:XXX

Where XXX indicates the presumed correct value is present

Just a note that GET, POST, PUT, DELETE, and other HTTP verbs all require a different signature. If you try to use a PUT pre-signed URL to do a POST, it will (and should) fail.

@skyzyx that's a good point, thank you! I tried POSTing to the pre-signed URI out of desperation. I'm expecting to use PUT

@fousheezy Are you using the latest version of the SDK? What does the presigned URL look like (without the actual signature value please)? I ask because we have various integration tests that test pre-signed URLs, and they're working. There may be something funny in your URL that we didn't think about when implementing the pre-signed URL feature.

Hi - I'm having a similar issue where the code below results in a 403 forbidden http response. Any help would be great!

$command = $this->s3->getCommand('PutObject', array(
'Bucket' => $bucket,
'Key' => $remoteName,
'Body' => '',
'ContentType' => 'video/wmv',
'ContentMD5' => false
));
$signedUrl = $command->createPresignedUrl('+5 minutes');
$client = new GuzzleHttp\Client();
$response = GuzzleHttp\put($signedUrl, [
'body' => fopen($filePath, 'r')
]);

@BCJFinlayson Did you try passing a Content-Length header to your Guzzle PUT request?

I tried that with the code below but I was still receiving the 403:

$client = new GuzzleHttp\Client();
$response = GuzzleHttp\put($signedUrl, [
'body' => fopen($filePath, 'r'),
'headers' => ['Content-Length' => filesize($filePath)]
]);

I think I'll try with cURL to see if it may be an issue with the Guzzle client.

Doh! I meant to write Content-Type. Guzzle will automatically insert a Content-Length. Sorry, still early for me :)

Thanks I got it, I just had the wrong ContentType in the S3 command! It's Friday forgive me :).

Ok, glad to hear it's working now.

Hi Everyone,

I'm getting a similar issue, using SDK version 2.16.6.

Here is my code :

$s3Creds = array('key' => AWS_KEY, 'secret' => AWS_SECRET, 'region' => 'ap-northeast-1');
$s3 = S3Client::factory($s3Creds);

//$photo->hashId is a 8 character alpha numeric string
$command = $s3->getCommand('PutObject', array(
    'Bucket'      => S3_BUCKET_NAME,
    'Key'         => $photo->hashId . '/raw.jpg',
    'ContentType' => 'image/jpeg',
    'Body'        => ''
));
$signedUrl = $command->createPresignedUrl('+50 minutes');
echo urldecode($signedUrl);

Then later I'm running the following curl command :

curl -v -T ./image.jpg "<signed url>"

As a result I get a 403 error :

<?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>
...

Forgot to add the Content Type Header on my curl request ...

curl -v -H 'Content-Type: image/jpeg' -T ./image.jpg '<signed url>'

Worked beautifully, I'm ashamed !

Florian

@fousheezy Did you were able to do it with postman?? I have the same problem.

Please note that when creating pre-signed URLs, the headers that are present when a request is signed must also be present when using the pre-signed URL. This means that creating things like pre-signed PUT requests with a content-type require that the actual PUT you send use the same content-type. This is important to keep in mind when using tools like curl, Postman, etc.

@mtdowling The problem was that the user who were signing the url didn't have rights to upload pictures to that bucket. Thanks your comments.

Hi have next error -

Fatal error: Call to undefined method Aws\Command::createPresignedUrl()

with this code -

$s3Creds = array('key' => AWS_KEY, 'secret' => AWS_SECRET, 'region' => 'ap-northeast-1');
$s3 = S3Client::factory($s3Creds);

//$photo->hashId is a 8 character alpha numeric string
$command = $s3->getCommand('PutObject', array(
    'Bucket'      => S3_BUCKET_NAME,
    'Key'         => $photo->hashId . '/raw.jpg',
    'ContentType' => 'image/jpeg',
    'Body'        => ''
));
$signedUrl = $command->createPresignedUrl('+50 minutes');
echo urldecode($signedUrl);

I'm using AWS SDK S3 - Release v2.1.39

@ihorbovkit
You will have to call createPresignedRequest in AWS SDK for PHP 3.x
Are you sure it's really v2.1.39 (which is SDK for Javascript)?

@shooding
Using s3.0.0 v3 - I did the following to get this to work.
$command = $s3->getCommand('GetObject', array(
'Bucket' => $this->customerBucket,
'Key' => $fileName,
'ContentType' => 'image/png',
'ResponseContentDisposition' => 'attachment; filename="'.$fileName.'"'
));
$signedUrl = $s3->createPresignedRequest($command, "+1 week");

In case someone else is looking at this and is in a similar situation as me, I got a similar SignatureDoesNotMatchError when my s3 bucket's CORS Configuration did not contain <AllowedHeader>*</AllowedHeader>

I ran into this when moving from one bucket to another, copying all the settings except for the CORS Configuration.

Struggled with this for a while, was getting the same Signature Does Not Match Error.

The change that fixed my issue was going from

fetch(presignedURL, { method: "PUT", body: svg })

to

fetch(presignedURL, { method: "PUT", body: new Blob([svg], {type: file.type})})

where svg was a variable storing the result of an uploaded svg file.

let reader = new FileReader();
reader.onload = (e) => {
    let svg = atob(e.target.result.split(/[,;]/).pop()); // decode svg file from base64

        // perform fetch
}

I solved this by adding 'Content-Type: text/plain' to my curl command

eg: curl -H 'Content-Type: text/plain' -X PUT -T fgl-template.txt <upload url>

Was this page helpful?
0 / 5 - 0 ratings