AWS S3:
Pre-Sign Uploads & Downloads in PHP

(without the SDK)

AWS S3 is an object-storage service from Amazon Web Services, allowing you to store files online around the world. For all intents and purposes, it’s infinitely scalable and fully managed – meaning you can upload objects without any setup or management burden. Think of it as a turbo-charged, pay-as-you-go FTP server that’s managed for you. There’s plenty of Desktop Clients that support it, plus you can access it programatically using their API or SDK.

Typically you need an access key and secret key to do anything with S3, although you can create pre-signed access credentials to allow someone else to perform a specific action on your behalf. This is called pre-signing.

If you are using S3 with PHP, you may wish create pre-signed upload requests so third-parties can directly upload to your S3 bucket with these special restricted credentials. You may also wish to allow people to directly download files using a similar method.

S3 Pre-Sign Upload Code

This PHP code allows you to create these pre-signed uploads, restricting the user to only uploading specifically named files:

<?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
    );
}

?>

The beauty of this code is that it doesn’t require the AWS SDK for PHP. It’s fully standalone – making it perfect for anyone who just wants to do some pre-signing and nothing else. It’s based on the AWS-provided specification.

Of course, if you want to use the SDK you can still do that. Here’s the official docs.

You can upload directly from a HTML Form in an end-user’s browser, using the required fields.

You could also upload server-side, or in a desktop-app, using a simple Python request:

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

 

S3 Pre-Signed Download Code

You can also pre-sign downloads, allowing others to download one of your files for a limited time. Here’s some standalone PHP code that can create the pre-signed GET URL:

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

}

?>

Get the Broadcast Technology Newsletter

Sign up for the email newsletter about media and technology. Sent irregularly. No spam.

I'm Anthony Eden, and I'm a IT Professional, Broadcast Technician, Software Developer, and Solutions Engineer. I've been working in broadcast media since 2008, and developing software and websites for just as long. Right now, I provide freelance services through Media Realm - in particular, to the media and not-for-profit industries.

Follow Anthony on Twitter: @anthony_eden