AWS Signature (version 4) in Apex

Over the past few days I have been struggling to create the AWS signature version 4 in Apex to send a request via query string, I have followed the step-by-step instructions for signing aws requests, regardless I’m getting the error <Code>IncompleteSignature</Code> <Message>Request must contain a signature that conforms to AWS standards</Message>, I don’t know where I could have done a mistake, here are the methods (in a class called AWS) I’m using to sign:

AWS

public with sharing abstract class AWS {
    public string secret { get; set; }
    public string key { get; set; }

    public Blob createCanonicalRequest(...){...}
    public String getStringToSign(...){...}
    public String signature(...){...}
}

createCanonicalRequest

public Blob createCanonicalRequest(String method, String queryString, Map<String, String> headers, String payload){
    String canonicalRequest = method + '\n';
    canonicalRequest += '/' + '\n';
    canonicalRequest += queryString + '\n';

    List<String> headersList = new List<String>(headers.keySet());
    headersList.sort();

    for (String header : headersList){
        String headerContent = header.toLowerCase() + ':' + headers.get(header).trim();
        canonicalRequest += headerContent + '\n';
    }

    for (String header : headersList){
        canonicalRequest += header.toLowerCase() + ';';
    }

    canonicalRequest = canonicalRequest.removeEnd(';');
    canonicalRequest += '\n';
    canonicalRequest += EncodingUtil.convertToHex(Crypto.generateDigest('SHA256', Blob.valueOf(payload))).toLowerCase();

    return Crypto.generateDigest('SHA256', Blob.valueOf(canonicalRequest));
}

getStringToSign

public String getStringToSign(Blob canonicalRequest, Datetime requestdate, String region, String service){
    String stringToSign = 'AWS4-HMAC-SHA256' + '\n';

    String formatedDate = requestdate.formatGmt('yyyyMMdd') + 'T' + requestdate.formatGmt('hhmmss') + 'Z';
    stringToSign += formatedDate + '\n';
    stringToSign += requestdate.formatGmt('yyyyMMdd') + '/' + region + '/' + service + '/aws4_request\n'; 
    //stringToSign += String.valueOf(canonicalRequest);
    stringToSign += EncodingUtil.base64Encode(canonicalRequest);

    return stringToSign;
}

signature

public String signature(String secret, String today, String region, String service, String stringToSign){
    Blob privateKey = Crypto.generateMac('HmacSHA256', Crypto.generateMac('HmacSHA256', Crypto.generateMac('HmacSHA256', Crypto.generateMac('HmacSHA256', 
                        Blob.valueOf(today), Blob.valueOf('AWS4' + secret)), 
                            Blob.valueOf(region)), 
                                Blob.valueOf(service)), 
                                    Blob.valueOf('aws4_request'));
    return EncodingUtil.convertToHex(Crypto.generateMac('HmacSHA256', Blob.valueOf(stringToSign), privateKey));
}

And I’m using them to create a topic in SNS like this:

public with sharing class SNS extends AWS {
    String region = 'eu-west-1';
    String endpoint = 'http://sns.' + region + '.amazonaws.com';
    String method = 'GET';
    String service = 'sns';

    public SNS(String key, String secret) {
        this.key = key;
        this.secret = secret;
    }

    public HttpResponse createTopic(String topicName){
        HttpRequest request = new HttpRequest();
        request.setMethod(method);
        request.setEndpoint(endpoint);

        request.setBody(getCreateTopicParameters(topicName));

        Http http = new Http();

        try {
            return http.send(request);;
        } catch (System.CalloutException e) {
            throw e;
        }
    }

    private String getCreateTopicParameters(String topicName){
        String body = 'Name=' + topicName;
        body += '&Action=CreateTopic';
        body += '&SignatureMethod=HmacSHA256';
        body += '&AWSAccessKeyId=' + this.key;
        body += '&SignatureVersion=4';

        String signature = getSignature(body);

        body += '&Signature=' + signature;
        return body;
    }

    private String getSignature(String body){
        Datetime now = Datetime.now();
        String today = now.formatGmt('yyyyMMdd');
        Map<String,String> headers = new Map<String,String>();
        headers.put('host',endpoint);

        Blob canonicalRequest = createCanonicalRequest(method, body, headers,'Action=CreateTopic');
        String stringToSign = getStringToSign(canonicalRequest, now, region, service);
        String signature = signature(secret, today, region, service, stringToSign);
        return signature;
    }
}

If someone can point out where I went wrong, I would really appreciate it.

Answer

Here’s a gist that shows the basic framework that I developed. To actually implement a sub-service using this framework, you simply need a couple of extra steps, outlined here:

public class AWSS3_GetService extends AWS {
    public override void init() {
        endpoint = new Url('https://s3.amazonaws.com/');
        resource = '/';
        region = 'us-east-1';
        service = 's3';
        accessKey = 'my-key-here';
        method = HttpMethod.XGET;
        //  You can specify "payload" here if a body is required
        //  payload = Blob.valueof('some-text');

        //  This method helps prevent leaking secret key, 
        //  as it is never serialized
        createSigningKey('my-secret-key-here');
   }
   public String[] getBuckets() {
       HttpResponse response = sendRequest();
       String[] results = new String[0];
       // Read response XML; if we get this far, no exception happened
       // This code was omitted for brevity
       return results;
   }
}

Edit: Your code looks “similar” to mine, but there’s a ton of potential pitfalls. I see you’re not sorting your query string correctly (as far as I can tell), and you’re using the query-string version of the authorization code (which you’ll see in my code is implemented as the Authorization header). The Signature query parameter itself is probably malformed; it took me quite a bit of effort (several days, in fact), to get AWS to accept a valid request from me.

Attribution
Source : Link , Question Author : Jose , Answer Author : sfdcfox

Leave a Comment