Skip to content

Instantly share code, notes, and snippets.

@anthonyeden
Last active December 5, 2023 09:48
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save anthonyeden/4448695ad531016ec12bcdacc9d91cb8 to your computer and use it in GitHub Desktop.
Save anthonyeden/4448695ad531016ec12bcdacc9d91cb8 to your computer and use it in GitHub Desktop.
AWS S3: Pre-sign Upload & Download Requests [PHP]
<?php
function AWS_S3_PresignDownload($AWSAccessKeyId, $AWSSecretAccessKey, $BucketName, $AWSRegion, $canonical_uri, $expires = 8400) {
// Creates a signed download link for an AWS S3 file
// Based on https://gist.github.com/kelvinmo/d78be66c4f36415a6b80
$encoded_uri = str_replace('%2F', '/', rawurlencode($canonical_uri));
// Specify the hostname for the S3 endpoint
if($AWSRegion == 'us-east-1') {
$hostname = trim($BucketName .".s3.amazonaws.com");
$header_string = "host:" . $hostname . "\n";
$signed_headers_string = "host";
} else {
$hostname = trim($BucketName . ".s3-" . $AWSRegion . ".amazonaws.com");
$header_string = "host:" . $hostname . "\n";
$signed_headers_string = "host";
}
$date_text = gmdate('Ymd', time());
$time_text = $date_text . 'T000000Z';
$algorithm = 'AWS4-HMAC-SHA256';
$scope = $date_text . "/" . $AWSRegion . "/s3/aws4_request";
$x_amz_params = array(
'X-Amz-Algorithm' => $algorithm,
'X-Amz-Credential' => $AWSAccessKeyId . '/' . $scope,
'X-Amz-Date' => $time_text,
'X-Amz-SignedHeaders' => $signed_headers_string
);
if ($expires > 0) {
// 'Expires' is the number of seconds until the request becomes invalid
$x_amz_params['X-Amz-Expires'] = $expires;
}
ksort($x_amz_params);
$query_string = "";
foreach ($x_amz_params as $key => $value) {
$query_string .= rawurlencode($key) . '=' . rawurlencode($value) . "&";
}
$query_string = substr($query_string, 0, -1);
$canonical_request = "GET\n" . $encoded_uri . "\n" . $query_string . "\n" . $header_string . "\n" . $signed_headers_string . "\nUNSIGNED-PAYLOAD";
$string_to_sign = $algorithm . "\n" . $time_text . "\n" . $scope . "\n" . hash('sha256', $canonical_request, false);
$signing_key = hash_hmac('sha256', 'aws4_request', hash_hmac('sha256', 's3', hash_hmac('sha256', $AWSRegion, hash_hmac('sha256', $date_text, 'AWS4' . $AWSSecretAccessKey, true), true), true), true);
$signature = hash_hmac('sha256', $string_to_sign, $signing_key);
return 'https://' . $hostname . $encoded_uri . '?' . $query_string . '&X-Amz-Signature=' . $signature;
}
?>
<?php
function AWS_S3_hmac_sha256($key, $msg, $binary = true) {
return hash_hmac("sha256", $msg, $key, $binary);
}
function AWS_S3_PresignUpload($BucketName, $AWSAccessKeyId, $AWSSecretAccessKey, $AWSRegion, $UploadFilenameStartsWith) {
/* Function to presign an AWS S3 file upload.
This method of uploading can allow clients to securely upload files
directly to S3, while ensuring certian conditions are enforced (e.g. upload filename)
Written by Anthony Eden http://mediarealm.com.au/
*/
$AWSService = "s3";
$AWSRequest = "aws4_request";
$date = date("Ymd");
$AWSPolicy = '{ "expiration": "'.gmdate("Y-m-d", strtotime("tomorrow")).'T12:00:00.000Z",
"conditions": [
{"bucket": "'.$BucketName.'"},
["starts-with", "$key", "'.$UploadFilenameStartsWith.'"],
{"x-amz-server-side-encryption": "AES256"},
{"x-amz-credential": "'.$AWSAccessKeyId.'/'.$date.'/'.$AWSRegion.'/'.$AWSService.'/'.$AWSRequest.'"},
{"x-amz-algorithm": "AWS4-HMAC-SHA256"},
{"x-amz-date": "'.$date.'T000000Z" }
]
}';
$StringToSign = base64_encode($AWSPolicy);
$DateKey = AWS_S3_hmac_sha256("AWS4" . $AWSSecretAccessKey, $date);
$DateRegionKey = AWS_S3_hmac_sha256($DateKey, $AWSRegion);
$DateRegionServiceKey = AWS_S3_hmac_sha256($DateRegionKey, $AWSService);
$SigningKey = AWS_S3_hmac_sha256($DateRegionServiceKey, $AWSRequest);
$Signature = AWS_S3_hmac_sha256($SigningKey, $StringToSign, false);
return array(
"BucketName" => $BucketName,
"KeyPrefix" => $UploadFilenameStartsWith,
"x-amz-server-side-encryption" => "AES256",
"X-Amz-Credential" => $AWSAccessKeyId.'/'.$date.'/'.$AWSRegion.'/'.$AWSService.'/'.$AWSRequest,
"X-Amz-Algorithm" => "AWS4-HMAC-SHA256",
"X-Amz-Date" => $date.'T000000Z',
"Policy" => $StringToSign,
"X-Amz-Signature" => $Signature
);
}
?>
import os
import requests
def uploadS3(srcFilename, S3Upload):
# This method uploads a file to a S3 bucket using pre-signed credentials
# S3Upload is a dictionary returned by AWS_S3_Presign_Upload.php
# Determine the extension of the original file
filename, file_extension = os.path.splitext(srcFilename)
# Perform the upload
r = requests.post(
'http://' + S3Upload['BucketName'] + '.s3.amazonaws.com/',
files = {
'file': open(srcFilename, 'rb')
},
data = {
"key": S3Upload['KeyPrefix'] + file_extension,
"x-amz-server-side-encryption": S3Upload['x-amz-server-side-encryption'],
"X-Amz-Algorithm": S3Upload['X-Amz-Algorithm'],
"X-Amz-Credential": S3Upload['X-Amz-Credential'],
"X-Amz-Date": S3Upload['X-Amz-Date'],
"Policy": S3Upload['Policy'],
"X-Amz-Signature": S3Upload['X-Amz-Signature']
}
)
if r.status_code == 200 or r.status_code == 204:
# Success!
return True
else:
# Debug output
print "ERROR: Cannot upload file to S3", srcFilename
print r.status_code, r.reason
print r.text
return False
@davibennun
Copy link

Thanks a lot!

@umair321
Copy link

umair321 commented Aug 31, 2020

Tried download code but it is not working and saying Access Expired. Have solved it using following code

function AWS_S3_PresignDownload($AWSAccessKeyId, $AWSSecretAccessKey, $BucketName, $AWSRegion, $canonical_uri, $expires = 8400)
{
    $encoded_uri = str_replace('%2F', '/', rawurlencode($canonical_uri));
    // Specify the hostname for the S3 endpoint
    if ($AWSRegion == 'us-east-1') {
        $hostname = trim($BucketName . ".s3.amazonaws.com");
        $header_string = "host:" . $hostname . "\n";
        $signed_headers_string = "host";
    } else {
        $hostname =  trim($BucketName . ".s3-" . $AWSRegion . ".amazonaws.com");
        $header_string = "host:" . $hostname . "\n";
        $signed_headers_string = "host";
    }

    $currentTime = time();
    $date_text = gmdate('Ymd', $currentTime);

    $time_text = $date_text . 'T' . gmdate('His', $currentTime) . 'Z';
    $algorithm = 'AWS4-HMAC-SHA256';
    $scope = $date_text . "/" . $AWSRegion . "/s3/aws4_request";

    $x_amz_params = array(
        'X-Amz-Algorithm' => $algorithm,
        'X-Amz-Credential' => $AWSAccessKeyId . '/' . $scope,
        'X-Amz-Date' => $time_text,
        'X-Amz-SignedHeaders' => $signed_headers_string
    );

    // 'Expires' is the number of seconds until the request becomes invalid
    $x_amz_params['X-Amz-Expires'] = $expires + 30; // 30seocnds are less
    ksort($x_amz_params);

    $query_string = "";
    foreach ($x_amz_params as $key => $value) {
        $query_string .= rawurlencode($key) . '=' . rawurlencode($value) . "&";
    }
    
    $query_string = substr($query_string, 0, -1);

    $canonical_request = "GET\n" . $encoded_uri . "\n" . $query_string . "\n" . $header_string . "\n" . $signed_headers_string . "\nUNSIGNED-PAYLOAD";
    $string_to_sign = $algorithm . "\n" . $time_text . "\n" . $scope . "\n" . hash('sha256', $canonical_request, false);

    $signing_key = hash_hmac('sha256', 'aws4_request', hash_hmac('sha256', 's3', hash_hmac('sha256', $AWSRegion, hash_hmac('sha256', $date_text, 'AWS4' . $AWSSecretAccessKey, true), true), true), true);

    $signature = hash_hmac('sha256', $string_to_sign, $signing_key);
    return 'https://' . $hostname . $encoded_uri . '?' . $query_string . '&X-Amz-Signature=' . $signature;
}

@lazycipher
Copy link

@hmoffatt
Copy link

hmoffatt commented May 10, 2021

As @umair321 noted above the original code has the time hardcoded, so if you set the expiry time < 24 hours it will not work, depending on the time of day when you run it. Also, because it uses the <bucketname>.s3.<region>.amazonaws.com URLs, it does not work if your bucketname contains dots.

@jacobemcken
Copy link

Where did you find the specification for the implementation?

@anthonyeden
Copy link
Author

Where did you find the specification for the implementation?

To be honest @jacobemcken, I cannot remember - it was 6+ years ago! It was likely a mix of Stack Overflow and the official SDKs. I think there's a new version of the protocol out now, and I think you'd be better off using the SDK rather than re-implementing.

@jacobemcken
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment