1: -module(aws_signature_v4_SUITE).
    2: -compile([export_all, nowarn_export_all]).
    3: 
    4: -include_lib("common_test/include/ct.hrl").
    5: -include_lib("eunit/include/eunit.hrl").
    6: 
    7: -define(ACCESS_KEY_ID, "AKIAIAOAONIULXQGMOUA").
    8: -define(SECRET_ACCESS_KEY, "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").
    9: -define(REGION, "eu-west-1").
   10: -define(HOST, "s3-eu-west-1.amazonaws.com").
   11: -define(DATE, {{2014, 1, 2}, {0, 0, 0}}).
   12: -define(BUCKET, "testbucket").
   13: -define(EXPIRATION_TIME, 3600).
   14: 
   15: all() -> [
   16:           signs_basic_url,
   17:           signs_urls_with_additional_headers,
   18:           handles_unicode_path,
   19:           formats_datetime_timestamp,
   20:           pads_datetime_timestamp,
   21:           formats_date_timestamp,
   22:           pads_date_timestamp,
   23:           composes_scope_from_values,
   24:           does_not_encode_unreserved_set,
   25:           does_encode_reserved_set,
   26:           does_encode_utf8_characters
   27:          ].
   28: 
   29: %% Tests
   30: 
   31: %% sign
   32: 
   33: signs_basic_url(Config) ->
   34:     Path = <<"/", ?BUCKET, "/test/key">>,
   35:     Signature = sign(Path, ?config(queries, Config), ?config(headers, Config)),
   36:     ?assertEqual(<<"85bf825e9b67d7838f354b74d97253317584919d8a77b7b50675c2005f003f1f">>,
   37:                  Signature).
   38: 
   39: signs_urls_with_additional_headers(Config) ->
   40:     Headers = maps:put(<<"content-length">>, <<"12">>, ?config(headers, Config)),
   41:     Queries = queries(Headers, ?DATE, ?EXPIRATION_TIME),
   42:     Path = <<"/", ?BUCKET, "/test/key">>,
   43:     Signature = sign(Path, Queries, Headers),
   44:     ?assertEqual(<<"0ac4c8569e7a9e50af1a34db59678b4f23b98ff9734719b2727fe2e971626d72">>,
   45:                  Signature).
   46: 
   47: handles_unicode_path(Config) ->
   48:     Datetime = {{2017, 1, 3}, {14, 47, 32}},
   49:     Queries = queries(headers(), Datetime, 900),
   50:     Path = <<"/♠♣♥♦/test/key"/utf8>>,
   51:     Signature = sign(Path, Queries, ?config(headers, Config), Datetime),
   52:     ?assertEqual(<<"384a2f2a8f9f799abdd633971e48f9e100236d6be25a66513b59ea9dea43e76b">>,
   53:                  Signature).
   54: 
   55: %% datetime_iso8601
   56: 
   57: formats_datetime_timestamp(_Config) ->
   58:     Timestamp = {{2016, 12, 10}, {21, 13, 20}},
   59:     ?assertEqual(<<"20161210T211320Z">>, aws_signature_v4:datetime_iso8601(Timestamp)).
   60: 
   61: pads_datetime_timestamp(_Config) ->
   62:     Timestamp = {{2016, 2, 1}, {1, 3, 0}},
   63:     ?assertEqual(<<"20160201T010300Z">>, aws_signature_v4:datetime_iso8601(Timestamp)).
   64: 
   65: %% date_iso8601
   66: 
   67: formats_date_timestamp(_Config) ->
   68:     Timestamp = {{1234, 12, 31}, {0, 0, 0}},
   69:     ?assertEqual(<<"12341231">>, aws_signature_v4:date_iso8601(Timestamp)).
   70: 
   71: pads_date_timestamp(_Config) ->
   72:     Timestamp = {{1234, 2, 3}, {0, 0, 0}},
   73:     ?assertEqual(<<"12340203">>, aws_signature_v4:date_iso8601(Timestamp)).
   74: 
   75: %% compose_scope
   76: 
   77: composes_scope_from_values(_Config) ->
   78:     Timestamp = {{1234, 2, 3}, {0, 0, 0}},
   79:     Scope = aws_signature_v4:compose_scope(Timestamp, <<"Region">>, <<"Service">>),
   80:     ?assertEqual(<<"12340203/Region/Service/aws4_request">>, Scope).
   81: 
   82: %% uri_encode
   83: 
   84: does_not_encode_unreserved_set(_Config) ->
   85:     Uncoded = <<"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~~">>,
   86:     ?assertEqual(Uncoded, aws_signature_v4:uri_encode(Uncoded)).
   87: 
   88: does_encode_reserved_set(_Config) ->
   89:     Uncoded = <<"!*'();:@&=+$,/?%#[] ">>,
   90:     Encoded = <<"%21%2A%27%28%29%3B%3A%40%26%3D%2B%24%2C%2F%3F%25%23%5B%5D%20">>,
   91:     ?assertEqual(Encoded, aws_signature_v4:uri_encode(Uncoded)).
   92: 
   93: does_encode_utf8_characters(_Config) ->
   94:     Uncoded = <<"zażółć gęślą jaźń"/utf8>>,
   95:     Encoded = <<"za%C5%BC%C3%B3%C5%82%C4%87%20g%C4%99%C5%9Bl%C4%85%20ja%C5%BA%C5%84">>,
   96:     ?assertEqual(Encoded, aws_signature_v4:uri_encode(Uncoded)).
   97: 
   98: %% Fixtures
   99: 
  100: init_per_testcase(_, Config) ->
  101:     [{headers, headers()}, {queries, queries(headers(), ?DATE, ?EXPIRATION_TIME)} | Config].
  102: 
  103: %% Helpers
  104: 
  105: headers() ->
  106:     #{<<"host">> => <<?HOST>>}.
  107: 
  108: queries(Headers, UTCDateTime, ExpirationTime) ->
  109:     Scope = aws_signature_v4:compose_scope(UTCDateTime, <<?REGION>>, <<"s3">>),
  110:     #{
  111:        <<"X-Amz-Algorithm">> => <<"AWS4-HMAC-SHA256">>,
  112:        <<"X-Amz-Credential">> => <<?ACCESS_KEY_ID, "/", Scope/binary>>,
  113:        <<"X-Amz-Date">> => aws_signature_v4:datetime_iso8601(UTCDateTime),
  114:        <<"X-Amz-Expires">> => integer_to_binary(ExpirationTime),
  115:        <<"X-Amz-SignedHeaders">> => join_headers(Headers)
  116:      }.
  117: 
  118: join_headers(Headers) ->
  119:     maps:fold(
  120:       fun
  121:           (Key, _, <<>>) -> Key;
  122:           (Key, _, Acc) -> <<Acc/binary, ";", Key/binary>>
  123:       end, <<>>, Headers).
  124: 
  125: sign(URI, Queries, Headers) ->
  126:     sign(URI, Queries, Headers, ?DATE).
  127: 
  128: sign(URI, Queries, Headers, UTCDateTime) ->
  129:     aws_signature_v4:sign(<<"PUT">>, URI, Queries, Headers, UTCDateTime,
  130:                           <<?REGION>>, <<"s3">>, <<?SECRET_ACCESS_KEY>>).