./ct_report/coverage/aws_signature_v4.COVER.html

1 %%==============================================================================
2 %% Copyright 2016 Erlang Solutions Ltd.
3 %%
4 %% Licensed under the Apache License, Version 2.0 (the "License");
5 %% you may not use this file except in compliance with the License.
6 %% You may obtain a copy of the License at
7 %%
8 %% http://www.apache.org/licenses/LICENSE-2.0
9 %%
10 %% Unless required by applicable law or agreed to in writing, software
11 %% distributed under the License is distributed on an "AS IS" BASIS,
12 %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 %% See the License for the specific language governing permissions and
14 %% limitations under the License.
15 %%==============================================================================
16
17 -module(aws_signature_v4).
18 -author('konrad.zemek@erlang-solutions.com').
19
20 -export([sign/8]).
21 -export([datetime_iso8601/1, date_iso8601/1, compose_scope/3, uri_encode/1]).
22
23 -ignore_xref([date_iso8601/1]).
24
25 %%--------------------------------------------------------------------
26 %% API
27 %%--------------------------------------------------------------------
28
29 %% @doc
30 %% Signs AWS request with version 4 signature according to
31 %% https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
32 %% @end
33 -spec sign(Method :: binary(), URI :: binary(), Queries :: #{binary() => binary()},
34 Headers :: #{binary() => binary()}, UTCDateTime :: calendar:datetime(),
35 Region :: binary(), Service :: binary(), SecretAccessKey :: binary()) ->
36 Signature :: binary().
37 sign(Method, URI, Queries, Headers, UTCDateTime, Region, Service, SecretAccessKey) ->
38 21 CanonicalRequest = create_canonical_request(Method, URI, Queries, Headers),
39 21 StringToSign = create_string_to_sign(UTCDateTime, Region, Service, CanonicalRequest),
40 21 Signature = calculate_signature(UTCDateTime, Region, Service, SecretAccessKey, StringToSign),
41 21 hex(Signature).
42
43
44 %% @doc
45 %% Composes an AWS scope string in the form of `<date>/<region>/<service>/aws4_request'
46 %% @end
47 -spec compose_scope(UTCDateTime :: calendar:datetime(), Region :: binary(), Service :: binary())
48 -> Scope :: binary().
49 compose_scope(UTCDateTime, Region, Service) ->
50 42 <<(date_iso8601(UTCDateTime))/binary, "/", Region/binary, "/",
51 Service/binary, "/", "aws4_request">>.
52
53
54 %% @doc
55 %% Formats timestamp as `[YYYYMMDD]T[hhmmss]Z' (brackets added for readability).
56 %% @end
57 -spec datetime_iso8601(UTCDateTime :: calendar:datetime()) -> ISO8601DateTime :: binary().
58 datetime_iso8601(UTCDateTime) ->
59 42 {_, {H, M, S}} = UTCDateTime,
60 42 DateComponent = date_iso8601(UTCDateTime),
61 42 TimeComponent = list_to_binary(io_lib:format("~2..0B~2..0B~2..0B", [H, M, S])),
62 42 <<DateComponent/binary, "T", TimeComponent/binary, "Z">>.
63
64
65 %% @doc
66 %% Formats timestamp as `YYYYMMDD'.
67 %% @end
68 -spec date_iso8601(UTCDateTime :: calendar:datetime()) -> ISO8601Date :: binary().
69 date_iso8601(UTCDateTime) ->
70 105 {{Y, MM, D}, _} = UTCDateTime,
71 105 Str = io_lib:format("~B~2..0B~2..0B", [Y, MM, D]),
72 105 list_to_binary(Str).
73
74
75 %% @doc
76 %% Encodes data according to RFC 3986. Handles utf-8 encoded data, as opposed
77 %% to http_uri:encode/1 .
78 %% @end
79 -spec uri_encode(Data :: binary()) -> URIEncodedData :: binary().
80 uri_encode(Data) ->
81 483 uri_encode(Data, true).
82
83 %%--------------------------------------------------------------------
84 %% Signing steps
85 %%--------------------------------------------------------------------
86
87 %% Task 1: Create a Canonical Request
88 %% https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html#canonical-request
89 %% With a caveat that payload is unknown and thus unsigned, as in
90 %% https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
91 -spec create_canonical_request(Method :: binary(), URI :: binary(),
92 Queries :: #{binary() => binary()},
93 Headers :: #{binary() => binary()}) ->
94 CanonicalRequest :: binary().
95 create_canonical_request(Method, URI, Queries, Headers) ->
96 21 <<
97 Method/binary, "\n",
98 (uri_encode(URI, false))/binary, "\n",
99 (canonical_query_string(Queries))/binary, "\n",
100 (canonical_headers(Headers))/binary, "\n",
101 (signed_headers(Headers))/binary, "\n",
102 "UNSIGNED-PAYLOAD"
103 >>.
104
105 %% Task 2: Create a String to Sign
106 %% https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html#request-string
107 -spec create_string_to_sign(UTCDateTime :: calendar:datetime(), Region :: binary(),
108 Service :: binary(), CanonicalRequest :: binary()) ->
109 StringToSign :: binary().
110 create_string_to_sign(UTCDateTime, Region, Service, CanonicalRequest) ->
111 21 <<
112 "AWS4-HMAC-SHA256\n",
113 (datetime_iso8601(UTCDateTime))/binary, "\n",
114 (compose_scope(UTCDateTime, Region, Service))/binary, "\n",
115 (hex(sha256_hash(CanonicalRequest)))/binary
116 >>.
117
118 %% Task 3: Calculate Signature
119 %% https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html#signing-key
120 -spec calculate_signature(UTCDateTime :: calendar:datetime(), Region :: binary(),
121 Service :: binary(), SecretAccessKey :: binary(),
122 StringToSign :: binary()) ->
123 Signature :: binary().
124 calculate_signature(UTCDateTime, Region, Service, SecretAccessKey, StringToSign) ->
125 21 DateKey = hmac_sha256(<<"AWS4", SecretAccessKey/binary>>, date_iso8601(UTCDateTime)),
126 21 DateRegionKey = hmac_sha256(DateKey, Region),
127 21 DateRegionServiceKey = hmac_sha256(DateRegionKey, Service),
128 21 SigningKey = hmac_sha256(DateRegionServiceKey, <<"aws4_request">>),
129 21 hmac_sha256(SigningKey, StringToSign).
130
131
132 -spec canonical_query_string(Queries :: #{binary() => binary()}) -> binary().
133 canonical_query_string(Queries) ->
134 21 EncodedQueries = [{uri_encode(Key), uri_encode(Val)} || {Key, Val} <- maps:to_list(Queries)],
135 21 SortedQueries = lists:keysort(1, EncodedQueries),
136 21 WithAmp = << <<Key/binary, "=", Val/binary, "&">> || {Key, Val} <- SortedQueries >>,
137 21 binary_part(WithAmp, 0, byte_size(WithAmp) - 1).
138
139
140 -spec canonical_headers(Headers :: #{binary() => binary()}) -> binary().
141 canonical_headers(Headers) ->
142 21 SortedHeaders = lists:keysort(1, maps:to_list(Headers)),
143 21 << <<Key/binary, ":", Value/binary, "\n">> || {Key, Value} <- SortedHeaders >>.
144
145
146 -spec signed_headers(Headers :: #{binary() => binary()}) -> binary().
147 signed_headers(Headers) ->
148 21 SortedHeaders = lists:sort(maps:keys(Headers)),
149 21 WithColon = << <<Key/binary, ";">> || Key <- SortedHeaders >>,
150 21 binary_part(WithColon, 0, byte_size(WithColon) - 1).
151
152 %%--------------------------------------------------------------------
153 %% Helpers
154 %%--------------------------------------------------------------------
155
156 -spec uri_encode(Data :: binary(), EncodeSlash :: boolean()) -> URIEncodedData :: binary().
157 uri_encode(Data, EncodeSlash) ->
158 504 << <<(uri_encode_char(C, EncodeSlash))/binary>> || <<C>> <= Data >>.
159
160
161 -spec uri_encode_char(C :: integer(), EncodeSlash :: boolean()) -> EncodedChar :: binary().
162 2081 uri_encode_char(C, _) when C >= $A, C =< $Z -> <<C>>;
163 5306 uri_encode_char(C, _) when C >= $a, C =< $z -> <<C>>;
164 3070 uri_encode_char(C, _) when C >= $0, C =< $9 -> <<C>>;
165 824 uri_encode_char(C, _) when C == $_; C == $-; C == $~; C == $. -> <<C>>;
166 42 uri_encode_char($/, false) -> <<$/>>;
167 272 uri_encode_char(C, _) -> list_to_binary([$% | integer_to_list(C, 16)]).
168
169
170 -spec hex(Data :: binary()) -> HexEncoded :: binary().
171 hex(Data) ->
172 42 base16:encode(Data).
173
174
175 -spec sha256_hash(Data :: binary()) -> Hash :: binary().
176 sha256_hash(Data) ->
177 21 crypto:hash(sha256, Data).
178
179
180 -spec hmac_sha256(Key :: binary(), Data :: binary()) -> Hash :: binary().
181 hmac_sha256(Key, Data) ->
182 105 crypto:mac(hmac, sha256, Key, Data).
Line Hits Source