[Spaces API] How to properly create Authorization header in PHP?

Posted November 11, 2017 6.9k views

The Goal

Hello all, my goal is to eventually use DO Spaces to store images for my application. However, I am trying to create an API to upload a simple text document to try.

The Issue

It seems my Authorization header is failing for some reason. It seems fine to me, but an expert is out there who knows what is going on better than I do.

The API tells me to call a hex() function in the pseudo-code of the signature creation of the API docs (Screenshot Authorization Header Pseudo-code)

But as I pointed out in that screenshot, the final signature shown in the example header is clearly not hex, but a hash. So I am confused there. The API call returns XML with an AccessDenied error. (Would be great if these errors were more specific >.>)

Below is my code, and below that is the header I am sending out. I appreciate any help, thank you. The function that makes the header and signature is getAuthorizationSignature()

public function getAuthorizationSignature(
            $canonicalRequest = "PUT\n$path\n$queryString\n$headersWithValues\n$headersNoValues\n$sha256HashOfBody";

            $stringToSign = "AWS4-HMAC-SHA256" 
                . "\n" 
                . date("Y-m-d") 
                . "\n" 
                . date("Ymd") . "/" . $this->region . "/" . "s3/aws4_request" 
                . "\n" 
                . (hash("sha256", $canonicalRequest));

            $dateKey = hash_hmac("sha256", "AWS4" + $this->spaces_private_key, date("Ymd"));
            $dateRegionKey = hash_hmac("sha256", $dateKey, $this->region);
            $dateRegionServiceKey = hash_hmac("sha256", $dateRegionKey, "s3");
            $signingKey = hash_hmac("sha256", $dateRegionServiceKey, "aws4_request");

            $signature = (hash_hmac("sha256", $signingKey, $stringToSign));

            return $signature;

        public function getSignedHeaderString($headers){
            $tools = new tools();
            $newHeaders = '';
            foreach($headers as $header){
                $newHeaders .= $tools->stripWhitespace(strtolower($header)) . "\n";

            return $newHeaders;

        public function getAuthorizationHeader($httpMethod, $path, $queryString, $headersNoValues, $headers, $sha256HashOfBody){
            $headersWithValues = $this->getSignedHeaderString($headers);
            $headersNoValues = implode(";", $headersNoValues);
            $credential = $this->spaces_access_key . "/" . date("Ymd") . "/" . $this->region . "/s3/aws4_request";
            $authHeader = "Authorization: AWS4-HMAC-SHA256 Credential=$credential, SignedHeaders=$headersNoValues, Signature=" . $this->getAuthorizationSignature($httpMethod, $path, $queryString, $headersNoValues, $headersWithValues, $sha256HashOfBody);

            return [$authHeader];

        public function upload($content, $objectKey, $contentType){

            // I am overriding what I originally passed to this function for testing purposes
            $content = "Text file contents";
            $contentType = "text/plain";
            $objectKey = "text.txt";

            $requestBody = $content;
            $requestMethod = "PUT";
            $contentLength = strlen($content);
            $payloadHash = hash("sha256", $content);

            $requestHeaders = array(
                "Content-Length: $contentLength",
                "x-amz-acl: public-read",
                "x-amz-content-sha256: $payloadHash",
                "Content-Type: $contentType",

            $requestHeaders_NoValues = array(
                "content-length", "x-amz-acl", "content-type", "x-amz-content-sha256"

            $authHeader = $this->getAuthorizationHeader($requestMethod, "/$objectKey", "", $requestHeaders_NoValues, $requestHeaders, $payloadHash);
            $finalHeader = $authHeader + $requestHeaders;

            $curl = curl_init();
            curl_setopt($curl, CURLOPT_URL, $this->host . "/" . $objectKey);
            curl_setopt($curl, CURLOPT_HTTPHEADER, $finalHeader);
            curl_setopt($curl, CURLOPT_POSTFIELDS, $content);
            curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "PUT");
            curl_setopt($curl, CURLOPT_VERBOSE, 1);
            $response = curl_exec($curl);
            return $response;

The header I send out:

    [0] => Authorization: AWS4-HMAC-SHA256 Credential=[Access Key]/20171111/nyc3/s3/aws4_request, SignedHeaders=content-length;content-type;x-amz-acl;x-amz-content-sha256, Signature=0dff50d6f0abccf82320ce03922bbc9cd7596b681098c4d1604a0e24e350231a
    [1] => Content-Length: 18
    [2] => x-amz-acl: public-read
    [3] => x-amz-content-sha256: b3a11fb73200f7f32322eb34639c6fb543c3bab8e0e9b161bc8f7d49b5ca7dc8
    [4] => Content-Type: text/plain
edited by kamaln7
1 comment

These answers are provided by our Community. If you find them useful, show some love by clicking the heart. If you run into issues leave a comment, or add your own answer to help others.

Submit an Answer
3 answers

I am having the same troubles with their Spaces Docs. It seems, as a new product, it is not yet well explained and there aren’t enough source on the Internet to go through this. I am getting a “SignatureDoesNotMatcht” error…there is something that is not quite right..

  • Very sorry to hear that, quite annoying that the docs aren’t very clear on authorization.

    I even tried using an actual client library for signing requests (AWS SV4) and returned that same error.

    If you use a packet inspector (like Fiddler) and see what is going on when you upload to the Spaces in your browser through their control panel, their signature looks nothing like an SV4 signature.

    DO, can we get a better explanation of this API authorization - or just stop using the bulky, useless Amazon method and create a simpler, robust home-brewed authorization.

    • Yeah, I am still trying to get it working - no success yet. The support told me that the UI for changing CORS will come online in some time (not estimated yet)..

      I am desperately trying to change the CORS settings for my Spaces volume so I disallow file crawling bots..

I am having this same issue. I want to be able to access the spaces API to pull file meta data but can’t seem to wrap my head around all the hoops you have to jump through. I basically created a function much like nox7.

I am implemented auth as PoC.
This code work

function send() {
        $body = 'Empty text.';

        $headers = [
            'Content-Length'       => strlen($body),
            'Content-Type'         => 'text/plain',
            'Host'                 => '<SPACE>.<REGION>',
            'x-amz-content-sha256' => hash('sha256', $body),
            'x-amz-date'           => gmdate('Ymd\THis\Z'),
//            'x-amz-acl'            => 'public-read',

        $do = new DOSpaceAuth('<REGION>');
        $auth = $do->sign(
                'method'       => 'PUT',
                'endpoint'     => '/example.txt',
                'query_string' => '',
                'body'         => $body,

        $client = new \GuzzleHttp\Client();
        try {
            $response = $client->put('https://<SPACE>.<REGION>', [
                'headers' => array_merge($headers, ['Authorization' => $auth]),
                'body'    => $body,

            file_put_contents('php://stderr', $response->getStatusCode());
        } catch (\GuzzleHttp\Exception\BadResponseException $exception) {
            file_put_contents('php://stderr', $exception->getMessage());
            file_put_contents('php://stderr', $exception->getRequest()->getBody()->getContents());
class DOSpaceAuth
    const algorithm = 'AWS4-HMAC-SHA256';
    const version = 'AWS4';
    const hash = 'sha256';

    /** @var  string */
    protected $region;

    public function __construct(string $region)
        $this->region = $region;

    public function sign(array $request, string $access, string $secret, array $headers)
        $canonicalHeaders = [];
        foreach ($headers as $header => $value) {
            $header = strtolower(trim($header));
            $canonicalHeaders[$header] = $header . ':' . trim($value);

        $signingHeaders = join(';', array_keys($canonicalHeaders));

        $canonicalRequest = [
            join(PHP_EOL, $canonicalHeaders) . PHP_EOL,
            hash(self::hash, $request['body'] ?: ''),

        $longDate = $headers['x-amz-date'];
        $date = date('Ymd', strtotime($longDate));

        $toSign = sprintf("%s\n%s\n%s/%s/s3/aws4_request\n%s",
            hash(self::hash, join(PHP_EOL, $canonicalRequest))

        // IMPORTANT third argument in this block need be is a true for get raw hash.
        $dateKey = hash_hmac(self::hash, $date, self::version . $secret, true);
        $regionKey = hash_hmac(self::hash, $this->region, $dateKey, true);
        $serviceKey = hash_hmac(self::hash, 's3', $regionKey, true);
        $signingKey = hash_hmac(self::hash, 'aws4_request', $serviceKey, true);

        // For signature need be a hex string
        $signature = hash_hmac(self::hash, $toSign, $signingKey);

        return join(',', [
            sprintf('%s Credential=%s/%s/%s/s3/aws4_request', self::algorithm, $access, $date, $this->region),
            'SignedHeaders=' . $signingHeaders,
            'Signature=' . $signature