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