1

Has anybody worked with creating pre-signed urls for Amazon S3 within APEX along with specifying server side encryption?

Currently I am generating pre-signed urls for uploading and downloading files to the S3 bucket. These urls are consumed by an angular front end and this is working fine.

However we need to ask S3 to encrypt the files.I thought this was pretty simple and involved setting a header in the request. But I am unable to do this. I am getting the dreaded error stating the signature does not match the calculated signature. The following documentation states that SSE-S3 does not work with pre-sgned urls.

http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingServerSideEncryption.html

“You can't enforce whether or not objects are encrypted with SSE-S3 when they are uploaded using pre-signed URLs. “

There is another option of doing using SSE-C. But adding those headers in the signature causes the signature to be incorrect. These are the headers:

x-amz-server-side​-encryption​-customer-algorithm

x-amz-server-side​-encryption​-customer-key

x-amz-server-side​-encryption​-customer-key-MD5

Relevant original code: Setting headers:

Map<string, string> defaults = new Map<string, string>();
            defaults.put('X-Amz-Algorithm', S3Connection.AWS_HEADER_ENCRYPTION_SCHEME);
            defaults.put('X-Amz-Credential', this.accessKey + '/' + this.dateStampYYYYMMDD + '/' + this.awsRegion + '/s3/aws4_request');
            defaults.put('X-Amz-Date', this.dateStampISO); // convert to ISO 8601 format
            defaults.put('X-Amz-Expires', this.expiresSeconds);
            defaults.put('X-Amz-SignedHeaders', 'host');

Calculating S3 canonical string and signature.

public string getRequestString() {

    // ==== CONSTRUCT THE REQUEST STRING FROM THE DATA ====
    // Now construct the canonical request string from these elements.
    string canonicalRequestString = this.httpVerb + '\n'
        + '/' + bucketName + '/' + this.canonicalUri + '\n';

    // Add the query string
    // we assume the elements remain in alphabetical order. TODO: sort just to be sure.
    for (string key : this.canonicalQueryMap.keySet()) {
        canonicalRequestString += awsUriEncode(key) + '=' + awsUriEncode(this.canonicalQueryMap.get(key)) + '&';
    }

    for (string key : this.additionalCanonicalHeaders.keySet()) {
        canonicalRequestString += awsUriEncode(key) + '=' + awsUriEncode(this.additionalCanonicalHeaders.get(key)) + '&';
    }

    // replace the last & with a \n
    canonicalRequestString = canonicalRequestString.left(canonicalRequestString.length() - 1) +

'\n';

    // Add the canonical headers with a linefeed at the end of each
    for (string key : this.canonicalHeaders.keySet()) {
        if(this.canonicalHeaders.get(key) != null) {
            canonicalRequestString += key.toLowerCase() + ':' + this.canonicalHeaders.get(key).trim() + '\n';
        }
    }

    // add an additional linefeed
    canonicalRequestString += '\n';


    // Add signed headers
    // we assume the elements remain in alphabetical order.
    // TODO: sort just to be sure and check that the map isn't empty.
    integer keyNum = 0;
    for (string key : this.signedHeaders) {
        keyNum ++;
        if(keyNum > 1) {
            canonicalRequestString += ';';
        }
        canonicalRequestString += key.toLowerCase();
    }

    // add an additional linefeed
    canonicalRequestString += '\n';


    // Add the final line which specifies that we aren't sure of the file size.
    canonicalRequestString += 'UNSIGNED-PAYLOAD';

    return canonicalRequestString;
}

public string getStringToSign() {
    // ==== CONSTRUCT THE STRING TO SIGN ====
    string stringToSign = S3Connection.AWS_HEADER_ENCRYPTION_SCHEME + '\n'
        + this.dateStampISO + '\n'
        + this.dateStampYYYYMMDD + '/' + this.awsRegion + '/s3/aws4_request' + '\n'
        + EncodingUtil.convertToHex(Crypto.generateDigest(S3Connection.AWS_ENCRYPTION_SCHEME,

blob.valueOf(this.getRequestString()))); return stringToSign; }

public blob getSigningKey() {
    // ==== GENERATE THE SIGNING KEY ====
    Blob dateKey = Crypto.generateMac('hmacSHA256', Blob.valueOf(this.dateStampYYYYMMDD), Blob.valueOf('AWS4' +

this.accessSecret)); Blob dateRegionKey = Crypto.generateMac('hmacSHA256', Blob.valueOf(this.awsRegion), dateKey); Blob dateRegionServiceKey = Crypto.generateMac('hmacSHA256', blob.valueOf(this.awsServiceName), dateRegionKey); Blob signingKey = Crypto.generateMac('hmacSHA256', blob.valueOf('aws4_request'), dateRegionServiceKey);

    return signingKey;
}
public string getSignature() {
    // ==== GENERATE THE SIGNATURE ====
    return this.generateSignature(this.getStringToSign(), this.getSigningKey());
}

public string generateSignature(string stringToSign, blob signingKey) {
    blob signatureBlob = Crypto.generateMac('hmacSHA256', blob.valueOf(stringToSign), signingKey);
    return EncodingUtil.convertToHex(signatureBlob);
}

I assume server side encryption is a pretty general requirement. How do we achieve it in APEX with pre-signed urls? Thanks!

1

I solved the problem. Amazon S3 requires the headers to be in alphabetical order.

All the documentation shows the header as : x-amz-server-side-encryption But when creating the canonical request we need to specify it like this (Camel case). Got me stumped for a while.

defaults.put('X-Amz-Server-Side-Encryption','aws:kms');

or

defaults.put('X-Amz-Server-Side-Encryption','AES256');
  • we have the similar requirement and when I tried below, it gives me invalid signature error. Any suggestions? req.setHeader('X-Amz-Server-Side-Encryption','AES256'); – srikanth pasupuleti Dec 19 '16 at 17:11
  • I'm stuck with the same problem ! i change my CORS configuration to accept specific headers : <ExposeHeader>x-amz-server-side-encryption-customer-algorithm</ExposeHeader> <ExposeHeader>x-amz-server-side-encryption-customer-key</ExposeHeader> <ExposeHeader>x-amz-server-side-encryption-customer-key-MD5</ExposeHeader> I also add my parameters in my request : params.SSECustomerAlgorithm = 'AES256'; params.SSECustomerKey = 'azertyuiop'; /* Strings will be Base-64 encoded on your behalf */ params.SSECustomerKeyMD5 = AWS.util.crypto.md5('azertyuiop'); Did you resolve your problem with aws s3 getSigne – Clément Costa Jan 9 '18 at 20:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.