Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

An example of an Amazon Web Services style HMAC api for mochiweb.

This is designed to make it easy to build API authentication without re-inventing the crypto/schema wheel.

It should enable a common set of client libraries to be built.

Includes:
* complete documentation of:
  - the schema
  - the reference implementation
  - how to create and deploy a custom implementation
* unit tests against the Amazon API documentation
  • Loading branch information...
commit 2d3e7ea64469e91a6a60b1d8c1b6e3fb6fa8588e 1 parent b277802
Vixo authored July 22, 2011
203  examples/hmac_api/README
... ...
@@ -0,0 +1,203 @@
  1
+Introduction
  2
+------------
  3
+
  4
+This example shows how to make an Amazon-style HMAC authentication system for an API with mochiweb.
  5
+
  6
+Purpose
  7
+-------
  8
+
  9
+The purpose of this example is to:
  10
+* make it easy to implement an API in mochiweb
  11
+  - using a proven approach so that 'amateurs' don't have to reinvent crypto
  12
+* make it easy to generate client libraries for that API so that client-side implementors can:
  13
+  - reuse closely related code examples
  14
+  - build compatibility unit tests instead of fiddling around debugging their library against live implementations of the system
  15
+
  16
+Scope
  17
+-----
  18
+
  19
+The scope of this document is:
  20
+* a description of the client-server exchange
  21
+* an reference implementation of
  22
+  - the server-side implementation of the exchange
  23
+  - the client-side implemtation of the exchange
  24
+* developing a custom implementation of an API
  25
+* deploying that implementation to new client-side users to build their client libraries
  26
+
  27
+Contents
  28
+--------
  29
+
  30
+Subsequent sections of this document are:
  31
+* the client-server exchange
  32
+* the reference implementation in this example
  33
+* building a custom implementation
  34
+* deploying a custom implementation
  35
+
  36
+The Client-Server Exchange
  37
+--------------------------
  38
+
  39
+OVERVIEW
  40
+
  41
+This section describes the client-server exchange for an Amazon-style API authentication schema. It has the following characterisics:
  42
+* based on a public key/private key
  43
+* used to authenticate non-SSL api requests
  44
+* not a full once-use schema and is vulnerable to replay attacks within a short time window
  45
+
  46
+TYPE OF API
  47
+
  48
+The api described in this document is:
  49
+* suitable for machine-machine communication
  50
+
  51
+The api described in this document is NOT:
  52
+* an implementation of 2-legged OAUTH
  53
+  - see URL HERE
  54
+* an implementation of 3-legged OAUTH
  55
+
  56
+It is not suitable for use in applications where an end user has to log into a service and piggy-back on top of a keypair security system.
  57
+
  58
+THE CLIENT LIBRARY HERE IS **NOT** AN AMAZON CLIENT LIBRARY. AMAZON DOES FUNKY STUFF WITH HOSTNAMES AND PUSHES THEM ONTO THE URL IN CANONICALISATION! THE CLIENT LIBRARY IS AMAZON-A-LIKE ENOUGH TO USE THE AMAZON DOCOS TO BUILD A TEST SUITE.
  59
+
  60
+STEP 1
  61
+
  62
+The client is issued with a pair of keys, one public, one private, for example:
  63
+* public:  "bcaa49f2a4f7d4f92ac36c8bf66d5bb6"
  64
+* private: "92bc93d6b8aaec1cde772f903e06daf5"
  65
+
  66
+In the Amazon docs these are referred to as:
  67
+* AWSAccessKeyId     (public)
  68
+* AWSSecretAccessKey (private)
  69
+
  70
+These can be generated by the function:
  71
+hmac_api_lib:get_api_keypair/0
  72
+
  73
+This function returns cryptographically strong random numbers using the openSSL crypto library under the covers.
  74
+
  75
+The public key is used as a declaration of identity, "I am bcaa49..."
  76
+
  77
+The private key is never passed over the wire and is used to construct hash client side and server side.
  78
+
  79
+STEP 2
  80
+
  81
+The client prepares their request:
  82
+* url
  83
+* time of request
  84
+* action (GET, POST, etc)
  85
+* type of request (application/json, etc)
  86
+* contents of request
  87
+* etc, etc
  88
+
  89
+These components are then turned into a string called the canonical form.
  90
+
  91
+The HTTP protocol is permissive, it treats different requests as if they were the same. For instance it doesn't care about the order in which headers are sent, and allows the same header to contain multiple values as a list or be specified multiple times as a key, value pair.
  92
+
  93
+Intermediate machines betweeen the client and server MAY pack and repack the HTTP request as long as they don't alter its meaning in a narrow sense. This means that the format of the HTTP request is not guaranteed to be maintained.
  94
+
  95
+The canonical form simply ensures that all the valid ways of making the same request are represented by the same string - irrespective of how this is done.
  96
+
  97
+The canonical form handles POST bodies and query paramters and silently discards anchors in URL's.
  98
+
  99
+A hash of this string is made with the private key.
  100
+
  101
+STEP 3
  102
+
  103
+The client makes the request to the server:
  104
+* the signature is included in the request in the standard HTTPAuthorisation header. (As the Amazon documentation points out this is infelicituous as it is being used for Authentication not Authorization, but hey!).
  105
+
  106
+The Authorization header constructed has the form:
  107
+<scheme name><space><public key><colon><signature>
  108
+
  109
+An Amazon one looks like:
  110
+Authorization: AWS 0PN5J17HBGZHT7JJ3X82:frJIUN8DYpKDtOLCwo//yllqDzg=
  111
+               --- -------------------- ----------------------------
  112
+               sch    public key                 signature
  113
+
  114
+The HTTP request is made.
  115
+
  116
+STEP 4
  117
+
  118
+The request is processes:
  119
+* the server receives the request
  120
+* the server constructs the canonical form from the attributes of the request:
  121
+  - url
  122
+  - date header
  123
+  - action (GET, POST, etc)
  124
+  - content type of request (application/json, etc)
  125
+  - some custom headers
  126
+  - etc, etc
  127
+* the server takes the client's public key from the HTTPAuthorization header and looks up the client's private key
  128
+* the server signs the canonical form with the private key
  129
+* the server compares:
  130
+  - the signature in the request to the signature it has just generated
  131
+  - the time encoded in the request with the server time
  132
+* the request is accepted or denied
  133
+
  134
+The time comparison is 'fuzzy' different server's clocks will be out of synch to a degree, the request may have acquired a time from an intermediate machine along the way, etc, etc. Normally a 'clock skew' time is allowed - in Amazon's case this if 15 minutes.
  135
+
  136
+NOTA BENE: THIS CLOCK SKEW TIME ALLOWS FOR REPLAY ATTACKS WHERE A BAD GUY SIMPLY CAPTURES A REPLAYS TRAFFIC.
  137
+
  138
+EXTENSION
  139
+
  140
+It is possible to extend this schema to prevent replay attacks. The server issues a nonce token (a random string) which is included in the signature. When the server authorizes the request it stores the token and prevents any request with that token (ie a replay) being authorized again.
  141
+
  142
+The client receives its next nonce token in the response to a successful request.
  143
+
  144
+The Reference Implementation In This Example
  145
+--------------------------------------------
  146
+
  147
+The reference implementation used in this example is that described in the Amazon documentation here:
  148
+http://docs.amazonwebservices.com/AmazonS3/latest/dev/index.html?RESTAuthentication.html
  149
+
  150
+The try out the reference implementation:
  151
+* create a new mochiweb project as per the mochiweb README
  152
+  - make app PROJECT=project_name
  153
+* copy hmac_api_lib.erl and hmac_api_client.erl into project_name/src
  154
+* copy hmac_api.hrl into project_name/include
  155
+* edit project_name_web.erl and add a call to hmac_api_lib:authorize_request/1
  156
+
  157
+authorize/request/1 should be called in the loop of project_name_web.erl as per:
  158
+
  159
+    loop(Req, DocRoot) ->
  160
+        Auth = hmac_api_lib:authorize_request(Req),
  161
+        io:format("Auth is ~p~n", [Auth]),
  162
+        "/" ++ Path = Req:get(path),
  163
+        ...
  164
+
  165
+When this is done you are ready to test the api:
  166
+* run 'make' in project_name/ to build the Erlang
  167
+* start the web server with 'start-dev.sh' in project_name/
  168
+
  169
+To test the api run this command from the Erlang shell:
  170
+* hmac_api_client:fire().
  171
+
  172
+The reference implementation uses 5 constants defined in hmac_api.hrl.
  173
+* schema
  174
+* headerprefix
  175
+* dateheader
  176
+* publickey
  177
+* privatekey
  178
+
  179
+Building A Custom Implementation
  180
+--------------------------------
  181
+
  182
+The simplest custom implementation is simply take the existing code and change the values of the following constants:
  183
+* schema
  184
+* headerprefix
  185
+* dateheader
  186
+
  187
+It the API is to be used 'as is' please use the values which are commented out in hmac_api.hrl
  188
+
  189
+Client libraries written in other languges than Erlang can reimplement the test suite in hmac_api_lib.erl.
  190
+
  191
+More sophisticated changes will involve changes to the canonicalisation functions.
  192
+
  193
+Use of a generic schema should make reuse of client libraries easier across different platforms.
  194
+
  195
+Deploying A Custom Implementation
  196
+---------------------------------
  197
+
  198
+When deploying a custom implementation, the server-side code should be released with unit tests so the client-side developer can easily build a robust client.
  199
+
  200
+In addition to that you will need to specify:
  201
+* description of how the API works:
  202
+  - ie the acceptable methods and urls
  203
+  - custom headers and their usage (if appropriate)
43  examples/hmac_api/hmac_api.hrl
... ...
@@ -0,0 +1,43 @@
  1
+-author("Hypernumbers Ltd <gordon@hypernumbers.com>").
  2
+
  3
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  4
+%%%                                                                          %%%
  5
+%%% Reference values for testing against Amazon documents                    %%%
  6
+%%%                                                                          %%%
  7
+%%% These need to be changed in production!                                  %%%
  8
+%%%                                                                          %%%
  9
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  10
+-define(schema, "AWS").
  11
+% defines the prefix for headers to be included in the signature
  12
+-define(headerprefix, $x,$-,$a,$m,$z,$-).
  13
+% defines the date header
  14
+-define(dateheader, "x-amz-date").
  15
+
  16
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  17
+%%%                                                                          %%%
  18
+%%% Default values for defining a generic API                                %%%
  19
+%%%                                                                          %%%
  20
+%%% Only change these if you alter the canonicalisation                      %%%
  21
+%%%                                                                          %%%
  22
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  23
+%-define(schema, "MOCHIAPI").
  24
+%-define(headerprefix, $x,$-,$m,$o,$c,$h,$i,$w,$a,$p,$i,$-).
  25
+%-define(dateheader, "x-mochiapi-date").
  26
+
  27
+% a couple of keys for testing
  28
+% these are taken from the document
  29
+% % http://docs.amazonwebservices.com/AmazonS3/latest/dev/index.html?RESTAuthentication.html
  30
+% they are not valid keys!
  31
+-define(publickey,  "0PN5J17HBGZHT7JJ3X82").
  32
+-define(privatekey, "uV3F3YluFJax1cknvbcGwgjvx4QpvB+leU8dUj2o").
  33
+
  34
+
  35
+-record(hmac_signature,
  36
+        {
  37
+          method,
  38
+          contentmd5,
  39
+          contenttype,
  40
+          date,
  41
+          headers,
  42
+          resource
  43
+         }).
34  examples/hmac_api/hmac_api_client.erl
... ...
@@ -0,0 +1,34 @@
  1
+-module(hmac_api_client).
  2
+
  3
+-export([
  4
+         fire/0
  5
+       ]).
  6
+
  7
+-include("hmac_api.hrl").
  8
+-author("Hypernumbers Ltd <gordon@hypernumbers.com>").
  9
+
  10
+fire() ->
  11
+    URL = "http://127.0.0.1:8080/some/page/yeah/",
  12
+    % Dates SHOULD conform to Section 3.3 of RFC2616
  13
+    % the examples from the RFC are:
  14
+    % Sun, 06 Nov 1994 08:49:37 GMT  ; RFC 822, updated by RFC 1123
  15
+    % Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036
  16
+    % Sun Nov  6 08:49:37 1994       ; ANSI C's asctime() format
  17
+
  18
+    % Dates can be conveniently generated using dh_date.erl
  19
+    % https://github.com/daleharvey/dh_date
  20
+    % which is largely compatible with
  21
+    % http://uk.php.net/date
  22
+
  23
+    % You MIGHT find it convenient to insist on times in UTC only
  24
+    % as it reduces the errors caused by summer time and other
  25
+    % conversion issues
  26
+    Method = post,
  27
+    Headers = [{"content-type", "application/json"},
  28
+               {"date", "Sun, 10 Jul 2011 05:07:19"}],
  29
+    ContentType = "application/json",
  30
+    Body = "blah",
  31
+    HTTPAuthHeader = hmac_api_lib:sign(?privatekey, Method, URL,
  32
+                                       Headers, ContentType),
  33
+    httpc:request(Method, {URL, [HTTPAuthHeader | Headers],
  34
+                           ContentType, Body}, [], []).
440  examples/hmac_api/hmac_api_lib.erl
... ...
@@ -0,0 +1,440 @@
  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
+-define(RFC2116_SP, "\x20").
  9
+-define(RFC2116_HT, "\t"
  10
+
  11
+-compile(export_all).
  12
+
  13
+%%% this library supports the hmac_sha api on both the client-side
  14
+%%% AND the server-side
  15
+%%%
  16
+%%% sign/5 is used client-side to sign a request
  17
+%%% - it returns an HTTPAuthorization header
  18
+%%%
  19
+%%% authorize_request/1 takes a mochiweb Request as an arguement
  20
+%%% and checks that the request matches the signature
  21
+%%%
  22
+%%% get_api_keypair/0 creates a pair of public/private keys
  23
+%%%
  24
+%%% THIS LIB DOESN'T IMPLEMENT THE AMAZON API IT ONLY IMPLEMENTS
  25
+%%% ENOUGH OF IT TO GENERATE A TEST SUITE.
  26
+%%%
  27
+%%% THE AMAZON API MUNGES HOSTNAME AND PATHS IN A CUSTOM WAY
  28
+%%% THIS IMPLEMENTATION DOESN'T
  29
+-export([
  30
+         authorize_request/1,
  31
+         sign/5,
  32
+         get_api_keypair/0
  33
+        ]).
  34
+
  35
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  36
+%%%                                                                          %%%
  37
+%%% API                                                                      %%%
  38
+%%%                                                                          %%%
  39
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  40
+
  41
+authorize_request(Req) ->
  42
+    Method      = Req:get(method),
  43
+    Path        = Req:get(path),
  44
+    Headers     = normalise(mochiweb_headers:to_list(Req:get(headers))),
  45
+    ContentMD5  = get_header(Headers, "content-md5"),
  46
+    ContentType = get_header(Headers, "content-type"),
  47
+    Date        = get_header(Headers, "date"),
  48
+    IncAuth     = get_header(Headers, "authorization"),
  49
+    {_Schema, _PublicKey, _Sig} = breakout(IncAuth),
  50
+    % normally you would use the public key to look up the private key
  51
+    PrivateKey  = ?privatekey,
  52
+    Signature = #hmac_signature{method = Method,
  53
+                                contentmd5 = ContentMD5,
  54
+                                contenttype = ContentType,
  55
+                                date = Date,
  56
+                                headers = Headers,
  57
+                                resource = Path},
  58
+    Signed = sign_data(PrivateKey, Signature),
  59
+    {_, AuthHeader} = make_HTTPAuth_header(Signed),
  60
+    case AuthHeader of
  61
+        IncAuth -> "match";
  62
+        _       -> "no_match"
  63
+    end.
  64
+
  65
+sign(PrivateKey, Method, URL, Headers, ContentType) ->
  66
+    Headers2 = normalise(Headers),
  67
+    ContentMD5 = get_header(Headers2, "content-md5"),
  68
+    Date = get_header(Headers2, "date"),
  69
+    Signature = #hmac_signature{method = Method,
  70
+                                contentmd5 = ContentMD5,
  71
+                                contenttype = ContentType,
  72
+                                date = Date,
  73
+                                headers = Headers,
  74
+                                resource = URL},
  75
+    SignedSig = sign_data(PrivateKey, Signature),
  76
+    make_HTTPAuth_header(SignedSig).
  77
+
  78
+
  79
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  80
+%%%                                                                          %%%
  81
+%%% Internal Functions                                                       %%%
  82
+%%%                                                                          %%%
  83
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  84
+
  85
+breakout(Header) ->
  86
+    [Schema, Tail] = string:tokens(Header, " "),
  87
+    [PublicKey, Signature] = string:tokens(Tail, ":"),
  88
+    {Schema, PublicKey, Signature}.
  89
+
  90
+get_api_keypair() ->
  91
+    Public  = mochihex:to_hex(binary_to_list(crypto:strong_rand_bytes(16))),
  92
+    Private = mochihex:to_hex(binary_to_list(crypto:strong_rand_bytes(16))),
  93
+    {Public, Private}.
  94
+
  95
+make_HTTPAuth_header(Signature) ->
  96
+    {"Authorization", ?schema ++ " "
  97
+     ++ ?publickey ++ ":" ++ Signature}.
  98
+
  99
+make_signature_string(#hmac_signature{} = S) ->
  100
+    Date = get_date(S#hmac_signature.headers, S#hmac_signature.date),
  101
+    string:to_upper(atom_to_list(S#hmac_signature.method)) ++ "\n"
  102
+        ++ S#hmac_signature.contentmd5 ++ "\n"
  103
+        ++ S#hmac_signature.contenttype ++ "\n"
  104
+        ++ Date ++ "\n"
  105
+        ++ canonicalise_headers(S#hmac_signature.headers)
  106
+        ++ canonicalise_resource(S#hmac_signature.resource).
  107
+
  108
+sign_data(PrivateKey, #hmac_signature{} = Signature) ->
  109
+    Str = make_signature_string(Signature),
  110
+    sign2(PrivateKey, Str).
  111
+
  112
+% this fn is the entry point for a unit test which is why it is broken out...
  113
+% if yer encryption and utf8 and base45 doo-dahs don't work then
  114
+% yer Donald is well and truly Ducked so ye may as weel test it...
  115
+sign2(PrivateKey, Str) ->
  116
+    Sign = xmerl_ucs:to_utf8(Str),
  117
+    binary_to_list(base64:encode(crypto:sha_mac(PrivateKey, Sign))).
  118
+
  119
+canonicalise_headers([]) -> "\n";
  120
+canonicalise_headers(List) when is_list(List) ->
  121
+    List2 = [{string:to_lower(K), V} || {K, V} <- lists:sort(List)],
  122
+    c_headers2(consolidate(List2, []), []).
  123
+
  124
+c_headers2([], Acc)       -> string:join(Acc, "\n") ++ "\n";
  125
+c_headers2([{[?headerprefix | Rest], Key} | T], Acc) ->
  126
+    Hd = string:strip([?headerprefix | Rest]) ++ ":" ++ string:strip(Key),
  127
+    c_headers2(T, [Hd | Acc]);
  128
+c_headers2([_H | T], Acc) -> c_headers2(T, Acc).
  129
+
  130
+consolidate([H | []], Acc) -> [H | Acc];
  131
+consolidate([{H, K1}, {H, K2} | Rest], Acc) ->
  132
+    consolidate([{H, join(K1, K2)} | Rest], Acc);
  133
+consolidate([{H1, K1}, {H2, K2} | Rest], Acc) ->
  134
+    consolidate([{rectify(H2), rectify(K2)} | Rest], [{H1, K1} | Acc]).
  135
+
  136
+join(A, B) -> string:strip(A) ++ ";" ++ string:strip(B).
  137
+
  138
+% removes line spacing as per RFC 2616 Section 4.2
  139
+rectify(String) ->
  140
+    Re = "[\x20* | \t*]+",
  141
+    re:replace(String, Re, " ", [{return, list}, global]).
  142
+
  143
+canonicalise_resource("http://"  ++ Rest) -> c_res2(Rest);
  144
+canonicalise_resource("https://" ++ Rest) -> c_res2(Rest);
  145
+canonicalise_resource(X)                  -> c_res3(X).
  146
+
  147
+c_res2(Rest) ->
  148
+    N = string:str(Rest, "/"),
  149
+    {_, Tail} = lists:split(N, Rest),
  150
+    c_res3("/" ++ Tail).
  151
+
  152
+c_res3(Tail) ->
  153
+    URL = case string:str(Tail, "#") of
  154
+              0 -> Tail;
  155
+              N -> {U, _Anchor} = lists:split(N, Tail),
  156
+                   U
  157
+          end,
  158
+    U3 = case string:str(URL, "?") of
  159
+             0  -> URL;
  160
+             N2 -> {U2, Q} = lists:split(N2, URL),
  161
+                   U2 ++ canonicalise_query(Q)
  162
+    end,
  163
+    string:to_lower(U3).
  164
+
  165
+canonicalise_query(List) ->
  166
+    List1 = string:to_lower(List),
  167
+    List2 = string:tokens(List1, "&"),
  168
+    string:join(lists:sort(List2), "&").
  169
+
  170
+%% if there's a header date take it and ditch the date
  171
+get_date([], Date)            -> Date;
  172
+get_date([{K, _V} | T], Date) -> case string:to_lower(K) of
  173
+                                    ?dateheader -> [];
  174
+                                    _           ->  get_date(T, Date)
  175
+                                end.
  176
+
  177
+normalise(List) -> norm2(List, []).
  178
+
  179
+norm2([], Acc) -> Acc;
  180
+norm2([{K, V} | T], Acc) when is_atom(K) ->
  181
+    norm2(T, [{string:to_lower(atom_to_list(K)), V} | Acc]);
  182
+norm2([H | T], Acc) -> norm2(T, [H | Acc]).
  183
+
  184
+get_header(Headers, Type) ->
  185
+    case lists:keyfind(Type, 1, Headers) of
  186
+        false   -> [];
  187
+        {_K, V} -> V
  188
+    end.
  189
+
  190
+
  191
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  192
+%%%                                                                          %%%
  193
+%%% Unit Tests                                                               %%%
  194
+%%%                                                                          %%%
  195
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  196
+
  197
+% taken from Amazon docs
  198
+% http://docs.amazonwebservices.com/AmazonS3/latest/dev/index.html?RESTAuthentication.html
  199
+hash_test1(_) ->
  200
+    Sig = "DELETE\n\n\n\nx-amz-date:Tue, 27 Mar 2007 21:20:26 +0000\n/johnsmith/photos/puppy.jpg",
  201
+    Key = ?privatekey,
  202
+    Hash = sign2(Key, Sig),
  203
+    Expected = "k3nL7gH3+PadhTEVn5Ip83xlYzk=",
  204
+    ?assertEqual(Expected, Hash).
  205
+
  206
+% taken from Amazon docs
  207
+% http://docs.amazonwebservices.com/AmazonS3/latest/dev/index.html?RESTAuthentication.html
  208
+hash_test2(_) ->
  209
+    Sig = "GET\n\n\nTue, 27 Mar 2007 19:44:46 +0000\n/johnsmith/?acl",
  210
+    Key = "uV3F3YluFJax1cknvbcGwgjvx4QpvB+leU8dUj2o",
  211
+    Hash = sign2(Key, Sig),
  212
+    Expected = "thdUi9VAkzhkniLj96JIrOPGi0g=",
  213
+    ?assertEqual(Expected, Hash).
  214
+
  215
+% taken from Amazon docs
  216
+% http://docs.amazonwebservices.com/AmazonS3/latest/dev/index.html?RESTAuthentication.html
  217
+hash_test3(_) ->
  218
+    Sig = "GET\n\n\nWed, 28 Mar 2007 01:49:49 +0000\n/dictionary/"
  219
+        ++ "fran%C3%A7ais/pr%c3%a9f%c3%a8re",
  220
+    Key = "uV3F3YluFJax1cknvbcGwgjvx4QpvB+leU8dUj2o",
  221
+    Hash = sign2(Key, Sig),
  222
+    Expected = "dxhSBHoI6eVSPcXJqEghlUzZMnY=",
  223
+    ?assertEqual(Expected, Hash).
  224
+
  225
+signature_test1(_) ->
  226
+    URL = "http://example.com:90/tongs/ya/bas",
  227
+    Method = post,
  228
+    ContentMD5 = "",
  229
+    ContentType = "",
  230
+    Date = "Sun, 10 Jul 2011 05:07:19 UTC",
  231
+    Headers = [],
  232
+    Signature = #hmac_signature{method = Method,
  233
+                                contentmd5 = ContentMD5,
  234
+                                contenttype = ContentType,
  235
+                                date = Date,
  236
+                                headers = Headers,
  237
+                                resource = URL},
  238
+    Sig = make_signature_string(Signature),
  239
+    Expected = "POST\n\n\nSun, 10 Jul 2011 05:07:19 UTC\n\n/tongs/ya/bas",
  240
+    ?assertEqual(Expected, Sig).
  241
+
  242
+signature_test2(_) ->
  243
+    URL = "http://example.com:90/tongs/ya/bas",
  244
+    Method = get,
  245
+    ContentMD5 = "",
  246
+    ContentType = "",
  247
+    Date = "Sun, 10 Jul 2011 05:07:19 UTC",
  248
+    Headers = [{"x-amz-acl", "public-read"}],
  249
+    Signature = #hmac_signature{method = Method,
  250
+                                contentmd5 = ContentMD5,
  251
+                                contenttype = ContentType,
  252
+                                date = Date,
  253
+                                headers = Headers,
  254
+                                resource = URL},
  255
+    Sig = make_signature_string(Signature),
  256
+    Expected = "GET\n\n\nSun, 10 Jul 2011 05:07:19 UTC\nx-amz-acl:public-read\n/tongs/ya/bas",
  257
+    ?assertEqual(Expected, Sig).
  258
+
  259
+signature_test3(_) ->
  260
+    URL = "http://example.com:90/tongs/ya/bas",
  261
+    Method = get,
  262
+    ContentMD5 = "",
  263
+    ContentType = "",
  264
+    Date = "Sun, 10 Jul 2011 05:07:19 UTC",
  265
+    Headers = [{"x-amz-acl", "public-read"},
  266
+               {"yantze", "blast-off"},
  267
+               {"x-amz-doobie", "bongwater"},
  268
+               {"x-amz-acl", "public-write"}],
  269
+    Signature = #hmac_signature{method = Method,
  270
+                                contentmd5 = ContentMD5,
  271
+                                contenttype = ContentType,
  272
+                                date = Date,
  273
+                                headers = Headers,
  274
+                                resource = URL},
  275
+    Sig = make_signature_string(Signature),
  276
+    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",
  277
+    ?assertEqual(Expected, Sig).
  278
+
  279
+signature_test4(_) ->
  280
+    URL = "http://example.com:90/tongs/ya/bas",
  281
+    Method = get,
  282
+    ContentMD5 = "",
  283
+    ContentType = "",
  284
+    Date = "Sun, 10 Jul 2011 05:07:19 UTC",
  285
+    Headers = [{"x-amz-acl", "public-read"},
  286
+               {"yantze", "blast-off"},
  287
+               {"x-amz-doobie  oobie \t boobie ", "bongwater"},
  288
+               {"x-amz-acl", "public-write"}],
  289
+    Signature = #hmac_signature{method = Method,
  290
+                                contentmd5 = ContentMD5,
  291
+                                contenttype = ContentType,
  292
+                                date = Date,
  293
+                                headers = Headers,
  294
+                                resource = URL},
  295
+    Sig = make_signature_string(Signature),
  296
+    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",
  297
+    ?assertEqual(Expected, Sig).
  298
+
  299
+signature_test5(_) ->
  300
+    URL = "http://example.com:90/tongs/ya/bas",
  301
+    Method = get,
  302
+    ContentMD5 = "",
  303
+    ContentType = "",
  304
+    Date = "Sun, 10 Jul 2011 05:07:19 UTC",
  305
+    Headers = [{"x-amz-acl", "public-Read"},
  306
+               {"yantze", "Blast-Off"},
  307
+               {"x-amz-doobie  Oobie \t boobie ", "bongwater"},
  308
+               {"x-amz-acl", "public-write"}],
  309
+    Signature = #hmac_signature{method = Method,
  310
+                                contentmd5 = ContentMD5,
  311
+                                contenttype = ContentType,
  312
+                                date = Date,
  313
+                                headers = Headers,
  314
+                                resource = URL},
  315
+    Sig = make_signature_string(Signature),
  316
+    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",
  317
+    ?assertEqual(Expected, Sig).
  318
+
  319
+signature_test6(_) ->
  320
+    URL = "http://example.com:90/tongs/ya/bas/?andy&zbish=bash&bosh=burp",
  321
+    Method = get,
  322
+    ContentMD5 = "",
  323
+    ContentType = "",
  324
+    Date = "Sun, 10 Jul 2011 05:07:19 UTC",
  325
+    Headers = [],
  326
+    Signature = #hmac_signature{method = Method,
  327
+                                contentmd5 = ContentMD5,
  328
+                                contenttype = ContentType,
  329
+                                date = Date,
  330
+                                headers = Headers,
  331
+                                resource = URL},
  332
+    Sig = make_signature_string(Signature),
  333
+    Expected = "GET\n\n\nSun, 10 Jul 2011 05:07:19 UTC\n\n"
  334
+        ++ "/tongs/ya/bas/?andy&bosh=burp&zbish=bash",
  335
+    ?assertEqual(Expected, Sig).
  336
+
  337
+signature_test7(_) ->
  338
+    URL = "http://exAMPLE.Com:90/tONgs/ya/bas/?ANdy&ZBish=Bash&bOsh=burp",
  339
+    Method = get,
  340
+    ContentMD5 = "",
  341
+    ContentType = "",
  342
+    Date = "Sun, 10 Jul 2011 05:07:19 UTC",
  343
+    Headers = [],
  344
+    Signature = #hmac_signature{method = Method,
  345
+                                contentmd5 = ContentMD5,
  346
+                                contenttype = ContentType,
  347
+                                date = Date,
  348
+                                headers = Headers,
  349
+                                resource = URL},
  350
+    Sig = make_signature_string(Signature),
  351
+    Expected = "GET\n\n\nSun, 10 Jul 2011 05:07:19 UTC\n\n"
  352
+        ++"/tongs/ya/bas/?andy&bosh=burp&zbish=bash",
  353
+    ?assertEqual(Expected, Sig).
  354
+
  355
+signature_test8(_) ->
  356
+    URL = "http://exAMPLE.Com:90/tONgs/ya/bas/?ANdy&ZBish=Bash&bOsh=burp",
  357
+    Method = get,
  358
+    ContentMD5 = "",
  359
+    ContentType = "",
  360
+    Date = "",
  361
+    Headers = [{"x-aMz-daTe", "Tue, 27 Mar 2007 21:20:26 +0000"}],
  362
+    Signature = #hmac_signature{method = Method,
  363
+                                contentmd5 = ContentMD5,
  364
+                                contenttype = ContentType,
  365
+                                date = Date,
  366
+                                headers = Headers,
  367
+                                resource = URL},
  368
+    Sig = make_signature_string(Signature),
  369
+    Expected = "GET\n\n\n\n"
  370
+        ++"x-amz-date:Tue, 27 Mar 2007 21:20:26 +0000\n"
  371
+        ++"/tongs/ya/bas/?andy&bosh=burp&zbish=bash",
  372
+    ?assertEqual(Expected, Sig).
  373
+
  374
+signature_test9(_) ->
  375
+    URL = "http://exAMPLE.Com:90/tONgs/ya/bas/?ANdy&ZBish=Bash&bOsh=burp",
  376
+    Method = get,
  377
+    ContentMD5 = "",
  378
+    ContentType = "",
  379
+    Date = "Sun, 10 Jul 2011 05:07:19 UTC",
  380
+    Headers = [{"x-amz-date", "Tue, 27 Mar 2007 21:20:26 +0000"}],
  381
+    Signature = #hmac_signature{method = Method,
  382
+                                contentmd5 = ContentMD5,
  383
+                                contenttype = ContentType,
  384
+                                date = Date,
  385
+                                headers = Headers,
  386
+                                resource = URL},
  387
+    Sig = make_signature_string(Signature),
  388
+    Expected = "GET\n\n\n\n"
  389
+        ++"x-amz-date:Tue, 27 Mar 2007 21:20:26 +0000\n"
  390
+        ++"/tongs/ya/bas/?andy&bosh=burp&zbish=bash",
  391
+    ?assertEqual(Expected, Sig).
  392
+
  393
+amazon_test1(_) ->
  394
+    URL = "http://exAMPLE.Com:90/johnsmith/photos/puppy.jpg",
  395
+    Method = delete,
  396
+    ContentMD5 = "",
  397
+    ContentType = "",
  398
+    Date = "",
  399
+    Headers = [{"x-amz-date", "Tue, 27 Mar 2007 21:20:26 +0000"}],
  400
+    Signature = #hmac_signature{method = Method,
  401
+                                contentmd5 = ContentMD5,
  402
+                                contenttype = ContentType,
  403
+                                date = Date,
  404
+                                headers = Headers,
  405
+                                resource = URL},
  406
+    Sig = sign_data(?privatekey, Signature),
  407
+    Expected = "k3nL7gH3+PadhTEVn5Ip83xlYzk=",
  408
+    ?assertEqual(Expected, Sig).
  409
+
  410
+unit_test_() ->
  411
+    Setup   = fun() -> ok end,
  412
+    Cleanup = fun(_) -> ok end,
  413
+
  414
+    Series1 = [
  415
+               fun hash_test1/1,
  416
+               fun hash_test2/1,
  417
+               fun hash_test3/1
  418
+              ],
  419
+
  420
+    Series2 = [
  421
+               fun signature_test1/1,
  422
+               fun signature_test2/1,
  423
+               fun signature_test3/1,
  424
+               fun signature_test4/1,
  425
+               fun signature_test5/1,
  426
+               fun signature_test6/1,
  427
+               fun signature_test7/1,
  428
+               fun signature_test8/1,
  429
+               fun signature_test9/1
  430
+               ],
  431
+
  432
+    Series3 = [
  433
+               fun amazon_test1/1
  434
+               ],
  435
+
  436
+    {setup, Setup, Cleanup, [
  437
+                             {with, [], Series1},
  438
+                             {with, [], Series2},
  439
+                             {with, [], Series3}
  440
+                            ]}.

0 notes on commit 2d3e7ea

Please sign in to comment.
Something went wrong with that request. Please try again.