Hillegass Chapter 28 - code to calculate amazon signature

Discussion in 'Mac Programming' started by John Baughman, Aug 31, 2009.

  1. John Baughman macrumors member

    Joined:
    Oct 27, 2003
    #1
    OK, I give up. After reading the Amazon Web Services docs and trying some code snippets I found on line, I am stumped as to how to get a signed request to work.

    Does anyone have a working example of how to properly prepare a signed request for use in the chapter 28 Web Service lesson?

    No matter what I do I get the following returned error...

    "The request signature we calculated does not match the signature you provided."

    I am using 2 methods I found on line...

    Code:
    - (NSData *)HMACSHA1withKey:(NSString *)key forString:(NSString *)string
    
    - (NSString *)base64forData:(NSData *)data
    
    
    ;

    I am calling them with...

    Code:
    NSString *signature = [self base64forData:[self HMACSHA1withKey:secretAccessKey forString:stringToSign]];
    
    I think that either I am constructing the request incorrectly, or the calculation code is not calculating the signature correctly.

    I notice that the calculated signature I am getting does not appear to be encoded as I have read it should be in that it does not percentage plus and equal signs. I tried doing this with stringByReplacingOccurrencesOfString, but it still did not work.

    I really would like to get this to work so I can complete this chapter and do the challenge.

    thanks,

    John
     
  2. petron macrumors member

    Joined:
    May 22, 2009
    Location:
    Malmo, Sweden
    #2
    Hi John,
    You are still a head of me, I planned to look at it tonight but I am very... very tierd after all day at work and some work in the garden. I will probably start to read the chapter 28 tomorrow.

    I just made a fast search. I do not know if you tried this page:

    http://apisigning.com/service.html

    It seems like there is a chance to go via Amazon API signing service.

    The old way stopped to work 15 of August, bad luck.
    Let me know if you find something.

    /petron
     
  3. John Baughman thread starter macrumors member

    Joined:
    Oct 27, 2003
    #3
    Hi Petron,

    Thanks for the link. I had run across that site but did not pay attention to it. After your post I took a closer look and after questioning why they are asking for my "secret key" after amazon saying I should not give to anyone, I went ahead and signed up for their free account.

    Bottom line it worked great. Now I can finish the chapter. I would still like to learn how to calculate the signature myself. If you figure out how, let me know.

    Thanks,

    John
     
  4. petron macrumors member

    Joined:
    May 22, 2009
    Location:
    Malmo, Sweden
    #4
    I am glad I could help,
    I may start to look at it myself in a few days. Thanks for feedback.
    /petron
     
  5. petron macrumors member

    Joined:
    May 22, 2009
    Location:
    Malmo, Sweden
    #5
    Hello John,
    The access to the Amazon worked for me as well, but I stopped at the same problem you described in another thread.

    The tableView is not called, but the numberOfRowsInTableView method has been called twice. I check if the datasource for the tableView is APPController. It seems to be correct but I probably need to check all the spelling once again. Started to debug it at the midnight so I gave up pretty fast. One thing is clear, I have to make a closer look at the code and relations... I even cleand the project and build it again.

    I did not looked at the signing much but it seems to work when using command line (terminal) openssl with x509 signing utility and using digest SHA1 , I think I can incorporate it into the xcode via the same way as I did for the sqlite interfaces...But it has to wait for a while.
    /petron
     
  6. petron macrumors member

    Joined:
    May 22, 2009
    Location:
    Malmo, Sweden
    #6
    OK, I have found the problem.
    I forgot to call [tableView reloadData]

    See you later John.

    /petron
     
  7. Andy Jensen macrumors newbie

    Joined:
    Sep 12, 2009
    #7
    John,

    I think you are on the right track. I was able to get my Cocoa application to sign its own requests and successfully retrieve results from Amazon. It was a bit tricky, and took some trial and error. A few pointers:

    1. Sort the fields in the query string before signing them. That is, the field "AWSAccessKeyId=xxxxxx" must be first.

    2. % escape all characters in the query string except = and &. I use the following:
    Code:
    (NSString*)CFURLCreateStringByAddingPercentEscapes(
                   NULL,
                   (CFStringRef)stringToEscape,
                   NULL,
                   (CFStringRef)@"!*'();:@+$,/?%#[]",
                   kCFStringEncodingUTF8);
    3. Construct your string to sign by using this format:
    Code:
    [NSString stringWithFormat:@"GET\necs.amazonaws.com\n/onca/xml\n%@",
              canonicalQuery]
    where canonicalQuery is constructed as above in steps 1 and 2.

    4. Be sure to use SHA256, not SHA1 when calculating the HMAC checksum.

    5. % escape the checksum before adding it to your query string:
    Code:
    (NSString*)CFURLCreateStringByAddingPercentEscapes(
                              NULL,
                              (CFStringRef)signature,
                              NULL,
                              (CFStringRef)@"+/=",
                              kCFStringEncodingUTF8);
    6. You must have a "Timestamp=..." in the query string. The time must be close to the current time, or Amazon says it is expired. It must also be in GMT, so I used:

    Code:
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss'Z'"];
    [dateFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"GMT"]];
    NSString *date = [dateFormatter stringFromDate:[NSDate date]];
    The rest should be as per the code in the book.

    I created a helper class, much like Amazon's Java example, to do the signing. I actually compared the url to the result of their Java example until it matched, but I had to strip a trailing CRLF from their signature to make it work. Also, fix the timestamp to a constant string close to the current time for testing. This allows you to compare the signature exactly.

    I used an array to hold my query fields to make them easier to sort:

    Code:
    NSArray *urlFields = [[NSArray alloc] initWithObjects:
                          @"Service=AWSECommerceService",
                          @"Operation=ItemSearch",
                          @"SearchIndex=Books",
                          [NSString stringWithFormat:@"Keywords=%@", searchString],
                          nil];
    I sorted and joined them all at once with:

    Code:
    [[request sortedArrayUsingSelector:@selector(compare:)]
                        componentsJoinedByString:@"&"];
    Finally, I had to modify sample code I found to calculate the HMAC. Here is what I ended up with:

    Code:
    #import <CommonCrypto/CommonHMAC.h>
    
    - (NSData *)HMACforString:(NSString *)string
    {
    	NSData *clearTextData = [string dataUsingEncoding:NSUTF8StringEncoding];
    	
    	uint8_t digest[CC_SHA256_DIGEST_LENGTH] = {0};
    	
    	CCHmacContext hmacContext;
    	CCHmacInit(&hmacContext, kCCHmacAlgSHA256, secretKeyData.bytes, secretKeyData.length);
    	CCHmacUpdate(&hmacContext, clearTextData.bytes, clearTextData.length);
    	CCHmacFinal(&hmacContext, digest);
    	
    	return [NSData dataWithBytes:digest length:CC_SHA256_DIGEST_LENGTH];
    }
    
    secretKeyData is an instance variable in my helper class which was created from my AWS secret key by:

    Code:
    [secretKey dataUsingEncoding:NSUTF8StringEncoding]
     
  8. Pommade macrumors newbie

    Joined:
    Oct 4, 2009
    #8
    My helper class which sign my request

    Hello, here 's my way to complete the "AmaZon" program of the chapter 28.

    Like Andy, i 've decide to create an helper class, named "HMACSHA256", but i 've gave myself one condition:
    - i 've decided that HMACSHA256 would work as a black box with an input and an output; i mean by that i 've decided to corrupt the less possible the code from the book (adding only a method call which calculate the signature in the guenine code), so then HMACSHA256 have a public method which take my non-signed urlString and return a signed and well-formed request.

    So then here 's the modifications ( in bold) i did to the book code:

    Code:
    [I]- (IBAction)fetchBooks:(id)sender
    {
            // the first part of the code, same as the book code.
            ...
    
    	NSString *urlString = [NSString stringWithFormat:
    						   @"http://ecs.amazonaws.com/onca/xml?"
    						   @"Service=AWSECommerceService&"
    						   @"AWSAccessKeyId=%@&"
    						   @"Operation=ItemSearch&"
    						   @"SearchIndex=Books&"
    						   @"Keywords=%@&"
    						   @"Version=2009-01-01",
    						   AWS_ID, searchString];
    	
    [/I][B]	NSString *signedUrlString = [HMACSHA256 getSignedRequest:urlString];
    
    	NSURL *url = [NSURL URLWithString:signedUrlString];
    [/B][I]        // The rest of the code unmodified.
            ...
    
    }
    [/I]
    Now here 's my HMACSHA256 helper class code:

    HMACSHA256.h

    Code:
    [B]#import <Cocoa/Cocoa.h>
    
    
    @interface HMACSHA256 : NSObject {
    }
    
    + (NSData *)HMACforString:(NSString *)string;
    + (NSString *)base64forData:(NSData *)data;
    
    + (NSString *)appendTimestamp:(NSString *)undatedUrlString;
    + (NSString *)sortUrlString:(NSString *)unsortedUrlString;
    + (NSString *)transformServiceAddress:(NSString *)serviceAddress;
    + (NSString *)signUrlString:(NSString *)unsignedUrlString;
    
    + (NSString *)getSignedRequest:(NSString *)datedUnsortedUnsignedUrlString;
    + (NSString *)getSignedRequestTest:(NSString *)datedUnsortedUnsignedUrlString;
    
    @end[/B]
    
    and now the HMACSHA256.m code (most of it has been inspired from Andy's code)

    Code:
    #import "HMACSHA256.h"
    #import <CommonCrypto/CommonHMAC.h>
    
    #define SECRET_KEY @"[I]put your secret key here"[/I]
    
    @implementation HMACSHA256
    
    #pragma mark HMACSHA256 et base64Encodage methodes
    
    // The base64Encodage method has been found online, like Petron.
    // I use the HMACforString method of Andy.
    
    + (NSData *)HMACforString:(NSString *)string
    {
    	NSString *secretKey = SECRET_KEY;
    	NSData *clearTextData = [string dataUsingEncoding:NSUTF8StringEncoding];
    	NSData *secretKeyData = [secretKey dataUsingEncoding:NSUTF8StringEncoding];
    	[secretKey release];
    	
    	uint8_t digest[CC_SHA256_DIGEST_LENGTH] = {0};
    	
    	CCHmacContext hmacContext;
    	CCHmacInit(&hmacContext, kCCHmacAlgSHA256, secretKeyData.bytes, secretKeyData.length);
    	CCHmacUpdate(&hmacContext, clearTextData.bytes, clearTextData.length);
    	CCHmacFinal(&hmacContext, digest);
    	
    	NSData *result = [NSData dataWithBytes:digest length:CC_SHA256_DIGEST_LENGTH];
    	
    	return result;
    }
    
    + (NSString *)base64forData:(NSData *)data
    {
        static const char encodingTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    	
        if ([data length] == 0)
            return @"";
    	
        char *characters = malloc((([data length] + 2) / 3) * 4);
        if (characters == NULL)
            return nil;
        NSUInteger length = 0;
    	
        NSUInteger i = 0;
        while (i < [data length])
        {
            char buffer[3] = {0,0,0};
            short bufferLength = 0;
            while (bufferLength < 3 && i < [data length])
    			buffer[bufferLength++] = ((char *)[data bytes])[i++];
    		
            //  Encode the bytes in the buffer to four characters, including padding "=" characters if necessary.
            characters[length++] = encodingTable[(buffer[0] & 0xFC) >> 2];
            characters[length++] = encodingTable[((buffer[0] & 0x03) << 4) | ((buffer[1] & 0xF0) >> 4)];
            if (bufferLength > 1)
    			characters[length++] = encodingTable[((buffer[1] & 0x0F) << 2) | ((buffer[2] & 0xC0) >> 6)];
            else characters[length++] = '=';
            if (bufferLength > 2)
    			characters[length++] = encodingTable[buffer[2] & 0x3F];
            else characters[length++] = '=';        
        }
    	
    	NSString *encodedString = [[[NSString alloc] initWithBytesNoCopy:characters
    															  length:length
    															encoding:NSASCIIStringEncoding
    														freeWhenDone:YES] autorelease];
    	
        return encodedString;
    }
    
    #pragma mark Arrange the request methods
    
    + (NSString *)appendTimestamp:(NSString *)undatedUrlString
    {
    	NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    	[dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss'Z'"];
    	[dateFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"GMT"]];
    	NSString *date = [dateFormatter stringFromDate:[NSDate date]];
    	NSString *datedUrlString = [undatedUrlString stringByAppendingFormat:@"&Timestamp=%@",date];
    
    	[dateFormatter release];
    	dateFormatter = nil;
    
    	return datedUrlString;
    }
    
    + (NSString *)sortUrlString:(NSString *)unsortedUrlString
    {
    	// Split and conserve the service address from the request.
    	NSArray *unsortedUrlStringArrayWithHTTPAddress = [unsortedUrlString componentsSeparatedByString:@"?"];
    	
    	NSString *serviceAddress =
    		[NSString stringWithFormat:@"%@", [unsortedUrlStringArrayWithHTTPAddress objectAtIndex:0]];
    	
    	NSString *unsortedUrlStringWithoutHTTPAddress =
    		[NSString stringWithFormat:@"%@", [unsortedUrlStringArrayWithHTTPAddress objectAtIndex:1]];
    
    	// Free the unsortedUrlStringArrayWithHTTPAddress array.
    	[unsortedUrlStringArrayWithHTTPAddress release];
    	
    	// % Escape the request string.
    	NSString *escapedUnsortedUrlString = 
    		(NSString *) CFURLCreateStringByAddingPercentEscapes(
    														NULL, (CFStringRef)unsortedUrlStringWithoutHTTPAddress,
    														NULL, (CFStringRef)@"!*'();:@+$,/?%#[]",
    														kCFStringEncodingUTF8); 
    	
    	// Split all items from the requestin an array and sort the array insensitive to the case.
    	NSArray *arrayUrlString = [escapedUnsortedUrlString componentsSeparatedByString:@"&"];
    	arrayUrlString = [arrayUrlString sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
    	
    	// Join all items.
    	int i,itemCount = [arrayUrlString count];
    	
    	NSString *enumString;
    	NSString *appendStringArray;
    	
    	// Find and place the AWSAccessKeyId=XXXX field first.
    	for (enumString in arrayUrlString) {
    		if ([enumString rangeOfString:@"AWSAccessKeyId="].location != NSNotFound) {
    			appendStringArray = [NSString stringWithFormat:@"%@",enumString];
    		}
    	}
    	
    	// Append the other fields to the first field, voiding recopying it a second times.
    	for (i = 0; i < itemCount;i++) {
    		if([[arrayUrlString objectAtIndex:i] rangeOfString:@"AWSAccessKeyId="].location == NSNotFound) {
    			appendStringArray = [appendStringArray stringByAppendingFormat:
    								  @"&%@",[arrayUrlString objectAtIndex:i]];
    		}
    	}
    	
    	// Join the serviceAddress string to the request.
    	appendStringArray = [NSString stringWithFormat:@"%@?%@", serviceAddress, appendStringArray];
    	
    	return appendStringArray;
    }
    
    + (NSString *)transformServiceAddress:(NSString *)serviceAddress
    {
    	// Transform the serviceAddress string to fit for the signature calculation.
    	NSString *tSA = [serviceAddress stringByReplacingOccurrencesOfString:@"http://" withString:@"GET\n"];
    	tSA = [tSA stringByReplacingOccurrencesOfString:@"/onca/xml" withString:@"\n/onca/xml\n"];
    	return tSA;
    }
    
    + (NSString *)signUrlString:(NSString *)unsignedUrlString
    {
    	// Split and conserve the service address from the request.
    	NSArray *splitRequest = [unsignedUrlString componentsSeparatedByString:@"?"];
    	
    	NSString *serviceAddress =
    		[NSString stringWithFormat:@"%@", [splitRequest objectAtIndex:0]];
    	
    	NSString *canonicalRequestForm =
    		[NSString stringWithFormat:@"%@", [splitRequest objectAtIndex:1]];
    	
    	// Free the array.
    	// [splitRequest release];
    	
    	// Getting the canonical form of the service address.
    	NSString *sACanonicalForm = [HMACSHA256 transformServiceAddress:serviceAddress];
    	
    	NSString *fullCanonicalFormRequest = [NSString 
    									 stringWithFormat:@"%@%@", sACanonicalForm, canonicalRequestForm];
    	
    	// Calcul the signature from the canonical request and % escape it.
    	NSString *signatureUncoded = [HMACSHA256 base64forData:[HMACSHA256 HMACforString:fullCanonicalFormRequest]];
    	NSString *signature = (NSString *)CFURLCreateStringByAddingPercentEscapes(
    																			  NULL,(CFStringRef) signatureUncoded,
    																			  NULL, (CFStringRef)@"+/=",
    																			  kCFStringEncodingUTF8);
    	[signature autorelease];
    	
    	// Rejoin service address string, canonical request string and the signature field.
    	NSString *signedUrlString = [NSString stringWithFormat:
    								 @"%@?%@&Signature=%@", serviceAddress, canonicalRequestForm,signature];
    
    	return signedUrlString;
    }
    
    + (NSString *)getSignedRequest:(NSString *)undatedUnsortedUnsignedUrlString
    {
    	// Append "&Timestamp=XXXX" field.
    	NSString *datedUnsortedUnsignedUrlString = [HMACSHA256 appendTimestamp:undatedUnsortedUnsignedUrlString];
    	
    	// Sort and % escape the request fields.
    	NSString *datedSortedUnsignedUrlString = [HMACSHA256 sortUrlString:datedUnsortedUnsignedUrlString];
    	
    	// Calcul and append the "&Signature=XXXX" field to the rest of the request.
    	NSString *datedSortedSignedUrlString = [HMACSHA256 signUrlString:datedSortedUnsignedUrlString];
    	
    	return datedSortedSignedUrlString;
    }
    
    @end
    

    I 've tested this code and it works, but i assum that it 's far from being well optimized and i'm sure that there is lot of memory leaks.
    If any one wish to correct my code, feel free, it 'll help me by the way to improve my knowledge of Cocoa.

    Pommade.
     
  9. Darkroom Guest

    Darkroom

    Joined:
    Dec 15, 2006
    Location:
    Montréal, Canada
    #9
    this is mentioned on books product page (bignerdranch.com under Errata)

     
  10. petron macrumors member

    Joined:
    May 22, 2009
    Location:
    Malmo, Sweden
    #10
    You are right Darkroom...
    If you looked at the beginning of this thread we, mentioned that this has been changed in 15th of August. and there is a link where you can get your own account at AWS.

    /petron
     
  11. Pommade macrumors newbie

    Joined:
    Oct 4, 2009
    #11
    I paraphrase petron and i write that you 're in the truth Darkroom.
    But, like John Baughman, our thread creator, i was frustraded when i tried to implement this exercise and was'nt able to make it work. It literally ate me the fact i 've paid 40€ a book where one whole chapter was no more practicable.

    When i posted a version of an helper class code, my goal was and stil being to prevent avarage joe programmer like me to have headache trying to make this exercise work.

    So, sure this workabout is extrem, but with all the brains here, we can provide a way to go through this chapter at it was originally writen for purpose, and not looking here or there bits of an answer (we hardly understand when we begin programmation on Cocoa).

    Andy Jensen showed us the way, i only proposed a made solution which works on my mini-mac, but which unfortunately is not well optimized and not well memory-leak proof.

    I hardly recommand you (again) to try this piece of code, and i invite you (if you can) to improve it; so that people who learn and practice Cocoa with the Aaron Hillgass book could focus on what really matters in chapter 28, and not be stopped by an unpleasant difficulty.

    Sincerely, me and the other.
     
  12. dave-gallagher macrumors newbie

    Joined:
    Feb 1, 2010
    #12
    Pommade, thank you very much for posting that code. It really helped. :) There were a few errors in it (memory leaks). In the code posted below, I fixed them. Comments were changed a bit. Also, I changed how to use it. Instead of storing the Amazon.com AWS secret key inside of HMACSHA256.m, you pass it from your AppController.m class as an NSString AWS_SECRET like so (inside of fetchBooks):


    AppController.m (incomplete)

    Code:
    #define AWS_SECRET	@"YourAmazonAWSSecretKeyGoesHere"
    
    
    NSString *signedURLString = [HMACSHA256 getSignedRequest:urlString withSecret:AWS_SECRET];
    NSURL *url	= [NSURL URLWithString:signedURLString];
    


    HMACSHA256.h

    Code:
    // http://forums.macrumors.com/archive/index.php/t-776833.html
    
    
    @interface HMACSHA256 : NSObject
    {
    	
    }
    
    #pragma mark Public Interface
    + (NSString *)getSignedRequest:(NSString *)undatedUnsortedUnsignedUrlString
    					withSecret:(NSString *)secretKey;
    
    #pragma mark Helpers for Creation of Signed Amazon.com AWS REST Request
    + (NSString *)appendTimestamp:(NSString *)undatedUrlString;
    + (NSString *)sortUrlString:(NSString *)unsortedUrlString;
    + (NSString *)signUrlString:(NSString *)unsignedUrlString
    				 withSecret:(NSString *)secretKey;
    
    #pragma mark HMAC SHA256 signing, Base64 encoding, and Canonical Transformation
    + (NSData *)HMACforString:(NSString *)string
    			   withSecret:(NSString *)secretKey;
    + (NSString *)base64forData:(NSData *)data;
    + (NSString *)transformServiceAddress:(NSString *)serviceAddress;
    
    @end
    

    HMACSHA256.m

    Code:
    #import "HMACSHA256.h"
    #import <CommonCrypto/CommonHMAC.h>
    
    @implementation HMACSHA256
    
    
    #pragma mark Public Interface
    
    + (NSString *)getSignedRequest:(NSString *)undatedUnsortedUnsignedUrlString
    					withSecret:(NSString *)secretKey
    {
    	// Append "&Timestamp=XXXX" field.
    	NSString *datedUnsortedUnsignedUrlString	= [HMACSHA256 appendTimestamp:undatedUnsortedUnsignedUrlString];
    	
    	// Sort and % escape the request fields.
    	NSString *datedSortedUnsignedUrlString		= [HMACSHA256 sortUrlString:datedUnsortedUnsignedUrlString];
    	
    	// Calc and append the "&Signature=XXXX" field to the rest of the request.
    	NSString *datedSortedSignedUrlString		= [HMACSHA256 signUrlString:datedSortedUnsignedUrlString withSecret:secretKey];
    	
    	return datedSortedSignedUrlString;
    }
    
    
    #pragma mark Helpers for Creation of Signed Amazon.com AWS REST Request
    
    + (NSString *)appendTimestamp:(NSString *)undatedUrlString
    {
    	// Adds a timestamp (now) to the AWS REST request in GMT.
    	
    	NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    	[dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss'Z'"];
    	[dateFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"GMT"]];
    	NSString *date = [dateFormatter stringFromDate:[NSDate date]];
    	NSString *datedUrlString = [undatedUrlString stringByAppendingFormat:@"&Timestamp=%@",date];
    	
    	[dateFormatter release];
    	dateFormatter = nil;
    	
    	return datedUrlString;
    }
    
    + (NSString *)sortUrlString:(NSString *)unsortedUrlString
    {
    	// Sorts request fields by "binary value" (not alphabetically).
    	
    	// Splitting the service address (e.g. "http://ecs.amazonaws.com/onca/xml?") from REST request.
    	NSArray	 *unsortedUrlStringArrayWithHTTPAddress = [unsortedUrlString componentsSeparatedByString:@"?"];
    	NSString *serviceAddress						= [NSString stringWithFormat:@"%@", [unsortedUrlStringArrayWithHTTPAddress objectAtIndex:0]];
    	
    	// Capturing 2nd half of the REST request.  This is what we will sort.
    	NSString *unsortedUrlStringWithoutHTTPAddress	= [NSString stringWithFormat:@"%@", [unsortedUrlStringArrayWithHTTPAddress objectAtIndex:1]];
    	
    	// % escape the 2nd half of the REST request string.
    	NSString *escapedUnsortedUrlString = (NSString *)CFURLCreateStringByAddingPercentEscapes(NULL, (CFStringRef)unsortedUrlStringWithoutHTTPAddress,
    																							  NULL, (CFStringRef)@"!*'();:@+$,/?%#[]",
    																							  kCFStringEncodingUTF8);
    	[escapedUnsortedUrlString autorelease];
    	
    	// Split 2nd half of REST request by "&".
    	NSArray *arrayUrlString = [escapedUnsortedUrlString componentsSeparatedByString:@"&"];
    	
    	// ...then perform a "byte sort" (insensitive to case).
    	arrayUrlString = [arrayUrlString sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
    	
    	// Join all items.
    	int i, itemCount = [arrayUrlString count];
    	
    	NSString *enumString;
    	NSString *appendStringArray;
    	
    	// Find and place the AWSAccessKeyId=XXXX field first.
    	for (enumString in arrayUrlString) {
    		if ([enumString rangeOfString:@"AWSAccessKeyId="].location != NSNotFound) {
    			appendStringArray = [NSString stringWithFormat:@"%@", enumString];
    		}
    	}
    	
    	// Append the other fields to the first field without inserting "AWSAccessKeyId=" a second time.
    	for (i = 0; i < itemCount;i++) {
    		if([[arrayUrlString objectAtIndex:i] rangeOfString:@"AWSAccessKeyId="].location == NSNotFound) {
    			appendStringArray = [appendStringArray stringByAppendingFormat:@"&%@", [arrayUrlString objectAtIndex:i]];
    		}
    	}
    	
    	// Join the |serviceAddress| string to the request.
    	appendStringArray = [NSString stringWithFormat:@"%@?%@", serviceAddress, appendStringArray];
    	
    	return appendStringArray;
    }
    
    + (NSString *)signUrlString:(NSString *)unsignedUrlString
    				 withSecret:(NSString *)secretKey
    {
    	// Splitting the service address (e.g. "http://ecs.amazonaws.com/onca/xml?") from REST request.
    	NSArray  *splitRequest				= [unsignedUrlString componentsSeparatedByString:@"?"];
    	NSString *serviceAddress			= [NSString stringWithFormat:@"%@", [splitRequest objectAtIndex:0]];
    	
    	// Capturing 2nd half of the REST request.  This is what we will sign. 
    	NSString *canonicalRequestForm		= [NSString stringWithFormat:@"%@", [splitRequest objectAtIndex:1]];
    	
    	// Getting the canonical form of the service address.
    	NSString *sACanonicalForm			= [HMACSHA256 transformServiceAddress:serviceAddress];
    	NSString *fullCanonicalFormRequest	= [NSString stringWithFormat:@"%@%@", sACanonicalForm, canonicalRequestForm];
    	
    	// Calculating the signature from the canonical request and % escaping it.
    	NSString *signatureUncoded			= [HMACSHA256 base64forData:[HMACSHA256 HMACforString:fullCanonicalFormRequest withSecret:secretKey]];
    	NSString *signature					= (NSString *)CFURLCreateStringByAddingPercentEscapes(NULL, (CFStringRef) signatureUncoded,
    																							   NULL, (CFStringRef) @"+/=",
    																							   kCFStringEncodingUTF8);
    	[signature autorelease];
    	
    	// Rejoining service address string, canonical request string, and the signature field.
    	NSString *signedUrlString			= [NSString stringWithFormat:@"%@?%@&Signature=%@", serviceAddress, canonicalRequestForm, signature];
    	
    	return signedUrlString;
    }
    
    
    #pragma mark HMAC SHA256 signing, Base64 encoding, and Canonical Transformation
    
    + (NSData *)HMACforString:(NSString *)string
    			   withSecret:(NSString *)secretKey
    {
    	// Returns an NSData created by signing a string w/SHA256 using secretKey.
    	
    	NSData *clearTextData = [string dataUsingEncoding:NSUTF8StringEncoding];
    	NSData *secretKeyData = [secretKey dataUsingEncoding:NSUTF8StringEncoding];
    	
    	uint8_t digest[CC_SHA256_DIGEST_LENGTH] = {0};
    	
    	// CommonCrypto Functions
    	CCHmacContext hmacContext;
    	CCHmacInit(&hmacContext, kCCHmacAlgSHA256, secretKeyData.bytes, secretKeyData.length);
    	CCHmacUpdate(&hmacContext, clearTextData.bytes, clearTextData.length);
    	CCHmacFinal(&hmacContext, digest);
    	
    	NSData *result = [NSData dataWithBytes:digest length:CC_SHA256_DIGEST_LENGTH];
    	
    	return result;
    }
    
    + (NSString *)base64forData:(NSData *)data
    {
    	// Encodes given data into a MIME Base64 string.
    	
    	static const char encodingTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    	
    	if ([data length] == 0)
    		return @"";
    	
    	char *characters = malloc((([data length] + 2) / 3) * 4);
    	if (characters == NULL)
    		return nil;
    	NSUInteger length = 0;
    	
    	NSUInteger i = 0;
    	while (i < [data length])
    	{
    		char buffer[3] = {0,0,0};
    		short bufferLength = 0;
    		while (bufferLength < 3 && i < [data length])
    			buffer[bufferLength++] = ((char *)[data bytes])[i++];
    		
    		// Encode the bytes in the buffer to four characters, including padding "=" characters if necessary.
    		characters[length++] = encodingTable[(buffer[0] & 0xFC) >> 2];
    		characters[length++] = encodingTable[((buffer[0] & 0x03) << 4) | ((buffer[1] & 0xF0) >> 4)];
    		if (bufferLength > 1)
    			characters[length++] = encodingTable[((buffer[1] & 0x0F) << 2) | ((buffer[2] & 0xC0) >> 6)];
    		else characters[length++] = '=';
    		if (bufferLength > 2)
    			characters[length++] = encodingTable[buffer[2] & 0x3F];
    		else characters[length++] = '='; 
    	}
    	
    	NSString *encodedString = [[[NSString alloc] initWithBytesNoCopy:characters
    															  length:length
    															encoding:NSASCIIStringEncoding
    														freeWhenDone:YES] autorelease];
    	
    	return encodedString;
    }
    
    + (NSString *)transformServiceAddress:(NSString *)serviceAddress
    {
    	// Transform the serviceAddress string to fit for the signature calculation.
    	
    	NSString *tSA = [serviceAddress stringByReplacingOccurrencesOfString:@"http://" withString:@"GET\n"];
    	tSA = [tSA stringByReplacingOccurrencesOfString:@"/onca/xml" withString:@"\n/onca/xml\n"];
    	return tSA;
    }
    
    
    @end
    
     
  13. sjames macrumors newbie

    Joined:
    Jun 6, 2010
    #13
    Hi

    Thanks for posting this code I have been able to use it to complete the chapter.

    One point which is not immediately clear,

    You need to use two amazon access keys in the sample code
    1. Access Key ID - Used in the request
    2. Secret Access Key - Used to sign the request and is passed to the helper class provided by John Baug..
     
  14. paul54729 macrumors newbie

    Joined:
    Sep 13, 2010
    #14
    Bad code generation in latest compiler?

    The following line of code fails to find the string for the access key even though my debugging code to display the strings shows the string exists:
    Code:
    		if ([enumString rangeOfString:@"AWSAccessKeyId="].location != NSNotFound) {
    
    (The .location is -1, which is also the value of NSNotFound.)

    As a result, the code posted on this thread doesn't work as advertised.
     
  15. chown33 macrumors 604

    Joined:
    Aug 9, 2009
    #15
    Post compilable runnable example code that demonstrates the problem.

    Also post the exact input data you're using.

    Use a fake access-key and secret-key. If you don't know the exact form, then copy and paste the example access-key and secret-key from Amazon's S3 documentation examples.

    Assuming for the moment that other people have used the code successfully, it's up to you to provide the example that shows a problem. I'm not saying you're wrong, just that without seeing exactly what your code and data actually is, there's no way for anyone else to replicate the problem, much less figure out a solution.


    EDIT:
    The following example code works, demonstrating that the problem does not lie in the code for HMACSHA256.m.

    If I had to guess, I'd guess that you either haven't included an "AWSAccessKeyId" parameter at all, or it has different letter-case, or it wasn't preceded by "&" to delimit it from a prior parameter.

    mainTest.m
    Code:
    #import <Foundation/Foundation.h>
    
    #import "HMACSHA256.h"
    
    // The key values are fake.  
    // Copied from AWS's documentation examples.
    #define AWS_ACCESS_KEY  @"0PN5J17HBGZHT7JJ3X82"
    #define AWS_SECRET	@"/Ml61L9VxlzloZ091/lkqVV5X1/YvaJtI9hW4Wr9"
    
    int main ( int argc, const char * argv[]) 
    {
        NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    
    	NSString * accessKeyID = AWS_ACCESS_KEY;
    	NSString * sdbAction = @"explodensparken";
    
    	// The urlString MUST be in the form:  serviceAddress?p1=v1&p2=v2&..etc..
    	// In particular, there MUST be a single ? delimiter, and one or more & delimiters.
    	NSString * urlString = [NSString stringWithFormat:
    			@"http://example.com/servicepath/?"
    			@"&Version=2009-04-15"
    			@"&SignatureMethod=HmacSHA256&SignatureVersion=2" 
    			@"&AWSAccessKeyId=%@&Action=%@",   // intentionally NOT sorted
    			accessKeyID, sdbAction ];
    
    	NSString *signedURLString = [HMACSHA256 getSignedRequest:urlString withSecret:AWS_SECRET];
    
    	NSLog(@"urlString = %@", urlString);
    	NSLog(@"signed = %@", signedURLString);
    	
        [pool drain];
        return 0;
    }
    Compiled as:
    Code:
    gcc -framework Foundation -std=c99 mainTest.m  HMACSHA256.m
    
    Run as:
    Code:
    ./a.out
    
    Output:
    Code:
    2010-09-25 15:40:36.677 a.out[7022:903]  sortUrlString: http://example.com/servicepath/?&Version=2009-04-15&SignatureMethod=HmacSHA256&SignatureVersion=2&AWSAccessKeyId=0PN5J17HBGZHT7JJ3X82&Action=spitzensparken&Timestamp=2010-09-25T22:40:36Z
    2010-09-25 15:40:36.679 a.out[7022:903] urlString = http://example.com/servicepath/?&Version=2009-04-15&SignatureMethod=HmacSHA256&SignatureVersion=2&AWSAccessKeyId=0PN5J17HBGZHT7JJ3X82&Action=spitzensparken
    2010-09-25 15:40:36.679 a.out[7022:903] signed = http://example.com/servicepath/?AWSAccessKeyId=0PN5J17HBGZHT7JJ3X82&&Action=spitzensparken&SignatureMethod=HmacSHA256&SignatureVersion=2&Timestamp=2010-09-25T22%3A40%3A36Z&Version=2009-04-15&Signature=jyXM7GkudtjVUdIiFT1T1grtWrdl6U7c%2FXtPMTVm9hw%3D
    
     
  16. paul54729 macrumors newbie

    Joined:
    Sep 13, 2010
    #16
    Thanks, chown, for taking the time to look at the problem I was having. The cause of the problem was that I had spelled AWSAccessKeyId with a capital D. It's amazing how one can stare at the problem so long that one becomes blind to the obvious. :)
     

Share This Page