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 |
17 |
CanonicalRequest = create_canonical_request(Method, URI, Queries, Headers), |
39 |
17 |
StringToSign = create_string_to_sign(UTCDateTime, Region, Service, CanonicalRequest), |
40 |
17 |
Signature = calculate_signature(UTCDateTime, Region, Service, SecretAccessKey, StringToSign), |
41 |
17 |
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 |
34 |
<<(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 |
34 |
{_, {H, M, S}} = UTCDateTime, |
60 |
34 |
DateComponent = date_iso8601(UTCDateTime), |
61 |
34 |
TimeComponent = list_to_binary(io_lib:format("~2..0B~2..0B~2..0B", [H, M, S])), |
62 |
34 |
<<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 |
85 |
{{Y, MM, D}, _} = UTCDateTime, |
71 |
85 |
Str = io_lib:format("~B~2..0B~2..0B", [Y, MM, D]), |
72 |
85 |
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 |
391 |
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 |
17 |
<< |
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 |
17 |
<< |
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 |
17 |
DateKey = hmac_sha256(<<"AWS4", SecretAccessKey/binary>>, date_iso8601(UTCDateTime)), |
126 |
17 |
DateRegionKey = hmac_sha256(DateKey, Region), |
127 |
17 |
DateRegionServiceKey = hmac_sha256(DateRegionKey, Service), |
128 |
17 |
SigningKey = hmac_sha256(DateRegionServiceKey, <<"aws4_request">>), |
129 |
17 |
hmac_sha256(SigningKey, StringToSign). |
130 |
|
|
131 |
|
|
132 |
|
-spec canonical_query_string(Queries :: #{binary() => binary()}) -> binary(). |
133 |
|
canonical_query_string(Queries) -> |
134 |
17 |
EncodedQueries = [{uri_encode(Key), uri_encode(Val)} || {Key, Val} <- maps:to_list(Queries)], |
135 |
17 |
SortedQueries = lists:keysort(1, EncodedQueries), |
136 |
17 |
WithAmp = << <<Key/binary, "=", Val/binary, "&">> || {Key, Val} <- SortedQueries >>, |
137 |
17 |
binary_part(WithAmp, 0, byte_size(WithAmp) - 1). |
138 |
|
|
139 |
|
|
140 |
|
-spec canonical_headers(Headers :: #{binary() => binary()}) -> binary(). |
141 |
|
canonical_headers(Headers) -> |
142 |
17 |
SortedHeaders = lists:keysort(1, maps:to_list(Headers)), |
143 |
17 |
<< <<Key/binary, ":", Value/binary, "\n">> || {Key, Value} <- SortedHeaders >>. |
144 |
|
|
145 |
|
|
146 |
|
-spec signed_headers(Headers :: #{binary() => binary()}) -> binary(). |
147 |
|
signed_headers(Headers) -> |
148 |
17 |
SortedHeaders = lists:sort(maps:keys(Headers)), |
149 |
17 |
WithColon = << <<Key/binary, ";">> || Key <- SortedHeaders >>, |
150 |
17 |
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 |
408 |
<< <<(uri_encode_char(C, EncodeSlash))/binary>> || <<C>> <= Data >>. |
159 |
|
|
160 |
|
|
161 |
|
-spec uri_encode_char(C :: integer(), EncodeSlash :: boolean()) -> EncodedChar :: binary(). |
162 |
1685 |
uri_encode_char(C, _) when C >= $A, C =< $Z -> <<C>>; |
163 |
4329 |
uri_encode_char(C, _) when C >= $a, C =< $z -> <<C>>; |
164 |
2451 |
uri_encode_char(C, _) when C >= $0, C =< $9 -> <<C>>; |
165 |
668 |
uri_encode_char(C, _) when C == $_; C == $-; C == $~; C == $. -> <<C>>; |
166 |
34 |
uri_encode_char($/, false) -> <<$/>>; |
167 |
220 |
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 |
34 |
base16:encode(Data). |
173 |
|
|
174 |
|
|
175 |
|
-spec sha256_hash(Data :: binary()) -> Hash :: binary(). |
176 |
|
sha256_hash(Data) -> |
177 |
17 |
crypto:hash(sha256, Data). |
178 |
|
|
179 |
|
|
180 |
|
-spec hmac_sha256(Key :: binary(), Data :: binary()) -> Hash :: binary(). |
181 |
|
hmac_sha256(Key, Data) -> |
182 |
85 |
crypto:mac(hmac, sha256, Key, Data). |