Question

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

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(
            $http_method, 
            $path, 
            $queryStringParams, 
            $headersNoValues, 
            $headersWithValues, 
            $sha256HashOfBody
        ){
            $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);
            sort($headersNoValues);
            $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(
			    "Host: aamc-cdn.nyc3.digitaloceanspaces.com",
			    "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);
            print_r($finalHeader);
            return $response;
		}

The header I send out:

Array
(
    [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
)
Show comments

Submit an answer

This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Sign In or Sign Up to Answer

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.

Want to learn more? Join the DigitalOcean Community!

Join our DigitalOcean community of over a million developers for free! Get help and share knowledge in Q&A, subscribe to topics of interest, and get courses and tools that will help you grow as a developer and scale your project or business.

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…

Hi, 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>.digitaloceanspaces.com',
            '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,
            ],
            'ACCESS_KEY',
            'SECRET_KEY',
            $headers
        );

        $client = new \GuzzleHttp\Client();
        try {
            $response = $client->put('https://<SPACE>.<REGION>.digitaloceanspaces.com/example.txt', [
                '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);
        }

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

        $canonicalRequest = [
            $request['method'],
            $request['endpoint'],
            $request['query_string'],
            join(PHP_EOL, $canonicalHeaders) . PHP_EOL,
            $signingHeaders,
            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",
            self::algorithm,
            $longDate,
            $date,
            $this->region,
            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
        ]);
    }
}

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.