'Aws4 sign S3 PUT requests
Writing specialized S3 file upload request signing function that will run on Cloudflare workers (I guess should be the same as in browsers):
let s3PutSign = function(region, keyId, keySecret, contentType, date, bucket, fileName) {
return crypto.subtle.importKey('raw', new TextEncoder().encode(keySecret), { name: 'HMAC', hash: 'SHA-256' }, true, ['sign'])
.then(key => {
let path = `/${bucket}/${fileName}`
let strToSign = `PUT\n\n${contentType}\n${date}\n${path}`
return crypto.subtle.sign('HMAC', key, new TextEncoder().encode(strToSign))
.then(sig => {
return {
url: `https://s3.${region}.amazonaws.com${path}`,
headers: {
'content-type': contentType,
'Authorization': `AWS ${keyId}:${btoa(sig)}`,
'x-amz-date': new Date(new Date().getTime() + 10000).toISOString().replace(/[:\-]|\.\d{3}/g, '').substr(0, 17)
}
}
})
})
}
Wrote function using PUT example: https://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html
Variable strToSign
:
PUT
application/pdf
Wed, 27 May 2020 12:26:33 GMT
/mybucket/file.pdf
function result:
{
url: "https://s3.eu-central-1.amazonaws.com/mybucket/file.pdf",
headers: {
content-type: "application/pdf",
Authorization: "AWS AKXAJE7XIIVXQZ4X7FXQ:W29iamVXZCBBcnJheUJ1ZmZlcl0=",
x-amz-date: "20200527T122643Z"
}
}
Requests always result this response:
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>InvalidRequest</Code>
<Message>The authorization mechanism you have provided is not supported. Please use AWS4-HMAC-SHA256.</Message>
<RequestId>7CECC87D5E855C48</RequestId>
<HostId>rtGLR0u9Qc29bllgKnJf7xD00iQ0+/BZog5G/wYWjsN8tkXio9Baq7GZvbQTD40EVCQ9FzuCo9c=</HostId>
</Error>
Please advise how to debug or give a hint what could be wrong with this function.
Solution 1:[1]
Researching this a bit, it seems that AWS4-HMAC-SHA256
may define a specific hashing algorithm. Looking at this (awesome) gist, the author calls out the full algo name.
You might try replacing your call ({ name: 'HMAC', hash: 'SHA-256' }
) with { name: 'HMAC', hash: 'AWS4-HMAC-SHA256' }
Another thought is to remove the dash (-
) from your algorithm name. Go from SHA-256
to SHA256
and see if that makes a diff.
Solution 2:[2]
I know the post is not new, but maybe it will help someone else. :) Here is a working code example, that I gathered from AWS documentation and some other sources.
// make the call
let signer = new S3Signer(body,'ACCESKEYS','SECRETACCESSKEY','us-east-1','my-test-bucket23.s3.us-east-1.amazonaws.com','/something/SOME/2023/02/some6.pdf','multipart/form-data');
let signerRes = await signer.makeTheCall();
// implementation
const axios = require('axios');
const crypto = require('crypto');
class S3Signer {
constructor(body, accessKey, secretKey, region, host, path, contentType) {
this.region = region; //'us-east-1';
this.host = host; // `my-test-bucket23.s3.us-east-1.amazonaws.com`
this.method = 'PUT';
this.service = 's3';
this.path = path; // `/something/SOME/2023/02/some6.pdf`
this.url = `https://${this.host}${this.path}`;
this.contentType = contentType; //'multipart/form-data';
this.amzDate = null;
this.body = body;
//this.bodyUTF8 = body.toString('utf8');
this.algorithm = 'AWS4-HMAC-SHA256';
this.credentialScope = '';
this.signedHeaders = 'content-type;host;x-amz-content-sha256;x-amz-date';
this.accessKey = accessKey;
this.secretKey = secretKey;
}
getSignatureKey = (key, dateStamp, regionName, serviceName) => {
let kDate = this.hmac(('AWS4' + key), dateStamp);
let kRegion = this.hmac(kDate, regionName);
let kService = this.hmac(kRegion, serviceName);
let kSigning = this.hmac(kService, 'aws4_request');
return kSigning;
}
makeTheCall = async () => {
let canonicalRequest = this.createCanonicalReq();
let signature = this.calculateSignature(canonicalRequest);
// ************* TASK 4: ADD SIGNING INFORMATION TO THE REQUEST *************
// Put the signature information in a header named Authorization.
let authorizationHeader = this.algorithm + ' ' + 'Credential=' + this.accessKey + '/' + this.credentialScope + ', ' + 'SignedHeaders=' + this.signedHeaders + ', ' + 'Signature=' + signature;
// For DynamoDB, the request can include any headers, but MUST include "host", "x-amz-date",
// "x-amz-target", "content-type", and "Authorization". Except for the authorization
// header, the headers must be included in the canonical_headers and signed_headers values, as
// noted earlier. Order here is not significant.
//// Python note: The 'host' header is added automatically by the Python 'requests' library.
let headers = {
'Authorization': authorizationHeader,
'Content-Type': this.contentType,
'X-Amz-Content-Sha256' : this.hash(this.body, 'hex'),
'X-Amz-Date': this.amzDate
}
let request = {
host: this.host,
method: this.method,
url: this.url,
data: this.body,
body: this.body,
path: this.path,
headers: headers
}
// send the file to s3
let res = await axios(request);
console.log(res);
return res;
}
calculateSignature = (canonicalReq) => {
// SHA-256 (recommended)
let dateStamp = this.amzDate.substring(0, 8);
this.credentialScope = dateStamp + '/' + this.region + '/' + this.service + '/' + 'aws4_request'
let stringSign = this.algorithm + '\n' + this.amzDate + '\n' + this.credentialScope + '\n' + this.hash(canonicalReq, 'hex')
// ************* TASK 3: CALCULATE THE SIGNATURE *************
// Create the signing key using the function defined above.
let signingKey = this.getSignatureKey(this.secretKey, dateStamp, this.region, this.service);
// Sign the string_to_sign using the signing_key
let signature = this.hmac(signingKey, stringSign, 'hex');
return signature;
}
createCanonicalReq = () => {
// ************* TASK 1: CREATE A CANONICAL REQUEST *************
// http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
// Step 1 is to define the verb (GET, POST, etc.)--already done.
// Step 2: Create canonical URI--the part of the URI from domain to query
// string (use '/' if no path)
let canonical_uri = this.path;
//// Step 3: Create the canonical query string. In this example, request
// parameters are passed in the body of the request and the query string
// is blank.
let canonical_querystring = '';
///// set the date
let date = new Date();
this.amzDate = date.toISOString().replace(/[:\-]|\.\d{3}/g, '')
// Step 4: Create the canonical headers. Header names must be trimmed
// and lowercase, and sorted in code point order from low to high.
// Note that there is a trailing \n.
let canonical_headers = 'content-type:' + this.contentType + '\n' + 'host:' + this.host + '\n'
+ 'x-amz-content-sha256:' + this.hash(this.body, 'hex') + '\n'
+ 'x-amz-date:' + this.amzDate + '\n'
// Step 5: Create the list of signed headers. This lists the headers
// in the canonical_headers list, delimited with ";" and in alpha order.
// Note: The request can include any headers; canonical_headers and
// signed_headers include those that you want to be included in the
// hash of the request. "Host" and "x-amz-date" are always required.
// For DynamoDB, content-type and x-amz-target are also required.
//this.signedHeaders = 'content-type;host;x-amz-date'
// Step 6: Create payload hash. In this example, the payload (body of
// the request) contains the request parameters.
let payload_hash = this.hash(this.body, 'hex');
// Step 7: Combine elements to create canonical request
let canonicalRequest = this.method + '\n' + canonical_uri + '\n' + canonical_querystring + '\n' + canonical_headers + '\n' + this.signedHeaders + '\n' + payload_hash
return canonicalRequest;
}
hmac = (key, string, encoding) => {
return crypto.createHmac('sha256', key).update(string, 'utf8').digest(encoding)
}
hash = (string, encoding) => {
return crypto.createHash('sha256').update(string, 'utf8').digest(encoding)
}
// This function assumes the string has already been percent encoded
// only if you have reuquest query
encodeRfc3986 = (urlEncodedString) => {
return urlEncodedString.replace(/[!'()*]/g, (c) => {
return '%' + c.charCodeAt(0).toString(16).toUpperCase()
})
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | SeaDude |
Solution 2 | Oguz |