PageRenderTime 71ms CodeModel.GetById 15ms app.highlight 50ms RepoModel.GetById 1ms app.codeStats 0ms

/examples/hmac_api/hmac_api_lib.erl

http://github.com/basho/mochiweb
Erlang | 435 lines | 345 code | 47 blank | 43 comment | 1 complexity | cecdb48516cff4f58b614a29e586afd2 MD5 | raw file
  1-module(hmac_api_lib).
  2
  3-include("hmac_api.hrl").
  4-include_lib("eunit/include/eunit.hrl").
  5
  6-author("Hypernumbers Ltd <gordon@hypernumbers.com>").
  7
  8%%% this library supports the hmac_sha api on both the client-side
  9%%% AND the server-side
 10%%%
 11%%% sign/5 is used client-side to sign a request
 12%%% - it returns an HTTPAuthorization header
 13%%%
 14%%% authorize_request/1 takes a mochiweb Request as an arguement
 15%%% and checks that the request matches the signature
 16%%%
 17%%% get_api_keypair/0 creates a pair of public/private keys
 18%%%
 19%%% THIS LIB DOESN'T IMPLEMENT THE AMAZON API IT ONLY IMPLEMENTS
 20%%% ENOUGH OF IT TO GENERATE A TEST SUITE.
 21%%%
 22%%% THE AMAZON API MUNGES HOSTNAME AND PATHS IN A CUSTOM WAY
 23%%% THIS IMPLEMENTATION DOESN'T
 24-export([
 25         authorize_request/1,
 26         sign/5,
 27         get_api_keypair/0
 28        ]).
 29
 30%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 31%%%                                                                          %%%
 32%%% API                                                                      %%%
 33%%%                                                                          %%%
 34%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 35
 36authorize_request(Req) ->
 37    Method      = Req:get(method),
 38    Path        = Req:get(path),
 39    Headers     = normalise(mochiweb_headers:to_list(Req:get(headers))),
 40    ContentMD5  = get_header(Headers, "content-md5"),
 41    ContentType = get_header(Headers, "content-type"),
 42    Date        = get_header(Headers, "date"),
 43    IncAuth     = get_header(Headers, "authorization"),
 44    {_Schema, _PublicKey, _Sig} = breakout(IncAuth),
 45    %% normally you would use the public key to look up the private key
 46    PrivateKey  = ?privatekey,
 47    Signature = #hmac_signature{method = Method,
 48                                contentmd5 = ContentMD5,
 49                                contenttype = ContentType,
 50                                date = Date,
 51                                headers = Headers,
 52                                resource = Path},
 53    Signed = sign_data(PrivateKey, Signature),
 54    {_, AuthHeader} = make_HTTPAuth_header(Signed),
 55    case AuthHeader of
 56        IncAuth -> "match";
 57        _       -> "no_match"
 58    end.
 59
 60sign(PrivateKey, Method, URL, Headers, ContentType) ->
 61    Headers2 = normalise(Headers),
 62    ContentMD5 = get_header(Headers2, "content-md5"),
 63    Date = get_header(Headers2, "date"),
 64    Signature = #hmac_signature{method = Method,
 65                                contentmd5 = ContentMD5,
 66                                contenttype = ContentType,
 67                                date = Date,
 68                                headers = Headers,
 69                                resource = URL},
 70    SignedSig = sign_data(PrivateKey, Signature),
 71    make_HTTPAuth_header(SignedSig).
 72
 73
 74%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 75%%%                                                                          %%%
 76%%% Internal Functions                                                       %%%
 77%%%                                                                          %%%
 78%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 79
 80breakout(Header) ->
 81    [Schema, Tail] = string:tokens(Header, " "),
 82    [PublicKey, Signature] = string:tokens(Tail, ":"),
 83    {Schema, PublicKey, Signature}.
 84
 85get_api_keypair() ->
 86    Public  = mochihex:to_hex(binary_to_list(crypto:strong_rand_bytes(16))),
 87    Private = mochihex:to_hex(binary_to_list(crypto:strong_rand_bytes(16))),
 88    {Public, Private}.
 89
 90make_HTTPAuth_header(Signature) ->
 91    {"Authorization", ?schema ++ " "
 92     ++ ?publickey ++ ":" ++ Signature}.
 93
 94make_signature_string(#hmac_signature{} = S) ->
 95    Date = get_date(S#hmac_signature.headers, S#hmac_signature.date),
 96    string:to_upper(atom_to_list(S#hmac_signature.method)) ++ "\n"
 97        ++ S#hmac_signature.contentmd5 ++ "\n"
 98        ++ S#hmac_signature.contenttype ++ "\n"
 99        ++ Date ++ "\n"
100        ++ canonicalise_headers(S#hmac_signature.headers)
101        ++ canonicalise_resource(S#hmac_signature.resource).
102
103sign_data(PrivateKey, #hmac_signature{} = Signature) ->
104    Str = make_signature_string(Signature),
105    sign2(PrivateKey, Str).
106
107%% this fn is the entry point for a unit test which is why it is broken out...
108%% if yer encryption and utf8 and base45 doo-dahs don't work then
109%% yer Donald is well and truly Ducked so ye may as weel test it...
110sign2(PrivateKey, Str) ->
111    Sign = xmerl_ucs:to_utf8(Str),
112    binary_to_list(base64:encode(crypto:sha_mac(PrivateKey, Sign))).
113
114canonicalise_headers([]) -> "\n";
115canonicalise_headers(List) when is_list(List) ->
116    List2 = [{string:to_lower(K), V} || {K, V} <- lists:sort(List)],
117    c_headers2(consolidate(List2, []), []).
118
119c_headers2([], Acc)       -> string:join(Acc, "\n") ++ "\n";
120c_headers2([{?headerprefix ++ Rest, Key} | T], Acc) ->
121    Hd = string:strip(?headerprefix ++ Rest) ++ ":" ++ string:strip(Key),
122    c_headers2(T, [Hd | Acc]);
123c_headers2([_H | T], Acc) -> c_headers2(T, Acc).
124
125consolidate([H | []], Acc) -> [H | Acc];
126consolidate([{H, K1}, {H, K2} | Rest], Acc) ->
127    consolidate([{H, join(K1, K2)} | Rest], Acc);
128consolidate([{H1, K1}, {H2, K2} | Rest], Acc) ->
129    consolidate([{rectify(H2), rectify(K2)} | Rest], [{H1, K1} | Acc]).
130
131join(A, B) -> string:strip(A) ++ ";" ++ string:strip(B).
132
133%% removes line spacing as per RFC 2616 Section 4.2
134rectify(String) ->
135    Re = "[\x20* | \t*]+",
136    re:replace(String, Re, " ", [{return, list}, global]).
137
138canonicalise_resource("http://"  ++ Rest) -> c_res2(Rest);
139canonicalise_resource("https://" ++ Rest) -> c_res2(Rest);
140canonicalise_resource(X)                  -> c_res3(X).
141
142c_res2(Rest) ->
143    N = string:str(Rest, "/"),
144    {_, Tail} = lists:split(N, Rest),
145    c_res3("/" ++ Tail).
146
147c_res3(Tail) ->
148    URL = case string:str(Tail, "#") of
149              0 -> Tail;
150              N -> {U, _Anchor} = lists:split(N, Tail),
151                   U
152          end,
153    U3 = case string:str(URL, "?") of
154             0  -> URL;
155             N2 -> {U2, Q} = lists:split(N2, URL),
156                   U2 ++ canonicalise_query(Q)
157         end,
158    string:to_lower(U3).
159
160canonicalise_query(List) ->
161    List1 = string:to_lower(List),
162    List2 = string:tokens(List1, "&"),
163    string:join(lists:sort(List2), "&").
164
165%% if there's a header date take it and ditch the date
166get_date([], Date)            -> Date;
167get_date([{K, _V} | T], Date) -> case string:to_lower(K) of
168                                     ?dateheader -> [];
169                                     _           ->  get_date(T, Date)
170                                 end.
171
172normalise(List) -> norm2(List, []).
173
174norm2([], Acc) -> Acc;
175norm2([{K, V} | T], Acc) when is_atom(K) ->
176    norm2(T, [{string:to_lower(atom_to_list(K)), V} | Acc]);
177norm2([H | T], Acc) -> norm2(T, [H | Acc]).
178
179get_header(Headers, Type) ->
180    case lists:keyfind(Type, 1, Headers) of
181        false   -> [];
182        {_K, V} -> V
183    end.
184
185
186%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
187%%%                                                                          %%%
188%%% Unit Tests                                                               %%%
189%%%                                                                          %%%
190%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
191
192                                                % taken from Amazon docs
193%% http://docs.amazonwebservices.com/AmazonS3/latest/dev/index.html?RESTAuthentication.html
194hash_test1(_) ->
195    Sig = "DELETE\n\n\n\nx-amz-date:Tue, 27 Mar 2007 21:20:26 +0000\n/johnsmith/photos/puppy.jpg",
196    Key = ?privatekey,
197    Hash = sign2(Key, Sig),
198    Expected = "k3nL7gH3+PadhTEVn5Ip83xlYzk=",
199    ?assertEqual(Expected, Hash).
200
201%% taken from Amazon docs
202%% http://docs.amazonwebservices.com/AmazonS3/latest/dev/index.html?RESTAuthentication.html
203hash_test2(_) ->
204    Sig = "GET\n\n\nTue, 27 Mar 2007 19:44:46 +0000\n/johnsmith/?acl",
205    Key = "uV3F3YluFJax1cknvbcGwgjvx4QpvB+leU8dUj2o",
206    Hash = sign2(Key, Sig),
207    Expected = "thdUi9VAkzhkniLj96JIrOPGi0g=",
208    ?assertEqual(Expected, Hash).
209
210%% taken from Amazon docs
211%% http://docs.amazonwebservices.com/AmazonS3/latest/dev/index.html?RESTAuthentication.html
212hash_test3(_) ->
213    Sig = "GET\n\n\nWed, 28 Mar 2007 01:49:49 +0000\n/dictionary/"
214        ++ "fran%C3%A7ais/pr%c3%a9f%c3%a8re",
215    Key = "uV3F3YluFJax1cknvbcGwgjvx4QpvB+leU8dUj2o",
216    Hash = sign2(Key, Sig),
217    Expected = "dxhSBHoI6eVSPcXJqEghlUzZMnY=",
218    ?assertEqual(Expected, Hash).
219
220signature_test1(_) ->
221    URL = "http://example.com:90/tongs/ya/bas",
222    Method = post,
223    ContentMD5 = "",
224    ContentType = "",
225    Date = "Sun, 10 Jul 2011 05:07:19 UTC",
226    Headers = [],
227    Signature = #hmac_signature{method = Method,
228                                contentmd5 = ContentMD5,
229                                contenttype = ContentType,
230                                date = Date,
231                                headers = Headers,
232                                resource = URL},
233    Sig = make_signature_string(Signature),
234    Expected = "POST\n\n\nSun, 10 Jul 2011 05:07:19 UTC\n\n/tongs/ya/bas",
235    ?assertEqual(Expected, Sig).
236
237signature_test2(_) ->
238    URL = "http://example.com:90/tongs/ya/bas",
239    Method = get,
240    ContentMD5 = "",
241    ContentType = "",
242    Date = "Sun, 10 Jul 2011 05:07:19 UTC",
243    Headers = [{"x-amz-acl", "public-read"}],
244    Signature = #hmac_signature{method = Method,
245                                contentmd5 = ContentMD5,
246                                contenttype = ContentType,
247                                date = Date,
248                                headers = Headers,
249                                resource = URL},
250    Sig = make_signature_string(Signature),
251    Expected = "GET\n\n\nSun, 10 Jul 2011 05:07:19 UTC\nx-amz-acl:public-read\n/tongs/ya/bas",
252    ?assertEqual(Expected, Sig).
253
254signature_test3(_) ->
255    URL = "http://example.com:90/tongs/ya/bas",
256    Method = get,
257    ContentMD5 = "",
258    ContentType = "",
259    Date = "Sun, 10 Jul 2011 05:07:19 UTC",
260    Headers = [{"x-amz-acl", "public-read"},
261               {"yantze", "blast-off"},
262               {"x-amz-doobie", "bongwater"},
263               {"x-amz-acl", "public-write"}],
264    Signature = #hmac_signature{method = Method,
265                                contentmd5 = ContentMD5,
266                                contenttype = ContentType,
267                                date = Date,
268                                headers = Headers,
269                                resource = URL},
270    Sig = make_signature_string(Signature),
271    Expected = "GET\n\n\nSun, 10 Jul 2011 05:07:19 UTC\nx-amz-acl:public-read;public-write\nx-amz-doobie:bongwater\n/tongs/ya/bas",
272    ?assertEqual(Expected, Sig).
273
274signature_test4(_) ->
275    URL = "http://example.com:90/tongs/ya/bas",
276    Method = get,
277    ContentMD5 = "",
278    ContentType = "",
279    Date = "Sun, 10 Jul 2011 05:07:19 UTC",
280    Headers = [{"x-amz-acl", "public-read"},
281               {"yantze", "blast-off"},
282               {"x-amz-doobie  oobie \t boobie ", "bongwater"},
283               {"x-amz-acl", "public-write"}],
284    Signature = #hmac_signature{method = Method,
285                                contentmd5 = ContentMD5,
286                                contenttype = ContentType,
287                                date = Date,
288                                headers = Headers,
289                                resource = URL},
290    Sig = make_signature_string(Signature),
291    Expected = "GET\n\n\nSun, 10 Jul 2011 05:07:19 UTC\nx-amz-acl:public-read;public-write\nx-amz-doobie oobie boobie:bongwater\n/tongs/ya/bas",
292    ?assertEqual(Expected, Sig).
293
294signature_test5(_) ->
295    URL = "http://example.com:90/tongs/ya/bas",
296    Method = get,
297    ContentMD5 = "",
298    ContentType = "",
299    Date = "Sun, 10 Jul 2011 05:07:19 UTC",
300    Headers = [{"x-amz-acl", "public-Read"},
301               {"yantze", "Blast-Off"},
302               {"x-amz-doobie  Oobie \t boobie ", "bongwater"},
303               {"x-amz-acl", "public-write"}],
304    Signature = #hmac_signature{method = Method,
305                                contentmd5 = ContentMD5,
306                                contenttype = ContentType,
307                                date = Date,
308                                headers = Headers,
309                                resource = URL},
310    Sig = make_signature_string(Signature),
311    Expected = "GET\n\n\nSun, 10 Jul 2011 05:07:19 UTC\nx-amz-acl:public-Read;public-write\nx-amz-doobie oobie boobie:bongwater\n/tongs/ya/bas",
312    ?assertEqual(Expected, Sig).
313
314signature_test6(_) ->
315    URL = "http://example.com:90/tongs/ya/bas/?andy&zbish=bash&bosh=burp",
316    Method = get,
317    ContentMD5 = "",
318    ContentType = "",
319    Date = "Sun, 10 Jul 2011 05:07:19 UTC",
320    Headers = [],
321    Signature = #hmac_signature{method = Method,
322                                contentmd5 = ContentMD5,
323                                contenttype = ContentType,
324                                date = Date,
325                                headers = Headers,
326                                resource = URL},
327    Sig = make_signature_string(Signature),
328    Expected = "GET\n\n\nSun, 10 Jul 2011 05:07:19 UTC\n\n"
329        ++ "/tongs/ya/bas/?andy&bosh=burp&zbish=bash",
330    ?assertEqual(Expected, Sig).
331
332signature_test7(_) ->
333    URL = "http://exAMPLE.Com:90/tONgs/ya/bas/?ANdy&ZBish=Bash&bOsh=burp",
334    Method = get,
335    ContentMD5 = "",
336    ContentType = "",
337    Date = "Sun, 10 Jul 2011 05:07:19 UTC",
338    Headers = [],
339    Signature = #hmac_signature{method = Method,
340                                contentmd5 = ContentMD5,
341                                contenttype = ContentType,
342                                date = Date,
343                                headers = Headers,
344                                resource = URL},
345    Sig = make_signature_string(Signature),
346    Expected = "GET\n\n\nSun, 10 Jul 2011 05:07:19 UTC\n\n"
347        ++"/tongs/ya/bas/?andy&bosh=burp&zbish=bash",
348    ?assertEqual(Expected, Sig).
349
350signature_test8(_) ->
351    URL = "http://exAMPLE.Com:90/tONgs/ya/bas/?ANdy&ZBish=Bash&bOsh=burp",
352    Method = get,
353    ContentMD5 = "",
354    ContentType = "",
355    Date = "",
356    Headers = [{"x-aMz-daTe", "Tue, 27 Mar 2007 21:20:26 +0000"}],
357    Signature = #hmac_signature{method = Method,
358                                contentmd5 = ContentMD5,
359                                contenttype = ContentType,
360                                date = Date,
361                                headers = Headers,
362                                resource = URL},
363    Sig = make_signature_string(Signature),
364    Expected = "GET\n\n\n\n"
365        ++"x-amz-date:Tue, 27 Mar 2007 21:20:26 +0000\n"
366        ++"/tongs/ya/bas/?andy&bosh=burp&zbish=bash",
367    ?assertEqual(Expected, Sig).
368
369signature_test9(_) ->
370    URL = "http://exAMPLE.Com:90/tONgs/ya/bas/?ANdy&ZBish=Bash&bOsh=burp",
371    Method = get,
372    ContentMD5 = "",
373    ContentType = "",
374    Date = "Sun, 10 Jul 2011 05:07:19 UTC",
375    Headers = [{"x-amz-date", "Tue, 27 Mar 2007 21:20:26 +0000"}],
376    Signature = #hmac_signature{method = Method,
377                                contentmd5 = ContentMD5,
378                                contenttype = ContentType,
379                                date = Date,
380                                headers = Headers,
381                                resource = URL},
382    Sig = make_signature_string(Signature),
383    Expected = "GET\n\n\n\n"
384        ++"x-amz-date:Tue, 27 Mar 2007 21:20:26 +0000\n"
385        ++"/tongs/ya/bas/?andy&bosh=burp&zbish=bash",
386    ?assertEqual(Expected, Sig).
387
388amazon_test1(_) ->
389    URL = "http://exAMPLE.Com:90/johnsmith/photos/puppy.jpg",
390    Method = delete,
391    ContentMD5 = "",
392    ContentType = "",
393    Date = "",
394    Headers = [{"x-amz-date", "Tue, 27 Mar 2007 21:20:26 +0000"}],
395    Signature = #hmac_signature{method = Method,
396                                contentmd5 = ContentMD5,
397                                contenttype = ContentType,
398                                date = Date,
399                                headers = Headers,
400                                resource = URL},
401    Sig = sign_data(?privatekey, Signature),
402    Expected = "k3nL7gH3+PadhTEVn5Ip83xlYzk=",
403    ?assertEqual(Expected, Sig).
404
405unit_test_() ->
406    Setup   = fun() -> ok end,
407    Cleanup = fun(_) -> ok end,
408
409    Series1 = [
410               fun hash_test1/1,
411               fun hash_test2/1,
412               fun hash_test3/1
413              ],
414
415    Series2 = [
416               fun signature_test1/1,
417               fun signature_test2/1,
418               fun signature_test3/1,
419               fun signature_test4/1,
420               fun signature_test5/1,
421               fun signature_test6/1,
422               fun signature_test7/1,
423               fun signature_test8/1,
424               fun signature_test9/1
425              ],
426
427    Series3 = [
428               fun amazon_test1/1
429              ],
430
431    {setup, Setup, Cleanup, [
432                             {with, [], Series1},
433                             {with, [], Series2},
434                             {with, [], Series3}
435                            ]}.